More updates

- Theme Engine fully implemented
- Bundle attempt for sources
- Server implementation optional
- Settings properly implemented
This commit is contained in:
Syer10
2021-04-25 22:13:06 -04:00
parent bf718967dd
commit 2bac6d5e8c
39 changed files with 1155 additions and 180 deletions

View File

@@ -13,6 +13,7 @@ import ca.gosyer.data.library.LibraryPreferences
import ca.gosyer.data.server.Http import ca.gosyer.data.server.Http
import ca.gosyer.data.server.HttpProvider import ca.gosyer.data.server.HttpProvider
import ca.gosyer.data.server.ServerPreferences import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.ServerService
import ca.gosyer.data.server.interactions.CategoryInteractionHandler import ca.gosyer.data.server.interactions.CategoryInteractionHandler
import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import ca.gosyer.data.server.interactions.ExtensionInteractionHandler import ca.gosyer.data.server.interactions.ExtensionInteractionHandler
@@ -63,4 +64,8 @@ val DataModule = module {
.toClass<MangaInteractionHandler>() .toClass<MangaInteractionHandler>()
bind<SourceInteractionHandler>() bind<SourceInteractionHandler>()
.toClass<SourceInteractionHandler>() .toClass<SourceInteractionHandler>()
bind<ServerService>()
.toClass<ServerService>()
.singleton()
} }

View File

@@ -15,4 +15,8 @@ class LibraryPreferences(private val preferenceStore: PreferenceStore) {
fun displayMode(): Preference<DisplayMode> { fun displayMode(): Preference<DisplayMode> {
return preferenceStore.getJsonObject("display_mode", DisplayMode.CompactGrid, DisplayMode.serializer()) return preferenceStore.getJsonObject("display_mode", DisplayMode.CompactGrid, DisplayMode.serializer())
} }
fun showAllCategory(): Preference<Boolean> {
return preferenceStore.getBoolean("show_all_category", false)
}
} }

View File

