feat(bookshelf): add ServerSettingsScreen for bookshelf-api/ABS config
- Create ServerSettingsViewModel (Hilt) backed by SharedPreferences ServerSettings. - Create ServerSettingsScreen Compose UI with URL/token fields and Save button. - Add SERVER_SETTINGS_ROUTE to AppDestinations and wire in AppNavigation. - Add settings icon in BookshelfLibraryScreen top bar to open settings. - ':app:assembleOssDebug' passes.
This commit is contained in:
parent
d999e9df77
commit
1666ae0fe2
4 changed files with 194 additions and 0 deletions
|
|
@ -83,6 +83,7 @@ object AppDestinations {
|
||||||
const val AI_SETTINGS_SCREEN_ROUTE = "ai_settings_screen_route"
|
const val AI_SETTINGS_SCREEN_ROUTE = "ai_settings_screen_route"
|
||||||
const val SETTINGS_SCREEN_ROUTE = "settings_screen_route"
|
const val SETTINGS_SCREEN_ROUTE = "settings_screen_route"
|
||||||
const val BOOKSHELF_LIBRARY_ROUTE = "bookshelf_library"
|
const val BOOKSHELF_LIBRARY_ROUTE = "bookshelf_library"
|
||||||
|
const val SERVER_SETTINGS_ROUTE = "server_settings"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shouldInterceptAppNavBack(
|
fun shouldInterceptAppNavBack(
|
||||||
|
|
@ -438,9 +439,20 @@ fun AppNavigation(
|
||||||
viewModel = bookshelfViewModel,
|
viewModel = bookshelfViewModel,
|
||||||
onItemClick = { item ->
|
onItemClick = { item ->
|
||||||
Timber.d("Bookshelf item selected: ${item.id}")
|
Timber.d("Bookshelf item selected: ${item.id}")
|
||||||
|
},
|
||||||
|
onOpenSettings = {
|
||||||
|
navController.navigateIfReady(AppDestinations.SERVER_SETTINGS_ROUTE)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(route = AppDestinations.SERVER_SETTINGS_ROUTE) {
|
||||||
|
val settingsViewModel: org.dueattendant149.bookreader.bookshelf.ServerSettingsViewModel = hiltViewModel()
|
||||||
|
org.dueattendant149.bookreader.bookshelf.ServerSettingsScreen(
|
||||||
|
viewModel = settingsViewModel,
|
||||||
|
onBackClick = { navController.popBackStackIfReady() }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
package org.dueattendant149.bookreader.bookshelf
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ServerSettingsScreen(
|
||||||
|
viewModel: ServerSettingsViewModel,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
var bookshelfUrl by remember(uiState.bookshelfUrl) { androidx.compose.runtime.mutableStateOf(uiState.bookshelfUrl) }
|
||||||
|
var absUrl by remember(uiState.absUrl) { androidx.compose.runtime.mutableStateOf(uiState.absUrl) }
|
||||||
|
var absToken by remember(uiState.absToken) { androidx.compose.runtime.mutableStateOf(uiState.absToken) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Server settings") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBackClick) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text("Bookshelf API")
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = bookshelfUrl,
|
||||||
|
onValueChange = {
|
||||||
|
bookshelfUrl = it
|
||||||
|
viewModel.updateBookshelfUrl(it)
|
||||||
|
},
|
||||||
|
label = { Text("https://books.dueattendant149.org/") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Text("Audiobookshelf")
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = absUrl,
|
||||||
|
onValueChange = {
|
||||||
|
absUrl = it
|
||||||
|
viewModel.updateAbsUrl(it)
|
||||||
|
},
|
||||||
|
label = { Text("https://abs.dueattendant149.org/") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = absToken,
|
||||||
|
onValueChange = {
|
||||||
|
absToken = it
|
||||||
|
viewModel.updateAbsToken(it)
|
||||||
|
},
|
||||||
|
label = { Text("Bearer token") },
|
||||||
|
singleLine = true,
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = viewModel::save,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.saved) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text("Saved")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package org.dueattendant149.bookreader.bookshelf
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.dueattendant149.bookreader.data.settings.ServerSettings
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class ServerSettingsUiState(
|
||||||
|
val bookshelfUrl: String = "",
|
||||||
|
val absUrl: String = "",
|
||||||
|
val absToken: String = "",
|
||||||
|
val saved: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ServerSettingsViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val serverSettings: ServerSettings,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(ServerSettingsUiState())
|
||||||
|
val uiState: StateFlow<ServerSettingsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
_uiState.value = ServerSettingsUiState(
|
||||||
|
bookshelfUrl = serverSettings.getBookshelfUrl() ?: "",
|
||||||
|
absUrl = serverSettings.getAbsUrl() ?: "",
|
||||||
|
absToken = serverSettings.getAbsToken() ?: "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateBookshelfUrl(url: String) {
|
||||||
|
_uiState.value = _uiState.value.copy(bookshelfUrl = url, saved = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAbsUrl(url: String) {
|
||||||
|
_uiState.value = _uiState.value.copy(absUrl = url, saved = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAbsToken(token: String) {
|
||||||
|
_uiState.value = _uiState.value.copy(absToken = token, saved = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
serverSettings.setBookshelfUrl(_uiState.value.bookshelfUrl.trim())
|
||||||
|
serverSettings.setAbsUrl(_uiState.value.absUrl.trim())
|
||||||
|
serverSettings.setAbsToken(_uiState.value.absToken.trim())
|
||||||
|
_uiState.value = _uiState.value.copy(saved = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,9 +8,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
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.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
|
@ -33,6 +37,7 @@ import org.dueattendant149.bookreader.data.remote.bookshelfapi.model.UnifiedItem
|
||||||
fun BookshelfLibraryScreen(
|
fun BookshelfLibraryScreen(
|
||||||
viewModel: BookshelfViewModel,
|
viewModel: BookshelfViewModel,
|
||||||
onItemClick: (UnifiedItemResponse) -> Unit,
|
onItemClick: (UnifiedItemResponse) -> Unit,
|
||||||
|
onOpenSettings: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
|
@ -46,6 +51,11 @@ fun BookshelfLibraryScreen(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("Server Library") },
|
title = { Text("Server Library") },
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onOpenSettings) {
|
||||||
|
Icon(Icons.Default.Settings, contentDescription = "Server settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue