mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Rewrite navigation to use Multiplatform Voyager Navigation
This commit is contained in:
@@ -29,7 +29,8 @@ dependencies {
|
||||
implementation(compose.uiTooling)
|
||||
implementation(compose.materialIconsExtended)
|
||||
implementation(compose("org.jetbrains.compose.ui:ui-util"))
|
||||
implementation(libs.composeRouter)
|
||||
implementation(libs.voyagerCore)
|
||||
implementation(libs.voyagerNavigation)
|
||||
implementation(libs.accompanistPager)
|
||||
implementation(libs.accompanistFlowLayout)
|
||||
implementation(libs.kamel)
|
||||
|
||||
@@ -14,7 +14,6 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.configureSwingGlobalsForCompose
|
||||
import androidx.compose.ui.input.key.Key
|
||||
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.base.WindowDialog
|
||||
import ca.gosyer.ui.base.theme.AppTheme
|
||||
import ca.gosyer.ui.main.DebugOverlay
|
||||
import ca.gosyer.ui.main.MainMenu
|
||||
import ca.gosyer.ui.main.components.DebugOverlay
|
||||
import ca.gosyer.ui.main.components.Tray
|
||||
import ca.gosyer.ui.util.compose.WindowGet
|
||||
import ca.gosyer.uicore.components.LoadingScreen
|
||||
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.theme.DarculaTheme
|
||||
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.GlobalScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -124,10 +120,6 @@ suspend fun main() {
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
|
||||
val backPressHandler = remember { BackPressHandler() }
|
||||
|
||||
val rootBundle = remember { Bundle() }
|
||||
val windowState = rememberWindowState(
|
||||
size = size,
|
||||
position = position,
|
||||
@@ -158,7 +150,8 @@ suspend fun main() {
|
||||
if (it.type == KeyEventType.KeyUp) {
|
||||
when (it.key) {
|
||||
Key.Home -> {
|
||||
backPressHandler.handle()
|
||||
// backPressHandler.handle()
|
||||
false
|
||||
}
|
||||
Key.F3 -> {
|
||||
displayDebugInfoFlow.value = !displayDebugInfoFlow.value
|
||||
@@ -170,29 +163,25 @@ suspend fun main() {
|
||||
}
|
||||
) {
|
||||
AppTheme {
|
||||
CompositionLocalProvider(
|
||||
LocalBackPressHandler provides backPressHandler,
|
||||
) {
|
||||
Crossfade(serverService.initialized.collectAsState().value) { initialized ->
|
||||
when (initialized) {
|
||||
ServerResult.STARTED, ServerResult.UNUSED -> {
|
||||
Box {
|
||||
MainMenu(rootBundle)
|
||||
val displayDebugInfo by displayDebugInfoFlow.collectAsState()
|
||||
if (displayDebugInfo) {
|
||||
DebugOverlay()
|
||||
}
|
||||
Crossfade(serverService.initialized.collectAsState().value) { initialized ->
|
||||
when (initialized) {
|
||||
ServerResult.STARTED, ServerResult.UNUSED -> {
|
||||
Box {
|
||||
MainMenu()
|
||||
val displayDebugInfo by displayDebugInfoFlow.collectAsState()
|
||||
if (displayDebugInfo) {
|
||||
DebugOverlay()
|
||||
}
|
||||
}
|
||||
ServerResult.STARTING, ServerResult.FAILED -> {
|
||||
Surface {
|
||||
LoadingScreen(
|
||||
initialized == ServerResult.STARTING,
|
||||
errorMessage = stringResource(MR.strings.unable_to_start_server),
|
||||
retryMessage = stringResource(MR.strings.action_start_anyway),
|
||||
retry = serverService::startAnyway
|
||||
)
|
||||
}
|
||||
}
|
||||
ServerResult.STARTING, ServerResult.FAILED -> {
|
||||
Surface {
|
||||
LoadingScreen(
|
||||
initialized == ServerResult.STARTING,
|
||||
errorMessage = stringResource(MR.strings.unable_to_start_server),
|
||||
retryMessage = stringResource(MR.strings.action_start_anyway),
|
||||
retry = serverService::startAnyway
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ json = "1.3.2"
|
||||
xmlUtil = "0.84.0"
|
||||
|
||||
# Compose
|
||||
composeRouter = "0.24.2-jetbrains-2"
|
||||
voyager = "1.0.0-beta15"
|
||||
accompanist = "0.18.1"
|
||||
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" }
|
||||
|
||||
# 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" }
|
||||
accompanistFlowLayout = { module = "ca.gosyer:accompanist-flowlayout", version.ref = "accompanist" }
|
||||
kamel = { module = "com.alialbaali.kamel:kamel-image", version.ref = "kamel" }
|
||||
|
||||
@@ -23,6 +23,7 @@ kotlin {
|
||||
all {
|
||||
languageSettings {
|
||||
optIn("kotlin.RequiresOptIn")
|
||||
optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
optIn("com.google.accompanist.pager.ExperimentalPagerApi")
|
||||
optIn("androidx.compose.foundation.ExperimentalFoundationApi")
|
||||
optIn("androidx.compose.material.ExperimentalMaterialApi")
|
||||
@@ -35,6 +36,8 @@ kotlin {
|
||||
api(kotlin("stdlib-common"))
|
||||
api(libs.coroutinesCore)
|
||||
api(libs.kamel)
|
||||
api(libs.voyagerCore)
|
||||
api(libs.voyagerNavigation)
|
||||
api(project(":core"))
|
||||
api(project(":i18n"))
|
||||
api(project(":data"))
|
||||
@@ -60,7 +63,6 @@ kotlin {
|
||||
api(libs.coroutinesSwing)
|
||||
api(libs.accompanistPager)
|
||||
api(libs.accompanistFlowLayout)
|
||||
api(libs.composeRouter)
|
||||
api(libs.krokiCoroutines)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import androidx.compose.ui.window.WindowPosition
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
import ca.gosyer.ui.AppComponent
|
||||
import ca.gosyer.ui.base.theme.AppTheme
|
||||
import ca.gosyer.util.lang.launchApplication
|
||||
import ca.gosyer.ui.util.lang.launchApplication
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
|
||||
@@ -12,14 +12,11 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import ca.gosyer.ui.main.Routes
|
||||
import com.github.zsoltk.compose.router.BackStack
|
||||
|
||||
val LocalMenuController =
|
||||
compositionLocalOf<MenuController?> { null }
|
||||
val LocalDisplayController =
|
||||
compositionLocalOf<DisplayController?> { null }
|
||||
|
||||
class MenuController(
|
||||
val backStack: BackStack<Routes>,
|
||||
class DisplayController(
|
||||
private val _sideMenuVisible: MutableState<Boolean> = mutableStateOf(true),
|
||||
private val _isDrawer: MutableState<Boolean> = mutableStateOf(false),
|
||||
) {
|
||||
@@ -38,25 +35,12 @@ class MenuController(
|
||||
fun setAsNotDrawer() {
|
||||
_isDrawer.value = false
|
||||
}
|
||||
|
||||
fun push(route: Routes) {
|
||||
backStack.push(route)
|
||||
if (isDrawer) {
|
||||
closeSideMenu()
|
||||
}
|
||||
}
|
||||
fun newRoot(route: Routes) {
|
||||
backStack.newRoot(route)
|
||||
if (isDrawer) {
|
||||
closeSideMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun withMenuController(controller: MenuController, content: @Composable () -> Unit) {
|
||||
fun withDisplayController(controller: DisplayController, content: @Composable () -> Unit) {
|
||||
CompositionLocalProvider(
|
||||
LocalMenuController provides controller,
|
||||
LocalDisplayController provides controller,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -66,13 +66,16 @@ import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.uicore.components.BoxWithTooltipSurface
|
||||
import ca.gosyer.uicore.resources.stringResource
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
|
||||
@Composable
|
||||
fun Toolbar(
|
||||
name: String,
|
||||
menuController: MenuController? = LocalMenuController.current,
|
||||
closable: Boolean,
|
||||
onClose: () -> Unit = { menuController?.backStack?.pop() },
|
||||
displayController: DisplayController? = LocalDisplayController.current,
|
||||
navigator: Navigator? = LocalNavigator.current,
|
||||
closable: Boolean = (navigator?.size ?: 0) > 1,
|
||||
onClose: () -> Unit = { navigator?.pop() },
|
||||
modifier: Modifier = Modifier,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
backgroundColor: Color = MaterialTheme.colors.surface, // CustomColors.current.bars,
|
||||
@@ -98,14 +101,14 @@ fun Toolbar(
|
||||
Modifier.fillMaxHeight().animateContentSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (menuController != null) {
|
||||
if (menuController.isDrawer) {
|
||||
ActionIcon(menuController::openSideMenu, "Open nav", Icons.Rounded.Menu)
|
||||
if (displayController != null) {
|
||||
if (displayController.isDrawer) {
|
||||
ActionIcon(displayController::openSideMenu, "Open nav", Icons.Rounded.Menu)
|
||||
} else {
|
||||
AnimatedVisibility(
|
||||
!menuController.sideMenuVisible
|
||||
!displayController.sideMenuVisible
|
||||
) {
|
||||
ActionIcon(menuController::openSideMenu, "Open nav", Icons.Rounded.Sort)
|
||||
ActionIcon(displayController::openSideMenu, "Open nav", Icons.Rounded.Sort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ import ca.gosyer.data.ui.UiPreferences
|
||||
import ca.gosyer.data.ui.model.ThemeMode
|
||||
import ca.gosyer.uicore.theme.Theme
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
@@ -39,7 +40,8 @@ import me.tatarka.inject.annotations.Inject
|
||||
*/
|
||||
@Composable
|
||||
fun AppTheme(content: @Composable () -> Unit) {
|
||||
val vm = viewModel<AppThemeViewModel>()
|
||||
val vmFactory = LocalViewModelFactory.current
|
||||
val vm = remember { vmFactory.instantiate<AppThemeViewModel>() }
|
||||
val colors = vm.getColors()
|
||||
/*val systemUiController = rememberSystemUiController()*/
|
||||
|
||||
@@ -61,6 +63,8 @@ fun AppTheme(content: @Composable () -> Unit) {
|
||||
class AppThemeViewModel @Inject constructor(
|
||||
private val uiPreferences: UiPreferences
|
||||
) : ViewModel() {
|
||||
override val scope = MainScope()
|
||||
|
||||
private val themeMode = uiPreferences.themeMode().asStateFlow()
|
||||
private val lightTheme = uiPreferences.lightTheme().asStateFlow()
|
||||
private val darkTheme = uiPreferences.darkTheme().asStateFlow()
|
||||
@@ -130,7 +134,8 @@ class AppThemeViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
override fun onDispose() {
|
||||
baseThemeScope.cancel()
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
package ca.gosyer.ui.base.vm
|
||||
|
||||
import ca.gosyer.ui.base.theme.AppThemeViewModel
|
||||
import ca.gosyer.ui.categories.CategoriesMenuViewModel
|
||||
import ca.gosyer.ui.downloads.DownloadsMenuViewModel
|
||||
import ca.gosyer.ui.extensions.ExtensionsMenuViewModel
|
||||
import ca.gosyer.ui.categories.CategoriesScreenViewModel
|
||||
import ca.gosyer.ui.downloads.DownloadsScreenViewModel
|
||||
import ca.gosyer.ui.extensions.ExtensionsScreenViewModel
|
||||
import ca.gosyer.ui.library.LibraryScreenViewModel
|
||||
import ca.gosyer.ui.main.MainViewModel
|
||||
import ca.gosyer.ui.main.components.DebugOverlayViewModel
|
||||
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.settings.SettingsAdvancedViewModel
|
||||
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.SettingsServerViewModel
|
||||
import ca.gosyer.ui.settings.ThemesViewModel
|
||||
import ca.gosyer.ui.sources.SourcesMenuViewModel
|
||||
import ca.gosyer.ui.sources.components.SourceHomeScreenViewModel
|
||||
import ca.gosyer.ui.sources.components.SourceScreenViewModel
|
||||
import ca.gosyer.ui.sources.components.filter.SourceFiltersViewModel
|
||||
import ca.gosyer.ui.sources.settings.SourceSettingsViewModel
|
||||
import ca.gosyer.ui.updates.UpdatesMenuViewModel
|
||||
import ca.gosyer.ui.sources.SourcesScreenViewModel
|
||||
import ca.gosyer.ui.sources.browse.SourceScreenViewModel
|
||||
import ca.gosyer.ui.sources.browse.filter.SourceFiltersViewModel
|
||||
import ca.gosyer.ui.sources.home.SourceHomeScreenViewModel
|
||||
import ca.gosyer.ui.sources.settings.SourceSettingsScreenViewModel
|
||||
import ca.gosyer.ui.updates.UpdatesScreenViewModel
|
||||
import ca.gosyer.uicore.vm.ViewModel
|
||||
import ca.gosyer.uicore.vm.ViewModelFactory
|
||||
import com.github.zsoltk.compose.savedinstancestate.Bundle
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Inject
|
||||
class ViewModelFactoryImpl(
|
||||
private val appThemeFactory: () -> AppThemeViewModel,
|
||||
private val categoryFactory: () -> CategoriesMenuViewModel,
|
||||
private val downloadsFactory: () -> DownloadsMenuViewModel,
|
||||
private val extensionsFactory: () -> ExtensionsMenuViewModel,
|
||||
private val libraryFactory: (bundle: Bundle) -> LibraryScreenViewModel,
|
||||
private val categoryFactory: () -> CategoriesScreenViewModel,
|
||||
private val downloadsFactory: (Boolean) -> DownloadsScreenViewModel,
|
||||
private val extensionsFactory: () -> ExtensionsScreenViewModel,
|
||||
private val libraryFactory: () -> LibraryScreenViewModel,
|
||||
private val debugOverlayFactory: () -> DebugOverlayViewModel,
|
||||
private val trayFactory: () -> TrayViewModel,
|
||||
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 settingsAdvancedFactory: () -> SettingsAdvancedViewModel,
|
||||
private val themesFactory: () -> ThemesViewModel,
|
||||
@@ -55,25 +54,25 @@ class ViewModelFactoryImpl(
|
||||
private val settingsReaderFactory: () -> SettingsReaderViewModel,
|
||||
private val settingsServerFactory: () -> SettingsServerViewModel,
|
||||
private val sourceFiltersFactory: (params: SourceFiltersViewModel.Params) -> SourceFiltersViewModel,
|
||||
private val sourceSettingsFactory: (params: SourceSettingsViewModel.Params) -> SourceSettingsViewModel,
|
||||
private val sourceHomeFactory: (bundle: Bundle) -> SourceHomeScreenViewModel,
|
||||
private val sourceSettingsFactory: (params: SourceSettingsScreenViewModel.Params) -> SourceSettingsScreenViewModel,
|
||||
private val sourceHomeFactory: () -> SourceHomeScreenViewModel,
|
||||
private val sourceFactory: (params: SourceScreenViewModel.Params) -> SourceScreenViewModel,
|
||||
private val sourcesFactory: (bundle: Bundle) -> SourcesMenuViewModel,
|
||||
private val updatesFactory: () -> UpdatesMenuViewModel
|
||||
): ViewModelFactory() {
|
||||
private val sourcesFactory: () -> SourcesScreenViewModel,
|
||||
private val updatesFactory: () -> UpdatesScreenViewModel
|
||||
) : ViewModelFactory() {
|
||||
|
||||
override fun <VM : ViewModel> instantiate(klass: KClass<VM>, arg1: Any?): VM {
|
||||
@Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_ANY")
|
||||
return when (klass) {
|
||||
AppThemeViewModel::class -> appThemeFactory()
|
||||
CategoriesMenuViewModel::class -> categoryFactory()
|
||||
DownloadsMenuViewModel::class -> downloadsFactory()
|
||||
ExtensionsMenuViewModel::class -> extensionsFactory()
|
||||
LibraryScreenViewModel::class -> libraryFactory(arg1 as Bundle)
|
||||
CategoriesScreenViewModel::class -> categoryFactory()
|
||||
DownloadsScreenViewModel::class -> downloadsFactory(arg1 as Boolean)
|
||||
ExtensionsScreenViewModel::class -> extensionsFactory()
|
||||
LibraryScreenViewModel::class -> libraryFactory()
|
||||
DebugOverlayViewModel::class -> debugOverlayFactory()
|
||||
TrayViewModel::class -> trayFactory()
|
||||
MainViewModel::class -> mainFactory()
|
||||
MangaMenuViewModel::class -> mangaFactory(arg1 as MangaMenuViewModel.Params)
|
||||
MangaScreenViewModel::class -> mangaFactory(arg1 as MangaScreenViewModel.Params)
|
||||
ReaderMenuViewModel::class -> readerFactory(arg1 as ReaderMenuViewModel.Params)
|
||||
SettingsAdvancedViewModel::class -> settingsAdvancedFactory()
|
||||
ThemesViewModel::class -> themesFactory()
|
||||
@@ -83,11 +82,11 @@ class ViewModelFactoryImpl(
|
||||
SettingsReaderViewModel::class -> settingsReaderFactory()
|
||||
SettingsServerViewModel::class -> settingsServerFactory()
|
||||
SourceFiltersViewModel::class -> sourceFiltersFactory(arg1 as SourceFiltersViewModel.Params)
|
||||
SourceSettingsViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsViewModel.Params)
|
||||
SourceHomeScreenViewModel::class -> sourceHomeFactory(arg1 as Bundle)
|
||||
SourceSettingsScreenViewModel::class -> sourceSettingsFactory(arg1 as SourceSettingsScreenViewModel.Params)
|
||||
SourceHomeScreenViewModel::class -> sourceHomeFactory()
|
||||
SourceScreenViewModel::class -> sourceFactory(arg1 as SourceScreenViewModel.Params)
|
||||
SourcesMenuViewModel::class -> sourcesFactory(arg1 as Bundle)
|
||||
UpdatesMenuViewModel::class -> updatesFactory()
|
||||
SourcesScreenViewModel::class -> sourcesFactory()
|
||||
UpdatesScreenViewModel::class -> updatesFactory()
|
||||
else -> throw IllegalArgumentException("Unknown ViewModel $klass")
|
||||
} as VM
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class CategoriesMenuViewModel @Inject constructor(
|
||||
class CategoriesScreenViewModel @Inject constructor(
|
||||
private val categoryHandler: CategoryInteractionHandler
|
||||
) : ViewModel() {
|
||||
private var originalCategories = emptyList<Category>()
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.TextField
|
||||
@@ -14,11 +14,12 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.presentation.build.BuildKonfig
|
||||
import ca.gosyer.ui.base.WindowDialog
|
||||
import ca.gosyer.ui.categories.CategoriesScreenViewModel
|
||||
import ca.gosyer.uicore.resources.stringResource
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
fun openRenameDialog(
|
||||
category: CategoriesMenuViewModel.MenuCategory,
|
||||
category: CategoriesScreenViewModel.MenuCategory,
|
||||
onRename: (String) -> Unit
|
||||
) {
|
||||
val newName = MutableStateFlow(TextFieldValue(category.name))
|
||||
@@ -44,8 +45,8 @@ fun openRenameDialog(
|
||||
}
|
||||
|
||||
fun openDeleteDialog(
|
||||
category: CategoriesMenuViewModel.MenuCategory,
|
||||
onDelete: (CategoriesMenuViewModel.MenuCategory) -> Unit
|
||||
category: CategoriesScreenViewModel.MenuCategory,
|
||||
onDelete: (CategoriesScreenViewModel.MenuCategory) -> Unit
|
||||
) {
|
||||
WindowDialog(
|
||||
title = "${BuildKonfig.NAME} - Categories - Delete Dialog",
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.layout.Box
|
||||
@@ -40,16 +40,11 @@ import androidx.compose.material.icons.rounded.KeyboardArrowUp
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.presentation.build.BuildKonfig
|
||||
import ca.gosyer.uicore.vm.viewModel
|
||||
import ca.gosyer.util.compose.ThemedWindow
|
||||
import ca.gosyer.util.lang.launchApplication
|
||||
import ca.gosyer.ui.categories.CategoriesScreenViewModel.MenuCategory
|
||||
import ca.gosyer.uicore.resources.stringResource
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
@@ -57,23 +52,18 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import mu.KotlinLogging
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun openCategoriesMenu(notifyFinished: (() -> Unit)? = null) {
|
||||
launchApplication {
|
||||
ThemedWindow(
|
||||
::exitApplication,
|
||||
title = "${BuildKonfig.NAME} - Categories"
|
||||
) {
|
||||
CategoriesMenu(notifyFinished)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@Composable
|
||||
fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) {
|
||||
val vm = viewModel<CategoriesMenuViewModel>()
|
||||
val categories by vm.categories.collectAsState()
|
||||
fun CategoriesScreenContent(
|
||||
categories: List<MenuCategory>,
|
||||
updateRemoteCategories: suspend () -> Unit,
|
||||
moveCategoryUp: (MenuCategory) -> Unit,
|
||||
moveCategoryDown: (MenuCategory) -> Unit,
|
||||
renameCategory: (MenuCategory, String) -> Unit,
|
||||
deleteCategory: (MenuCategory) -> Unit,
|
||||
createCategory: (String) -> Unit,
|
||||
notifyFinished: (() -> Unit)? = null
|
||||
) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
val logger = KotlinLogging.logger {}
|
||||
@@ -81,7 +71,7 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) {
|
||||
logger.debug { throwable }
|
||||
}
|
||||
GlobalScope.launch(handler) {
|
||||
vm.updateRemoteCategories()
|
||||
updateRemoteCategories()
|
||||
notifyFinished?.invoke()
|
||||
}
|
||||
}
|
||||
@@ -96,16 +86,16 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) {
|
||||
category = category,
|
||||
moveUpEnabled = i != 0,
|
||||
moveDownEnabled = i != categories.lastIndex,
|
||||
onMoveUp = { vm.moveUp(category) },
|
||||
onMoveDown = { vm.moveDown(category) },
|
||||
onMoveUp = { moveCategoryUp(category) },
|
||||
onMoveDown = { moveCategoryDown(category) },
|
||||
onRename = {
|
||||
openRenameDialog(category) {
|
||||
vm.renameCategory(category, it)
|
||||
renameCategory(category, it)
|
||||
}
|
||||
},
|
||||
onDelete = {
|
||||
openDeleteDialog(category) {
|
||||
vm.deleteCategory(category)
|
||||
deleteCategory(category)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -120,7 +110,7 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) {
|
||||
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp),
|
||||
onClick = {
|
||||
openCreateDialog {
|
||||
vm.createCategory(it)
|
||||
createCategory(it)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -136,7 +126,7 @@ fun CategoriesMenu(notifyFinished: (() -> Unit)? = null) {
|
||||
|
||||
@Composable
|
||||
private fun CategoryRow(
|
||||
category: CategoriesMenuViewModel.MenuCategory,
|
||||
category: MenuCategory,
|
||||
moveUpEnabled: Boolean = true,
|
||||
moveDownEnabled: Boolean = true,
|
||||
onMoveUp: () -> Unit = {},
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,24 @@ import ca.gosyer.data.models.Chapter
|
||||
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
|
||||
import ca.gosyer.data.server.interactions.DownloadInteractionHandler
|
||||
import ca.gosyer.uicore.vm.ViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class DownloadsMenuViewModel @Inject constructor(
|
||||
class DownloadsScreenViewModel @Inject constructor(
|
||||
private val downloadService: DownloadService,
|
||||
private val downloadsHandler: DownloadInteractionHandler,
|
||||
private val chapterHandler: ChapterInteractionHandler
|
||||
private val chapterHandler: ChapterInteractionHandler,
|
||||
standalone: Boolean
|
||||
) : ViewModel() {
|
||||
private val uiScope = if (standalone) {
|
||||
MainScope()
|
||||
} else null
|
||||
|
||||
override val scope: CoroutineScope
|
||||
get() = uiScope ?: super.scope
|
||||
|
||||
val serviceStatus get() = downloadService.status
|
||||
val downloaderStatus get() = downloadService.downloaderStatus
|
||||
val downloadQueue get() = downloadService.downloadQueue
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.foundation.VerticalScrollbar
|
||||
@@ -28,7 +28,6 @@ import androidx.compose.material.Icon
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.ProgressIndicatorDefaults
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.PlayArrow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.models.Chapter
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.presentation.build.BuildKonfig
|
||||
import ca.gosyer.ui.base.navigation.ActionIcon
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.ui.manga.openMangaMenu
|
||||
import ca.gosyer.uicore.components.DropdownIconButton
|
||||
import ca.gosyer.uicore.components.MangaListItem
|
||||
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.MangaListItemTitle
|
||||
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 io.kamel.image.lazyPainterResource
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun openDownloadsMenu() {
|
||||
launchApplication {
|
||||
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
|
||||
Surface {
|
||||
DownloadsMenu(::openMangaMenu)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DownloadsMenu(onMangaClick: (Long) -> Unit) {
|
||||
val vm = viewModel<DownloadsMenuViewModel>()
|
||||
val downloadQueue by vm.downloadQueue.collectAsState()
|
||||
|
||||
fun DownloadsScreenContent(
|
||||
downloadQueue: List<DownloadChapter>,
|
||||
downloadStatus: DownloaderStatus,
|
||||
startDownloading: () -> Unit,
|
||||
pauseDownloading: () -> Unit,
|
||||
clearQueue: () -> Unit,
|
||||
onMangaClick: (Long) -> Unit,
|
||||
stopDownload: (Chapter) -> Unit,
|
||||
moveDownloadToBottom: (Chapter) -> Unit
|
||||
) {
|
||||
Column {
|
||||
Toolbar(
|
||||
stringResource(MR.strings.location_downloads),
|
||||
closable = false,
|
||||
actions = {
|
||||
val downloadStatus by vm.downloaderStatus.collectAsState()
|
||||
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 {
|
||||
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 {
|
||||
@@ -103,8 +89,8 @@ fun DownloadsMenu(onMangaClick: (Long) -> Unit) {
|
||||
DownloadsItem(
|
||||
it,
|
||||
{ onMangaClick(it.mangaId) },
|
||||
vm::stopDownload,
|
||||
vm::moveToBottom
|
||||
stopDownload,
|
||||
moveDownloadToBottom
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import java.util.Locale
|
||||
|
||||
class ExtensionsMenuViewModel @Inject constructor(
|
||||
class ExtensionsScreenViewModel @Inject constructor(
|
||||
private val extensionHandler: ExtensionInteractionHandler,
|
||||
extensionPreferences: ExtensionPreferences
|
||||
) : ViewModel() {
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.background
|
||||
@@ -26,7 +26,6 @@ import androidx.compose.foundation.rememberScrollbarAdapter
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
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.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.rememberWindowState
|
||||
import ca.gosyer.data.models.Extension
|
||||
import ca.gosyer.i18n.MR
|
||||
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.uicore.components.LoadingScreen
|
||||
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 io.kamel.image.lazyPainterResource
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
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
|
||||
fun ExtensionsMenu() {
|
||||
val vm = viewModel<ExtensionsMenuViewModel>()
|
||||
val extensions by vm.extensions.collectAsState()
|
||||
val isLoading by vm.isLoading.collectAsState()
|
||||
val search by vm.searchQuery.collectAsState()
|
||||
|
||||
fun ExtensionsScreenContent(
|
||||
extensions: Map<String, List<Extension>>,
|
||||
isLoading: Boolean,
|
||||
query: String?,
|
||||
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) {
|
||||
Column {
|
||||
ExtensionsToolbar(
|
||||
search,
|
||||
vm::search,
|
||||
vm.enabledLangs,
|
||||
vm::getSourceLanguages,
|
||||
vm::setEnabledLanguages
|
||||
query,
|
||||
setQuery,
|
||||
enabledLangs,
|
||||
getSourceLanguages,
|
||||
setEnabledLanguages
|
||||
)
|
||||
LoadingScreen(isLoading)
|
||||
}
|
||||
} else {
|
||||
val state = persistentLazyListState()
|
||||
val state = rememberLazyListState()
|
||||
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
item {
|
||||
ExtensionsToolbar(
|
||||
search,
|
||||
vm::search,
|
||||
vm.enabledLangs,
|
||||
vm::getSourceLanguages,
|
||||
vm::setEnabledLanguages
|
||||
query,
|
||||
setQuery,
|
||||
enabledLangs,
|
||||
getSourceLanguages,
|
||||
setEnabledLanguages
|
||||
)
|
||||
}
|
||||
extensions.forEach { (header, items) ->
|
||||
@@ -120,9 +106,9 @@ fun ExtensionsMenu() {
|
||||
items(items) { extension ->
|
||||
ExtensionItem(
|
||||
extension,
|
||||
onInstallClicked = vm::install,
|
||||
onUpdateClicked = vm::update,
|
||||
onUninstallClicked = vm::uninstall
|
||||
onInstallClicked = installExtension,
|
||||
onUpdateClicked = updateExtension,
|
||||
onUninstallClicked = uninstallExtension
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
@@ -6,197 +6,55 @@
|
||||
|
||||
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.Tab
|
||||
import androidx.compose.material.Text
|
||||
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.getValue
|
||||
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 androidx.compose.runtime.remember
|
||||
import ca.gosyer.presentation.build.BuildKonfig
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.ui.manga.openMangaMenu
|
||||
import ca.gosyer.uicore.components.LoadingScreen
|
||||
import ca.gosyer.ui.library.components.LibraryScreenContent
|
||||
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 ca.gosyer.util.compose.ThemedWindow
|
||||
import ca.gosyer.util.lang.launchApplication
|
||||
import com.github.zsoltk.compose.savedinstancestate.Bundle
|
||||
import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
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
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun openLibraryMenu() {
|
||||
launchApplication {
|
||||
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
|
||||
CompositionLocalProvider(
|
||||
LocalSavedInstanceState provides Bundle()
|
||||
) {
|
||||
Surface {
|
||||
LibraryScreen()
|
||||
}
|
||||
Surface {
|
||||
Navigator(remember { LibraryScreen() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LibraryScreen(onClickManga: (Long) -> Unit = ::openMangaMenu) {
|
||||
LibraryScreen(LocalSavedInstanceState.current, onClickManga)
|
||||
}
|
||||
class LibraryScreen : Screen {
|
||||
|
||||
@Composable
|
||||
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)
|
||||
override val key: ScreenKey = uniqueScreenKey
|
||||
|
||||
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 = 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 {}
|
||||
}
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val vm = viewModel<LibraryScreenViewModel>()
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
LibraryScreenContent(
|
||||
categories = vm.categories.collectAsState().value,
|
||||
selectedCategoryIndex = vm.selectedCategoryIndex.collectAsState().value,
|
||||
displayMode = vm.displayMode.collectAsState().value,
|
||||
isLoading = vm.isLoading.collectAsState().value,
|
||||
error = vm.error.collectAsState().value,
|
||||
query = vm.query.collectAsState().value,
|
||||
updateQuery = vm::updateQuery,
|
||||
getLibraryForPage = { vm.getLibraryForCategoryId(it).collectAsState() },
|
||||
onPageChanged = vm::setSelectedPage,
|
||||
onClickManga = { navigator push MangaScreen(it) },
|
||||
onRemoveMangaClicked = vm::removeManga
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,6 @@ import ca.gosyer.data.server.interactions.CategoryInteractionHandler
|
||||
import ca.gosyer.data.server.interactions.LibraryInteractionHandler
|
||||
import ca.gosyer.data.server.interactions.UpdatesInteractionHandler
|
||||
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.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -74,13 +71,12 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
private val categoryHandler: CategoryInteractionHandler,
|
||||
private val libraryHandler: LibraryInteractionHandler,
|
||||
private val updatesHandler: UpdatesInteractionHandler,
|
||||
libraryPreferences: LibraryPreferences,
|
||||
private val bundle: Bundle,
|
||||
libraryPreferences: LibraryPreferences
|
||||
) : ViewModel() {
|
||||
private val library = Library(MutableStateFlow(emptyList()), mutableMapOf())
|
||||
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 displayMode = libraryPreferences.displayMode().stateIn(scope)
|
||||
@@ -91,7 +87,7 @@ class LibraryScreenViewModel @Inject constructor(
|
||||
private val _error = MutableStateFlow<String?>(null)
|
||||
val error = _error.asStateFlow()
|
||||
|
||||
private val _query = saveStringInBundle(scope, bundle, QUERY_KEY)
|
||||
private val _query = MutableStateFlow("")
|
||||
val query = _query.asStateFlow()
|
||||
|
||||
init {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.layout.Row
|
||||
@@ -36,7 +36,9 @@ fun LibraryMangaBadges(
|
||||
if (downloaded != null && downloaded > 0) {
|
||||
Text(
|
||||
text = downloaded.toString(),
|
||||
modifier = Modifier.background(MaterialTheme.colors.secondary).then(BadgesInnerPadding),
|
||||
modifier = Modifier.background(MaterialTheme.colors.secondary).then(
|
||||
BadgesInnerPadding
|
||||
),
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onSecondary
|
||||
)
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.VerticalScrollbar
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
package ca.gosyer.ui.main
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -26,45 +25,25 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.ui.base.navigation.LocalMenuController
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
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.base.navigation.DisplayController
|
||||
import ca.gosyer.ui.base.navigation.withDisplayController
|
||||
import ca.gosyer.ui.main.components.SideMenu
|
||||
import ca.gosyer.ui.manga.MangaMenu
|
||||
import ca.gosyer.ui.reader.openReaderMenu
|
||||
import ca.gosyer.ui.settings.SettingsAdvancedScreen
|
||||
import ca.gosyer.ui.settings.SettingsAppearance
|
||||
import ca.gosyer.ui.settings.SettingsBackupScreen
|
||||
import ca.gosyer.ui.settings.SettingsBrowseScreen
|
||||
import ca.gosyer.ui.settings.SettingsGeneralScreen
|
||||
import ca.gosyer.ui.settings.SettingsLibraryScreen
|
||||
import ca.gosyer.ui.settings.SettingsReaderScreen
|
||||
import ca.gosyer.ui.settings.SettingsScreen
|
||||
import ca.gosyer.ui.settings.SettingsServerScreen
|
||||
import ca.gosyer.ui.sources.SourcesMenu
|
||||
import 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
|
||||
import ca.gosyer.uicore.vm.LocalViewModelFactory
|
||||
import cafe.adriel.voyager.navigator.CurrentScreen
|
||||
import cafe.adriel.voyager.navigator.Navigator
|
||||
|
||||
const val SIDE_MENU_EXPAND_DURATION = 500
|
||||
|
||||
@Composable
|
||||
fun MainMenu(rootBundle: Bundle) {
|
||||
val vm = viewModel<MainViewModel>()
|
||||
fun MainMenu() {
|
||||
val vmFactory = LocalViewModelFactory.current
|
||||
val vm = remember { vmFactory.instantiate<MainViewModel>() }
|
||||
Surface {
|
||||
Router("TopLevel", vm.startScreen.toRoute()) { backStack ->
|
||||
val controller = remember {
|
||||
MenuController(backStack)
|
||||
}
|
||||
Navigator(vm.startScreen.toScreen()) { navigator ->
|
||||
val controller = remember { DisplayController() }
|
||||
BoxWithConstraints {
|
||||
// if (maxWidth > 720.dp) {
|
||||
WideMainMenu(rootBundle, controller)
|
||||
WideMainMenu(navigator, controller)
|
||||
// } else {
|
||||
// SkinnyMainMenu(rootBundle, controller)
|
||||
// }
|
||||
@@ -75,8 +54,8 @@ fun MainMenu(rootBundle: Bundle) {
|
||||
|
||||
@Composable
|
||||
fun SkinnyMainMenu(
|
||||
rootBundle: Bundle,
|
||||
controller: MenuController
|
||||
navigator: Navigator,
|
||||
controller: DisplayController
|
||||
) {
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
LaunchedEffect(controller.sideMenuVisible) {
|
||||
@@ -104,21 +83,21 @@ fun SkinnyMainMenu(
|
||||
|
||||
ModalDrawer(
|
||||
{
|
||||
SideMenu(Modifier.fillMaxWidth(), controller)
|
||||
SideMenu(Modifier.fillMaxWidth(), controller, navigator)
|
||||
},
|
||||
drawerState = drawerState,
|
||||
gesturesEnabled = drawerState.isOpen
|
||||
) {
|
||||
withMenuController(controller) {
|
||||
MainWindow(Modifier, rootBundle)
|
||||
withDisplayController(controller) {
|
||||
MainWindow(Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WideMainMenu(
|
||||
rootBundle: Bundle,
|
||||
controller: MenuController
|
||||
navigator: Navigator,
|
||||
controller: DisplayController
|
||||
) {
|
||||
Box {
|
||||
val startPadding by animateDpAsState(
|
||||
@@ -130,72 +109,17 @@ fun WideMainMenu(
|
||||
animationSpec = tween(SIDE_MENU_EXPAND_DURATION)
|
||||
)
|
||||
if (startPadding != 0.dp) {
|
||||
SideMenu(Modifier.width(200.dp), controller)
|
||||
SideMenu(Modifier.width(200.dp), controller, navigator)
|
||||
}
|
||||
withMenuController(controller) {
|
||||
MainWindow(Modifier.padding(start = startPadding), rootBundle)
|
||||
withDisplayController(controller) {
|
||||
MainWindow(Modifier.padding(start = startPadding))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainWindow(modifier: Modifier, rootBundle: Bundle) {
|
||||
Surface(Modifier.fillMaxSize().then(modifier)) {
|
||||
val menuController = LocalMenuController.current!!
|
||||
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")
|
||||
}
|
||||
}*/
|
||||
fun MainWindow(modifier: Modifier) {
|
||||
Surface(Modifier.fillMaxSize() then modifier) {
|
||||
CurrentScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,13 @@ package ca.gosyer.ui.main
|
||||
|
||||
import ca.gosyer.data.ui.UiPreferences
|
||||
import ca.gosyer.uicore.vm.ViewModel
|
||||
import kotlinx.coroutines.MainScope
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class MainViewModel @Inject constructor(
|
||||
uiPreferences: UiPreferences
|
||||
) : ViewModel() {
|
||||
override val scope = MainScope()
|
||||
|
||||
val startScreen = uiPreferences.startScreen().get()
|
||||
}
|
||||
|
||||
@@ -7,37 +7,14 @@
|
||||
package ca.gosyer.ui.main
|
||||
|
||||
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 {
|
||||
object Library : Routes()
|
||||
object Updates : Routes()
|
||||
object Sources : Routes()
|
||||
object Extensions : Routes()
|
||||
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
|
||||
fun StartScreen.toScreen() = when (this) {
|
||||
StartScreen.Library -> LibraryScreen()
|
||||
StartScreen.Updates -> UpdatesScreen()
|
||||
StartScreen.Sources -> SourcesScreen()
|
||||
StartScreen.Extensions -> ExtensionsScreen()
|
||||
}
|
||||
|
||||
@@ -22,28 +22,37 @@ import androidx.compose.material.icons.rounded.Store
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
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.library.LibraryScreen
|
||||
import ca.gosyer.ui.library.openLibraryMenu
|
||||
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 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 kotlin.reflect.KClass
|
||||
|
||||
enum class TopLevelMenus(
|
||||
val textKey: StringResource,
|
||||
val unselectedIcon: ImageVector,
|
||||
val selectedIcon: ImageVector,
|
||||
val menu: Routes,
|
||||
val screen: KClass<*>,
|
||||
val createScreen: () -> Screen,
|
||||
val top: Boolean,
|
||||
val openInNewWindow: () -> Unit = {},
|
||||
val extraInfo: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
Library(MR.strings.location_library, Icons.Outlined.Book, Icons.Rounded.Book, Routes.Library, true, ::openLibraryMenu),
|
||||
Updates(MR.strings.location_updates, Icons.Outlined.NewReleases, Icons.Rounded.NewReleases, Routes.Updates, true, ::openLibraryMenu),
|
||||
Sources(MR.strings.location_sources, Icons.Outlined.Explore, Icons.Rounded.Explore, Routes.Sources, true, ::openSourcesMenu),
|
||||
Extensions(MR.strings.location_extensions, Icons.Outlined.Store, Icons.Rounded.Store, Routes.Extensions, true, ::openExtensionsMenu),
|
||||
Downloads(MR.strings.location_downloads, Icons.Outlined.Download, Icons.Rounded.Download, Routes.Downloads, false, extraInfo = { DownloadsExtraInfo() }),
|
||||
Settings(MR.strings.location_settings, Icons.Outlined.Settings, Icons.Rounded.Settings, Routes.Settings, false);
|
||||
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, UpdatesScreen::class, { UpdatesScreen() }, true, ::openLibraryMenu),
|
||||
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, ExtensionsScreen::class, { ExtensionsScreen() }, true, ::openExtensionsMenu),
|
||||
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, SettingsScreen::class, { SettingsScreen() }, false);
|
||||
|
||||
fun isSelected(backStack: BackStack<Routes>) = backStack.elements.first() == menu
|
||||
fun isSelected(navigator: Navigator) = navigator.items.first()::class == screen
|
||||
}
|
||||
|
||||
@@ -4,20 +4,21 @@
|
||||
* 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.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import ca.gosyer.uicore.vm.viewModel
|
||||
import ca.gosyer.ui.main.components.DebugOverlayViewModel
|
||||
import ca.gosyer.uicore.vm.LocalViewModelFactory
|
||||
|
||||
@Composable
|
||||
fun DebugOverlay() {
|
||||
val vm = viewModel<DebugOverlayViewModel>()
|
||||
val vmFactory = LocalViewModelFactory.current
|
||||
val vm = remember { vmFactory.instantiate<DebugOverlayViewModel>() }
|
||||
val usedMemory by vm.usedMemoryFlow.collectAsState()
|
||||
Column {
|
||||
Text("$usedMemory/${vm.maxMemory}", color = Color.White)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
package ca.gosyer.ui.main.components
|
||||
|
||||
import ca.gosyer.uicore.vm.ViewModel
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -14,6 +15,8 @@ import me.tatarka.inject.annotations.Inject
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class DebugOverlayViewModel @Inject constructor() : ViewModel() {
|
||||
override val scope = MainScope()
|
||||
|
||||
val runtime: Runtime = Runtime.getRuntime()
|
||||
val maxMemory = runtime.maxMemory().formatSize()
|
||||
val usedMemoryFlow = MutableStateFlow(runtime.usedMemory().formatSize())
|
||||
|
||||
@@ -15,17 +15,19 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.data.base.WebsocketService
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.uicore.vm.viewModel
|
||||
import ca.gosyer.ui.downloads.DownloadsMenuViewModel
|
||||
import ca.gosyer.ui.downloads.DownloadsScreenViewModel
|
||||
import ca.gosyer.uicore.resources.stringResource
|
||||
import ca.gosyer.uicore.vm.LocalViewModelFactory
|
||||
|
||||
@Composable
|
||||
fun DownloadsExtraInfo() {
|
||||
val vm = viewModel<DownloadsMenuViewModel>()
|
||||
val vmFactory = LocalViewModelFactory.current
|
||||
val vm = remember { vmFactory.instantiate<DownloadsScreenViewModel>(true) }
|
||||
val status by vm.serviceStatus.collectAsState()
|
||||
val list by vm.downloadQueue.collectAsState()
|
||||
val text = when (status) {
|
||||
|
||||
@@ -29,11 +29,12 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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 cafe.adriel.voyager.navigator.Navigator
|
||||
|
||||
@Composable
|
||||
fun SideMenu(modifier: Modifier, controller: MenuController) {
|
||||
fun SideMenu(modifier: Modifier, controller: DisplayController, navigator: Navigator) {
|
||||
Surface(modifier then Modifier.fillMaxHeight(), elevation = 2.dp) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.fillMaxSize().padding(horizontal = 4.dp)) {
|
||||
@@ -54,19 +55,17 @@ fun SideMenu(modifier: Modifier, controller: MenuController) {
|
||||
Spacer(Modifier.height(20.dp))
|
||||
remember { TopLevelMenus.values().filter(TopLevelMenus::top) }.forEach { topLevelMenu ->
|
||||
SideMenuItem(
|
||||
topLevelMenu.isSelected(controller.backStack),
|
||||
topLevelMenu,
|
||||
controller::newRoot
|
||||
)
|
||||
topLevelMenu.isSelected(navigator),
|
||||
topLevelMenu
|
||||
) { navigator replaceAll it }
|
||||
}
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.align(Alignment.BottomStart).padding(bottom = 8.dp)) {
|
||||
remember { TopLevelMenus.values().filterNot(TopLevelMenus::top) }.forEach { topLevelMenu ->
|
||||
SideMenuItem(
|
||||
topLevelMenu.isSelected(controller.backStack),
|
||||
topLevelMenu.isSelected(navigator),
|
||||
topLevelMenu,
|
||||
controller::newRoot
|
||||
)
|
||||
) { navigator replaceAll it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,17 +25,17 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.ui.main.Routes
|
||||
import ca.gosyer.ui.main.TopLevelMenus
|
||||
import ca.gosyer.uicore.components.combinedMouseClickable
|
||||
import ca.gosyer.uicore.resources.stringResource
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
|
||||
@Composable
|
||||
fun SideMenuItem(selected: Boolean, topLevelMenu: TopLevelMenus, newRoot: (Routes) -> Unit) {
|
||||
fun SideMenuItem(selected: Boolean, topLevelMenu: TopLevelMenus, newRoot: (Screen) -> Unit) {
|
||||
SideMenuItem(
|
||||
selected,
|
||||
stringResource(topLevelMenu.textKey),
|
||||
topLevelMenu.menu,
|
||||
topLevelMenu.createScreen,
|
||||
topLevelMenu.selectedIcon,
|
||||
topLevelMenu.unselectedIcon,
|
||||
topLevelMenu.openInNewWindow,
|
||||
@@ -48,12 +48,12 @@ fun SideMenuItem(selected: Boolean, topLevelMenu: TopLevelMenus, newRoot: (Route
|
||||
private fun SideMenuItem(
|
||||
selected: Boolean,
|
||||
text: String,
|
||||
menu: Routes,
|
||||
createScreen: () -> Screen,
|
||||
selectedIcon: ImageVector,
|
||||
unselectedIcon: ImageVector,
|
||||
onMiddleClick: () -> Unit,
|
||||
extraInfo: (@Composable () -> Unit)? = null,
|
||||
onClick: (Routes) -> Unit
|
||||
onClick: (Screen) -> Unit
|
||||
) {
|
||||
Card(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -70,7 +70,7 @@ private fun SideMenuItem(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.height(40.dp)
|
||||
.combinedMouseClickable(
|
||||
onClick = { onClick(menu) },
|
||||
onClick = { onClick(createScreen()) },
|
||||
onMiddleClick = { onMiddleClick() }
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -8,6 +8,7 @@ package ca.gosyer.ui.main.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.window.ApplicationScope
|
||||
import androidx.compose.ui.window.Notification
|
||||
@@ -15,13 +16,14 @@ import androidx.compose.ui.window.Tray
|
||||
import androidx.compose.ui.window.rememberTrayState
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.presentation.build.BuildKonfig
|
||||
import ca.gosyer.uicore.vm.viewModel
|
||||
import ca.gosyer.uicore.vm.LocalViewModelFactory
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun ApplicationScope.Tray(icon: Painter) {
|
||||
val vm = viewModel<TrayViewModel>()
|
||||
val vmFactory = LocalViewModelFactory.current
|
||||
val vm = remember { vmFactory.instantiate<TrayViewModel>() }
|
||||
val trayState = rememberTrayState()
|
||||
Tray(
|
||||
icon,
|
||||
|
||||
@@ -8,11 +8,14 @@ package ca.gosyer.ui.main.components
|
||||
|
||||
import ca.gosyer.data.update.UpdateChecker
|
||||
import ca.gosyer.uicore.vm.ViewModel
|
||||
import kotlinx.coroutines.MainScope
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class TrayViewModel @Inject constructor(
|
||||
private val updateChecker: UpdateChecker
|
||||
) : ViewModel() {
|
||||
override val scope = MainScope()
|
||||
|
||||
init {
|
||||
updateChecker.checkForUpdates()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
class MangaMenuViewModel @Inject constructor(
|
||||
class MangaScreenViewModel @Inject constructor(
|
||||
private val mangaHandler: MangaInteractionHandler,
|
||||
private val chapterHandler: ChapterInteractionHandler,
|
||||
private val categoryHandler: CategoryInteractionHandler,
|
||||
@@ -226,7 +226,7 @@ class MangaMenuViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
override fun onDispose() {
|
||||
downloadService.removeWatch(params.mangaId)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.layout.Arrangement
|
||||
@@ -4,9 +4,8 @@
|
||||
* 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.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -32,13 +31,7 @@ import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -50,137 +43,12 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
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.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.vm.viewModel
|
||||
import ca.gosyer.util.compose.ThemedWindow
|
||||
import ca.gosyer.util.lang.launchApplication
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import ca.gosyer.uicore.resources.stringResource
|
||||
import io.kamel.image.lazyPainterResource
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
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
|
||||
fun MangaItem(manga: Manga) {
|
||||
BoxWithConstraints(Modifier.padding(8.dp)) {
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,13 +71,13 @@ import ca.gosyer.ui.reader.navigation.RightAndLeftNavigation
|
||||
import ca.gosyer.ui.reader.navigation.navigationClickable
|
||||
import ca.gosyer.ui.reader.viewer.ContinuousReader
|
||||
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.LoadingScreen
|
||||
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.vm.LocalViewModelFactory
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
@@ -156,9 +156,8 @@ fun ReaderMenu(
|
||||
mangaId: Long,
|
||||
hotkeyFlow: SharedFlow<KeyEvent>
|
||||
) {
|
||||
val vm = viewModel {
|
||||
instantiate<ReaderMenuViewModel>(ReaderMenuViewModel.Params(chapterIndex, mangaId))
|
||||
}
|
||||
val vmFactory = LocalViewModelFactory.current
|
||||
val vm = remember { vmFactory.instantiate<ReaderMenuViewModel>(ReaderMenuViewModel.Params(chapterIndex, mangaId)) }
|
||||
|
||||
val state by vm.state.collectAsState()
|
||||
val previousChapter by vm.previousChapter.collectAsState()
|
||||
|
||||
@@ -17,7 +17,6 @@ import ca.gosyer.data.reader.ReaderPreferences
|
||||
import ca.gosyer.data.reader.model.Direction
|
||||
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
|
||||
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.Navigation
|
||||
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.ViewerChapters
|
||||
import ca.gosyer.uicore.prefs.asStateIn
|
||||
import ca.gosyer.uicore.vm.ViewModel
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -279,7 +279,7 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
override fun onDispose() {
|
||||
viewerChapters.recycle()
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import ca.gosyer.data.reader.ReaderPreferences
|
||||
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
|
||||
import ca.gosyer.ui.reader.model.ReaderChapter
|
||||
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.ktor.client.features.onDownload
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@@ -24,7 +24,7 @@ import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import ca.gosyer.ui.reader.model.Navigation
|
||||
import ca.gosyer.util.compose.contains
|
||||
import ca.gosyer.ui.util.compose.contains
|
||||
|
||||
fun Modifier.navigationClickable(
|
||||
navigation: ViewerNavigation,
|
||||
|
||||
@@ -21,14 +21,29 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.data.update.UpdatePreferences
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
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.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
|
||||
|
||||
class SettingsAdvancedScreen : Screen {
|
||||
override val key: ScreenKey = uniqueScreenKey
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val vm = viewModel<SettingsAdvancedViewModel>()
|
||||
SettingsAdvancedScreenContent(
|
||||
updatesEnabled = vm.updatesEnabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsAdvancedViewModel @Inject constructor(
|
||||
updatePreferences: UpdatePreferences,
|
||||
) : ViewModel() {
|
||||
@@ -36,15 +51,16 @@ class SettingsAdvancedViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsAdvancedScreen(menuController: MenuController) {
|
||||
val vm = viewModel<SettingsAdvancedViewModel>()
|
||||
fun SettingsAdvancedScreenContent(
|
||||
updatesEnabled: PreferenceMutableStateFlow<Boolean>
|
||||
) {
|
||||
Column {
|
||||
Toolbar(stringResource(MR.strings.settings_advanced_screen), menuController, true)
|
||||
Toolbar(stringResource(MR.strings.settings_advanced_screen))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
item {
|
||||
SwitchPreference(preference = vm.updatesEnabled, title = stringResource(MR.strings.update_checker))
|
||||
SwitchPreference(preference = updatesEnabled, title = stringResource(MR.strings.update_checker))
|
||||
}
|
||||
}
|
||||
VerticalScrollbar(
|
||||
|
||||
@@ -37,7 +37,6 @@ import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.data.ui.UiPreferences
|
||||
import ca.gosyer.data.ui.model.ThemeMode
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.ui.base.prefs.ChoicePreference
|
||||
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.getDarkColors
|
||||
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.themes
|
||||
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
|
||||
|
||||
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(
|
||||
private val uiPreferences: UiPreferences,
|
||||
) : ViewModel() {
|
||||
@@ -72,23 +91,26 @@ class ThemesViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsAppearance(menuController: MenuController) {
|
||||
val vm = viewModel<ThemesViewModel>()
|
||||
|
||||
val activeColors = vm.getActiveColors()
|
||||
fun SettingsAppearanceScreenContent(
|
||||
activeColors: AppColorsPreferenceState,
|
||||
themeMode: PreferenceMutableStateFlow<ThemeMode>,
|
||||
lightTheme: PreferenceMutableStateFlow<Int>,
|
||||
darkTheme: PreferenceMutableStateFlow<Int>,
|
||||
windowDecorations: PreferenceMutableStateFlow<Boolean>
|
||||
) {
|
||||
val isLight = MaterialTheme.colors.isLight
|
||||
val themesForCurrentMode = remember(isLight) {
|
||||
themes.filter { it.colors.isLight == isLight }
|
||||
}
|
||||
|
||||
Column {
|
||||
Toolbar(stringResource(MR.strings.settings_appearance_screen), menuController, true)
|
||||
Toolbar(stringResource(MR.strings.settings_appearance_screen))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
item {
|
||||
ChoicePreference(
|
||||
preference = vm.themeMode,
|
||||
preference = themeMode,
|
||||
choices = mapOf(
|
||||
ThemeMode.System to stringResource(MR.strings.theme_follow_system),
|
||||
ThemeMode.Light to stringResource(MR.strings.theme_light),
|
||||
@@ -107,7 +129,7 @@ fun SettingsAppearance(menuController: MenuController) {
|
||||
ThemeItem(
|
||||
theme,
|
||||
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.secondaryStateFlow.value = it.colors.secondary
|
||||
}
|
||||
@@ -133,7 +155,7 @@ fun SettingsAppearance(menuController: MenuController) {
|
||||
}
|
||||
item {
|
||||
SwitchPreference(
|
||||
vm.windowDecorations,
|
||||
windowDecorations,
|
||||
stringResource(MR.strings.window_decorations),
|
||||
stringResource(MR.strings.window_decorations_sub)
|
||||
)
|
||||
|
||||
@@ -27,7 +27,6 @@ import androidx.compose.material.icons.rounded.Warning
|
||||
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.Modifier
|
||||
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.i18n.MR
|
||||
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.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.util.system.filePicker
|
||||
import ca.gosyer.util.system.fileSaver
|
||||
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 io.ktor.client.features.onDownload
|
||||
import io.ktor.client.features.onUpload
|
||||
import io.ktor.http.isSuccess
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -62,6 +64,29 @@ import okio.Path.Companion.toOkioPath
|
||||
import okio.buffer
|
||||
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(
|
||||
private val backupHandler: BackupInteractionHandler
|
||||
) : ViewModel() {
|
||||
@@ -186,22 +211,28 @@ class SettingsBackupViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsBackupScreen(menuController: MenuController) {
|
||||
val vm = viewModel<SettingsBackupViewModel>()
|
||||
val restoring by vm.restoring.collectAsState()
|
||||
val restoringProgress by vm.restoringProgress.collectAsState()
|
||||
val restoreStatus by vm.restoreStatus.collectAsState()
|
||||
val creating by vm.creating.collectAsState()
|
||||
val creatingProgress by vm.creatingProgress.collectAsState()
|
||||
val creatingStatus by vm.creatingStatus.collectAsState()
|
||||
private fun SettingsBackupScreenContent(
|
||||
restoring: Boolean,
|
||||
restoringProgress: Float?,
|
||||
restoreStatus: SettingsBackupViewModel.Status,
|
||||
creating: Boolean,
|
||||
creatingProgress: Float?,
|
||||
creatingStatus: SettingsBackupViewModel.Status,
|
||||
missingSourceFlow: SharedFlow<Pair<Path, List<String>>>,
|
||||
createFlow: SharedFlow<Pair<String, (Path) -> Unit>>,
|
||||
restoreFile: (Path?) -> Unit,
|
||||
restoreBackup: (Path) -> Unit,
|
||||
stopRestore: () -> Unit,
|
||||
exportBackup: () -> Unit
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
vm.missingSourceFlow.collect { (backup, sources) ->
|
||||
openMissingSourcesDialog(sources, { vm.restoreBackup(backup) }, vm::stopRestore)
|
||||
missingSourceFlow.collect { (backup, sources) ->
|
||||
openMissingSourcesDialog(sources, { restoreBackup(backup) }, stopRestore)
|
||||
}
|
||||
}
|
||||
launch {
|
||||
vm.createFlow.collect { (filename, function) ->
|
||||
createFlow.collect { (filename, function) ->
|
||||
fileSaver(filename, "proto.gz") {
|
||||
function(it.selectedFile.toOkioPath())
|
||||
}
|
||||
@@ -210,7 +241,7 @@ fun SettingsBackupScreen(menuController: MenuController) {
|
||||
}
|
||||
|
||||
Column {
|
||||
Toolbar(stringResource(MR.strings.settings_backup_screen), menuController, true)
|
||||
Toolbar(stringResource(MR.strings.settings_backup_screen))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
@@ -223,7 +254,7 @@ fun SettingsBackupScreen(menuController: MenuController) {
|
||||
restoreStatus
|
||||
) {
|
||||
filePicker("gz") {
|
||||
vm.restoreFile(it.selectedFile.toOkioPath())
|
||||
restoreFile(it.selectedFile.toOkioPath())
|
||||
}
|
||||
}
|
||||
PreferenceFile(
|
||||
@@ -232,7 +263,7 @@ fun SettingsBackupScreen(menuController: MenuController) {
|
||||
creating,
|
||||
creatingProgress,
|
||||
creatingStatus,
|
||||
vm::exportBackup
|
||||
exportBackup
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
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
|
||||
fun SettingsBrowseScreen(menuController: MenuController) {
|
||||
fun SettingsBrowseScreenContent() {
|
||||
Column {
|
||||
Toolbar(stringResource(MR.strings.settings_browse_screen), menuController, true)
|
||||
Toolbar(stringResource(MR.strings.settings_browse_screen))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
|
||||
@@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
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
|
||||
fun SettingsDownloadsScreen(menuController: MenuController) {
|
||||
fun SettingsDownloadsScreenContent() {
|
||||
Column {
|
||||
Toolbar(stringResource(MR.strings.settings_download_screen), menuController, true)
|
||||
Toolbar(stringResource(MR.strings.settings_download_screen))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
|
||||
@@ -23,13 +23,16 @@ import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.data.ui.UiPreferences
|
||||
import ca.gosyer.data.ui.model.StartScreen
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.ui.base.prefs.ChoicePreference
|
||||
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.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 okio.Path.Companion.toPath
|
||||
import okio.asResourceFileSystem
|
||||
@@ -39,6 +42,24 @@ import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
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(
|
||||
uiPreferences: UiPreferences,
|
||||
) : ViewModel() {
|
||||
@@ -97,23 +118,30 @@ class SettingsGeneralViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsGeneralScreen(menuController: MenuController) {
|
||||
val vm = viewModel<SettingsGeneralViewModel>()
|
||||
fun SettingsGeneralScreenContent(
|
||||
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 {
|
||||
Toolbar(stringResource(MR.strings.settings_general_screen), menuController, closable = true)
|
||||
Toolbar(stringResource(MR.strings.settings_general_screen))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
item {
|
||||
ChoicePreference(
|
||||
preference = vm.startScreen,
|
||||
preference = startScreen,
|
||||
title = stringResource(MR.strings.start_screen),
|
||||
choices = vm.getStartScreenChoices()
|
||||
choices = startScreenChoices
|
||||
)
|
||||
}
|
||||
item {
|
||||
SwitchPreference(
|
||||
preference = vm.confirmExit,
|
||||
preference = confirmExit,
|
||||
title = stringResource(MR.strings.confirm_exit)
|
||||
)
|
||||
}
|
||||
@@ -122,16 +150,16 @@ fun SettingsGeneralScreen(menuController: MenuController) {
|
||||
}
|
||||
item {
|
||||
ChoicePreference(
|
||||
preference = vm.language,
|
||||
preference = language,
|
||||
title = stringResource(MR.strings.language),
|
||||
choices = vm.getLanguageChoices(),
|
||||
choices = languageChoices,
|
||||
)
|
||||
}
|
||||
item {
|
||||
ChoicePreference(
|
||||
preference = vm.dateFormat,
|
||||
preference = dateFormat,
|
||||
title = stringResource(MR.strings.date_format),
|
||||
choices = vm.getDateChoices()
|
||||
choices = dateFormatChoices
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,19 +23,36 @@ import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.data.library.LibraryPreferences
|
||||
import ca.gosyer.data.server.interactions.CategoryInteractionHandler
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.ui.base.prefs.PreferenceRow
|
||||
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.ui.categories.openCategoriesMenu
|
||||
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.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
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(
|
||||
libraryPreferences: LibraryPreferences,
|
||||
private val categoryHandler: CategoryInteractionHandler
|
||||
@@ -57,25 +74,27 @@ class SettingsLibraryViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsLibraryScreen(menuController: MenuController) {
|
||||
val vm = viewModel<SettingsLibraryViewModel>()
|
||||
|
||||
fun SettingsLibraryScreenContent(
|
||||
showAllCategory: PreferenceMutableStateFlow<Boolean>,
|
||||
refreshCategoryCount: () -> Unit,
|
||||
categoriesSize: Int
|
||||
) {
|
||||
Column {
|
||||
Toolbar(stringResource(MR.strings.settings_library_screen), menuController, true)
|
||||
Toolbar(stringResource(MR.strings.settings_library_screen))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
item {
|
||||
SwitchPreference(
|
||||
preference = vm.showAllCategory,
|
||||
preference = showAllCategory,
|
||||
title = stringResource(MR.strings.show_all_category)
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceRow(
|
||||
stringResource(MR.strings.location_categories),
|
||||
onClick = { openCategoriesMenu(vm::refreshCategoryCount) },
|
||||
subtitle = vm.categories.collectAsState().value.toString()
|
||||
onClick = { openCategoriesMenu(refreshCategoryCount) },
|
||||
subtitle = categoriesSize.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
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
|
||||
fun SettingsParentalControlsScreen(menuController: MenuController) {
|
||||
fun SettingsParentalControlsScreenContent() {
|
||||
Column {
|
||||
Toolbar(stringResource(MR.strings.settings_parental_control_screen), menuController, true)
|
||||
Toolbar(stringResource(MR.strings.settings_parental_control_screen))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
|
||||
@@ -29,16 +29,18 @@ import ca.gosyer.data.reader.model.Direction
|
||||
import ca.gosyer.data.reader.model.ImageScale
|
||||
import ca.gosyer.data.reader.model.NavigationMode
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.ui.base.prefs.ChoicePreference
|
||||
import ca.gosyer.ui.base.prefs.ExpandablePreference
|
||||
import ca.gosyer.ui.base.prefs.SwitchPreference
|
||||
import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow
|
||||
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.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.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -46,6 +48,25 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
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(
|
||||
readerPreferences: ReaderPreferences
|
||||
) : ViewModel() {
|
||||
@@ -125,18 +146,25 @@ data class ReaderModePreference(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsReaderScreen(menuController: MenuController) {
|
||||
val vm = viewModel<SettingsReaderViewModel>()
|
||||
val modeSettings by vm.modeSettings.collectAsState()
|
||||
fun SettingsReaderScreenContent(
|
||||
modes: Map<String, String>,
|
||||
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 {
|
||||
Toolbar(stringResource(MR.strings.settings_reader), menuController, true)
|
||||
Toolbar(stringResource(MR.strings.settings_reader))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
item {
|
||||
ChoicePreference(
|
||||
vm.selectedMode,
|
||||
vm.modes.collectAsState().value.associateWith { it },
|
||||
selectedMode,
|
||||
modes,
|
||||
stringResource(MR.strings.reader_mode)
|
||||
)
|
||||
}
|
||||
@@ -148,7 +176,7 @@ fun SettingsReaderScreen(menuController: MenuController) {
|
||||
ExpandablePreference(it.mode) {
|
||||
ChoicePreference(
|
||||
it.direction,
|
||||
vm.getDirectionChoices(),
|
||||
directionChoices,
|
||||
stringResource(MR.strings.direction),
|
||||
enabled = !it.defaultMode
|
||||
)
|
||||
@@ -162,7 +190,7 @@ fun SettingsReaderScreen(menuController: MenuController) {
|
||||
if (continuous) {
|
||||
ChoicePreference(
|
||||
it.padding,
|
||||
vm.getPaddingChoices(),
|
||||
paddingChoices,
|
||||
stringResource(MR.strings.page_padding)
|
||||
)
|
||||
val direction by it.direction.collectAsState()
|
||||
@@ -190,20 +218,20 @@ fun SettingsReaderScreen(menuController: MenuController) {
|
||||
}
|
||||
ChoicePreference(
|
||||
it.maxSize,
|
||||
vm.getMaxSizeChoices(direction),
|
||||
getMaxSizeChoices(direction),
|
||||
maxSizeTitle,
|
||||
maxSizeSubtitle
|
||||
)
|
||||
} else {
|
||||
ChoicePreference(
|
||||
it.imageScale,
|
||||
vm.getImageScaleChoices(),
|
||||
imageScaleChoices,
|
||||
stringResource(MR.strings.image_scale)
|
||||
)
|
||||
}
|
||||
ChoicePreference(
|
||||
it.navigationMode,
|
||||
vm.getNavigationModeChoices(),
|
||||
navigationModeChoices,
|
||||
stringResource(MR.strings.navigation_mode)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,14 +29,28 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.ui.base.prefs.PreferenceRow
|
||||
import ca.gosyer.ui.main.Routes
|
||||
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
|
||||
fun SettingsScreen(menuController: MenuController) {
|
||||
fun SettingsScreenContent(navigator: Navigator) {
|
||||
Column {
|
||||
Toolbar(stringResource(MR.strings.location_settings), closable = false)
|
||||
Box {
|
||||
@@ -46,84 +60,84 @@ fun SettingsScreen(menuController: MenuController) {
|
||||
PreferenceRow(
|
||||
title = stringResource(MR.strings.settings_general),
|
||||
icon = Icons.Rounded.Tune,
|
||||
onClick = { menuController.push(Routes.SettingsGeneral) }
|
||||
onClick = { navigator push SettingsGeneralScreen() }
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceRow(
|
||||
title = stringResource(MR.strings.settings_appearance),
|
||||
icon = Icons.Rounded.Palette,
|
||||
onClick = { menuController.push(Routes.SettingsAppearance) }
|
||||
onClick = { navigator push SettingsAppearanceScreen() }
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceRow(
|
||||
title = stringResource(MR.strings.settings_server),
|
||||
icon = Icons.Rounded.Computer,
|
||||
onClick = { menuController.push(Routes.SettingsServer) }
|
||||
onClick = { navigator push SettingsServerScreen() }
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceRow(
|
||||
title = stringResource(MR.strings.settings_library),
|
||||
icon = Icons.Rounded.CollectionsBookmark,
|
||||
onClick = { menuController.push(Routes.SettingsLibrary) }
|
||||
onClick = { navigator push SettingsLibraryScreen() }
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceRow(
|
||||
title = stringResource(MR.strings.settings_reader),
|
||||
icon = Icons.Rounded.ChromeReaderMode,
|
||||
onClick = { menuController.push(Routes.SettingsReader) }
|
||||
onClick = { navigator push SettingsReaderScreen() }
|
||||
)
|
||||
}
|
||||
/*item {
|
||||
Pref(
|
||||
title = stringResource(MR.strings.settings_download),
|
||||
icon = Icons.Rounded.GetApp,
|
||||
onClick = { navController.push(Route.SettingsDownloads) }
|
||||
onClick = { navigator push SettingsDownloadsScreen() }
|
||||
)
|
||||
}
|
||||
item {
|
||||
Pref(
|
||||
title = stringResource(MR.strings.settings_tracking),
|
||||
icon = Icons.Rounded.Sync,
|
||||
onClick = { navController.push(Route.SettingsTracking) }
|
||||
onClick = { navigator push SettingsTrackingScreen() }
|
||||
)
|
||||
}*/
|
||||
item {
|
||||
PreferenceRow(
|
||||
title = stringResource(MR.strings.settings_browse),
|
||||
icon = Icons.Rounded.Explore,
|
||||
onClick = { menuController.push(Routes.SettingsBrowse) }
|
||||
onClick = { navigator push SettingsBrowseScreen() }
|
||||
)
|
||||
}
|
||||
item {
|
||||
PreferenceRow(
|
||||
title = stringResource(MR.strings.settings_backup),
|
||||
icon = Icons.Rounded.Backup,
|
||||
onClick = { menuController.push(Routes.SettingsBackup) }
|
||||
onClick = { navigator push SettingsBackupScreen() }
|
||||
)
|
||||
}
|
||||
/*item {
|
||||
Pref(
|
||||
title = stringResource(MR.strings.settings_security),
|
||||
icon = Icons.Rounded.Security,
|
||||
onClick = { navController.push(Route.SettingsSecurity) }
|
||||
onClick = { navigator push SettingsSecurityScreen() }
|
||||
)
|
||||
}
|
||||
item {
|
||||
Pref(
|
||||
title = stringResource(MR.strings.settings_parental_controls),
|
||||
icon = Icons.Rounded.PeopleOutline,
|
||||
onClick = { navController.push(Route.SettingsParentalControls) }
|
||||
onClick = { navigator push SettingsParentalControlsScreen() }
|
||||
)
|
||||
}*/
|
||||
item {
|
||||
PreferenceRow(
|
||||
title = stringResource(MR.strings.settings_advanced),
|
||||
icon = Icons.Rounded.Code,
|
||||
onClick = { menuController.push(Routes.SettingsAdvanced) }
|
||||
onClick = { navigator push SettingsAdvancedScreen() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
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
|
||||
fun SettingsSecurityScreen(menuController: MenuController) {
|
||||
fun SettingsSecurityScreenContent() {
|
||||
Column {
|
||||
Toolbar(stringResource(MR.strings.settings_security_screen), menuController, true)
|
||||
Toolbar(stringResource(MR.strings.settings_security_screen))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
|
||||
@@ -34,23 +34,68 @@ import ca.gosyer.data.server.ServerService
|
||||
import ca.gosyer.data.server.model.Auth
|
||||
import ca.gosyer.data.server.model.Proxy
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.ui.base.prefs.ChoicePreference
|
||||
import ca.gosyer.ui.base.prefs.EditTextPreference
|
||||
import ca.gosyer.ui.base.prefs.PreferenceRow
|
||||
import ca.gosyer.ui.base.prefs.SwitchPreference
|
||||
import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow
|
||||
import ca.gosyer.uicore.prefs.asStateIn
|
||||
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.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.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
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(
|
||||
serverPreferences: ServerPreferences,
|
||||
serverHostPreferences: ServerHostPreferences,
|
||||
@@ -136,26 +181,53 @@ class SettingsServerViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsServerScreen(menuController: MenuController) {
|
||||
val vm = viewModel<SettingsServerViewModel>()
|
||||
val host by vm.host.collectAsState()
|
||||
val basicAuthEnabled by vm.basicAuthEnabled.collectAsState()
|
||||
val proxy by vm.proxy.collectAsState()
|
||||
val auth by vm.auth.collectAsState()
|
||||
fun SettingsServerScreenContent(
|
||||
hostValue: Boolean,
|
||||
basicAuthEnabledValue: Boolean,
|
||||
proxyValue: Proxy,
|
||||
authValue: Auth,
|
||||
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) {
|
||||
onDispose {
|
||||
vm.restartServer()
|
||||
restartServer()
|
||||
}
|
||||
}
|
||||
Column {
|
||||
Toolbar(stringResource(MR.strings.settings_server_screen), menuController, true)
|
||||
Toolbar(stringResource(MR.strings.settings_server_screen))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
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 {
|
||||
PreferenceRow(
|
||||
stringResource(MR.strings.host_settings),
|
||||
@@ -164,105 +236,105 @@ fun SettingsServerScreen(menuController: MenuController) {
|
||||
)
|
||||
}
|
||||
item {
|
||||
val ip by vm.ip.collectAsState()
|
||||
val ipValue by ip.collectAsState()
|
||||
EditTextPreference(
|
||||
preference = vm.ip,
|
||||
preference = ip,
|
||||
title = stringResource(MR.strings.host_ip),
|
||||
subtitle = stringResource(MR.strings.host_ip_sub, ip),
|
||||
changeListener = vm::serverSettingChanged
|
||||
subtitle = stringResource(MR.strings.host_ip_sub, ipValue),
|
||||
changeListener = serverSettingChanged
|
||||
)
|
||||
}
|
||||
item {
|
||||
val port by vm.port.collectAsState()
|
||||
val portValue by port.collectAsState()
|
||||
EditTextPreference(
|
||||
preference = vm.port,
|
||||
preference = port,
|
||||
title = stringResource(MR.strings.host_port),
|
||||
subtitle = stringResource(MR.strings.host_port_sub, port),
|
||||
changeListener = vm::serverSettingChanged
|
||||
subtitle = stringResource(MR.strings.host_port_sub, portValue),
|
||||
changeListener = serverSettingChanged
|
||||
)
|
||||
}
|
||||
item {
|
||||
SwitchPreference(
|
||||
preference = vm.socksProxyEnabled,
|
||||
preference = socksProxyEnabled,
|
||||
title = stringResource(MR.strings.host_socks_enabled),
|
||||
changeListener = vm::serverSettingChanged
|
||||
changeListener = serverSettingChanged
|
||||
)
|
||||
}
|
||||
item {
|
||||
val proxyHost by vm.socksProxyHost.collectAsState()
|
||||
val proxyHost by socksProxyHost.collectAsState()
|
||||
EditTextPreference(
|
||||
preference = vm.socksProxyHost,
|
||||
preference = socksProxyHost,
|
||||
title = stringResource(MR.strings.host_socks_host),
|
||||
subtitle = stringResource(MR.strings.host_socks_host_sub, proxyHost),
|
||||
changeListener = vm::serverSettingChanged
|
||||
changeListener = serverSettingChanged
|
||||
)
|
||||
}
|
||||
item {
|
||||
val proxyPort by vm.socksProxyPort.collectAsState()
|
||||
val proxyPort by socksProxyPort.collectAsState()
|
||||
EditTextPreference(
|
||||
preference = vm.socksProxyPort,
|
||||
preference = socksProxyPort,
|
||||
title = stringResource(MR.strings.host_socks_port),
|
||||
subtitle = stringResource(MR.strings.host_socks_port_sub, proxyPort),
|
||||
changeListener = vm::serverSettingChanged
|
||||
changeListener = serverSettingChanged
|
||||
)
|
||||
}
|
||||
item {
|
||||
SwitchPreference(
|
||||
preference = vm.debugLogsEnabled,
|
||||
preference = debugLogsEnabled,
|
||||
title = stringResource(MR.strings.host_debug_logging),
|
||||
subtitle = stringResource(MR.strings.host_debug_logging_sub),
|
||||
changeListener = vm::serverSettingChanged
|
||||
changeListener = serverSettingChanged
|
||||
)
|
||||
}
|
||||
item {
|
||||
SwitchPreference(
|
||||
preference = vm.systemTrayEnabled,
|
||||
preference = systemTrayEnabled,
|
||||
title = stringResource(MR.strings.host_system_tray),
|
||||
subtitle = stringResource(MR.strings.host_system_tray_sub),
|
||||
changeListener = vm::serverSettingChanged
|
||||
changeListener = serverSettingChanged
|
||||
)
|
||||
}
|
||||
item {
|
||||
SwitchPreference(
|
||||
preference = vm.webUIEnabled,
|
||||
preference = webUIEnabled,
|
||||
title = stringResource(MR.strings.host_webui),
|
||||
subtitle = stringResource(MR.strings.host_webui_sub),
|
||||
changeListener = vm::serverSettingChanged
|
||||
changeListener = serverSettingChanged
|
||||
)
|
||||
}
|
||||
item {
|
||||
val webUIEnabled by vm.webUIEnabled.collectAsState()
|
||||
val webUIEnabledValue by webUIEnabled.collectAsState()
|
||||
SwitchPreference(
|
||||
preference = vm.openInBrowserEnabled,
|
||||
preference = openInBrowserEnabled,
|
||||
title = stringResource(MR.strings.host_open_in_browser),
|
||||
subtitle = stringResource(MR.strings.host_open_in_browser_sub),
|
||||
changeListener = vm::serverSettingChanged,
|
||||
enabled = webUIEnabled
|
||||
changeListener = serverSettingChanged,
|
||||
enabled = webUIEnabledValue
|
||||
)
|
||||
}
|
||||
item {
|
||||
SwitchPreference(
|
||||
preference = vm.basicAuthEnabled,
|
||||
preference = basicAuthEnabled,
|
||||
title = stringResource(MR.strings.basic_auth),
|
||||
subtitle = stringResource(MR.strings.host_basic_auth_sub),
|
||||
changeListener = vm::serverSettingChanged
|
||||
changeListener = serverSettingChanged
|
||||
)
|
||||
}
|
||||
item {
|
||||
EditTextPreference(
|
||||
preference = vm.basicAuthUsername,
|
||||
preference = basicAuthUsername,
|
||||
title = stringResource(MR.strings.host_basic_auth_username),
|
||||
changeListener = vm::serverSettingChanged,
|
||||
enabled = basicAuthEnabled
|
||||
changeListener = serverSettingChanged,
|
||||
enabled = basicAuthEnabledValue
|
||||
)
|
||||
}
|
||||
item {
|
||||
EditTextPreference(
|
||||
preference = vm.basicAuthPassword,
|
||||
preference = basicAuthPassword,
|
||||
title = stringResource(MR.strings.host_basic_auth_password),
|
||||
changeListener = vm::serverSettingChanged,
|
||||
changeListener = serverSettingChanged,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
enabled = basicAuthEnabled
|
||||
enabled = basicAuthEnabledValue
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -271,16 +343,16 @@ fun SettingsServerScreen(menuController: MenuController) {
|
||||
}
|
||||
item {
|
||||
EditTextPreference(
|
||||
vm.serverUrl,
|
||||
serverUrl,
|
||||
stringResource(MR.strings.server_url),
|
||||
subtitle = vm.serverUrl.collectAsState().value
|
||||
subtitle = serverUrl.collectAsState().value
|
||||
)
|
||||
}
|
||||
item {
|
||||
EditTextPreference(
|
||||
vm.serverPort,
|
||||
serverPort,
|
||||
stringResource(MR.strings.server_port),
|
||||
subtitle = vm.serverPort.collectAsState().value
|
||||
subtitle = serverPort.collectAsState().value
|
||||
)
|
||||
}
|
||||
|
||||
@@ -292,53 +364,53 @@ fun SettingsServerScreen(menuController: MenuController) {
|
||||
)
|
||||
}
|
||||
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.HTTP_PROXY -> {
|
||||
item {
|
||||
EditTextPreference(
|
||||
vm.httpHost,
|
||||
httpHost,
|
||||
stringResource(MR.strings.http_proxy),
|
||||
vm.httpHost.collectAsState().value
|
||||
httpHost.collectAsState().value
|
||||
)
|
||||
}
|
||||
item {
|
||||
EditTextPreference(
|
||||
vm.httpPort,
|
||||
httpPort,
|
||||
stringResource(MR.strings.http_port),
|
||||
vm.httpPort.collectAsState().value
|
||||
httpPort.collectAsState().value
|
||||
)
|
||||
}
|
||||
}
|
||||
Proxy.SOCKS_PROXY -> {
|
||||
item {
|
||||
EditTextPreference(
|
||||
vm.socksHost,
|
||||
socksHost,
|
||||
stringResource(MR.strings.socks_proxy),
|
||||
vm.socksHost.collectAsState().value
|
||||
socksHost.collectAsState().value
|
||||
)
|
||||
}
|
||||
item {
|
||||
EditTextPreference(
|
||||
vm.socksPort,
|
||||
socksPort,
|
||||
stringResource(MR.strings.socks_port),
|
||||
vm.socksPort.collectAsState().value
|
||||
socksPort.collectAsState().value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
EditTextPreference(vm.authUsername, stringResource(MR.strings.auth_username))
|
||||
EditTextPreference(authUsername, stringResource(MR.strings.auth_username))
|
||||
}
|
||||
item {
|
||||
EditTextPreference(
|
||||
vm.authPassword,
|
||||
authPassword,
|
||||
stringResource(MR.strings.auth_password),
|
||||
visualTransformation = PasswordVisualTransformation()
|
||||
)
|
||||
|
||||
@@ -20,14 +20,25 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.MenuController
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
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
|
||||
fun SettingsTrackingScreen(menuController: MenuController) {
|
||||
fun SettingsTrackingScreenContent() {
|
||||
Column {
|
||||
Toolbar(stringResource(MR.strings.settings_tracking_screen), menuController, true)
|
||||
Toolbar(stringResource(MR.strings.settings_tracking_screen))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.data.models.Manga
|
||||
@@ -12,11 +12,6 @@ import ca.gosyer.data.models.MangaPage
|
||||
import ca.gosyer.data.models.Source
|
||||
import ca.gosyer.data.server.interactions.SourceInteractionHandler
|
||||
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.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -24,7 +19,6 @@ import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class SourceScreenViewModel(
|
||||
private val source: Source,
|
||||
private val bundle: Bundle,
|
||||
private val sourceHandler: SourceInteractionHandler
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -33,49 +27,40 @@ class SourceScreenViewModel(
|
||||
params: Params
|
||||
) : this(
|
||||
params.source,
|
||||
params.bundle,
|
||||
sourceHandler
|
||||
)
|
||||
|
||||
private val _mangas = saveObjectInBundle(scope, bundle, MANGAS_KEY) { emptyList<Manga>() }
|
||||
private val _mangas = MutableStateFlow(emptyList<Manga>())
|
||||
val mangas = _mangas.asStateFlow()
|
||||
|
||||
private val _hasNextPage = saveBooleanInBundle(scope, bundle, NEXT_PAGE_KEY, false)
|
||||
private val _hasNextPage = MutableStateFlow(false)
|
||||
val hasNextPage = _hasNextPage.asStateFlow()
|
||||
|
||||
private val _loading = MutableStateFlow(true)
|
||||
val loading = _loading.asStateFlow()
|
||||
|
||||
private val _isLatest = saveBooleanInBundle(scope, bundle, IS_LATEST_KEY, false)
|
||||
private val _isLatest = MutableStateFlow(false)
|
||||
val isLatest = _isLatest.asStateFlow()
|
||||
|
||||
private val _filterButtonEnabled = saveBooleanInBundle(scope, bundle, SHOW_FILTERS, false)
|
||||
val filterButtonEnabled = _filterButtonEnabled.asStateFlow()
|
||||
|
||||
private val _latestButtonEnabled = saveBooleanInBundle(scope, bundle, SHOW_LATEST, false)
|
||||
private val _latestButtonEnabled = MutableStateFlow(false)
|
||||
val latestButtonEnabled = _latestButtonEnabled.asStateFlow()
|
||||
|
||||
private val _showingFilters = MutableStateFlow(false)
|
||||
val showingFilters = _showingFilters.asStateFlow()
|
||||
|
||||
private val _usingFilters = MutableStateFlow(false)
|
||||
|
||||
private val _sourceSearchQuery = MutableStateFlow<String?>(null)
|
||||
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()
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
try {
|
||||
if (bundle[MANGAS_KEY] == null) {
|
||||
val (mangas, hasNextPage) = getPage()
|
||||
_mangas.value = mangas
|
||||
_hasNextPage.value = hasNextPage
|
||||
}
|
||||
val (mangas, hasNextPage) = getPage()
|
||||
_mangas.value = mangas
|
||||
_hasNextPage.value = hasNextPage
|
||||
} catch (e: Exception) {
|
||||
e.throwIfCancellation()
|
||||
} 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) {
|
||||
if (isLatest.value != toLatest) {
|
||||
cleanBundle()
|
||||
_isLatest.value = toLatest
|
||||
// [loadNextPage] increments by 1
|
||||
_pageNum.value = 0
|
||||
@@ -135,7 +109,6 @@ class SourceScreenViewModel(
|
||||
}
|
||||
|
||||
fun startSearch(query: String?) {
|
||||
cleanBundle(false)
|
||||
_pageNum.value = 0
|
||||
_hasNextPage.value = true
|
||||
_loading.value = true
|
||||
@@ -144,15 +117,9 @@ class SourceScreenViewModel(
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
fun showingFilters(show: Boolean) {
|
||||
_showingFilters.value = show
|
||||
}
|
||||
fun setUsingFilters(usingFilters: Boolean) {
|
||||
_usingFilters.value = usingFilters
|
||||
}
|
||||
fun enableFilters(enabled: Boolean) {
|
||||
_filterButtonEnabled.value = enabled
|
||||
}
|
||||
fun enableLatest(enabled: Boolean) {
|
||||
_latestButtonEnabled.value = enabled
|
||||
}
|
||||
@@ -164,15 +131,5 @@ class SourceScreenViewModel(
|
||||
startSearch(sourceSearchQuery.value)
|
||||
}
|
||||
|
||||
data class Params(val source: Source, val bundle: Bundle)
|
||||
|
||||
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"
|
||||
}
|
||||
data class Params(val source: Source)
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.layout.Box
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.GridCells
|
||||
import androidx.compose.foundation.lazy.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollbarAdapter
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.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.Modifier
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
@@ -33,38 +32,40 @@ import ca.gosyer.data.models.Source
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.TextActionIcon
|
||||
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.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 io.kamel.image.lazyPainterResource
|
||||
|
||||
@Composable
|
||||
fun SourceScreen(
|
||||
bundle: Bundle,
|
||||
fun SourceScreenContent(
|
||||
source: Source,
|
||||
onMangaClick: (Long) -> 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) {
|
||||
instantiate<SourceScreenViewModel>(SourceScreenViewModel.Params(source, bundle))
|
||||
}
|
||||
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)
|
||||
LaunchedEffect(source) {
|
||||
enableLatest(source.supportsLatest)
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -72,44 +73,35 @@ fun SourceScreen(
|
||||
source = source,
|
||||
onCloseSourceTabClick = onCloseSourceTabClick,
|
||||
sourceSearchQuery = sourceSearchQuery,
|
||||
onSearch = vm::search,
|
||||
onSubmitSearch = vm::submitSearch,
|
||||
onSearch = search,
|
||||
onSubmitSearch = submitSearch,
|
||||
onSourceSettingsClick = onSourceSettingsClick,
|
||||
showFilterButton = showFilterButton,
|
||||
showLatestButton = showLatestButton,
|
||||
isLatest = isLatest,
|
||||
showingFilters = showingFilters,
|
||||
onClickMode = vm::setMode,
|
||||
onToggleFiltersClick = vm::showingFilters,
|
||||
onClickMode = setMode,
|
||||
onToggleFiltersClick = setShowingFilters,
|
||||
)
|
||||
Box {
|
||||
MangaTable(
|
||||
bundle = bundle,
|
||||
mangas = mangas,
|
||||
isLoading = loading,
|
||||
hasNextPage = hasNextPage,
|
||||
onLoadNextPage = vm::loadNextPage,
|
||||
onLoadNextPage = loadNextPage,
|
||||
onMangaClick = onMangaClick,
|
||||
)
|
||||
BundleScope("filters", autoDispose = false) {
|
||||
SourceFiltersMenu(
|
||||
bundle = bundle,
|
||||
modifier = Modifier.align(Alignment.TopEnd),
|
||||
sourceId = source.id,
|
||||
showFilters = showingFilters && !isLatest,
|
||||
onSearchClicked = {
|
||||
vm.setUsingFilters(true)
|
||||
vm.showingFilters(false)
|
||||
vm.submitSearch()
|
||||
},
|
||||
onResetClicked = {
|
||||
vm.setUsingFilters(false)
|
||||
vm.showingFilters(false)
|
||||
vm.submitSearch()
|
||||
},
|
||||
showFiltersButton = vm::enableFilters
|
||||
)
|
||||
}
|
||||
SourceFiltersMenu(
|
||||
modifier = Modifier.align(Alignment.TopEnd),
|
||||
showFilters = showingFilters && !isLatest,
|
||||
filters = filters,
|
||||
onSearchClicked = {
|
||||
setUsingFilters(true)
|
||||
setShowingFilters(false)
|
||||
submitSearch()
|
||||
},
|
||||
resetFiltersClicked = resetFiltersClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,7 +175,6 @@ fun SourceToolbar(
|
||||
|
||||
@Composable
|
||||
private fun MangaTable(
|
||||
bundle: Bundle,
|
||||
mangas: List<Manga>,
|
||||
isLoading: Boolean = false,
|
||||
hasNextPage: Boolean = false,
|
||||
@@ -193,9 +184,9 @@ private fun MangaTable(
|
||||
if (isLoading || mangas.isEmpty()) {
|
||||
LoadingScreen(isLoading)
|
||||
} else {
|
||||
val persistentState = persistentLazyListState(bundle)
|
||||
val lazyListState = rememberLazyListState()
|
||||
Box {
|
||||
LazyVerticalGrid(GridCells.Adaptive(160.dp), state = persistentState) {
|
||||
LazyVerticalGrid(GridCells.Adaptive(160.dp), state = lazyListState) {
|
||||
itemsIndexed(mangas) { index, manga ->
|
||||
if (hasNextPage && index == mangas.lastIndex) {
|
||||
LaunchedEffect(Unit) { onLoadNextPage() }
|
||||
@@ -210,7 +201,7 @@ private fun MangaTable(
|
||||
}
|
||||
}
|
||||
VerticalScrollbar(
|
||||
rememberScrollbarAdapter(persistentState),
|
||||
rememberScrollbarAdapter(lazyListState),
|
||||
Modifier.align(Alignment.CenterEnd)
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = 4.dp, vertical = 8.dp)
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.core.LinearEasing
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.Button
|
||||
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.rounded.ArrowUpward
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
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.i18n.MR
|
||||
import ca.gosyer.ui.base.prefs.ExpandablePreference
|
||||
import ca.gosyer.uicore.vm.viewModel
|
||||
import ca.gosyer.ui.sources.components.filter.model.SourceFiltersView
|
||||
import ca.gosyer.ui.sources.browse.filter.model.SourceFiltersView
|
||||
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 kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SourceFiltersMenu(
|
||||
bundle: Bundle,
|
||||
modifier: Modifier,
|
||||
sourceId: Long,
|
||||
showFilters: Boolean,
|
||||
filters: List<SourceFiltersView<*, *>>,
|
||||
onSearchClicked: () -> Unit,
|
||||
onResetClicked: () -> Unit,
|
||||
showFiltersButton: (Boolean) -> Unit
|
||||
resetFiltersClicked: () -> 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(
|
||||
showFilters,
|
||||
enter = fadeIn() + slideInHorizontally(initialOffsetX = { it * 2 }),
|
||||
@@ -116,7 +94,7 @@ fun SourceFiltersMenu(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
TextButton(vm::resetFilters) {
|
||||
TextButton(resetFiltersClicked) {
|
||||
Text(stringResource(MR.strings.reset_filters))
|
||||
}
|
||||
Button(onSearchClicked) {
|
||||
@@ -126,7 +104,7 @@ fun SourceFiltersMenu(
|
||||
}
|
||||
val expandedGroups = remember { mutableStateListOf<Int>() }
|
||||
Box {
|
||||
val lazyListState = persistentLazyListState()
|
||||
val lazyListState = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), lazyListState) {
|
||||
items(
|
||||
items = filters,
|
||||
@@ -4,15 +4,14 @@
|
||||
* 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.logging.CKLogger
|
||||
import ca.gosyer.data.models.sourcefilters.SourceFilter
|
||||
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.ui.sources.components.filter.model.SourceFiltersView
|
||||
import com.github.zsoltk.compose.savedinstancestate.Bundle
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -27,7 +26,6 @@ import me.tatarka.inject.annotations.Inject
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class SourceFiltersViewModel(
|
||||
private val bundle: Bundle,
|
||||
private val sourceId: Long,
|
||||
private val sourceHandler: SourceInteractionHandler
|
||||
) : ViewModel() {
|
||||
@@ -35,7 +33,6 @@ class SourceFiltersViewModel(
|
||||
sourceHandler: SourceInteractionHandler,
|
||||
params: Params,
|
||||
) : this(
|
||||
params.bundle,
|
||||
params.sourceId,
|
||||
sourceHandler
|
||||
)
|
||||
@@ -49,14 +46,21 @@ class SourceFiltersViewModel(
|
||||
private val _resetFilters = MutableSharedFlow<Unit>()
|
||||
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()
|
||||
|
||||
init {
|
||||
getFilters(initialLoad = !bundle.getBoolean(FILTERING, false))
|
||||
getFilters(initialLoad = true)
|
||||
|
||||
filters.onEach { settings ->
|
||||
subscriptions.forEach { it.cancel() }
|
||||
subscriptions.clear()
|
||||
_filterButtonEnabled.value = settings.isNotEmpty()
|
||||
subscriptions += settings.flatMap { filter ->
|
||||
if (filter is SourceFiltersView.Group) {
|
||||
filter.state.value.map { childFilter ->
|
||||
@@ -81,15 +85,18 @@ class SourceFiltersViewModel(
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun showingFilters(show: Boolean) {
|
||||
_showingFilters.value = show
|
||||
}
|
||||
fun enableFilters(enabled: Boolean) {
|
||||
_filterButtonEnabled.value = enabled
|
||||
}
|
||||
|
||||
private fun getFilters(initialLoad: Boolean = false) {
|
||||
scope.launch {
|
||||
try {
|
||||
_filters.value = sourceHandler.getFilterList(sourceId, reset = initialLoad).toView()
|
||||
if (!initialLoad) {
|
||||
bundle.putBoolean(FILTERING, true)
|
||||
} else {
|
||||
_resetFilters.emit(Unit)
|
||||
}
|
||||
_resetFilters.emit(Unit)
|
||||
} catch (e: Exception) {
|
||||
e.throwIfCancellation()
|
||||
} finally {
|
||||
@@ -100,12 +107,11 @@ class SourceFiltersViewModel(
|
||||
|
||||
fun resetFilters() {
|
||||
scope.launch {
|
||||
bundle.remove(FILTERING)
|
||||
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 ->
|
||||
SourceFiltersView(index, sourcePreference)
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.GroupFilter
|
||||
@@ -4,9 +4,8 @@
|
||||
* 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.TooltipPlacement
|
||||
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.rounded.Home
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
@@ -40,68 +38,43 @@ import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.data.models.Source
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.presentation.build.BuildKonfig
|
||||
import ca.gosyer.ui.manga.openMangaMenu
|
||||
import ca.gosyer.ui.sources.components.SourceHomeScreen
|
||||
import ca.gosyer.ui.sources.components.SourceScreen
|
||||
import ca.gosyer.ui.sources.settings.openSourceSettingsMenu
|
||||
import ca.gosyer.ui.sources.browse.SourceScreen
|
||||
import ca.gosyer.ui.sources.home.SourceHomeScreen
|
||||
import ca.gosyer.uicore.components.combinedMouseClickable
|
||||
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 io.kamel.image.lazyPainterResource
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun openSourcesMenu() {
|
||||
launchApplication {
|
||||
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
|
||||
Surface {
|
||||
CompositionLocalProvider(
|
||||
LocalSavedInstanceState provides Bundle()
|
||||
) {
|
||||
SourcesMenu(
|
||||
::openSourceSettingsMenu,
|
||||
::openMangaMenu
|
||||
)
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun SourcesMenu(
|
||||
sourceTabs: List<Source?>,
|
||||
selectedSourceTab: Source?,
|
||||
selectTab: (Source?) -> Unit,
|
||||
closeTab: (Source) -> Unit
|
||||
) {
|
||||
val homeScreen = remember { SourceHomeScreen() }
|
||||
SourcesNavigator(
|
||||
homeScreen,
|
||||
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
|
||||
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
|
||||
)
|
||||
CurrentSource()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.logging.CKLogger
|
||||
@@ -12,7 +12,6 @@ import ca.gosyer.data.catalog.CatalogPreferences
|
||||
import ca.gosyer.data.models.Source
|
||||
import ca.gosyer.data.server.interactions.SourceInteractionHandler
|
||||
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.launch
|
||||
@@ -20,8 +19,7 @@ import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class SourceHomeScreenViewModel @Inject constructor(
|
||||
private val sourceHandler: SourceInteractionHandler,
|
||||
catalogPreferences: CatalogPreferences,
|
||||
private val bundle: Bundle,
|
||||
catalogPreferences: CatalogPreferences
|
||||
) : ViewModel() {
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.VerticalScrollbar
|
||||
@@ -32,9 +32,6 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Translate
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
@@ -45,41 +42,31 @@ import ca.gosyer.data.models.Source
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.ui.base.navigation.TextActionIcon
|
||||
import ca.gosyer.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.uicore.vm.viewModel
|
||||
import ca.gosyer.ui.extensions.LanguageDialog
|
||||
import ca.gosyer.ui.extensions.components.LanguageDialog
|
||||
import ca.gosyer.uicore.components.LoadingScreen
|
||||
import ca.gosyer.uicore.image.KamelImage
|
||||
import com.github.zsoltk.compose.savedinstancestate.Bundle
|
||||
import ca.gosyer.uicore.resources.stringResource
|
||||
import io.kamel.image.lazyPainterResource
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Composable
|
||||
fun SourceHomeScreen(
|
||||
bundle: Bundle,
|
||||
fun SourceHomeScreenContent(
|
||||
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()) {
|
||||
LoadingScreen(isLoading)
|
||||
} else {
|
||||
Column {
|
||||
SourceHomeScreenToolbar(
|
||||
vm.languages,
|
||||
vm::getSourceLanguages,
|
||||
vm::setEnabledLanguages
|
||||
languages,
|
||||
getSourceLanguages,
|
||||
setEnabledLanguages
|
||||
)
|
||||
Box(Modifier.fillMaxSize(), Alignment.TopCenter) {
|
||||
val state = rememberLazyListState()
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,8 @@ package ca.gosyer.ui.sources.settings
|
||||
import ca.gosyer.core.lang.throwIfCancellation
|
||||
import ca.gosyer.data.models.sourcepreference.SourcePreference
|
||||
import ca.gosyer.data.server.interactions.SourceInteractionHandler
|
||||
import ca.gosyer.uicore.vm.ViewModel
|
||||
import ca.gosyer.ui.sources.settings.model.SourceSettingsView
|
||||
import ca.gosyer.uicore.vm.ViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class SourceSettingsViewModel @Inject constructor(
|
||||
class SourceSettingsScreenViewModel @Inject constructor(
|
||||
private val sourceHandler: SourceInteractionHandler,
|
||||
private val params: Params
|
||||
) : ViewModel() {
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.layout.Box
|
||||
@@ -33,43 +33,27 @@ import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.presentation.build.BuildKonfig
|
||||
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.prefs.ChoiceDialog
|
||||
import ca.gosyer.ui.base.prefs.MultiSelectDialog
|
||||
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.EditText
|
||||
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.Switch
|
||||
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 kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun openSourceSettingsMenu(sourceId: Long) {
|
||||
launchApplication {
|
||||
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
|
||||
SourceSettingsMenu(sourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
import kotlin.collections.List as KtList
|
||||
|
||||
@Composable
|
||||
fun SourceSettingsMenu(sourceId: Long, menuController: MenuController? = LocalMenuController.current) {
|
||||
val vm = viewModel {
|
||||
instantiate<SourceSettingsViewModel>(SourceSettingsViewModel.Params(sourceId))
|
||||
}
|
||||
val settings by vm.sourceSettings.collectAsState()
|
||||
|
||||
fun SourceSettingsScreenContent(
|
||||
settings: KtList<SourceSettingsView<*, *>>
|
||||
) {
|
||||
Column {
|
||||
Toolbar(stringResource(MR.strings.location_settings), menuController, menuController != null)
|
||||
Toolbar(stringResource(MR.strings.location_settings))
|
||||
Box {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(Modifier.fillMaxSize(), state) {
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import me.tatarka.inject.annotations.Inject
|
||||
|
||||
class UpdatesMenuViewModel @Inject constructor(
|
||||
class UpdatesScreenViewModel @Inject constructor(
|
||||
private val chapterHandler: ChapterInteractionHandler,
|
||||
private val updatesHandler: UpdatesInteractionHandler,
|
||||
private val downloadService: DownloadService
|
||||
@@ -113,7 +113,7 @@ class UpdatesMenuViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
override fun onDispose() {
|
||||
downloadService.removeWatches(mangaIds)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.clickable
|
||||
@@ -23,8 +23,6 @@ import androidx.compose.foundation.rememberScrollbarAdapter
|
||||
import androidx.compose.material.MaterialTheme
|
||||
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.Modifier
|
||||
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.MangaListItemTitle
|
||||
import ca.gosyer.uicore.components.mangaAspectRatio
|
||||
import ca.gosyer.uicore.vm.viewModel
|
||||
import ca.gosyer.uicore.resources.stringResource
|
||||
import io.kamel.image.lazyPainterResource
|
||||
|
||||
@Composable
|
||||
fun UpdatesMenu(
|
||||
fun UpdatesScreenContent(
|
||||
isLoading: Boolean,
|
||||
updates: List<ChapterDownloadItem>,
|
||||
loadNextPage: () -> 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 {
|
||||
Toolbar(stringResource(MR.strings.location_updates), closable = false)
|
||||
if (isLoading || updates.isEmpty()) {
|
||||
@@ -67,7 +67,7 @@ fun UpdatesMenu(
|
||||
itemsIndexed(updates) { index, item ->
|
||||
LaunchedEffect(Unit) {
|
||||
if (index == updates.lastIndex) {
|
||||
vm.loadNextPage()
|
||||
loadNextPage()
|
||||
}
|
||||
}
|
||||
val manga = item.manga!!
|
||||
@@ -76,9 +76,9 @@ fun UpdatesMenu(
|
||||
item,
|
||||
onClickItem = { openChapter(chapter.index, chapter.mangaId) },
|
||||
onClickCover = { openManga(manga.id) },
|
||||
onClickDownload = vm::downloadChapter,
|
||||
onClickDeleteDownload = vm::deleteDownloadedChapter,
|
||||
onClickStopDownload = vm::stopDownloadingChapter
|
||||
onClickDownload = downloadChapter,
|
||||
onClickDeleteDownload = deleteDownloadedChapter,
|
||||
onClickStopDownload = stopDownloadingChapter
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.collectAsState
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.toComposeImageBitmap
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.Size
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.ui.graphics.painter.Painter
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.dp
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.ui.window.ApplicationScope
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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 kotlinx.coroutines.DelicateCoroutinesApi
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 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.flow.Flow
|
||||
|
||||
@@ -31,6 +31,7 @@ kotlin {
|
||||
api(kotlin("stdlib-common"))
|
||||
api(libs.coroutinesCore)
|
||||
api(libs.kamel)
|
||||
api(libs.voyagerCore)
|
||||
api(project(":core"))
|
||||
api(project(":i18n"))
|
||||
api(compose.desktop.currentOs)
|
||||
|
||||
@@ -8,36 +8,20 @@ package ca.gosyer.uicore.vm
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisallowComposableCalls
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
|
||||
@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 viewModel = remember(key) {
|
||||
viewModelFactory.instantiate<VM>()
|
||||
}
|
||||
DisposableEffect(viewModel) {
|
||||
onDispose {
|
||||
viewModel.destroy()
|
||||
}
|
||||
}
|
||||
return viewModel
|
||||
return rememberScreenModel(tag) { viewModelFactory.instantiate() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
inline fun <reified VM : ViewModel> viewModel(
|
||||
key: Any? = Unit,
|
||||
inline fun <reified VM : ViewModel> Screen.viewModel(
|
||||
tag: String? = null,
|
||||
crossinline factory: @DisallowComposableCalls ViewModelFactory.() -> VM
|
||||
): VM {
|
||||
val viewModelFactory = LocalViewModelFactory.current
|
||||
val viewModel = remember(key) {
|
||||
viewModelFactory.factory()
|
||||
}
|
||||
DisposableEffect(viewModel) {
|
||||
onDispose {
|
||||
viewModel.destroy()
|
||||
}
|
||||
}
|
||||
return viewModel
|
||||
return rememberScreenModel(tag) { viewModelFactory.factory() }
|
||||
}
|
||||
|
||||
@@ -8,23 +8,17 @@ package ca.gosyer.uicore.vm
|
||||
|
||||
import ca.gosyer.core.prefs.Preference
|
||||
import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class ViewModel {
|
||||
abstract class ViewModel : ScreenModel {
|
||||
|
||||
protected val scope = MainScope()
|
||||
|
||||
fun destroy() {
|
||||
scope.cancel()
|
||||
onDestroy()
|
||||
}
|
||||
|
||||
open fun onDestroy() {}
|
||||
protected open val scope
|
||||
get() = coroutineScope
|
||||
|
||||
fun <T> Preference<T>.asStateFlow() = PreferenceMutableStateFlow(this, scope)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user