@@ -10,6 +10,11 @@ import ca.gosyer.common.prefs.Preference
import ca.gosyer.common.prefs.PreferenceStore import ca.gosyer.common.prefs.PreferenceStore
class ServerPreferences(private val preferenceStore: PreferenceStore) { class ServerPreferences(private val preferenceStore: PreferenceStore) {
fun host(): Preference<Boolean> {
return preferenceStore.getBoolean("host", true)
}
fun server(): Preference<String> { fun server(): Preference<String> {
return preferenceStore.getString("server_url", "http://localhost:4567") return preferenceStore.getString("server_url", "http://localhost:4567")
} }

View File

@@ -0,0 +1,101 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.data.server
import ca.gosyer.util.system.userDataDir
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import mu.KotlinLogging
import java.io.BufferedReader
import java.io.File
import javax.inject.Inject
import kotlin.concurrent.thread
class ServerService @Inject constructor(
val serverPreferences: ServerPreferences
) {
private val host = serverPreferences.host().stateIn(GlobalScope)
val initialized = MutableStateFlow(
if (host.value) {
ServerResult.STARTING
} else {
ServerResult.UNUSED
}
)
var process: Process? = null
init {
host.onEach {
process?.destroy()
initialized.value = if (host.value) {
ServerResult.STARTING
} else {
ServerResult.UNUSED
return@onEach
}
GlobalScope.launch {
val logger = KotlinLogging.logger("Server")
val runtime = Runtime.getRuntime()
val jarFile = File(userDataDir,"Tachidesk.jar")
if (!jarFile.exists()) {
logger.info { "Copying server to resources" }
javaClass.getResourceAsStream("/Tachidesk.jar")?.buffered()?.use { input ->
jarFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
val javaLibraryPath = System.getProperty("java.library.path").substringBefore(File.pathSeparator)
val javaExeFile = File(javaLibraryPath, "java.exe")
val javaUnixFile = File(javaLibraryPath, "java")
val javaExePath = when {
javaExeFile.exists() ->'"' + javaExeFile.absolutePath + '"'
javaUnixFile.exists() -> '"' + javaUnixFile.absolutePath + '"'
else -> "java"
}
logger.info { "Starting server with $javaExePath" }
val reader: BufferedReader
process = runtime.exec("""$javaExePath -jar "${jarFile.absolutePath}"""").also {
reader = it.inputStream.bufferedReader()
}
runtime.addShutdownHook(thread(start = false) {
process?.destroy()
})
logger.info { "Server started successfully" }
var line: String?
while (reader.readLine().also { line = it } != null) {
if (initialized.value == ServerResult.STARTING) {
if (line?.contains("Javalin started") == true) {
initialized.value = ServerResult.STARTED
} else if (line?.contains("Javalin has stopped") == true) {
initialized.value = ServerResult.FAILED
}
}
logger.info { line }
}
logger.info { "Server closed" }
val exitVal = process?.waitFor()
logger.info { "Process exitValue: $exitVal" }
}
}.launchIn(GlobalScope)
}
enum class ServerResult {
UNUSED,
STARTING,
STARTED,
FAILED;
}
}

View File

@@ -8,7 +8,7 @@ package ca.gosyer.data.ui
import ca.gosyer.common.prefs.Preference import ca.gosyer.common.prefs.Preference
import ca.gosyer.common.prefs.PreferenceStore import ca.gosyer.common.prefs.PreferenceStore
import ca.gosyer.data.ui.model.Screen import ca.gosyer.data.ui.model.StartScreen
import ca.gosyer.data.ui.model.ThemeMode import ca.gosyer.data.ui.model.ThemeMode
class UiPreferences(private val preferenceStore: PreferenceStore) { class UiPreferences(private val preferenceStore: PreferenceStore) {
@@ -41,8 +41,8 @@ class UiPreferences(private val preferenceStore: PreferenceStore) {
return preferenceStore.getInt("color_secondary_dark", 0) return preferenceStore.getInt("color_secondary_dark", 0)
} }
fun startScreen(): Preference<Screen> { fun startScreen(): Preference<StartScreen> {
return preferenceStore.getJsonObject("start_screen", Screen.Library, Screen.serializer()) return preferenceStore.getJsonObject("start_screen", StartScreen.Library, StartScreen.serializer())
} }
fun confirmExit(): Preference<Boolean> { fun confirmExit(): Preference<Boolean> {

View File

@@ -11,7 +11,7 @@ package ca.gosyer.data.ui.model
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
enum class Screen { enum class StartScreen {
Library, Library,
// Updates, // Updates,
// History, // History,

View File

@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton import androidx.compose.material.OutlinedButton
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
@@ -27,6 +26,7 @@ import androidx.compose.ui.input.key.Key
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.ui.base.theme.AppTheme
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
@Suppress("FunctionName") @Suppress("FunctionName")
@@ -69,7 +69,7 @@ fun WindowDialog(
window.keyboard.setShortcut(Key.Escape, onNegativeButton.plusClose()) window.keyboard.setShortcut(Key.Escape, onNegativeButton.plusClose())
window.show { window.show {
MaterialTheme { AppTheme {
Surface { Surface {
Column(verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxSize(),) { Column(verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxSize(),) {
Row(content = row, modifier = Modifier.fillMaxWidth()) Row(content = row, modifier = Modifier.fillMaxWidth())
@@ -117,10 +117,9 @@ fun WindowDialog(
} }
window.show { window.show {
MaterialTheme { AppTheme {
Surface { Surface {
Column( Column(
verticalArrangement = Arrangement.Bottom,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
content(window) content(window)

View File

@@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@@ -79,6 +78,7 @@ fun ColorPickerDialog(
WindowDialog( WindowDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
size = IntSize(300, 520),
title = title, title = title,
content = { content = {
val showPresetsState by showPresets.collectAsState() val showPresetsState by showPresets.collectAsState()
@@ -157,7 +157,7 @@ private fun ColorPresets(
.background(MaterialTheme.colors.onBackground.copy(alpha = 0.2f)) .background(MaterialTheme.colors.onBackground.copy(alpha = 0.2f))
) )
LazyRow { LazyVerticalGrid(cells = GridCells.Fixed(5)) {
items(shades) { color -> items(shades) { color ->
ColorPresetItem( ColorPresetItem(
color = color, color = color,

View File

@@ -9,7 +9,9 @@ package ca.gosyer.ui.base.components
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -19,13 +21,20 @@ import androidx.compose.ui.unit.sp
import kotlin.random.Random import kotlin.random.Random
@Composable @Composable
fun ErrorScreen(errorMessage: String? = null) { fun ErrorScreen(errorMessage: String? = null, retry: (() -> Unit)? = null) {
Box(Modifier.fillMaxSize()) { Surface {
Column(modifier = Modifier.align(Alignment.Center)) { Box(Modifier.fillMaxSize()) {
val errorFace = remember { getRandomErrorFace() } Column(modifier = Modifier.align(Alignment.Center)) {
Text(errorFace, fontSize = 36.sp, color = MaterialTheme.colors.onBackground) val errorFace = remember { getRandomErrorFace() }
if (errorMessage != null) { Text(errorFace, fontSize = 36.sp, color = MaterialTheme.colors.onBackground)
Text(errorMessage, color = MaterialTheme.colors.onBackground) if (errorMessage != null) {
Text(errorMessage, color = MaterialTheme.colors.onBackground)
}
if (retry != null) {
Button(retry) {
Text("Retry")
}
}
} }
} }
} }

View File

@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -22,14 +23,16 @@ fun LoadingScreen(
modifier: Modifier = Modifier.fillMaxSize(), modifier: Modifier = Modifier.fillMaxSize(),
errorMessage: String? = null errorMessage: String? = null
) { ) {
BoxWithConstraints(modifier) { Surface(modifier) {
if (isLoading) { BoxWithConstraints {
val size = remember(maxHeight, maxWidth) { if (isLoading) {
min(maxHeight, maxWidth) / 2 val size = remember(maxHeight, maxWidth) {
min(maxHeight, maxWidth) / 2
}
CircularProgressIndicator(Modifier.align(Alignment.Center).size(size))
} else {
ErrorScreen(errorMessage)
} }
CircularProgressIndicator(Modifier.align(Alignment.Center).size(size))
} else {
ErrorScreen(errorMessage)
} }
} }
} }

View File

@@ -45,7 +45,7 @@ fun MangaGridItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(mangaAspectRatio) .aspectRatio(mangaAspectRatio)
.padding(4.dp) .padding(8.dp)
.clickable(onClick = onClick), .clickable(onClick = onClick),
elevation = 4.dp, elevation = 4.dp,
shape = RoundedCornerShape(4.dp) shape = RoundedCornerShape(4.dp)

View File

@@ -6,24 +6,24 @@
package ca.gosyer.ui.base.components package ca.gosyer.ui.base.components
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.material.AppBarDefaults
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import ca.gosyer.ui.main.Route
import ca.gosyer.ui.main.Routing
import com.github.zsoltk.compose.router.BackStack import com.github.zsoltk.compose.router.BackStack
import compose.icons.FontAwesomeIcons import compose.icons.FontAwesomeIcons
import compose.icons.fontawesomeicons.Regular import compose.icons.fontawesomeicons.Regular
@@ -32,13 +32,39 @@ import compose.icons.fontawesomeicons.regular.WindowClose
@Composable @Composable
fun Toolbar( fun Toolbar(
name: String, name: String,
router: BackStack<Routing>? = null, router: BackStack<Route>? = null,
closable: Boolean, closable: Boolean,
modifier: Modifier = Modifier,
actions: @Composable RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.primary, //CustomColors.current.bars,
contentColor: Color = MaterialTheme.colors.onPrimary, //CustomColors.current.onBars,
elevation: Dp = AppBarDefaults.TopAppBarElevation,
search: ((String) -> Unit)? = null search: ((String) -> Unit)? = null
) { ) {
val searchText = remember { mutableStateOf("") } val searchText = remember { mutableStateOf("") }
Surface(Modifier.fillMaxWidth().height(32.dp), elevation = 2.dp) { Surface(Modifier.fillMaxWidth().height(32.dp), elevation = 2.dp) {
Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween) { TopAppBar(
{
Text(name)
},
modifier,
actions = @Composable {
actions()
if (closable) {
IconButton(
onClick = {
router?.pop()
}
) {
Icon(FontAwesomeIcons.Regular.WindowClose, "close")
}
}
},
backgroundColor = backgroundColor,
contentColor = contentColor,
elevation = elevation
)
/*Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(name, fontSize = 24.sp) Text(name, fontSize = 24.sp)
if (search != null) { if (search != null) {
BasicTextField( BasicTextField(
@@ -58,6 +84,6 @@ fun Toolbar(
Icon(FontAwesomeIcons.Regular.WindowClose, "close", Modifier.size(32.dp)) Icon(FontAwesomeIcons.Regular.WindowClose, "close", Modifier.size(32.dp))
} }
} }
} }*/
} }
} }

View File

@@ -20,7 +20,7 @@ class PreferenceMutableStateFlow<T>(
init { init {
preference.changes() preference.changes()
.onEach { value = it } .onEach { state.value = it }
.launchIn(scope) .launchIn(scope)
} }

View File

@@ -27,16 +27,23 @@ import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.LocalContentColor import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.RadioButton import androidx.compose.material.RadioButton
import androidx.compose.material.Switch import androidx.compose.material.Switch
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.ui.base.WindowDialog import ca.gosyer.ui.base.WindowDialog
@@ -64,13 +71,14 @@ class PreferenceScope {
title: String, title: String,
subtitle: String? = null subtitle: String? = null
) { ) {
val prefValue by preference.collectAsState()
Pref( Pref(
title = title, title = title,
subtitle = if (subtitle == null) choices[preference.value] else null, subtitle = if (subtitle == null) choices[prefValue] else null,
onClick = { onClick = {
ChoiceDialog( ChoiceDialog(
items = choices.toList(), items = choices.toList(),
selected = preference.value, selected = prefValue,
title = title, title = title,
onSelected = { selected -> onSelected = { selected ->
preference.value = selected preference.value = selected
@@ -102,7 +110,8 @@ class PreferenceScope {
}, },
onLongClick = { preference.value = Color.Unspecified }, onLongClick = { preference.value = Color.Unspecified },
action = { action = {
if (preference.value != Color.Unspecified || unsetColor != Color.Unspecified) { val prefValue by preference.collectAsState()
if (prefValue != Color.Unspecified || unsetColor != Color.Unspecified) {
val borderColor = MaterialTheme.colors.onBackground.copy(alpha = 0.54f) val borderColor = MaterialTheme.colors.onBackground.copy(alpha = 0.54f)
Box( Box(
modifier = Modifier modifier = Modifier
@@ -214,8 +223,37 @@ fun SwitchPref(
title = title, title = title,
subtitle = subtitle, subtitle = subtitle,
icon = icon, icon = icon,
action = { Switch(checked = preference.value, onCheckedChange = null) }, action = {
val prefValue by preference.collectAsState()
Switch(checked = prefValue, onCheckedChange = null)
},
onClick = { preference.value = !preference.value } onClick = { preference.value = !preference.value }
) )
} }
@Composable
fun EditTextPref(
preference: PreferenceMutableStateFlow<String>,
title: String,
subtitle: String? = null,
icon: ImageVector? = null
) {
var editText by remember { mutableStateOf(TextFieldValue(preference.value)) }
Pref(
title = title,
subtitle = subtitle,
icon = icon,
onClick = {
WindowDialog(
title,
onPositiveButton = {
preference.value = editText.text
}
) {
OutlinedTextField(editText, onValueChange = {
editText = it
})
}
}
)
}

View File

@@ -9,9 +9,10 @@ package ca.gosyer.ui.base.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import ca.gosyer.common.prefs.Preference import ca.gosyer.common.prefs.Preference
import ca.gosyer.data.ui.UiPreferences import ca.gosyer.data.ui.UiPreferences
import ca.gosyer.ui.base.prefs.PreferenceMutableStateFlow
import ca.gosyer.ui.base.prefs.asColor import ca.gosyer.ui.base.prefs.asColor
import ca.gosyer.ui.base.prefs.asStateIn
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
data class AppColorsPreference( data class AppColorsPreference(
val primary: Preference<Color>, val primary: Preference<Color>,
@@ -19,8 +20,8 @@ data class AppColorsPreference(
) )
class AppColorsPreferenceState( class AppColorsPreferenceState(
val primaryStateFlow: StateFlow<Color>, val primaryStateFlow: PreferenceMutableStateFlow<Color>,
val secondaryStateFlow: StateFlow<Color> val secondaryStateFlow: PreferenceMutableStateFlow<Color>
) )
fun UiPreferences.getLightColors(): AppColorsPreference { fun UiPreferences.getLightColors(): AppColorsPreference {
@@ -37,9 +38,9 @@ fun UiPreferences.getDarkColors(): AppColorsPreference {
) )
} }
fun AppColorsPreference.asState(scope: CoroutineScope): AppColorsPreferenceState { fun AppColorsPreference.asStateFlow(scope: CoroutineScope): AppColorsPreferenceState {
return AppColorsPreferenceState( return AppColorsPreferenceState(
primary.stateIn(scope), primary.asStateIn(scope),
secondary.stateIn(scope) secondary.asStateIn(scope)
) )
} }

View File

@@ -59,9 +59,9 @@ private class AppThemeViewModel @Inject constructor(
baseThemeJob.cancelChildren() baseThemeJob.cancelChildren()
if (baseTheme.colors.isLight) { if (baseTheme.colors.isLight) {
uiPreferences.getLightColors().asState(baseThemeScope) uiPreferences.getLightColors().asStateFlow(baseThemeScope)
} else { } else {
uiPreferences.getDarkColors().asState(baseThemeScope) uiPreferences.getDarkColors().asStateFlow(baseThemeScope)
} }
} }

View File

@@ -12,6 +12,7 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ScrollableTabRow import androidx.compose.material.ScrollableTabRow
import androidx.compose.material.Tab import androidx.compose.material.Tab
@@ -22,6 +23,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ca.gosyer.data.library.model.DisplayMode import ca.gosyer.data.library.model.DisplayMode
import ca.gosyer.data.models.Category import ca.gosyer.data.models.Category
@@ -58,7 +60,7 @@ fun LibraryScreen(onClickManga: (Long) -> Unit = { openMangaMenu(it) }) {
sheetState = sheetState, sheetState = sheetState,
sheetContent = { *//*LibrarySheet()*//* } sheetContent = { *//*LibrarySheet()*//* }
) {*/ ) {*/
Column { Column(Modifier.fillMaxWidth()) {
/*Toolbar( /*Toolbar(
title = { title = {
val text = if (vm.showCategoryTabs) { val text = if (vm.showCategoryTabs) {

View File

@@ -27,6 +27,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -35,12 +36,22 @@ import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.ui.extensions.ExtensionsMenu import ca.gosyer.ui.extensions.ExtensionsMenu
import ca.gosyer.ui.library.LibraryScreen import ca.gosyer.ui.library.LibraryScreen
import ca.gosyer.ui.manga.MangaMenu import ca.gosyer.ui.manga.MangaMenu
import ca.gosyer.ui.settings.SettingsAdvancedScreen
import ca.gosyer.ui.settings.SettingsAppearance
import ca.gosyer.ui.settings.SettingsBackupScreen
import ca.gosyer.ui.settings.SettingsBrowseScreen
import ca.gosyer.ui.settings.SettingsGeneralScreen
import ca.gosyer.ui.settings.SettingsLibraryScreen
import ca.gosyer.ui.settings.SettingsReaderScreen
import ca.gosyer.ui.settings.SettingsScreen
import ca.gosyer.ui.settings.SettingsServerScreen
import ca.gosyer.ui.sources.SourcesMenu import ca.gosyer.ui.sources.SourcesMenu
import com.github.zsoltk.compose.router.Router import com.github.zsoltk.compose.router.Router
import compose.icons.FontAwesomeIcons import compose.icons.FontAwesomeIcons
import compose.icons.fontawesomeicons.Regular import compose.icons.fontawesomeicons.Regular
import compose.icons.fontawesomeicons.regular.Bookmark import compose.icons.fontawesomeicons.regular.Bookmark
import compose.icons.fontawesomeicons.regular.Compass import compose.icons.fontawesomeicons.regular.Compass
import compose.icons.fontawesomeicons.regular.Edit
import compose.icons.fontawesomeicons.regular.Map import compose.icons.fontawesomeicons.regular.Map
@Composable @Composable
@@ -48,51 +59,67 @@ fun MainMenu() {
val vm = viewModel<MainViewModel>() val vm = viewModel<MainViewModel>()
Surface { Surface {
Router<Routing>("TopLevel", Routing.LibraryMenu) { backStack -> Router<Route>("TopLevel", Route.Library) { backStack ->
Row { Row {
Column(Modifier.width(200.dp).fillMaxHeight(),) { Surface(elevation = 2.dp) {
Box(Modifier.fillMaxWidth().height(60.dp)) { Column(Modifier.width(200.dp).fillMaxHeight(),) {
Text(BuildConfig.NAME, fontSize = 30.sp, modifier = Modifier.align(Alignment.Center)) Box(Modifier.fillMaxWidth().height(60.dp)) {
} Text(BuildConfig.NAME, fontSize = 30.sp, modifier = Modifier.align(Alignment.Center))
Spacer(Modifier.height(20.dp)) }
remember { TopLevelMenus.values() }.forEach { topLevelMenu -> Spacer(Modifier.height(20.dp))
MainMenuItem(topLevelMenu, backStack.elements.first() == topLevelMenu.menu) { remember { TopLevelMenus.values() }.forEach { topLevelMenu ->
backStack.newRoot(it) MainMenuItem(topLevelMenu, backStack.elements.first() == topLevelMenu.menu) {
backStack.newRoot(it)
}
} }
}
/*Button( /*Button(
onClick = ::openExtensionsMenu onClick = ::openExtensionsMenu
) { ) {
Text("Extensions") Text("Extensions")
}
Button(
onClick = ::openSourcesMenu
) {
Text("Sources")
}
Button(
onClick = ::openLibraryMenu
) {
Text("Library")
}
Button(
onClick = ::openCategoriesMenu
) {
Text("Categories")
}*/
} }
Button(
onClick = ::openSourcesMenu
) {
Text("Sources")
}
Button(
onClick = ::openLibraryMenu
) {
Text("Library")
}
Button(
onClick = ::openCategoriesMenu
) {
Text("Categories")
}*/
} }
Column(Modifier.fillMaxSize()) { Column(Modifier.fillMaxSize()) {
when (val routing = backStack.last()) { when (val routing = backStack.last()) {
is Routing.LibraryMenu -> LibraryScreen { is Route.Library -> LibraryScreen {
backStack.push(Routing.MangaMenu(it)) backStack.push(Route.Manga(it))
} }
is Routing.SourcesMenu -> SourcesMenu { is Route.Sources -> SourcesMenu {
backStack.push(Routing.MangaMenu(it)) backStack.push(Route.Manga(it))
} }
is Routing.ExtensionsMenu -> ExtensionsMenu() is Route.Extensions -> ExtensionsMenu()
is Routing.MangaMenu -> MangaMenu(routing.mangaId, backStack) is Route.Manga -> MangaMenu(routing.mangaId, backStack)
is Route.Settings -> SettingsScreen(backStack)
is Route.SettingsGeneral -> SettingsGeneralScreen(backStack)
is Route.SettingsAppearance -> SettingsAppearance(backStack)
is Route.SettingsServer -> SettingsServerScreen(backStack)
is Route.SettingsLibrary -> SettingsLibraryScreen(backStack)
is Route.SettingsReader -> SettingsReaderScreen(backStack)
/*is Route.SettingsDownloads -> SettingsDownloadsScreen(backStack)
is Route.SettingsTracking -> SettingsTrackingScreen(backStack)*/
is Route.SettingsBrowse -> SettingsBrowseScreen(backStack)
is Route.SettingsBackup -> SettingsBackupScreen(backStack)
/*is Route.SettingsSecurity -> SettingsSecurityScreen(backStack)
is Route.SettingsParentalControls -> SettingsParentalControlsScreen(backStack)*/
is Route.SettingsAdvanced -> SettingsAdvancedScreen(backStack)
} }
} }
} }
@@ -102,7 +129,7 @@ fun MainMenu() {
} }
@Composable @Composable
fun MainMenuItem(menu: TopLevelMenus, selected: Boolean, onClick: (Routing) -> Unit) { fun MainMenuItem(menu: TopLevelMenus, selected: Boolean, onClick: (Route) -> Unit) {
Card( Card(
Modifier.clickable { onClick(menu.menu) }.fillMaxWidth().height(40.dp), Modifier.clickable { onClick(menu.menu) }.fillMaxWidth().height(40.dp),
backgroundColor = if (!selected) { backgroundColor = if (!selected) {
@@ -115,22 +142,37 @@ fun MainMenuItem(menu: TopLevelMenus, selected: Boolean, onClick: (Routing) -> U
) { ) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize()) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize()) {
Spacer(Modifier.width(16.dp)) Spacer(Modifier.width(16.dp))
Image(menu.icon, menu.text, modifier = Modifier.size(20.dp)) Image(menu.icon, menu.text, modifier = Modifier.size(20.dp), colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface))
Spacer(Modifier.width(16.dp)) Spacer(Modifier.width(16.dp))
Text(menu.text) Text(menu.text, color = MaterialTheme.colors.onSurface)
} }
} }
} }
enum class TopLevelMenus(val text: String, val icon: ImageVector, val menu: Routing) { enum class TopLevelMenus(val text: String, val icon: ImageVector, val menu: Route) {
Library("Library", FontAwesomeIcons.Regular.Bookmark, Routing.LibraryMenu), Library("Library", FontAwesomeIcons.Regular.Bookmark, Route.Library),
Sources("Sources", FontAwesomeIcons.Regular.Compass, Routing.SourcesMenu), Sources("Sources", FontAwesomeIcons.Regular.Compass, Route.Sources),
Extensions("Extensions", FontAwesomeIcons.Regular.Map, Routing.ExtensionsMenu); Extensions("Extensions", FontAwesomeIcons.Regular.Map, Route.Extensions),
Settings("Settings", FontAwesomeIcons.Regular.Edit, Route.Settings)
} }
sealed class Routing { sealed class Route {
object LibraryMenu : Routing() object Library : Route()
object SourcesMenu : Routing() object Sources : Route()
object ExtensionsMenu : Routing() object Extensions : Route()
data class MangaMenu(val mangaId: Long): Routing() data class Manga(val mangaId: Long): Route()
object Settings : Route()
object SettingsGeneral : Route()
object SettingsAppearance : Route()
object SettingsLibrary : Route()
object SettingsReader : Route()
/*object SettingsDownloads : Route()
object SettingsTracking : Route()*/
object SettingsBrowse : Route()
object SettingsBackup : Route()
object SettingsServer : Route()
/*object SettingsSecurity : Route()
object SettingsParentalControls : Route()*/
object SettingsAdvanced : Route()
} }

View File

@@ -7,29 +7,24 @@
package ca.gosyer.ui.main package ca.gosyer.ui.main
import androidx.compose.desktop.AppWindow import androidx.compose.desktop.AppWindow
import androidx.compose.desktop.DesktopMaterialTheme
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
import ca.gosyer.BuildConfig import ca.gosyer.BuildConfig
import ca.gosyer.data.DataModule import ca.gosyer.data.DataModule
import ca.gosyer.data.server.ServerService
import ca.gosyer.data.server.ServerService.ServerResult
import ca.gosyer.ui.base.components.ErrorScreen
import ca.gosyer.ui.base.components.LoadingScreen import ca.gosyer.ui.base.components.LoadingScreen
import ca.gosyer.ui.base.theme.AppTheme import ca.gosyer.ui.base.theme.AppTheme
import ca.gosyer.util.system.userDataDir
import com.github.zsoltk.compose.backpress.BackPressHandler import com.github.zsoltk.compose.backpress.BackPressHandler
import com.github.zsoltk.compose.backpress.LocalBackPressHandler import com.github.zsoltk.compose.backpress.LocalBackPressHandler
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import mu.KotlinLogging
import org.apache.logging.log4j.core.config.Configurator import org.apache.logging.log4j.core.config.Configurator
import toothpick.configuration.Configuration import toothpick.configuration.Configuration
import toothpick.ktp.KTP import toothpick.ktp.KTP
import java.io.BufferedReader import toothpick.ktp.extension.getInstance
import java.io.File
import javax.swing.SwingUtilities import javax.swing.SwingUtilities
import kotlin.concurrent.thread
fun main() { fun main() {
val clazz = MainViewModel::class.java val clazz = MainViewModel::class.java
@@ -38,51 +33,6 @@ fun main() {
clazz.classLoader, clazz.classLoader,
clazz.getResource("log4j2.xml")?.toURI() clazz.getResource("log4j2.xml")?.toURI()
) )
val serverInitialized = MutableStateFlow(false)
GlobalScope.launch {
val logger = KotlinLogging.logger("Server")
val runtime = Runtime.getRuntime()
val jarFile = File(userDataDir,"Tachidesk.jar")
if (!jarFile.exists()) {
logger.info { "Copying server to resources" }
javaClass.getResourceAsStream("/Tachidesk.jar")?.buffered()?.use { input ->
jarFile.outputStream().use { output ->
input.copyTo(output)
}
}
}
val javaLibraryPath = System.getProperty("java.library.path").substringBefore(File.pathSeparator)
val javaExeFile = File(javaLibraryPath, "java.exe")
val javaUnixFile = File(javaLibraryPath, "java")
val javaExePath = when {
javaExeFile.exists() ->'"' + javaExeFile.absolutePath + '"'
javaUnixFile.exists() -> '"' + javaUnixFile.absolutePath + '"'
else -> "java"
}
logger.info { "Starting server with $javaExePath" }
val reader: BufferedReader
val process = runtime.exec("""$javaExePath -jar "${jarFile.absolutePath}"""").also {
reader = it.inputStream.bufferedReader()
}
runtime.addShutdownHook(thread(start = false) {
process?.destroy()
})
logger.info { "Server started successfully" }
var line: String?
while (reader.readLine().also { line = it } != null) {
if (!serverInitialized.value && line?.contains("Javalin started") == true) {
serverInitialized.value = true
}
logger.info { line }
}
logger.info { "Server closed" }
val exitVal = process.waitFor()
logger.info { "Process exitValue: $exitVal" }
}
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
System.setProperty("kotlinx.coroutines.debug", "on") System.setProperty("kotlinx.coroutines.debug", "on")
@@ -96,11 +46,13 @@ fun main() {
} }
) )
KTP.openRootScope() val scope = KTP.openRootScope()
.installModules( .installModules(
DataModule DataModule
) )
val serverService = scope.getInstance<ServerService>()
SwingUtilities.invokeLater { SwingUtilities.invokeLater {
val window = AppWindow( val window = AppWindow(
title = BuildConfig.NAME title = BuildConfig.NAME
@@ -115,11 +67,13 @@ fun main() {
CompositionLocalProvider( CompositionLocalProvider(
LocalBackPressHandler provides backPressHandler LocalBackPressHandler provides backPressHandler
) { ) {
val initialized by serverInitialized.collectAsState() val initialized by serverService.initialized.collectAsState()
if (initialized) { if (initialized == ServerResult.STARTED || initialized == ServerResult.UNUSED) {
MainMenu() MainMenu()
} else { } else if (initialized == ServerResult.STARTING) {
LoadingScreen() LoadingScreen()
} else if (initialized == ServerResult.FAILED) {
ErrorScreen("Unable to start server")
} }
} }
} }

View File

@@ -46,9 +46,10 @@ import ca.gosyer.ui.base.components.LoadingScreen
import ca.gosyer.ui.base.components.Toolbar import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.components.mangaAspectRatio import ca.gosyer.ui.base.components.mangaAspectRatio
import ca.gosyer.ui.base.vm.viewModel import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.ui.main.Routing import ca.gosyer.ui.main.Route
import ca.gosyer.util.compose.ThemedWindow import ca.gosyer.util.compose.ThemedWindow
import com.github.zsoltk.compose.router.BackStack import com.github.zsoltk.compose.router.BackStack
import java.util.Date
fun openMangaMenu(mangaId: Long) { fun openMangaMenu(mangaId: Long) {
ThemedWindow("TachideskJUI") { ThemedWindow("TachideskJUI") {
@@ -57,7 +58,7 @@ fun openMangaMenu(mangaId: Long) {
} }
@Composable @Composable
fun MangaMenu(mangaId: Long, backStack: BackStack<Routing>? = null) { fun MangaMenu(mangaId: Long, backStack: BackStack<Route>? = null) {
val vm = viewModel<MangaMenuViewModel> { val vm = viewModel<MangaMenuViewModel> {
MangaMenuViewModel.Params(mangaId) MangaMenuViewModel.Params(mangaId)
} }
@@ -65,6 +66,7 @@ fun MangaMenu(mangaId: Long, backStack: BackStack<Routing>? = null) {
val chapters by vm.chapters.collectAsState() val chapters by vm.chapters.collectAsState()
val isLoading by vm.isLoading.collectAsState() val isLoading by vm.isLoading.collectAsState()
val serverUrl by vm.serverUrl.collectAsState() val serverUrl by vm.serverUrl.collectAsState()
val dateFormat by vm.dateFormat.collectAsState()
Column(Modifier.background(MaterialTheme.colors.background)) { Column(Modifier.background(MaterialTheme.colors.background)) {
Toolbar("Manga", backStack, backStack != null) Toolbar("Manga", backStack, backStack != null)
@@ -86,7 +88,7 @@ fun MangaMenu(mangaId: Long, backStack: BackStack<Routing>? = null) {
items(items) { items(items) {
when (it) { when (it) {
is MangaMenu.MangaMenuManga -> MangaItem(it.manga, serverUrl) is MangaMenu.MangaMenuManga -> MangaItem(it.manga, serverUrl)
is MangaMenu.MangaMenuChapter -> ChapterItem(it.chapter) is MangaMenu.MangaMenuChapter -> ChapterItem(it.chapter, dateFormat::format)
} }
} }
} }
@@ -176,20 +178,20 @@ private fun MangaInfo(manga: Manga, modifier: Modifier = Modifier) {
} }
@Composable @Composable
private fun ChapterItem(chapter: Chapter) { fun ChapterItem(chapter: Chapter, format: (Date) -> String) {
Surface(modifier = Modifier.fillMaxWidth().height(70.dp).padding(4.dp), elevation = 2.dp) { Surface(modifier = Modifier.fillMaxWidth().height(70.dp).padding(4.dp), elevation = 1.dp) {
Column(Modifier.padding(4.dp)) { Column(Modifier.padding(4.dp)) {
Text(chapter.name, fontSize = 20.sp) Text(chapter.name, fontSize = 20.sp, maxLines = 1)
val description = mutableListOf<String>() val description = mutableListOf<String>()
if (chapter.dateUpload != 0L) { if (chapter.dateUpload != 0L) {
description += chapter.dateUpload.toString() description += format(Date(chapter.dateUpload))
} }
if (!chapter.scanlator.isNullOrEmpty()) { if (!chapter.scanlator.isNullOrEmpty()) {
description += chapter.scanlator description += chapter.scanlator
} }
if (description.isNotEmpty()) { if (description.isNotEmpty()) {
Spacer(Modifier.height(2.dp)) Spacer(Modifier.height(2.dp))
Text(description.joinToString(" - ")) Text(description.joinToString(" - "), maxLines = 1)
} }
} }
} }

View File

@@ -12,18 +12,24 @@ import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.interactions.ChapterInteractionHandler import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import ca.gosyer.data.server.interactions.LibraryInteractionHandler import ca.gosyer.data.server.interactions.LibraryInteractionHandler
import ca.gosyer.data.server.interactions.MangaInteractionHandler import ca.gosyer.data.server.interactions.MangaInteractionHandler
import ca.gosyer.data.ui.UiPreferences
import ca.gosyer.ui.base.vm.ViewModel import ca.gosyer.ui.base.vm.ViewModel
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class MangaMenuViewModel @Inject constructor( class MangaMenuViewModel @Inject constructor(
private val params: Params, private val params: Params,
private val uiPreferences: UiPreferences,
private val mangaHandler: MangaInteractionHandler, private val mangaHandler: MangaInteractionHandler,
private val chapterHandler: ChapterInteractionHandler, private val chapterHandler: ChapterInteractionHandler,
private val libraryHandler: LibraryInteractionHandler, private val libraryHandler: LibraryInteractionHandler,
@@ -40,6 +46,12 @@ class MangaMenuViewModel @Inject constructor(
private val _isLoading = MutableStateFlow(true) private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow() val isLoading = _isLoading.asStateFlow()
val dateFormat = uiPreferences.dateFormat().changes()
.map {
getDateFormat(it)
}
.asStateFlow(getDateFormat(uiPreferences.dateFormat().get()))
init { init {
scope.launch { scope.launch {
refreshMangaAsync(params.mangaId).await() to refreshChaptersAsync(params.mangaId).await() refreshMangaAsync(params.mangaId).await() to refreshChaptersAsync(params.mangaId).await()
@@ -82,5 +94,10 @@ class MangaMenuViewModel @Inject constructor(
} }
private fun getDateFormat(format: String): DateFormat = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(format, Locale.getDefault())
}
data class Params(val mangaId: Long) data class Params(val mangaId: Long)
} }

View File

@@ -0,0 +1,29 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
@Composable
fun SettingsAdvancedScreen(navController: BackStack<Route>) {
Column {
Toolbar("Advanced Settings", navController, true)
PreferencesScrollableColumn {
}
}
}

View File

@@ -0,0 +1,169 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ca.gosyer.data.ui.UiPreferences
import ca.gosyer.data.ui.model.ThemeMode
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.base.theme.AppColorsPreferenceState
import ca.gosyer.ui.base.theme.Theme
import ca.gosyer.ui.base.theme.asStateFlow
import ca.gosyer.ui.base.theme.getDarkColors
import ca.gosyer.ui.base.theme.getLightColors
import ca.gosyer.ui.base.theme.themes
import ca.gosyer.ui.base.vm.ViewModel
import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
import javax.inject.Inject
class ThemesViewModel @Inject constructor(
private val uiPreferences: UiPreferences,
) : ViewModel() {
val themeMode = uiPreferences.themeMode().asStateFlow()
val lightTheme = uiPreferences.lightTheme().asStateFlow()
val darkTheme = uiPreferences.darkTheme().asStateFlow()
val lightColors = uiPreferences.getLightColors().asStateFlow(scope)
val darkColors = uiPreferences.getDarkColors().asStateFlow(scope)
@Composable
fun getActiveColors(): AppColorsPreferenceState {
return if (MaterialTheme.colors.isLight) lightColors else darkColors
}
}
@Composable
fun SettingsAppearance(navController: BackStack<Route>) {
val vm = viewModel<ThemesViewModel>()
val activeColors = vm.getActiveColors()
val isLight = MaterialTheme.colors.isLight
val themesForCurrentMode = remember(isLight) {
themes.filter { it.colors.isLight == isLight }
}
Column {
Toolbar("Appearance Settings", navController, true)
PreferencesScrollableColumn {
ChoicePref(
preference = vm.themeMode,
choices = mapOf(
//ThemeMode.System to R.string.follow_system_settings,
ThemeMode.Light to "Light",
ThemeMode.Dark to "Dark"
),
title = "Theme"
)
Text(
"Preset themes",
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 4.dp)
)
LazyRow(modifier = Modifier.padding(horizontal = 8.dp)) {
items(themesForCurrentMode) { theme ->
ThemeItem(theme, onClick = {
(if (isLight) vm.lightTheme else vm.darkTheme).value = it.id
activeColors.primaryStateFlow.value = it.colors.primary
activeColors.secondaryStateFlow.value = it.colors.secondary
})
}
}
ColorPref(
preference = activeColors.primaryStateFlow, title = "Color primary",
subtitle = "Displayed most frequently across your app",
unsetColor = MaterialTheme.colors.primary
)
ColorPref(
preference = activeColors.secondaryStateFlow, title = "Color secondary",
subtitle = "Accents select parts of the UI",
unsetColor = MaterialTheme.colors.secondary
)
}
}
}
@Composable
private fun ThemeItem(
theme: Theme,
onClick: (Theme) -> Unit
) {
val borders = MaterialTheme.shapes.small
val borderColor = if (theme.colors.isLight) {
Color.Black.copy(alpha = 0.25f)
} else {
Color.White.copy(alpha = 0.15f)
}
Surface(
elevation = 4.dp, color = theme.colors.background, shape = borders,
modifier = Modifier
.size(100.dp, 160.dp)
.padding(8.dp)
.border(1.dp, borderColor, borders)
.clickable(onClick = { onClick(theme) })
) {
Column {
Box(
Modifier
.fillMaxWidth()
.weight(1f)
.padding(6.dp)
) {
Text("Text", fontSize = 11.sp)
Button(
onClick = {},
enabled = false,
contentPadding = PaddingValues(),
modifier = Modifier
.align(Alignment.BottomStart)
.size(40.dp, 20.dp),
content = {},
colors = ButtonDefaults.buttonColors(
disabledBackgroundColor = theme.colors.primary
)
)
Surface(Modifier
.size(24.dp)
.align(Alignment.BottomEnd),
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
color = theme.colors.secondary,
elevation = 6.dp,
content = { }
)
}
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
@Composable
fun SettingsBackupScreen(navController: BackStack<Route>) {
Column {
Toolbar("Backup Settings", navController, true)
PreferencesScrollableColumn {
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
@Composable
fun SettingsBrowseScreen(navController: BackStack<Route>) {
Column {
Toolbar("Browse Settings", navController, true)
PreferencesScrollableColumn {
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
@Composable
fun SettingsDownloadsScreen(navController: BackStack<Route>) {
Column {
Toolbar("Download Settings", navController, true)
PreferencesScrollableColumn {
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import ca.gosyer.data.ui.UiPreferences
import ca.gosyer.data.ui.model.StartScreen
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.base.prefs.SwitchPref
import ca.gosyer.ui.base.vm.ViewModel
import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
class SettingsGeneralViewModel @Inject constructor(
uiPreferences: UiPreferences
) : ViewModel() {
val startScreen = uiPreferences.startScreen().asStateFlow()
val confirmExit = uiPreferences.confirmExit().asStateFlow()
val language = uiPreferences.language().asStateFlow()
val dateFormat = uiPreferences.dateFormat().asStateFlow()
private val now = Date()
@Composable
fun getLanguageChoices(): Map<String, String> {
val currentLocaleDisplayName =
Locale.getDefault().let { it.getDisplayName(it).capitalize() }
return mapOf(
"" to "System Default ($currentLocaleDisplayName)"
)
}
@Composable
fun getDateChoices(): Map<String, String> {
return mapOf(
"" to "System Default",
"MM/dd/yy" to "MM/dd/yy",
"dd/MM/yy" to "dd/MM/yy",
"yyyy-MM-dd" to "yyyy-MM-dd"
).mapValues { "${it.value} (${getFormattedDate(it.key)})" }
}
private fun getFormattedDate(prefValue: String): String {
return when (prefValue) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(prefValue, Locale.getDefault())
}.format(now.time)
}
}
@Composable
fun SettingsGeneralScreen(navController: BackStack<Route>) {
val vm = viewModel<SettingsGeneralViewModel>()
Column {
Toolbar("General Settings", navController, true)
PreferencesScrollableColumn {
ChoicePref(
preference = vm.startScreen,
title = "Start Screen",
choices = mapOf(
StartScreen.Library to "Library",
StartScreen.Sources to "Sources",
StartScreen.Extensions to "Extensions",
)
)
SwitchPref(preference = vm.confirmExit, title = "Confirm Exit")
Divider()
ChoicePref(
preference = vm.language,
title = "Language",
choices = vm.getLanguageChoices(),
)
ChoicePref(
preference = vm.dateFormat,
title = "Date Format",
choices = vm.getDateChoices()
)
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import ca.gosyer.data.library.LibraryPreferences
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.base.prefs.SwitchPref
import ca.gosyer.ui.base.vm.ViewModel
import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
import javax.inject.Inject
class SettingsLibraryViewModel @Inject constructor(
libraryPreferences: LibraryPreferences
) : ViewModel() {
val showAllCategory = libraryPreferences.showAllCategory().asStateFlow()
}
@Composable
fun SettingsLibraryScreen(navController: BackStack<Route>) {
val vm = viewModel<SettingsLibraryViewModel>()
Column {
Toolbar("Library Settings", navController, true)
PreferencesScrollableColumn {
SwitchPref(preference = vm.showAllCategory, title = "Show all category")
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
@Composable
fun SettingsParentalControlsScreen(navController: BackStack<Route>) {
Column {
Toolbar("Parental Control Settings", navController, true)
PreferencesScrollableColumn {
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
@Composable
fun SettingsReaderScreen(navController: BackStack<Route>) {
Column {
Toolbar("Reader Settings", navController, true)
PreferencesScrollableColumn {
}
}
}

View File

@@ -0,0 +1,94 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.Pref
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
import compose.icons.FontAwesomeIcons
import compose.icons.fontawesomeicons.Regular
import compose.icons.fontawesomeicons.regular.Edit
@Composable
fun SettingsScreen(navController: BackStack<Route>) {
Column {
Toolbar("Settings", closable = false)
PreferencesScrollableColumn {
Pref(
title = "General",
icon = FontAwesomeIcons.Regular.Edit,
onClick = { navController.push(Route.SettingsGeneral) }
)
Pref(
title = "Appearance",
icon = FontAwesomeIcons.Regular.Edit,
onClick = { navController.push(Route.SettingsAppearance) }
)
Pref(
title = "Server",
icon = FontAwesomeIcons.Regular.Edit,
onClick = { navController.push(Route.SettingsServer) }
)
Pref(
title = "Library",
icon = FontAwesomeIcons.Regular.Edit,
onClick = { navController.push(Route.SettingsLibrary) }
)
Pref(
title = "Reader",
icon = FontAwesomeIcons.Regular.Edit,
onClick = { navController.push(Route.SettingsReader) }
)
/*Pref(
title = "Downloads",
icon = FontAwesomeIcons.Regular.Edit,
onClick = { navController.push(Route.SettingsDownloads) }
)
Pref(
title = "Tracking",
icon = FontAwesomeIcons.Regular.Edit,
onClick = { navController.push(Route.SettingsTracking) }
)
*/
Pref(
title = "Browse",
icon = FontAwesomeIcons.Regular.Edit,
onClick = { navController.push(Route.SettingsBrowse) }
)
Pref(
title = "Backup",
icon = FontAwesomeIcons.Regular.Edit,
onClick = { navController.push(Route.SettingsBackup) }
)
/*Pref(
title = "Security",
icon = FontAwesomeIcons.Regular.Edit,
onClick = { navController.push(Route.SettingsSecurity) }
)
Pref(
title = "Parental Controls",
icon = FontAwesomeIcons.Regular.User,
onClick = { navController.push(Route.SettingsParentalControls) }
)*/
Pref(
title = "Advanced",
icon = FontAwesomeIcons.Regular.Edit,
onClick = { navController.push(Route.SettingsAdvanced) }
)
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
@Composable
fun SettingsSecurityScreen(navController: BackStack<Route>) {
Column {
Toolbar("Security Settings", navController, true)
PreferencesScrollableColumn {
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.EditTextPref
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.base.prefs.SwitchPref
import ca.gosyer.ui.base.prefs.asStateIn
import ca.gosyer.ui.base.vm.ViewModel
import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
import javax.inject.Inject
class SettingsServerViewModel @Inject constructor(
private val serverPreferences: ServerPreferences
): ViewModel() {
val host = serverPreferences.host().asStateIn(scope)
val serverUrl = serverPreferences.server().asStateIn(scope)
}
@Composable
fun SettingsServerScreen(navController: BackStack<Route>) {
val vm = viewModel<SettingsServerViewModel>()
Column {
Toolbar("Server Settings", navController, true)
SwitchPref(preference = vm.host, title = "Host server inside TachideskJUI")
PreferencesScrollableColumn {
EditTextPref(vm.serverUrl, "Server Url", subtitle = vm.serverUrl.collectAsState().value)
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.prefs.PreferencesScrollableColumn
import ca.gosyer.ui.main.Route
import com.github.zsoltk.compose.router.BackStack
@Composable
fun SettingsTrackingScreen(navController: BackStack<Route>) {
Column {
Toolbar("Tracking Settings", navController, true)
PreferencesScrollableColumn {
}
}
}

View File

@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.Card
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -33,6 +32,7 @@ import ca.gosyer.ui.manga.openMangaMenu
import ca.gosyer.ui.sources.components.SourceHomeScreen import ca.gosyer.ui.sources.components.SourceHomeScreen
import ca.gosyer.ui.sources.components.SourceScreen import ca.gosyer.ui.sources.components.SourceScreen
import ca.gosyer.util.compose.ThemedWindow import ca.gosyer.util.compose.ThemedWindow
import com.github.zsoltk.compose.savedinstancestate.Bundle
import com.github.zsoltk.compose.savedinstancestate.BundleScope import com.github.zsoltk.compose.savedinstancestate.BundleScope
fun openSourcesMenu() { fun openSourcesMenu() {
@@ -43,9 +43,20 @@ fun openSourcesMenu() {
} }
} }
private const val SOURCE_MENU_KEY = "source_menu"
@Composable @Composable
fun SourcesMenu(onMangaClick: (Long) -> Unit) { fun SourcesMenu(onMangaClick: (Long) -> Unit) {
val vm = viewModel<SourcesMenuViewModel>() BundleScope(SOURCE_MENU_KEY, autoDispose = false) {
SourcesMenu(it, onMangaClick)
}
}
@Composable
fun SourcesMenu(bundle: Bundle, onMangaClick: (Long) -> Unit) {
val vm = viewModel<SourcesMenuViewModel> {
bundle
}
val isLoading by vm.isLoading.collectAsState() val isLoading by vm.isLoading.collectAsState()
val sources by vm.sources.collectAsState() val sources by vm.sources.collectAsState()
val sourceTabs by vm.sourceTabs.collectAsState() val sourceTabs by vm.sourceTabs.collectAsState()
@@ -56,27 +67,28 @@ fun SourcesMenu(onMangaClick: (Long) -> Unit) {
Column { Column {
Toolbar(selectedSourceTab?.name ?: "Sources", closable = false) Toolbar(selectedSourceTab?.name ?: "Sources", closable = false)
Row { Row {
LazyColumn(Modifier.fillMaxHeight().width(64.dp)) { Surface(elevation = 1.dp) {
items(sourceTabs) { source -> LazyColumn(Modifier.fillMaxHeight().width(64.dp)) {
Card( items(sourceTabs) { source ->
Modifier val modifier = Modifier
.clickable { .clickable {
vm.selectTab(source) vm.selectTab(source)
} }
.requiredHeight(64.dp) .requiredHeight(64.dp)
.requiredWidth(64.dp), .requiredWidth(64.dp)
) {
if (source != null) { if (source != null) {
KtorImage(source.iconUrl(serverUrl),) KtorImage(source.iconUrl(serverUrl), imageModifier = modifier)
} else { } else {
Icon(Icons.Default.Home, "Home") Icon(Icons.Default.Home, "Home", modifier = modifier)
} }
} }
} }
} }
val selectedSource: Source? = selectedSourceTab val selectedSource: Source? = selectedSourceTab
BundleScope(selectedSource?.name ?: "home") { BundleScope("Sources") {
if (selectedSource != null) { if (selectedSource != null) {
SourceScreen(selectedSource, onMangaClick) SourceScreen(selectedSource, onMangaClick)
} else { } else {

View File

@@ -11,14 +11,19 @@ import ca.gosyer.data.models.Source
import ca.gosyer.data.server.ServerPreferences import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel import ca.gosyer.ui.base.vm.ViewModel
import com.github.zsoltk.compose.savedinstancestate.Bundle
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mu.KotlinLogging import mu.KotlinLogging
import javax.inject.Inject import javax.inject.Inject
class SourcesMenuViewModel @Inject constructor( class SourcesMenuViewModel @Inject constructor(
private val bundle: Bundle,
private val sourceHandler: SourceInteractionHandler, private val sourceHandler: SourceInteractionHandler,
serverPreferences: ServerPreferences, serverPreferences: ServerPreferences,
catalogPreferences: CatalogPreferences catalogPreferences: CatalogPreferences
@@ -42,6 +47,22 @@ class SourcesMenuViewModel @Inject constructor(
val selectedSourceTab = _selectedSourceTab.asStateFlow() val selectedSourceTab = _selectedSourceTab.asStateFlow()
init { init {
_sourceTabs.drop(1)
.onEach { sources ->
bundle.putLongArray(SOURCE_TABS_KEY, sources.mapNotNull { it?.id }.toLongArray())
}
.launchIn(scope)
_selectedSourceTab.drop(1)
.onEach {
if (it != null) {
bundle.putLong(SELECTED_SOURCE_TAB, it.id)
} else {
bundle.remove(SELECTED_SOURCE_TAB)
}
}
.launchIn(scope)
getSources() getSources()
} }
@@ -55,6 +76,18 @@ class SourcesMenuViewModel @Inject constructor(
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
} finally { } finally {
val sourceTabs = bundle.getLongArray(SOURCE_TABS_KEY)
if (sourceTabs != null) {
_sourceTabs.value = listOf(null) + sourceTabs.toList()
.mapNotNull { sourceId ->
_sources.value.find { it.id == sourceId }
}
_selectedSourceTab.value = bundle.getLong(SELECTED_SOURCE_TAB, -1).let { id ->
if (id != -1L) {
_sources.value.find { it.id == id }
} else null
}
}
_isLoading.value = false _isLoading.value = false
} }
} }
@@ -77,4 +110,9 @@ class SourcesMenuViewModel @Inject constructor(
_selectedSourceTab.value = null _selectedSourceTab.value = null
} }
} }
companion object {
const val SOURCE_TABS_KEY = "source_tabs"
const val SELECTED_SOURCE_TAB = "selected_tab"
}
} }

View File

@@ -26,15 +26,23 @@ import ca.gosyer.data.models.Source
import ca.gosyer.ui.base.components.LoadingScreen import ca.gosyer.ui.base.components.LoadingScreen
import ca.gosyer.ui.base.components.MangaGridItem import ca.gosyer.ui.base.components.MangaGridItem
import ca.gosyer.ui.base.vm.viewModel import ca.gosyer.ui.base.vm.viewModel
import com.github.zsoltk.compose.savedinstancestate.Bundle
import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState
@Composable @Composable
fun SourceScreen( fun SourceScreen(
source: Source, source: Source,
onMangaClick: (Long) -> Unit onMangaClick: (Long) -> Unit
) { ) {
val upstream = LocalSavedInstanceState.current
val vm = viewModel<SourceScreenViewModel>() val vm = viewModel<SourceScreenViewModel>()
remember(source) { val bundle = remember(source.id) {
vm.init(source) upstream.getBundle(source.id.toString())
?: Bundle().also { upstream.putBundle(source.id.toString(), it) }
}
remember(source.id) {
vm.init(source, bundle)
} }
val mangas by vm.mangas.collectAsState() val mangas by vm.mangas.collectAsState()
val hasNextPage by vm.hasNextPage.collectAsState() val hasNextPage by vm.hasNextPage.collectAsState()

View File

@@ -10,18 +10,27 @@ import ca.gosyer.data.models.Manga
import ca.gosyer.data.models.MangaPage import ca.gosyer.data.models.MangaPage
import ca.gosyer.data.models.Source import ca.gosyer.data.models.Source
import ca.gosyer.data.server.ServerPreferences import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.interactions.MangaInteractionHandler
import ca.gosyer.data.server.interactions.SourceInteractionHandler import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel import ca.gosyer.ui.base.vm.ViewModel
import ca.gosyer.util.compose.getJsonObjectArray
import ca.gosyer.util.compose.putJsonObjectArray
import com.github.zsoltk.compose.savedinstancestate.Bundle
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class SourceScreenViewModel @Inject constructor( class SourceScreenViewModel @Inject constructor(
private val sourceHandler: SourceInteractionHandler, private val sourceHandler: SourceInteractionHandler,
private val mangaHandler: MangaInteractionHandler,
serverPreferences: ServerPreferences serverPreferences: ServerPreferences
): ViewModel() { ): ViewModel() {
private lateinit var source: Source private lateinit var source: Source
private lateinit var bundle: Bundle
val serverUrl = serverPreferences.server().stateIn(scope) val serverUrl = serverPreferences.server().stateIn(scope)
@@ -40,15 +49,38 @@ class SourceScreenViewModel @Inject constructor(
private val _pageNum = MutableStateFlow(1) private val _pageNum = MutableStateFlow(1)
val pageNum = _pageNum.asStateFlow() val pageNum = _pageNum.asStateFlow()
fun init(source: Source) { init {
_mangas.drop(1)
.onEach { manga ->
bundle.putJsonObjectArray(MANGAS_KEY, manga)
}
.launchIn(scope)
_hasNextPage.drop(1)
.onEach {
bundle.putBoolean(NEXT_PAGE_KEY, it)
}
.launchIn(scope)
_pageNum.drop(1)
.onEach {
bundle.putInt(PAGE_NUM_KEY, it)
}
.launchIn(scope)
}
fun init(source: Source, bundle: Bundle) {
this.source = source this.source = source
this.bundle = bundle
scope.launch { scope.launch {
_loading.value = true _loading.value = true
_mangas.value = emptyList() _mangas.value = emptyList()
_hasNextPage.value = false _hasNextPage.value = false
_pageNum.value = 1 _pageNum.value = bundle.getInt(PAGE_NUM_KEY, 1)
_isLatest.value = source.supportsLatest _isLatest.value = bundle.getBoolean(IS_LATEST_KEY, source.supportsLatest)
val page = getPage() val page = bundle.getJsonObjectArray<Manga>(MANGAS_KEY)
?.let {
MangaPage(it.filterNotNull(), bundle.getBoolean(NEXT_PAGE_KEY, true))
}
?: getPage()
_mangas.value += page.mangaList _mangas.value += page.mangaList
_hasNextPage.value = page.hasNextPage _hasNextPage.value = page.hasNextPage
_loading.value = false _loading.value = false
@@ -69,7 +101,11 @@ class SourceScreenViewModel @Inject constructor(
fun setMode(toLatest: Boolean) { fun setMode(toLatest: Boolean) {
if (isLatest.value != toLatest){ if (isLatest.value != toLatest){
_isLatest.value = toLatest _isLatest.value = toLatest
init(source) bundle.remove(MANGAS_KEY)
bundle.remove(NEXT_PAGE_KEY)
bundle.remove(PAGE_NUM_KEY)
bundle.remove(IS_LATEST_KEY)
init(source, bundle)
} }
} }
@@ -80,4 +116,11 @@ class SourceScreenViewModel @Inject constructor(
sourceHandler.getPopularManga(source, pageNum.value) sourceHandler.getPopularManga(source, pageNum.value)
} }
} }
companion object {
const val MANGAS_KEY = "mangas"
const val NEXT_PAGE_KEY = "next_page"
const val PAGE_NUM_KEY = "next_page"
const val IS_LATEST_KEY = "is_latest"
}
} }

View File

@@ -0,0 +1,28 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.util.compose
import com.github.zsoltk.compose.savedinstancestate.Bundle
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
inline fun <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.putJsonObjectArray(key: String, items: T) {
putString(key, Json.encodeToString(items))
}
inline fun <reified T> Bundle.getJsonObjectArray(key: String): List<T?>? {
return getString(key)?.let { Json.decodeFromString(it) }
}