Phase 5b: RemoteLibrary tab — fetch libraries/books, simple list UI
This commit is contained in:
parent
0c2dfbd592
commit
cff4db13ab
5 changed files with 232 additions and 3 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.bookshelf
|
||||
|
||||
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.library.Book
|
||||
import org.dueattendant149.bookshelf.domain.model.remote.RemoteLibrary
|
||||
import org.dueattendant149.bookshelf.domain.use_case.remote.FetchBooksUseCase
|
||||
import org.dueattendant149.bookshelf.domain.use_case.remote.FetchLibrariesUseCase
|
||||
import javax.inject.Inject
|
||||
|
||||
data class RemoteLibraryState(
|
||||
val libraries: List<RemoteLibrary> = emptyList(),
|
||||
val books: List<Book> = emptyList(),
|
||||
val selectedLibrary: RemoteLibrary? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class RemoteLibraryModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val fetchLibrariesUseCase: FetchLibrariesUseCase,
|
||||
private val fetchBooksUseCase: FetchBooksUseCase,
|
||||
private val serverSettings: ServerSettings,
|
||||
) : ViewModel() {
|
||||
private val _state = MutableStateFlow(RemoteLibraryState())
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
loadLibraries()
|
||||
}
|
||||
|
||||
fun loadLibraries() {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
if (!serverSettings.hasCredentials()) {
|
||||
_state.update { it.copy(isLoading = false, error = "Server not configured") }
|
||||
return@launch
|
||||
}
|
||||
val result = fetchLibrariesUseCase()
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
libraries = result.getOrDefault(emptyList()),
|
||||
error = result.exceptionOrNull()?.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectLibrary(library: RemoteLibrary) {
|
||||
_state.update { it.copy(selectedLibrary = library, books = emptyList(), error = null) }
|
||||
loadBooks(library.id)
|
||||
}
|
||||
|
||||
fun loadBooks(libraryId: String) {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
val result = fetchBooksUseCase(libraryId)
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
books = result.getOrDefault(emptyList()),
|
||||
error = result.exceptionOrNull()?.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.bookshelf
|
||||
|
||||
import android.os.Parcelable
|
||||
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.bookshelf.RemoteLibraryContent
|
||||
|
||||
@Parcelize
|
||||
object RemoteLibraryScreen : Screen, Parcelable {
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val model = hiltViewModel<RemoteLibraryModel>()
|
||||
RemoteLibraryContent(model = model)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ import org.dueattendant149.bookshelf.presentation.browse.BrowseModel
|
|||
import org.dueattendant149.bookshelf.presentation.browse.BrowseScreen
|
||||
import org.dueattendant149.bookshelf.presentation.history.HistoryModel
|
||||
import org.dueattendant149.bookshelf.presentation.history.HistoryScreen
|
||||
import org.dueattendant149.bookshelf.presentation.bookshelf.RemoteLibraryScreen
|
||||
import org.dueattendant149.bookshelf.presentation.library.LibraryModel
|
||||
import org.dueattendant149.bookshelf.presentation.library.LibraryScreen
|
||||
import org.dueattendant149.bookshelf.presentation.navigator.NavigatorItem
|
||||
|
|
@ -107,6 +108,13 @@ class MainActivity : AppCompatActivity() {
|
|||
tooltip = R.string.browse_content_desc,
|
||||
selectedIcon = R.drawable.browse_screen_filled,
|
||||
unselectedIcon = R.drawable.browse_screen_outlined
|
||||
),
|
||||
NavigatorItem(
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -137,14 +145,14 @@ class MainActivity : AppCompatActivity() {
|
|||
},
|
||||
contentKey = {
|
||||
when (it) {
|
||||
LibraryScreen, HistoryScreen, BrowseScreen -> "tabs"
|
||||
LibraryScreen, HistoryScreen, BrowseScreen, RemoteLibraryScreen -> "tabs"
|
||||
else -> it
|
||||
}
|
||||
},
|
||||
backHandlerEnabled = { it != StartScreen }
|
||||
) { screen ->
|
||||
when (screen) {
|
||||
LibraryScreen, HistoryScreen, BrowseScreen -> {
|
||||
LibraryScreen, HistoryScreen, BrowseScreen, RemoteLibraryScreen -> {
|
||||
NavigatorTabs(
|
||||
currentTab = screen,
|
||||
transitionSpec = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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.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.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
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.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.dueattendant149.bookshelf.presentation.bookshelf.RemoteLibraryModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RemoteLibraryContent(model: RemoteLibraryModel) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Bookshelf") }
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(state.libraries.size) { index ->
|
||||
val library = state.libraries[index]
|
||||
val selected = state.selectedLibrary?.id == library.id
|
||||
Card(
|
||||
onClick = { model.selectLibrary(library) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = library.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = "${library.mediaType} • ${library.itemCount} items",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(state.books.size) { index ->
|
||||
val book = state.books[index]
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = book.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
Text(
|
||||
text = book.author.getAsString() ?: "",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -489,6 +489,8 @@
|
|||
<string name="library_content_desc">Your library</string>
|
||||
<string name="history_content_desc">Reading history</string>
|
||||
<string name="browse_content_desc">Add books</string>
|
||||
<string name="bookshelf_screen">Bookshelf</string>
|
||||
<string name="bookshelf_content_desc">Remote bookshelf</string>
|
||||
<string name="arrow_content_desc">Arrow</string>
|
||||
<string name="reset_start_content_desc">Reset start guide</string>
|
||||
<string name="open_in_web_content_desc">Open in web</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue