feat(ui-polish): improve RemoteLibrary & BookshelfSettings UI, add icons/strings
This commit is contained in:
parent
1ed35239fd
commit
23298b0f26
7 changed files with 302 additions and 85 deletions
|
|
@ -113,8 +113,8 @@ class MainActivity : AppCompatActivity() {
|
|||
screen = RemoteLibraryScreen,
|
||||
title = R.string.bookshelf_screen,
|
||||
tooltip = R.string.bookshelf_content_desc,
|
||||
selectedIcon = R.drawable.library_screen_filled,
|
||||
unselectedIcon = R.drawable.library_screen_outlined
|
||||
selectedIcon = R.drawable.bookshelf_screen_filled,
|
||||
unselectedIcon = R.drawable.bookshelf_screen_outlined
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ data class BookshelfSettingsState(
|
|||
val libraries: List<RemoteLibrary> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val librariesSuccess: Boolean = false,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -53,28 +54,58 @@ class BookshelfSettingsModel
|
|||
}
|
||||
}
|
||||
|
||||
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 updateBookshelfUrl(value: String) = _state.update {
|
||||
it.copy(
|
||||
bookshelfUrl = value,
|
||||
librariesSuccess = false,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun updateAbsUrl(value: String) = _state.update {
|
||||
it.copy(
|
||||
absUrl = value,
|
||||
librariesSuccess = false,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun updateAbsToken(value: String) = _state.update {
|
||||
it.copy(
|
||||
absToken = value,
|
||||
librariesSuccess = false,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun save() {
|
||||
viewModelScope.launch {
|
||||
serverSettings.setBookshelfUrl(_state.value.bookshelfUrl)
|
||||
serverSettings.setAbsUrl(_state.value.absUrl)
|
||||
serverSettings.setAbsToken(_state.value.absToken)
|
||||
val current = _state.value
|
||||
serverSettings.setBookshelfUrl(current.bookshelfUrl)
|
||||
serverSettings.setAbsUrl(current.absUrl)
|
||||
serverSettings.setAbsToken(current.absToken)
|
||||
}
|
||||
}
|
||||
|
||||
fun testLibraries() {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = true,
|
||||
error = null,
|
||||
librariesSuccess = false,
|
||||
)
|
||||
}
|
||||
save()
|
||||
|
||||
val result = fetchLibrariesUseCase()
|
||||
val libraries = result.getOrDefault(emptyList())
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
libraries = result.getOrDefault(emptyList()),
|
||||
libraries = libraries,
|
||||
error = result.exceptionOrNull()?.message,
|
||||
librariesSuccess = result.isSuccess && libraries.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,69 +7,77 @@
|
|||
package org.dueattendant149.bookshelf.ui.bookshelf
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CloudOff
|
||||
import androidx.compose.material.icons.filled.CloudQueue
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.dueattendant149.bookshelf.R
|
||||
import org.dueattendant149.bookshelf.presentation.bookshelf.RemoteLibraryModel
|
||||
import org.dueattendant149.bookshelf.ui.common.components.placeholder.EmptyPlaceholder
|
||||
import org.dueattendant149.bookshelf.ui.common.components.placeholder.ErrorPlaceholder
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RemoteLibraryContent(model: RemoteLibraryModel) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val refreshState = rememberPullRefreshState(
|
||||
refreshing = state.isLoading,
|
||||
onRefresh = model::loadLibraries
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Bookshelf") }
|
||||
title = { Text(stringResource(R.string.bookshelf_screen)) }
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
.pullRefresh(refreshState)
|
||||
.padding(padding)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item {
|
||||
Button(
|
||||
onClick = model::loadLibraries,
|
||||
enabled = !state.isLoading,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Refresh libraries")
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isLoading) {
|
||||
item {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
state.error?.let { error ->
|
||||
item {
|
||||
Text(
|
||||
text = error,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
text = stringResource(R.string.bookshelf_libraries_header),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(state.libraries.size) { index ->
|
||||
val library = state.libraries[index]
|
||||
|
|
@ -78,15 +86,43 @@ fun RemoteLibraryContent(model: RemoteLibraryModel) {
|
|||
onClick = { model.selectLibrary(library) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = library.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = "${library.mediaType} • ${library.itemCount} items",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 16.dp)
|
||||
text = stringResource(
|
||||
R.string.bookshelf_library_subtitle,
|
||||
library.mediaType,
|
||||
library.itemCount
|
||||
)
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
if (selected) {
|
||||
Text(
|
||||
text = stringResource(R.string.bookshelf_selected_label),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.selectedLibrary?.let { library ->
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.bookshelf_books_header, library.name),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -96,18 +132,64 @@ fun RemoteLibraryContent(model: RemoteLibraryModel) {
|
|||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = book.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = book.author.getAsString() ?: "",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 16.dp)
|
||||
text = book.author.getAsString()
|
||||
?: stringResource(R.string.unknown_author)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isLoading && state.libraries.isEmpty() && state.books.isEmpty()) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.isLoading && state.libraries.isEmpty() && state.error == null) {
|
||||
EmptyPlaceholder(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
message = stringResource(R.string.bookshelf_empty_message),
|
||||
icon = rememberVectorPainter(Icons.Default.CloudQueue),
|
||||
actionTitle = stringResource(R.string.bookshelf_refresh_action),
|
||||
action = model::loadLibraries
|
||||
)
|
||||
}
|
||||
|
||||
state.error?.let { error ->
|
||||
ErrorPlaceholder(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
errorMessage = error,
|
||||
icon = rememberVectorPainter(Icons.Default.CloudOff),
|
||||
actionTitle = stringResource(R.string.bookshelf_retry_action),
|
||||
action = model::loadLibraries
|
||||
)
|
||||
}
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = state.isLoading,
|
||||
state = refreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
backgroundColor = MaterialTheme.colorScheme.inverseSurface,
|
||||
contentColor = MaterialTheme.colorScheme.inverseOnSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,15 +8,21 @@ package org.dueattendant149.bookshelf.ui.settings.bookshelf
|
|||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -24,9 +30,12 @@ import androidx.compose.material3.TopAppBar
|
|||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.dueattendant149.bookshelf.R
|
||||
import org.dueattendant149.bookshelf.presentation.settings.BookshelfSettingsModel
|
||||
import org.dueattendant149.bookshelf.ui.navigator.NavigatorBackIconButton
|
||||
|
||||
|
|
@ -43,7 +52,7 @@ fun BookshelfSettingsContent(
|
|||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Bookshelf server") },
|
||||
title = { Text(stringResource(R.string.bookshelf_settings_screen_title)) },
|
||||
navigationIcon = {
|
||||
NavigatorBackIconButton(navigateBack = navigateBack)
|
||||
},
|
||||
|
|
@ -63,8 +72,9 @@ fun BookshelfSettingsContent(
|
|||
OutlinedTextField(
|
||||
value = state.bookshelfUrl,
|
||||
onValueChange = model::updateBookshelfUrl,
|
||||
label = { Text("Bookshelf API URL") },
|
||||
label = { Text(stringResource(R.string.bookshelf_url_option)) },
|
||||
placeholder = { Text("http://bookshelf-api:8073") },
|
||||
supportingText = { Text(stringResource(R.string.bookshelf_url_option_desc)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
|
@ -72,8 +82,9 @@ fun BookshelfSettingsContent(
|
|||
OutlinedTextField(
|
||||
value = state.absUrl,
|
||||
onValueChange = model::updateAbsUrl,
|
||||
label = { Text("Audiobookshelf URL") },
|
||||
label = { Text(stringResource(R.string.abs_url_option)) },
|
||||
placeholder = { Text("http://192.168.1.119:13378") },
|
||||
supportingText = { Text(stringResource(R.string.abs_url_option_desc)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
|
@ -81,20 +92,54 @@ fun BookshelfSettingsContent(
|
|||
OutlinedTextField(
|
||||
value = state.absToken,
|
||||
onValueChange = model::updateAbsToken,
|
||||
label = { Text("ABS token") },
|
||||
label = { Text(stringResource(R.string.abs_token_option)) },
|
||||
supportingText = { Text(stringResource(R.string.abs_token_option_desc)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = model::save,
|
||||
enabled = !state.isLoading,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(R.string.save_button))
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = model::testLibraries,
|
||||
enabled = !state.isLoading,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
if (state.isLoading) {
|
||||
CircularProgressIndicator()
|
||||
} else {
|
||||
Text("Fetch libraries")
|
||||
Text(stringResource(R.string.fetch_libraries_button))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
state.librariesSuccess.takeIf { it }?.let {
|
||||
item {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.libraries_fetched_successful,
|
||||
state.libraries.size
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -102,7 +147,7 @@ fun BookshelfSettingsContent(
|
|||
item {
|
||||
Text(
|
||||
text = error,
|
||||
color = androidx.compose.material3.MaterialTheme.colorScheme.error,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -110,7 +155,15 @@ fun BookshelfSettingsContent(
|
|||
val library = state.libraries[index]
|
||||
ListItem(
|
||||
headlineContent = { Text(library.name) },
|
||||
supportingContent = { Text("${library.mediaType} • ${library.itemCount} items") }
|
||||
supportingContent = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.bookshelf_library_subtitle,
|
||||
library.mediaType,
|
||||
library.itemCount
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
app/src/main/res/drawable/bookshelf_screen_filled.xml
Normal file
16
app/src/main/res/drawable/bookshelf_screen_filled.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<!--
|
||||
~ Book's Story — free and open-source Material You eBook reader.
|
||||
~ Copyright (C) 2024-2026 Acclorite
|
||||
~ SPDX-License-Identifier: GPL-3.0-only
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18.95,11.45c-0.3,-2.1 -2.15,-3.7 -4.35,-3.55c-0.65,-2.35 -2.85,-4.05 -5.45,-4.05c-2.95,0 -5.4,2.05 -6,4.85C1.55,9.35 0,11.45 0,13.9c0,2.95 2.4,5.35 5.35,5.35h13.3c2.4,0 4.35,-1.95 4.35,-4.35C23,12.85 21.25,11.55 18.95,11.45zM14,13h-1v4.5l-1.25,-0.75L10.5,17.5V13h-1c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h4.5c0.55,0 1,0.45 1,1S14.55,13 14,13z" />
|
||||
</vector>
|
||||
16
app/src/main/res/drawable/bookshelf_screen_outlined.xml
Normal file
16
app/src/main/res/drawable/bookshelf_screen_outlined.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<!--
|
||||
~ Book's Story — free and open-source Material You eBook reader.
|
||||
~ Copyright (C) 2024-2026 Acclorite
|
||||
~ SPDX-License-Identifier: GPL-3.0-only
|
||||
-->
|
||||
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18.95,11.45c-0.3,-2.1 -2.15,-3.7 -4.35,-3.55c-0.65,-2.35 -2.85,-4.05 -5.45,-4.05c-2.95,0 -5.4,2.05 -6,4.85C1.55,9.35 0,11.45 0,13.9c0,2.95 2.4,5.35 5.35,5.35h13.3c2.4,0 4.35,-1.95 4.35,-4.35C23,12.85 21.25,11.55 18.95,11.45zM18.65,17.65H5.35C3.2,17.65 1.5,15.95 1.5,13.9c0,-1.75 1.15,-3.25 2.85,-3.7l0.55,-0.15l0.15,-0.55c0.4,-2.1 2.25,-3.65 4.4,-3.65c1.95,0 3.7,1.2 4.35,3l0.25,0.7l0.7,-0.05c1.65,-0.1 3.05,1.15 3.05,2.8c0,1.6 -1.3,2.9 -2.9,2.9zM9.5,13v4.5l1.25,-0.75l1.25,0.75V13H14c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1h-4.5c-0.55,0 -1,0.45 -1,1S8.95,13 9.5,13z" />
|
||||
</vector>
|
||||
|
|
@ -137,6 +137,25 @@
|
|||
<string name="bookshelf_settings">Bookshelf</string>
|
||||
<string name="bookshelf_settings_desc">Server, ABS, TTS</string>
|
||||
|
||||
<!-- Bookshelf server settings -->
|
||||
<string name="bookshelf_settings_screen_title">Bookshelf server</string>
|
||||
<string name="bookshelf_url_option">Bookshelf API URL</string>
|
||||
<string name="bookshelf_url_option_desc">Base URL of the bookshelf-api service</string>
|
||||
<string name="abs_url_option">Audiobookshelf URL</string>
|
||||
<string name="abs_url_option_desc">Direct URL of the Audiobookshelf server</string>
|
||||
<string name="abs_token_option">ABS token</string>
|
||||
<string name="abs_token_option_desc">API token from Audiobookshelf settings</string>
|
||||
<string name="save_button">Save</string>
|
||||
<string name="fetch_libraries_button">Fetch libraries</string>
|
||||
<string name="libraries_fetched_successful">Connected to %1$d libraries</string>
|
||||
<string name="bookshelf_libraries_header">Libraries</string>
|
||||
<string name="bookshelf_library_subtitle">%1$s • %2$d items</string>
|
||||
<string name="bookshelf_selected_label">Selected</string>
|
||||
<string name="bookshelf_books_header">Books in %1$s</string>
|
||||
<string name="bookshelf_empty_message">No remote libraries found</string>
|
||||
<string name="bookshelf_refresh_action">Refresh</string>
|
||||
<string name="bookshelf_retry_action">Retry</string>
|
||||
|
||||
<!-- Settings subcategories -->
|
||||
<string name="font_reader_settings">Font</string>
|
||||
<string name="text_reader_settings">Text</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue