Phase 5a: server settings UI — BookshelfSettings screen, model, navigation, strings
This commit is contained in:
parent
2f9431f6e6
commit
0c2dfbd592
8 changed files with 264 additions and 2 deletions
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2026 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dueattendant149.bookshelf.presentation.settings
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.dueattendant149.bookshelf.data.settings.ServerSettings
|
||||||
|
import org.dueattendant149.bookshelf.domain.model.remote.RemoteLibrary
|
||||||
|
import org.dueattendant149.bookshelf.domain.use_case.remote.FetchLibrariesUseCase
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class BookshelfSettingsState(
|
||||||
|
val bookshelfUrl: String = "",
|
||||||
|
val absUrl: String = "",
|
||||||
|
val absToken: String = "",
|
||||||
|
val libraries: List<RemoteLibrary> = emptyList(),
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class BookshelfSettingsModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val serverSettings: ServerSettings,
|
||||||
|
private val fetchLibrariesUseCase: FetchLibrariesUseCase,
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _state = MutableStateFlow(BookshelfSettingsState())
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
bookshelfUrl = serverSettings.getBookshelfUrl() ?: "",
|
||||||
|
absUrl = serverSettings.getAbsUrl() ?: "",
|
||||||
|
absToken = serverSettings.getAbsToken() ?: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateBookshelfUrl(value: String) = _state.update { it.copy(bookshelfUrl = value) }
|
||||||
|
fun updateAbsUrl(value: String) = _state.update { it.copy(absUrl = value) }
|
||||||
|
fun updateAbsToken(value: String) = _state.update { it.copy(absToken = value) }
|
||||||
|
|
||||||
|
fun save() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
serverSettings.setBookshelfUrl(_state.value.bookshelfUrl)
|
||||||
|
serverSettings.setAbsUrl(_state.value.absUrl)
|
||||||
|
serverSettings.setAbsToken(_state.value.absToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testLibraries() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
save()
|
||||||
|
val result = fetchLibrariesUseCase()
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
libraries = result.getOrDefault(emptyList()),
|
||||||
|
error = result.exceptionOrNull()?.message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2026 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dueattendant149.bookshelf.presentation.settings
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.dueattendant149.bookshelf.presentation.navigator.Screen
|
||||||
|
import org.dueattendant149.bookshelf.ui.common.components.top_bar.collapsibleTopAppBarScrollBehavior
|
||||||
|
import org.dueattendant149.bookshelf.ui.navigator.LocalNavigator
|
||||||
|
import org.dueattendant149.bookshelf.ui.settings.bookshelf.BookshelfSettingsContent
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object BookshelfSettingsScreen : Screen, Parcelable {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val navigator = LocalNavigator.current
|
||||||
|
val model = hiltViewModel<BookshelfSettingsModel>()
|
||||||
|
val (scrollBehavior, listState) = TopAppBarDefaults.collapsibleTopAppBarScrollBehavior()
|
||||||
|
|
||||||
|
BookshelfSettingsContent(
|
||||||
|
model = model,
|
||||||
|
listState = listState,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigateBack = {
|
||||||
|
navigator.pop()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.dueattendant149.bookshelf.presentation.navigator.Screen
|
import org.dueattendant149.bookshelf.presentation.navigator.Screen
|
||||||
|
import org.dueattendant149.bookshelf.presentation.settings.BookshelfSettingsScreen
|
||||||
import org.dueattendant149.bookshelf.presentation.start.StartScreen
|
import org.dueattendant149.bookshelf.presentation.start.StartScreen
|
||||||
import org.dueattendant149.bookshelf.ui.common.components.top_bar.collapsibleTopAppBarScrollBehavior
|
import org.dueattendant149.bookshelf.ui.common.components.top_bar.collapsibleTopAppBarScrollBehavior
|
||||||
import org.dueattendant149.bookshelf.ui.common.helpers.LocalSettings
|
import org.dueattendant149.bookshelf.ui.common.helpers.LocalSettings
|
||||||
|
|
@ -46,6 +47,9 @@ object SettingsScreen : Screen, Parcelable {
|
||||||
navigateToBrowseSettings = {
|
navigateToBrowseSettings = {
|
||||||
navigator.push(BrowseSettingsScreen)
|
navigator.push(BrowseSettingsScreen)
|
||||||
},
|
},
|
||||||
|
navigateToBookshelfSettings = {
|
||||||
|
navigator.push(BookshelfSettingsScreen)
|
||||||
|
},
|
||||||
navigateToStart = {
|
navigateToStart = {
|
||||||
settings.showStartScreen.update(true)
|
settings.showStartScreen.update(true)
|
||||||
navigator.push(StartScreen, saveInBackStack = false)
|
navigator.push(StartScreen, saveInBackStack = false)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ fun SettingsContent(
|
||||||
navigateToReaderSettings: () -> Unit,
|
navigateToReaderSettings: () -> Unit,
|
||||||
navigateToLibrarySettings: () -> Unit,
|
navigateToLibrarySettings: () -> Unit,
|
||||||
navigateToBrowseSettings: () -> Unit,
|
navigateToBrowseSettings: () -> Unit,
|
||||||
|
navigateToBookshelfSettings: () -> Unit,
|
||||||
navigateToStart: () -> Unit,
|
navigateToStart: () -> Unit,
|
||||||
navigateBack: () -> Unit
|
navigateBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
@ -32,6 +33,7 @@ fun SettingsContent(
|
||||||
navigateToReaderSettings = navigateToReaderSettings,
|
navigateToReaderSettings = navigateToReaderSettings,
|
||||||
navigateToLibrarySettings = navigateToLibrarySettings,
|
navigateToLibrarySettings = navigateToLibrarySettings,
|
||||||
navigateToBrowseSettings = navigateToBrowseSettings,
|
navigateToBrowseSettings = navigateToBrowseSettings,
|
||||||
|
navigateToBookshelfSettings = navigateToBookshelfSettings,
|
||||||
navigateToStart = navigateToStart,
|
navigateToStart = navigateToStart,
|
||||||
navigateBack = navigateBack
|
navigateBack = navigateBack
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Cloud
|
||||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||||
import androidx.compose.material.icons.outlined.DisplaySettings
|
import androidx.compose.material.icons.outlined.DisplaySettings
|
||||||
import androidx.compose.material.icons.outlined.Explore
|
import androidx.compose.material.icons.outlined.Explore
|
||||||
|
|
@ -31,7 +32,8 @@ fun SettingsLayout(
|
||||||
navigateToAppearanceSettings: () -> Unit,
|
navigateToAppearanceSettings: () -> Unit,
|
||||||
navigateToReaderSettings: () -> Unit,
|
navigateToReaderSettings: () -> Unit,
|
||||||
navigateToLibrarySettings: () -> Unit,
|
navigateToLibrarySettings: () -> Unit,
|
||||||
navigateToBrowseSettings: () -> Unit
|
navigateToBrowseSettings: () -> Unit,
|
||||||
|
navigateToBookshelfSettings: () -> Unit
|
||||||
) {
|
) {
|
||||||
LazyColumnWithScrollbar(
|
LazyColumnWithScrollbar(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -94,5 +96,16 @@ fun SettingsLayout(
|
||||||
navigateToBrowseSettings()
|
navigateToBrowseSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
SettingsLayoutItem(
|
||||||
|
index = 5,
|
||||||
|
icon = Icons.Outlined.Cloud,
|
||||||
|
title = stringResource(id = R.string.bookshelf_settings),
|
||||||
|
description = stringResource(id = R.string.bookshelf_settings_desc)
|
||||||
|
) {
|
||||||
|
navigateToBookshelfSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -29,6 +29,7 @@ fun SettingsScaffold(
|
||||||
navigateToReaderSettings: () -> Unit,
|
navigateToReaderSettings: () -> Unit,
|
||||||
navigateToLibrarySettings: () -> Unit,
|
navigateToLibrarySettings: () -> Unit,
|
||||||
navigateToBrowseSettings: () -> Unit,
|
navigateToBrowseSettings: () -> Unit,
|
||||||
|
navigateToBookshelfSettings: () -> Unit,
|
||||||
navigateToStart: () -> Unit,
|
navigateToStart: () -> Unit,
|
||||||
navigateBack: () -> Unit
|
navigateBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
@ -53,7 +54,8 @@ fun SettingsScaffold(
|
||||||
navigateToAppearanceSettings = navigateToAppearanceSettings,
|
navigateToAppearanceSettings = navigateToAppearanceSettings,
|
||||||
navigateToReaderSettings = navigateToReaderSettings,
|
navigateToReaderSettings = navigateToReaderSettings,
|
||||||
navigateToLibrarySettings = navigateToLibrarySettings,
|
navigateToLibrarySettings = navigateToLibrarySettings,
|
||||||
navigateToBrowseSettings = navigateToBrowseSettings
|
navigateToBrowseSettings = navigateToBrowseSettings,
|
||||||
|
navigateToBookshelfSettings = navigateToBookshelfSettings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* Book's Story — free and open-source Material You eBook reader.
|
||||||
|
* Copyright (C) 2024-2026 Acclorite
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dueattendant149.bookshelf.ui.settings.bookshelf
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import org.dueattendant149.bookshelf.presentation.settings.BookshelfSettingsModel
|
||||||
|
import org.dueattendant149.bookshelf.ui.navigator.NavigatorBackIconButton
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun BookshelfSettingsContent(
|
||||||
|
model: BookshelfSettingsModel,
|
||||||
|
listState: LazyListState,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
val state by model.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Bookshelf server") },
|
||||||
|
navigationIcon = {
|
||||||
|
NavigatorBackIconButton(navigateBack = navigateBack)
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.bookshelfUrl,
|
||||||
|
onValueChange = model::updateBookshelfUrl,
|
||||||
|
label = { Text("Bookshelf API URL") },
|
||||||
|
placeholder = { Text("http://bookshelf-api:8073") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.absUrl,
|
||||||
|
onValueChange = model::updateAbsUrl,
|
||||||
|
label = { Text("Audiobookshelf URL") },
|
||||||
|
placeholder = { Text("http://192.168.1.119:13378") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.absToken,
|
||||||
|
onValueChange = model::updateAbsToken,
|
||||||
|
label = { Text("ABS token") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Button(
|
||||||
|
onClick = model::testLibraries,
|
||||||
|
enabled = !state.isLoading,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
} else {
|
||||||
|
Text("Fetch libraries")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.error?.let { error ->
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = error,
|
||||||
|
color = androidx.compose.material3.MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(state.libraries.size) { index ->
|
||||||
|
val library = state.libraries[index]
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(library.name) },
|
||||||
|
supportingContent = { Text("${library.mediaType} • ${library.itemCount} items") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -134,6 +134,8 @@
|
||||||
<string name="browse_settings_desc">Scan, display</string>
|
<string name="browse_settings_desc">Scan, display</string>
|
||||||
<string name="library_settings">Library</string>
|
<string name="library_settings">Library</string>
|
||||||
<string name="library_settings_desc">Categories, display, tabs</string>
|
<string name="library_settings_desc">Categories, display, tabs</string>
|
||||||
|
<string name="bookshelf_settings">Bookshelf</string>
|
||||||
|
<string name="bookshelf_settings_desc">Server, ABS, TTS</string>
|
||||||
|
|
||||||
<!-- Settings subcategories -->
|
<!-- Settings subcategories -->
|
||||||
<string name="font_reader_settings">Font</string>
|
<string name="font_reader_settings">Font</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue