Rewrite navigation to use Multiplatform Voyager Navigation

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

View File

@@ -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)

View File

@@ -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,14 +163,11 @@ suspend fun main() {
}
) {
AppTheme {
CompositionLocalProvider(
LocalBackPressHandler provides backPressHandler,
) {
Crossfade(serverService.initialized.collectAsState().value) { initialized ->
when (initialized) {
ServerResult.STARTED, ServerResult.UNUSED -> {
Box {
MainMenu(rootBundle)
MainMenu()
val displayDebugInfo by displayDebugInfoFlow.collectAsState()
if (displayDebugInfo) {
DebugOverlay()
@@ -200,5 +190,4 @@ suspend fun main() {
}
}
}
}
}

View File

@@ -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" }

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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
)
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,56 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.categories
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.categories.components.CategoriesScreenContent
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.Navigator
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openCategoriesMenu(notifyFinished: (() -> Unit)? = null) {
launchApplication {
ThemedWindow(
::exitApplication,
title = "${BuildKonfig.NAME} - Categories"
) {
Navigator(remember { CategoriesScreen(notifyFinished) })
}
}
}
class CategoriesScreen(
@Transient
private val notifyFinished: (() -> Unit)? = null
) : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<CategoriesScreenViewModel>()
CategoriesScreenContent(
categories = vm.categories.collectAsState().value,
updateRemoteCategories = vm::updateRemoteCategories,
moveCategoryUp = vm::moveUp,
moveCategoryDown = vm::moveDown,
renameCategory = vm::renameCategory,
deleteCategory = vm::deleteCategory,
createCategory = vm::createCategory,
notifyFinished = notifyFinished
)
}
}

View File

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

View File

@@ -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",

View File

@@ -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 = {},

View File

@@ -0,0 +1,58 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.downloads
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.downloads.components.DownloadsScreenContent
import ca.gosyer.ui.manga.MangaScreen
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openDownloadsMenu() {
launchApplication {
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
Surface {
Navigator(remember { DownloadsScreen() })
}
}
}
}
class DownloadsScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel {
instantiate<DownloadsScreenViewModel>(false)
}
val navigator = LocalNavigator.currentOrThrow
DownloadsScreenContent(
downloadQueue = vm.downloadQueue.collectAsState().value,
downloadStatus = vm.downloaderStatus.collectAsState().value,
startDownloading = vm::start,
pauseDownloading = vm::pause,
clearQueue = vm::clear,
onMangaClick = { navigator push MangaScreen(it) },
stopDownload = vm::stopDownload,
moveDownloadToBottom = vm::moveToBottom
)
}
}

View File

@@ -11,14 +11,24 @@ import ca.gosyer.data.models.Chapter
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import ca.gosyer.data.server.interactions.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

View File

@@ -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
)
}
}

View File

@@ -0,0 +1,60 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.extensions
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.rememberWindowState
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.extensions.components.ExtensionsScreenContent
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.Navigator
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openExtensionsMenu() {
launchApplication {
val state = rememberWindowState(size = DpSize(550.dp, 700.dp))
ThemedWindow(::exitApplication, state, title = BuildKonfig.NAME) {
Surface {
Navigator(remember { ExtensionsScreen() })
}
}
}
}
class ExtensionsScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<ExtensionsScreenViewModel>()
ExtensionsScreenContent(
extensions = vm.extensions.collectAsState().value,
isLoading = vm.isLoading.collectAsState().value,
query = vm.searchQuery.collectAsState().value,
setQuery = vm::search,
enabledLangs = vm.enabledLangs,
getSourceLanguages = vm::getSourceLanguages,
setEnabledLanguages = vm::setEnabledLanguages,
installExtension = vm::install,
updateExtension = vm::update,
uninstallExtension = vm::uninstall
)
}
}

View File

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

View File

@@ -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))
}

View File

@@ -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()
}
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,
@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 = onClickManga,
onClickManga = { navigator push MangaScreen(it) },
onRemoveMangaClicked = vm::removeManga
)
}
// }
}
}
@Composable
private fun LibraryTabs(
visible: Boolean,
categories: List<Category>,
selectedPage: Int,
onPageChanged: (Int) -> Unit
) {
if (categories.isEmpty()) return
AnimatedVisibility(
visible = visible,
enter = expandVertically(),
exit = shrinkVertically()
) {
ScrollableTabRow(
selectedTabIndex = selectedPage,
backgroundColor = MaterialTheme.colors.surface,
// contentColor = CustomColors.current.onBars,
edgePadding = 0.dp
) {
categories.fastForEachIndexed { i, category ->
Tab(
selected = selectedPage == i,
onClick = { onPageChanged(i) },
text = { Text(category.name) }
)
}
}
}
}
@Composable
private fun LibraryPager(
categories: List<Category>,
displayMode: DisplayMode,
selectedPage: Int,
getLibraryForPage: @Composable (Long) -> State<List<Manga>>,
onPageChanged: (Int) -> Unit,
onClickManga: (Long) -> Unit,
onRemoveMangaClicked: (Long) -> Unit
) {
if (categories.isEmpty()) return
val state = rememberPagerState(categories.size, selectedPage)
LaunchedEffect(state.currentPage) {
if (state.currentPage != selectedPage) {
onPageChanged(state.currentPage)
}
}
LaunchedEffect(selectedPage) {
if (state.currentPage != selectedPage) {
state.animateScrollToPage(selectedPage)
}
}
HorizontalPager(state = state) {
val library by getLibraryForPage(categories[it].id)
when (displayMode) {
DisplayMode.CompactGrid -> LibraryMangaCompactGrid(
library = library,
onClickManga = onClickManga,
onRemoveMangaClicked = onRemoveMangaClicked
)
/*DisplayMode.ComfortableGrid -> LibraryMangaComfortableGrid(
library = library,
onClickManga = onClickManga
)
DisplayMode.List -> LibraryMangaList(
library = library,
onClickManga = onClickManga
)*/
else -> Box {}
}
}
}

View File

@@ -15,9 +15,6 @@ import ca.gosyer.data.server.interactions.CategoryInteractionHandler
import ca.gosyer.data.server.interactions.LibraryInteractionHandler
import ca.gosyer.data.server.interactions.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 {

View File

@@ -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
)

View File

@@ -0,0 +1,62 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.library.components
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import ca.gosyer.data.library.model.DisplayMode
import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
@Composable
fun LibraryPager(
categories: List<Category>,
displayMode: DisplayMode,
selectedPage: Int,
getLibraryForPage: @Composable (Long) -> State<List<Manga>>,
onPageChanged: (Int) -> Unit,
onClickManga: (Long) -> Unit,
onRemoveMangaClicked: (Long) -> Unit
) {
if (categories.isEmpty()) return
val state = rememberPagerState(categories.size, selectedPage)
LaunchedEffect(state.currentPage) {
if (state.currentPage != selectedPage) {
onPageChanged(state.currentPage)
}
}
LaunchedEffect(selectedPage) {
if (state.currentPage != selectedPage) {
state.animateScrollToPage(selectedPage)
}
}
HorizontalPager(state = state) {
val library by getLibraryForPage(categories[it].id)
when (displayMode) {
DisplayMode.CompactGrid -> LibraryMangaCompactGrid(
library = library,
onClickManga = onClickManga,
onRemoveMangaClicked = onRemoveMangaClicked
)
/*DisplayMode.ComfortableGrid -> LibraryMangaComfortableGrid(
library = library,
onClickManga = onClickManga
)
DisplayMode.List -> LibraryMangaList(
library = library,
onClickManga = onClickManga
)*/
else -> Box {}
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.library.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import ca.gosyer.data.library.model.DisplayMode
import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga
import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.resources.stringResource
@Composable
fun LibraryScreenContent(
categories: List<Category>,
selectedCategoryIndex: Int,
displayMode: DisplayMode,
isLoading: Boolean,
error: String?,
query: String,
updateQuery: (String) -> Unit,
getLibraryForPage: @Composable (Long) -> State<List<Manga>>,
onPageChanged: (Int) -> Unit,
onClickManga: (Long) -> Unit,
onRemoveMangaClicked: (Long) -> Unit
) {
// val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
if (categories.isEmpty()) {
LoadingScreen(isLoading, errorMessage = error)
} else {
/*ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = { *//*LibrarySheet()*//* }
) {*/
Column(Modifier.fillMaxWidth()) {
/*Toolbar(
title = {
val text = if (vm.showCategoryTabs) {
stringResource(R.string.library_label)
} else {
vm.selectedCategory?.visibleName.orEmpty()
}
Text(text)
},
actions = {
IconButton(onClick = { scope.launch { sheetState.show() }}) {
Icon(Icons.Rounded.FilterList, contentDescription = null)
}
}
)*/
Toolbar(
stringResource(MR.strings.location_library),
closable = false,
searchText = query,
search = updateQuery
)
LibraryTabs(
visible = true, // vm.showCategoryTabs,
categories = categories,
selectedPage = selectedCategoryIndex,
onPageChanged = onPageChanged
)
LibraryPager(
categories = categories,
displayMode = displayMode,
selectedPage = selectedCategoryIndex,
getLibraryForPage = getLibraryForPage,
onPageChanged = onPageChanged,
onClickManga = onClickManga,
onRemoveMangaClicked = onRemoveMangaClicked
)
}
// }
}
}

View File

@@ -0,0 +1,50 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.library.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ScrollableTabRow
import androidx.compose.material.Tab
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import ca.gosyer.data.models.Category
@Composable
fun LibraryTabs(
visible: Boolean,
categories: List<Category>,
selectedPage: Int,
onPageChanged: (Int) -> Unit
) {
if (categories.isEmpty()) return
AnimatedVisibility(
visible = visible,
enter = expandVertically(),
exit = shrinkVertically()
) {
ScrollableTabRow(
selectedTabIndex = selectedPage,
backgroundColor = MaterialTheme.colors.surface,
// contentColor = CustomColors.current.onBars,
edgePadding = 0.dp
) {
categories.fastForEachIndexed { i, category ->
Tab(
selected = selectedPage == i,
onClick = { onPageChanged(i) },
text = { Text(category.name) }
)
}
}
}
}

View File

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

View File

@@ -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()
}
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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())

View File

@@ -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) {

View File

@@ -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 }
}
}
}

View File

@@ -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() }
)
) {

View File

@@ -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,

View File

@@ -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()
}

View File

@@ -0,0 +1,66 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.manga
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.manga.components.MangaScreenContent
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.Navigator
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openMangaMenu(mangaId: Long) {
launchApplication {
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
Surface {
Navigator(remember { MangaScreen(mangaId) })
}
}
}
}
class MangaScreen(private val mangaId: Long) : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel {
instantiate<MangaScreenViewModel>(MangaScreenViewModel.Params(mangaId))
}
MangaScreenContent(
isLoading = vm.isLoading.collectAsState().value,
manga = vm.manga.collectAsState().value,
chapters = vm.chapters.collectAsState().value,
dateTimeFormatter = vm.dateTimeFormatter.collectAsState().value,
categoriesExist = vm.categoriesExist.collectAsState().value,
chooseCategoriesFlow = vm.chooseCategoriesFlow,
addFavorite = vm::addFavorite,
setCategories = vm::setCategories,
toggleFavorite = vm::toggleFavorite,
refreshManga = vm::refreshManga,
toggleRead = vm::toggleRead,
toggleBookmarked = vm::toggleBookmarked,
markPreviousRead = vm::markPreviousRead,
downloadChapter = vm::downloadChapter,
deleteDownload = vm::deleteDownload,
stopDownloadingChapter = vm::stopDownloadingChapter,
loadChapters = vm::loadChapters,
loadManga = vm::loadManga
)
}
}

View File

@@ -33,7 +33,7 @@ import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.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)
}

View File

@@ -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

View File

@@ -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)) {

View File

@@ -0,0 +1,150 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.manga.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Favorite
import androidx.compose.material.icons.rounded.FavoriteBorder
import androidx.compose.material.icons.rounded.Label
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga
import ca.gosyer.i18n.MR
import ca.gosyer.ui.base.chapter.ChapterDownloadItem
import ca.gosyer.ui.base.navigation.TextActionIcon
import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.reader.openReaderMenu
import ca.gosyer.uicore.components.ErrorScreen
import ca.gosyer.uicore.components.LoadingScreen
import ca.gosyer.uicore.resources.stringResource
import kotlinx.coroutines.flow.SharedFlow
import java.time.format.DateTimeFormatter
@Composable
fun MangaScreenContent(
isLoading: Boolean,
manga: Manga?,
chapters: List<ChapterDownloadItem>,
dateTimeFormatter: DateTimeFormatter,
categoriesExist: Boolean,
chooseCategoriesFlow: SharedFlow<Pair<List<Category>, List<Category>>>,
addFavorite: (List<Category>, List<Category>) -> Unit,
setCategories: () -> Unit,
toggleFavorite: () -> Unit,
refreshManga: () -> Unit,
toggleRead: (Int) -> Unit,
toggleBookmarked: (Int) -> Unit,
markPreviousRead: (Int) -> Unit,
downloadChapter: (Int) -> Unit,
deleteDownload: (Int) -> Unit,
stopDownloadingChapter: (Int) -> Unit,
loadChapters: () -> Unit,
loadManga: () -> Unit
) {
LaunchedEffect(Unit) {
chooseCategoriesFlow.collect { (availableCategories, usedCategories) ->
openCategorySelectDialog(availableCategories, usedCategories, addFavorite)
}
}
Box {
Column {
Toolbar(
stringResource(MR.strings.location_manga),
actions = {
AnimatedVisibility(categoriesExist && manga?.inLibrary == true) {
TextActionIcon(
setCategories,
stringResource(MR.strings.edit_categories),
Icons.Rounded.Label
)
}
TextActionIcon(
toggleFavorite,
stringResource(if (manga?.inLibrary == true) MR.strings.action_remove_favorite else MR.strings.action_favorite),
if (manga?.inLibrary == true) {
Icons.Rounded.Favorite
} else {
Icons.Rounded.FavoriteBorder
},
manga != null
)
TextActionIcon(
refreshManga,
stringResource(MR.strings.action_refresh_manga),
Icons.Rounded.Refresh,
!isLoading
)
}
)
manga.let { manga ->
if (manga != null) {
Box {
val state = rememberLazyListState()
LazyColumn(state = state) {
item {
MangaItem(manga)
}
if (chapters.isNotEmpty()) {
items(chapters) { chapter ->
ChapterItem(
chapter,
dateTimeFormatter::format,
onClick = { openReaderMenu(it, manga.id) },
toggleRead = toggleRead,
toggleBookmarked = toggleBookmarked,
markPreviousAsRead = markPreviousRead,
onClickDownload = downloadChapter,
onClickDeleteChapter = deleteDownload,
onClickStopDownload = stopDownloadingChapter
)
}
} else if (!isLoading) {
item {
ErrorScreen(
stringResource(MR.strings.no_chapters_found),
Modifier.height(400.dp).fillMaxWidth(),
retry = loadChapters
)
}
}
}
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd)
.fillMaxHeight()
.padding(horizontal = 4.dp, vertical = 8.dp),
adapter = rememberScrollbarAdapter(state)
)
}
} else if (!isLoading) {
ErrorScreen(stringResource(MR.strings.failed_manga_fetch), retry = loadManga)
}
}
}
if (isLoading) {
LoadingScreen()
}
}
}

View File

@@ -71,13 +71,13 @@ import ca.gosyer.ui.reader.navigation.RightAndLeftNavigation
import ca.gosyer.ui.reader.navigation.navigationClickable
import ca.gosyer.ui.reader.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()

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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(

View File

@@ -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)
)

View File

@@ -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
)
}
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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
)
}
}

View File

@@ -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()
)
}
}

View File

@@ -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) {

View File

@@ -29,16 +29,18 @@ import ca.gosyer.data.reader.model.Direction
import ca.gosyer.data.reader.model.ImageScale
import ca.gosyer.data.reader.model.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)
)
}

View File

@@ -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() }
)
}
}

View File

@@ -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) {

View File

@@ -34,23 +34,68 @@ import ca.gosyer.data.server.ServerService
import ca.gosyer.data.server.model.Auth
import ca.gosyer.data.server.model.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()
)

View File

@@ -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) {

View File

@@ -1,85 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.sources
import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.models.Source
import ca.gosyer.uicore.vm.ViewModel
import com.github.zsoltk.compose.savedinstancestate.Bundle
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import me.tatarka.inject.annotations.Inject
class SourcesMenuViewModel @Inject constructor(
private val bundle: Bundle
) : ViewModel() {
private val _sourceTabs = MutableStateFlow<List<Source?>>(listOf(null))
val sourceTabs = _sourceTabs.asStateFlow()
private val _selectedSourceTab = MutableStateFlow<Source?>(null)
val selectedSourceTab = _selectedSourceTab.asStateFlow()
init {
_sourceTabs.drop(1)
.onEach { sources ->
bundle.putLongArray(SOURCE_TABS_KEY, sources.mapNotNull { it?.id }.toLongArray())
}
.launchIn(scope)
_selectedSourceTab.drop(1)
.onEach {
if (it != null) {
bundle.putLong(SELECTED_SOURCE_TAB, it.id)
} else {
bundle.remove(SELECTED_SOURCE_TAB)
}
}
.launchIn(scope)
}
fun selectTab(source: Source?) {
_selectedSourceTab.value = source
}
fun addTab(source: Source) {
if (source !in _sourceTabs.value) {
_sourceTabs.value += source
}
selectTab(source)
}
fun closeTab(source: Source) {
_sourceTabs.value -= source
if (selectedSourceTab.value?.id == source.id) {
_selectedSourceTab.value = null
}
bundle.remove(source.id.toString())
}
fun setLoadedSources(sources: List<Source>) {
val sourceTabs = bundle.getLongArray(SOURCE_TABS_KEY)
if (sourceTabs != null) {
_sourceTabs.value = listOf(null) + sourceTabs.toList()
.mapNotNull { sourceId ->
sources.find { it.id == sourceId }
}
_selectedSourceTab.value = bundle.getLong(SELECTED_SOURCE_TAB, -1).let { id ->
if (id != -1L) {
sources.find { it.id == id }
} else null
}
}
}
private companion object : CKLogger({}) {
const val SOURCE_TABS_KEY = "source_tabs"
const val SELECTED_SOURCE_TAB = "selected_tab"
}
}

View File

@@ -0,0 +1,49 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.sources
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.sources.components.SourcesMenu
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.Navigator
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openSourcesMenu() {
launchApplication {
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
Surface {
Navigator(remember { SourcesScreen() })
}
}
}
}
class SourcesScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<SourcesScreenViewModel>()
SourcesMenu(
sourceTabs = vm.sourceTabs.collectAsState().value,
selectedSourceTab = vm.selectedSourceTab.collectAsState().value,
selectTab = vm::selectTab,
closeTab = vm::closeTab
)
}
}

View File

