Phase 5a: server settings UI — BookshelfSettings screen, model, navigation, strings

This commit is contained in:
Atte149 2026-06-16 09:45:17 +03:00
parent 2f9431f6e6
commit 0c2dfbd592
8 changed files with 264 additions and 2 deletions

View file

@ -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,
)
}
}
}
}

View file

@ -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()
}
)
}
}

View file

@ -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)

View file

@ -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
) )

View file

@ -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()
}
}
} }
} }

View file

@ -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
) )
} }
} }

View file

@ -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") }
)
}
}
}
}

View file

@ -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>