mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
More updates
- Theme Engine fully implemented - Bundle attempt for sources - Server implementation optional - Settings properly implemented
This commit is contained in:
@@ -13,6 +13,7 @@ import ca.gosyer.data.library.LibraryPreferences
|
||||
import ca.gosyer.data.server.Http
|
||||
import ca.gosyer.data.server.HttpProvider
|
||||
import ca.gosyer.data.server.ServerPreferences
|
||||
import ca.gosyer.data.server.ServerService
|
||||
import ca.gosyer.data.server.interactions.CategoryInteractionHandler
|
||||
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
|
||||
import ca.gosyer.data.server.interactions.ExtensionInteractionHandler
|
||||
@@ -63,4 +64,8 @@ val DataModule = module {
|
||||
.toClass<MangaInteractionHandler>()
|
||||
bind<SourceInteractionHandler>()
|
||||
.toClass<SourceInteractionHandler>()
|
||||
|
||||
bind<ServerService>()
|
||||
.toClass<ServerService>()
|
||||
.singleton()
|
||||
}
|
||||
|
||||
@@ -15,4 +15,8 @@ class LibraryPreferences(private val preferenceStore: PreferenceStore) {
|
||||
fun displayMode(): Preference<DisplayMode> {
|
||||
return preferenceStore.getJsonObject("display_mode", DisplayMode.CompactGrid, DisplayMode.serializer())
|
||||
}
|
||||
|
||||
fun showAllCategory(): Preference<Boolean> {
|
||||
return preferenceStore.getBoolean("show_all_category", false)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,11 @@ import ca.gosyer.common.prefs.Preference
|
||||
import ca.gosyer.common.prefs.PreferenceStore
|
||||
|
||||
class ServerPreferences(private val preferenceStore: PreferenceStore) {
|
||||
|
||||
fun host(): Preference<Boolean> {
|
||||
return preferenceStore.getBoolean("host", true)
|
||||
}
|
||||
|
||||
fun server(): Preference<String> {
|
||||
return preferenceStore.getString("server_url", "http://localhost:4567")
|
||||
}
|
||||
|
||||
101
src/main/kotlin/ca/gosyer/data/server/ServerService.kt
Normal file
101
src/main/kotlin/ca/gosyer/data/server/ServerService.kt
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ package ca.gosyer.data.ui
|
||||
|
||||
import ca.gosyer.common.prefs.Preference
|
||||
import ca.gosyer.common.prefs.PreferenceStore
|
||||
import ca.gosyer.data.ui.model.Screen
|
||||
import ca.gosyer.data.ui.model.StartScreen
|
||||
import ca.gosyer.data.ui.model.ThemeMode
|
||||
|
||||
class UiPreferences(private val preferenceStore: PreferenceStore) {
|
||||
@@ -41,8 +41,8 @@ class UiPreferences(private val preferenceStore: PreferenceStore) {
|
||||
return preferenceStore.getInt("color_secondary_dark", 0)
|
||||
}
|
||||
|
||||
fun startScreen(): Preference<Screen> {
|
||||
return preferenceStore.getJsonObject("start_screen", Screen.Library, Screen.serializer())
|
||||
fun startScreen(): Preference<StartScreen> {
|
||||
return preferenceStore.getJsonObject("start_screen", StartScreen.Library, StartScreen.serializer())
|
||||
}
|
||||
|
||||
fun confirmExit(): Preference<Boolean> {
|
||||
|
||||
@@ -11,7 +11,7 @@ package ca.gosyer.data.ui.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class Screen {
|
||||
enum class StartScreen {
|
||||
Library,
|
||||
// Updates,
|
||||
// History,
|
||||
@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
@@ -27,6 +26,7 @@ import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.ui.base.theme.AppTheme
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
@Suppress("FunctionName")
|
||||
@@ -69,7 +69,7 @@ fun WindowDialog(
|
||||
window.keyboard.setShortcut(Key.Escape, onNegativeButton.plusClose())
|
||||
|
||||
window.show {
|
||||
MaterialTheme {
|
||||
AppTheme {
|
||||
Surface {
|
||||
Column(verticalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxSize(),) {
|
||||
Row(content = row, modifier = Modifier.fillMaxWidth())
|
||||
@@ -117,10 +117,9 @@ fun WindowDialog(
|
||||
}
|
||||
|
||||
window.show {
|
||||
MaterialTheme {
|
||||
AppTheme {
|
||||
Surface {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
content(window)
|
||||
|
||||
@@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.GridCells
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -79,6 +78,7 @@ fun ColorPickerDialog(
|
||||
|
||||
WindowDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
size = IntSize(300, 520),
|
||||
title = title,
|
||||
content = {
|
||||
val showPresetsState by showPresets.collectAsState()
|
||||
@@ -157,7 +157,7 @@ private fun ColorPresets(
|
||||
.background(MaterialTheme.colors.onBackground.copy(alpha = 0.2f))
|
||||
)
|
||||
|
||||
LazyRow {
|
||||
LazyVerticalGrid(cells = GridCells.Fixed(5)) {
|
||||
items(shades) { color ->
|
||||
ColorPresetItem(
|
||||
color = color,
|
||||
|
||||
@@ -9,7 +9,9 @@ package ca.gosyer.ui.base.components
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -19,7 +21,8 @@ import androidx.compose.ui.unit.sp
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
fun ErrorScreen(errorMessage: String? = null) {
|
||||
fun ErrorScreen(errorMessage: String? = null, retry: (() -> Unit)? = null) {
|
||||
Surface {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
val errorFace = remember { getRandomErrorFace() }
|
||||
@@ -27,6 +30,12 @@ fun ErrorScreen(errorMessage: String? = null) {
|
||||
if (errorMessage != null) {
|
||||
Text(errorMessage, color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
if (retry != null) {
|
||||
Button(retry) {
|
||||
Text("Retry")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -22,7 +23,8 @@ fun LoadingScreen(
|
||||
modifier: Modifier = Modifier.fillMaxSize(),
|
||||
errorMessage: String? = null
|
||||
) {
|
||||
BoxWithConstraints(modifier) {
|
||||
Surface(modifier) {
|
||||
BoxWithConstraints {
|
||||
if (isLoading) {
|
||||
val size = remember(maxHeight, maxWidth) {
|
||||
min(maxHeight, maxWidth) / 2
|
||||
@@ -33,3 +35,4 @@ fun LoadingScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ fun MangaGridItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(mangaAspectRatio)
|
||||
.padding(4.dp)
|
||||
.padding(8.dp)
|
||||
.clickable(onClick = onClick),
|
||||
elevation = 4.dp,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
|
||||
@@ -6,24 +6,24 @@
|
||||
|
||||
package ca.gosyer.ui.base.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material.AppBarDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import ca.gosyer.ui.main.Routing
|
||||
import ca.gosyer.ui.main.Route
|
||||
import com.github.zsoltk.compose.router.BackStack
|
||||
import compose.icons.FontAwesomeIcons
|
||||
import compose.icons.fontawesomeicons.Regular
|
||||
@@ -32,13 +32,39 @@ import compose.icons.fontawesomeicons.regular.WindowClose
|
||||
@Composable
|
||||
fun Toolbar(
|
||||
name: String,
|
||||
router: BackStack<Routing>? = null,
|
||||
router: BackStack<Route>? = null,
|
||||
closable: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
backgroundColor: Color = MaterialTheme.colors.primary, //CustomColors.current.bars,
|
||||
contentColor: Color = MaterialTheme.colors.onPrimary, //CustomColors.current.onBars,
|
||||
elevation: Dp = AppBarDefaults.TopAppBarElevation,
|
||||
search: ((String) -> Unit)? = null
|
||||
) {
|
||||
val searchText = remember { mutableStateOf("") }
|
||||
Surface(Modifier.fillMaxWidth().height(32.dp), elevation = 2.dp) {
|
||||
Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
TopAppBar(
|
||||
{
|
||||
Text(name)
|
||||
},
|
||||
modifier,
|
||||
actions = @Composable {
|
||||
actions()
|
||||
if (closable) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
router?.pop()
|
||||
}
|
||||
) {
|
||||
Icon(FontAwesomeIcons.Regular.WindowClose, "close")
|
||||
}
|
||||
}
|
||||
},
|
||||
backgroundColor = backgroundColor,
|
||||
contentColor = contentColor,
|
||||
elevation = elevation
|
||||
)
|
||||
/*Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(name, fontSize = 24.sp)
|
||||
if (search != null) {
|
||||
BasicTextField(
|
||||
@@ -58,6 +84,6 @@ fun Toolbar(
|
||||
Icon(FontAwesomeIcons.Regular.WindowClose, "close", Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class PreferenceMutableStateFlow<T>(
|
||||
|
||||
init {
|
||||
preference.changes()
|
||||
.onEach { value = it }
|
||||
.onEach { state.value = it }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,16 +27,23 @@ import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.ui.base.WindowDialog
|
||||
@@ -64,13 +71,14 @@ class PreferenceScope {
|
||||
title: String,
|
||||
subtitle: String? = null
|
||||
) {
|
||||
val prefValue by preference.collectAsState()
|
||||
Pref(
|
||||
title = title,
|
||||
subtitle = if (subtitle == null) choices[preference.value] else null,
|
||||
subtitle = if (subtitle == null) choices[prefValue] else null,
|
||||
onClick = {
|
||||
ChoiceDialog(
|
||||
items = choices.toList(),
|
||||
selected = preference.value,
|
||||
selected = prefValue,
|
||||
title = title,
|
||||
onSelected = { selected ->
|
||||
preference.value = selected
|
||||
@@ -102,7 +110,8 @@ class PreferenceScope {
|
||||
},
|
||||
onLongClick = { preference.value = Color.Unspecified },
|
||||
action = {
|
||||
if (preference.value != Color.Unspecified || unsetColor != Color.Unspecified) {
|
||||
val prefValue by preference.collectAsState()
|
||||
if (prefValue != Color.Unspecified || unsetColor != Color.Unspecified) {
|
||||
val borderColor = MaterialTheme.colors.onBackground.copy(alpha = 0.54f)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -214,8 +223,37 @@ fun SwitchPref(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
icon = icon,
|
||||
action = { Switch(checked = preference.value, onCheckedChange = null) },
|
||||
action = {
|
||||
val prefValue by preference.collectAsState()
|
||||
Switch(checked = prefValue, onCheckedChange = null)
|
||||
},
|
||||
onClick = { preference.value = !preference.value }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditTextPref(
|
||||
preference: PreferenceMutableStateFlow<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
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -9,9 +9,10 @@ package ca.gosyer.ui.base.theme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import ca.gosyer.common.prefs.Preference
|
||||
import ca.gosyer.data.ui.UiPreferences
|
||||
import ca.gosyer.ui.base.prefs.PreferenceMutableStateFlow
|
||||
import ca.gosyer.ui.base.prefs.asColor
|
||||
import ca.gosyer.ui.base.prefs.asStateIn
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
data class AppColorsPreference(
|
||||
val primary: Preference<Color>,
|
||||
@@ -19,8 +20,8 @@ data class AppColorsPreference(
|
||||
)
|
||||
|
||||
class AppColorsPreferenceState(
|
||||
val primaryStateFlow: StateFlow<Color>,
|
||||
val secondaryStateFlow: StateFlow<Color>
|
||||
val primaryStateFlow: PreferenceMutableStateFlow<Color>,
|
||||
val secondaryStateFlow: PreferenceMutableStateFlow<Color>
|
||||
)
|
||||
|
||||
fun UiPreferences.getLightColors(): AppColorsPreference {
|
||||
@@ -37,9 +38,9 @@ fun UiPreferences.getDarkColors(): AppColorsPreference {
|
||||
)
|
||||
}
|
||||
|
||||
fun AppColorsPreference.asState(scope: CoroutineScope): AppColorsPreferenceState {
|
||||
fun AppColorsPreference.asStateFlow(scope: CoroutineScope): AppColorsPreferenceState {
|
||||
return AppColorsPreferenceState(
|
||||
primary.stateIn(scope),
|
||||
secondary.stateIn(scope)
|
||||
primary.asStateIn(scope),
|
||||
secondary.asStateIn(scope)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,9 +59,9 @@ private class AppThemeViewModel @Inject constructor(
|
||||
baseThemeJob.cancelChildren()
|
||||
|
||||
if (baseTheme.colors.isLight) {
|
||||
uiPreferences.getLightColors().asState(baseThemeScope)
|
||||
uiPreferences.getLightColors().asStateFlow(baseThemeScope)
|
||||
} else {
|
||||
uiPreferences.getDarkColors().asState(baseThemeScope)
|
||||
uiPreferences.getDarkColors().asStateFlow(baseThemeScope)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ScrollableTabRow
|
||||
import androidx.compose.material.Tab
|
||||
@@ -22,6 +23,7 @@ import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.data.library.model.DisplayMode
|
||||
import ca.gosyer.data.models.Category
|
||||
@@ -58,7 +60,7 @@ fun LibraryScreen(onClickManga: (Long) -> Unit = { openMangaMenu(it) }) {
|
||||
sheetState = sheetState,
|
||||
sheetContent = { *//*LibrarySheet()*//* }
|
||||
) {*/
|
||||
Column {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
/*Toolbar(
|
||||
title = {
|
||||
val text = if (vm.showCategoryTabs) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -35,12 +36,22 @@ import ca.gosyer.ui.base.vm.viewModel
|
||||
import ca.gosyer.ui.extensions.ExtensionsMenu
|
||||
import ca.gosyer.ui.library.LibraryScreen
|
||||
import ca.gosyer.ui.manga.MangaMenu
|
||||
import ca.gosyer.ui.settings.SettingsAdvancedScreen
|
||||
import ca.gosyer.ui.settings.SettingsAppearance
|
||||
import ca.gosyer.ui.settings.SettingsBackupScreen
|
||||
import ca.gosyer.ui.settings.SettingsBrowseScreen
|
||||
import ca.gosyer.ui.settings.SettingsGeneralScreen
|
||||
import ca.gosyer.ui.settings.SettingsLibraryScreen
|
||||
import ca.gosyer.ui.settings.SettingsReaderScreen
|
||||
import ca.gosyer.ui.settings.SettingsScreen
|
||||
import ca.gosyer.ui.settings.SettingsServerScreen
|
||||
import ca.gosyer.ui.sources.SourcesMenu
|
||||
import com.github.zsoltk.compose.router.Router
|
||||
import compose.icons.FontAwesomeIcons
|
||||
import compose.icons.fontawesomeicons.Regular
|
||||
import compose.icons.fontawesomeicons.regular.Bookmark
|
||||
import compose.icons.fontawesomeicons.regular.Compass
|
||||
import compose.icons.fontawesomeicons.regular.Edit
|
||||
import compose.icons.fontawesomeicons.regular.Map
|
||||
|
||||
@Composable
|
||||
@@ -48,8 +59,9 @@ fun MainMenu() {
|
||||
val vm = viewModel<MainViewModel>()
|
||||
Surface {
|
||||
|
||||
Router<Routing>("TopLevel", Routing.LibraryMenu) { backStack ->
|
||||
Router<Route>("TopLevel", Route.Library) { backStack ->
|
||||
Row {
|
||||
Surface(elevation = 2.dp) {
|
||||
Column(Modifier.width(200.dp).fillMaxHeight(),) {
|
||||
Box(Modifier.fillMaxWidth().height(60.dp)) {
|
||||
Text(BuildConfig.NAME, fontSize = 30.sp, modifier = Modifier.align(Alignment.Center))
|
||||
@@ -82,17 +94,32 @@ fun MainMenu() {
|
||||
Text("Categories")
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
when (val routing = backStack.last()) {
|
||||
is Routing.LibraryMenu -> LibraryScreen {
|
||||
backStack.push(Routing.MangaMenu(it))
|
||||
is Route.Library -> LibraryScreen {
|
||||
backStack.push(Route.Manga(it))
|
||||
}
|
||||
is Routing.SourcesMenu -> SourcesMenu {
|
||||
backStack.push(Routing.MangaMenu(it))
|
||||
is Route.Sources -> SourcesMenu {
|
||||
backStack.push(Route.Manga(it))
|
||||
}
|
||||
is Routing.ExtensionsMenu -> ExtensionsMenu()
|
||||
is Routing.MangaMenu -> MangaMenu(routing.mangaId, backStack)
|
||||
is Route.Extensions -> ExtensionsMenu()
|
||||
is Route.Manga -> MangaMenu(routing.mangaId, backStack)
|
||||
|
||||
is Route.Settings -> SettingsScreen(backStack)
|
||||
is Route.SettingsGeneral -> SettingsGeneralScreen(backStack)
|
||||
is Route.SettingsAppearance -> SettingsAppearance(backStack)
|
||||
is Route.SettingsServer -> SettingsServerScreen(backStack)
|
||||
is Route.SettingsLibrary -> SettingsLibraryScreen(backStack)
|
||||
is Route.SettingsReader -> SettingsReaderScreen(backStack)
|
||||
/*is Route.SettingsDownloads -> SettingsDownloadsScreen(backStack)
|
||||
is Route.SettingsTracking -> SettingsTrackingScreen(backStack)*/
|
||||
is Route.SettingsBrowse -> SettingsBrowseScreen(backStack)
|
||||
is Route.SettingsBackup -> SettingsBackupScreen(backStack)
|
||||
/*is Route.SettingsSecurity -> SettingsSecurityScreen(backStack)
|
||||
is Route.SettingsParentalControls -> SettingsParentalControlsScreen(backStack)*/
|
||||
is Route.SettingsAdvanced -> SettingsAdvancedScreen(backStack)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,7 +129,7 @@ fun MainMenu() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainMenuItem(menu: TopLevelMenus, selected: Boolean, onClick: (Routing) -> Unit) {
|
||||
fun MainMenuItem(menu: TopLevelMenus, selected: Boolean, onClick: (Route) -> Unit) {
|
||||
Card(
|
||||
Modifier.clickable { onClick(menu.menu) }.fillMaxWidth().height(40.dp),
|
||||
backgroundColor = if (!selected) {
|
||||
@@ -115,22 +142,37 @@ fun MainMenuItem(menu: TopLevelMenus, selected: Boolean, onClick: (Routing) -> U
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize()) {
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Image(menu.icon, menu.text, modifier = Modifier.size(20.dp))
|
||||
Image(menu.icon, menu.text, modifier = Modifier.size(20.dp), colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface))
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Text(menu.text)
|
||||
Text(menu.text, color = MaterialTheme.colors.onSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class TopLevelMenus(val text: String, val icon: ImageVector, val menu: Routing) {
|
||||
Library("Library", FontAwesomeIcons.Regular.Bookmark, Routing.LibraryMenu),
|
||||
Sources("Sources", FontAwesomeIcons.Regular.Compass, Routing.SourcesMenu),
|
||||
Extensions("Extensions", FontAwesomeIcons.Regular.Map, Routing.ExtensionsMenu);
|
||||
enum class TopLevelMenus(val text: String, val icon: ImageVector, val menu: Route) {
|
||||
Library("Library", FontAwesomeIcons.Regular.Bookmark, Route.Library),
|
||||
Sources("Sources", FontAwesomeIcons.Regular.Compass, Route.Sources),
|
||||
Extensions("Extensions", FontAwesomeIcons.Regular.Map, Route.Extensions),
|
||||
Settings("Settings", FontAwesomeIcons.Regular.Edit, Route.Settings)
|
||||
}
|
||||
|
||||
sealed class Routing {
|
||||
object LibraryMenu : Routing()
|
||||
object SourcesMenu : Routing()
|
||||
object ExtensionsMenu : Routing()
|
||||
data class MangaMenu(val mangaId: Long): Routing()
|
||||
sealed class Route {
|
||||
object Library : Route()
|
||||
object Sources : Route()
|
||||
object Extensions : Route()
|
||||
data class Manga(val mangaId: Long): Route()
|
||||
|
||||
object Settings : Route()
|
||||
object SettingsGeneral : Route()
|
||||
object SettingsAppearance : Route()
|
||||
object SettingsLibrary : Route()
|
||||
object SettingsReader : Route()
|
||||
/*object SettingsDownloads : Route()
|
||||
object SettingsTracking : Route()*/
|
||||
object SettingsBrowse : Route()
|
||||
object SettingsBackup : Route()
|
||||
object SettingsServer : Route()
|
||||
/*object SettingsSecurity : Route()
|
||||
object SettingsParentalControls : Route()*/
|
||||
object SettingsAdvanced : Route()
|
||||
}
|
||||
@@ -7,29 +7,24 @@
|
||||
package ca.gosyer.ui.main
|
||||
|
||||
import androidx.compose.desktop.AppWindow
|
||||
import androidx.compose.desktop.DesktopMaterialTheme
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import ca.gosyer.BuildConfig
|
||||
import ca.gosyer.data.DataModule
|
||||
import ca.gosyer.data.server.ServerService
|
||||
import ca.gosyer.data.server.ServerService.ServerResult
|
||||
import ca.gosyer.ui.base.components.ErrorScreen
|
||||
import ca.gosyer.ui.base.components.LoadingScreen
|
||||
import ca.gosyer.ui.base.theme.AppTheme
|
||||
import ca.gosyer.util.system.userDataDir
|
||||
import com.github.zsoltk.compose.backpress.BackPressHandler
|
||||
import com.github.zsoltk.compose.backpress.LocalBackPressHandler
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import mu.KotlinLogging
|
||||
import org.apache.logging.log4j.core.config.Configurator
|
||||
import toothpick.configuration.Configuration
|
||||
import toothpick.ktp.KTP
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import toothpick.ktp.extension.getInstance
|
||||
import javax.swing.SwingUtilities
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
fun main() {
|
||||
val clazz = MainViewModel::class.java
|
||||
@@ -38,51 +33,6 @@ fun main() {
|
||||
clazz.classLoader,
|
||||
clazz.getResource("log4j2.xml")?.toURI()
|
||||
)
|
||||
val serverInitialized = MutableStateFlow(false)
|
||||
|
||||
GlobalScope.launch {
|
||||
val logger = KotlinLogging.logger("Server")
|
||||
val runtime = Runtime.getRuntime()
|
||||
|
||||
val jarFile = File(userDataDir,"Tachidesk.jar")
|
||||
if (!jarFile.exists()) {
|
||||
logger.info { "Copying server to resources" }
|
||||
javaClass.getResourceAsStream("/Tachidesk.jar")?.buffered()?.use { input ->
|
||||
jarFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val javaLibraryPath = System.getProperty("java.library.path").substringBefore(File.pathSeparator)
|
||||
val javaExeFile = File(javaLibraryPath, "java.exe")
|
||||
val javaUnixFile = File(javaLibraryPath, "java")
|
||||
val javaExePath = when {
|
||||
javaExeFile.exists() ->'"' + javaExeFile.absolutePath + '"'
|
||||
javaUnixFile.exists() -> '"' + javaUnixFile.absolutePath + '"'
|
||||
else -> "java"
|
||||
}
|
||||
|
||||
logger.info { "Starting server with $javaExePath" }
|
||||
val reader: BufferedReader
|
||||
val process = runtime.exec("""$javaExePath -jar "${jarFile.absolutePath}"""").also {
|
||||
reader = it.inputStream.bufferedReader()
|
||||
}
|
||||
runtime.addShutdownHook(thread(start = false) {
|
||||
process?.destroy()
|
||||
})
|
||||
logger.info { "Server started successfully" }
|
||||
var line: String?
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
if (!serverInitialized.value && line?.contains("Javalin started") == true) {
|
||||
serverInitialized.value = true
|
||||
}
|
||||
logger.info { line }
|
||||
}
|
||||
logger.info { "Server closed" }
|
||||
val exitVal = process.waitFor()
|
||||
logger.info { "Process exitValue: $exitVal" }
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
System.setProperty("kotlinx.coroutines.debug", "on")
|
||||
@@ -96,11 +46,13 @@ fun main() {
|
||||
}
|
||||
)
|
||||
|
||||
KTP.openRootScope()
|
||||
val scope = KTP.openRootScope()
|
||||
.installModules(
|
||||
DataModule
|
||||
)
|
||||
|
||||
val serverService = scope.getInstance<ServerService>()
|
||||
|
||||
SwingUtilities.invokeLater {
|
||||
val window = AppWindow(
|
||||
title = BuildConfig.NAME
|
||||
@@ -115,11 +67,13 @@ fun main() {
|
||||
CompositionLocalProvider(
|
||||
LocalBackPressHandler provides backPressHandler
|
||||
) {
|
||||
val initialized by serverInitialized.collectAsState()
|
||||
if (initialized) {
|
||||
val initialized by serverService.initialized.collectAsState()
|
||||
if (initialized == ServerResult.STARTED || initialized == ServerResult.UNUSED) {
|
||||
MainMenu()
|
||||
} else {
|
||||
} else if (initialized == ServerResult.STARTING) {
|
||||
LoadingScreen()
|
||||
} else if (initialized == ServerResult.FAILED) {
|
||||
ErrorScreen("Unable to start server")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +46,10 @@ import ca.gosyer.ui.base.components.LoadingScreen
|
||||
import ca.gosyer.ui.base.components.Toolbar
|
||||
import ca.gosyer.ui.base.components.mangaAspectRatio
|
||||
import ca.gosyer.ui.base.vm.viewModel
|
||||
import ca.gosyer.ui.main.Routing
|
||||
import ca.gosyer.ui.main.Route
|
||||
import ca.gosyer.util.compose.ThemedWindow
|
||||
import com.github.zsoltk.compose.router.BackStack
|
||||
import java.util.Date
|
||||
|
||||
fun openMangaMenu(mangaId: Long) {
|
||||
ThemedWindow("TachideskJUI") {
|
||||
@@ -57,7 +58,7 @@ fun openMangaMenu(mangaId: Long) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MangaMenu(mangaId: Long, backStack: BackStack<Routing>? = null) {
|
||||
fun MangaMenu(mangaId: Long, backStack: BackStack<Route>? = null) {
|
||||
val vm = viewModel<MangaMenuViewModel> {
|
||||
MangaMenuViewModel.Params(mangaId)
|
||||
}
|
||||
@@ -65,6 +66,7 @@ fun MangaMenu(mangaId: Long, backStack: BackStack<Routing>? = null) {
|
||||
val chapters by vm.chapters.collectAsState()
|
||||
val isLoading by vm.isLoading.collectAsState()
|
||||
val serverUrl by vm.serverUrl.collectAsState()
|
||||
val dateFormat by vm.dateFormat.collectAsState()
|
||||
|
||||
Column(Modifier.background(MaterialTheme.colors.background)) {
|
||||
Toolbar("Manga", backStack, backStack != null)
|
||||
@@ -86,7 +88,7 @@ fun MangaMenu(mangaId: Long, backStack: BackStack<Routing>? = null) {
|
||||
items(items) {
|
||||
when (it) {
|
||||
is MangaMenu.MangaMenuManga -> MangaItem(it.manga, serverUrl)
|
||||
is MangaMenu.MangaMenuChapter -> ChapterItem(it.chapter)
|
||||
is MangaMenu.MangaMenuChapter -> ChapterItem(it.chapter, dateFormat::format)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,20 +178,20 @@ private fun MangaInfo(manga: Manga, modifier: Modifier = Modifier) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChapterItem(chapter: Chapter) {
|
||||
Surface(modifier = Modifier.fillMaxWidth().height(70.dp).padding(4.dp), elevation = 2.dp) {
|
||||
fun ChapterItem(chapter: Chapter, format: (Date) -> String) {
|
||||
Surface(modifier = Modifier.fillMaxWidth().height(70.dp).padding(4.dp), elevation = 1.dp) {
|
||||
Column(Modifier.padding(4.dp)) {
|
||||
Text(chapter.name, fontSize = 20.sp)
|
||||
Text(chapter.name, fontSize = 20.sp, maxLines = 1)
|
||||
val description = mutableListOf<String>()
|
||||
if (chapter.dateUpload != 0L) {
|
||||
description += chapter.dateUpload.toString()
|
||||
description += format(Date(chapter.dateUpload))
|
||||
}
|
||||
if (!chapter.scanlator.isNullOrEmpty()) {
|
||||
description += chapter.scanlator
|
||||
}
|
||||
if (description.isNotEmpty()) {
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(description.joinToString(" - "))
|
||||
Text(description.joinToString(" - "), maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,24 @@ import ca.gosyer.data.server.ServerPreferences
|
||||
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
|
||||
import ca.gosyer.data.server.interactions.LibraryInteractionHandler
|
||||
import ca.gosyer.data.server.interactions.MangaInteractionHandler
|
||||
import ca.gosyer.data.ui.UiPreferences
|
||||
import ca.gosyer.ui.base.vm.ViewModel
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
class MangaMenuViewModel @Inject constructor(
|
||||
private val params: Params,
|
||||
private val uiPreferences: UiPreferences,
|
||||
private val mangaHandler: MangaInteractionHandler,
|
||||
private val chapterHandler: ChapterInteractionHandler,
|
||||
private val libraryHandler: LibraryInteractionHandler,
|
||||
@@ -40,6 +46,12 @@ class MangaMenuViewModel @Inject constructor(
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
val dateFormat = uiPreferences.dateFormat().changes()
|
||||
.map {
|
||||
getDateFormat(it)
|
||||
}
|
||||
.asStateFlow(getDateFormat(uiPreferences.dateFormat().get()))
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
refreshMangaAsync(params.mangaId).await() to refreshChaptersAsync(params.mangaId).await()
|
||||
@@ -82,5 +94,10 @@ class MangaMenuViewModel @Inject constructor(
|
||||
|
||||
}
|
||||
|
||||
private fun getDateFormat(format: String): DateFormat = when (format) {
|
||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
||||
}
|
||||
|
||||
data class Params(val mangaId: Long)
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/main/kotlin/ca/gosyer/ui/settings/SettingsGeneralScreen.kt
Normal file
100
src/main/kotlin/ca/gosyer/ui/settings/SettingsGeneralScreen.kt
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/main/kotlin/ca/gosyer/ui/settings/SettingsScreen.kt
Normal file
94
src/main/kotlin/ca/gosyer/ui/settings/SettingsScreen.kt
Normal 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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -33,6 +32,7 @@ import ca.gosyer.ui.manga.openMangaMenu
|
||||
import ca.gosyer.ui.sources.components.SourceHomeScreen
|
||||
import ca.gosyer.ui.sources.components.SourceScreen
|
||||
import ca.gosyer.util.compose.ThemedWindow
|
||||
import com.github.zsoltk.compose.savedinstancestate.Bundle
|
||||
import com.github.zsoltk.compose.savedinstancestate.BundleScope
|
||||
|
||||
fun openSourcesMenu() {
|
||||
@@ -43,9 +43,20 @@ fun openSourcesMenu() {
|
||||
}
|
||||
}
|
||||
|
||||
private const val SOURCE_MENU_KEY = "source_menu"
|
||||
|
||||
@Composable
|
||||
fun SourcesMenu(onMangaClick: (Long) -> Unit) {
|
||||
val vm = viewModel<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 sources by vm.sources.collectAsState()
|
||||
val sourceTabs by vm.sourceTabs.collectAsState()
|
||||
@@ -56,27 +67,28 @@ fun SourcesMenu(onMangaClick: (Long) -> Unit) {
|
||||
Column {
|
||||
Toolbar(selectedSourceTab?.name ?: "Sources", closable = false)
|
||||
Row {
|
||||
Surface(elevation = 1.dp) {
|
||||
LazyColumn(Modifier.fillMaxHeight().width(64.dp)) {
|
||||
items(sourceTabs) { source ->
|
||||
Card(
|
||||
Modifier
|
||||
val modifier = Modifier
|
||||
.clickable {
|
||||
vm.selectTab(source)
|
||||
}
|
||||
.requiredHeight(64.dp)
|
||||
.requiredWidth(64.dp),
|
||||
) {
|
||||
.requiredWidth(64.dp)
|
||||
|
||||
if (source != null) {
|
||||
KtorImage(source.iconUrl(serverUrl),)
|
||||
KtorImage(source.iconUrl(serverUrl), imageModifier = modifier)
|
||||
} else {
|
||||
Icon(Icons.Default.Home, "Home")
|
||||
Icon(Icons.Default.Home, "Home", modifier = modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val selectedSource: Source? = selectedSourceTab
|
||||
BundleScope(selectedSource?.name ?: "home") {
|
||||
BundleScope("Sources") {
|
||||
if (selectedSource != null) {
|
||||
SourceScreen(selectedSource, onMangaClick)
|
||||
} else {
|
||||
|
||||
@@ -11,14 +11,19 @@ import ca.gosyer.data.models.Source
|
||||
import ca.gosyer.data.server.ServerPreferences
|
||||
import ca.gosyer.data.server.interactions.SourceInteractionHandler
|
||||
import ca.gosyer.ui.base.vm.ViewModel
|
||||
import com.github.zsoltk.compose.savedinstancestate.Bundle
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import mu.KotlinLogging
|
||||
import javax.inject.Inject
|
||||
|
||||
class SourcesMenuViewModel @Inject constructor(
|
||||
private val bundle: Bundle,
|
||||
private val sourceHandler: SourceInteractionHandler,
|
||||
serverPreferences: ServerPreferences,
|
||||
catalogPreferences: CatalogPreferences
|
||||
@@ -42,6 +47,22 @@ class SourcesMenuViewModel @Inject constructor(
|
||||
val selectedSourceTab = _selectedSourceTab.asStateFlow()
|
||||
|
||||
init {
|
||||
_sourceTabs.drop(1)
|
||||
.onEach { sources ->
|
||||
bundle.putLongArray(SOURCE_TABS_KEY, sources.mapNotNull { it?.id }.toLongArray())
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
_selectedSourceTab.drop(1)
|
||||
.onEach {
|
||||
if (it != null) {
|
||||
bundle.putLong(SELECTED_SOURCE_TAB, it.id)
|
||||
} else {
|
||||
bundle.remove(SELECTED_SOURCE_TAB)
|
||||
}
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
||||
getSources()
|
||||
}
|
||||
|
||||
@@ -55,6 +76,18 @@ class SourcesMenuViewModel @Inject constructor(
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
} finally {
|
||||
val sourceTabs = bundle.getLongArray(SOURCE_TABS_KEY)
|
||||
if (sourceTabs != null) {
|
||||
_sourceTabs.value = listOf(null) + sourceTabs.toList()
|
||||
.mapNotNull { sourceId ->
|
||||
_sources.value.find { it.id == sourceId }
|
||||
}
|
||||
_selectedSourceTab.value = bundle.getLong(SELECTED_SOURCE_TAB, -1).let { id ->
|
||||
if (id != -1L) {
|
||||
_sources.value.find { it.id == id }
|
||||
} else null
|
||||
}
|
||||
}
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
@@ -77,4 +110,9 @@ class SourcesMenuViewModel @Inject constructor(
|
||||
_selectedSourceTab.value = null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SOURCE_TABS_KEY = "source_tabs"
|
||||
const val SELECTED_SOURCE_TAB = "selected_tab"
|
||||
}
|
||||
}
|
||||
@@ -26,15 +26,23 @@ import ca.gosyer.data.models.Source
|
||||
import ca.gosyer.ui.base.components.LoadingScreen
|
||||
import ca.gosyer.ui.base.components.MangaGridItem
|
||||
import ca.gosyer.ui.base.vm.viewModel
|
||||
import com.github.zsoltk.compose.savedinstancestate.Bundle
|
||||
import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState
|
||||
|
||||
@Composable
|
||||
fun SourceScreen(
|
||||
source: Source,
|
||||
onMangaClick: (Long) -> Unit
|
||||
) {
|
||||
val upstream = LocalSavedInstanceState.current
|
||||
|
||||
val vm = viewModel<SourceScreenViewModel>()
|
||||
remember(source) {
|
||||
vm.init(source)
|
||||
val bundle = remember(source.id) {
|
||||
upstream.getBundle(source.id.toString())
|
||||
?: Bundle().also { upstream.putBundle(source.id.toString(), it) }
|
||||
}
|
||||
remember(source.id) {
|
||||
vm.init(source, bundle)
|
||||
}
|
||||
val mangas by vm.mangas.collectAsState()
|
||||
val hasNextPage by vm.hasNextPage.collectAsState()
|
||||
|
||||
@@ -10,18 +10,27 @@ import ca.gosyer.data.models.Manga
|
||||
import ca.gosyer.data.models.MangaPage
|
||||
import ca.gosyer.data.models.Source
|
||||
import ca.gosyer.data.server.ServerPreferences
|
||||
import ca.gosyer.data.server.interactions.MangaInteractionHandler
|
||||
import ca.gosyer.data.server.interactions.SourceInteractionHandler
|
||||
import ca.gosyer.ui.base.vm.ViewModel
|
||||
import ca.gosyer.util.compose.getJsonObjectArray
|
||||
import ca.gosyer.util.compose.putJsonObjectArray
|
||||
import com.github.zsoltk.compose.savedinstancestate.Bundle
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class SourceScreenViewModel @Inject constructor(
|
||||
private val sourceHandler: SourceInteractionHandler,
|
||||
private val mangaHandler: MangaInteractionHandler,
|
||||
serverPreferences: ServerPreferences
|
||||
): ViewModel() {
|
||||
private lateinit var source: Source
|
||||
private lateinit var bundle: Bundle
|
||||
|
||||
val serverUrl = serverPreferences.server().stateIn(scope)
|
||||
|
||||
@@ -40,15 +49,38 @@ class SourceScreenViewModel @Inject constructor(
|
||||
private val _pageNum = MutableStateFlow(1)
|
||||
val pageNum = _pageNum.asStateFlow()
|
||||
|
||||
fun init(source: Source) {
|
||||
init {
|
||||
_mangas.drop(1)
|
||||
.onEach { manga ->
|
||||
bundle.putJsonObjectArray(MANGAS_KEY, manga)
|
||||
}
|
||||
.launchIn(scope)
|
||||
_hasNextPage.drop(1)
|
||||
.onEach {
|
||||
bundle.putBoolean(NEXT_PAGE_KEY, it)
|
||||
}
|
||||
.launchIn(scope)
|
||||
_pageNum.drop(1)
|
||||
.onEach {
|
||||
bundle.putInt(PAGE_NUM_KEY, it)
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun init(source: Source, bundle: Bundle) {
|
||||
this.source = source
|
||||
this.bundle = bundle
|
||||
scope.launch {
|
||||
_loading.value = true
|
||||
_mangas.value = emptyList()
|
||||
_hasNextPage.value = false
|
||||
_pageNum.value = 1
|
||||
_isLatest.value = source.supportsLatest
|
||||
val page = getPage()
|
||||
_pageNum.value = bundle.getInt(PAGE_NUM_KEY, 1)
|
||||
_isLatest.value = bundle.getBoolean(IS_LATEST_KEY, source.supportsLatest)
|
||||
val page = bundle.getJsonObjectArray<Manga>(MANGAS_KEY)
|
||||
?.let {
|
||||
MangaPage(it.filterNotNull(), bundle.getBoolean(NEXT_PAGE_KEY, true))
|
||||
}
|
||||
?: getPage()
|
||||
_mangas.value += page.mangaList
|
||||
_hasNextPage.value = page.hasNextPage
|
||||
_loading.value = false
|
||||
@@ -69,7 +101,11 @@ class SourceScreenViewModel @Inject constructor(
|
||||
fun setMode(toLatest: Boolean) {
|
||||
if (isLatest.value != toLatest){
|
||||
_isLatest.value = toLatest
|
||||
init(source)
|
||||
bundle.remove(MANGAS_KEY)
|
||||
bundle.remove(NEXT_PAGE_KEY)
|
||||
bundle.remove(PAGE_NUM_KEY)
|
||||
bundle.remove(IS_LATEST_KEY)
|
||||
init(source, bundle)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,4 +116,11 @@ class SourceScreenViewModel @Inject constructor(
|
||||
sourceHandler.getPopularManga(source, pageNum.value)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MANGAS_KEY = "mangas"
|
||||
const val NEXT_PAGE_KEY = "next_page"
|
||||
const val PAGE_NUM_KEY = "next_page"
|
||||
const val IS_LATEST_KEY = "is_latest"
|
||||
}
|
||||
}
|
||||
28
src/main/kotlin/ca/gosyer/util/compose/Bundle.kt
Normal file
28
src/main/kotlin/ca/gosyer/util/compose/Bundle.kt
Normal 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) }
|
||||
}
|
||||
Reference in New Issue
Block a user