@@ -0,0 +1,35 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.sources
import ca.gosyer.data.models.Source
import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import me.tatarka.inject.annotations.Inject
class SourcesScreenViewModel @Inject constructor() : ViewModel() {
private val _sourceTabs = MutableStateFlow<List<Source?>>(listOf(null))
val sourceTabs = _sourceTabs.asStateFlow()
private val _selectedSourceTab = MutableStateFlow<Source?>(null)
val selectedSourceTab = _selectedSourceTab.asStateFlow()
fun selectTab(source: Source?) {
if (source !in _sourceTabs.value) {
_sourceTabs.value += source
}
_selectedSourceTab.value = source
}
fun closeTab(source: Source) {
_sourceTabs.value -= source
if (selectedSourceTab.value?.id == source.id) {
_selectedSourceTab.value = null
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.sources.browse
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import ca.gosyer.data.models.Source
import ca.gosyer.ui.manga.MangaScreen
import ca.gosyer.ui.sources.browse.components.SourceScreenContent
import ca.gosyer.ui.sources.browse.filter.SourceFiltersViewModel
import ca.gosyer.ui.sources.components.LocalSourcesNavigator
import ca.gosyer.ui.sources.settings.SourceSettingsScreen
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
class SourceScreen(val source: Source) : Screen {
override val key: ScreenKey = source.id.toString()
@Composable
override fun Content() {
val sourceVM = viewModel {
instantiate<SourceScreenViewModel>(SourceScreenViewModel.Params(source))
}
val filterVM = viewModel {
instantiate<SourceFiltersViewModel>(SourceFiltersViewModel.Params(source.id))
}
val navigator = LocalNavigator.currentOrThrow
val sourcesNavigator = LocalSourcesNavigator.current
SourceScreenContent(
source = source,
onMangaClick = { navigator push MangaScreen(it) },
onCloseSourceTabClick = sourcesNavigator::remove,
onSourceSettingsClick = { navigator push SourceSettingsScreen(it) },
mangas = sourceVM.mangas.collectAsState().value,
hasNextPage = sourceVM.hasNextPage.collectAsState().value,
loading = sourceVM.loading.collectAsState().value,
isLatest = sourceVM.isLatest.collectAsState().value,
showLatestButton = source.supportsLatest,
sourceSearchQuery = sourceVM.sourceSearchQuery.collectAsState().value,
enableLatest = sourceVM::enableLatest,
search = sourceVM::search,
submitSearch = sourceVM::submitSearch,
setMode = sourceVM::setMode,
loadNextPage = sourceVM::loadNextPage,
setUsingFilters = sourceVM::setUsingFilters,
// FilterVM
filters = filterVM.filters.collectAsState().value,
showingFilters = filterVM.showingFilters.collectAsState().value,
showFilterButton = filterVM.filterButtonEnabled.collectAsState().value,
setShowingFilters = filterVM::showingFilters,
resetFiltersClicked = {
sourceVM.setUsingFilters(false)
filterVM.resetFilters()
}
)
}
}

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
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
}
} 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)
}

View File

@@ -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,46 +73,37 @@ 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,
filters = filters,
onSearchClicked = {
vm.setUsingFilters(true)
vm.showingFilters(false)
vm.submitSearch()
setUsingFilters(true)
setShowingFilters(false)
submitSearch()
},
onResetClicked = {
vm.setUsingFilters(false)
vm.showingFilters(false)
vm.submitSearch()
},
showFiltersButton = vm::enableFilters
resetFiltersClicked = resetFiltersClicked
)
}
}
}
}
@Composable
@@ -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)

View File

@@ -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,

View File

@@ -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)
}
} 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)

View File

@@ -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

View File

