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.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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
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.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> {
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ class PreferenceMutableStateFlow<T>(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
preference.changes()
|
preference.changes()
|
||||||
.onEach { value = it }
|
.onEach { state.value = it }
|
||||||
.launchIn(scope)
|
.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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.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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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