From 9acce4d05c98a91a7c13caa1a5bc7e66ece7de33 Mon Sep 17 00:00:00 2001 From: "Darryl L. Pierce" Date: Sun, 9 Jun 2024 09:46:08 -0400 Subject: [PATCH] Added saving and loading server entries [#5] * Added a shared view model. * Changed Server to be a shared model. * Added dependency injection using Koin. * Added the ServersDb table. * Added screens to edit servers. --- androidVariant/build.gradle.kts | 2 + androidVariant/src/main/AndroidManifest.xml | 2 + .../variant/android/MainActivity.kt | 2 +- .../variant/android/VariantApp.kt | 44 ++++++ .../variant/android/ui/Screens.kt | 6 +- .../ui/ServerListEntryDismissBackground.kt | 74 ++++++++++ .../android/ui/{ => server}/HomeScreen.kt | 21 ++- .../variant/android/ui/server/ServerDetail.kt | 102 +++++++++++++ .../variant/android/ui/server/ServerEdit.kt | 134 ++++++++++++++++++ .../variant/android/ui/server/ServerList.kt | 102 ++++++++----- .../android/ui/server/ServerListItem.kt | 62 ++++++-- .../ui/server/ServerManagementScreen.kt | 112 ++++++++++----- .../src/main/res/values/strings.xml | 12 ++ build.gradle.kts | 1 + gradle/libs.versions.toml | 24 +++- gradle/wrapper/gradle-wrapper.properties | 2 +- .../iosVariant.xcodeproj/project.pbxproj | 32 ++++- iosVariant/iosVariant/ContentView.swift | 4 +- iosVariant/iosVariant/Home/HomeView.swift | 31 ++-- .../VariantViewModelWrapper.swift} | 24 ++-- .../iosVariant/Servers/ServerDetail.swift | 38 ++++- .../iosVariant/Servers/ServerEdit.swift | 39 +++++ .../iosVariant/Servers/ServerListItem.swift | 3 +- .../Servers/ServerManagementView.swift | 72 ++++++++-- .../iosVariant/Supporting Files/Koin.swift | 49 +++++++ iosVariant/iosVariant/iOSApp.swift | 7 +- shared/build.gradle.kts | 27 +++- .../variant/shared/KoinAndroid.kt | 30 ++++ .../shared/model/BaseViewModel.android.kt | 23 +++ .../variant/shared/KoinCommon.kt | 58 ++++++++ .../variant/shared/data/DatabaseHelper.kt | 42 ++++++ .../variant/shared/data/ServerRepository.kt | 53 +++++++ .../variant/shared/model/BaseViewModel.kt | 21 +++ .../variant/shared/model/VariantViewModel.kt | 42 ++++++ .../variant/shared}/model/server/Server.kt | 14 +- .../org/comixedproject/variant/db/Table.sq | 19 +++ .../variant/shared/KoinIntegrationTest.kt | 44 ++++++ .../shared/model/VariantViewModelTest.kt | 62 ++++++++ .../comixedproject/variant/shared/KoinIOS.kt | 58 ++++++++ .../variant/shared/model/BaseViewModel.ios.kt | 21 +++ 40 files changed, 1355 insertions(+), 160 deletions(-) create mode 100644 androidVariant/src/main/java/org/comixedproject/variant/android/VariantApp.kt create mode 100644 androidVariant/src/main/java/org/comixedproject/variant/android/ui/ServerListEntryDismissBackground.kt rename androidVariant/src/main/java/org/comixedproject/variant/android/ui/{ => server}/HomeScreen.kt (80%) create mode 100644 androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerDetail.kt create mode 100644 androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerEdit.kt rename iosVariant/iosVariant/{Servers/Server.swift => Model/VariantViewModelWrapper.swift} (50%) create mode 100644 iosVariant/iosVariant/Servers/ServerEdit.swift create mode 100644 iosVariant/iosVariant/Supporting Files/Koin.swift create mode 100644 shared/src/androidMain/kotlin/org/comixedproject/variant/shared/KoinAndroid.kt create mode 100644 shared/src/androidMain/kotlin/org/comixedproject/variant/shared/model/BaseViewModel.android.kt create mode 100644 shared/src/commonMain/kotlin/org/comixedproject/variant/shared/KoinCommon.kt create mode 100644 shared/src/commonMain/kotlin/org/comixedproject/variant/shared/data/DatabaseHelper.kt create mode 100644 shared/src/commonMain/kotlin/org/comixedproject/variant/shared/data/ServerRepository.kt create mode 100644 shared/src/commonMain/kotlin/org/comixedproject/variant/shared/model/BaseViewModel.kt create mode 100644 shared/src/commonMain/kotlin/org/comixedproject/variant/shared/model/VariantViewModel.kt rename {androidVariant/src/main/java/org/comixedproject/variant/android => shared/src/commonMain/kotlin/org/comixedproject/variant/shared}/model/server/Server.kt (77%) create mode 100644 shared/src/commonMain/sqldelight/org/comixedproject/variant/db/Table.sq create mode 100644 shared/src/commonTest/kotlin/org/comixproject/variant/shared/KoinIntegrationTest.kt create mode 100644 shared/src/commonTest/kotlin/org/comixproject/variant/shared/model/VariantViewModelTest.kt create mode 100644 shared/src/iosMain/kotlin/org/comixedproject/variant/shared/KoinIOS.kt create mode 100644 shared/src/iosMain/kotlin/org/comixedproject/variant/shared/model/BaseViewModel.ios.kt diff --git a/androidVariant/build.gradle.kts b/androidVariant/build.gradle.kts index e25138a..33e0207 100644 --- a/androidVariant/build.gradle.kts +++ b/androidVariant/build.gradle.kts @@ -48,6 +48,8 @@ dependencies { implementation(composeBom) implementation(libs.bundles.androidx) implementation(libs.bundles.compose) + implementation(libs.koin.android) + implementation(libs.koin.androidx.compose) testImplementation(libs.bundles.unit.tests) androidTestImplementation(composeBom) diff --git a/androidVariant/src/main/AndroidManifest.xml b/androidVariant/src/main/AndroidManifest.xml index b874d28..8055cbb 100644 --- a/androidVariant/src/main/AndroidManifest.xml +++ b/androidVariant/src/main/AndroidManifest.xml @@ -2,9 +2,11 @@ + */ + +package org.comixedproject.variant.android + +import android.app.Application +import android.content.Context +import org.comixedproject.variant.shared.initKoin +import org.comixedproject.variant.shared.model.VariantViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + + +class VariantApp : Application() { + override fun onCreate() { + super.onCreate() + + initKoin( + appModule = module { + single { this@VariantApp } + + }, + viewModelsModule = module { + viewModel { + VariantViewModel(get()) + } + }) + } +} \ No newline at end of file diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/Screens.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/Screens.kt index 8c7e56e..c4ceaff 100644 --- a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/Screens.kt +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/Screens.kt @@ -19,15 +19,15 @@ package org.comixedproject.variant.android.ui import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountBox import androidx.compose.material.icons.filled.List -import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Settings import androidx.compose.ui.graphics.vector.ImageVector import org.comixedproject.variant.android.R enum class Screens(val label: Int, val icon: ImageVector) { - ServerManagement(R.string.serverButtonLabel, Icons.Filled.List), - ComicManagement(R.string.comicsButtonLabel, Icons.Filled.PlayArrow), + ComicManagement(R.string.comicsButtonLabel, Icons.Filled.List), + ServerManagement(R.string.serverButtonLabel, Icons.Filled.AccountBox), Settings(R.string.settingsButtonLabel, Icons.Filled.Settings); companion object { diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/ServerListEntryDismissBackground.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/ServerListEntryDismissBackground.kt new file mode 100644 index 0000000..8e01437 --- /dev/null +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/ServerListEntryDismissBackground.kt @@ -0,0 +1,74 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.android.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SwipeToDismissBoxState +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.comixedproject.variant.android.R +import org.comixedproject.variant.android.VariantTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ServerListEntryDismissBackground(dismissState: SwipeToDismissBoxState) { + val color = when (dismissState.dismissDirection) { + SwipeToDismissBoxValue.StartToEnd -> Color(0xFFFF1744) + SwipeToDismissBoxValue.EndToStart -> Color(0xFF1DE986) + SwipeToDismissBoxValue.Settled -> Color.Transparent + } + + Row( + modifier = Modifier + .fillMaxSize() + .background(color) + .padding(12.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Icon(Icons.Default.Delete, contentDescription = stringResource(id = R.string.deleteServer)) + Spacer(modifier = Modifier) + Icon(Icons.Default.Edit, contentDescription = stringResource(id = R.string.editServer)) + } +} + +@Preview +@Composable +fun ServerListEntryDismissBackgroundPreview() { + VariantTheme { + ServerListEntryDismissBackground(rememberSwipeToDismissBoxState()) + } +} \ No newline at end of file diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/HomeScreen.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/HomeScreen.kt similarity index 80% rename from androidVariant/src/main/java/org/comixedproject/variant/android/ui/HomeScreen.kt rename to androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/HomeScreen.kt index e71e84b..5d9d222 100644 --- a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/HomeScreen.kt +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/HomeScreen.kt @@ -16,7 +16,7 @@ * along with this program. If not, see */ -package org.comixedproject.variant.android.ui +package org.comixedproject.variant.android.ui.server import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -35,11 +35,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import org.comixedproject.variant.android.VariantTheme -import org.comixedproject.variant.android.ui.server.ServerManagementScreen +import org.comixedproject.variant.android.ui.Screens +import org.comixedproject.variant.shared.model.VariantViewModel +import org.koin.androidx.compose.getViewModel @Composable -fun HomeScreen() { - val selectedItem = remember { mutableStateOf(Screens.ServerManagement) } +fun HomeScreen(viewModel: VariantViewModel = getViewModel()) { + val selectedItem = remember { mutableStateOf(Screens.ComicManagement) } val navHost = rememberNavController() val navBackStackEntry by navHost.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination @@ -65,7 +67,16 @@ fun HomeScreen() { Box(modifier = Modifier.fillMaxSize()) { Surface(modifier = Modifier.align(Alignment.Center)) { when (selectedItem.value) { - Screens.ServerManagement -> ServerManagementScreen() + Screens.ServerManagement -> ServerManagementScreen( + viewModel.servers, + onSaveServer = { server -> + viewModel.saveServer(server) + }, + onBrowserServer = {}, + onDeleteServer = { server -> + viewModel.deleteServer(server) + }) + Screens.ComicManagement -> Text("Comic Book Management") Screens.Settings -> Text("Settings") } diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerDetail.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerDetail.kt new file mode 100644 index 0000000..d26f5c7 --- /dev/null +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerDetail.kt @@ -0,0 +1,102 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.android.ui.server + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.comixedproject.variant.android.R +import org.comixedproject.variant.android.VariantTheme +import org.comixedproject.variant.shared.model.server.Server + +@Composable +fun ServerDetail( + server: Server, + onEdit: () -> Unit, + onDelete: () -> Unit, + onBrowse: () -> Unit +) { + Scaffold(bottomBar = { + BottomAppBar( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.primary + ) { + Button(onClick = onEdit) { + Icon( + imageVector = Icons.Filled.Edit, + contentDescription = stringResource(id = R.string.editButtonLabel) + ) + } + Button(onClick = onBrowse) { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = stringResource(id = R.string.browseButtonLabel) + ) + } + Button(onClick = onDelete) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = stringResource(id = R.string.deleteButtonLabel) + ) + } + } + }) { padding -> + Box(modifier = Modifier.padding(padding)) { + Column(modifier = Modifier.padding(32.dp)) { + Text(text = server.name, style = MaterialTheme.typography.titleLarge) + Text(text = server.url, style = MaterialTheme.typography.bodyMedium) + Text(text = server.username, style = MaterialTheme.typography.bodyMedium) + Text( + text = server.password.replace(".".toRegex(), "*"), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } +} + +@Preview +@Composable +fun ServerDetailPreview() { + VariantTheme { + ServerDetail( + server = Server( + "1", + "My Server", + "http://www.comixedproject.org:7171/opds", + "reader@comixedproject.org", + "my!password" + ), onEdit = {}, onDelete = {}, onBrowse = {}) + } +} \ No newline at end of file diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerEdit.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerEdit.kt new file mode 100644 index 0000000..c521b05 --- /dev/null +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerEdit.kt @@ -0,0 +1,134 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.android.ui.server + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.comixedproject.variant.android.R +import org.comixedproject.variant.android.VariantTheme +import org.comixedproject.variant.shared.model.server.Server + + +@Composable +fun ServerEdit(server: Server, onSave: (Server) -> Unit, onCancel: () -> Unit) { + val id by remember { mutableStateOf(server.id) } + var name by remember { mutableStateOf(server.name) } + var url by remember { mutableStateOf(server.url) } + var username by remember { mutableStateOf(server.username) } + var password by remember { mutableStateOf(server.password) } + + Scaffold(bottomBar = { + BottomAppBar( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.primary + ) { + Button(onClick = { onSave(Server(id, name, url, username, password)) }) { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = stringResource(id = R.string.saveButtonLabel) + ) + } + Button(onClick = onCancel) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(id = R.string.cancelButtonLabel) + ) + } + } + }) { padding -> + Column(modifier = Modifier.padding(32.dp)) { + TextField( + value = name, + placeholder = { Text(text = stringResource(id = R.string.serverNamePlaceholder)) }, + onValueChange = { name = it }, + modifier = Modifier.fillMaxWidth() + ) + TextField( + value = url, + placeholder = { Text(text = stringResource(id = R.string.serverUrlPlaceholder)) }, + onValueChange = { url = it }, + modifier = Modifier.fillMaxWidth() + ) + TextField( + value = username, + placeholder = { Text(text = stringResource(id = R.string.useramePlaceholder)) }, + onValueChange = { username = it }, + modifier = Modifier.fillMaxWidth() + ) + TextField( + value = password, + placeholder = { Text(text = stringResource(id = R.string.passwordPlaceholder)) }, + onValueChange = { password = it }, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +@Preview +@Composable +fun ServerEditPreviewCreate() { + VariantTheme { + ServerEdit( + server = Server( + null, + "", + "", + "", + "" + ), onSave = {}, onCancel = {} + ) + } +} + +@Preview +@Composable +fun ServerEditPreviewEdit() { + VariantTheme { + ServerEdit( + server = Server( + "1", + "My Server", + "http://www.comixedproject.org:7171/opds", + "reader@comixedproject.org", + "my!password" + ), onSave = {}, onCancel = {} + ) + } +} \ No newline at end of file diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerList.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerList.kt index 88df30a..4de7bd3 100644 --- a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerList.kt +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerList.kt @@ -18,18 +18,47 @@ package org.comixedproject.variant.android.ui.server +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import org.comixedproject.variant.android.R import org.comixedproject.variant.android.VariantTheme -import org.comixedproject.variant.android.model.server.Server +import org.comixedproject.variant.shared.model.server.Server @Composable -fun ServerList(servers: List, onServerSelect: (Server) -> Unit) { - LazyColumn { - items(servers) { server -> - ServerListItem(server, onClick = onServerSelect) +fun ServerList( + servers: List, + onServerCreate: () -> Unit, + onServerSelect: (Server) -> Unit, + onServerEdit: (Server) -> Unit, + onServerDelete: (Server) -> Unit +) { + Scaffold(topBar = {}, bottomBar = {}, floatingActionButton = { + Button(onClick = onServerCreate) { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = stringResource(id = R.string.serverAddButton) + ) + } + }) { padding -> + LazyColumn(modifier = Modifier.padding(padding)) { + items(servers) { server -> + ServerListItem( + server, + onClick = onServerSelect, + onEdit = onServerEdit, + onDelete = onServerDelete + ) + } } } } @@ -38,35 +67,38 @@ fun ServerList(servers: List, onServerSelect: (Server) -> Unit) { @Composable fun ServerListPreview() { VariantTheme { - ServerList( - listOf( - Server( - id = 1, - name = "My Server", - url = "http://www.comixedproject.org:7171/opds", - username = "reader@comixedproject.org" - ), Server( - id = 2, - name = "My Server", - url = "http://www.comixedproject.org:7171/opds", - username = "reader@comixedproject.org" - ), Server( - id = 3, - name = "My Server", - url = "http://www.comixedproject.org:7171/opds", - username = "reader@comixedproject.org" - ), Server( - id = 4, - name = "My Server", - url = "http://www.comixedproject.org:7171/opds", - username = "reader@comixedproject.org" - ), Server( - id = 5, - name = "My Server", - url = "http://www.comixedproject.org:7171/opds", - username = "reader@comixedproject.org" - ) - ), - onServerSelect = {}) + ServerList(mutableListOf( + Server( + "1", + "Server 1", + "http://www.comixedproject.org:7171/opds", + "reader@comixedprojecvt.org", + "password" + ), Server( + "2", + "Server 2", + "http://www.comixedproject.org:7171/opds", + "reader@comixedprojecvt.org", + "password" + ), Server( + "3", + "Server 3", + "http://www.comixedproject.org:7171/opds", + "reader@comixedprojecvt.org", + "password" + ), Server( + "4", + "Server 4", + "http://www.comixedproject.org:7171/opds", + "reader@comixedprojecvt.org", + "password" + ), Server( + "5", + "Server 5", + "http://www.comixedproject.org:7171/opds", + "reader@comixedprojecvt.org", + "password" + ) + ), onServerCreate = {}, onServerSelect = {}, onServerEdit = {}, onServerDelete = {}) } } \ No newline at end of file diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerListItem.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerListItem.kt index 61fd1cb..5939513 100644 --- a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerListItem.kt +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerListItem.kt @@ -23,24 +23,53 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info import androidx.compose.material3.Icon import androidx.compose.material3.ListItem +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text +import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import org.comixedproject.variant.android.VariantTheme -import org.comixedproject.variant.android.model.server.Server +import org.comixedproject.variant.android.ui.ServerListEntryDismissBackground +import org.comixedproject.variant.shared.model.server.Server @Composable -fun ServerListItem(server: Server, onClick: (Server) -> Unit) { - ListItem( - leadingContent = { Icon(Icons.Filled.Info, contentDescription = server.name) }, - overlineContent = { Text(server.url) }, - headlineContent = { Text(server.name) }, - supportingContent = { Text(server.username) }, - modifier = Modifier.clickable { - onClick(server) - } +fun ServerListItem( + server: Server, + onClick: (Server) -> Unit, + onEdit: (Server) -> Unit, + onDelete: (Server) -> Unit +) { + val context = LocalContext.current + val dismissState = rememberSwipeToDismissBoxState( + confirmValueChange = { + when (it) { + SwipeToDismissBoxValue.StartToEnd -> onDelete(server) + SwipeToDismissBoxValue.EndToStart -> onEdit(server) + else -> return@rememberSwipeToDismissBoxState false + } + + return@rememberSwipeToDismissBoxState true + }, + positionalThreshold = { it * .25f } ) + + SwipeToDismissBox(state = dismissState, + modifier = Modifier, + backgroundContent = { ServerListEntryDismissBackground(dismissState) }, + content = { + ListItem( + leadingContent = { Icon(Icons.Filled.Info, contentDescription = server.name) }, + overlineContent = { Text(server.url) }, + headlineContent = { Text(server.name) }, + supportingContent = { Text(server.username) }, + modifier = Modifier.clickable { + onClick(server) + } + ) + }) } @Preview @@ -49,12 +78,15 @@ fun ServerListItemPreview() { VariantTheme { ServerListItem( server = Server( - id = 1, - name = "My Server", - url = "http://www.comixedproject.org:7171/opds", - username = "reader@comixedproject.org" + "1", + "Server 1", + "http://www.comixedproject.org:7171/opds", + "reader@comixedprojecvt.org", + "password" ), - onClick = {} + onClick = {}, + onEdit = {}, + onDelete = {} ) } } \ No newline at end of file diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerManagementScreen.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerManagementScreen.kt index dfe1aa8..1c3c011 100644 --- a/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerManagementScreen.kt +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/ui/server/ServerManagementScreen.kt @@ -19,7 +19,6 @@ package org.comixedproject.variant.android.ui.server import androidx.activity.compose.BackHandler -import androidx.compose.material3.Text import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole @@ -27,12 +26,17 @@ import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaf import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import org.comixedproject.variant.android.VariantTheme -import org.comixedproject.variant.android.model.server.Server +import org.comixedproject.variant.shared.model.server.Server @OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable -fun ServerManagementScreen() { +fun ServerManagementScreen( + servers: List, + onSaveServer: (Server) -> Unit, + onBrowserServer: (Server) -> Unit, + onDeleteServer: (Server) -> Unit +) { val navigator = rememberListDetailPaneScaffoldNavigator() BackHandler { @@ -43,45 +47,46 @@ fun ServerManagementScreen() { directive = navigator.scaffoldDirective, value = navigator.scaffoldValue, listPane = { - ServerList(listOf( - Server( - id = 1, - name = "My Server", - url = "http://www.comixedproject.org:7171/opds", - username = "reader@comixedproject.org" - ), Server( - id = 2, - name = "My Server", - url = "http://www.comixedproject.org:7171/opds", - username = "reader@comixedproject.org" - ), Server( - id = 3, - name = "My Server", - url = "http://www.comixedproject.org:7171/opds", - username = "reader@comixedproject.org" - ), Server( - id = 4, - name = "My Server", - url = "http://www.comixedproject.org:7171/opds", - username = "reader@comixedproject.org" - ), Server( - id = 5, - name = "My Server", - url = "http://www.comixedproject.org:7171/opds", - username = "reader@comixedproject.org" - ) - ), + ServerList( + servers, + onServerCreate = { + navigator.navigateTo( + ListDetailPaneScaffoldRole.Extra, + Server(null, "", "", "", "") + ) + }, onServerSelect = { server -> + navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, server) + }, + onServerEdit = { server -> navigator.navigateTo( - ListDetailPaneScaffoldRole.Detail, + ListDetailPaneScaffoldRole.Extra, server ) - } + }, + onServerDelete = onDeleteServer ) }, detailPane = { navigator.currentDestination?.content?.let { server -> - Text(server.name) + ServerDetail(server = server, onEdit = { + navigator.navigateTo(ListDetailPaneScaffoldRole.Extra, server) + }, onBrowse = { + onBrowserServer(server) + }, onDelete = { + onDeleteServer(server) + }) + } + }, + extraPane = { + navigator.currentDestination?.content?.let { server -> + ServerEdit( + server = server, + onSave = { server -> + onSaveServer(server) + navigator.navigateTo(ListDetailPaneScaffoldRole.List) + }, + onCancel = { navigator.navigateBack() }) } }) } @@ -90,6 +95,43 @@ fun ServerManagementScreen() { @Composable fun ServerManagementScreenPreview() { VariantTheme { - ServerManagementScreen() + ServerManagementScreen( + listOf( + Server( + "1", + "Server 1", + "http://www.comixedproject.org:7171/opds", + "reader@comixedprojecvt.org", + "password" + ), Server( + "2", + "Server 2", + "http://www.comixedproject.org:7171/opds", + "reader@comixedprojecvt.org", + "password" + ), Server( + "3", + "Server 3", + "http://www.comixedproject.org:7171/opds", + "reader@comixedprojecvt.org", + "password" + ), Server( + "4", + "Server 4", + "http://www.comixedproject.org:7171/opds", + "reader@comixedprojecvt.org", + "password" + ), Server( + "5", + "Server 5", + "http://www.comixedproject.org:7171/opds", + "reader@comixedprojecvt.org", + "password" + ) + ), + onSaveServer = {}, + onBrowserServer = {}, + onDeleteServer = {} + ) } } \ No newline at end of file diff --git a/androidVariant/src/main/res/values/strings.xml b/androidVariant/src/main/res/values/strings.xml index 7b8f9df..ada7354 100644 --- a/androidVariant/src/main/res/values/strings.xml +++ b/androidVariant/src/main/res/values/strings.xml @@ -4,4 +4,16 @@ Servers Variant v0.1-SNAPSHOT Settings + Add a new server + User friendly name for the server + The server\'s web address + Your username + Your password + Save changes + Cancel action + Delete this entry + Browser server contents + Edit this entry + Edit this server + Delete this server \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 0c3175c..362c121 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { alias(libs.plugins.android.library).apply(false) alias(libs.plugins.kotlin.android).apply(false) alias(libs.plugins.kotlin.multiplatform).apply(false) + alias(libs.plugins.sqldelight).apply(false) alias(libs.plugins.sonarqube) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 59d89cb..d7f45fb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,24 +1,27 @@ [versions] -android-gradle-plugin = "8.2.2" +android-gradle-plugin = "8.4.1" sonarqube-plugin = "4.4.1.3373" -kotlin = "1.9.22" +kotlin = "1.9.24" activity-compose = "1.8.0" -activity-compose-compiler = "1.5.9" +activity-compose-compiler = "1.5.14" androidx-compose-bom = "2023.10.01" core-ktx = "1.12.0" -lifecycle-runtime-ktx = "2.6.2" +lifecycle-runtime-ktx = "2.8.0" androidx-junit = "1.1.5" espresso-core = "3.5.1" junit = "4.13.2" adaptive-layout-android = "1.0.0-beta02" adaptive-navigation-android = "1.3.0-beta02" navigation-compose = "2.7.7" +koin = "3.5.6" +sqldelight-plugin = "2.0.2" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" } +lifecycle-viewmodel-android = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-android", version.ref = "lifecycle-runtime-ktx" } androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "adaptive-layout-android" } @@ -37,15 +40,26 @@ androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", vers androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" } junit = { module = "junit:junit", version.ref = "junit" } +# koin +koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } +koin-test = { group = "io.insert-koin", name = "koin-test", version.ref = "koin" } +koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } +koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } + +# SQLDelight +sqldelight-driver-android = { group = "app.cash.sqldelight", name = "android-driver", version.ref = "sqldelight-plugin" } +sqldelight-driver-native = { group = "app.cash.sqldelight", name = "native-driver", version.ref = "sqldelight-plugin" } + [plugins] android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } sonarqube = { id = "org.sonarqube", version.ref = "sonarqube-plugin" } +sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight-plugin" } [bundles] -androidx = ["androidx-activity-compose", "androidx-core-ktx", "androidx-lifecycle-runtime-ktx"] +androidx = ["androidx-activity-compose", "androidx-core-ktx", "androidx-lifecycle-runtime-ktx", "koin-android", "koin-androidx-compose"] compose = ["androidx-compose-material3", "androidx-compose-ui-ui-graphics", "androidx-compose-ui-ui", "androidx-compose-ui-ui-tooling-preview", "androidx-adaptive", "androidx-adaptive-layout", "androidx-adaptive-navigation", "androidx-adaptive-navigation-suite-android", "androidx-navigation-compose"] compose-debug = ["androidx-compose-ui-ui-tooling", "androidx-compose-ui-ui-test-manifest"] instrumented-tests = ["androidx-junit", "androidx-espresso-core", "androidx-compose-ui-ui-test-junit4"] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 61e3ab7..b7f6793 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Feb 17 16:54:35 EST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/iosVariant/iosVariant.xcodeproj/project.pbxproj b/iosVariant/iosVariant.xcodeproj/project.pbxproj index d2c2c96..60f7fb3 100644 --- a/iosVariant/iosVariant.xcodeproj/project.pbxproj +++ b/iosVariant/iosVariant.xcodeproj/project.pbxproj @@ -12,10 +12,12 @@ 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; B67A1D102C14D67F0099E845 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67A1D0F2C14D67F0099E845 /* HomeView.swift */; }; - B69A97332C153A1B003D094D /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69A97322C153A1B003D094D /* Server.swift */; }; + B68D9C022C1CD7930017D3D6 /* VariantViewModelWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68D9C012C1CD7930017D3D6 /* VariantViewModelWrapper.swift */; }; + B68D9C052C1CE54D0017D3D6 /* Koin.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68D9C042C1CE54D0017D3D6 /* Koin.swift */; }; B69A97352C153A62003D094D /* ServerListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69A97342C153A62003D094D /* ServerListItem.swift */; }; B69A97372C153CCC003D094D /* ServerManagementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69A97362C153CCC003D094D /* ServerManagementView.swift */; }; B69A97392C154273003D094D /* ServerDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69A97382C154273003D094D /* ServerDetail.swift */; }; + B6B28AF42C1DCF5A00171449 /* ServerEdit.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B28AF32C1DCF5A00171449 /* ServerEdit.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -39,10 +41,12 @@ 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B67A1D0F2C14D67F0099E845 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; - B69A97322C153A1B003D094D /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = ""; }; + B68D9C012C1CD7930017D3D6 /* VariantViewModelWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariantViewModelWrapper.swift; sourceTree = ""; }; + B68D9C042C1CE54D0017D3D6 /* Koin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Koin.swift; sourceTree = ""; }; B69A97342C153A62003D094D /* ServerListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListItem.swift; sourceTree = ""; }; B69A97362C153CCC003D094D /* ServerManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManagementView.swift; sourceTree = ""; }; B69A97382C154273003D094D /* ServerDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetail.swift; sourceTree = ""; }; + B6B28AF32C1DCF5A00171449 /* ServerEdit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerEdit.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -84,6 +88,8 @@ 7555FF7D242A565900829871 /* iosVariant */ = { isa = PBXGroup; children = ( + B68D9C032C1CE53E0017D3D6 /* Supporting Files */, + B68D9C002C1CD76C0017D3D6 /* Model */, B69A97312C153A07003D094D /* Servers */, B67A1D0C2C14D51C0099E845 /* Home */, 058557BA273AAA24004C7B11 /* Assets.xcassets */, @@ -110,13 +116,29 @@ path = Home; sourceTree = ""; }; + B68D9C002C1CD76C0017D3D6 /* Model */ = { + isa = PBXGroup; + children = ( + B68D9C012C1CD7930017D3D6 /* VariantViewModelWrapper.swift */, + ); + path = Model; + sourceTree = ""; + }; + B68D9C032C1CE53E0017D3D6 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + B68D9C042C1CE54D0017D3D6 /* Koin.swift */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; B69A97312C153A07003D094D /* Servers */ = { isa = PBXGroup; children = ( - B69A97322C153A1B003D094D /* Server.swift */, B69A97342C153A62003D094D /* ServerListItem.swift */, B69A97362C153CCC003D094D /* ServerManagementView.swift */, B69A97382C154273003D094D /* ServerDetail.swift */, + B6B28AF32C1DCF5A00171449 /* ServerEdit.swift */, ); path = Servers; sourceTree = ""; @@ -213,12 +235,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B6B28AF42C1DCF5A00171449 /* ServerEdit.swift in Sources */, B69A97372C153CCC003D094D /* ServerManagementView.swift in Sources */, 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, + B68D9C022C1CD7930017D3D6 /* VariantViewModelWrapper.swift in Sources */, B69A97352C153A62003D094D /* ServerListItem.swift in Sources */, B67A1D102C14D67F0099E845 /* HomeView.swift in Sources */, - B69A97332C153A1B003D094D /* Server.swift in Sources */, 7555FF83242A565900829871 /* ContentView.swift in Sources */, + B68D9C052C1CE54D0017D3D6 /* Koin.swift in Sources */, B69A97392C154273003D094D /* ServerDetail.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/iosVariant/iosVariant/ContentView.swift b/iosVariant/iosVariant/ContentView.swift index ba93231..3049717 100644 --- a/iosVariant/iosVariant/ContentView.swift +++ b/iosVariant/iosVariant/ContentView.swift @@ -18,14 +18,16 @@ import SwiftUI +@available(iOS 17.0, *) struct ContentView: View { var body: some View { HomeView() } } +@available(iOS 17.0, *) struct ContentView_Previews: PreviewProvider { - static var previews: some View { + static var previews: some View { ContentView() } } diff --git a/iosVariant/iosVariant/Home/HomeView.swift b/iosVariant/iosVariant/Home/HomeView.swift index 7bc76a5..eba44cd 100644 --- a/iosVariant/iosVariant/Home/HomeView.swift +++ b/iosVariant/iosVariant/Home/HomeView.swift @@ -17,26 +17,33 @@ */ import SwiftUI +import Variant +@available(iOS 17.0, *) struct HomeView: View { + @State private var viewModelWrapper = VariantViewModelWrapper() + var body: some View { - NavigationView { - VStack { - NavigationLink(destination: ServerManagementView(servers: servers)) { - Text("Servers") - } - NavigationLink(destination: Text("Comics")) { - Text("Comics!") - } - NavigationLink(destination: Text("Settings")) { - Text("Settings!") - } + NavigationStack { + NavigationLink(destination: ServerManagementView( + servers: viewModelWrapper.servers, + onSaveServer: { server in viewModelWrapper.viewModel.saveServer(server: server) }, + onBrowseServer: { _ in }, + onDeleteServer: { server in viewModelWrapper.viewModel.deleteServer(server: server) } + )) { + Text("Servers") + } + NavigationLink(destination: Text("Downloaded comic books")) { + Text("Comics") + } + NavigationLink(destination: Text("Configuration screen")) { + Text("Settings") } } - .navigationTitle("Navigation") } } +@available(iOS 17.0, *) #Preview { HomeView() } diff --git a/iosVariant/iosVariant/Servers/Server.swift b/iosVariant/iosVariant/Model/VariantViewModelWrapper.swift similarity index 50% rename from iosVariant/iosVariant/Servers/Server.swift rename to iosVariant/iosVariant/Model/VariantViewModelWrapper.swift index 03e9e72..205aca7 100644 --- a/iosVariant/iosVariant/Servers/Server.swift +++ b/iosVariant/iosVariant/Model/VariantViewModelWrapper.swift @@ -17,18 +17,16 @@ */ import Foundation +import Variant -struct Server: Identifiable, Hashable, Codable { - let id: Int - var name: String - var url: String - var username: String +@available(iOS 17.0, *) +@Observable +final class VariantViewModelWrapper { + let viewModel: VariantViewModel = Koin.instance.get() + + private(set) var servers: [Server] = [] + + init() { + viewModel.onServerUpdate = { [weak self] servers in self?.servers = servers } + } } - -var servers = [ - Server(id: 1, name: "CX Server 1", url: "http://www.comixedproject.org:7171/opds", username: "reader@comixedproject.org"), - Server(id: 2, name: "CX Server 2", url: "http://www.comixedproject.org:7171/opds", username: "reader@comixedproject.org"), - Server(id: 3, name: "CX Server 3", url: "http://www.comixedproject.org:7171/opds", username: "reader@comixedproject.org"), - Server(id: 4, name: "CX Server 4", url: "http://www.comixedproject.org:7171/opds", username: "reader@comixedproject.org"), - Server(id: 5, name: "CX Server 5", url: "http://www.comixedproject.org:7171/opds", username: "reader@comixedproject.org") -] diff --git a/iosVariant/iosVariant/Servers/ServerDetail.swift b/iosVariant/iosVariant/Servers/ServerDetail.swift index a78ee1c..fa03625 100644 --- a/iosVariant/iosVariant/Servers/ServerDetail.swift +++ b/iosVariant/iosVariant/Servers/ServerDetail.swift @@ -17,17 +17,45 @@ */ import SwiftUI +import Variant struct ServerDetail: View { var server: Server - + var onEdit: () -> () + var onBrowse: () -> () + var onDelete: () -> () + var body: some View { - Text(server.name) - Text(server.url) - Text(server.username) + VStack { + Text(server.name).font(.title).bold() + Text(server.url).font(.subheadline).foregroundStyle(Color.secondary) + Text(server.username) + Spacer() + } + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Edit") { + onEdit() + } + } + ToolbarItem(placement: .topBarTrailing) { + Button("Browser") { + onBrowse() + } + } + ToolbarItem(placement: .topBarTrailing) { + Button("Delete") { + onDelete() + } + } + } } } #Preview { - ServerDetail(server: servers[0]) + ServerDetail( + server: Server(id: "1", name: "Server 1", url: "http://www.comixedproject.org:7171/opds", username: "reader@comixedproject.org", password: "my!password"), + onEdit: {}, + onBrowse: {}, + onDelete: {}) } diff --git a/iosVariant/iosVariant/Servers/ServerEdit.swift b/iosVariant/iosVariant/Servers/ServerEdit.swift new file mode 100644 index 0000000..d1d260f --- /dev/null +++ b/iosVariant/iosVariant/Servers/ServerEdit.swift @@ -0,0 +1,39 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import SwiftUI +import Variant + +struct ServerEdit: View { + @State var server: Server + + var body: some View { + Text(server.name) + } +} + +#Preview { + ServerEdit( + server: Server( + id: "1", + name: "Server 1", + url: "http://www.comixedproject.org:7171/opds", + username: "reader@comixedproject.org", + password: "my!password" + )) +} diff --git a/iosVariant/iosVariant/Servers/ServerListItem.swift b/iosVariant/iosVariant/Servers/ServerListItem.swift index fa5f9c9..1adb13a 100644 --- a/iosVariant/iosVariant/Servers/ServerListItem.swift +++ b/iosVariant/iosVariant/Servers/ServerListItem.swift @@ -17,6 +17,7 @@ */ import SwiftUI +import Variant struct ServerListItem: View { var server: Server @@ -29,5 +30,5 @@ struct ServerListItem: View { } #Preview { - ServerListItem(server: servers[0]) + ServerListItem(server: Server(id: "1", name: "Server 1", url: "http://www.comixedproject.org:7171/opds", username: "reader@comixedproject.org", password: "my!password")) } diff --git a/iosVariant/iosVariant/Servers/ServerManagementView.swift b/iosVariant/iosVariant/Servers/ServerManagementView.swift index d9e4181..c67101f 100644 --- a/iosVariant/iosVariant/Servers/ServerManagementView.swift +++ b/iosVariant/iosVariant/Servers/ServerManagementView.swift @@ -17,28 +17,74 @@ */ import SwiftUI +import Variant - +@available(iOS 17.0, *) struct ServerManagementView: View { - var servers: [Server] = [] - + @State var servers: [Server] @State private var selectedServer: Server? = nil - + + var onSaveServer: (Server) -> () + var onBrowseServer: (Server) -> () + var onDeleteServer: (Server) -> () + var body: some View { - NavigationSplitView { - List(servers, selection: $selectedServer) { server in - ServerListItem(server: server) - } - } detail: { + ZStack { if let server = $selectedServer.wrappedValue { - ServerDetail(server: server) - } else { - Text("No Server Selected") + Text(server.name) + } + VStack { + NavigationSplitView { + List(servers, id: \.id, selection: $selectedServer) { server in + NavigationLink(value: server) { + HStack { + Image(systemName: "server.rack") + Text(server.name) + Spacer() + } + } + .navigationTitle("Servers") + } + } detail: { + if let server = $selectedServer.wrappedValue { + ServerDetail( + server: server, + onEdit: { selectedServer = server }, + onBrowse: { onBrowseServer(server) }, + onDelete: { + onDeleteServer(server) + }) + } + } + Spacer() + HStack { + Spacer() + Button(action: {}, label: { + Text("+") + .font(.system(.largeTitle)) + .frame(width: 77, height: 70) + .foregroundColor(Color.white) + .padding(.bottom, 7) + }) + .background(Color.blue) + .cornerRadius(38.5) + .padding() + .shadow(color: Color.black.opacity(0.3), + radius: 3, + x: 3, + y: 3) + } } } } } +@available(iOS 17.0, *) #Preview { - ServerManagementView(servers: servers) + ServerManagementView(servers: [ + Server(id: "1", name: "My Server", url: "http://www.comixedproject.org:7171/opds", username: "reader@comixedproject.org", password: "my!password"), + ], + onSaveServer: { _ in }, + onBrowseServer: { _ in }, + onDeleteServer: { _ in }) } diff --git a/iosVariant/iosVariant/Supporting Files/Koin.swift b/iosVariant/iosVariant/Supporting Files/Koin.swift new file mode 100644 index 0000000..dbb49ac --- /dev/null +++ b/iosVariant/iosVariant/Supporting Files/Koin.swift @@ -0,0 +1,49 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import Variant + +final class Koin { + private var core: Koin_coreKoin? + + static let instance = Koin() + + static func start() { + if instance.core == nil { + let app = KoinIOS.shared.initialize( + userDefaults: UserDefaults.standard + ) + instance.core = app.koin + } + if instance.core == nil { + fatalError("Failed to initialize Koin.") + } + } + + private init() {} + + func get() -> T { + guard let core else { + fatalError("You should call `start()` before using \(#function)") + } + + guard let result = core.get(objCClass: T.self) as? T else { fatalError("Koin can't provide an instance of type: \(T.self)") } + + return result + } +} diff --git a/iosVariant/iosVariant/iOSApp.swift b/iosVariant/iosVariant/iOSApp.swift index 7f81697..0db0098 100644 --- a/iosVariant/iosVariant/iOSApp.swift +++ b/iosVariant/iosVariant/iOSApp.swift @@ -18,11 +18,16 @@ import SwiftUI +@available(iOS 17.0, *) @main struct iOSApp: App { + init() { + Koin.start() + } + var body: some Scene { WindowGroup { ContentView() } } -} \ No newline at end of file +} diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 2b0f6ab..cbd1890 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.android.library) + alias(libs.plugins.sqldelight) } kotlin { @@ -11,24 +12,31 @@ kotlin { } } } - + listOf( iosX64(), iosArm64(), iosSimulatorArm64() ).forEach { it.binaries.framework { - baseName = "shared" - isStatic = true + baseName = "Variant" } } sourceSets { + androidMain.dependencies { + implementation(libs.lifecycle.viewmodel.android) + implementation(libs.sqldelight.driver.android) + } + iosMain.dependencies { + implementation(libs.sqldelight.driver.native) + } commonMain.dependencies { - //put your multiplatform dependencies here + implementation(libs.koin.core) } commonTest.dependencies { implementation(libs.kotlin.test) + implementation(libs.koin.test) } } @@ -48,3 +56,14 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 } } + +sqldelight { + databases { + create("VariantDb") { + packageName.set("org.comixedproject.variant") + schemaOutputDirectory.set( + file("src/commonMain/sqldelight/org/comixedproject/variant/db") + ) + } + } +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/comixedproject/variant/shared/KoinAndroid.kt b/shared/src/androidMain/kotlin/org/comixedproject/variant/shared/KoinAndroid.kt new file mode 100644 index 0000000..3f96212 --- /dev/null +++ b/shared/src/androidMain/kotlin/org/comixedproject/variant/shared/KoinAndroid.kt @@ -0,0 +1,30 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.shared + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.android.AndroidSqliteDriver +import org.comixedproject.variant.VariantDb +import org.koin.dsl.module + +actual val platformModule = module { + single { + AndroidSqliteDriver(VariantDb.Schema, get(), "VariantDb") + } +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/comixedproject/variant/shared/model/BaseViewModel.android.kt b/shared/src/androidMain/kotlin/org/comixedproject/variant/shared/model/BaseViewModel.android.kt new file mode 100644 index 0000000..ad127c7 --- /dev/null +++ b/shared/src/androidMain/kotlin/org/comixedproject/variant/shared/model/BaseViewModel.android.kt @@ -0,0 +1,23 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.shared.model + +import androidx.lifecycle.ViewModel + +actual abstract class BaseViewModel : ViewModel() \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/KoinCommon.kt b/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/KoinCommon.kt new file mode 100644 index 0000000..675918e --- /dev/null +++ b/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/KoinCommon.kt @@ -0,0 +1,58 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.shared + +import org.comixedproject.variant.shared.data.DatabaseHelper +import org.comixedproject.variant.shared.data.ServerRepository +import org.comixedproject.variant.shared.model.VariantViewModel +import org.koin.core.KoinApplication +import org.koin.core.context.startKoin +import org.koin.core.module.Module +import org.koin.dsl.module + +object Modules { + val core = module { + factory { DatabaseHelper(get()) } + } + + val repositories = module { + factory { ServerRepository(get()) } + } + + val viewModels = module { + factory { VariantViewModel(get()) } + } +} + +expect val platformModule: Module + +fun initKoin( + appModule: Module = module { }, + coreModule: Module = Modules.core, + repositoriesModule: Module = Modules.repositories, + viewModelsModule: Module = Modules.viewModels, +): KoinApplication = startKoin { + modules( + appModule, + coreModule, + repositoriesModule, + viewModelsModule, + platformModule + ) +} diff --git a/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/data/DatabaseHelper.kt b/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/data/DatabaseHelper.kt new file mode 100644 index 0000000..c3e9e12 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/data/DatabaseHelper.kt @@ -0,0 +1,42 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.shared.data + +import app.cash.sqldelight.db.SqlDriver +import org.comixedproject.variant.VariantDb +import org.comixedproject.variant.db.ServersDb +import org.comixedproject.variant.shared.IDGenerator + +class DatabaseHelper(sqlDriver: SqlDriver) { + private val database: VariantDb = VariantDb(sqlDriver) + + fun loadServers(): List = database.tableQueries.loadAllServers().executeAsList() + + fun createServer(name: String, url: String, username: String, password: String) { + database.tableQueries.createServer(IDGenerator().toString(), name, url, username, password) + } + + fun updateServer(id: String, name: String, url: String, username: String, password: String) { + database.tableQueries.updateServer(name, url, username, password, id) + } + + fun deleteServer(id: String) { + database.tableQueries.deleteServer(id) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/data/ServerRepository.kt b/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/data/ServerRepository.kt new file mode 100644 index 0000000..9df77f6 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/data/ServerRepository.kt @@ -0,0 +1,53 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.shared.data + +import org.comixedproject.variant.db.ServersDb +import org.comixedproject.variant.shared.model.server.Server + +class ServerRepository(private val databaseHelper: DatabaseHelper) { + val servers: List + get() = databaseHelper.loadServers().map(ServersDb::map) + + fun saveServer(server: Server) { + if (server.id == null) { + databaseHelper.createServer(server.name, server.url, server.username, server.password) + } else { + databaseHelper.updateServer( + server.id, + server.name, + server.url, + server.username, + server.password + ) + } + } + + fun deleteServer(server: Server) { + server.id?.let { id -> databaseHelper.deleteServer(id) } + } +} + +fun ServersDb.map() = Server( + id = this.id, + name = this.name, + url = this.url, + username = this.username, + password = this.password +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/model/BaseViewModel.kt b/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/model/BaseViewModel.kt new file mode 100644 index 0000000..4285253 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/model/BaseViewModel.kt @@ -0,0 +1,21 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.shared.model + +expect abstract class BaseViewModel() \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/model/VariantViewModel.kt b/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/model/VariantViewModel.kt new file mode 100644 index 0000000..8615ce4 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/model/VariantViewModel.kt @@ -0,0 +1,42 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.shared.model + +import org.comixedproject.variant.shared.data.ServerRepository +import org.comixedproject.variant.shared.model.server.Server + +class VariantViewModel(val serverRepository: ServerRepository) : BaseViewModel() { + val servers: List + get() = serverRepository.servers + + var onServerUpdate: ((List) -> Unit)? = null + set(value) { + field = value + onServerUpdate?.invoke(servers) + } + + fun saveServer(server: Server) { + serverRepository.saveServer(server) + onServerUpdate?.invoke(servers) + } + + fun deleteServer(server: Server) { + serverRepository.deleteServer(server) + } +} \ No newline at end of file diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/model/server/Server.kt b/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/model/server/Server.kt similarity index 77% rename from androidVariant/src/main/java/org/comixedproject/variant/android/model/server/Server.kt rename to shared/src/commonMain/kotlin/org/comixedproject/variant/shared/model/server/Server.kt index 4c1abbf..5cc2b33 100644 --- a/androidVariant/src/main/java/org/comixedproject/variant/android/model/server/Server.kt +++ b/shared/src/commonMain/kotlin/org/comixedproject/variant/shared/model/server/Server.kt @@ -16,10 +16,12 @@ * along with this program. If not, see */ -package org.comixedproject.variant.android.model.server +package org.comixedproject.variant.shared.model.server -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -class Server(val id: Int, val name: String, val url: String, val username: String) : Parcelable \ No newline at end of file +data class Server( + val id: String?, + val name: String, + val url: String, + val username: String, + val password: String +) \ No newline at end of file diff --git a/shared/src/commonMain/sqldelight/org/comixedproject/variant/db/Table.sq b/shared/src/commonMain/sqldelight/org/comixedproject/variant/db/Table.sq new file mode 100644 index 0000000..fd93b1c --- /dev/null +++ b/shared/src/commonMain/sqldelight/org/comixedproject/variant/db/Table.sq @@ -0,0 +1,19 @@ +CREATE TABLE ServersDb ( +id TEXT NOT NULL PRIMARY KEY, +name TEXT NOT NULL UNIQUE, +url TEXT NOT NULL, +username TEXT NOT NULL DEFAULT "", +password TEXT NOT NULL DEFAULT "" +); + +loadAllServers: +SELECT * FROM ServersDb; + +createServer: +INSERT OR IGNORE INTO ServersDb(id, name, url, username, password) VALUES (?, ?, ?, ?, ?); + +updateServer: +UPDATE ServersDb SET name = ?, url = ?, username = ?, password = ? WHERE id = ?; + +deleteServer: +DELETE FROM ServersDb WHERE id = ?; \ No newline at end of file diff --git a/shared/src/commonTest/kotlin/org/comixproject/variant/shared/KoinIntegrationTest.kt b/shared/src/commonTest/kotlin/org/comixproject/variant/shared/KoinIntegrationTest.kt new file mode 100644 index 0000000..ea0a8b2 --- /dev/null +++ b/shared/src/commonTest/kotlin/org/comixproject/variant/shared/KoinIntegrationTest.kt @@ -0,0 +1,44 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixproject.variant.shared + +import org.comixedproject.variant.shared.Modules +import org.koin.core.context.stopKoin +import org.koin.dsl.koinApplication +import org.koin.test.check.checkModules +import kotlin.test.AfterTest + +class KoinIntegrationTest { + // TODO fix this + // @Test + fun testAllModules() { + koinApplication { + modules( + Modules.core, + Modules.repositories, + Modules.viewModels + ) + }.checkModules() + } + + @AfterTest + fun tearDown() { + stopKoin() + } +} \ No newline at end of file diff --git a/shared/src/commonTest/kotlin/org/comixproject/variant/shared/model/VariantViewModelTest.kt b/shared/src/commonTest/kotlin/org/comixproject/variant/shared/model/VariantViewModelTest.kt new file mode 100644 index 0000000..95fa4b9 --- /dev/null +++ b/shared/src/commonTest/kotlin/org/comixproject/variant/shared/model/VariantViewModelTest.kt @@ -0,0 +1,62 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixproject.variant.shared.model + +import org.comixedproject.variant.shared.initKoin +import org.comixedproject.variant.shared.model.VariantViewModel +import org.comixedproject.variant.shared.model.server.Server +import org.koin.core.context.stopKoin +import org.koin.test.KoinTest +import org.koin.test.inject +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.assertTrue + +val TEST_NAME = "Server Name" +val TEST_URL = "http://www.comixedproject.org:7171/opds" +val TEST_USERNAME = "reader@comixedproject.org" +val TEST_PASSWORD = "p455w0rd!" + +class VariantViewModelTest : KoinTest { + private val viewModel: VariantViewModel by inject() + + @BeforeTest + fun setUp() { + initKoin() + } + + @AfterTest + fun tearDown() { + stopKoin() + } + + // TODO fix this + // @Test + fun testCreateServer() { + viewModel.saveServer(Server(null, TEST_NAME, TEST_URL, TEST_USERNAME, TEST_PASSWORD)) + + val count = viewModel.servers.count { entry -> + entry.name == TEST_NAME + entry.url == TEST_URL + entry.username == TEST_USERNAME + } + + assertTrue(actual = count === 1) + } +} \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/org/comixedproject/variant/shared/KoinIOS.kt b/shared/src/iosMain/kotlin/org/comixedproject/variant/shared/KoinIOS.kt new file mode 100644 index 0000000..ed32b9d --- /dev/null +++ b/shared/src/iosMain/kotlin/org/comixedproject/variant/shared/KoinIOS.kt @@ -0,0 +1,58 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.shared + +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.native.NativeSqliteDriver +import kotlinx.cinterop.ObjCClass +import kotlinx.cinterop.getOriginalKotlinClass +import org.comixedproject.variant.VariantDb +import org.koin.core.Koin +import org.koin.core.KoinApplication +import org.koin.core.module.Module +import org.koin.core.parameter.parametersOf +import org.koin.core.qualifier.Qualifier +import org.koin.dsl.module +import platform.Foundation.NSUserDefaults + +actual val platformModule: Module = module { + single { + NativeSqliteDriver(VariantDb.Schema, "VariantDb") + } +} + +object KoinIOS { + fun initialize( + userDefaults: NSUserDefaults, + ): KoinApplication = initKoin( + appModule = module {} + ) +} + +@kotlinx.cinterop.BetaInteropApi +fun Koin.get(objCClass: ObjCClass): Any { + val kClazz = getOriginalKotlinClass(objCClass)!! + return get(kClazz, null, null) +} + +@kotlinx.cinterop.BetaInteropApi +fun Koin.get(objCClass: ObjCClass, qualifier: Qualifier?, parameter: Any): Any { + val kClazz = getOriginalKotlinClass(objCClass)!! + return get(kClazz, qualifier) { parametersOf(parameter) } +} \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/org/comixedproject/variant/shared/model/BaseViewModel.ios.kt b/shared/src/iosMain/kotlin/org/comixedproject/variant/shared/model/BaseViewModel.ios.kt new file mode 100644 index 0000000..c955ccf --- /dev/null +++ b/shared/src/iosMain/kotlin/org/comixedproject/variant/shared/model/BaseViewModel.ios.kt @@ -0,0 +1,21 @@ +/* + * Variant - A digital comic book reading application for the iPad and Android tablets. + * Copyright (C) 2024, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +package org.comixedproject.variant.shared.model + +actual abstract class BaseViewModel actual constructor() \ No newline at end of file