diff --git a/src/main/kotlin/ca/gosyer/data/DataModule.kt b/src/main/kotlin/ca/gosyer/data/DataModule.kt index 4dadb4d6..6b5ec00d 100644 --- a/src/main/kotlin/ca/gosyer/data/DataModule.kt +++ b/src/main/kotlin/ca/gosyer/data/DataModule.kt @@ -13,6 +13,7 @@ import ca.gosyer.data.library.LibraryPreferences import ca.gosyer.data.server.Http import ca.gosyer.data.server.HttpProvider import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.ServerService import ca.gosyer.data.server.interactions.CategoryInteractionHandler import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.data.server.interactions.ExtensionInteractionHandler @@ -63,4 +64,8 @@ val DataModule = module { .toClass() bind() .toClass() + + bind() + .toClass() + .singleton() } diff --git a/src/main/kotlin/ca/gosyer/data/library/LibraryPreferences.kt b/src/main/kotlin/ca/gosyer/data/library/LibraryPreferences.kt index 5a08110e..4517e373 100644 --- a/src/main/kotlin/ca/gosyer/data/library/LibraryPreferences.kt +++ b/src/main/kotlin/ca/gosyer/data/library/LibraryPreferences.kt @@ -15,4 +15,8 @@ class LibraryPreferences(private val preferenceStore: PreferenceStore) { fun displayMode(): Preference { return preferenceStore.getJsonObject("display_mode", DisplayMode.CompactGrid, DisplayMode.serializer()) } + + fun showAllCategory(): Preference { + return preferenceStore.getBoolean("show_all_category", false) + } } \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/data/server/ServerPreferences.kt b/src/main/kotlin/ca/gosyer/data/server/ServerPreferences.kt index bc5dac1f..97c8bfd8 100644 --- a/src/main/kotlin/ca/gosyer/data/server/ServerPreferences.kt +++ b/src/main/kotlin/ca/gosyer/data/server/ServerPreferences.kt @@ -10,6 +10,11 @@ import ca.gosyer.common.prefs.Preference import ca.gosyer.common.prefs.PreferenceStore class ServerPreferences(private val preferenceStore: PreferenceStore) { + + fun host(): Preference { + return preferenceStore.getBoolean("host", true) + } + fun server(): Preference { return preferenceStore.getString("server_url", "http://localhost:4567") } diff --git a/src/main/kotlin/ca/gosyer/data/server/ServerService.kt b/src/main/kotlin/ca/gosyer/data/server/ServerService.kt new file mode 100644 index 00000000..29fb0259 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/data/server/ServerService.kt @@ -0,0 +1,101 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.data.server + +import ca.gosyer.util.system.userDataDir +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import mu.KotlinLogging +import java.io.BufferedReader +import java.io.File +import javax.inject.Inject +import kotlin.concurrent.thread + +class ServerService @Inject constructor( + val serverPreferences: ServerPreferences +) { + private val host = serverPreferences.host().stateIn(GlobalScope) + val initialized = MutableStateFlow( + if (host.value) { + ServerResult.STARTING + } else { + ServerResult.UNUSED + } + ) + var process: Process? = null + + init { + host.onEach { + process?.destroy() + initialized.value = if (host.value) { + ServerResult.STARTING + } else { + ServerResult.UNUSED + return@onEach + } + GlobalScope.launch { + + val logger = KotlinLogging.logger("Server") + val runtime = Runtime.getRuntime() + + val jarFile = File(userDataDir,"Tachidesk.jar") + if (!jarFile.exists()) { + logger.info { "Copying server to resources" } + javaClass.getResourceAsStream("/Tachidesk.jar")?.buffered()?.use { input -> + jarFile.outputStream().use { output -> + input.copyTo(output) + } + } + } + + val javaLibraryPath = System.getProperty("java.library.path").substringBefore(File.pathSeparator) + val javaExeFile = File(javaLibraryPath, "java.exe") + val javaUnixFile = File(javaLibraryPath, "java") + val javaExePath = when { + javaExeFile.exists() ->'"' + javaExeFile.absolutePath + '"' + javaUnixFile.exists() -> '"' + javaUnixFile.absolutePath + '"' + else -> "java" + } + + logger.info { "Starting server with $javaExePath" } + val reader: BufferedReader + process = runtime.exec("""$javaExePath -jar "${jarFile.absolutePath}"""").also { + reader = it.inputStream.bufferedReader() + } + runtime.addShutdownHook(thread(start = false) { + process?.destroy() + }) + logger.info { "Server started successfully" } + var line: String? + while (reader.readLine().also { line = it } != null) { + if (initialized.value == ServerResult.STARTING) { + if (line?.contains("Javalin started") == true) { + initialized.value = ServerResult.STARTED + } else if (line?.contains("Javalin has stopped") == true) { + initialized.value = ServerResult.FAILED + } + } + logger.info { line } + } + logger.info { "Server closed" } + val exitVal = process?.waitFor() + logger.info { "Process exitValue: $exitVal" } + + } + }.launchIn(GlobalScope) + } + + enum class ServerResult { + UNUSED, + STARTING, + STARTED, + FAILED; + } +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/data/ui/UiPreferences.kt b/src/main/kotlin/ca/gosyer/data/ui/UiPreferences.kt index 09f3b005..52a692ce 100644 --- a/src/main/kotlin/ca/gosyer/data/ui/UiPreferences.kt +++ b/src/main/kotlin/ca/gosyer/data/ui/UiPreferences.kt @@ -8,7 +8,7 @@ package ca.gosyer.data.ui import ca.gosyer.common.prefs.Preference import ca.gosyer.common.prefs.PreferenceStore -import ca.gosyer.data.ui.model.Screen +import ca.gosyer.data.ui.model.StartScreen import ca.gosyer.data.ui.model.ThemeMode class UiPreferences(private val preferenceStore: PreferenceStore) { @@ -41,8 +41,8 @@ class UiPreferences(private val preferenceStore: PreferenceStore) { return preferenceStore.getInt("color_secondary_dark", 0) } - fun startScreen(): Preference { - return preferenceStore.getJsonObject("start_screen", Screen.Library, Screen.serializer()) + fun startScreen(): Preference { + return preferenceStore.getJsonObject("start_screen", StartScreen.Library, StartScreen.serializer()) } fun confirmExit(): Preference { diff --git a/src/main/kotlin/ca/gosyer/data/ui/model/Screen.kt b/src/main/kotlin/ca/gosyer/data/ui/model/StartScreen.kt similarity index 94% rename from src/main/kotlin/ca/gosyer/data/ui/model/Screen.kt rename to src/main/kotlin/ca/gosyer/data/ui/model/StartScreen.kt index da63e2bc..938866e2 100644 --- a/src/main/kotlin/ca/gosyer/data/ui/model/Screen.kt +++ b/src/main/kotlin/ca/gosyer/data/ui/model/StartScreen.kt @@ -11,7 +11,7 @@ package ca.gosyer.data.ui.model import kotlinx.serialization.Serializable @Serializable -enum class Screen { +enum class StartScreen { Library, // Updates, // History, diff --git a/src/main/kotlin/ca/gosyer/ui/base/WindowDialog.kt b/src/main/kotlin/ca/gosyer/ui/base/WindowDialog.kt index efb1ca4f..fcaa729d 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/WindowDialog.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/WindowDialog.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedButton import androidx.compose.material.Surface import androidx.compose.material.Text @@ -27,6 +26,7 @@ import androidx.compose.ui.input.key.Key import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import ca.gosyer.ui.base.theme.AppTheme import javax.swing.SwingUtilities @Suppress("FunctionName") @@ -69,7 +69,7 @@ fun WindowDialog( window.keyboard.setShortcut(Key.Escape, onNegativeButton.plusClose()) window.show { - MaterialTheme { + AppTheme { Surface { Column(verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxSize(),) { Row(content = row, modifier = Modifier.fillMaxWidth()) @@ -117,10 +117,9 @@ fun WindowDialog( } window.show { - MaterialTheme { + AppTheme { Surface { Column( - verticalArrangement = Arrangement.Bottom, modifier = Modifier.fillMaxSize() ) { content(window) diff --git a/src/main/kotlin/ca/gosyer/ui/base/components/ColorPickerDialog.kt b/src/main/kotlin/ca/gosyer/ui/base/components/ColorPickerDialog.kt index 923ec1f1..165cb3bc 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/components/ColorPickerDialog.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/components/ColorPickerDialog.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.GridCells -import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape @@ -79,6 +78,7 @@ fun ColorPickerDialog( WindowDialog( onDismissRequest = onDismissRequest, + size = IntSize(300, 520), title = title, content = { val showPresetsState by showPresets.collectAsState() @@ -157,7 +157,7 @@ private fun ColorPresets( .background(MaterialTheme.colors.onBackground.copy(alpha = 0.2f)) ) - LazyRow { + LazyVerticalGrid(cells = GridCells.Fixed(5)) { items(shades) { color -> ColorPresetItem( color = color, diff --git a/src/main/kotlin/ca/gosyer/ui/base/components/ErrorScreen.kt b/src/main/kotlin/ca/gosyer/ui/base/components/ErrorScreen.kt index 86e018ef..5515e28d 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/components/ErrorScreen.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/components/ErrorScreen.kt @@ -9,7 +9,9 @@ package ca.gosyer.ui.base.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Button import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -19,13 +21,20 @@ import androidx.compose.ui.unit.sp import kotlin.random.Random @Composable -fun ErrorScreen(errorMessage: String? = null) { - Box(Modifier.fillMaxSize()) { - Column(modifier = Modifier.align(Alignment.Center)) { - val errorFace = remember { getRandomErrorFace() } - Text(errorFace, fontSize = 36.sp, color = MaterialTheme.colors.onBackground) - if (errorMessage != null) { - Text(errorMessage, color = MaterialTheme.colors.onBackground) +fun ErrorScreen(errorMessage: String? = null, retry: (() -> Unit)? = null) { + Surface { + Box(Modifier.fillMaxSize()) { + Column(modifier = Modifier.align(Alignment.Center)) { + val errorFace = remember { getRandomErrorFace() } + Text(errorFace, fontSize = 36.sp, color = MaterialTheme.colors.onBackground) + if (errorMessage != null) { + Text(errorMessage, color = MaterialTheme.colors.onBackground) + } + if (retry != null) { + Button(retry) { + Text("Retry") + } + } } } } diff --git a/src/main/kotlin/ca/gosyer/ui/base/components/LoadingScreen.kt b/src/main/kotlin/ca/gosyer/ui/base/components/LoadingScreen.kt index b73f607d..cb274207 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/components/LoadingScreen.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/components/LoadingScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -22,14 +23,16 @@ fun LoadingScreen( modifier: Modifier = Modifier.fillMaxSize(), errorMessage: String? = null ) { - BoxWithConstraints(modifier) { - if (isLoading) { - val size = remember(maxHeight, maxWidth) { - min(maxHeight, maxWidth) / 2 + Surface(modifier) { + BoxWithConstraints { + if (isLoading) { + val size = remember(maxHeight, maxWidth) { + min(maxHeight, maxWidth) / 2 + } + CircularProgressIndicator(Modifier.align(Alignment.Center).size(size)) + } else { + ErrorScreen(errorMessage) } - CircularProgressIndicator(Modifier.align(Alignment.Center).size(size)) - } else { - ErrorScreen(errorMessage) } } } \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/ui/base/components/Manga.kt b/src/main/kotlin/ca/gosyer/ui/base/components/Manga.kt index 2968c4e8..f4a50893 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/components/Manga.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/components/Manga.kt @@ -45,7 +45,7 @@ fun MangaGridItem( modifier = Modifier .fillMaxWidth() .aspectRatio(mangaAspectRatio) - .padding(4.dp) + .padding(8.dp) .clickable(onClick = onClick), elevation = 4.dp, shape = RoundedCornerShape(4.dp) diff --git a/src/main/kotlin/ca/gosyer/ui/base/components/Toolbar.kt b/src/main/kotlin/ca/gosyer/ui/base/components/Toolbar.kt index 8bd75f25..7267ea61 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/components/Toolbar.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/components/Toolbar.kt @@ -6,24 +6,24 @@ package ca.gosyer.ui.base.components -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.AppBarDefaults import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text +import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import ca.gosyer.ui.main.Routing +import ca.gosyer.ui.main.Route import com.github.zsoltk.compose.router.BackStack import compose.icons.FontAwesomeIcons import compose.icons.fontawesomeicons.Regular @@ -32,13 +32,39 @@ import compose.icons.fontawesomeicons.regular.WindowClose @Composable fun Toolbar( name: String, - router: BackStack? = null, + router: BackStack? = null, closable: Boolean, + modifier: Modifier = Modifier, + actions: @Composable RowScope.() -> Unit = {}, + backgroundColor: Color = MaterialTheme.colors.primary, //CustomColors.current.bars, + contentColor: Color = MaterialTheme.colors.onPrimary, //CustomColors.current.onBars, + elevation: Dp = AppBarDefaults.TopAppBarElevation, search: ((String) -> Unit)? = null ) { val searchText = remember { mutableStateOf("") } Surface(Modifier.fillMaxWidth().height(32.dp), elevation = 2.dp) { - Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween) { + TopAppBar( + { + Text(name) + }, + modifier, + actions = @Composable { + actions() + if (closable) { + IconButton( + onClick = { + router?.pop() + } + ) { + Icon(FontAwesomeIcons.Regular.WindowClose, "close") + } + } + }, + backgroundColor = backgroundColor, + contentColor = contentColor, + elevation = elevation + ) + /*Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween) { Text(name, fontSize = 24.sp) if (search != null) { BasicTextField( @@ -58,6 +84,6 @@ fun Toolbar( Icon(FontAwesomeIcons.Regular.WindowClose, "close", Modifier.size(32.dp)) } } - } + }*/ } } \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/ui/base/prefs/PreferenceMutableState.kt b/src/main/kotlin/ca/gosyer/ui/base/prefs/PreferenceMutableState.kt index a576a68e..661adba9 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/prefs/PreferenceMutableState.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/prefs/PreferenceMutableState.kt @@ -20,7 +20,7 @@ class PreferenceMutableStateFlow( init { preference.changes() - .onEach { value = it } + .onEach { state.value = it } .launchIn(scope) } diff --git a/src/main/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt b/src/main/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt index 2326e108..0c821f76 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt @@ -27,16 +27,23 @@ import androidx.compose.material.ContentAlpha import androidx.compose.material.Icon import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField import androidx.compose.material.RadioButton import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import ca.gosyer.ui.base.WindowDialog @@ -64,13 +71,14 @@ class PreferenceScope { title: String, subtitle: String? = null ) { + val prefValue by preference.collectAsState() Pref( title = title, - subtitle = if (subtitle == null) choices[preference.value] else null, + subtitle = if (subtitle == null) choices[prefValue] else null, onClick = { ChoiceDialog( items = choices.toList(), - selected = preference.value, + selected = prefValue, title = title, onSelected = { selected -> preference.value = selected @@ -102,7 +110,8 @@ class PreferenceScope { }, onLongClick = { preference.value = Color.Unspecified }, action = { - if (preference.value != Color.Unspecified || unsetColor != Color.Unspecified) { + val prefValue by preference.collectAsState() + if (prefValue != Color.Unspecified || unsetColor != Color.Unspecified) { val borderColor = MaterialTheme.colors.onBackground.copy(alpha = 0.54f) Box( modifier = Modifier @@ -214,8 +223,37 @@ fun SwitchPref( title = title, subtitle = subtitle, icon = icon, - action = { Switch(checked = preference.value, onCheckedChange = null) }, + action = { + val prefValue by preference.collectAsState() + Switch(checked = prefValue, onCheckedChange = null) + }, onClick = { preference.value = !preference.value } ) } +@Composable +fun EditTextPref( + preference: PreferenceMutableStateFlow, + title: String, + subtitle: String? = null, + icon: ImageVector? = null +) { + var editText by remember { mutableStateOf(TextFieldValue(preference.value)) } + Pref( + title = title, + subtitle = subtitle, + icon = icon, + onClick = { + WindowDialog( + title, + onPositiveButton = { + preference.value = editText.text + } + ) { + OutlinedTextField(editText, onValueChange = { + editText = it + }) + } + } + ) +} \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/ui/base/theme/AppColorsPreference.kt b/src/main/kotlin/ca/gosyer/ui/base/theme/AppColorsPreference.kt index b6203fdd..9f2ed188 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/theme/AppColorsPreference.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/theme/AppColorsPreference.kt @@ -9,9 +9,10 @@ package ca.gosyer.ui.base.theme import androidx.compose.ui.graphics.Color import ca.gosyer.common.prefs.Preference import ca.gosyer.data.ui.UiPreferences +import ca.gosyer.ui.base.prefs.PreferenceMutableStateFlow import ca.gosyer.ui.base.prefs.asColor +import ca.gosyer.ui.base.prefs.asStateIn import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow data class AppColorsPreference( val primary: Preference, @@ -19,8 +20,8 @@ data class AppColorsPreference( ) class AppColorsPreferenceState( - val primaryStateFlow: StateFlow, - val secondaryStateFlow: StateFlow + val primaryStateFlow: PreferenceMutableStateFlow, + val secondaryStateFlow: PreferenceMutableStateFlow ) fun UiPreferences.getLightColors(): AppColorsPreference { @@ -37,9 +38,9 @@ fun UiPreferences.getDarkColors(): AppColorsPreference { ) } -fun AppColorsPreference.asState(scope: CoroutineScope): AppColorsPreferenceState { +fun AppColorsPreference.asStateFlow(scope: CoroutineScope): AppColorsPreferenceState { return AppColorsPreferenceState( - primary.stateIn(scope), - secondary.stateIn(scope) + primary.asStateIn(scope), + secondary.asStateIn(scope) ) } diff --git a/src/main/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt b/src/main/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt index 81d94d73..69dbb7e4 100644 --- a/src/main/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt +++ b/src/main/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt @@ -59,9 +59,9 @@ private class AppThemeViewModel @Inject constructor( baseThemeJob.cancelChildren() if (baseTheme.colors.isLight) { - uiPreferences.getLightColors().asState(baseThemeScope) + uiPreferences.getLightColors().asStateFlow(baseThemeScope) } else { - uiPreferences.getDarkColors().asState(baseThemeScope) + uiPreferences.getDarkColors().asStateFlow(baseThemeScope) } } diff --git a/src/main/kotlin/ca/gosyer/ui/library/LibraryScreen.kt b/src/main/kotlin/ca/gosyer/ui/library/LibraryScreen.kt index 336e69ed..83dd05a5 100644 --- a/src/main/kotlin/ca/gosyer/ui/library/LibraryScreen.kt +++ b/src/main/kotlin/ca/gosyer/ui/library/LibraryScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ScrollableTabRow import androidx.compose.material.Tab @@ -22,6 +23,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import ca.gosyer.data.library.model.DisplayMode import ca.gosyer.data.models.Category @@ -58,7 +60,7 @@ fun LibraryScreen(onClickManga: (Long) -> Unit = { openMangaMenu(it) }) { sheetState = sheetState, sheetContent = { *//*LibrarySheet()*//* } ) {*/ - Column { + Column(Modifier.fillMaxWidth()) { /*Toolbar( title = { val text = if (vm.showCategoryTabs) { diff --git a/src/main/kotlin/ca/gosyer/ui/main/MainMenu.kt b/src/main/kotlin/ca/gosyer/ui/main/MainMenu.kt index fdba5b7d..9e25f523 100644 --- a/src/main/kotlin/ca/gosyer/ui/main/MainMenu.kt +++ b/src/main/kotlin/ca/gosyer/ui/main/MainMenu.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -35,12 +36,22 @@ import ca.gosyer.ui.base.vm.viewModel import ca.gosyer.ui.extensions.ExtensionsMenu import ca.gosyer.ui.library.LibraryScreen import ca.gosyer.ui.manga.MangaMenu +import ca.gosyer.ui.settings.SettingsAdvancedScreen +import ca.gosyer.ui.settings.SettingsAppearance +import ca.gosyer.ui.settings.SettingsBackupScreen +import ca.gosyer.ui.settings.SettingsBrowseScreen +import ca.gosyer.ui.settings.SettingsGeneralScreen +import ca.gosyer.ui.settings.SettingsLibraryScreen +import ca.gosyer.ui.settings.SettingsReaderScreen +import ca.gosyer.ui.settings.SettingsScreen +import ca.gosyer.ui.settings.SettingsServerScreen import ca.gosyer.ui.sources.SourcesMenu import com.github.zsoltk.compose.router.Router import compose.icons.FontAwesomeIcons import compose.icons.fontawesomeicons.Regular import compose.icons.fontawesomeicons.regular.Bookmark import compose.icons.fontawesomeicons.regular.Compass +import compose.icons.fontawesomeicons.regular.Edit import compose.icons.fontawesomeicons.regular.Map @Composable @@ -48,51 +59,67 @@ fun MainMenu() { val vm = viewModel() Surface { - Router("TopLevel", Routing.LibraryMenu) { backStack -> + Router("TopLevel", Route.Library) { backStack -> Row { - Column(Modifier.width(200.dp).fillMaxHeight(),) { - Box(Modifier.fillMaxWidth().height(60.dp)) { - Text(BuildConfig.NAME, fontSize = 30.sp, modifier = Modifier.align(Alignment.Center)) - } - Spacer(Modifier.height(20.dp)) - remember { TopLevelMenus.values() }.forEach { topLevelMenu -> - MainMenuItem(topLevelMenu, backStack.elements.first() == topLevelMenu.menu) { - backStack.newRoot(it) + Surface(elevation = 2.dp) { + Column(Modifier.width(200.dp).fillMaxHeight(),) { + Box(Modifier.fillMaxWidth().height(60.dp)) { + Text(BuildConfig.NAME, fontSize = 30.sp, modifier = Modifier.align(Alignment.Center)) + } + Spacer(Modifier.height(20.dp)) + remember { TopLevelMenus.values() }.forEach { topLevelMenu -> + MainMenuItem(topLevelMenu, backStack.elements.first() == topLevelMenu.menu) { + backStack.newRoot(it) + } } - } - /*Button( - onClick = ::openExtensionsMenu - ) { - Text("Extensions") + /*Button( + onClick = ::openExtensionsMenu + ) { + Text("Extensions") + } + Button( + onClick = ::openSourcesMenu + ) { + Text("Sources") + } + Button( + onClick = ::openLibraryMenu + ) { + Text("Library") + } + Button( + onClick = ::openCategoriesMenu + ) { + Text("Categories") + }*/ } - Button( - onClick = ::openSourcesMenu - ) { - Text("Sources") - } - Button( - onClick = ::openLibraryMenu - ) { - Text("Library") - } - Button( - onClick = ::openCategoriesMenu - ) { - Text("Categories") - }*/ } Column(Modifier.fillMaxSize()) { when (val routing = backStack.last()) { - is Routing.LibraryMenu -> LibraryScreen { - backStack.push(Routing.MangaMenu(it)) + is Route.Library -> LibraryScreen { + backStack.push(Route.Manga(it)) } - is Routing.SourcesMenu -> SourcesMenu { - backStack.push(Routing.MangaMenu(it)) + is Route.Sources -> SourcesMenu { + backStack.push(Route.Manga(it)) } - is Routing.ExtensionsMenu -> ExtensionsMenu() - is Routing.MangaMenu -> MangaMenu(routing.mangaId, backStack) + is Route.Extensions -> ExtensionsMenu() + is Route.Manga -> MangaMenu(routing.mangaId, backStack) + + is Route.Settings -> SettingsScreen(backStack) + is Route.SettingsGeneral -> SettingsGeneralScreen(backStack) + is Route.SettingsAppearance -> SettingsAppearance(backStack) + is Route.SettingsServer -> SettingsServerScreen(backStack) + is Route.SettingsLibrary -> SettingsLibraryScreen(backStack) + is Route.SettingsReader -> SettingsReaderScreen(backStack) + /*is Route.SettingsDownloads -> SettingsDownloadsScreen(backStack) + is Route.SettingsTracking -> SettingsTrackingScreen(backStack)*/ + is Route.SettingsBrowse -> SettingsBrowseScreen(backStack) + is Route.SettingsBackup -> SettingsBackupScreen(backStack) + /*is Route.SettingsSecurity -> SettingsSecurityScreen(backStack) + is Route.SettingsParentalControls -> SettingsParentalControlsScreen(backStack)*/ + is Route.SettingsAdvanced -> SettingsAdvancedScreen(backStack) } } } @@ -102,7 +129,7 @@ fun MainMenu() { } @Composable -fun MainMenuItem(menu: TopLevelMenus, selected: Boolean, onClick: (Routing) -> Unit) { +fun MainMenuItem(menu: TopLevelMenus, selected: Boolean, onClick: (Route) -> Unit) { Card( Modifier.clickable { onClick(menu.menu) }.fillMaxWidth().height(40.dp), backgroundColor = if (!selected) { @@ -115,22 +142,37 @@ fun MainMenuItem(menu: TopLevelMenus, selected: Boolean, onClick: (Routing) -> U ) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize()) { Spacer(Modifier.width(16.dp)) - Image(menu.icon, menu.text, modifier = Modifier.size(20.dp)) + Image(menu.icon, menu.text, modifier = Modifier.size(20.dp), colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface)) Spacer(Modifier.width(16.dp)) - Text(menu.text) + Text(menu.text, color = MaterialTheme.colors.onSurface) } } } -enum class TopLevelMenus(val text: String, val icon: ImageVector, val menu: Routing) { - Library("Library", FontAwesomeIcons.Regular.Bookmark, Routing.LibraryMenu), - Sources("Sources", FontAwesomeIcons.Regular.Compass, Routing.SourcesMenu), - Extensions("Extensions", FontAwesomeIcons.Regular.Map, Routing.ExtensionsMenu); +enum class TopLevelMenus(val text: String, val icon: ImageVector, val menu: Route) { + Library("Library", FontAwesomeIcons.Regular.Bookmark, Route.Library), + Sources("Sources", FontAwesomeIcons.Regular.Compass, Route.Sources), + Extensions("Extensions", FontAwesomeIcons.Regular.Map, Route.Extensions), + Settings("Settings", FontAwesomeIcons.Regular.Edit, Route.Settings) } -sealed class Routing { - object LibraryMenu : Routing() - object SourcesMenu : Routing() - object ExtensionsMenu : Routing() - data class MangaMenu(val mangaId: Long): Routing() +sealed class Route { + object Library : Route() + object Sources : Route() + object Extensions : Route() + data class Manga(val mangaId: Long): Route() + + object Settings : Route() + object SettingsGeneral : Route() + object SettingsAppearance : Route() + object SettingsLibrary : Route() + object SettingsReader : Route() + /*object SettingsDownloads : Route() + object SettingsTracking : Route()*/ + object SettingsBrowse : Route() + object SettingsBackup : Route() + object SettingsServer : Route() + /*object SettingsSecurity : Route() + object SettingsParentalControls : Route()*/ + object SettingsAdvanced : Route() } \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/ui/main/main.kt b/src/main/kotlin/ca/gosyer/ui/main/main.kt index bdbbcae3..b740c80a 100644 --- a/src/main/kotlin/ca/gosyer/ui/main/main.kt +++ b/src/main/kotlin/ca/gosyer/ui/main/main.kt @@ -7,29 +7,24 @@ package ca.gosyer.ui.main import androidx.compose.desktop.AppWindow -import androidx.compose.desktop.DesktopMaterialTheme import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.input.key.Key import ca.gosyer.BuildConfig import ca.gosyer.data.DataModule +import ca.gosyer.data.server.ServerService +import ca.gosyer.data.server.ServerService.ServerResult +import ca.gosyer.ui.base.components.ErrorScreen import ca.gosyer.ui.base.components.LoadingScreen import ca.gosyer.ui.base.theme.AppTheme -import ca.gosyer.util.system.userDataDir import com.github.zsoltk.compose.backpress.BackPressHandler import com.github.zsoltk.compose.backpress.LocalBackPressHandler -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import mu.KotlinLogging import org.apache.logging.log4j.core.config.Configurator import toothpick.configuration.Configuration import toothpick.ktp.KTP -import java.io.BufferedReader -import java.io.File +import toothpick.ktp.extension.getInstance import javax.swing.SwingUtilities -import kotlin.concurrent.thread fun main() { val clazz = MainViewModel::class.java @@ -38,51 +33,6 @@ fun main() { clazz.classLoader, clazz.getResource("log4j2.xml")?.toURI() ) - val serverInitialized = MutableStateFlow(false) - - GlobalScope.launch { - val logger = KotlinLogging.logger("Server") - val runtime = Runtime.getRuntime() - - val jarFile = File(userDataDir,"Tachidesk.jar") - if (!jarFile.exists()) { - logger.info { "Copying server to resources" } - javaClass.getResourceAsStream("/Tachidesk.jar")?.buffered()?.use { input -> - jarFile.outputStream().use { output -> - input.copyTo(output) - } - } - } - - val javaLibraryPath = System.getProperty("java.library.path").substringBefore(File.pathSeparator) - val javaExeFile = File(javaLibraryPath, "java.exe") - val javaUnixFile = File(javaLibraryPath, "java") - val javaExePath = when { - javaExeFile.exists() ->'"' + javaExeFile.absolutePath + '"' - javaUnixFile.exists() -> '"' + javaUnixFile.absolutePath + '"' - else -> "java" - } - - logger.info { "Starting server with $javaExePath" } - val reader: BufferedReader - val process = runtime.exec("""$javaExePath -jar "${jarFile.absolutePath}"""").also { - reader = it.inputStream.bufferedReader() - } - runtime.addShutdownHook(thread(start = false) { - process?.destroy() - }) - logger.info { "Server started successfully" } - var line: String? - while (reader.readLine().also { line = it } != null) { - if (!serverInitialized.value && line?.contains("Javalin started") == true) { - serverInitialized.value = true - } - logger.info { line } - } - logger.info { "Server closed" } - val exitVal = process.waitFor() - logger.info { "Process exitValue: $exitVal" } - } if (BuildConfig.DEBUG) { System.setProperty("kotlinx.coroutines.debug", "on") @@ -96,11 +46,13 @@ fun main() { } ) - KTP.openRootScope() + val scope = KTP.openRootScope() .installModules( DataModule ) + val serverService = scope.getInstance() + SwingUtilities.invokeLater { val window = AppWindow( title = BuildConfig.NAME @@ -115,11 +67,13 @@ fun main() { CompositionLocalProvider( LocalBackPressHandler provides backPressHandler ) { - val initialized by serverInitialized.collectAsState() - if (initialized) { + val initialized by serverService.initialized.collectAsState() + if (initialized == ServerResult.STARTED || initialized == ServerResult.UNUSED) { MainMenu() - } else { + } else if (initialized == ServerResult.STARTING) { LoadingScreen() + } else if (initialized == ServerResult.FAILED) { + ErrorScreen("Unable to start server") } } } diff --git a/src/main/kotlin/ca/gosyer/ui/manga/MangaMenu.kt b/src/main/kotlin/ca/gosyer/ui/manga/MangaMenu.kt index 2104e8fd..62ed0e1c 100644 --- a/src/main/kotlin/ca/gosyer/ui/manga/MangaMenu.kt +++ b/src/main/kotlin/ca/gosyer/ui/manga/MangaMenu.kt @@ -46,9 +46,10 @@ import ca.gosyer.ui.base.components.LoadingScreen import ca.gosyer.ui.base.components.Toolbar import ca.gosyer.ui.base.components.mangaAspectRatio import ca.gosyer.ui.base.vm.viewModel -import ca.gosyer.ui.main.Routing +import ca.gosyer.ui.main.Route import ca.gosyer.util.compose.ThemedWindow import com.github.zsoltk.compose.router.BackStack +import java.util.Date fun openMangaMenu(mangaId: Long) { ThemedWindow("TachideskJUI") { @@ -57,7 +58,7 @@ fun openMangaMenu(mangaId: Long) { } @Composable -fun MangaMenu(mangaId: Long, backStack: BackStack? = null) { +fun MangaMenu(mangaId: Long, backStack: BackStack? = null) { val vm = viewModel { MangaMenuViewModel.Params(mangaId) } @@ -65,6 +66,7 @@ fun MangaMenu(mangaId: Long, backStack: BackStack? = null) { val chapters by vm.chapters.collectAsState() val isLoading by vm.isLoading.collectAsState() val serverUrl by vm.serverUrl.collectAsState() + val dateFormat by vm.dateFormat.collectAsState() Column(Modifier.background(MaterialTheme.colors.background)) { Toolbar("Manga", backStack, backStack != null) @@ -86,7 +88,7 @@ fun MangaMenu(mangaId: Long, backStack: BackStack? = null) { items(items) { when (it) { is MangaMenu.MangaMenuManga -> MangaItem(it.manga, serverUrl) - is MangaMenu.MangaMenuChapter -> ChapterItem(it.chapter) + is MangaMenu.MangaMenuChapter -> ChapterItem(it.chapter, dateFormat::format) } } } @@ -176,20 +178,20 @@ private fun MangaInfo(manga: Manga, modifier: Modifier = Modifier) { } @Composable -private fun ChapterItem(chapter: Chapter) { - Surface(modifier = Modifier.fillMaxWidth().height(70.dp).padding(4.dp), elevation = 2.dp) { +fun ChapterItem(chapter: Chapter, format: (Date) -> String) { + Surface(modifier = Modifier.fillMaxWidth().height(70.dp).padding(4.dp), elevation = 1.dp) { Column(Modifier.padding(4.dp)) { - Text(chapter.name, fontSize = 20.sp) + Text(chapter.name, fontSize = 20.sp, maxLines = 1) val description = mutableListOf() if (chapter.dateUpload != 0L) { - description += chapter.dateUpload.toString() + description += format(Date(chapter.dateUpload)) } if (!chapter.scanlator.isNullOrEmpty()) { description += chapter.scanlator } if (description.isNotEmpty()) { Spacer(Modifier.height(2.dp)) - Text(description.joinToString(" - ")) + Text(description.joinToString(" - "), maxLines = 1) } } } diff --git a/src/main/kotlin/ca/gosyer/ui/manga/MangaMenuViewModel.kt b/src/main/kotlin/ca/gosyer/ui/manga/MangaMenuViewModel.kt index 92c1f842..d0317d2c 100644 --- a/src/main/kotlin/ca/gosyer/ui/manga/MangaMenuViewModel.kt +++ b/src/main/kotlin/ca/gosyer/ui/manga/MangaMenuViewModel.kt @@ -12,18 +12,24 @@ import ca.gosyer.data.server.ServerPreferences import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.data.server.interactions.LibraryInteractionHandler import ca.gosyer.data.server.interactions.MangaInteractionHandler +import ca.gosyer.data.ui.UiPreferences import ca.gosyer.ui.base.vm.ViewModel import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Locale import javax.inject.Inject class MangaMenuViewModel @Inject constructor( private val params: Params, + private val uiPreferences: UiPreferences, private val mangaHandler: MangaInteractionHandler, private val chapterHandler: ChapterInteractionHandler, private val libraryHandler: LibraryInteractionHandler, @@ -40,6 +46,12 @@ class MangaMenuViewModel @Inject constructor( private val _isLoading = MutableStateFlow(true) val isLoading = _isLoading.asStateFlow() + val dateFormat = uiPreferences.dateFormat().changes() + .map { + getDateFormat(it) + } + .asStateFlow(getDateFormat(uiPreferences.dateFormat().get())) + init { scope.launch { refreshMangaAsync(params.mangaId).await() to refreshChaptersAsync(params.mangaId).await() @@ -82,5 +94,10 @@ class MangaMenuViewModel @Inject constructor( } + private fun getDateFormat(format: String): DateFormat = when (format) { + "" -> DateFormat.getDateInstance(DateFormat.SHORT) + else -> SimpleDateFormat(format, Locale.getDefault()) + } + data class Params(val mangaId: Long) } \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsAdvancedScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsAdvancedScreen.kt new file mode 100644 index 00000000..e8d9dae3 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsAdvancedScreen.kt @@ -0,0 +1,29 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack + +@Composable +fun SettingsAdvancedScreen(navController: BackStack) { + Column { + Toolbar("Advanced Settings", navController, true) + PreferencesScrollableColumn { + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsAppearanceScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsAppearanceScreen.kt new file mode 100644 index 00000000..141d6abe --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsAppearanceScreen.kt @@ -0,0 +1,169 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import ca.gosyer.data.ui.UiPreferences +import ca.gosyer.data.ui.model.ThemeMode +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.base.theme.AppColorsPreferenceState +import ca.gosyer.ui.base.theme.Theme +import ca.gosyer.ui.base.theme.asStateFlow +import ca.gosyer.ui.base.theme.getDarkColors +import ca.gosyer.ui.base.theme.getLightColors +import ca.gosyer.ui.base.theme.themes +import ca.gosyer.ui.base.vm.ViewModel +import ca.gosyer.ui.base.vm.viewModel +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack +import javax.inject.Inject + +class ThemesViewModel @Inject constructor( + private val uiPreferences: UiPreferences, +) : ViewModel() { + + val themeMode = uiPreferences.themeMode().asStateFlow() + val lightTheme = uiPreferences.lightTheme().asStateFlow() + val darkTheme = uiPreferences.darkTheme().asStateFlow() + val lightColors = uiPreferences.getLightColors().asStateFlow(scope) + val darkColors = uiPreferences.getDarkColors().asStateFlow(scope) + + @Composable + fun getActiveColors(): AppColorsPreferenceState { + return if (MaterialTheme.colors.isLight) lightColors else darkColors + } +} + +@Composable +fun SettingsAppearance(navController: BackStack) { + val vm = viewModel() + + val activeColors = vm.getActiveColors() + val isLight = MaterialTheme.colors.isLight + val themesForCurrentMode = remember(isLight) { + themes.filter { it.colors.isLight == isLight } + } + + Column { + Toolbar("Appearance Settings", navController, true) + PreferencesScrollableColumn { + ChoicePref( + preference = vm.themeMode, + choices = mapOf( + //ThemeMode.System to R.string.follow_system_settings, + ThemeMode.Light to "Light", + ThemeMode.Dark to "Dark" + ), + title = "Theme" + ) + Text( + "Preset themes", + modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 4.dp) + ) + LazyRow(modifier = Modifier.padding(horizontal = 8.dp)) { + items(themesForCurrentMode) { theme -> + ThemeItem(theme, onClick = { + (if (isLight) vm.lightTheme else vm.darkTheme).value = it.id + activeColors.primaryStateFlow.value = it.colors.primary + activeColors.secondaryStateFlow.value = it.colors.secondary + }) + } + } + ColorPref( + preference = activeColors.primaryStateFlow, title = "Color primary", + subtitle = "Displayed most frequently across your app", + unsetColor = MaterialTheme.colors.primary + ) + ColorPref( + preference = activeColors.secondaryStateFlow, title = "Color secondary", + subtitle = "Accents select parts of the UI", + unsetColor = MaterialTheme.colors.secondary + ) + } + } +} + +@Composable +private fun ThemeItem( + theme: Theme, + onClick: (Theme) -> Unit +) { + val borders = MaterialTheme.shapes.small + val borderColor = if (theme.colors.isLight) { + Color.Black.copy(alpha = 0.25f) + } else { + Color.White.copy(alpha = 0.15f) + } + Surface( + elevation = 4.dp, color = theme.colors.background, shape = borders, + modifier = Modifier + .size(100.dp, 160.dp) + .padding(8.dp) + .border(1.dp, borderColor, borders) + .clickable(onClick = { onClick(theme) }) + ) { + Column { + Box( + Modifier + .fillMaxWidth() + .weight(1f) + .padding(6.dp) + ) { + Text("Text", fontSize = 11.sp) + Button( + onClick = {}, + enabled = false, + contentPadding = PaddingValues(), + modifier = Modifier + .align(Alignment.BottomStart) + .size(40.dp, 20.dp), + content = {}, + colors = ButtonDefaults.buttonColors( + disabledBackgroundColor = theme.colors.primary + ) + ) + Surface(Modifier + .size(24.dp) + .align(Alignment.BottomEnd), + shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), + color = theme.colors.secondary, + elevation = 6.dp, + content = { } + ) + } + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt new file mode 100644 index 00000000..79ed010a --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsBackupScreen.kt @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack + +@Composable +fun SettingsBackupScreen(navController: BackStack) { + Column { + Toolbar("Backup Settings", navController, true) + PreferencesScrollableColumn { + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsBrowseScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsBrowseScreen.kt new file mode 100644 index 00000000..4af87329 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsBrowseScreen.kt @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack + +@Composable +fun SettingsBrowseScreen(navController: BackStack) { + Column { + Toolbar("Browse Settings", navController, true) + PreferencesScrollableColumn { + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsDownloadsScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsDownloadsScreen.kt new file mode 100644 index 00000000..1d821f78 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsDownloadsScreen.kt @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack + +@Composable +fun SettingsDownloadsScreen(navController: BackStack) { + Column { + Toolbar("Download Settings", navController, true) + PreferencesScrollableColumn { + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsGeneralScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsGeneralScreen.kt new file mode 100644 index 00000000..d9703fe1 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsGeneralScreen.kt @@ -0,0 +1,100 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Divider +import androidx.compose.runtime.Composable +import ca.gosyer.data.ui.UiPreferences +import ca.gosyer.data.ui.model.StartScreen +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.base.prefs.SwitchPref +import ca.gosyer.ui.base.vm.ViewModel +import ca.gosyer.ui.base.vm.viewModel +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import javax.inject.Inject + +class SettingsGeneralViewModel @Inject constructor( + uiPreferences: UiPreferences +) : ViewModel() { + + val startScreen = uiPreferences.startScreen().asStateFlow() + val confirmExit = uiPreferences.confirmExit().asStateFlow() + val language = uiPreferences.language().asStateFlow() + val dateFormat = uiPreferences.dateFormat().asStateFlow() + + private val now = Date() + + @Composable + fun getLanguageChoices(): Map { + val currentLocaleDisplayName = + Locale.getDefault().let { it.getDisplayName(it).capitalize() } + return mapOf( + "" to "System Default ($currentLocaleDisplayName)" + ) + } + + @Composable + fun getDateChoices(): Map { + return mapOf( + "" to "System Default", + "MM/dd/yy" to "MM/dd/yy", + "dd/MM/yy" to "dd/MM/yy", + "yyyy-MM-dd" to "yyyy-MM-dd" + ).mapValues { "${it.value} (${getFormattedDate(it.key)})" } + } + + private fun getFormattedDate(prefValue: String): String { + return when (prefValue) { + "" -> DateFormat.getDateInstance(DateFormat.SHORT) + else -> SimpleDateFormat(prefValue, Locale.getDefault()) + }.format(now.time) + } +} + +@Composable +fun SettingsGeneralScreen(navController: BackStack) { + val vm = viewModel() + Column { + Toolbar("General Settings", navController, true) + PreferencesScrollableColumn { + ChoicePref( + preference = vm.startScreen, + title = "Start Screen", + choices = mapOf( + StartScreen.Library to "Library", + StartScreen.Sources to "Sources", + StartScreen.Extensions to "Extensions", + ) + ) + SwitchPref(preference = vm.confirmExit, title = "Confirm Exit") + Divider() + ChoicePref( + preference = vm.language, + title = "Language", + choices = vm.getLanguageChoices(), + ) + ChoicePref( + preference = vm.dateFormat, + title = "Date Format", + choices = vm.getDateChoices() + ) + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt new file mode 100644 index 00000000..715a5571 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt @@ -0,0 +1,44 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import ca.gosyer.data.library.LibraryPreferences +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.base.prefs.SwitchPref +import ca.gosyer.ui.base.vm.ViewModel +import ca.gosyer.ui.base.vm.viewModel +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack +import javax.inject.Inject + +class SettingsLibraryViewModel @Inject constructor( + libraryPreferences: LibraryPreferences +) : ViewModel() { + + val showAllCategory = libraryPreferences.showAllCategory().asStateFlow() +} + +@Composable +fun SettingsLibraryScreen(navController: BackStack) { + val vm = viewModel() + + Column { + Toolbar("Library Settings", navController, true) + PreferencesScrollableColumn { + SwitchPref(preference = vm.showAllCategory, title = "Show all category") + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsParentalControlsScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsParentalControlsScreen.kt new file mode 100644 index 00000000..54196ef2 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsParentalControlsScreen.kt @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack + +@Composable +fun SettingsParentalControlsScreen(navController: BackStack) { + Column { + Toolbar("Parental Control Settings", navController, true) + PreferencesScrollableColumn { + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsReaderScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsReaderScreen.kt new file mode 100644 index 00000000..ca280190 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsReaderScreen.kt @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack + +@Composable +fun SettingsReaderScreen(navController: BackStack) { + Column { + Toolbar("Reader Settings", navController, true) + PreferencesScrollableColumn { + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsScreen.kt new file mode 100644 index 00000000..0640670d --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsScreen.kt @@ -0,0 +1,94 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.Pref +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack +import compose.icons.FontAwesomeIcons +import compose.icons.fontawesomeicons.Regular +import compose.icons.fontawesomeicons.regular.Edit + +@Composable +fun SettingsScreen(navController: BackStack) { + Column { + Toolbar("Settings", closable = false) + PreferencesScrollableColumn { + Pref( + title = "General", + icon = FontAwesomeIcons.Regular.Edit, + onClick = { navController.push(Route.SettingsGeneral) } + ) + Pref( + title = "Appearance", + icon = FontAwesomeIcons.Regular.Edit, + onClick = { navController.push(Route.SettingsAppearance) } + ) + Pref( + title = "Server", + icon = FontAwesomeIcons.Regular.Edit, + onClick = { navController.push(Route.SettingsServer) } + ) + Pref( + title = "Library", + icon = FontAwesomeIcons.Regular.Edit, + onClick = { navController.push(Route.SettingsLibrary) } + ) + Pref( + title = "Reader", + icon = FontAwesomeIcons.Regular.Edit, + onClick = { navController.push(Route.SettingsReader) } + ) + /*Pref( + title = "Downloads", + icon = FontAwesomeIcons.Regular.Edit, + onClick = { navController.push(Route.SettingsDownloads) } + ) + Pref( + title = "Tracking", + icon = FontAwesomeIcons.Regular.Edit, + onClick = { navController.push(Route.SettingsTracking) } + ) + */ + Pref( + title = "Browse", + icon = FontAwesomeIcons.Regular.Edit, + onClick = { navController.push(Route.SettingsBrowse) } + ) + Pref( + title = "Backup", + icon = FontAwesomeIcons.Regular.Edit, + onClick = { navController.push(Route.SettingsBackup) } + ) + /*Pref( + title = "Security", + icon = FontAwesomeIcons.Regular.Edit, + onClick = { navController.push(Route.SettingsSecurity) } + ) + Pref( + title = "Parental Controls", + icon = FontAwesomeIcons.Regular.User, + onClick = { navController.push(Route.SettingsParentalControls) } + )*/ + Pref( + title = "Advanced", + icon = FontAwesomeIcons.Regular.Edit, + onClick = { navController.push(Route.SettingsAdvanced) } + ) + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsSecurityScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsSecurityScreen.kt new file mode 100644 index 00000000..a9611ce7 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsSecurityScreen.kt @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack + +@Composable +fun SettingsSecurityScreen(navController: BackStack) { + Column { + Toolbar("Security Settings", navController, true) + PreferencesScrollableColumn { + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsServerScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsServerScreen.kt new file mode 100644 index 00000000..1dbf0bb7 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsServerScreen.kt @@ -0,0 +1,41 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.EditTextPref +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.base.prefs.SwitchPref +import ca.gosyer.ui.base.prefs.asStateIn +import ca.gosyer.ui.base.vm.ViewModel +import ca.gosyer.ui.base.vm.viewModel +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack +import javax.inject.Inject + +class SettingsServerViewModel @Inject constructor( + private val serverPreferences: ServerPreferences +): ViewModel() { + val host = serverPreferences.host().asStateIn(scope) + val serverUrl = serverPreferences.server().asStateIn(scope) +} + +@Composable +fun SettingsServerScreen(navController: BackStack) { + val vm = viewModel() + Column { + Toolbar("Server Settings", navController, true) + SwitchPref(preference = vm.host, title = "Host server inside TachideskJUI") + PreferencesScrollableColumn { + EditTextPref(vm.serverUrl, "Server Url", subtitle = vm.serverUrl.collectAsState().value) + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/settings/SettingsTrackingScreen.kt b/src/main/kotlin/ca/gosyer/ui/settings/SettingsTrackingScreen.kt new file mode 100644 index 00000000..f8ad0fb0 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/ui/settings/SettingsTrackingScreen.kt @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.ui.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import ca.gosyer.ui.base.components.Toolbar +import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn +import ca.gosyer.ui.main.Route +import com.github.zsoltk.compose.router.BackStack + +@Composable +fun SettingsTrackingScreen(navController: BackStack) { + Column { + Toolbar("Tracking Settings", navController, true) + PreferencesScrollableColumn { + } + } +} diff --git a/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt b/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt index 34741b8c..bd7b1bba 100644 --- a/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt +++ b/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenu.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.Card import androidx.compose.material.Icon import androidx.compose.material.Surface import androidx.compose.material.icons.Icons @@ -33,6 +32,7 @@ import ca.gosyer.ui.manga.openMangaMenu import ca.gosyer.ui.sources.components.SourceHomeScreen import ca.gosyer.ui.sources.components.SourceScreen import ca.gosyer.util.compose.ThemedWindow +import com.github.zsoltk.compose.savedinstancestate.Bundle import com.github.zsoltk.compose.savedinstancestate.BundleScope fun openSourcesMenu() { @@ -43,9 +43,20 @@ fun openSourcesMenu() { } } +private const val SOURCE_MENU_KEY = "source_menu" + @Composable fun SourcesMenu(onMangaClick: (Long) -> Unit) { - val vm = viewModel() + BundleScope(SOURCE_MENU_KEY, autoDispose = false) { + SourcesMenu(it, onMangaClick) + } +} + +@Composable +fun SourcesMenu(bundle: Bundle, onMangaClick: (Long) -> Unit) { + val vm = viewModel { + bundle + } val isLoading by vm.isLoading.collectAsState() val sources by vm.sources.collectAsState() val sourceTabs by vm.sourceTabs.collectAsState() @@ -56,27 +67,28 @@ fun SourcesMenu(onMangaClick: (Long) -> Unit) { Column { Toolbar(selectedSourceTab?.name ?: "Sources", closable = false) Row { - LazyColumn(Modifier.fillMaxHeight().width(64.dp)) { - items(sourceTabs) { source -> - Card( - Modifier + Surface(elevation = 1.dp) { + LazyColumn(Modifier.fillMaxHeight().width(64.dp)) { + items(sourceTabs) { source -> + val modifier = Modifier .clickable { vm.selectTab(source) } .requiredHeight(64.dp) - .requiredWidth(64.dp), - ) { + .requiredWidth(64.dp) + if (source != null) { - KtorImage(source.iconUrl(serverUrl),) + KtorImage(source.iconUrl(serverUrl), imageModifier = modifier) } else { - Icon(Icons.Default.Home, "Home") + Icon(Icons.Default.Home, "Home", modifier = modifier) } } } } + val selectedSource: Source? = selectedSourceTab - BundleScope(selectedSource?.name ?: "home") { + BundleScope("Sources") { if (selectedSource != null) { SourceScreen(selectedSource, onMangaClick) } else { diff --git a/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenuViewModel.kt b/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenuViewModel.kt index 1e8dd680..272f5bd4 100644 --- a/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenuViewModel.kt +++ b/src/main/kotlin/ca/gosyer/ui/sources/SourcesMenuViewModel.kt @@ -11,14 +11,19 @@ import ca.gosyer.data.models.Source import ca.gosyer.data.server.ServerPreferences import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.ui.base.vm.ViewModel +import com.github.zsoltk.compose.savedinstancestate.Bundle import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import mu.KotlinLogging import javax.inject.Inject class SourcesMenuViewModel @Inject constructor( + private val bundle: Bundle, private val sourceHandler: SourceInteractionHandler, serverPreferences: ServerPreferences, catalogPreferences: CatalogPreferences @@ -42,6 +47,22 @@ class SourcesMenuViewModel @Inject constructor( val selectedSourceTab = _selectedSourceTab.asStateFlow() init { + _sourceTabs.drop(1) + .onEach { sources -> + bundle.putLongArray(SOURCE_TABS_KEY, sources.mapNotNull { it?.id }.toLongArray()) + } + .launchIn(scope) + + _selectedSourceTab.drop(1) + .onEach { + if (it != null) { + bundle.putLong(SELECTED_SOURCE_TAB, it.id) + } else { + bundle.remove(SELECTED_SOURCE_TAB) + } + } + .launchIn(scope) + getSources() } @@ -55,6 +76,18 @@ class SourcesMenuViewModel @Inject constructor( } catch (e: Exception) { if (e is CancellationException) throw e } finally { + val sourceTabs = bundle.getLongArray(SOURCE_TABS_KEY) + if (sourceTabs != null) { + _sourceTabs.value = listOf(null) + sourceTabs.toList() + .mapNotNull { sourceId -> + _sources.value.find { it.id == sourceId } + } + _selectedSourceTab.value = bundle.getLong(SELECTED_SOURCE_TAB, -1).let { id -> + if (id != -1L) { + _sources.value.find { it.id == id } + } else null + } + } _isLoading.value = false } } @@ -77,4 +110,9 @@ class SourcesMenuViewModel @Inject constructor( _selectedSourceTab.value = null } } + + companion object { + const val SOURCE_TABS_KEY = "source_tabs" + const val SELECTED_SOURCE_TAB = "selected_tab" + } } \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreen.kt b/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreen.kt index 3b2d4e9d..92e2b173 100644 --- a/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreen.kt +++ b/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreen.kt @@ -26,15 +26,23 @@ import ca.gosyer.data.models.Source import ca.gosyer.ui.base.components.LoadingScreen import ca.gosyer.ui.base.components.MangaGridItem import ca.gosyer.ui.base.vm.viewModel +import com.github.zsoltk.compose.savedinstancestate.Bundle +import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState @Composable fun SourceScreen( source: Source, onMangaClick: (Long) -> Unit ) { + val upstream = LocalSavedInstanceState.current + val vm = viewModel() - remember(source) { - vm.init(source) + val bundle = remember(source.id) { + upstream.getBundle(source.id.toString()) + ?: Bundle().also { upstream.putBundle(source.id.toString(), it) } + } + remember(source.id) { + vm.init(source, bundle) } val mangas by vm.mangas.collectAsState() val hasNextPage by vm.hasNextPage.collectAsState() diff --git a/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreenViewModel.kt b/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreenViewModel.kt index 08190be7..e400716a 100644 --- a/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreenViewModel.kt +++ b/src/main/kotlin/ca/gosyer/ui/sources/components/SourceScreenViewModel.kt @@ -10,18 +10,27 @@ import ca.gosyer.data.models.Manga import ca.gosyer.data.models.MangaPage import ca.gosyer.data.models.Source import ca.gosyer.data.server.ServerPreferences +import ca.gosyer.data.server.interactions.MangaInteractionHandler import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.ui.base.vm.ViewModel +import ca.gosyer.util.compose.getJsonObjectArray +import ca.gosyer.util.compose.putJsonObjectArray +import com.github.zsoltk.compose.savedinstancestate.Bundle import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject class SourceScreenViewModel @Inject constructor( private val sourceHandler: SourceInteractionHandler, + private val mangaHandler: MangaInteractionHandler, serverPreferences: ServerPreferences ): ViewModel() { private lateinit var source: Source + private lateinit var bundle: Bundle val serverUrl = serverPreferences.server().stateIn(scope) @@ -40,15 +49,38 @@ class SourceScreenViewModel @Inject constructor( private val _pageNum = MutableStateFlow(1) val pageNum = _pageNum.asStateFlow() - fun init(source: Source) { + init { + _mangas.drop(1) + .onEach { manga -> + bundle.putJsonObjectArray(MANGAS_KEY, manga) + } + .launchIn(scope) + _hasNextPage.drop(1) + .onEach { + bundle.putBoolean(NEXT_PAGE_KEY, it) + } + .launchIn(scope) + _pageNum.drop(1) + .onEach { + bundle.putInt(PAGE_NUM_KEY, it) + } + .launchIn(scope) + } + + fun init(source: Source, bundle: Bundle) { this.source = source + this.bundle = bundle scope.launch { _loading.value = true _mangas.value = emptyList() _hasNextPage.value = false - _pageNum.value = 1 - _isLatest.value = source.supportsLatest - val page = getPage() + _pageNum.value = bundle.getInt(PAGE_NUM_KEY, 1) + _isLatest.value = bundle.getBoolean(IS_LATEST_KEY, source.supportsLatest) + val page = bundle.getJsonObjectArray(MANGAS_KEY) + ?.let { + MangaPage(it.filterNotNull(), bundle.getBoolean(NEXT_PAGE_KEY, true)) + } + ?: getPage() _mangas.value += page.mangaList _hasNextPage.value = page.hasNextPage _loading.value = false @@ -69,7 +101,11 @@ class SourceScreenViewModel @Inject constructor( fun setMode(toLatest: Boolean) { if (isLatest.value != toLatest){ _isLatest.value = toLatest - init(source) + bundle.remove(MANGAS_KEY) + bundle.remove(NEXT_PAGE_KEY) + bundle.remove(PAGE_NUM_KEY) + bundle.remove(IS_LATEST_KEY) + init(source, bundle) } } @@ -80,4 +116,11 @@ class SourceScreenViewModel @Inject constructor( sourceHandler.getPopularManga(source, pageNum.value) } } + + companion object { + const val MANGAS_KEY = "mangas" + const val NEXT_PAGE_KEY = "next_page" + const val PAGE_NUM_KEY = "next_page" + const val IS_LATEST_KEY = "is_latest" + } } \ No newline at end of file diff --git a/src/main/kotlin/ca/gosyer/util/compose/Bundle.kt b/src/main/kotlin/ca/gosyer/util/compose/Bundle.kt new file mode 100644 index 00000000..e90ed3e3 --- /dev/null +++ b/src/main/kotlin/ca/gosyer/util/compose/Bundle.kt @@ -0,0 +1,28 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package ca.gosyer.util.compose + +import com.github.zsoltk.compose.savedinstancestate.Bundle +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +inline fun Bundle.putJsonObject(key: String, item: T) { + putString(key, Json.encodeToString(item)) +} + +inline fun Bundle.getJsonObject(key: String): T? { + return getString(key)?.let { Json.decodeFromString(it) } +} + +inline fun Bundle.putJsonObjectArray(key: String, items: T) { + putString(key, Json.encodeToString(items)) +} + +inline fun Bundle.getJsonObjectArray(key: String): List? { + return getString(key)?.let { Json.decodeFromString(it) } +}