More updates

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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