diff --git a/app/src/main/java/org/dueattendant149/bookshelf/presentation/bookshelf/RemoteLibraryModel.kt b/app/src/main/java/org/dueattendant149/bookshelf/presentation/bookshelf/RemoteLibraryModel.kt new file mode 100644 index 00000000..48fe61c3 --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/presentation/bookshelf/RemoteLibraryModel.kt @@ -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 = emptyList(), + val books: List = 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, + ) + } + } + } + } diff --git a/app/src/main/java/org/dueattendant149/bookshelf/presentation/bookshelf/RemoteLibraryScreen.kt b/app/src/main/java/org/dueattendant149/bookshelf/presentation/bookshelf/RemoteLibraryScreen.kt new file mode 100644 index 00000000..d75e4386 --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/presentation/bookshelf/RemoteLibraryScreen.kt @@ -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() + RemoteLibraryContent(model = model) + } +} diff --git a/app/src/main/java/org/dueattendant149/bookshelf/presentation/main/MainActivity.kt b/app/src/main/java/org/dueattendant149/bookshelf/presentation/main/MainActivity.kt index 6e56e2c5..13cb3628 100644 --- a/app/src/main/java/org/dueattendant149/bookshelf/presentation/main/MainActivity.kt +++ b/app/src/main/java/org/dueattendant149/bookshelf/presentation/main/MainActivity.kt @@ -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 -> + ) { screen -> when (screen) { - LibraryScreen, HistoryScreen, BrowseScreen -> { + LibraryScreen, HistoryScreen, BrowseScreen, RemoteLibraryScreen -> { NavigatorTabs( currentTab = screen, transitionSpec = { diff --git a/app/src/main/java/org/dueattendant149/bookshelf/ui/bookshelf/RemoteLibraryContent.kt b/app/src/main/java/org/dueattendant149/bookshelf/ui/bookshelf/RemoteLibraryContent.kt new file mode 100644 index 00000000..a2aef5e7 --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/ui/bookshelf/RemoteLibraryContent.kt @@ -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) + ) + } + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64a30bfe..a2997b4f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -489,6 +489,8 @@ Your library Reading history Add books + Bookshelf + Remote bookshelf Arrow Reset start guide Open in web