@@ -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(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)
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)
}
val sourceTabs by vm.sourceTabs.collectAsState()
val selectedSourceTab by vm.selectedSourceTab.collectAsState()
Row {
SourcesSideMenu(
sourceTabs = sourceTabs,
onSourceTabClick = vm::selectTab,
onCloseSourceTabClick = vm::closeTab
onSourceTabClick = selectTab,
onCloseSourceTabClick = {
closeTab(it)
navigator.stateHolder.removeState(it.id)
}
)
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
)
}
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.sources.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.SaveableStateHolder
import androidx.compose.runtime.staticCompositionLocalOf
import ca.gosyer.data.models.Source
import ca.gosyer.ui.sources.browse.SourceScreen
import ca.gosyer.ui.sources.home.SourceHomeScreen
import cafe.adriel.voyager.navigator.Navigator
typealias SourcesNavigatorContent = @Composable (sourcesNavigator: SourcesNavigator) -> Unit
val LocalSourcesNavigator: ProvidableCompositionLocal<SourcesNavigator> =
staticCompositionLocalOf { error("SourcesNavigator not initialized") }
@Composable
fun SourcesNavigator(
homeScreen: SourceHomeScreen,
removeSource: (Source) -> Unit,
selectSource: (Source) -> Unit,
content: SourcesNavigatorContent = { CurrentSource() }
) {
Navigator(homeScreen, autoDispose = false, onBackPressed = null) { navigator ->
val sourcesNavigator = remember(navigator) {
SourcesNavigator(navigator, homeScreen, removeSource, selectSource)
}
CompositionLocalProvider(LocalSourcesNavigator provides sourcesNavigator) {
content(sourcesNavigator)
}
}
}
class SourcesNavigator internal constructor(
private val navigator: Navigator,
private val homeScreen: SourceHomeScreen,
private val removeSource: (Source) -> Unit,
private val selectSource: (Source) -> Unit,
val stateHolder: SaveableStateHolder = navigator.stateHolder
) {
fun remove(source: Source) {
removeSource(source)
navigator replaceAll homeScreen
stateHolder.removeState(source.id)
}
fun select(source: Source) {
selectSource(source)
navigator replaceAll SourceScreen(source)
}
var current
get() = navigator.lastItem
set(value) = navigator replaceAll value
}
@Composable
fun CurrentSource() {
val sourcesNavigator = LocalSourcesNavigator.current
val currentSource = sourcesNavigator.current
sourcesNavigator.stateHolder.SaveableStateProvider(currentSource.key) {
currentSource.Content()
}
}

View File

@@ -0,0 +1,35 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.sources.home
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import ca.gosyer.ui.sources.components.LocalSourcesNavigator
import ca.gosyer.ui.sources.home.components.SourceHomeScreenContent
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
class SourceHomeScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<SourceHomeScreenViewModel>()
val sourcesNavigator = LocalSourcesNavigator.current
SourceHomeScreenContent(
onAddSource = sourcesNavigator::select,
isLoading = vm.isLoading.collectAsState().value,
sources = vm.sources.collectAsState().value,
languages = vm.languages,
getSourceLanguages = vm::getSourceLanguages,
setEnabledLanguages = vm::setEnabledLanguages
)
}
}

View File

@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
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()

View File

@@ -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()

View File

@@ -0,0 +1,45 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.sources.settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import ca.gosyer.presentation.build.BuildKonfig
import ca.gosyer.ui.sources.settings.components.SourceSettingsScreenContent
import ca.gosyer.ui.util.compose.ThemedWindow
import ca.gosyer.ui.util.lang.launchApplication
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.Navigator
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
fun openSourceSettingsMenu(sourceId: Long) {
launchApplication {
ThemedWindow(::exitApplication, title = BuildKonfig.NAME) {
Navigator(remember { SourceSettingsScreen(sourceId) })
}
}
}
class SourceSettingsScreen(private val sourceId: Long) : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel {
instantiate<SourceSettingsScreenViewModel>(SourceSettingsScreenViewModel.Params(sourceId))
}
SourceSettingsScreenContent(
settings = vm.sourceSettings.collectAsState().value
)
}
}

View File

@@ -9,8 +9,8 @@ package ca.gosyer.ui.sources.settings
import ca.gosyer.core.lang.throwIfCancellation
import ca.gosyer.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() {

View File

@@ -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) {

View File

@@ -0,0 +1,40 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.updates
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import ca.gosyer.ui.manga.MangaScreen
import ca.gosyer.ui.reader.openReaderMenu
import ca.gosyer.ui.updates.components.UpdatesScreenContent
import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
class UpdatesScreen : Screen {
override val key: ScreenKey = uniqueScreenKey
@Composable
override fun Content() {
val vm = viewModel<UpdatesScreenViewModel>()
val navigator = LocalNavigator.currentOrThrow
UpdatesScreenContent(
isLoading = vm.isLoading.collectAsState().value,
updates = vm.updates.collectAsState().value,
loadNextPage = vm::loadNextPage,
openChapter = ::openReaderMenu,
openManga = { navigator push MangaScreen(it) },
downloadChapter = vm::downloadChapter,
deleteDownloadedChapter = vm::deleteDownloadedChapter,
stopDownloadingChapter = vm::stopDownloadingChapter
)
}
}

View File

@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import 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)
}
}

