Rewrite navigation to use Multiplatform Voyager Navigation

This commit is contained in:
Syer10
2022-01-30 20:47:37 -05:00
parent 3f1fc1cba0
commit d1481734e8
94 changed files with 1862 additions and 1463 deletions

View File

@@ -8,4 +8,4 @@ package ca.gosyer.core.prefs
expect class PreferenceStoreFactory() { expect class PreferenceStoreFactory() {
fun create(vararg names: String): PreferenceStore fun create(vararg names: String): PreferenceStore
} }

View File

@@ -29,7 +29,8 @@ dependencies {
implementation(compose.uiTooling) implementation(compose.uiTooling)
implementation(compose.materialIconsExtended) implementation(compose.materialIconsExtended)
implementation(compose("org.jetbrains.compose.ui:ui-util")) implementation(compose("org.jetbrains.compose.ui:ui-util"))
implementation(libs.composeRouter) implementation(libs.voyagerCore)
implementation(libs.voyagerNavigation)
implementation(libs.accompanistPager) implementation(libs.accompanistPager)
implementation(libs.accompanistFlowLayout) implementation(libs.accompanistFlowLayout)
implementation(libs.kamel) implementation(libs.kamel)

View File

@@ -14,7 +14,6 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.configureSwingGlobalsForCompose import androidx.compose.ui.configureSwingGlobalsForCompose
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.KeyEventType
@@ -35,19 +34,16 @@ import ca.gosyer.i18n.MR
import ca.gosyer.ui.AppComponent import ca.gosyer.ui.AppComponent
import ca.gosyer.ui.base.WindowDialog import ca.gosyer.ui.base.WindowDialog
import ca.gosyer.ui.base.theme.AppTheme import ca.gosyer.ui.base.theme.AppTheme
import ca.gosyer.ui.main.DebugOverlay
import ca.gosyer.ui.main.MainMenu import ca.gosyer.ui.main.MainMenu
import ca.gosyer.ui.main.components.DebugOverlay
import ca.gosyer.ui.main.components.Tray import ca.gosyer.ui.main.components.Tray
import ca.gosyer.ui.util.compose.WindowGet
import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.prefs.asStateIn import ca.gosyer.uicore.prefs.asStateIn
import ca.gosyer.util.compose.WindowGet import ca.gosyer.uicore.resources.stringResource
import com.github.weisj.darklaf.LafManager import com.github.weisj.darklaf.LafManager
import com.github.weisj.darklaf.theme.DarculaTheme import com.github.weisj.darklaf.theme.DarculaTheme
import com.github.weisj.darklaf.theme.IntelliJTheme import com.github.weisj.darklaf.theme.IntelliJTheme
import com.github.zsoltk.compose.backpress.BackPressHandler
import com.github.zsoltk.compose.backpress.LocalBackPressHandler
import com.github.zsoltk.compose.savedinstancestate.Bundle
import ca.gosyer.uicore.resources.stringResource
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -124,10 +120,6 @@ suspend fun main() {
exitProcess(0) exitProcess(0)
} }
} }
val backPressHandler = remember { BackPressHandler() }
val rootBundle = remember { Bundle() }
val windowState = rememberWindowState( val windowState = rememberWindowState(
size = size, size = size,
position = position, position = position,
@@ -158,7 +150,8 @@ suspend fun main() {
if (it.type == KeyEventType.KeyUp) { if (it.type == KeyEventType.KeyUp) {
when (it.key) { when (it.key) {
Key.Home -> { Key.Home -> {
backPressHandler.handle() // backPressHandler.handle()
false
} }
Key.F3 -> { Key.F3 -> {
displayDebugInfoFlow.value = !displayDebugInfoFlow.value displayDebugInfoFlow.value = !displayDebugInfoFlow.value
@@ -170,29 +163,25 @@ suspend fun main() {
} }
) { ) {
AppTheme { AppTheme {
CompositionLocalProvider( Crossfade(serverService.initialized.collectAsState().value) { initialized ->
LocalBackPressHandler provides backPressHandler, when (initialized) {
) { ServerResult.STARTED, ServerResult.UNUSED -> {
Crossfade(serverService.initialized.collectAsState().value) { initialized -> Box {
when (initialized) { MainMenu()
ServerResult.STARTED, ServerResult.UNUSED -> { val displayDebugInfo by displayDebugInfoFlow.collectAsState()
Box { if (displayDebugInfo) {
MainMenu(rootBundle) DebugOverlay()
val displayDebugInfo by displayDebugInfoFlow.collectAsState()
if (displayDebugInfo) {
DebugOverlay()
}
} }
} }
ServerResult.STARTING, ServerResult.FAILED -> { }
Surface { ServerResult.STARTING, ServerResult.FAILED -> {
LoadingScreen( Surface {
initialized == ServerResult.STARTING, LoadingScreen(
errorMessage = stringResource(MR.strings.unable_to_start_server), initialized == ServerResult.STARTING,
retryMessage = stringResource(MR.strings.action_start_anyway), errorMessage = stringResource(MR.strings.unable_to_start_server),
retry = serverService::startAnyway retryMessage = stringResource(MR.strings.action_start_anyway),
) retry = serverService::startAnyway
} )
} }
} }
} }

View File

@@ -8,7 +8,7 @@ json = "1.3.2"
xmlUtil = "0.84.0" xmlUtil = "0.84.0"
# Compose # Compose
composeRouter = "0.24.2-jetbrains-2" voyager = "1.0.0-beta15"
accompanist = "0.18.1" accompanist = "0.18.1"
kamel = "0.3.0" kamel = "0.3.0"
@@ -52,7 +52,8 @@ xmlUtilCore = { module = "io.github.pdvrieze.xmlutil:core", version.ref = "xmlUt
xmlUtilSerialization = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "xmlUtil" } xmlUtilSerialization = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "xmlUtil" }
# Compose # Compose
composeRouter = { module = "ca.gosyer:compose-router", version.ref = "composeRouter" } voyagerCore = { module = "cafe.adriel.voyager:voyager-core", version.ref = "voyager" }
voyagerNavigation = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
accompanistPager = { module = "ca.gosyer:accompanist-pager", version.ref = "accompanist" } accompanistPager = { module = "ca.gosyer:accompanist-pager", version.ref = "accompanist" }
accompanistFlowLayout = { module = "ca.gosyer:accompanist-flowlayout", version.ref = "accompanist" } accompanistFlowLayout = { module = "ca.gosyer:accompanist-flowlayout", version.ref = "accompanist" }
kamel = { module = "com.alialbaali.kamel:kamel-image", version.ref = "kamel" } kamel = { module = "com.alialbaali.kamel:kamel-image", version.ref = "kamel" }

View File

@@ -23,6 +23,7 @@ kotlin {
all { all {
languageSettings { languageSettings {
optIn("kotlin.RequiresOptIn") optIn("kotlin.RequiresOptIn")
optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
optIn("com.google.accompanist.pager.ExperimentalPagerApi") optIn("com.google.accompanist.pager.ExperimentalPagerApi")
optIn("androidx.compose.foundation.ExperimentalFoundationApi") optIn("androidx.compose.foundation.ExperimentalFoundationApi")
optIn("androidx.compose.material.ExperimentalMaterialApi") optIn("androidx.compose.material.ExperimentalMaterialApi")
@@ -35,6 +36,8 @@ kotlin {
api(kotlin("stdlib-common")) api(kotlin("stdlib-common"))
api(libs.coroutinesCore) api(libs.coroutinesCore)
api(libs.kamel) api(libs.kamel)
api(libs.voyagerCore)
api(libs.voyagerNavigation)
api(project(":core")) api(project(":core"))
api(project(":i18n")) api(project(":i18n"))
api(project(":data")) api(project(":data"))
@@ -60,7 +63,6 @@ kotlin {
api(libs.coroutinesSwing) api(libs.coroutinesSwing)
api(libs.accompanistPager) api(libs.accompanistPager)
api(libs.accompanistFlowLayout) api(libs.accompanistFlowLayout)
api(libs.composeRouter)
api(libs.krokiCoroutines) api(libs.krokiCoroutines)
} }
} }

View File

@@ -36,7 +36,7 @@ import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import ca.gosyer.ui.AppComponent import ca.gosyer.ui.AppComponent
import ca.gosyer.ui.base.theme.AppTheme import ca.gosyer.ui.base.theme.AppTheme
import ca.gosyer.util.lang.launchApplication import ca.gosyer.ui.util.lang.launchApplication
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)

View File

@@ -12,14 +12,11 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import ca.gosyer.ui.main.Routes
import com.github.zsoltk.compose.router.BackStack
val LocalMenuController = val LocalDisplayController =
compositionLocalOf<MenuController?> { null } compositionLocalOf<DisplayController?> { null }
class MenuController( class DisplayController(
val backStack: BackStack<Routes>,
private val _sideMenuVisible: MutableState<Boolean> = mutableStateOf(true), private val _sideMenuVisible: MutableState<Boolean> = mutableStateOf(true),
private val _isDrawer: MutableState<Boolean> = mutableStateOf(false), private val _isDrawer: MutableState<Boolean> = mutableStateOf(false),
) { ) {
@@ -38,25 +35,12 @@ class MenuController(
fun setAsNotDrawer() { fun setAsNotDrawer() {
_isDrawer.value = false _isDrawer.value = false
} }
fun push(route: Routes) {
backStack.push(route)
if (isDrawer) {
closeSideMenu()
}
}
fun newRoot(route: Routes) {
backStack.newRoot(route)
if (isDrawer) {
closeSideMenu()
}
}
} }
@Composable @Composable
fun withMenuController(controller: MenuController, content: @Composable () -> Unit) { fun withDisplayController(controller: DisplayController, content: @Composable () -> Unit) {
CompositionLocalProvider( CompositionLocalProvider(
LocalMenuController provides controller, LocalDisplayController provides controller,
content = content content = content
) )
} }

View File

@@ -66,13 +66,16 @@ import androidx.compose.ui.unit.sp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.uicore.components.BoxWithTooltipSurface import ca.gosyer.uicore.components.BoxWithTooltipSurface
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
@Composable @Composable
fun Toolbar( fun Toolbar(
name: String, name: String,
menuController: MenuController? = LocalMenuController.current, displayController: DisplayController? = LocalDisplayController.current,
closable: Boolean, navigator: Navigator? = LocalNavigator.current,
onClose: () -> Unit = { menuController?.backStack?.pop() }, closable: Boolean = (navigator?.size ?: 0) > 1,
onClose: () -> Unit = { navigator?.pop() },
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
actions: @Composable RowScope.() -> Unit = {}, actions: @Composable RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.surface, // CustomColors.current.bars, backgroundColor: Color = MaterialTheme.colors.surface, // CustomColors.current.bars,
@@ -98,14 +101,14 @@ fun Toolbar(
Modifier.fillMaxHeight().animateContentSize(), Modifier.fillMaxHeight().animateContentSize(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (menuController != null) { if (displayController != null) {
if (menuController.isDrawer) { if (displayController.isDrawer) {
ActionIcon(menuController::openSideMenu, "Open nav", Icons.Rounded.Menu) ActionIcon(displayController::openSideMenu, "Open nav", Icons.Rounded.Menu)
} else { } else {
AnimatedVisibility( AnimatedVisibility(
!menuController.sideMenuVisible !displayController.sideMenuVisible
) { ) {
ActionIcon(menuController::openSideMenu, "Open nav", Icons.Rounded.Sort) ActionIcon(displayController::openSideMenu, "Open nav", Icons.Rounded.Sort)
} }
} }
} }

View File

@@ -25,9 +25,10 @@ import ca.gosyer.data.ui.UiPreferences
import ca.gosyer.data.ui.model.ThemeMode import ca.gosyer.data.ui.model.ThemeMode
import ca.gosyer.uicore.theme.Theme import ca.gosyer.uicore.theme.Theme
import ca.gosyer.uicore.theme.themes import ca.gosyer.uicore.theme.themes
import ca.gosyer.uicore.vm.LocalViewModelFactory
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.uicore.vm.viewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.cancelChildren
@@ -39,7 +40,8 @@ import me.tatarka.inject.annotations.Inject
*/ */
@Composable @Composable
fun AppTheme(content: @Composable () -> Unit) { fun AppTheme(content: @Composable () -> Unit) {
val vm = viewModel<AppThemeViewModel>() val vmFactory = LocalViewModelFactory.current
val vm = remember { vmFactory.instantiate<AppThemeViewModel>() }
val colors = vm.getColors() val colors = vm.getColors()
/*val systemUiController = rememberSystemUiController()*/ /*val systemUiController = rememberSystemUiController()*/
@@ -61,6 +63,8 @@ fun AppTheme(content: @Composable () -> Unit) {
class AppThemeViewModel @Inject constructor( class AppThemeViewModel @Inject constructor(
private val uiPreferences: UiPreferences private val uiPreferences: UiPreferences
) : ViewModel() { ) : ViewModel() {
override val scope = MainScope()
private val themeMode = uiPreferences.themeMode().asStateFlow() private val themeMode = uiPreferences.themeMode().asStateFlow()
private val lightTheme = uiPreferences.lightTheme().asStateFlow() private val lightTheme = uiPreferences.lightTheme().asStateFlow()
private val darkTheme = uiPreferences.darkTheme().asStateFlow() private val darkTheme = uiPreferences.darkTheme().asStateFlow()
@@ -130,7 +134,8 @@ class AppThemeViewModel @Inject constructor(
) )
} }
override fun onDestroy() { override fun onDispose() {
baseThemeScope.cancel() baseThemeScope.cancel()
scope.cancel()
} }
} }

View File

@@ -7,14 +7,14 @@
package ca.gosyer.ui.base.vm package ca.gosyer.ui.base.vm
import ca.gosyer.ui.base.theme.AppThemeViewModel import ca.gosyer.ui.base.theme.AppThemeViewModel
import ca.gosyer.ui.categories.CategoriesMenuViewModel import ca.gosyer.ui.categories.CategoriesScreenViewModel
import ca.gosyer.ui.downloads.DownloadsMenuViewModel import ca.gosyer.ui.downloads.DownloadsScreenViewModel
import ca.gosyer.ui.extensions.ExtensionsMenuViewModel import ca.gosyer.ui.extensions.ExtensionsScreenViewModel
import ca.gosyer.ui.library.LibraryScreenViewModel import ca.gosyer.ui.library.LibraryScreenViewModel
import ca.gosyer.ui.main.MainViewModel import ca.gosyer.ui.main.MainViewModel
import ca.gosyer.ui.main.components.DebugOverlayViewModel import ca.gosyer.ui.main.components.DebugOverlayViewModel
import ca.gosyer.ui.main.components.TrayViewModel import ca.gosyer.ui.main.components.TrayViewModel
import ca.gosyer.ui.manga.MangaMenuViewModel import ca.gosyer.ui.manga.MangaScreenViewModel
import ca.gosyer.ui.reader.ReaderMenuViewModel import ca.gosyer.ui.reader.ReaderMenuViewModel
import ca.gosyer.ui.settings.SettingsAdvancedViewModel import ca.gosyer.ui.settings.SettingsAdvancedViewModel
import ca.gosyer.ui.settings.SettingsBackupViewModel import ca.gosyer.ui.settings.SettingsBackupViewModel
@@ -23,29 +23,28 @@ import ca.gosyer.ui.settings.SettingsLibraryViewModel
import ca.gosyer.ui.settings.SettingsReaderViewModel import ca.gosyer.ui.settings.SettingsReaderViewModel
import ca.gosyer.ui.settings.SettingsServerViewModel import ca.gosyer.ui.settings.SettingsServerViewModel
import ca.gosyer.ui.settings.ThemesViewModel import ca.gosyer.ui.settings.ThemesViewModel
import ca.gosyer.ui.sources.SourcesMenuViewModel import ca.gosyer.ui.sources.SourcesScreenViewModel
import ca.gosyer.ui.sources.components.SourceHomeScreenViewModel import ca.gosyer.ui.sources.browse.SourceScreenViewModel
import ca.gosyer.ui.sources.components.SourceScreenViewModel import ca.gosyer.ui.sources.browse.filter.SourceFiltersViewModel
import ca.gosyer.ui.sources.components.filter.SourceFiltersViewModel import ca.gosyer.ui.sources.home.SourceHomeScreenViewModel
import ca.gosyer.ui.sources.settings.SourceSettingsViewModel import ca.gosyer.ui.sources.settings.SourceSettingsScreenViewModel
import ca.gosyer.ui.updates.UpdatesMenuViewModel import ca.gosyer.ui.updates.UpdatesScreenViewModel
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.uicore.vm.ViewModelFactory import ca.gosyer.uicore.vm.ViewModelFactory
import com.github.zsoltk.compose.savedinstancestate.Bundle
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import kotlin.reflect.KClass import kotlin.reflect.KClass
@Inject @Inject
class ViewModelFactoryImpl( class ViewModelFactoryImpl(
private val appThemeFactory: () -> AppThemeViewModel, private val appThemeFactory: () -> AppThemeViewModel,
private val categoryFactory: () -> CategoriesMenuViewModel, private val categoryFactory: () -> CategoriesScreenViewModel,
private val downloadsFactory: () -> DownloadsMenuViewModel, private val downloadsFactory: (Boolean) -> DownloadsScreenViewModel,
private val extensionsFactory: () -> ExtensionsMenuViewModel, private val extensionsFactory: () -> ExtensionsScreenViewModel,
private val libraryFactory: (bundle: Bundle) -> LibraryScreenViewModel, private val libraryFactory: () -> LibraryScreenViewModel,
private val debugOverlayFactory: () -> DebugOverlayViewModel, private val debugOverlayFactory: () -> DebugOverlayViewModel,
private val trayFactory: () -> TrayViewModel, private val trayFactory: () -> TrayViewModel,
private val mainFactory: () -> MainViewModel, private val mainFactory: () -> MainViewModel,
private val mangaFactory: (params: MangaMenuViewModel.Params) -> MangaMenuViewModel, private val mangaFactory: (params: MangaScreenViewModel.Params) -> MangaScreenViewModel,
private val readerFactory: (params: ReaderMenuViewModel.Params) -> ReaderMenuViewModel, private val readerFactory: (params: ReaderMenuViewModel.Params) -> ReaderMenuViewModel,
private val settingsAdvancedFactory: () -> SettingsAdvancedViewModel, private val settingsAdvancedFactory: () -> SettingsAdvancedViewModel,
private val themesFactory: () -> ThemesViewModel, private val themesFactory: () -> ThemesViewModel,
@@ -55,25 +54,25 @@ class ViewModelFactoryImpl(
private val settingsReaderFactory: () -> SettingsReaderViewModel, private val settingsReaderFactory: () -> SettingsReaderViewModel,
private val settingsServerFactory: () -> SettingsServerViewModel, private val settingsServerFactory: () -> SettingsServerViewModel,
private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel, private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel,
private val sourceSettingsFactory: (params: SourceSettingsViewModel.Params) -> SourceSettingsViewModel, private val sourceSettingsFactory: (params: SourceSettingsScreenViewModel.Params) -> SourceSettingsScreenViewModel,
private val sourceHomeFactory: (bundle: Bundle) -> SourceHomeScreenViewModel, private val sourceHomeFactory: () -> SourceHomeScreenViewModel,
private val sourceFactory: (params: SourceScreenViewModel.Params) -> SourceScreenViewModel, private val sourceFactory: (params: SourceScreenViewModel.Params) -> SourceScreenViewModel,
private val sourcesFactory: (bundle: Bundle) -> SourcesMenuViewModel, private val sourcesFactory: () -> SourcesScreenViewModel,
private val updatesFactory: () -> UpdatesMenuViewModel private val updatesFactory: () -> UpdatesScreenViewModel
): ViewModelFactory() { ) : ViewModelFactory() {
override fun <VM : ViewModel> instantiate(klass: KClass<VM>, arg1: Any?): VM { override fun <VM : ViewModel> instantiate(klass: KClass<VM>, arg1: Any?): VM {
@Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY") @Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
return when (klass) { return when (klass) {
AppThemeViewModel::class -> appThemeFactory() AppThemeViewModel::class -> appThemeFactory()
CategoriesMenuViewModel::class -> categoryFactory() CategoriesScreenViewModel::class -> categoryFactory()
DownloadsMenuViewModel::class -> downloadsFactory() DownloadsScreenViewModel::class -> downloadsFactory(arg1 as Boolean)
ExtensionsMenuViewModel::class -> extensionsFactory() ExtensionsScreenViewModel::class -> extensionsFactory()
LibraryScreenViewModel::class -> libraryFactory(arg1 as Bundle) LibraryScreenViewModel::class -> libraryFactory()
DebugOverlayViewModel::class -> debugOverlayFactory() DebugOverlayViewModel::class -> debugOverlayFactory()
TrayViewModel::class -> trayFactory() TrayViewModel::class -> trayFactory()
MainViewModel::class -> mainFactory() MainViewModel::class -> mainFactory()
MangaMenuViewModel::class -> mangaFactory(arg1 as MangaMenuViewModel.Params) MangaScreenViewModel::class -> mangaFactory(arg1 as MangaScreenViewModel.Params)
ReaderMenuViewModel::class -> readerFactory(arg1 as ReaderMenuViewModel.Params) ReaderMenuViewModel::class -> readerFactory(arg1 as ReaderMenuViewModel.Params)
SettingsAdvancedViewModel::class -> settingsAdvancedFactory() SettingsAdvancedViewModel::class -> settingsAdvancedFactory()
ThemesViewModel::class -> themesFactory() ThemesViewModel::class -> themesFactory()
@@ -83,11 +82,11 @@ class ViewModelFactoryImpl(
SettingsReaderViewModel::class -> settingsReaderFactory() SettingsReaderViewModel::class -> settingsReaderFactory()
SettingsServerViewModel::class -> settingsServerFactory() SettingsServerViewModel::class -> settingsServerFactory()
SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params) SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params)
SourceSettingsViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsViewModel.Params) SourceSettingsScreenViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsScreenViewModel.Params)
SourceHomeScreenViewModel::class -> sourceHomeFactory(arg1 as Bundle) SourceHomeScreenViewModel::class -> sourceHomeFactory()
SourceScreenViewModel::class -> sourceFactory(arg1 as SourceScreenViewModel.Params) SourceScreenViewModel::class -> sourceFactory(arg1 as SourceScreenViewModel.Params)
SourcesMenuViewModel::class -> sourcesFactory(arg1 as Bundle) SourcesScreenViewModel::class -> sourcesFactory()
UpdatesMenuViewModel::class -> updatesFactory() UpdatesScreenViewModel::class -> updatesFactory()
else -> throw IllegalArgumentException("Unknown ViewModel $klass") else -> throw IllegalArgumentException("Unknown ViewModel $klass")
} as VM } as VM
} }

View File

@@ -0,0 +1,56 @@
/*
* 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.categories
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.categories.components.CategoriesScreenContent
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.Navigator
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openCategoriesMenu(notifyFinished: (() -> Unit)? = null) {
launchApplication {
ThemedWindow(
::exitApplication,
title = "${BuildKonfig.NAME} - Categories"
) {
Navigator(remember { CategoriesScreen(notifyFinished) })
}
}
}
class CategoriesScreen(
@Transient
private val notifyFinished: (() -> Unit)? = null
) : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<CategoriesScreenViewModel>()
CategoriesScreenContent(
categories = vm.categories.collectAsState().value,
updateRemoteCategories = vm::updateRemoteCategories,
moveCategoryUp = vm::moveUp,
moveCategoryDown = vm::moveDown,
renameCategory = vm::renameCategory,
deleteCategory = vm::deleteCategory,
createCategory = vm::createCategory,
notifyFinished = notifyFinished
)
}
}

View File

@@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class CategoriesMenuViewModel @Inject constructor( class CategoriesScreenViewModel @Inject constructor(
private val categoryHandler: CategoryInteractionHandler private val categoryHandler: CategoryInteractionHandler
) : ViewModel() { ) : ViewModel() {
private var originalCategories = emptyList<Category>() private var originalCategories = emptyList<Category>()

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.categories package ca.gosyer.ui.categories.components
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextField import androidx.compose.material.TextField
@@ -14,11 +14,12 @@ import androidx.compose.ui.text.input.TextFieldValue
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.base.WindowDialog import ca.gosyer.ui.base.WindowDialog
import ca.gosyer.ui.categories.CategoriesScreenViewModel
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
fun openRenameDialog( fun openRenameDialog(
category: CategoriesMenuViewModel.MenuCategory, category: CategoriesScreenViewModel.MenuCategory,
onRename: (String) -> Unit onRename: (String) -> Unit
) { ) {
val newName = MutableStateFlow(TextFieldValue(category.name)) val newName = MutableStateFlow(TextFieldValue(category.name))
@@ -44,8 +45,8 @@ fun openRenameDialog(
} }
fun openDeleteDialog( fun openDeleteDialog(
category: CategoriesMenuViewModel.MenuCategory, category: CategoriesScreenViewModel.MenuCategory,
onDelete: (CategoriesMenuViewModel.MenuCategory) -> Unit onDelete: (CategoriesScreenViewModel.MenuCategory) -> Unit
) { ) {
WindowDialog( WindowDialog(
title = "${BuildKonfig.NAME} - Categories - Delete Dialog", title = "${BuildKonfig.NAME} - Categories - Delete Dialog",

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.categories package ca.gosyer.ui.categories.components
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -40,16 +40,11 @@ import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.ui.categories.CategoriesScreenViewModel.MenuCategory
import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.util.compose.ThemedWindow
import ca.gosyer.util.lang.launchApplication
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
@@ -57,23 +52,18 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mu.KotlinLogging import mu.KotlinLogging
@OptIn(DelicateCoroutinesApi::class)
fun openCategoriesMenu(notifyFinished: (() -> Unit)? = null) {
launchApplication {
ThemedWindow(
::exitApplication,
title = "${BuildKonfig.NAME} - Categories"
) {
CategoriesMenu(notifyFinished)
}
}
}
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
@Composable @Composable
fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) { fun CategoriesScreenContent(
val vm = viewModel<CategoriesMenuViewModel>() categories: List<MenuCategory>,
val categories by vm.categories.collectAsState() updateRemoteCategories: suspend () -> Unit,
moveCategoryUp: (MenuCategory) -> Unit,
moveCategoryDown: (MenuCategory) -> Unit,
renameCategory: (MenuCategory, String) -> Unit,
deleteCategory: (MenuCategory) -> Unit,
createCategory: (String) -> Unit,
notifyFinished: (() -> Unit)? = null
) {
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
val logger = KotlinLogging.logger {} val logger = KotlinLogging.logger {}
@@ -81,7 +71,7 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) {
logger.debug { throwable } logger.debug { throwable }
} }
GlobalScope.launch(handler) { GlobalScope.launch(handler) {
vm.updateRemoteCategories() updateRemoteCategories()
notifyFinished?.invoke() notifyFinished?.invoke()
} }
} }
@@ -96,16 +86,16 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) {
category = category, category = category,
moveUpEnabled = i != 0, moveUpEnabled = i != 0,
moveDownEnabled = i != categories.lastIndex, moveDownEnabled = i != categories.lastIndex,
onMoveUp = { vm.moveUp(category) }, onMoveUp = { moveCategoryUp(category) },
onMoveDown = { vm.moveDown(category) }, onMoveDown = { moveCategoryDown(category) },
onRename = { onRename = {
openRenameDialog(category) { openRenameDialog(category) {
vm.renameCategory(category, it) renameCategory(category, it)
} }
}, },
onDelete = { onDelete = {
openDeleteDialog(category) { openDeleteDialog(category) {
vm.deleteCategory(category) deleteCategory(category)
} }
}, },
) )
@@ -120,7 +110,7 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) {
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp), modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
onClick = { onClick = {
openCreateDialog { openCreateDialog {
vm.createCategory(it) createCategory(it)
} }
} }
) )
@@ -136,7 +126,7 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) {
@Composable @Composable
private fun CategoryRow( private fun CategoryRow(
category: CategoriesMenuViewModel.MenuCategory, category: MenuCategory,
moveUpEnabled: Boolean = true, moveUpEnabled: Boolean = true,
moveDownEnabled: Boolean = true, moveDownEnabled: Boolean = true,
onMoveUp: () -> Unit = {}, onMoveUp: () -> Unit = {},

View File

@@ -0,0 +1,58 @@
/*
* 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.downloads
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.downloads.components.DownloadsScreenContent
import ca.gosyer.ui.manga.MangaScreen
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openDownloadsMenu() {
launchApplication {
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
Surface {
Navigator(remember { DownloadsScreen() })
}
}
}
}
class DownloadsScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel {
instantiate<DownloadsScreenViewModel>(false)
}
val navigator = LocalNavigator.currentOrThrow
DownloadsScreenContent(
downloadQueue = vm.downloadQueue.collectAsState().value,
downloadStatus = vm.downloaderStatus.collectAsState().value,
startDownloading = vm::start,
pauseDownloading = vm::pause,
clearQueue = vm::clear,
onMangaClick = { navigator push MangaScreen(it) },
stopDownload = vm::stopDownload,
moveDownloadToBottom = vm::moveToBottom
)
}
}

View File

@@ -11,14 +11,24 @@ import ca.gosyer.data.models.Chapter
import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import ca.gosyer.data.server.interactions.DownloadInteractionHandler import ca.gosyer.data.server.interactions.DownloadInteractionHandler
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class DownloadsMenuViewModel @Inject constructor( class DownloadsScreenViewModel @Inject constructor(
private val downloadService: DownloadService, private val downloadService: DownloadService,
private val downloadsHandler: DownloadInteractionHandler, private val downloadsHandler: DownloadInteractionHandler,
private val chapterHandler: ChapterInteractionHandler private val chapterHandler: ChapterInteractionHandler,
standalone: Boolean
) : ViewModel() { ) : ViewModel() {
private val uiScope = if (standalone) {
MainScope()
} else null
override val scope: CoroutineScope
get() = uiScope ?: super.scope
val serviceStatus get() = downloadService.status val serviceStatus get() = downloadService.status
val downloaderStatus get() = downloadService.downloaderStatus val downloaderStatus get() = downloadService.downloaderStatus
val downloadQueue get() = downloadService.downloadQueue val downloadQueue get() = downloadService.downloadQueue

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.downloads package ca.gosyer.ui.downloads.components
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
@@ -28,7 +28,6 @@ import androidx.compose.material.Icon
import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProgressIndicatorDefaults import androidx.compose.material.ProgressIndicatorDefaults
import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ClearAll import androidx.compose.material.icons.rounded.ClearAll
@@ -36,7 +35,6 @@ import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.Pause import androidx.compose.material.icons.rounded.Pause
import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material.icons.rounded.PlayArrow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -48,10 +46,8 @@ import ca.gosyer.data.download.model.DownloadChapter
import ca.gosyer.data.download.model.DownloaderStatus import ca.gosyer.data.download.model.DownloaderStatus
import ca.gosyer.data.models.Chapter import ca.gosyer.data.models.Chapter
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.base.navigation.ActionIcon import ca.gosyer.ui.base.navigation.ActionIcon
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.manga.openMangaMenu
import ca.gosyer.uicore.components.DropdownIconButton import ca.gosyer.uicore.components.DropdownIconButton
import ca.gosyer.uicore.components.MangaListItem import ca.gosyer.uicore.components.MangaListItem
import ca.gosyer.uicore.components.MangaListItemColumn import ca.gosyer.uicore.components.MangaListItemColumn
@@ -59,41 +55,31 @@ import ca.gosyer.uicore.components.MangaListItemImage
import ca.gosyer.uicore.components.MangaListItemSubtitle import ca.gosyer.uicore.components.MangaListItemSubtitle
import ca.gosyer.uicore.components.MangaListItemTitle import ca.gosyer.uicore.components.MangaListItemTitle
import ca.gosyer.uicore.components.mangaAspectRatio import ca.gosyer.uicore.components.mangaAspectRatio
import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.util.compose.ThemedWindow
import ca.gosyer.util.lang.launchApplication
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import io.kamel.image.lazyPainterResource import io.kamel.image.lazyPainterResource
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openDownloadsMenu() {
launchApplication {
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
Surface {
DownloadsMenu(::openMangaMenu)
}
}
}
}
@Composable @Composable
fun DownloadsMenu(onMangaClick: (Long) -> Unit) { fun DownloadsScreenContent(
val vm = viewModel<DownloadsMenuViewModel>() downloadQueue: List<DownloadChapter>,
val downloadQueue by vm.downloadQueue.collectAsState() downloadStatus: DownloaderStatus,
startDownloading: () -> Unit,
pauseDownloading: () -> Unit,
clearQueue: () -> Unit,
onMangaClick: (Long) -> Unit,
stopDownload: (Chapter) -> Unit,
moveDownloadToBottom: (Chapter) -> Unit
) {
Column { Column {
Toolbar( Toolbar(
stringResource(MR.strings.location_downloads), stringResource(MR.strings.location_downloads),
closable = false, closable = false,
actions = { actions = {
val downloadStatus by vm.downloaderStatus.collectAsState()
if (downloadStatus == DownloaderStatus.Started) { if (downloadStatus == DownloaderStatus.Started) {
ActionIcon(onClick = vm::pause, stringResource(MR.strings.action_pause), Icons.Rounded.Pause) ActionIcon(onClick = pauseDownloading, stringResource(MR.strings.action_pause), Icons.Rounded.Pause)
} else { } else {
ActionIcon(onClick = vm::start, stringResource(MR.strings.action_continue), Icons.Rounded.PlayArrow) ActionIcon(onClick = startDownloading, stringResource(MR.strings.action_continue), Icons.Rounded.PlayArrow)
} }
ActionIcon(onClick = vm::clear, stringResource(MR.strings.action_clear_queue), Icons.Rounded.ClearAll) ActionIcon(onClick = clearQueue, stringResource(MR.strings.action_clear_queue), Icons.Rounded.ClearAll)
} }
) )
Box { Box {
@@ -103,8 +89,8 @@ fun DownloadsMenu(onMangaClick: (Long) -> Unit) {
DownloadsItem( DownloadsItem(
it, it,
{ onMangaClick(it.mangaId) }, { onMangaClick(it.mangaId) },
vm::stopDownload, stopDownload,
vm::moveToBottom moveDownloadToBottom
) )
} }
} }

View File

@@ -0,0 +1,60 @@
/*
* 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.extensions
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.rememberWindowState
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.extensions.components.ExtensionsScreenContent
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.Navigator
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openExtensionsMenu() {
launchApplication {
val state = rememberWindowState(size = DpSize(550.dp, 700.dp))
ThemedWindow(::exitApplication, state, title = BuildKonfig.NAME) {
Surface {
Navigator(remember { ExtensionsScreen() })
}
}
}
}
class ExtensionsScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<ExtensionsScreenViewModel>()
ExtensionsScreenContent(
extensions = vm.extensions.collectAsState().value,
isLoading = vm.isLoading.collectAsState().value,
query = vm.searchQuery.collectAsState().value,
setQuery = vm::search,
enabledLangs = vm.enabledLangs,
getSourceLanguages = vm::getSourceLanguages,
setEnabledLanguages = vm::setEnabledLanguages,
installExtension = vm::install,
updateExtension = vm::update,
uninstallExtension = vm::uninstall
)
}
}

View File

@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import java.util.Locale import java.util.Locale
class ExtensionsMenuViewModel @Inject constructor( class ExtensionsScreenViewModel @Inject constructor(
private val extensionHandler: ExtensionInteractionHandler, private val extensionHandler: ExtensionInteractionHandler,
extensionPreferences: ExtensionPreferences extensionPreferences: ExtensionPreferences
) : ViewModel() { ) : ViewModel() {

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.extensions package ca.gosyer.ui.extensions.components
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -26,7 +26,6 @@ import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.ContentAlpha import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Switch import androidx.compose.material.Switch
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -42,10 +41,8 @@ import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.rememberWindowState
import ca.gosyer.data.models.Extension import ca.gosyer.data.models.Extension
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.presentation.build.BuildKonfig
@@ -54,59 +51,48 @@ import ca.gosyer.ui.base.navigation.TextActionIcon
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.image.KamelImage import ca.gosyer.uicore.image.KamelImage
import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.util.compose.ThemedWindow
import ca.gosyer.util.compose.persistentLazyListState
import ca.gosyer.util.lang.launchApplication
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import io.kamel.image.lazyPainterResource import io.kamel.image.lazyPainterResource
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.util.Locale import java.util.Locale
@OptIn(DelicateCoroutinesApi::class)
fun openExtensionsMenu() {
launchApplication {
val state = rememberWindowState(size = DpSize(550.dp, 700.dp))
ThemedWindow(::exitApplication, state, title = BuildKonfig.NAME) {
Surface {
ExtensionsMenu()
}
}
}
}
@Composable @Composable
fun ExtensionsMenu() { fun ExtensionsScreenContent(
val vm = viewModel<ExtensionsMenuViewModel>() extensions: Map<String, List<Extension>>,
val extensions by vm.extensions.collectAsState() isLoading: Boolean,
val isLoading by vm.isLoading.collectAsState() query: String?,
val search by vm.searchQuery.collectAsState() setQuery: (String) -> Unit,
enabledLangs: StateFlow<Set<String>>,
getSourceLanguages: () -> Set<String>,
setEnabledLanguages: (Set<String>) -> Unit,
installExtension: (Extension) -> Unit,
updateExtension: (Extension) -> Unit,
uninstallExtension: (Extension) -> Unit
) {
if (isLoading) { if (isLoading) {
Column { Column {
ExtensionsToolbar( ExtensionsToolbar(
search, query,
vm::search, setQuery,
vm.enabledLangs, enabledLangs,
vm::getSourceLanguages, getSourceLanguages,
vm::setEnabledLanguages setEnabledLanguages
) )
LoadingScreen(isLoading) LoadingScreen(isLoading)
} }
} else { } else {
val state = persistentLazyListState() val state = rememberLazyListState()
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {
item { item {
ExtensionsToolbar( ExtensionsToolbar(
search, query,
vm::search, setQuery,
vm.enabledLangs, enabledLangs,
vm::getSourceLanguages, getSourceLanguages,
vm::setEnabledLanguages setEnabledLanguages
) )
} }
extensions.forEach { (header, items) -> extensions.forEach { (header, items) ->
@@ -120,9 +106,9 @@ fun ExtensionsMenu() {
items(items) { extension -> items(items) { extension ->
ExtensionItem( ExtensionItem(
extension, extension,
onInstallClicked = vm::install, onInstallClicked = installExtension,
onUpdateClicked = vm::update, onUpdateClicked = updateExtension,
onUninstallClicked = vm::uninstall onUninstallClicked = uninstallExtension
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
} }

View File

@@ -6,197 +6,55 @@
package ca.gosyer.ui.library package ca.gosyer.ui.library
import androidx.compose.animation.AnimatedVisibility
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.MaterialTheme
import androidx.compose.material.ScrollableTabRow
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Tab
import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState 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 androidx.compose.ui.util.fastForEachIndexed
import ca.gosyer.data.library.model.DisplayMode
import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga
import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.library.components.LibraryScreenContent
import ca.gosyer.ui.manga.openMangaMenu import ca.gosyer.ui.manga.MangaScreen
import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.util.compose.ThemedWindow import cafe.adriel.voyager.core.screen.Screen
import ca.gosyer.util.lang.launchApplication import cafe.adriel.voyager.core.screen.ScreenKey
import com.github.zsoltk.compose.savedinstancestate.Bundle import cafe.adriel.voyager.core.screen.uniqueScreenKey
import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState import cafe.adriel.voyager.navigator.LocalNavigator
import com.google.accompanist.pager.HorizontalPager import cafe.adriel.voyager.navigator.Navigator
import com.google.accompanist.pager.rememberPagerState import cafe.adriel.voyager.navigator.currentOrThrow
import ca.gosyer.uicore.resources.stringResource
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
fun openLibraryMenu() { fun openLibraryMenu() {
launchApplication { launchApplication {
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
CompositionLocalProvider( Surface {
LocalSavedInstanceState provides Bundle() Navigator(remember { LibraryScreen() })
) {
Surface {
LibraryScreen()
}
} }
} }
} }
} }
@Composable class LibraryScreen : Screen {
fun LibraryScreen(onClickManga: (Long) -> Unit = ::openMangaMenu) {
LibraryScreen(LocalSavedInstanceState.current, onClickManga)
}
@Composable override val key: ScreenKey = uniqueScreenKey
fun LibraryScreen(bundle: Bundle, onClickManga: (Long) -> Unit = ::openMangaMenu) {
val vm = viewModel {
instantiate<LibraryScreenViewModel>(bundle)
}
val categories by vm.categories.collectAsState()
val selectedCategoryIndex by vm.selectedCategoryIndex.collectAsState()
val displayMode by vm.displayMode.collectAsState()
val isLoading by vm.isLoading.collectAsState()
val error by vm.error.collectAsState()
val query by vm.query.collectAsState()
// val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
if (categories.isEmpty()) { @Composable
LoadingScreen(isLoading, errorMessage = error) override fun Content() {
} else { val vm = viewModel<LibraryScreenViewModel>()
/*ModalBottomSheetLayout( val navigator = LocalNavigator.currentOrThrow
sheetState = sheetState, LibraryScreenContent(
sheetContent = { *//*LibrarySheet()*//* } categories = vm.categories.collectAsState().value,
) {*/ selectedCategoryIndex = vm.selectedCategoryIndex.collectAsState().value,
Column(Modifier.fillMaxWidth()) { displayMode = vm.displayMode.collectAsState().value,
/*Toolbar( isLoading = vm.isLoading.collectAsState().value,
title = { error = vm.error.collectAsState().value,
val text = if (vm.showCategoryTabs) { query = vm.query.collectAsState().value,
stringResource(R.string.library_label) updateQuery = vm::updateQuery,
} else { getLibraryForPage = { vm.getLibraryForCategoryId(it).collectAsState() },
vm.selectedCategory?.visibleName.orEmpty() onPageChanged = vm::setSelectedPage,
} onClickManga = { navigator push MangaScreen(it) },
Text(text) onRemoveMangaClicked = vm::removeManga
}, )
actions = {
IconButton(onClick = { scope.launch { sheetState.show() }}) {
Icon(Icons.Rounded.FilterList, contentDescription = null)
}
}
)*/
Toolbar(
stringResource(MR.strings.location_library),
closable = false,
searchText = query,
search = vm::updateQuery
)
LibraryTabs(
visible = true, // vm.showCategoryTabs,
categories = categories,
selectedPage = selectedCategoryIndex,
onPageChanged = vm::setSelectedPage
)
LibraryPager(
categories = categories,
displayMode = displayMode,
selectedPage = selectedCategoryIndex,
getLibraryForPage = { vm.getLibraryForCategoryId(it).collectAsState() },
onPageChanged = vm::setSelectedPage,
onClickManga = onClickManga,
onRemoveMangaClicked = vm::removeManga
)
}
// }
}
}
@Composable
private fun LibraryTabs(
visible: Boolean,
categories: List<Category>,
selectedPage: Int,
onPageChanged: (Int) -> Unit
) {
if (categories.isEmpty()) return
AnimatedVisibility(
visible = visible,
enter = expandVertically(),
exit = shrinkVertically()
) {
ScrollableTabRow(
selectedTabIndex = selectedPage,
backgroundColor = MaterialTheme.colors.surface,
// contentColor = CustomColors.current.onBars,
edgePadding = 0.dp
) {
categories.fastForEachIndexed { i, category ->
Tab(
selected = selectedPage == i,
onClick = { onPageChanged(i) },
text = { Text(category.name) }
)
}
}
}
}
@Composable
private fun LibraryPager(
categories: List<Category>,
displayMode: DisplayMode,
selectedPage: Int,
getLibraryForPage: @Composable (Long) -> State<List<Manga>>,
onPageChanged: (Int) -> Unit,
onClickManga: (Long) -> Unit,
onRemoveMangaClicked: (Long) -> Unit
) {
if (categories.isEmpty()) return
val state = rememberPagerState(categories.size, selectedPage)
LaunchedEffect(state.currentPage) {
if (state.currentPage != selectedPage) {
onPageChanged(state.currentPage)
}
}
LaunchedEffect(selectedPage) {
if (state.currentPage != selectedPage) {
state.animateScrollToPage(selectedPage)
}
}
HorizontalPager(state = state) {
val library by getLibraryForPage(categories[it].id)
when (displayMode) {
DisplayMode.CompactGrid -> LibraryMangaCompactGrid(
library = library,
onClickManga = onClickManga,
onRemoveMangaClicked = onRemoveMangaClicked
)
/*DisplayMode.ComfortableGrid -> LibraryMangaComfortableGrid(
library = library,
onClickManga = onClickManga
)
DisplayMode.List -> LibraryMangaList(
library = library,
onClickManga = onClickManga
)*/
else -> Box {}
}
} }
} }

View File

@@ -15,9 +15,6 @@ import ca.gosyer.data.server.interactions.CategoryInteractionHandler
import ca.gosyer.data.server.interactions.LibraryInteractionHandler import ca.gosyer.data.server.interactions.LibraryInteractionHandler
import ca.gosyer.data.server.interactions.UpdatesInteractionHandler import ca.gosyer.data.server.interactions.UpdatesInteractionHandler
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.util.compose.saveIntInBundle
import ca.gosyer.util.compose.saveStringInBundle
import com.github.zsoltk.compose.savedinstancestate.Bundle
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -74,13 +71,12 @@ class LibraryScreenViewModel @Inject constructor(
private val categoryHandler: CategoryInteractionHandler, private val categoryHandler: CategoryInteractionHandler,
private val libraryHandler: LibraryInteractionHandler, private val libraryHandler: LibraryInteractionHandler,
private val updatesHandler: UpdatesInteractionHandler, private val updatesHandler: UpdatesInteractionHandler,
libraryPreferences: LibraryPreferences, libraryPreferences: LibraryPreferences
private val bundle: Bundle,
) : ViewModel() { ) : ViewModel() {
private val library = Library(MutableStateFlow(emptyList()), mutableMapOf()) private val library = Library(MutableStateFlow(emptyList()), mutableMapOf())
val categories = library.categories.asStateFlow() val categories = library.categories.asStateFlow()
private val _selectedCategoryIndex = saveIntInBundle(scope, bundle, SELECTED_CATEGORY_KEY, 0) private val _selectedCategoryIndex = MutableStateFlow(0)
val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow() val selectedCategoryIndex = _selectedCategoryIndex.asStateFlow()
val displayMode = libraryPreferences.displayMode().stateIn(scope) val displayMode = libraryPreferences.displayMode().stateIn(scope)
@@ -91,7 +87,7 @@ class LibraryScreenViewModel @Inject constructor(
private val _error = MutableStateFlow<String?>(null) private val _error = MutableStateFlow<String?>(null)
val error = _error.asStateFlow() val error = _error.asStateFlow()
private val _query = saveStringInBundle(scope, bundle, QUERY_KEY) private val _query = MutableStateFlow("")
val query = _query.asStateFlow() val query = _query.asStateFlow()
init { init {

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.library package ca.gosyer.ui.library.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -36,7 +36,9 @@ fun LibraryMangaBadges(
if (downloaded != null && downloaded > 0) { if (downloaded != null && downloaded > 0) {
Text( Text(
text = downloaded.toString(), text = downloaded.toString(),
modifier = Modifier.background(MaterialTheme.colors.secondary).then(BadgesInnerPadding), modifier = Modifier.background(MaterialTheme.colors.secondary).then(
BadgesInnerPadding
),
style = MaterialTheme.typography.caption, style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.onSecondary color = MaterialTheme.colors.onSecondary
) )

View File

@@ -0,0 +1,62 @@
/*
* 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.library.components
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import ca.gosyer.data.library.model.DisplayMode
import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
@Composable
fun LibraryPager(
categories: List<Category>,
displayMode: DisplayMode,
selectedPage: Int,
getLibraryForPage: @Composable (Long) -> State<List<Manga>>,
onPageChanged: (Int) -> Unit,
onClickManga: (Long) -> Unit,
onRemoveMangaClicked: (Long) -> Unit
) {
if (categories.isEmpty()) return
val state = rememberPagerState(categories.size, selectedPage)
LaunchedEffect(state.currentPage) {
if (state.currentPage != selectedPage) {
onPageChanged(state.currentPage)
}
}
LaunchedEffect(selectedPage) {
if (state.currentPage != selectedPage) {
state.animateScrollToPage(selectedPage)
}
}
HorizontalPager(state = state) {
val library by getLibraryForPage(categories[it].id)
when (displayMode) {
DisplayMode.CompactGrid -> LibraryMangaCompactGrid(
library = library,
onClickManga = onClickManga,
onRemoveMangaClicked = onRemoveMangaClicked
)
/*DisplayMode.ComfortableGrid -> LibraryMangaComfortableGrid(
library = library,
onClickManga = onClickManga
)
DisplayMode.List -> LibraryMangaList(
library = library,
onClickManga = onClickManga
)*/
else -> Box {}
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.library.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import ca.gosyer.data.library.model.DisplayMode
import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga
import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.resources.stringResource
@Composable
fun LibraryScreenContent(
categories: List<Category>,
selectedCategoryIndex: Int,
displayMode: DisplayMode,
isLoading: Boolean,
error: String?,
query: String,
updateQuery: (String) -> Unit,
getLibraryForPage: @Composable (Long) -> State<List<Manga>>,
onPageChanged: (Int) -> Unit,
onClickManga: (Long) -> Unit,
onRemoveMangaClicked: (Long) -> Unit
) {
// val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
if (categories.isEmpty()) {
LoadingScreen(isLoading, errorMessage = error)
} else {
/*ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = { *//*LibrarySheet()*//* }
) {*/
Column(Modifier.fillMaxWidth()) {
/*Toolbar(
title = {
val text = if (vm.showCategoryTabs) {
stringResource(R.string.library_label)
} else {
vm.selectedCategory?.visibleName.orEmpty()
}
Text(text)
},
actions = {
IconButton(onClick = { scope.launch { sheetState.show() }}) {
Icon(Icons.Rounded.FilterList, contentDescription = null)
}
}
)*/
Toolbar(
stringResource(MR.strings.location_library),
closable = false,
searchText = query,
search = updateQuery
)
LibraryTabs(
visible = true, // vm.showCategoryTabs,
categories = categories,
selectedPage = selectedCategoryIndex,
onPageChanged = onPageChanged
)
LibraryPager(
categories = categories,
displayMode = displayMode,
selectedPage = selectedCategoryIndex,
getLibraryForPage = getLibraryForPage,
onPageChanged = onPageChanged,
onClickManga = onClickManga,
onRemoveMangaClicked = onRemoveMangaClicked
)
}
// }
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.library.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ScrollableTabRow
import androidx.compose.material.Tab
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import ca.gosyer.data.models.Category
@Composable
fun LibraryTabs(
visible: Boolean,
categories: List<Category>,
selectedPage: Int,
onPageChanged: (Int) -> Unit
) {
if (categories.isEmpty()) return
AnimatedVisibility(
visible = visible,
enter = expandVertically(),
exit = shrinkVertically()
) {
ScrollableTabRow(
selectedTabIndex = selectedPage,
backgroundColor = MaterialTheme.colors.surface,
// contentColor = CustomColors.current.onBars,
edgePadding = 0.dp
) {
categories.fastForEachIndexed { i, category ->
Tab(
selected = selectedPage == i,
onClick = { onPageChanged(i) },
text = { Text(category.name) }
)
}
}
}
}

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.library package ca.gosyer.ui.library.components
import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar

View File

@@ -6,7 +6,6 @@
package ca.gosyer.ui.main package ca.gosyer.ui.main
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -26,45 +25,25 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.ui.base.navigation.LocalMenuController import ca.gosyer.ui.base.navigation.DisplayController
import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.withDisplayController
import ca.gosyer.ui.base.navigation.withMenuController
import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.ui.downloads.DownloadsMenu
import ca.gosyer.ui.extensions.ExtensionsMenu
import ca.gosyer.ui.library.LibraryScreen
import ca.gosyer.ui.main.components.SideMenu import ca.gosyer.ui.main.components.SideMenu
import ca.gosyer.ui.manga.MangaMenu import ca.gosyer.uicore.vm.LocalViewModelFactory
import ca.gosyer.ui.reader.openReaderMenu import cafe.adriel.voyager.navigator.CurrentScreen
import ca.gosyer.ui.settings.SettingsAdvancedScreen import cafe.adriel.voyager.navigator.Navigator
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 ca.gosyer.ui.sources.settings.SourceSettingsMenu
import ca.gosyer.ui.updates.UpdatesMenu
import com.github.zsoltk.compose.router.Router
import com.github.zsoltk.compose.savedinstancestate.Bundle
import com.github.zsoltk.compose.savedinstancestate.BundleScope
const val SIDE_MENU_EXPAND_DURATION = 500 const val SIDE_MENU_EXPAND_DURATION = 500
@Composable @Composable
fun MainMenu(rootBundle: Bundle) { fun MainMenu() {
val vm = viewModel<MainViewModel>() val vmFactory = LocalViewModelFactory.current
val vm = remember { vmFactory.instantiate<MainViewModel>() }
Surface { Surface {
Router("TopLevel", vm.startScreen.toRoute()) { backStack -> Navigator(vm.startScreen.toScreen()) { navigator ->
val controller = remember { val controller = remember { DisplayController() }
MenuController(backStack)
}
BoxWithConstraints { BoxWithConstraints {
// if (maxWidth > 720.dp) { // if (maxWidth > 720.dp) {
WideMainMenu(rootBundle, controller) WideMainMenu(navigator, controller)
// } else { // } else {
// SkinnyMainMenu(rootBundle, controller) // SkinnyMainMenu(rootBundle, controller)
// } // }
@@ -75,8 +54,8 @@ fun MainMenu(rootBundle: Bundle) {
@Composable @Composable
fun SkinnyMainMenu( fun SkinnyMainMenu(
rootBundle: Bundle, navigator: Navigator,
controller: MenuController controller: DisplayController
) { ) {
val drawerState = rememberDrawerState(DrawerValue.Closed) val drawerState = rememberDrawerState(DrawerValue.Closed)
LaunchedEffect(controller.sideMenuVisible) { LaunchedEffect(controller.sideMenuVisible) {
@@ -104,21 +83,21 @@ fun SkinnyMainMenu(
ModalDrawer( ModalDrawer(
{ {
SideMenu(Modifier.fillMaxWidth(), controller) SideMenu(Modifier.fillMaxWidth(), controller, navigator)
}, },
drawerState = drawerState, drawerState = drawerState,
gesturesEnabled = drawerState.isOpen gesturesEnabled = drawerState.isOpen
) { ) {
withMenuController(controller) { withDisplayController(controller) {
MainWindow(Modifier, rootBundle) MainWindow(Modifier)
} }
} }
} }
@Composable @Composable
fun WideMainMenu( fun WideMainMenu(
rootBundle: Bundle, navigator: Navigator,
controller: MenuController controller: DisplayController
) { ) {
Box { Box {
val startPadding by animateDpAsState( val startPadding by animateDpAsState(
@@ -130,72 +109,17 @@ fun WideMainMenu(
animationSpec = tween(SIDE_MENU_EXPAND_DURATION) animationSpec = tween(SIDE_MENU_EXPAND_DURATION)
) )
if (startPadding != 0.dp) { if (startPadding != 0.dp) {
SideMenu(Modifier.width(200.dp), controller) SideMenu(Modifier.width(200.dp), controller, navigator)
} }
withMenuController(controller) { withDisplayController(controller) {
MainWindow(Modifier.padding(start = startPadding), rootBundle) MainWindow(Modifier.padding(start = startPadding))
} }
} }
} }
@Composable @Composable
fun MainWindow(modifier: Modifier, rootBundle: Bundle) { fun MainWindow(modifier: Modifier) {
Surface(Modifier.fillMaxSize().then(modifier)) { Surface(Modifier.fillMaxSize() then modifier) {
val menuController = LocalMenuController.current!! CurrentScreen()
BundleScope("K${menuController.backStack.lastIndex}", rootBundle, false) {
Crossfade(menuController.backStack.last()) { routing ->
when (routing) {
is Routes.Library -> LibraryScreen {
menuController.push(Routes.Manga(it))
}
is Routes.Updates -> UpdatesMenu(
openChapter = ::openReaderMenu,
openManga = { menuController.push(Routes.Manga(it)) }
)
is Routes.Sources -> SourcesMenu(
{
menuController.push(Routes.SourceSettings(it))
}
) {
menuController.push(Routes.Manga(it))
}
is Routes.Extensions -> ExtensionsMenu()
is Routes.Manga -> MangaMenu(routing.mangaId)
is Routes.Downloads -> DownloadsMenu {
menuController.push(Routes.Manga(it))
}
is Routes.SourceSettings -> SourceSettingsMenu(routing.sourceId)
is Routes.Settings -> SettingsScreen(menuController)
is Routes.SettingsGeneral -> SettingsGeneralScreen(menuController)
is Routes.SettingsAppearance -> SettingsAppearance(menuController)
is Routes.SettingsServer -> SettingsServerScreen(menuController)
is Routes.SettingsLibrary -> SettingsLibraryScreen(menuController)
is Routes.SettingsReader -> SettingsReaderScreen(menuController)
/*is Route.SettingsDownloads -> SettingsDownloadsScreen(menuController)
is Route.SettingsTracking -> SettingsTrackingScreen(menuController)*/
is Routes.SettingsBrowse -> SettingsBrowseScreen(menuController)
is Routes.SettingsBackup -> SettingsBackupScreen(menuController)
/*is Route.SettingsSecurity -> SettingsSecurityScreen(menuController)
is Route.SettingsParentalControls -> SettingsParentalControlsScreen(menuController)*/
is Routes.SettingsAdvanced -> SettingsAdvancedScreen(menuController)
}
}
}
/*Box(Modifier.padding(bottom = 32.dp).align(Alignment.BottomCenter)) {
val shape = RoundedCornerShape(50.dp)
Box(
Modifier
.width(200.dp)
.defaultMinSize(minHeight = 64.dp)
.shadow(4.dp, shape)
.background(SolidColor(Color.Gray), alpha = 0.2F)
.clip(shape),
contentAlignment = Alignment.Center
) {
Text("Test text")
}
}*/
} }
} }

View File

@@ -8,10 +8,13 @@ package ca.gosyer.ui.main
import ca.gosyer.data.ui.UiPreferences import ca.gosyer.data.ui.UiPreferences
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.MainScope
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class MainViewModel @Inject constructor( class MainViewModel @Inject constructor(
uiPreferences: UiPreferences uiPreferences: UiPreferences
) : ViewModel() { ) : ViewModel() {
override val scope = MainScope()
val startScreen = uiPreferences.startScreen().get() val startScreen = uiPreferences.startScreen().get()
} }

View File

@@ -7,37 +7,14 @@
package ca.gosyer.ui.main package ca.gosyer.ui.main
import ca.gosyer.data.ui.model.StartScreen import ca.gosyer.data.ui.model.StartScreen
import ca.gosyer.ui.extensions.ExtensionsScreen
import ca.gosyer.ui.library.LibraryScreen
import ca.gosyer.ui.sources.SourcesScreen
import ca.gosyer.ui.updates.UpdatesScreen
sealed class Routes { fun StartScreen.toScreen() = when (this) {
object Library : Routes() StartScreen.Library -> LibraryScreen()
object Updates : Routes() StartScreen.Updates -> UpdatesScreen()
object Sources : Routes() StartScreen.Sources -> SourcesScreen()
object Extensions : Routes() StartScreen.Extensions -> ExtensionsScreen()
data class Manga(val mangaId: Long) : Routes()
object Downloads : Routes()
data class SourceSettings(val sourceId: Long) : Routes()
object Settings : Routes()
object SettingsGeneral : Routes()
object SettingsAppearance : Routes()
object SettingsLibrary : Routes()
object SettingsReader : Routes()
/*object SettingsDownloads : Route()
object SettingsTracking : Route()*/
object SettingsBrowse : Routes()
object SettingsBackup : Routes()
object SettingsServer : Routes()
/*object SettingsSecurity : Route()
object SettingsParentalControls : Route()*/
object SettingsAdvanced : Routes()
}
fun StartScreen.toRoute() = when (this) {
StartScreen.Library -> Routes.Library
StartScreen.Updates -> Routes.Updates
StartScreen.Sources -> Routes.Sources
StartScreen.Extensions -> Routes.Extensions
} }

View File

@@ -22,28 +22,37 @@ import androidx.compose.material.icons.rounded.Store
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.downloads.DownloadsScreen
import ca.gosyer.ui.extensions.ExtensionsScreen
import ca.gosyer.ui.extensions.openExtensionsMenu import ca.gosyer.ui.extensions.openExtensionsMenu
import ca.gosyer.ui.library.LibraryScreen
import ca.gosyer.ui.library.openLibraryMenu import ca.gosyer.ui.library.openLibraryMenu
import ca.gosyer.ui.main.components.DownloadsExtraInfo import ca.gosyer.ui.main.components.DownloadsExtraInfo
import ca.gosyer.ui.settings.SettingsScreen
import ca.gosyer.ui.sources.SourcesScreen
import ca.gosyer.ui.sources.openSourcesMenu import ca.gosyer.ui.sources.openSourcesMenu
import com.github.zsoltk.compose.router.BackStack import ca.gosyer.ui.updates.UpdatesScreen
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.Navigator
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlin.reflect.KClass
enum class TopLevelMenus( enum class TopLevelMenus(
val textKey: StringResource, val textKey: StringResource,
val unselectedIcon: ImageVector, val unselectedIcon: ImageVector,
val selectedIcon: ImageVector, val selectedIcon: ImageVector,
val menu: Routes, val screen: KClass<*>,
val createScreen: () -> Screen,
val top: Boolean, val top: Boolean,
val openInNewWindow: () -> Unit = {}, val openInNewWindow: () -> Unit = {},
val extraInfo: (@Composable () -> Unit)? = null val extraInfo: (@Composable () -> Unit)? = null
) { ) {
Library(MR.strings.location_library, Icons.Outlined.Book, Icons.Rounded.Book, Routes.Library, true, ::openLibraryMenu), Library(MR.strings.location_library, Icons.Outlined.Book, Icons.Rounded.Book, LibraryScreen::class, { LibraryScreen() }, true, ::openLibraryMenu),
Updates(MR.strings.location_updates, Icons.Outlined.NewReleases, Icons.Rounded.NewReleases, Routes.Updates, true, ::openLibraryMenu), Updates(MR.strings.location_updates, Icons.Outlined.NewReleases, Icons.Rounded.NewReleases, UpdatesScreen::class, { UpdatesScreen() }, true, ::openLibraryMenu),
Sources(MR.strings.location_sources, Icons.Outlined.Explore, Icons.Rounded.Explore, Routes.Sources, true, ::openSourcesMenu), Sources(MR.strings.location_sources, Icons.Outlined.Explore, Icons.Rounded.Explore, SourcesScreen::class, { SourcesScreen() }, true, ::openSourcesMenu),
Extensions(MR.strings.location_extensions, Icons.Outlined.Store, Icons.Rounded.Store, Routes.Extensions, true, ::openExtensionsMenu), Extensions(MR.strings.location_extensions, Icons.Outlined.Store, Icons.Rounded.Store, ExtensionsScreen::class, { ExtensionsScreen() }, true, ::openExtensionsMenu),
Downloads(MR.strings.location_downloads, Icons.Outlined.Download, Icons.Rounded.Download, Routes.Downloads, false, extraInfo = { DownloadsExtraInfo() }), Downloads(MR.strings.location_downloads, Icons.Outlined.Download, Icons.Rounded.Download, DownloadsScreen::class, { DownloadsScreen() }, false, extraInfo = { DownloadsExtraInfo() }),
Settings(MR.strings.location_settings, Icons.Outlined.Settings, Icons.Rounded.Settings, Routes.Settings, false); Settings(MR.strings.location_settings, Icons.Outlined.Settings, Icons.Rounded.Settings, SettingsScreen::class, { SettingsScreen() }, false);
fun isSelected(backStack: BackStack<Routes>) = backStack.elements.first() == menu fun isSelected(navigator: Navigator) = navigator.items.first()::class == screen
} }

View File

@@ -4,20 +4,21 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.main package ca.gosyer.ui.main.components
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.LocalViewModelFactory
import ca.gosyer.ui.main.components.DebugOverlayViewModel
@Composable @Composable
fun DebugOverlay() { fun DebugOverlay() {
val vm = viewModel<DebugOverlayViewModel>() val vmFactory = LocalViewModelFactory.current
val vm = remember { vmFactory.instantiate<DebugOverlayViewModel>() }
val usedMemory by vm.usedMemoryFlow.collectAsState() val usedMemory by vm.usedMemoryFlow.collectAsState()
Column { Column {
Text("$usedMemory/${vm.maxMemory}", color = Color.White) Text("$usedMemory/${vm.maxMemory}", color = Color.White)

View File

@@ -7,6 +7,7 @@
package ca.gosyer.ui.main.components package ca.gosyer.ui.main.components
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -14,6 +15,8 @@ import me.tatarka.inject.annotations.Inject
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
class DebugOverlayViewModel @Inject constructor() : ViewModel() { class DebugOverlayViewModel @Inject constructor() : ViewModel() {
override val scope = MainScope()
val runtime: Runtime = Runtime.getRuntime() val runtime: Runtime = Runtime.getRuntime()
val maxMemory = runtime.maxMemory().formatSize() val maxMemory = runtime.maxMemory().formatSize()
val usedMemoryFlow = MutableStateFlow(runtime.usedMemory().formatSize()) val usedMemoryFlow = MutableStateFlow(runtime.usedMemory().formatSize())

View File

@@ -15,17 +15,19 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.data.base.WebsocketService import ca.gosyer.data.base.WebsocketService
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.ui.downloads.DownloadsScreenViewModel
import ca.gosyer.ui.downloads.DownloadsMenuViewModel
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import ca.gosyer.uicore.vm.LocalViewModelFactory
@Composable @Composable
fun DownloadsExtraInfo() { fun DownloadsExtraInfo() {
val vm = viewModel<DownloadsMenuViewModel>() val vmFactory = LocalViewModelFactory.current
val vm = remember { vmFactory.instantiate<DownloadsScreenViewModel>(true) }
val status by vm.serviceStatus.collectAsState() val status by vm.serviceStatus.collectAsState()
val list by vm.downloadQueue.collectAsState() val list by vm.downloadQueue.collectAsState()
val text = when (status) { val text = when (status) {

View File

@@ -29,11 +29,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.base.navigation.MenuController import ca.gosyer.ui.base.navigation.DisplayController
import ca.gosyer.ui.main.TopLevelMenus import ca.gosyer.ui.main.TopLevelMenus
import cafe.adriel.voyager.navigator.Navigator
@Composable @Composable
fun SideMenu(modifier: Modifier, controller: MenuController) { fun SideMenu(modifier: Modifier, controller: DisplayController, navigator: Navigator) {
Surface(modifier then Modifier.fillMaxHeight(), elevation = 2.dp) { Surface(modifier then Modifier.fillMaxHeight(), elevation = 2.dp) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
Column(Modifier.fillMaxSize().padding(horizontal = 4.dp)) { Column(Modifier.fillMaxSize().padding(horizontal = 4.dp)) {
@@ -54,19 +55,17 @@ fun SideMenu(modifier: Modifier, controller: MenuController) {
Spacer(Modifier.height(20.dp)) Spacer(Modifier.height(20.dp))
remember { TopLevelMenus.values().filter(TopLevelMenus::top) }.forEach { topLevelMenu -> remember { TopLevelMenus.values().filter(TopLevelMenus::top) }.forEach { topLevelMenu ->
SideMenuItem( SideMenuItem(
topLevelMenu.isSelected(controller.backStack), topLevelMenu.isSelected(navigator),
topLevelMenu, topLevelMenu
controller::newRoot ) { navigator replaceAll it }
)
} }
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
Column(Modifier.align(Alignment.BottomStart).padding(bottom = 8.dp)) { Column(Modifier.align(Alignment.BottomStart).padding(bottom = 8.dp)) {
remember { TopLevelMenus.values().filterNot(TopLevelMenus::top) }.forEach { topLevelMenu -> remember { TopLevelMenus.values().filterNot(TopLevelMenus::top) }.forEach { topLevelMenu ->
SideMenuItem( SideMenuItem(
topLevelMenu.isSelected(controller.backStack), topLevelMenu.isSelected(navigator),
topLevelMenu, topLevelMenu,
controller::newRoot ) { navigator replaceAll it }
)
} }
} }
} }

View File

@@ -25,17 +25,17 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.ui.main.Routes
import ca.gosyer.ui.main.TopLevelMenus import ca.gosyer.ui.main.TopLevelMenus
import ca.gosyer.uicore.components.combinedMouseClickable import ca.gosyer.uicore.components.combinedMouseClickable
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import cafe.adriel.voyager.core.screen.Screen
@Composable @Composable
fun SideMenuItem(selected: Boolean, topLevelMenu: TopLevelMenus, newRoot: (Routes) -> Unit) { fun SideMenuItem(selected: Boolean, topLevelMenu: TopLevelMenus, newRoot: (Screen) -> Unit) {
SideMenuItem( SideMenuItem(
selected, selected,
stringResource(topLevelMenu.textKey), stringResource(topLevelMenu.textKey),
topLevelMenu.menu, topLevelMenu.createScreen,
topLevelMenu.selectedIcon, topLevelMenu.selectedIcon,
topLevelMenu.unselectedIcon, topLevelMenu.unselectedIcon,
topLevelMenu.openInNewWindow, topLevelMenu.openInNewWindow,
@@ -48,12 +48,12 @@ fun SideMenuItem(selected: Boolean, topLevelMenu: TopLevelMenus, newRoot: (Route
private fun SideMenuItem( private fun SideMenuItem(
selected: Boolean, selected: Boolean,
text: String, text: String,
menu: Routes, createScreen: () -> Screen,
selectedIcon: ImageVector, selectedIcon: ImageVector,
unselectedIcon: ImageVector, unselectedIcon: ImageVector,
onMiddleClick: () -> Unit, onMiddleClick: () -> Unit,
extraInfo: (@Composable () -> Unit)? = null, extraInfo: (@Composable () -> Unit)? = null,
onClick: (Routes) -> Unit onClick: (Screen) -> Unit
) { ) {
Card( Card(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
@@ -70,7 +70,7 @@ private fun SideMenuItem(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
.height(40.dp) .height(40.dp)
.combinedMouseClickable( .combinedMouseClickable(
onClick = { onClick(menu) }, onClick = { onClick(createScreen()) },
onMiddleClick = { onMiddleClick() } onMiddleClick = { onMiddleClick() }
) )
) { ) {

View File

@@ -8,6 +8,7 @@ package ca.gosyer.ui.main.components
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.window.ApplicationScope import androidx.compose.ui.window.ApplicationScope
import androidx.compose.ui.window.Notification import androidx.compose.ui.window.Notification
@@ -15,13 +16,14 @@ import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.rememberTrayState import androidx.compose.ui.window.rememberTrayState
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.LocalViewModelFactory
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Locale import java.util.Locale
@Composable @Composable
fun ApplicationScope.Tray(icon: Painter) { fun ApplicationScope.Tray(icon: Painter) {
val vm = viewModel<TrayViewModel>() val vmFactory = LocalViewModelFactory.current
val vm = remember { vmFactory.instantiate<TrayViewModel>() }
val trayState = rememberTrayState() val trayState = rememberTrayState()
Tray( Tray(
icon, icon,

View File

@@ -8,11 +8,14 @@ package ca.gosyer.ui.main.components
import ca.gosyer.data.update.UpdateChecker import ca.gosyer.data.update.UpdateChecker
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.MainScope
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class TrayViewModel @Inject constructor( class TrayViewModel @Inject constructor(
private val updateChecker: UpdateChecker private val updateChecker: UpdateChecker
) : ViewModel() { ) : ViewModel() {
override val scope = MainScope()
init { init {
updateChecker.checkForUpdates() updateChecker.checkForUpdates()
} }

View File

@@ -0,0 +1,66 @@
/*
* 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.manga
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.manga.components.MangaScreenContent
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.Navigator
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openMangaMenu(mangaId: Long) {
launchApplication {
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
Surface {
Navigator(remember { MangaScreen(mangaId) })
}
}
}
}
class MangaScreen(private val mangaId: Long) : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel {
instantiate<MangaScreenViewModel>(MangaScreenViewModel.Params(mangaId))
}
MangaScreenContent(
isLoading = vm.isLoading.collectAsState().value,
manga = vm.manga.collectAsState().value,
chapters = vm.chapters.collectAsState().value,
dateTimeFormatter = vm.dateTimeFormatter.collectAsState().value,
categoriesExist = vm.categoriesExist.collectAsState().value,
chooseCategoriesFlow = vm.chooseCategoriesFlow,
addFavorite = vm::addFavorite,
setCategories = vm::setCategories,
toggleFavorite = vm::toggleFavorite,
refreshManga = vm::refreshManga,
toggleRead = vm::toggleRead,
toggleBookmarked = vm::toggleBookmarked,
markPreviousRead = vm::markPreviousRead,
downloadChapter = vm::downloadChapter,
deleteDownload = vm::deleteDownload,
stopDownloadingChapter = vm::stopDownloadingChapter,
loadChapters = vm::loadChapters,
loadManga = vm::loadManga
)
}
}

View File

@@ -33,7 +33,7 @@ import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.Locale import java.util.Locale
class MangaMenuViewModel @Inject constructor( class MangaScreenViewModel @Inject constructor(
private val mangaHandler: MangaInteractionHandler, private val mangaHandler: MangaInteractionHandler,
private val chapterHandler: ChapterInteractionHandler, private val chapterHandler: ChapterInteractionHandler,
private val categoryHandler: CategoryInteractionHandler, private val categoryHandler: CategoryInteractionHandler,
@@ -226,7 +226,7 @@ class MangaMenuViewModel @Inject constructor(
} }
} }
override fun onDestroy() { override fun onDispose() {
downloadService.removeWatch(params.mangaId) downloadService.removeWatch(params.mangaId)
} }

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.manga package ca.gosyer.ui.manga.components
import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement

View File

@@ -4,9 +4,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.manga package ca.gosyer.ui.manga.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -32,13 +31,7 @@ import androidx.compose.material.Checkbox
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Favorite
import androidx.compose.material.icons.rounded.FavoriteBorder
import androidx.compose.material.icons.rounded.Label
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -50,137 +43,12 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import ca.gosyer.data.models.Category import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga import ca.gosyer.data.models.Manga
import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.base.WindowDialog import ca.gosyer.ui.base.WindowDialog
import ca.gosyer.ui.base.navigation.LocalMenuController
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.TextActionIcon
import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.reader.openReaderMenu
import ca.gosyer.uicore.components.ErrorScreen
import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.image.KamelImage import ca.gosyer.uicore.image.KamelImage
import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.util.compose.ThemedWindow
import ca.gosyer.util.lang.launchApplication
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
import ca.gosyer.uicore.resources.stringResource
import io.kamel.image.lazyPainterResource import io.kamel.image.lazyPainterResource
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@OptIn(DelicateCoroutinesApi::class)
fun openMangaMenu(mangaId: Long) {
launchApplication {
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
Surface {
MangaMenu(mangaId)
}
}
}
}
@Composable
fun MangaMenu(mangaId: Long, menuController: MenuController? = LocalMenuController.current) {
val vm = viewModel {
instantiate<MangaMenuViewModel>(MangaMenuViewModel.Params(mangaId))
}
val manga by vm.manga.collectAsState()
val chapters by vm.chapters.collectAsState()
val isLoading by vm.isLoading.collectAsState()
val dateTimeFormatter by vm.dateTimeFormatter.collectAsState()
val categoriesExist by vm.categoriesExist.collectAsState()
LaunchedEffect(Unit) {
vm.chooseCategoriesFlow.collect { (availableCategories, usedCategories) ->
openCategorySelectDialog(availableCategories, usedCategories, vm::addFavorite)
}
}
Box {
Column {
Toolbar(
stringResource(MR.strings.location_manga),
menuController,
menuController != null,
actions = {
AnimatedVisibility(categoriesExist && manga?.inLibrary == true) {
TextActionIcon(
vm::setCategories,
stringResource(MR.strings.edit_categories),
Icons.Rounded.Label
)
}
TextActionIcon(
vm::toggleFavorite,
stringResource(if (manga?.inLibrary == true) MR.strings.action_remove_favorite else MR.strings.action_favorite),
if (manga?.inLibrary == true) {
Icons.Rounded.Favorite
} else {
Icons.Rounded.FavoriteBorder
},
manga != null
)
TextActionIcon(
vm::refreshManga,
stringResource(MR.strings.action_refresh_manga),
Icons.Rounded.Refresh,
!isLoading
)
}
)
manga.let { manga ->
if (manga != null) {
Box {
val state = rememberLazyListState()
LazyColumn(state = state) {
item {
MangaItem(manga)
}
if (chapters.isNotEmpty()) {
items(chapters) { chapter ->
ChapterItem(
chapter,
dateTimeFormatter::format,
onClick = { openReaderMenu(it, manga.id) },
toggleRead = vm::toggleRead,
toggleBookmarked = vm::toggleBookmarked,
markPreviousAsRead = vm::markPreviousRead,
onClickDownload = vm::downloadChapter,
onClickDeleteChapter = vm::deleteDownload,
onClickStopDownload = vm::stopDownloadingChapter
)
}
} else if (!isLoading) {
item {
ErrorScreen(
stringResource(MR.strings.no_chapters_found),
Modifier.height(400.dp).fillMaxWidth(),
retry = vm::loadChapters
)
}
}
}
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd)
.fillMaxHeight()
.padding(horizontal = 4.dp, vertical = 8.dp),
adapter = rememberScrollbarAdapter(state)
)
}
} else if (!isLoading) {
ErrorScreen(stringResource(MR.strings.failed_manga_fetch), retry = vm::loadManga)
}
}
}
if (isLoading) {
LoadingScreen()
}
}
}
@Composable @Composable
fun MangaItem(manga: Manga) { fun MangaItem(manga: Manga) {
BoxWithConstraints(Modifier.padding(8.dp)) { BoxWithConstraints(Modifier.padding(8.dp)) {

View File

@@ -0,0 +1,150 @@
/*
* 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.manga.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Favorite
import androidx.compose.material.icons.rounded.FavoriteBorder
import androidx.compose.material.icons.rounded.Label
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga
import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.chapter.ChapterDownloadItem
import ca.gosyer.ui.base.navigation.TextActionIcon
import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.reader.openReaderMenu
import ca.gosyer.uicore.components.ErrorScreen
import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.resources.stringResource
import kotlinx.coroutines.flow.SharedFlow
import java.time.format.DateTimeFormatter
@Composable
fun MangaScreenContent(
isLoading: Boolean,
manga: Manga?,
chapters: List<ChapterDownloadItem>,
dateTimeFormatter: DateTimeFormatter,
categoriesExist: Boolean,
chooseCategoriesFlow: SharedFlow<Pair<List<Category>, List<Category>>>,
addFavorite: (List<Category>, List<Category>) -> Unit,
setCategories: () -> Unit,
toggleFavorite: () -> Unit,
refreshManga: () -> Unit,
toggleRead: (Int) -> Unit,
toggleBookmarked: (Int) -> Unit,
markPreviousRead: (Int) -> Unit,
downloadChapter: (Int) -> Unit,
deleteDownload: (Int) -> Unit,
stopDownloadingChapter: (Int) -> Unit,
loadChapters: () -> Unit,
loadManga: () -> Unit
) {
LaunchedEffect(Unit) {
chooseCategoriesFlow.collect { (availableCategories, usedCategories) ->
openCategorySelectDialog(availableCategories, usedCategories, addFavorite)
}
}
Box {
Column {
Toolbar(
stringResource(MR.strings.location_manga),
actions = {
AnimatedVisibility(categoriesExist && manga?.inLibrary == true) {
TextActionIcon(
setCategories,
stringResource(MR.strings.edit_categories),
Icons.Rounded.Label
)
}
TextActionIcon(
toggleFavorite,
stringResource(if (manga?.inLibrary == true) MR.strings.action_remove_favorite else MR.strings.action_favorite),
if (manga?.inLibrary == true) {
Icons.Rounded.Favorite
} else {
Icons.Rounded.FavoriteBorder
},
manga != null
)
TextActionIcon(
refreshManga,
stringResource(MR.strings.action_refresh_manga),
Icons.Rounded.Refresh,
!isLoading
)
}
)
manga.let { manga ->
if (manga != null) {
Box {
val state = rememberLazyListState()
LazyColumn(state = state) {
item {
MangaItem(manga)
}
if (chapters.isNotEmpty()) {
items(chapters) { chapter ->
ChapterItem(
chapter,
dateTimeFormatter::format,
onClick = { openReaderMenu(it, manga.id) },
toggleRead = toggleRead,
toggleBookmarked = toggleBookmarked,
markPreviousAsRead = markPreviousRead,
onClickDownload = downloadChapter,
onClickDeleteChapter = deleteDownload,
onClickStopDownload = stopDownloadingChapter
)
}
} else if (!isLoading) {
item {
ErrorScreen(
stringResource(MR.strings.no_chapters_found),
Modifier.height(400.dp).fillMaxWidth(),
retry = loadChapters
)
}
}
}
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd)
.fillMaxHeight()
.padding(horizontal = 4.dp, vertical = 8.dp),
adapter = rememberScrollbarAdapter(state)
)
}
} else if (!isLoading) {
ErrorScreen(stringResource(MR.strings.failed_manga_fetch), retry = loadManga)
}
}
}
if (isLoading) {
LoadingScreen()
}
}
}

View File

@@ -71,13 +71,13 @@ import ca.gosyer.ui.reader.navigation.RightAndLeftNavigation
import ca.gosyer.ui.reader.navigation.navigationClickable import ca.gosyer.ui.reader.navigation.navigationClickable
import ca.gosyer.ui.reader.viewer.ContinuousReader import ca.gosyer.ui.reader.viewer.ContinuousReader
import ca.gosyer.ui.reader.viewer.PagerReader import ca.gosyer.ui.reader.viewer.PagerReader
import ca.gosyer.ui.util.compose.WindowGet
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.components.ErrorScreen import ca.gosyer.uicore.components.ErrorScreen
import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.components.mangaAspectRatio import ca.gosyer.uicore.components.mangaAspectRatio
import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.util.compose.WindowGet
import ca.gosyer.util.lang.launchApplication
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import ca.gosyer.uicore.vm.LocalViewModelFactory
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
@@ -156,9 +156,8 @@ fun ReaderMenu(
mangaId: Long, mangaId: Long,
hotkeyFlow: SharedFlow<KeyEvent> hotkeyFlow: SharedFlow<KeyEvent>
) { ) {
val vm = viewModel { val vmFactory = LocalViewModelFactory.current
instantiate<ReaderMenuViewModel>(ReaderMenuViewModel.Params(chapterIndex, mangaId)) val vm = remember { vmFactory.instantiate<ReaderMenuViewModel>(ReaderMenuViewModel.Params(chapterIndex, mangaId)) }
}
val state by vm.state.collectAsState() val state by vm.state.collectAsState()
val previousChapter by vm.previousChapter.collectAsState() val previousChapter by vm.previousChapter.collectAsState()

View File

@@ -17,7 +17,6 @@ import ca.gosyer.data.reader.ReaderPreferences
import ca.gosyer.data.reader.model.Direction import ca.gosyer.data.reader.model.Direction
import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import ca.gosyer.data.server.interactions.MangaInteractionHandler import ca.gosyer.data.server.interactions.MangaInteractionHandler
import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.ui.reader.model.MoveTo import ca.gosyer.ui.reader.model.MoveTo
import ca.gosyer.ui.reader.model.Navigation import ca.gosyer.ui.reader.model.Navigation
import ca.gosyer.ui.reader.model.PageMove import ca.gosyer.ui.reader.model.PageMove
@@ -25,6 +24,7 @@ import ca.gosyer.ui.reader.model.ReaderChapter
import ca.gosyer.ui.reader.model.ReaderPage import ca.gosyer.ui.reader.model.ReaderPage
import ca.gosyer.ui.reader.model.ViewerChapters import ca.gosyer.ui.reader.model.ViewerChapters
import ca.gosyer.uicore.prefs.asStateIn import ca.gosyer.uicore.prefs.asStateIn
import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -279,7 +279,7 @@ class ReaderMenuViewModel @Inject constructor(
} }
} }
override fun onDestroy() { override fun onDispose() {
viewerChapters.recycle() viewerChapters.recycle()
} }

View File

@@ -12,7 +12,7 @@ import ca.gosyer.data.reader.ReaderPreferences
import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import ca.gosyer.ui.reader.model.ReaderChapter import ca.gosyer.ui.reader.model.ReaderChapter
import ca.gosyer.ui.reader.model.ReaderPage import ca.gosyer.ui.reader.model.ReaderPage
import ca.gosyer.util.compose.toImageBitmap import ca.gosyer.ui.util.compose.toImageBitmap
import io.github.kerubistan.kroki.coroutines.priorityChannel import io.github.kerubistan.kroki.coroutines.priorityChannel
import io.ktor.client.features.onDownload import io.ktor.client.features.onDownload
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope

View File

@@ -24,7 +24,7 @@ import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.toSize import androidx.compose.ui.unit.toSize
import ca.gosyer.ui.reader.model.Navigation import ca.gosyer.ui.reader.model.Navigation
import ca.gosyer.util.compose.contains import ca.gosyer.ui.util.compose.contains
fun Modifier.navigationClickable( fun Modifier.navigationClickable(
navigation: ViewerNavigation, navigation: ViewerNavigation,

View File

@@ -21,14 +21,29 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.data.update.UpdatePreferences import ca.gosyer.data.update.UpdatePreferences
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.base.prefs.SwitchPreference import ca.gosyer.ui.base.prefs.SwitchPreference
import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow
import ca.gosyer.uicore.resources.stringResource
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.uicore.resources.stringResource import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class SettingsAdvancedScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<SettingsAdvancedViewModel>()
SettingsAdvancedScreenContent(
updatesEnabled = vm.updatesEnabled
)
}
}
class SettingsAdvancedViewModel @Inject constructor( class SettingsAdvancedViewModel @Inject constructor(
updatePreferences: UpdatePreferences, updatePreferences: UpdatePreferences,
) : ViewModel() { ) : ViewModel() {
@@ -36,15 +51,16 @@ class SettingsAdvancedViewModel @Inject constructor(
} }
@Composable @Composable
fun SettingsAdvancedScreen(menuController: MenuController) { fun SettingsAdvancedScreenContent(
val vm = viewModel<SettingsAdvancedViewModel>() updatesEnabled: PreferenceMutableStateFlow<Boolean>
) {
Column { Column {
Toolbar(stringResource(MR.strings.settings_advanced_screen), menuController, true) Toolbar(stringResource(MR.strings.settings_advanced_screen))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {
item { item {
SwitchPreference(preference = vm.updatesEnabled, title = stringResource(MR.strings.update_checker)) SwitchPreference(preference = updatesEnabled, title = stringResource(MR.strings.update_checker))
} }
} }
VerticalScrollbar( VerticalScrollbar(

View File

@@ -37,7 +37,6 @@ import androidx.compose.ui.unit.sp
import ca.gosyer.data.ui.UiPreferences import ca.gosyer.data.ui.UiPreferences
import ca.gosyer.data.ui.model.ThemeMode import ca.gosyer.data.ui.model.ThemeMode
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.base.prefs.ChoicePreference import ca.gosyer.ui.base.prefs.ChoicePreference
import ca.gosyer.ui.base.prefs.ColorPreference import ca.gosyer.ui.base.prefs.ColorPreference
@@ -46,13 +45,33 @@ import ca.gosyer.ui.base.theme.AppColorsPreferenceState
import ca.gosyer.ui.base.theme.asStateFlow import ca.gosyer.ui.base.theme.asStateFlow
import ca.gosyer.ui.base.theme.getDarkColors import ca.gosyer.ui.base.theme.getDarkColors
import ca.gosyer.ui.base.theme.getLightColors import ca.gosyer.ui.base.theme.getLightColors
import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow
import ca.gosyer.uicore.resources.stringResource
import ca.gosyer.uicore.theme.Theme import ca.gosyer.uicore.theme.Theme
import ca.gosyer.uicore.theme.themes import ca.gosyer.uicore.theme.themes
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.uicore.resources.stringResource import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class SettingsAppearanceScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<ThemesViewModel>()
SettingsAppearanceScreenContent(
activeColors = vm.getActiveColors(),
themeMode = vm.themeMode,
lightTheme = vm.lightTheme,
darkTheme = vm.darkTheme,
windowDecorations = vm.windowDecorations
)
}
}
class ThemesViewModel @Inject constructor( class ThemesViewModel @Inject constructor(
private val uiPreferences: UiPreferences, private val uiPreferences: UiPreferences,
) : ViewModel() { ) : ViewModel() {
@@ -72,23 +91,26 @@ class ThemesViewModel @Inject constructor(
} }
@Composable @Composable
fun SettingsAppearance(menuController: MenuController) { fun SettingsAppearanceScreenContent(
val vm = viewModel<ThemesViewModel>() activeColors: AppColorsPreferenceState,
themeMode: PreferenceMutableStateFlow<ThemeMode>,
val activeColors = vm.getActiveColors() lightTheme: PreferenceMutableStateFlow<Int>,
darkTheme: PreferenceMutableStateFlow<Int>,
windowDecorations: PreferenceMutableStateFlow<Boolean>
) {
val isLight = MaterialTheme.colors.isLight val isLight = MaterialTheme.colors.isLight
val themesForCurrentMode = remember(isLight) { val themesForCurrentMode = remember(isLight) {
themes.filter { it.colors.isLight == isLight } themes.filter { it.colors.isLight == isLight }
} }
Column { Column {
Toolbar(stringResource(MR.strings.settings_appearance_screen), menuController, true) Toolbar(stringResource(MR.strings.settings_appearance_screen))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {
item { item {
ChoicePreference( ChoicePreference(
preference = vm.themeMode, preference = themeMode,
choices = mapOf( choices = mapOf(
ThemeMode.System to stringResource(MR.strings.theme_follow_system), ThemeMode.System to stringResource(MR.strings.theme_follow_system),
ThemeMode.Light to stringResource(MR.strings.theme_light), ThemeMode.Light to stringResource(MR.strings.theme_light),
@@ -107,7 +129,7 @@ fun SettingsAppearance(menuController: MenuController) {
ThemeItem( ThemeItem(
theme, theme,
onClick = { onClick = {
(if (isLight) vm.lightTheme else vm.darkTheme).value = it.id (if (isLight) lightTheme else darkTheme).value = it.id
activeColors.primaryStateFlow.value = it.colors.primary activeColors.primaryStateFlow.value = it.colors.primary
activeColors.secondaryStateFlow.value = it.colors.secondary activeColors.secondaryStateFlow.value = it.colors.secondary
} }
@@ -133,7 +155,7 @@ fun SettingsAppearance(menuController: MenuController) {
} }
item { item {
SwitchPreference( SwitchPreference(
vm.windowDecorations, windowDecorations,
stringResource(MR.strings.window_decorations), stringResource(MR.strings.window_decorations),
stringResource(MR.strings.window_decorations_sub) stringResource(MR.strings.window_decorations_sub)
) )

View File

@@ -27,7 +27,6 @@ import androidx.compose.material.icons.rounded.Warning
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -38,20 +37,23 @@ import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.server.interactions.BackupInteractionHandler import ca.gosyer.data.server.interactions.BackupInteractionHandler
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.WindowDialog import ca.gosyer.ui.base.WindowDialog
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.base.prefs.PreferenceRow import ca.gosyer.ui.base.prefs.PreferenceRow
import ca.gosyer.ui.util.system.filePicker
import ca.gosyer.ui.util.system.fileSaver
import ca.gosyer.uicore.resources.stringResource
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.util.system.filePicker import cafe.adriel.voyager.core.screen.Screen
import ca.gosyer.util.system.fileSaver import cafe.adriel.voyager.core.screen.ScreenKey
import ca.gosyer.uicore.resources.stringResource import cafe.adriel.voyager.core.screen.uniqueScreenKey
import io.ktor.client.features.onDownload import io.ktor.client.features.onDownload
import io.ktor.client.features.onUpload import io.ktor.client.features.onUpload
import io.ktor.http.isSuccess import io.ktor.http.isSuccess
import io.ktor.utils.io.jvm.javaio.toInputStream import io.ktor.utils.io.jvm.javaio.toInputStream
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -62,6 +64,29 @@ import okio.Path.Companion.toOkioPath
import okio.buffer import okio.buffer
import okio.source import okio.source
class SettingsBackupScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<SettingsBackupViewModel>()
SettingsBackupScreenContent(
restoring = vm.restoring.collectAsState().value,
restoringProgress = vm.restoringProgress.collectAsState().value,
restoreStatus = vm.restoreStatus.collectAsState().value,
creating = vm.creating.collectAsState().value,
creatingProgress = vm.creatingProgress.collectAsState().value,
creatingStatus = vm.creatingStatus.collectAsState().value,
missingSourceFlow = vm.missingSourceFlow,
createFlow = vm.createFlow,
restoreFile = vm::restoreFile,
restoreBackup = vm::restoreBackup,
stopRestore = vm::stopRestore,
exportBackup = vm::exportBackup
)
}
}
class SettingsBackupViewModel @Inject constructor( class SettingsBackupViewModel @Inject constructor(
private val backupHandler: BackupInteractionHandler private val backupHandler: BackupInteractionHandler
) : ViewModel() { ) : ViewModel() {
@@ -186,22 +211,28 @@ class SettingsBackupViewModel @Inject constructor(
} }
@Composable @Composable
fun SettingsBackupScreen(menuController: MenuController) { private fun SettingsBackupScreenContent(
val vm = viewModel<SettingsBackupViewModel>() restoring: Boolean,
val restoring by vm.restoring.collectAsState() restoringProgress: Float?,
val restoringProgress by vm.restoringProgress.collectAsState() restoreStatus: SettingsBackupViewModel.Status,
val restoreStatus by vm.restoreStatus.collectAsState() creating: Boolean,
val creating by vm.creating.collectAsState() creatingProgress: Float?,
val creatingProgress by vm.creatingProgress.collectAsState() creatingStatus: SettingsBackupViewModel.Status,
val creatingStatus by vm.creatingStatus.collectAsState() missingSourceFlow: SharedFlow<Pair<Path, List<String>>>,
createFlow: SharedFlow<Pair<String, (Path) -> Unit>>,
restoreFile: (Path?) -> Unit,
restoreBackup: (Path) -> Unit,
stopRestore: () -> Unit,
exportBackup: () -> Unit
) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
launch { launch {
vm.missingSourceFlow.collect { (backup, sources) -> missingSourceFlow.collect { (backup, sources) ->
openMissingSourcesDialog(sources, { vm.restoreBackup(backup) }, vm::stopRestore) openMissingSourcesDialog(sources, { restoreBackup(backup) }, stopRestore)
} }
} }
launch { launch {
vm.createFlow.collect { (filename, function) -> createFlow.collect { (filename, function) ->
fileSaver(filename, "proto.gz") { fileSaver(filename, "proto.gz") {
function(it.selectedFile.toOkioPath()) function(it.selectedFile.toOkioPath())
} }
@@ -210,7 +241,7 @@ fun SettingsBackupScreen(menuController: MenuController) {
} }
Column { Column {
Toolbar(stringResource(MR.strings.settings_backup_screen), menuController, true) Toolbar(stringResource(MR.strings.settings_backup_screen))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {
@@ -223,7 +254,7 @@ fun SettingsBackupScreen(menuController: MenuController) {
restoreStatus restoreStatus
) { ) {
filePicker("gz") { filePicker("gz") {
vm.restoreFile(it.selectedFile.toOkioPath()) restoreFile(it.selectedFile.toOkioPath())
} }
} }
PreferenceFile( PreferenceFile(
@@ -232,7 +263,7 @@ fun SettingsBackupScreen(menuController: MenuController) {
creating, creating,
creatingProgress, creatingProgress,
creatingStatus, creatingStatus,
vm::exportBackup exportBackup
) )
} }
} }

View File

@@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
class SettingsBrowseScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
SettingsBrowseScreenContent()
}
}
@Composable @Composable
fun SettingsBrowseScreen(menuController: MenuController) { fun SettingsBrowseScreenContent() {
Column { Column {
Toolbar(stringResource(MR.strings.settings_browse_screen), menuController, true) Toolbar(stringResource(MR.strings.settings_browse_screen))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {

View File

@@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
class SettingsDownloadsScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
SettingsDownloadsScreenContent()
}
}
@Composable @Composable
fun SettingsDownloadsScreen(menuController: MenuController) { fun SettingsDownloadsScreenContent() {
Column { Column {
Toolbar(stringResource(MR.strings.settings_download_screen), menuController, true) Toolbar(stringResource(MR.strings.settings_download_screen))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {

View File

@@ -23,13 +23,16 @@ import androidx.compose.ui.unit.dp
import ca.gosyer.data.ui.UiPreferences import ca.gosyer.data.ui.UiPreferences
import ca.gosyer.data.ui.model.StartScreen import ca.gosyer.data.ui.model.StartScreen
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.base.prefs.ChoicePreference import ca.gosyer.ui.base.prefs.ChoicePreference
import ca.gosyer.ui.base.prefs.SwitchPreference import ca.gosyer.ui.base.prefs.SwitchPreference
import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow
import ca.gosyer.uicore.resources.stringResource
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.uicore.resources.stringResource import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import okio.Path.Companion.toPath import okio.Path.Companion.toPath
import okio.asResourceFileSystem import okio.asResourceFileSystem
@@ -39,6 +42,24 @@ import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.Locale import java.util.Locale
class SettingsGeneralScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<SettingsGeneralViewModel>()
SettingsGeneralScreenContent(
startScreen = vm.startScreen,
startScreenChoices = vm.getStartScreenChoices(),
confirmExit = vm.confirmExit,
language = vm.language,
languageChoices = vm.getLanguageChoices(),
dateFormat = vm.dateFormat,
dateFormatChoices = vm.getDateChoices()
)
}
}
class SettingsGeneralViewModel @Inject constructor( class SettingsGeneralViewModel @Inject constructor(
uiPreferences: UiPreferences, uiPreferences: UiPreferences,
) : ViewModel() { ) : ViewModel() {
@@ -97,23 +118,30 @@ class SettingsGeneralViewModel @Inject constructor(
} }
@Composable @Composable
fun SettingsGeneralScreen(menuController: MenuController) { fun SettingsGeneralScreenContent(
val vm = viewModel<SettingsGeneralViewModel>() startScreen: PreferenceMutableStateFlow<StartScreen>,
startScreenChoices: Map<StartScreen, String>,
confirmExit: PreferenceMutableStateFlow<Boolean>,
language: PreferenceMutableStateFlow<String>,
languageChoices: Map<String, String>,
dateFormat: PreferenceMutableStateFlow<String>,
dateFormatChoices: Map<String, String>
) {
Column { Column {
Toolbar(stringResource(MR.strings.settings_general_screen), menuController, closable = true) Toolbar(stringResource(MR.strings.settings_general_screen))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {
item { item {
ChoicePreference( ChoicePreference(
preference = vm.startScreen, preference = startScreen,
title = stringResource(MR.strings.start_screen), title = stringResource(MR.strings.start_screen),
choices = vm.getStartScreenChoices() choices = startScreenChoices
) )
} }
item { item {
SwitchPreference( SwitchPreference(
preference = vm.confirmExit, preference = confirmExit,
title = stringResource(MR.strings.confirm_exit) title = stringResource(MR.strings.confirm_exit)
) )
} }
@@ -122,16 +150,16 @@ fun SettingsGeneralScreen(menuController: MenuController) {
} }
item { item {
ChoicePreference( ChoicePreference(
preference = vm.language, preference = language,
title = stringResource(MR.strings.language), title = stringResource(MR.strings.language),
choices = vm.getLanguageChoices(), choices = languageChoices,
) )
} }
item { item {
ChoicePreference( ChoicePreference(
preference = vm.dateFormat, preference = dateFormat,
title = stringResource(MR.strings.date_format), title = stringResource(MR.strings.date_format),
choices = vm.getDateChoices() choices = dateFormatChoices
) )
} }
} }

View File

@@ -23,19 +23,36 @@ import androidx.compose.ui.unit.dp
import ca.gosyer.data.library.LibraryPreferences import ca.gosyer.data.library.LibraryPreferences
import ca.gosyer.data.server.interactions.CategoryInteractionHandler import ca.gosyer.data.server.interactions.CategoryInteractionHandler
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.base.prefs.PreferenceRow import ca.gosyer.ui.base.prefs.PreferenceRow
import ca.gosyer.ui.base.prefs.SwitchPreference import ca.gosyer.ui.base.prefs.SwitchPreference
import ca.gosyer.ui.categories.openCategoriesMenu
import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow
import ca.gosyer.uicore.resources.stringResource
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.ui.categories.openCategoriesMenu import cafe.adriel.voyager.core.screen.Screen
import ca.gosyer.uicore.resources.stringResource import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class SettingsLibraryScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<SettingsLibraryViewModel>()
SettingsLibraryScreenContent(
showAllCategory = vm.showAllCategory,
refreshCategoryCount = vm::refreshCategoryCount,
categoriesSize = vm.categories.collectAsState().value
)
}
}
class SettingsLibraryViewModel @Inject constructor( class SettingsLibraryViewModel @Inject constructor(
libraryPreferences: LibraryPreferences, libraryPreferences: LibraryPreferences,
private val categoryHandler: CategoryInteractionHandler private val categoryHandler: CategoryInteractionHandler
@@ -57,25 +74,27 @@ class SettingsLibraryViewModel @Inject constructor(
} }
@Composable @Composable
fun SettingsLibraryScreen(menuController: MenuController) { fun SettingsLibraryScreenContent(
val vm = viewModel<SettingsLibraryViewModel>() showAllCategory: PreferenceMutableStateFlow<Boolean>,
refreshCategoryCount: () -> Unit,
categoriesSize: Int
) {
Column { Column {
Toolbar(stringResource(MR.strings.settings_library_screen), menuController, true) Toolbar(stringResource(MR.strings.settings_library_screen))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {
item { item {
SwitchPreference( SwitchPreference(
preference = vm.showAllCategory, preference = showAllCategory,
title = stringResource(MR.strings.show_all_category) title = stringResource(MR.strings.show_all_category)
) )
} }
item { item {
PreferenceRow( PreferenceRow(
stringResource(MR.strings.location_categories), stringResource(MR.strings.location_categories),
onClick = { openCategoriesMenu(vm::refreshCategoryCount) }, onClick = { openCategoriesMenu(refreshCategoryCount) },
subtitle = vm.categories.collectAsState().value.toString() subtitle = categoriesSize.toString()
) )
} }
} }

View File

@@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
class SettingsParentalControlsScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
SettingsParentalControlsScreenContent()
}
}
@Composable @Composable
fun SettingsParentalControlsScreen(menuController: MenuController) { fun SettingsParentalControlsScreenContent() {
Column { Column {
Toolbar(stringResource(MR.strings.settings_parental_control_screen), menuController, true) Toolbar(stringResource(MR.strings.settings_parental_control_screen))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {

View File

@@ -29,16 +29,18 @@ import ca.gosyer.data.reader.model.Direction
import ca.gosyer.data.reader.model.ImageScale import ca.gosyer.data.reader.model.ImageScale
import ca.gosyer.data.reader.model.NavigationMode import ca.gosyer.data.reader.model.NavigationMode
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.base.prefs.ChoicePreference import ca.gosyer.ui.base.prefs.ChoicePreference
import ca.gosyer.ui.base.prefs.ExpandablePreference import ca.gosyer.ui.base.prefs.ExpandablePreference
import ca.gosyer.ui.base.prefs.SwitchPreference import ca.gosyer.ui.base.prefs.SwitchPreference
import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow
import ca.gosyer.uicore.prefs.asStateIn import ca.gosyer.uicore.prefs.asStateIn
import ca.gosyer.uicore.resources.stringResource
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.uicore.resources.stringResource import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -46,6 +48,25 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class SettingsReaderScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<SettingsReaderViewModel>()
SettingsReaderScreenContent(
modes = vm.modes.collectAsState().value.associateWith { it },
selectedMode = vm.selectedMode,
modeSettings = vm.modeSettings.collectAsState().value,
directionChoices = vm.getDirectionChoices(),
paddingChoices = vm.getPaddingChoices(),
getMaxSizeChoices = vm::getMaxSizeChoices,
imageScaleChoices = vm.getImageScaleChoices(),
navigationModeChoices = vm.getNavigationModeChoices()
)
}
}
class SettingsReaderViewModel @Inject constructor( class SettingsReaderViewModel @Inject constructor(
readerPreferences: ReaderPreferences readerPreferences: ReaderPreferences
) : ViewModel() { ) : ViewModel() {
@@ -125,18 +146,25 @@ data class ReaderModePreference(
} }
@Composable @Composable
fun SettingsReaderScreen(menuController: MenuController) { fun SettingsReaderScreenContent(
val vm = viewModel<SettingsReaderViewModel>() modes: Map<String, String>,
val modeSettings by vm.modeSettings.collectAsState() selectedMode: PreferenceMutableStateFlow<String>,
modeSettings: List<ReaderModePreference>,
directionChoices: Map<Direction, String>,
paddingChoices: Map<Int, String>,
getMaxSizeChoices: (Direction) -> Map<Int, String>,
imageScaleChoices: Map<ImageScale, String>,
navigationModeChoices: Map<NavigationMode, String>
) {
Column { Column {
Toolbar(stringResource(MR.strings.settings_reader), menuController, true) Toolbar(stringResource(MR.strings.settings_reader))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {
item { item {
ChoicePreference( ChoicePreference(
vm.selectedMode, selectedMode,
vm.modes.collectAsState().value.associateWith { it }, modes,
stringResource(MR.strings.reader_mode) stringResource(MR.strings.reader_mode)
) )
} }
@@ -148,7 +176,7 @@ fun SettingsReaderScreen(menuController: MenuController) {
ExpandablePreference(it.mode) { ExpandablePreference(it.mode) {
ChoicePreference( ChoicePreference(
it.direction, it.direction,
vm.getDirectionChoices(), directionChoices,
stringResource(MR.strings.direction), stringResource(MR.strings.direction),
enabled = !it.defaultMode enabled = !it.defaultMode
) )
@@ -162,7 +190,7 @@ fun SettingsReaderScreen(menuController: MenuController) {
if (continuous) { if (continuous) {
ChoicePreference( ChoicePreference(
it.padding, it.padding,
vm.getPaddingChoices(), paddingChoices,
stringResource(MR.strings.page_padding) stringResource(MR.strings.page_padding)
) )
val direction by it.direction.collectAsState() val direction by it.direction.collectAsState()
@@ -190,20 +218,20 @@ fun SettingsReaderScreen(menuController: MenuController) {
} }
ChoicePreference( ChoicePreference(
it.maxSize, it.maxSize,
vm.getMaxSizeChoices(direction), getMaxSizeChoices(direction),
maxSizeTitle, maxSizeTitle,
maxSizeSubtitle maxSizeSubtitle
) )
} else { } else {
ChoicePreference( ChoicePreference(
it.imageScale, it.imageScale,
vm.getImageScaleChoices(), imageScaleChoices,
stringResource(MR.strings.image_scale) stringResource(MR.strings.image_scale)
) )
} }
ChoicePreference( ChoicePreference(
it.navigationMode, it.navigationMode,
vm.getNavigationModeChoices(), navigationModeChoices,
stringResource(MR.strings.navigation_mode) stringResource(MR.strings.navigation_mode)
) )
} }

View File

@@ -29,14 +29,28 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.base.prefs.PreferenceRow import ca.gosyer.ui.base.prefs.PreferenceRow
import ca.gosyer.ui.main.Routes
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
class SettingsScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
SettingsScreenContent(navigator = LocalNavigator.currentOrThrow)
}
}
@Composable @Composable
fun SettingsScreen(menuController: MenuController) { fun SettingsScreenContent(navigator: Navigator) {
Column { Column {
Toolbar(stringResource(MR.strings.location_settings), closable = false) Toolbar(stringResource(MR.strings.location_settings), closable = false)
Box { Box {
@@ -46,84 +60,84 @@ fun SettingsScreen(menuController: MenuController) {
PreferenceRow( PreferenceRow(
title = stringResource(MR.strings.settings_general), title = stringResource(MR.strings.settings_general),
icon = Icons.Rounded.Tune, icon = Icons.Rounded.Tune,
onClick = { menuController.push(Routes.SettingsGeneral) } onClick = { navigator push SettingsGeneralScreen() }
) )
} }
item { item {
PreferenceRow( PreferenceRow(
title = stringResource(MR.strings.settings_appearance), title = stringResource(MR.strings.settings_appearance),
icon = Icons.Rounded.Palette, icon = Icons.Rounded.Palette,
onClick = { menuController.push(Routes.SettingsAppearance) } onClick = { navigator push SettingsAppearanceScreen() }
) )
} }
item { item {
PreferenceRow( PreferenceRow(
title = stringResource(MR.strings.settings_server), title = stringResource(MR.strings.settings_server),
icon = Icons.Rounded.Computer, icon = Icons.Rounded.Computer,
onClick = { menuController.push(Routes.SettingsServer) } onClick = { navigator push SettingsServerScreen() }
) )
} }
item { item {
PreferenceRow( PreferenceRow(
title = stringResource(MR.strings.settings_library), title = stringResource(MR.strings.settings_library),
icon = Icons.Rounded.CollectionsBookmark, icon = Icons.Rounded.CollectionsBookmark,
onClick = { menuController.push(Routes.SettingsLibrary) } onClick = { navigator push SettingsLibraryScreen() }
) )
} }
item { item {
PreferenceRow( PreferenceRow(
title = stringResource(MR.strings.settings_reader), title = stringResource(MR.strings.settings_reader),
icon = Icons.Rounded.ChromeReaderMode, icon = Icons.Rounded.ChromeReaderMode,
onClick = { menuController.push(Routes.SettingsReader) } onClick = { navigator push SettingsReaderScreen() }
) )
} }
/*item { /*item {
Pref( Pref(
title = stringResource(MR.strings.settings_download), title = stringResource(MR.strings.settings_download),
icon = Icons.Rounded.GetApp, icon = Icons.Rounded.GetApp,
onClick = { navController.push(Route.SettingsDownloads) } onClick = { navigator push SettingsDownloadsScreen() }
) )
} }
item { item {
Pref( Pref(
title = stringResource(MR.strings.settings_tracking), title = stringResource(MR.strings.settings_tracking),
icon = Icons.Rounded.Sync, icon = Icons.Rounded.Sync,
onClick = { navController.push(Route.SettingsTracking) } onClick = { navigator push SettingsTrackingScreen() }
) )
}*/ }*/
item { item {
PreferenceRow( PreferenceRow(
title = stringResource(MR.strings.settings_browse), title = stringResource(MR.strings.settings_browse),
icon = Icons.Rounded.Explore, icon = Icons.Rounded.Explore,
onClick = { menuController.push(Routes.SettingsBrowse) } onClick = { navigator push SettingsBrowseScreen() }
) )
} }
item { item {
PreferenceRow( PreferenceRow(
title = stringResource(MR.strings.settings_backup), title = stringResource(MR.strings.settings_backup),
icon = Icons.Rounded.Backup, icon = Icons.Rounded.Backup,
onClick = { menuController.push(Routes.SettingsBackup) } onClick = { navigator push SettingsBackupScreen() }
) )
} }
/*item { /*item {
Pref( Pref(
title = stringResource(MR.strings.settings_security), title = stringResource(MR.strings.settings_security),
icon = Icons.Rounded.Security, icon = Icons.Rounded.Security,
onClick = { navController.push(Route.SettingsSecurity) } onClick = { navigator push SettingsSecurityScreen() }
) )
} }
item { item {
Pref( Pref(
title = stringResource(MR.strings.settings_parental_controls), title = stringResource(MR.strings.settings_parental_controls),
icon = Icons.Rounded.PeopleOutline, icon = Icons.Rounded.PeopleOutline,
onClick = { navController.push(Route.SettingsParentalControls) } onClick = { navigator push SettingsParentalControlsScreen() }
) )
}*/ }*/
item { item {
PreferenceRow( PreferenceRow(
title = stringResource(MR.strings.settings_advanced), title = stringResource(MR.strings.settings_advanced),
icon = Icons.Rounded.Code, icon = Icons.Rounded.Code,
onClick = { menuController.push(Routes.SettingsAdvanced) } onClick = { navigator push SettingsAdvancedScreen() }
) )
} }
} }

View File

@@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
class SettingsSecurityScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
SettingsSecurityScreenContent()
}
}
@Composable @Composable
fun SettingsSecurityScreen(menuController: MenuController) { fun SettingsSecurityScreenContent() {
Column { Column {
Toolbar(stringResource(MR.strings.settings_security_screen), menuController, true) Toolbar(stringResource(MR.strings.settings_security_screen))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {

View File

@@ -34,23 +34,68 @@ import ca.gosyer.data.server.ServerService
import ca.gosyer.data.server.model.Auth import ca.gosyer.data.server.model.Auth
import ca.gosyer.data.server.model.Proxy import ca.gosyer.data.server.model.Proxy
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.base.prefs.ChoicePreference import ca.gosyer.ui.base.prefs.ChoicePreference
import ca.gosyer.ui.base.prefs.EditTextPreference import ca.gosyer.ui.base.prefs.EditTextPreference
import ca.gosyer.ui.base.prefs.PreferenceRow import ca.gosyer.ui.base.prefs.PreferenceRow
import ca.gosyer.ui.base.prefs.SwitchPreference import ca.gosyer.ui.base.prefs.SwitchPreference
import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow
import ca.gosyer.uicore.prefs.asStateIn import ca.gosyer.uicore.prefs.asStateIn
import ca.gosyer.uicore.prefs.asStringStateIn import ca.gosyer.uicore.prefs.asStringStateIn
import ca.gosyer.uicore.resources.stringResource
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.uicore.resources.stringResource import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class SettingsServerScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<SettingsServerViewModel>()
SettingsServerScreenContent(
hostValue = vm.host.collectAsState().value,
basicAuthEnabledValue = vm.basicAuthEnabled.collectAsState().value,
proxyValue = vm.proxy.collectAsState().value,
authValue = vm.auth.collectAsState().value,
restartServer = vm::restartServer,
serverSettingChanged = vm::serverSettingChanged,
host = vm.host,
ip = vm.ip,
port = vm.port,
socksProxyEnabled = vm.socksProxyEnabled,
socksProxyHost = vm.socksProxyHost,
socksProxyPort = vm.socksProxyPort,
debugLogsEnabled = vm.debugLogsEnabled,
systemTrayEnabled = vm.systemTrayEnabled,
webUIEnabled = vm.webUIEnabled,
openInBrowserEnabled = vm.openInBrowserEnabled,
basicAuthEnabled = vm.basicAuthEnabled,
basicAuthUsername = vm.basicAuthUsername,
basicAuthPassword = vm.basicAuthPassword,
serverUrl = vm.serverUrl,
serverPort = vm.serverPort,
proxy = vm.proxy,
proxyChoices = vm.getProxyChoices(),
httpHost = vm.httpHost,
httpPort = vm.httpPort,
socksHost = vm.socksHost,
socksPort = vm.socksPort,
auth = vm.auth,
authChoices = vm.getAuthChoices(),
authUsername = vm.authUsername,
authPassword = vm.authPassword
)
}
}
class SettingsServerViewModel @Inject constructor( class SettingsServerViewModel @Inject constructor(
serverPreferences: ServerPreferences, serverPreferences: ServerPreferences,
serverHostPreferences: ServerHostPreferences, serverHostPreferences: ServerHostPreferences,
@@ -136,26 +181,53 @@ class SettingsServerViewModel @Inject constructor(
} }
@Composable @Composable
fun SettingsServerScreen(menuController: MenuController) { fun SettingsServerScreenContent(
val vm = viewModel<SettingsServerViewModel>() hostValue: Boolean,
val host by vm.host.collectAsState() basicAuthEnabledValue: Boolean,
val basicAuthEnabled by vm.basicAuthEnabled.collectAsState() proxyValue: Proxy,
val proxy by vm.proxy.collectAsState() authValue: Auth,
val auth by vm.auth.collectAsState() restartServer: () -> Unit,
serverSettingChanged: () -> Unit,
host: PreferenceMutableStateFlow<Boolean>,
ip: PreferenceMutableStateFlow<String>,
port: PreferenceMutableStateFlow<String>,
socksProxyEnabled: PreferenceMutableStateFlow<Boolean>,
socksProxyHost: PreferenceMutableStateFlow<String>,
socksProxyPort: PreferenceMutableStateFlow<String>,
debugLogsEnabled: PreferenceMutableStateFlow<Boolean>,
systemTrayEnabled: PreferenceMutableStateFlow<Boolean>,
webUIEnabled: PreferenceMutableStateFlow<Boolean>,
openInBrowserEnabled: PreferenceMutableStateFlow<Boolean>,
basicAuthEnabled: PreferenceMutableStateFlow<Boolean>,
basicAuthUsername: PreferenceMutableStateFlow<String>,
basicAuthPassword: PreferenceMutableStateFlow<String>,
serverUrl: PreferenceMutableStateFlow<String>,
serverPort: PreferenceMutableStateFlow<String>,
proxy: PreferenceMutableStateFlow<Proxy>,
proxyChoices: Map<Proxy, String>,
httpHost: PreferenceMutableStateFlow<String>,
httpPort: PreferenceMutableStateFlow<String>,
socksHost: PreferenceMutableStateFlow<String>,
socksPort: PreferenceMutableStateFlow<String>,
auth: PreferenceMutableStateFlow<Auth>,
authChoices: Map<Auth, String>,
authUsername: PreferenceMutableStateFlow<String>,
authPassword: PreferenceMutableStateFlow<String>
) {
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
vm.restartServer() restartServer()
} }
} }
Column { Column {
Toolbar(stringResource(MR.strings.settings_server_screen), menuController, true) Toolbar(stringResource(MR.strings.settings_server_screen))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {
item { item {
SwitchPreference(preference = vm.host, title = stringResource(MR.strings.host_server)) SwitchPreference(preference = host, title = stringResource(MR.strings.host_server))
} }
if (host) { if (hostValue) {
item { item {
PreferenceRow( PreferenceRow(
stringResource(MR.strings.host_settings), stringResource(MR.strings.host_settings),
@@ -164,105 +236,105 @@ fun SettingsServerScreen(menuController: MenuController) {
) )
} }
item { item {
val ip by vm.ip.collectAsState() val ipValue by ip.collectAsState()
EditTextPreference( EditTextPreference(
preference = vm.ip, preference = ip,
title = stringResource(MR.strings.host_ip), title = stringResource(MR.strings.host_ip),
subtitle = stringResource(MR.strings.host_ip_sub, ip), subtitle = stringResource(MR.strings.host_ip_sub, ipValue),
changeListener = vm::serverSettingChanged changeListener = serverSettingChanged
) )
} }
item { item {
val port by vm.port.collectAsState() val portValue by port.collectAsState()
EditTextPreference( EditTextPreference(
preference = vm.port, preference = port,
title = stringResource(MR.strings.host_port), title = stringResource(MR.strings.host_port),
subtitle = stringResource(MR.strings.host_port_sub, port), subtitle = stringResource(MR.strings.host_port_sub, portValue),
changeListener = vm::serverSettingChanged changeListener = serverSettingChanged
) )
} }
item { item {
SwitchPreference( SwitchPreference(
preference = vm.socksProxyEnabled, preference = socksProxyEnabled,
title = stringResource(MR.strings.host_socks_enabled), title = stringResource(MR.strings.host_socks_enabled),
changeListener = vm::serverSettingChanged changeListener = serverSettingChanged
) )
} }
item { item {
val proxyHost by vm.socksProxyHost.collectAsState() val proxyHost by socksProxyHost.collectAsState()
EditTextPreference( EditTextPreference(
preference = vm.socksProxyHost, preference = socksProxyHost,
title = stringResource(MR.strings.host_socks_host), title = stringResource(MR.strings.host_socks_host),
subtitle = stringResource(MR.strings.host_socks_host_sub, proxyHost), subtitle = stringResource(MR.strings.host_socks_host_sub, proxyHost),
changeListener = vm::serverSettingChanged changeListener = serverSettingChanged
) )
} }
item { item {
val proxyPort by vm.socksProxyPort.collectAsState() val proxyPort by socksProxyPort.collectAsState()
EditTextPreference( EditTextPreference(
preference = vm.socksProxyPort, preference = socksProxyPort,
title = stringResource(MR.strings.host_socks_port), title = stringResource(MR.strings.host_socks_port),
subtitle = stringResource(MR.strings.host_socks_port_sub, proxyPort), subtitle = stringResource(MR.strings.host_socks_port_sub, proxyPort),
changeListener = vm::serverSettingChanged changeListener = serverSettingChanged
) )
} }
item { item {
SwitchPreference( SwitchPreference(
preference = vm.debugLogsEnabled, preference = debugLogsEnabled,
title = stringResource(MR.strings.host_debug_logging), title = stringResource(MR.strings.host_debug_logging),
subtitle = stringResource(MR.strings.host_debug_logging_sub), subtitle = stringResource(MR.strings.host_debug_logging_sub),
changeListener = vm::serverSettingChanged changeListener = serverSettingChanged
) )
} }
item { item {
SwitchPreference( SwitchPreference(
preference = vm.systemTrayEnabled, preference = systemTrayEnabled,
title = stringResource(MR.strings.host_system_tray), title = stringResource(MR.strings.host_system_tray),
subtitle = stringResource(MR.strings.host_system_tray_sub), subtitle = stringResource(MR.strings.host_system_tray_sub),
changeListener = vm::serverSettingChanged changeListener = serverSettingChanged
) )
} }
item { item {
SwitchPreference( SwitchPreference(
preference = vm.webUIEnabled, preference = webUIEnabled,
title = stringResource(MR.strings.host_webui), title = stringResource(MR.strings.host_webui),
subtitle = stringResource(MR.strings.host_webui_sub), subtitle = stringResource(MR.strings.host_webui_sub),
changeListener = vm::serverSettingChanged changeListener = serverSettingChanged
) )
} }
item { item {
val webUIEnabled by vm.webUIEnabled.collectAsState() val webUIEnabledValue by webUIEnabled.collectAsState()
SwitchPreference( SwitchPreference(
preference = vm.openInBrowserEnabled, preference = openInBrowserEnabled,
title = stringResource(MR.strings.host_open_in_browser), title = stringResource(MR.strings.host_open_in_browser),
subtitle = stringResource(MR.strings.host_open_in_browser_sub), subtitle = stringResource(MR.strings.host_open_in_browser_sub),
changeListener = vm::serverSettingChanged, changeListener = serverSettingChanged,
enabled = webUIEnabled enabled = webUIEnabledValue
) )
} }
item { item {
SwitchPreference( SwitchPreference(
preference = vm.basicAuthEnabled, preference = basicAuthEnabled,
title = stringResource(MR.strings.basic_auth), title = stringResource(MR.strings.basic_auth),
subtitle = stringResource(MR.strings.host_basic_auth_sub), subtitle = stringResource(MR.strings.host_basic_auth_sub),
changeListener = vm::serverSettingChanged changeListener = serverSettingChanged
) )
} }
item { item {
EditTextPreference( EditTextPreference(
preference = vm.basicAuthUsername, preference = basicAuthUsername,
title = stringResource(MR.strings.host_basic_auth_username), title = stringResource(MR.strings.host_basic_auth_username),
changeListener = vm::serverSettingChanged, changeListener = serverSettingChanged,
enabled = basicAuthEnabled enabled = basicAuthEnabledValue
) )
} }
item { item {
EditTextPreference( EditTextPreference(
preference = vm.basicAuthPassword, preference = basicAuthPassword,
title = stringResource(MR.strings.host_basic_auth_password), title = stringResource(MR.strings.host_basic_auth_password),
changeListener = vm::serverSettingChanged, changeListener = serverSettingChanged,
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
enabled = basicAuthEnabled enabled = basicAuthEnabledValue
) )
} }
} }
@@ -271,16 +343,16 @@ fun SettingsServerScreen(menuController: MenuController) {
} }
item { item {
EditTextPreference( EditTextPreference(
vm.serverUrl, serverUrl,
stringResource(MR.strings.server_url), stringResource(MR.strings.server_url),
subtitle = vm.serverUrl.collectAsState().value subtitle = serverUrl.collectAsState().value
) )
} }
item { item {
EditTextPreference( EditTextPreference(
vm.serverPort, serverPort,
stringResource(MR.strings.server_port), stringResource(MR.strings.server_port),
subtitle = vm.serverPort.collectAsState().value subtitle = serverPort.collectAsState().value
) )
} }
@@ -292,53 +364,53 @@ fun SettingsServerScreen(menuController: MenuController) {
) )
} }
item { item {
ChoicePreference(vm.proxy, vm.getProxyChoices(), stringResource(MR.strings.server_proxy)) ChoicePreference(proxy, proxyChoices, stringResource(MR.strings.server_proxy))
} }
when (proxy) { when (proxyValue) {
Proxy.NO_PROXY -> Unit Proxy.NO_PROXY -> Unit
Proxy.HTTP_PROXY -> { Proxy.HTTP_PROXY -> {
item { item {
EditTextPreference( EditTextPreference(
vm.httpHost, httpHost,
stringResource(MR.strings.http_proxy), stringResource(MR.strings.http_proxy),
vm.httpHost.collectAsState().value httpHost.collectAsState().value
) )
} }
item { item {
EditTextPreference( EditTextPreference(
vm.httpPort, httpPort,
stringResource(MR.strings.http_port), stringResource(MR.strings.http_port),
vm.httpPort.collectAsState().value httpPort.collectAsState().value
) )
} }
} }
Proxy.SOCKS_PROXY -> { Proxy.SOCKS_PROXY -> {
item { item {
EditTextPreference( EditTextPreference(
vm.socksHost, socksHost,
stringResource(MR.strings.socks_proxy), stringResource(MR.strings.socks_proxy),
vm.socksHost.collectAsState().value socksHost.collectAsState().value
) )
} }
item { item {
EditTextPreference( EditTextPreference(
vm.socksPort, socksPort,
stringResource(MR.strings.socks_port), stringResource(MR.strings.socks_port),
vm.socksPort.collectAsState().value socksPort.collectAsState().value
) )
} }
} }
} }
item { item {
ChoicePreference(vm.auth, vm.getAuthChoices(), stringResource(MR.strings.authentication)) ChoicePreference(auth, authChoices, stringResource(MR.strings.authentication))
} }
if (auth != Auth.NONE) { if (authValue != Auth.NONE) {
item { item {
EditTextPreference(vm.authUsername, stringResource(MR.strings.auth_username)) EditTextPreference(authUsername, stringResource(MR.strings.auth_username))
} }
item { item {
EditTextPreference( EditTextPreference(
vm.authPassword, authPassword,
stringResource(MR.strings.auth_password), stringResource(MR.strings.auth_password),
visualTransformation = PasswordVisualTransformation() visualTransformation = PasswordVisualTransformation()
) )

View File

@@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
class SettingsTrackingScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
SettingsTrackingScreenContent()
}
}
@Composable @Composable
fun SettingsTrackingScreen(menuController: MenuController) { fun SettingsTrackingScreenContent() {
Column { Column {
Toolbar(stringResource(MR.strings.settings_tracking_screen), menuController, true) Toolbar(stringResource(MR.strings.settings_tracking_screen))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {

View File

@@ -1,85 +0,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.sources
import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.models.Source
import ca.gosyer.uicore.vm.ViewModel
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 me.tatarka.inject.annotations.Inject
class SourcesMenuViewModel @Inject constructor(
private val bundle: Bundle
) : ViewModel() {
private val _sourceTabs = MutableStateFlow<List<Source?>>(listOf(null))
val sourceTabs = _sourceTabs.asStateFlow()
private val _selectedSourceTab = MutableStateFlow<Source?>(null)
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)
}
fun selectTab(source: Source?) {
_selectedSourceTab.value = source
}
fun addTab(source: Source) {
if (source !in _sourceTabs.value) {
_sourceTabs.value += source
}
selectTab(source)
}
fun closeTab(source: Source) {
_sourceTabs.value -= source
if (selectedSourceTab.value?.id == source.id) {
_selectedSourceTab.value = null
}
bundle.remove(source.id.toString())
}
fun setLoadedSources(sources: List<Source>) {
val sourceTabs = bundle.getLongArray(SOURCE_TABS_KEY)
if (sourceTabs != null) {
_sourceTabs.value = listOf(null) + sourceTabs.toList()
.mapNotNull { sourceId ->
sources.find { it.id == sourceId }
}
_selectedSourceTab.value = bundle.getLong(SELECTED_SOURCE_TAB, -1).let { id ->
if (id != -1L) {
sources.find { it.id == id }
} else null
}
}
}
private companion object : CKLogger({}) {
const val SOURCE_TABS_KEY = "source_tabs"
const val SELECTED_SOURCE_TAB = "selected_tab"
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.sources
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.sources.components.SourcesMenu
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.Navigator
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openSourcesMenu() {
launchApplication {
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
Surface {
Navigator(remember { SourcesScreen() })
}
}
}
}
class SourcesScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<SourcesScreenViewModel>()
SourcesMenu(
sourceTabs = vm.sourceTabs.collectAsState().value,
selectedSourceTab = vm.selectedSourceTab.collectAsState().value,
selectTab = vm::selectTab,
closeTab = vm::closeTab
)
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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.sources
import ca.gosyer.data.models.Source
import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import me.tatarka.inject.annotations.Inject
class SourcesScreenViewModel @Inject constructor() : ViewModel() {
private val _sourceTabs = MutableStateFlow<List<Source?>>(listOf(null))
val sourceTabs = _sourceTabs.asStateFlow()
private val _selectedSourceTab = MutableStateFlow<Source?>(null)
val selectedSourceTab = _selectedSourceTab.asStateFlow()
fun selectTab(source: Source?) {
if (source !in _sourceTabs.value) {
_sourceTabs.value += source
}
_selectedSourceTab.value = source
}
fun closeTab(source: Source) {
_sourceTabs.value -= source
if (selectedSourceTab.value?.id == source.id) {
_selectedSourceTab.value = null
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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.sources.browse
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import ca.gosyer.data.models.Source
import ca.gosyer.ui.manga.MangaScreen
import ca.gosyer.ui.sources.browse.components.SourceScreenContent
import ca.gosyer.ui.sources.browse.filter.SourceFiltersViewModel
import ca.gosyer.ui.sources.components.LocalSourcesNavigator
import ca.gosyer.ui.sources.settings.SourceSettingsScreen
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
class SourceScreen(val source: Source) : Screen {
override val key: ScreenKey = source.id.toString()
@Composable
override fun Content() {
val sourceVM = viewModel {
instantiate<SourceScreenViewModel>(SourceScreenViewModel.Params(source))
}
val filterVM = viewModel {
instantiate<SourceFiltersViewModel>(SourceFiltersViewModel.Params(source.id))
}
val navigator = LocalNavigator.currentOrThrow
val sourcesNavigator = LocalSourcesNavigator.current
SourceScreenContent(
source = source,
onMangaClick = { navigator push MangaScreen(it) },
onCloseSourceTabClick = sourcesNavigator::remove,
onSourceSettingsClick = { navigator push SourceSettingsScreen(it) },
mangas = sourceVM.mangas.collectAsState().value,
hasNextPage = sourceVM.hasNextPage.collectAsState().value,
loading = sourceVM.loading.collectAsState().value,
isLatest = sourceVM.isLatest.collectAsState().value,
showLatestButton = source.supportsLatest,
sourceSearchQuery = sourceVM.sourceSearchQuery.collectAsState().value,
enableLatest = sourceVM::enableLatest,
search = sourceVM::search,
submitSearch = sourceVM::submitSearch,
setMode = sourceVM::setMode,
loadNextPage = sourceVM::loadNextPage,
setUsingFilters = sourceVM::setUsingFilters,
// FilterVM
filters = filterVM.filters.collectAsState().value,
showingFilters = filterVM.showingFilters.collectAsState().value,
showFilterButton = filterVM.filterButtonEnabled.collectAsState().value,
setShowingFilters = filterVM::showingFilters,
resetFiltersClicked = {
sourceVM.setUsingFilters(false)
filterVM.resetFilters()
}
)
}
}

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.sources.components package ca.gosyer.ui.sources.browse
import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.lang.throwIfCancellation
import ca.gosyer.data.models.Manga import ca.gosyer.data.models.Manga
@@ -12,11 +12,6 @@ import ca.gosyer.data.models.MangaPage
import ca.gosyer.data.models.Source import ca.gosyer.data.models.Source
import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.util.compose.saveBooleanInBundle
import ca.gosyer.util.compose.saveIntInBundle
import ca.gosyer.util.compose.saveObjectInBundle
import ca.gosyer.util.compose.saveStringInBundle
import com.github.zsoltk.compose.savedinstancestate.Bundle
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -24,7 +19,6 @@ import me.tatarka.inject.annotations.Inject
class SourceScreenViewModel( class SourceScreenViewModel(
private val source: Source, private val source: Source,
private val bundle: Bundle,
private val sourceHandler: SourceInteractionHandler private val sourceHandler: SourceInteractionHandler
) : ViewModel() { ) : ViewModel() {
@@ -33,49 +27,40 @@ class SourceScreenViewModel(
params: Params params: Params
) : this( ) : this(
params.source, params.source,
params.bundle,
sourceHandler sourceHandler
) )
private val _mangas = saveObjectInBundle(scope, bundle, MANGAS_KEY) { emptyList<Manga>() } private val _mangas = MutableStateFlow(emptyList<Manga>())
val mangas = _mangas.asStateFlow() val mangas = _mangas.asStateFlow()
private val _hasNextPage = saveBooleanInBundle(scope, bundle, NEXT_PAGE_KEY, false) private val _hasNextPage = MutableStateFlow(false)
val hasNextPage = _hasNextPage.asStateFlow() val hasNextPage = _hasNextPage.asStateFlow()
private val _loading = MutableStateFlow(true) private val _loading = MutableStateFlow(true)
val loading = _loading.asStateFlow() val loading = _loading.asStateFlow()
private val _isLatest = saveBooleanInBundle(scope, bundle, IS_LATEST_KEY, false) private val _isLatest = MutableStateFlow(false)
val isLatest = _isLatest.asStateFlow() val isLatest = _isLatest.asStateFlow()
private val _filterButtonEnabled = saveBooleanInBundle(scope, bundle, SHOW_FILTERS, false) private val _latestButtonEnabled = MutableStateFlow(false)
val filterButtonEnabled = _filterButtonEnabled.asStateFlow()
private val _latestButtonEnabled = saveBooleanInBundle(scope, bundle, SHOW_LATEST, false)
val latestButtonEnabled = _latestButtonEnabled.asStateFlow() val latestButtonEnabled = _latestButtonEnabled.asStateFlow()
private val _showingFilters = MutableStateFlow(false)
val showingFilters = _showingFilters.asStateFlow()
private val _usingFilters = MutableStateFlow(false) private val _usingFilters = MutableStateFlow(false)
private val _sourceSearchQuery = MutableStateFlow<String?>(null) private val _sourceSearchQuery = MutableStateFlow<String?>(null)
val sourceSearchQuery = _sourceSearchQuery.asStateFlow() val sourceSearchQuery = _sourceSearchQuery.asStateFlow()
private val _query = saveStringInBundle(scope, bundle, QUERY_KEY) { null } private val _query = MutableStateFlow<String?>(null)
private val _pageNum = saveIntInBundle(scope, bundle, PAGE_NUM_KEY, 1) private val _pageNum = MutableStateFlow(1)
val pageNum = _pageNum.asStateFlow() val pageNum = _pageNum.asStateFlow()
init { init {
scope.launch { scope.launch {
try { try {
if (bundle[MANGAS_KEY] == null) { val (mangas, hasNextPage) = getPage()
val (mangas, hasNextPage) = getPage() _mangas.value = mangas
_mangas.value = mangas _hasNextPage.value = hasNextPage
_hasNextPage.value = hasNextPage
}
} catch (e: Exception) { } catch (e: Exception) {
e.throwIfCancellation() e.throwIfCancellation()
} finally { } finally {
@@ -103,19 +88,8 @@ class SourceScreenViewModel(
} }
} }
private fun cleanBundle(removeMode: Boolean = true) {
bundle.remove(MANGAS_KEY)
bundle.remove(NEXT_PAGE_KEY)
bundle.remove(PAGE_NUM_KEY)
if (removeMode) {
bundle.remove(IS_LATEST_KEY)
}
bundle.remove(QUERY_KEY)
}
fun setMode(toLatest: Boolean) { fun setMode(toLatest: Boolean) {
if (isLatest.value != toLatest) { if (isLatest.value != toLatest) {
cleanBundle()
_isLatest.value = toLatest _isLatest.value = toLatest
// [loadNextPage] increments by 1 // [loadNextPage] increments by 1
_pageNum.value = 0 _pageNum.value = 0
@@ -135,7 +109,6 @@ class SourceScreenViewModel(
} }
fun startSearch(query: String?) { fun startSearch(query: String?) {
cleanBundle(false)
_pageNum.value = 0 _pageNum.value = 0
_hasNextPage.value = true _hasNextPage.value = true
_loading.value = true _loading.value = true
@@ -144,15 +117,9 @@ class SourceScreenViewModel(
loadNextPage() loadNextPage()
} }
fun showingFilters(show: Boolean) {
_showingFilters.value = show
}
fun setUsingFilters(usingFilters: Boolean) { fun setUsingFilters(usingFilters: Boolean) {
_usingFilters.value = usingFilters _usingFilters.value = usingFilters
} }
fun enableFilters(enabled: Boolean) {
_filterButtonEnabled.value = enabled
}
fun enableLatest(enabled: Boolean) { fun enableLatest(enabled: Boolean) {
_latestButtonEnabled.value = enabled _latestButtonEnabled.value = enabled
} }
@@ -164,15 +131,5 @@ class SourceScreenViewModel(
startSearch(sourceSearchQuery.value) startSearch(sourceSearchQuery.value)
} }
data class Params(val source: Source, val bundle: Bundle) data class Params(val source: Source)
private companion object {
const val MANGAS_KEY = "mangas"
const val NEXT_PAGE_KEY = "next_page"
const val PAGE_NUM_KEY = "page_num"
const val IS_LATEST_KEY = "is_latest"
const val SHOW_FILTERS = "show_filters"
const val SHOW_LATEST = "show_latest"
const val QUERY_KEY = "query"
}
} }

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.sources.components package ca.gosyer.ui.sources.browse.components
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Explore import androidx.compose.material.icons.rounded.Explore
@@ -22,8 +23,6 @@ import androidx.compose.material.icons.rounded.NewReleases
import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Settings
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.FilterQuality
@@ -33,38 +32,40 @@ import ca.gosyer.data.models.Source
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.TextActionIcon import ca.gosyer.ui.base.navigation.TextActionIcon
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.sources.components.filter.SourceFiltersMenu import ca.gosyer.ui.sources.browse.filter.SourceFiltersMenu
import ca.gosyer.ui.sources.browse.filter.model.SourceFiltersView
import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.components.MangaGridItem import ca.gosyer.uicore.components.MangaGridItem
import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.util.compose.persistentLazyListState
import com.github.zsoltk.compose.savedinstancestate.Bundle
import com.github.zsoltk.compose.savedinstancestate.BundleScope
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import io.kamel.image.lazyPainterResource import io.kamel.image.lazyPainterResource
@Composable @Composable
fun SourceScreen( fun SourceScreenContent(
bundle: Bundle,
source: Source, source: Source,
onMangaClick: (Long) -> Unit, onMangaClick: (Long) -> Unit,
onCloseSourceTabClick: (Source) -> Unit, onCloseSourceTabClick: (Source) -> Unit,
onSourceSettingsClick: (Long) -> Unit onSourceSettingsClick: (Long) -> Unit,
mangas: List<Manga>,
hasNextPage: Boolean,
loading: Boolean,
isLatest: Boolean,
showLatestButton: Boolean,
sourceSearchQuery: String?,
enableLatest: (Boolean) -> Unit,
search: (String) -> Unit,
submitSearch: () -> Unit,
setMode: (Boolean) -> Unit,
loadNextPage: () -> Unit,
setUsingFilters: (Boolean) -> Unit,
// filter
filters: List<SourceFiltersView<*, *>>,
showingFilters: Boolean,
showFilterButton: Boolean,
setShowingFilters: (Boolean) -> Unit,
resetFiltersClicked: () -> Unit
) { ) {
val vm = viewModel(source.id) { LaunchedEffect(source) {
instantiate<SourceScreenViewModel>(SourceScreenViewModel.Params(source, bundle)) enableLatest(source.supportsLatest)
}
val mangas by vm.mangas.collectAsState()
val hasNextPage by vm.hasNextPage.collectAsState()
val loading by vm.loading.collectAsState()
val isLatest by vm.isLatest.collectAsState()
val showingFilters by vm.showingFilters.collectAsState()
val showFilterButton by vm.filterButtonEnabled.collectAsState()
val showLatestButton by vm.latestButtonEnabled.collectAsState()
val sourceSearchQuery by vm.sourceSearchQuery.collectAsState()
LaunchedEffect(vm to source) {
vm.enableLatest(source.supportsLatest)
} }
Column { Column {
@@ -72,44 +73,35 @@ fun SourceScreen(
source = source, source = source,
onCloseSourceTabClick = onCloseSourceTabClick, onCloseSourceTabClick = onCloseSourceTabClick,
sourceSearchQuery = sourceSearchQuery, sourceSearchQuery = sourceSearchQuery,
onSearch = vm::search, onSearch = search,
onSubmitSearch = vm::submitSearch, onSubmitSearch = submitSearch,
onSourceSettingsClick = onSourceSettingsClick, onSourceSettingsClick = onSourceSettingsClick,
showFilterButton = showFilterButton, showFilterButton = showFilterButton,
showLatestButton = showLatestButton, showLatestButton = showLatestButton,
isLatest = isLatest, isLatest = isLatest,
showingFilters = showingFilters, showingFilters = showingFilters,
onClickMode = vm::setMode, onClickMode = setMode,
onToggleFiltersClick = vm::showingFilters, onToggleFiltersClick = setShowingFilters,
) )
Box { Box {
MangaTable( MangaTable(
bundle = bundle,
mangas = mangas, mangas = mangas,
isLoading = loading, isLoading = loading,
hasNextPage = hasNextPage, hasNextPage = hasNextPage,
onLoadNextPage = vm::loadNextPage, onLoadNextPage = loadNextPage,
onMangaClick = onMangaClick, onMangaClick = onMangaClick,
) )
BundleScope("filters", autoDispose = false) { SourceFiltersMenu(
SourceFiltersMenu( modifier = Modifier.align(Alignment.TopEnd),
bundle = bundle, showFilters = showingFilters && !isLatest,
modifier = Modifier.align(Alignment.TopEnd), filters = filters,
sourceId = source.id, onSearchClicked = {
showFilters = showingFilters && !isLatest, setUsingFilters(true)
onSearchClicked = { setShowingFilters(false)
vm.setUsingFilters(true) submitSearch()
vm.showingFilters(false) },
vm.submitSearch() resetFiltersClicked = resetFiltersClicked
}, )
onResetClicked = {
vm.setUsingFilters(false)
vm.showingFilters(false)
vm.submitSearch()
},
showFiltersButton = vm::enableFilters
)
}
} }
} }
} }
@@ -183,7 +175,6 @@ fun SourceToolbar(
@Composable @Composable
private fun MangaTable( private fun MangaTable(
bundle: Bundle,
mangas: List<Manga>, mangas: List<Manga>,
isLoading: Boolean = false, isLoading: Boolean = false,
hasNextPage: Boolean = false, hasNextPage: Boolean = false,
@@ -193,9 +184,9 @@ private fun MangaTable(
if (isLoading || mangas.isEmpty()) { if (isLoading || mangas.isEmpty()) {
LoadingScreen(isLoading) LoadingScreen(isLoading)
} else { } else {
val persistentState = persistentLazyListState(bundle) val lazyListState = rememberLazyListState()
Box { Box {
LazyVerticalGrid(GridCells.Adaptive(160.dp), state = persistentState) { LazyVerticalGrid(GridCells.Adaptive(160.dp), state = lazyListState) {
itemsIndexed(mangas) { index, manga -> itemsIndexed(mangas) { index, manga ->
if (hasNextPage && index == mangas.lastIndex) { if (hasNextPage && index == mangas.lastIndex) {
LaunchedEffect(Unit) { onLoadNextPage() } LaunchedEffect(Unit) { onLoadNextPage() }
@@ -210,7 +201,7 @@ private fun MangaTable(
} }
} }
VerticalScrollbar( VerticalScrollbar(
rememberScrollbarAdapter(persistentState), rememberScrollbarAdapter(lazyListState),
Modifier.align(Alignment.CenterEnd) Modifier.align(Alignment.CenterEnd)
.fillMaxHeight() .fillMaxHeight()
.padding(horizontal = 4.dp, vertical = 8.dp) .padding(horizontal = 4.dp, vertical = 8.dp)

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.sources.components.filter package ca.gosyer.ui.sources.browse.filter
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
@@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.Checkbox import androidx.compose.material.Checkbox
@@ -48,7 +49,6 @@ import androidx.compose.material.TriStateCheckbox
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowUpward import androidx.compose.material.icons.rounded.ArrowUpward
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -67,41 +67,19 @@ import androidx.compose.ui.util.fastForEach
import ca.gosyer.data.models.sourcefilters.SortFilter import ca.gosyer.data.models.sourcefilters.SortFilter
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.prefs.ExpandablePreference import ca.gosyer.ui.base.prefs.ExpandablePreference
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.ui.sources.browse.filter.model.SourceFiltersView
import ca.gosyer.ui.sources.components.filter.model.SourceFiltersView
import ca.gosyer.uicore.components.Spinner import ca.gosyer.uicore.components.Spinner
import ca.gosyer.util.compose.persistentLazyListState
import com.github.zsoltk.compose.savedinstancestate.Bundle
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
@Composable @Composable
fun SourceFiltersMenu( fun SourceFiltersMenu(
bundle: Bundle,
modifier: Modifier, modifier: Modifier,
sourceId: Long,
showFilters: Boolean, showFilters: Boolean,
filters: List<SourceFiltersView<*, *>>,
onSearchClicked: () -> Unit, onSearchClicked: () -> Unit,
onResetClicked: () -> Unit, resetFiltersClicked: () -> Unit
showFiltersButton: (Boolean) -> Unit
) { ) {
val vm = viewModel(sourceId) {
instantiate<SourceFiltersViewModel>(SourceFiltersViewModel.Params(bundle, sourceId))
}
val filters by vm.filters.collectAsState()
DisposableEffect(filters) {
showFiltersButton(filters.isNotEmpty())
onDispose { showFiltersButton(false) }
}
LaunchedEffect(vm) {
launch {
vm.resetFilters.collect {
onResetClicked()
}
}
}
AnimatedVisibility( AnimatedVisibility(
showFilters, showFilters,
enter = fadeIn() + slideInHorizontally(initialOffsetX = { it * 2 }), enter = fadeIn() + slideInHorizontally(initialOffsetX = { it * 2 }),
@@ -116,7 +94,7 @@ fun SourceFiltersMenu(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
TextButton(vm::resetFilters) { TextButton(resetFiltersClicked) {
Text(stringResource(MR.strings.reset_filters)) Text(stringResource(MR.strings.reset_filters))
} }
Button(onSearchClicked) { Button(onSearchClicked) {
@@ -126,7 +104,7 @@ fun SourceFiltersMenu(
} }
val expandedGroups = remember { mutableStateListOf<Int>() } val expandedGroups = remember { mutableStateListOf<Int>() }
Box { Box {
val lazyListState = persistentLazyListState() val lazyListState = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), lazyListState) { LazyColumn(Modifier.fillMaxSize(), lazyListState) {
items( items(
items = filters, items = filters,

View File

@@ -4,15 +4,14 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.sources.components.filter package ca.gosyer.ui.sources.browse.filter
import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.lang.throwIfCancellation
import ca.gosyer.core.logging.CKLogger import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.models.sourcefilters.SourceFilter import ca.gosyer.data.models.sourcefilters.SourceFilter
import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.ui.sources.browse.filter.model.SourceFiltersView
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.ui.sources.components.filter.model.SourceFiltersView
import com.github.zsoltk.compose.savedinstancestate.Bundle
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -27,7 +26,6 @@ import me.tatarka.inject.annotations.Inject
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
class SourceFiltersViewModel( class SourceFiltersViewModel(
private val bundle: Bundle,
private val sourceId: Long, private val sourceId: Long,
private val sourceHandler: SourceInteractionHandler private val sourceHandler: SourceInteractionHandler
) : ViewModel() { ) : ViewModel() {
@@ -35,7 +33,6 @@ class SourceFiltersViewModel(
sourceHandler: SourceInteractionHandler, sourceHandler: SourceInteractionHandler,
params: Params, params: Params,
) : this( ) : this(
params.bundle,
params.sourceId, params.sourceId,
sourceHandler sourceHandler
) )
@@ -49,14 +46,21 @@ class SourceFiltersViewModel(
private val _resetFilters = MutableSharedFlow<Unit>() private val _resetFilters = MutableSharedFlow<Unit>()
val resetFilters = _resetFilters.asSharedFlow() val resetFilters = _resetFilters.asSharedFlow()
private val _showingFilters = MutableStateFlow(false)
val showingFilters = _showingFilters.asStateFlow()
private val _filterButtonEnabled = MutableStateFlow(false)
val filterButtonEnabled = _filterButtonEnabled.asStateFlow()
private val subscriptions: CopyOnWriteArrayList<Job> = CopyOnWriteArrayList() private val subscriptions: CopyOnWriteArrayList<Job> = CopyOnWriteArrayList()
init { init {
getFilters(initialLoad = !bundle.getBoolean(FILTERING, false)) getFilters(initialLoad = true)
filters.onEach { settings -> filters.onEach { settings ->
subscriptions.forEach { it.cancel() } subscriptions.forEach { it.cancel() }
subscriptions.clear() subscriptions.clear()
_filterButtonEnabled.value = settings.isNotEmpty()
subscriptions += settings.flatMap { filter -> subscriptions += settings.flatMap { filter ->
if (filter is SourceFiltersView.Group) { if (filter is SourceFiltersView.Group) {
filter.state.value.map { childFilter -> filter.state.value.map { childFilter ->
@@ -81,15 +85,18 @@ class SourceFiltersViewModel(
}.launchIn(scope) }.launchIn(scope)
} }
fun showingFilters(show: Boolean) {
_showingFilters.value = show
}
fun enableFilters(enabled: Boolean) {
_filterButtonEnabled.value = enabled
}
private fun getFilters(initialLoad: Boolean = false) { private fun getFilters(initialLoad: Boolean = false) {
scope.launch { scope.launch {
try { try {
_filters.value = sourceHandler.getFilterList(sourceId, reset = initialLoad).toView() _filters.value = sourceHandler.getFilterList(sourceId, reset = initialLoad).toView()
if (!initialLoad) { _resetFilters.emit(Unit)
bundle.putBoolean(FILTERING, true)
} else {
_resetFilters.emit(Unit)
}
} catch (e: Exception) { } catch (e: Exception) {
e.throwIfCancellation() e.throwIfCancellation()
} finally { } finally {
@@ -100,12 +107,11 @@ class SourceFiltersViewModel(
fun resetFilters() { fun resetFilters() {
scope.launch { scope.launch {
bundle.remove(FILTERING)
getFilters(initialLoad = true) getFilters(initialLoad = true)
} }
} }
data class Params(val bundle: Bundle, val sourceId: Long) data class Params(val sourceId: Long)
private fun List<SourceFilter>.toView() = mapIndexed { index, sourcePreference -> private fun List<SourceFilter>.toView() = mapIndexed { index, sourcePreference ->
SourceFiltersView(index, sourcePreference) SourceFiltersView(index, sourcePreference)

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.sources.components.filter.model package ca.gosyer.ui.sources.browse.filter.model
import ca.gosyer.data.models.sourcefilters.CheckBoxFilter import ca.gosyer.data.models.sourcefilters.CheckBoxFilter
import ca.gosyer.data.models.sourcefilters.GroupFilter import ca.gosyer.data.models.sourcefilters.GroupFilter

View File

@@ -4,9 +4,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.sources package ca.gosyer.ui.sources.components
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.TooltipArea import androidx.compose.foundation.TooltipArea
import androidx.compose.foundation.TooltipPlacement import androidx.compose.foundation.TooltipPlacement
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
@@ -29,9 +28,8 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Home import androidx.compose.material.icons.rounded.Home
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
@@ -40,68 +38,43 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.data.models.Source import ca.gosyer.data.models.Source
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.ui.sources.browse.SourceScreen
import ca.gosyer.ui.manga.openMangaMenu import ca.gosyer.ui.sources.home.SourceHomeScreen
import ca.gosyer.ui.sources.components.SourceHomeScreen
import ca.gosyer.ui.sources.components.SourceScreen
import ca.gosyer.ui.sources.settings.openSourceSettingsMenu
import ca.gosyer.uicore.components.combinedMouseClickable import ca.gosyer.uicore.components.combinedMouseClickable
import ca.gosyer.uicore.image.KamelImage import ca.gosyer.uicore.image.KamelImage
import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.util.compose.ThemedWindow
import ca.gosyer.util.lang.launchApplication
import com.github.zsoltk.compose.savedinstancestate.Bundle
import com.github.zsoltk.compose.savedinstancestate.BundleScope
import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import io.kamel.image.lazyPainterResource import io.kamel.image.lazyPainterResource
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class) @Composable
fun openSourcesMenu() { fun SourcesMenu(
launchApplication { sourceTabs: List<Source?>,
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) { selectedSourceTab: Source?,
Surface { selectTab: (Source?) -> Unit,
CompositionLocalProvider( closeTab: (Source) -> Unit
LocalSavedInstanceState provides Bundle() ) {
) { val homeScreen = remember { SourceHomeScreen() }
SourcesMenu( SourcesNavigator(
::openSourceSettingsMenu, homeScreen,
::openMangaMenu removeSource = closeTab,
) selectSource = selectTab
} ) { navigator ->
} LaunchedEffect(selectedSourceTab) {
navigator.current = if (selectedSourceTab == null) {
homeScreen
} else SourceScreen(selectedSourceTab)
} }
} Row {
} SourcesSideMenu(
sourceTabs = sourceTabs,
onSourceTabClick = selectTab,
onCloseSourceTabClick = {
closeTab(it)
navigator.stateHolder.removeState(it.id)
}
)
@Composable CurrentSource()
fun SourcesMenu(onSourceSettingsClick: (Long) -> Unit, onMangaClick: (Long) -> Unit) { }
SourcesMenu(LocalSavedInstanceState.current, onSourceSettingsClick, onMangaClick)
}
@Composable
fun SourcesMenu(bundle: Bundle, onSourceSettingsClick: (Long) -> Unit, onMangaClick: (Long) -> Unit) {
val vm = viewModel {
instantiate<SourcesMenuViewModel>(bundle)
}
val sourceTabs by vm.sourceTabs.collectAsState()
val selectedSourceTab by vm.selectedSourceTab.collectAsState()
Row {
SourcesSideMenu(
sourceTabs = sourceTabs,
onSourceTabClick = vm::selectTab,
onCloseSourceTabClick = vm::closeTab
)
SourceTab(
onLoadSources = vm::setLoadedSources,
onSourceClicked = vm::addTab,
selectedSourceTab = selectedSourceTab,
onMangaClick = onMangaClick,
onCloseSourceTabClick = vm::closeTab,
onSourceSettingsClick = onSourceSettingsClick
)
} }
} }
@@ -169,33 +142,3 @@ fun SourcesSideMenu(
} }
} }
} }
@Composable
fun SourceTab(
onLoadSources: (List<Source>) -> Unit,
onSourceClicked: (Source) -> Unit,
selectedSourceTab: Source?,
onMangaClick: (Long) -> Unit,
onCloseSourceTabClick: (Source) -> Unit,
onSourceSettingsClick: (Long) -> Unit
) {
Crossfade(selectedSourceTab) { selectedSource ->
BundleScope(selectedSource?.id.toString(), autoDispose = false) {
if (selectedSource != null) {
SourceScreen(
bundle = it,
source = selectedSource,
onMangaClick = onMangaClick,
onCloseSourceTabClick = onCloseSourceTabClick,
onSourceSettingsClick = onSourceSettingsClick
)
} else {
SourceHomeScreen(
bundle = it,
onAddSource = onSourceClicked,
onLoadSources = onLoadSources
)
}
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.sources.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.SaveableStateHolder
import androidx.compose.runtime.staticCompositionLocalOf
import ca.gosyer.data.models.Source
import ca.gosyer.ui.sources.browse.SourceScreen
import ca.gosyer.ui.sources.home.SourceHomeScreen
import cafe.adriel.voyager.navigator.Navigator
typealias SourcesNavigatorContent = @Composable (sourcesNavigator: SourcesNavigator) -> Unit
val LocalSourcesNavigator: ProvidableCompositionLocal<SourcesNavigator> =
staticCompositionLocalOf { error("SourcesNavigator not initialized") }
@Composable
fun SourcesNavigator(
homeScreen: SourceHomeScreen,
removeSource: (Source) -> Unit,
selectSource: (Source) -> Unit,
content: SourcesNavigatorContent = { CurrentSource() }
) {
Navigator(homeScreen, autoDispose = false, onBackPressed = null) { navigator ->
val sourcesNavigator = remember(navigator) {
SourcesNavigator(navigator, homeScreen, removeSource, selectSource)
}
CompositionLocalProvider(LocalSourcesNavigator provides sourcesNavigator) {
content(sourcesNavigator)
}
}
}
class SourcesNavigator internal constructor(
private val navigator: Navigator,
private val homeScreen: SourceHomeScreen,
private val removeSource: (Source) -> Unit,
private val selectSource: (Source) -> Unit,
val stateHolder: SaveableStateHolder = navigator.stateHolder
) {
fun remove(source: Source) {
removeSource(source)
navigator replaceAll homeScreen
stateHolder.removeState(source.id)
}
fun select(source: Source) {
selectSource(source)
navigator replaceAll SourceScreen(source)
}
var current
get() = navigator.lastItem
set(value) = navigator replaceAll value
}
@Composable
fun CurrentSource() {
val sourcesNavigator = LocalSourcesNavigator.current
val currentSource = sourcesNavigator.current
sourcesNavigator.stateHolder.SaveableStateProvider(currentSource.key) {
currentSource.Content()
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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.sources.home
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import ca.gosyer.ui.sources.components.LocalSourcesNavigator
import ca.gosyer.ui.sources.home.components.SourceHomeScreenContent
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
class SourceHomeScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<SourceHomeScreenViewModel>()
val sourcesNavigator = LocalSourcesNavigator.current
SourceHomeScreenContent(
onAddSource = sourcesNavigator::select,
isLoading = vm.isLoading.collectAsState().value,
sources = vm.sources.collectAsState().value,
languages = vm.languages,
getSourceLanguages = vm::getSourceLanguages,
setEnabledLanguages = vm::setEnabledLanguages
)
}
}

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.sources.components package ca.gosyer.ui.sources.home
import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.lang.throwIfCancellation
import ca.gosyer.core.logging.CKLogger import ca.gosyer.core.logging.CKLogger
@@ -12,7 +12,6 @@ import ca.gosyer.data.catalog.CatalogPreferences
import ca.gosyer.data.models.Source import ca.gosyer.data.models.Source
import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.uicore.vm.ViewModel import ca.gosyer.uicore.vm.ViewModel
import com.github.zsoltk.compose.savedinstancestate.Bundle
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -20,8 +19,7 @@ import me.tatarka.inject.annotations.Inject
class SourceHomeScreenViewModel @Inject constructor( class SourceHomeScreenViewModel @Inject constructor(
private val sourceHandler: SourceInteractionHandler, private val sourceHandler: SourceInteractionHandler,
catalogPreferences: CatalogPreferences, catalogPreferences: CatalogPreferences
private val bundle: Bundle,
) : ViewModel() { ) : ViewModel() {
private val _isLoading = MutableStateFlow(true) private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow() val isLoading = _isLoading.asStateFlow()

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.sources.components package ca.gosyer.ui.sources.home.components
import androidx.compose.foundation.TooltipArea import androidx.compose.foundation.TooltipArea
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
@@ -32,9 +32,6 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Translate import androidx.compose.material.icons.rounded.Translate
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
@@ -45,41 +42,31 @@ import ca.gosyer.data.models.Source
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.TextActionIcon import ca.gosyer.ui.base.navigation.TextActionIcon
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.uicore.vm.viewModel import ca.gosyer.ui.extensions.components.LanguageDialog
import ca.gosyer.ui.extensions.LanguageDialog
import ca.gosyer.uicore.components.LoadingScreen import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.image.KamelImage import ca.gosyer.uicore.image.KamelImage
import com.github.zsoltk.compose.savedinstancestate.Bundle
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import io.kamel.image.lazyPainterResource import io.kamel.image.lazyPainterResource
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@Composable @Composable
fun SourceHomeScreen( fun SourceHomeScreenContent(
bundle: Bundle,
onAddSource: (Source) -> Unit, onAddSource: (Source) -> Unit,
onLoadSources: (List<Source>) -> Unit isLoading: Boolean,
sources: List<Source>,
languages: StateFlow<Set<String>>,
getSourceLanguages: () -> Set<String>,
setEnabledLanguages: (Set<String>) -> Unit
) { ) {
val vm = viewModel {
instantiate<SourceHomeScreenViewModel>(bundle)
}
val sources by vm.sources.collectAsState()
val isLoading by vm.isLoading.collectAsState()
LaunchedEffect(sources) {
if (sources.isNotEmpty()) {
onLoadSources(sources)
}
}
if (sources.isEmpty()) { if (sources.isEmpty()) {
LoadingScreen(isLoading) LoadingScreen(isLoading)
} else { } else {
Column { Column {
SourceHomeScreenToolbar( SourceHomeScreenToolbar(
vm.languages, languages,
vm::getSourceLanguages, getSourceLanguages,
vm::setEnabledLanguages setEnabledLanguages
) )
Box(Modifier.fillMaxSize(), Alignment.TopCenter) { Box(Modifier.fillMaxSize(), Alignment.TopCenter) {
val state = rememberLazyListState() val state = rememberLazyListState()

View File

@@ -0,0 +1,45 @@
/*
* 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.sources.settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.sources.settings.components.SourceSettingsScreenContent
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.Navigator
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openSourceSettingsMenu(sourceId: Long) {
launchApplication {
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
Navigator(remember { SourceSettingsScreen(sourceId) })
}
}
}
class SourceSettingsScreen(private val sourceId: Long) : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel {
instantiate<SourceSettingsScreenViewModel>(SourceSettingsScreenViewModel.Params(sourceId))
}
SourceSettingsScreenContent(
settings = vm.sourceSettings.collectAsState().value
)
}
}

View File

@@ -9,8 +9,8 @@ package ca.gosyer.ui.sources.settings
import ca.gosyer.core.lang.throwIfCancellation import ca.gosyer.core.lang.throwIfCancellation
import ca.gosyer.data.models.sourcepreference.SourcePreference import ca.gosyer.data.models.sourcepreference.SourcePreference
import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.uicore.vm.ViewModel
import ca.gosyer.ui.sources.settings.model.SourceSettingsView import ca.gosyer.ui.sources.settings.model.SourceSettingsView
import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
class SourceSettingsViewModel @Inject constructor( class SourceSettingsScreenViewModel @Inject constructor(
private val sourceHandler: SourceInteractionHandler, private val sourceHandler: SourceInteractionHandler,
private val params: Params private val params: Params
) : ViewModel() { ) : ViewModel() {

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.sources.settings package ca.gosyer.ui.sources.settings.components
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -33,43 +33,27 @@ import androidx.compose.ui.unit.dp
import ca.gosyer.i18n.MR import ca.gosyer.i18n.MR
import ca.gosyer.presentation.build.BuildKonfig import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.base.WindowDialog import ca.gosyer.ui.base.WindowDialog
import ca.gosyer.ui.base.navigation.LocalMenuController
import ca.gosyer.ui.base.navigation.MenuController
import ca.gosyer.ui.base.navigation.Toolbar import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.base.prefs.ChoiceDialog import ca.gosyer.ui.base.prefs.ChoiceDialog
import ca.gosyer.ui.base.prefs.MultiSelectDialog import ca.gosyer.ui.base.prefs.MultiSelectDialog
import ca.gosyer.ui.base.prefs.PreferenceRow import ca.gosyer.ui.base.prefs.PreferenceRow
import ca.gosyer.ui.sources.settings.model.SourceSettingsView
import ca.gosyer.ui.sources.settings.model.SourceSettingsView.CheckBox import ca.gosyer.ui.sources.settings.model.SourceSettingsView.CheckBox
import ca.gosyer.ui.sources.settings.model.SourceSettingsView.EditText import ca.gosyer.ui.sources.settings.model.SourceSettingsView.EditText
import ca.gosyer.ui.sources.settings.model.SourceSettingsView.List import ca.gosyer.ui.sources.settings.model.SourceSettingsView.List
import ca.gosyer.ui.sources.settings.model.SourceSettingsView.MultiSelect import ca.gosyer.ui.sources.settings.model.SourceSettingsView.MultiSelect
import ca.gosyer.ui.sources.settings.model.SourceSettingsView.Switch import ca.gosyer.ui.sources.settings.model.SourceSettingsView.Switch
import ca.gosyer.ui.sources.settings.model.SourceSettingsView.TwoState import ca.gosyer.ui.sources.settings.model.SourceSettingsView.TwoState
import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.util.compose.ThemedWindow
import ca.gosyer.util.lang.launchApplication
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlin.collections.List as KtList
@OptIn(DelicateCoroutinesApi::class)
fun openSourceSettingsMenu(sourceId: Long) {
launchApplication {
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
SourceSettingsMenu(sourceId)
}
}
}
@Composable @Composable
fun SourceSettingsMenu(sourceId: Long, menuController: MenuController? = LocalMenuController.current) { fun SourceSettingsScreenContent(
val vm = viewModel { settings: KtList<SourceSettingsView<*, *>>
instantiate<SourceSettingsViewModel>(SourceSettingsViewModel.Params(sourceId)) ) {
}
val settings by vm.sourceSettings.collectAsState()
Column { Column {
Toolbar(stringResource(MR.strings.location_settings), menuController, menuController != null) Toolbar(stringResource(MR.strings.location_settings))
Box { Box {
val state = rememberLazyListState() val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize(), state) { LazyColumn(Modifier.fillMaxSize(), state) {

View File

@@ -0,0 +1,40 @@
/*
* 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.updates
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import ca.gosyer.ui.manga.MangaScreen
import ca.gosyer.ui.reader.openReaderMenu
import ca.gosyer.ui.updates.components.UpdatesScreenContent
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
class UpdatesScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<UpdatesScreenViewModel>()
val navigator = LocalNavigator.currentOrThrow
UpdatesScreenContent(
isLoading = vm.isLoading.collectAsState().value,
updates = vm.updates.collectAsState().value,
loadNextPage = vm::loadNextPage,
openChapter = ::openReaderMenu,
openManga = { navigator push MangaScreen(it) },
downloadChapter = vm::downloadChapter,
deleteDownloadedChapter = vm::deleteDownloadedChapter,
stopDownloadingChapter = vm::stopDownloadingChapter
)
}
}

View File

@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import me.tatarka.inject.annotations.Inject import me.tatarka.inject.annotations.Inject
class UpdatesMenuViewModel @Inject constructor( class UpdatesScreenViewModel @Inject constructor(
private val chapterHandler: ChapterInteractionHandler, private val chapterHandler: ChapterInteractionHandler,
private val updatesHandler: UpdatesInteractionHandler, private val updatesHandler: UpdatesInteractionHandler,
private val downloadService: DownloadService private val downloadService: DownloadService
@@ -113,7 +113,7 @@ class UpdatesMenuViewModel @Inject constructor(
} }
} }
override fun onDestroy() { override fun onDispose() {
downloadService.removeWatches(mangaIds) downloadService.removeWatches(mangaIds)
} }
} }

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.ui.updates package ca.gosyer.ui.updates.components
import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -23,8 +23,6 @@ import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
@@ -44,18 +42,20 @@ import ca.gosyer.uicore.components.MangaListItemImage
import ca.gosyer.uicore.components.MangaListItemSubtitle import ca.gosyer.uicore.components.MangaListItemSubtitle
import ca.gosyer.uicore.components.MangaListItemTitle import ca.gosyer.uicore.components.MangaListItemTitle
import ca.gosyer.uicore.components.mangaAspectRatio import ca.gosyer.uicore.components.mangaAspectRatio
import ca.gosyer.uicore.vm.viewModel
import ca.gosyer.uicore.resources.stringResource import ca.gosyer.uicore.resources.stringResource
import io.kamel.image.lazyPainterResource import io.kamel.image.lazyPainterResource
@Composable @Composable
fun UpdatesMenu( fun UpdatesScreenContent(
isLoading: Boolean,
updates: List<ChapterDownloadItem>,
loadNextPage: () -> Unit,
openChapter: (Int, Long) -> Unit, openChapter: (Int, Long) -> Unit,
openManga: (Long) -> Unit openManga: (Long) -> Unit,
downloadChapter: (Chapter) -> Unit,
deleteDownloadedChapter: (Chapter) -> Unit,
stopDownloadingChapter: (Chapter) -> Unit
) { ) {
val vm = viewModel<UpdatesMenuViewModel>()
val isLoading by vm.isLoading.collectAsState()
val updates by vm.updates.collectAsState()
Column { Column {
Toolbar(stringResource(MR.strings.location_updates), closable = false) Toolbar(stringResource(MR.strings.location_updates), closable = false)
if (isLoading || updates.isEmpty()) { if (isLoading || updates.isEmpty()) {
@@ -67,7 +67,7 @@ fun UpdatesMenu(
itemsIndexed(updates) { index, item -> itemsIndexed(updates) { index, item ->
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (index == updates.lastIndex) { if (index == updates.lastIndex) {
vm.loadNextPage() loadNextPage()
} }
} }
val manga = item.manga!! val manga = item.manga!!
@@ -76,9 +76,9 @@ fun UpdatesMenu(
item, item,
onClickItem = { openChapter(chapter.index, chapter.mangaId) }, onClickItem = { openChapter(chapter.index, chapter.mangaId) },
onClickCover = { openManga(manga.id) }, onClickCover = { openManga(manga.id) },
onClickDownload = vm::downloadChapter, onClickDownload = downloadChapter,
onClickDeleteDownload = vm::deleteDownloadedChapter, onClickDeleteDownload = deleteDownloadedChapter,
onClickStopDownload = vm::stopDownloadingChapter onClickStopDownload = stopDownloadingChapter
) )
} }
} }

View File

@@ -1,124 +0,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.util.compose
import com.github.zsoltk.compose.savedinstancestate.Bundle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
inline fun <reified T> Bundle.putJsonObject(key: String, item: T) {
putString(key, Json.encodeToString(item))
}
inline fun <reified T> Bundle.getJsonObject(key: String): T? {
return getString(key)?.let { Json.decodeFromString(it) }
}
inline fun <reified T> Bundle.getJsonObjectArray(key: String): List<T?>? {
return getString(key)?.let { Json.decodeFromString(it) }
}
inline fun <T> saveAnyInBundle(
scope: CoroutineScope,
bundle: Bundle,
key: String,
getValue: Bundle.(String) -> T?,
crossinline putValue: Bundle.(itemKey: String, item: T) -> Unit,
initialValue: () -> T
): MutableStateFlow<T> {
val item = bundle.getValue(key)
val flow: MutableStateFlow<T> = if (item != null) {
MutableStateFlow(item)
} else {
MutableStateFlow(initialValue())
}
flow.drop(1)
.mapLatest { bundle.putValue(key, it) }
.launchIn(scope)
return flow
}
inline fun <reified T> saveObjectInBundle(
scope: CoroutineScope,
bundle: Bundle,
key: String,
initialValue: () -> T
): MutableStateFlow<T> {
return saveAnyInBundle(
scope,
bundle,
key,
{ getJsonObject<T>(it) },
{ itemKey, item ->
putJsonObject(itemKey, item)
},
initialValue
)
}
fun saveIntInBundle(
scope: CoroutineScope,
bundle: Bundle,
key: String,
initialValue: Int
): MutableStateFlow<Int> {
return saveAnyInBundle(
scope,
bundle,
key,
{ getInt(key, initialValue) },
{ itemKey, item ->
putInt(itemKey, item)
},
{ initialValue }
)
}
fun saveBooleanInBundle(
scope: CoroutineScope,
bundle: Bundle,
key: String,
initialValue: Boolean
): MutableStateFlow<Boolean> {
return saveAnyInBundle(
scope,
bundle,
key,
{ getBoolean(key, initialValue) },
{ itemKey, item ->
putBoolean(itemKey, item)
},
{ initialValue }
)
}
fun saveStringInBundle(
scope: CoroutineScope,
bundle: Bundle,
key: String,
initialValue: () -> String? = { null }
): MutableStateFlow<String?> {
return saveAnyInBundle(
scope,
bundle,
key,
{ getString(key) ?: initialValue() },
{ itemKey, item ->
if (item != null) {
putString(itemKey, item)
} else remove(itemKey)
},
initialValue
)
}

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.util.compose package ca.gosyer.ui.util.compose
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.util.compose package ca.gosyer.ui.util.compose
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.util.compose package ca.gosyer.ui.util.compose
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.graphics.toComposeImageBitmap

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.util.compose package ca.gosyer.ui.util.compose
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size

View File

@@ -1,34 +0,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.util.compose
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import com.github.zsoltk.compose.savedinstancestate.Bundle
import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState
const val LAZY_LIST_ITEM = "lazy_list_item"
const val LAZY_LIST_OFFSET = "lazy_list_offset"
@Composable
fun persistentLazyListState(bundle: Bundle = LocalSavedInstanceState.current): LazyListState {
val state = rememberLazyListState(
remember { bundle.getInt(LAZY_LIST_ITEM, 0) },
remember { bundle.getInt(LAZY_LIST_OFFSET, 0) }
)
DisposableEffect(Unit) {
onDispose {
bundle.putInt(LAZY_LIST_ITEM, state.firstVisibleItemIndex)
bundle.putInt(LAZY_LIST_OFFSET, state.firstVisibleItemScrollOffset)
}
}
return state
}

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.util.compose package ca.gosyer.ui.util.compose
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.util.compose package ca.gosyer.ui.util.compose
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.util.lang package ca.gosyer.ui.util.lang
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.window.ApplicationScope import androidx.compose.ui.window.ApplicationScope

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.util.system package ca.gosyer.ui.util.system
import ca.gosyer.core.lang.launchUI import ca.gosyer.core.lang.launchUI
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
package ca.gosyer.util.system package ca.gosyer.ui.util.system
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow

View File

@@ -31,6 +31,7 @@ kotlin {
api(kotlin("stdlib-common")) api(kotlin("stdlib-common"))
api(libs.coroutinesCore) api(libs.coroutinesCore)
api(libs.kamel) api(libs.kamel)
api(libs.voyagerCore)
api(project(":core")) api(project(":core"))
api(project(":i18n")) api(project(":i18n"))
api(compose.desktop.currentOs) api(compose.desktop.currentOs)

View File

@@ -8,36 +8,20 @@ package ca.gosyer.uicore.vm
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.DisposableEffect import cafe.adriel.voyager.core.model.rememberScreenModel
import androidx.compose.runtime.remember import cafe.adriel.voyager.core.screen.Screen
@Composable @Composable
inline fun <reified VM : ViewModel> viewModel(key: Any? = Unit): VM { inline fun <reified VM : ViewModel> Screen.viewModel(tag: String? = null): VM {
val viewModelFactory = LocalViewModelFactory.current val viewModelFactory = LocalViewModelFactory.current
val viewModel = remember(key) { return rememberScreenModel(tag) { viewModelFactory.instantiate() }
viewModelFactory.instantiate<VM>()
}
DisposableEffect(viewModel) {
onDispose {
viewModel.destroy()
}
}
return viewModel
} }
@Composable @Composable
inline fun <reified VM : ViewModel> viewModel( inline fun <reified VM : ViewModel> Screen.viewModel(
key: Any? = Unit, tag: String? = null,
crossinline factory: @DisallowComposableCalls ViewModelFactory.() -> VM crossinline factory: @DisallowComposableCalls ViewModelFactory.() -> VM
): VM { ): VM {
val viewModelFactory = LocalViewModelFactory.current val viewModelFactory = LocalViewModelFactory.current
val viewModel = remember(key) { return rememberScreenModel(tag) { viewModelFactory.factory() }
viewModelFactory.factory()
}
DisposableEffect(viewModel) {
onDispose {
viewModel.destroy()
}
}
return viewModel
} }

View File

@@ -8,23 +8,17 @@ package ca.gosyer.uicore.vm
import ca.gosyer.core.prefs.Preference import ca.gosyer.core.prefs.Preference
import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow
import kotlinx.coroutines.MainScope import cafe.adriel.voyager.core.model.ScreenModel
import kotlinx.coroutines.cancel import cafe.adriel.voyager.core.model.coroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
abstract class ViewModel { abstract class ViewModel : ScreenModel {
protected val scope = MainScope() protected open val scope
get() = coroutineScope
fun destroy() {
scope.cancel()
onDestroy()
}
open fun onDestroy() {}
fun <T> Preference<T>.asStateFlow() = PreferenceMutableStateFlow(this, scope) fun <T> Preference<T>.asStateFlow() = PreferenceMutableStateFlow(this, scope)