View File

@@ -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
)
}
}

View File

@@ -1,124 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.util.compose
import com.github.zsoltk.compose.savedinstancestate.Bundle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
inline fun <reified T> Bundle.putJsonObject(key: String, item: T) {
putString(key, Json.encodeToString(item))
}
inline fun <reified T> Bundle.getJsonObject(key: String): T? {
return getString(key)?.let { Json.decodeFromString(it) }
}
inline fun <reified T> Bundle.getJsonObjectArray(key: String): List<T?>? {
return getString(key)?.let { Json.decodeFromString(it) }
}
inline fun <T> saveAnyInBundle(
scope: CoroutineScope,
bundle: Bundle,
key: String,
getValue: Bundle.(String) -> T?,
crossinline putValue: Bundle.(itemKey: String, item: T) -> Unit,
initialValue: () -> T
): MutableStateFlow<T> {
val item = bundle.getValue(key)
val flow: MutableStateFlow<T> = if (item != null) {
MutableStateFlow(item)
} else {
MutableStateFlow(initialValue())
}
flow.drop(1)
.mapLatest { bundle.putValue(key, it) }
.launchIn(scope)
return flow
}
inline fun <reified T> saveObjectInBundle(
scope: CoroutineScope,
bundle: Bundle,
key: String,
initialValue: () -> T
): MutableStateFlow<T> {
return saveAnyInBundle(
scope,
bundle,
key,
{ getJsonObject<T>(it) },
{ itemKey, item ->
putJsonObject(itemKey, item)
},
initialValue
)
}
fun saveIntInBundle(
scope: CoroutineScope,
bundle: Bundle,
key: String,
initialValue: Int
): MutableStateFlow<Int> {
return saveAnyInBundle(
scope,
bundle,
key,
{ getInt(key, initialValue) },
{ itemKey, item ->
putInt(itemKey, item)
},
{ initialValue }
)
}
fun saveBooleanInBundle(
scope: CoroutineScope,
bundle: Bundle,
key: String,
initialValue: Boolean
): MutableStateFlow<Boolean> {
return saveAnyInBundle(
scope,
bundle,
key,
{ getBoolean(key, initialValue) },
{ itemKey, item ->
putBoolean(itemKey, item)
},
{ initialValue }
)
}
fun saveStringInBundle(
scope: CoroutineScope,
bundle: Bundle,
key: String,
initialValue: () -> String? = { null }
): MutableStateFlow<String?> {
return saveAnyInBundle(
scope,
bundle,
key,
{ getString(key) ?: initialValue() },
{ itemKey, item ->
if (item != null) {
putString(itemKey, item)
} else remove(itemKey)
},
initialValue
)
}

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,34 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.util.compose
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import com.github.zsoltk.compose.savedinstancestate.Bundle
import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState
const val LAZY_LIST_ITEM = "lazy_list_item"
const val LAZY_LIST_OFFSET = "lazy_list_offset"
@Composable
fun persistentLazyListState(bundle: Bundle = LocalSavedInstanceState.current): LazyListState {
val state = rememberLazyListState(
remember { bundle.getInt(LAZY_LIST_ITEM, 0) },
remember { bundle.getInt(LAZY_LIST_OFFSET, 0) }
)
DisposableEffect(Unit) {
onDispose {
bundle.putInt(LAZY_LIST_ITEM, state.firstVisibleItemIndex)
bundle.putInt(LAZY_LIST_OFFSET, state.firstVisibleItemScrollOffset)
}
}
return state
}

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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() }
}

View File

@@ -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)