From e0d16813e91cd7cf64190ba9a38c48cf49493912 Mon Sep 17 00:00:00 2001 From: Max Grakov Date: Tue, 26 Nov 2024 10:31:00 +0100 Subject: [PATCH] Feature/68 whole book progress (#70) --- .../common/AudiobookshelfChannel.kt | 20 +++++- .../RecentListeningResponseConverter.kt | 7 +- .../common/model/MediaProgressResponse.kt | 1 + .../converter/BookResponseConverter.kt | 1 + .../library/model/BookResponse.kt | 1 + .../converter/PodcastResponseConverter.kt | 1 + .../podcast/model/PodcastResponse.kt | 1 + .../lissen/content/cache/LocalCacheModule.kt | 1 + .../lissen/content/cache/LocalCacheStorage.kt | 2 +- .../lissen/content/cache/Migrations.kt | 13 ++++ .../content/cache/api/CachedBookRepository.kt | 34 +++++++-- .../CachedBookEntityDetailedConverter.kt | 1 + .../CachedBookEntityRecentConverter.kt | 6 +- .../lissen/content/cache/dao/CachedBookDao.kt | 31 ++++++-- .../content/cache/entity/CachedBookEntity.kt | 1 + .../grakovne/lissen/domain/DetailedItem.kt | 1 + .../org/grakovne/lissen/domain/RecentBook.kt | 1 + .../ui/components/AsyncShimmeringImage.kt | 11 ++- .../ui/screens/library/LibraryScreen.kt | 2 + .../composables/RecentBooksComposable.kt | 72 ++++++++++++++++--- .../fallback/LibraryFallbackComposable.kt | 13 ++-- app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 23 files changed, 188 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/common/AudiobookshelfChannel.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/common/AudiobookshelfChannel.kt index c1486afc..cb8f7d24 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/common/AudiobookshelfChannel.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/common/AudiobookshelfChannel.kt @@ -55,10 +55,24 @@ abstract class AudiobookshelfChannel( .fetchLibraries() .map { libraryResponseConverter.apply(it) } - override suspend fun fetchRecentListenedBooks(libraryId: String): ApiResult> = - dataRepository + override suspend fun fetchRecentListenedBooks(libraryId: String): ApiResult> { + val progress: Map = dataRepository + .fetchUserInfoResponse() + .fold( + onSuccess = { + it + .user + .mediaProgress + ?.associate { item -> item.libraryItemId to item.progress } + ?: emptyMap() + }, + onFailure = { emptyMap() }, + ) + + return dataRepository .fetchPersonalizedFeed(libraryId) - .map { recentBookResponseConverter.apply(it) } + .map { recentBookResponseConverter.apply(it, progress) } + } override suspend fun fetchConnectionInfo(): ApiResult = dataRepository .fetchConnectionInfo() diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/common/converter/RecentListeningResponseConverter.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/common/converter/RecentListeningResponseConverter.kt index fe73d433..9c10db61 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/common/converter/RecentListeningResponseConverter.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/common/converter/RecentListeningResponseConverter.kt @@ -8,7 +8,10 @@ import javax.inject.Singleton @Singleton class RecentListeningResponseConverter @Inject constructor() { - fun apply(response: List): List = response + fun apply( + response: List, + progress: Map, + ): List = response .find { it.labelStringKey == LABEL_CONTINUE_LISTENING } ?.entities ?.distinctBy { it.id } @@ -17,10 +20,12 @@ class RecentListeningResponseConverter @Inject constructor() { id = it.id, title = it.media.metadata.title, author = it.media.metadata.authorName, + listenedPercentage = progress[it.id]?.let { it * 100 }?.toInt(), ) } ?: emptyList() companion object { + private const val LABEL_CONTINUE_LISTENING = "LabelContinueListening" } } diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/common/model/MediaProgressResponse.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/common/model/MediaProgressResponse.kt index 85a48bc0..a979afb6 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/common/model/MediaProgressResponse.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/common/model/MediaProgressResponse.kt @@ -6,4 +6,5 @@ data class MediaProgressResponse( val currentTime: Double, val isFinished: Boolean, val lastUpdate: Long, + val progress: Double, ) diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/library/converter/BookResponseConverter.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/library/converter/BookResponseConverter.kt index 1c0c02c4..3a7eb1b2 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/library/converter/BookResponseConverter.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/library/converter/BookResponseConverter.kt @@ -73,6 +73,7 @@ class BookResponseConverter @Inject constructor() { } ?: emptyList(), chapters = maybeChapters ?: filesAsChapters(), + libraryId = item.libraryId, progress = progressResponse ?.let { MediaProgress( diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/library/model/BookResponse.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/library/model/BookResponse.kt index 1fd91a85..30803194 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/library/model/BookResponse.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/library/model/BookResponse.kt @@ -3,6 +3,7 @@ package org.grakovne.lissen.channel.audiobookshelf.library.model data class BookResponse( val id: String, val ino: String, + val libraryId: String, val media: BookMedia, ) diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/podcast/converter/PodcastResponseConverter.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/podcast/converter/PodcastResponseConverter.kt index 1fbbf0c1..498b1de7 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/podcast/converter/PodcastResponseConverter.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/podcast/converter/PodcastResponseConverter.kt @@ -44,6 +44,7 @@ class PodcastResponseConverter @Inject constructor() { return DetailedItem( id = item.id, title = item.media.metadata.title, + libraryId = item.libraryId, author = item.media.metadata.author, files = orderedEpisodes ?.map { diff --git a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/podcast/model/PodcastResponse.kt b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/podcast/model/PodcastResponse.kt index d82d75cd..831a90f6 100644 --- a/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/podcast/model/PodcastResponse.kt +++ b/app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/podcast/model/PodcastResponse.kt @@ -3,6 +3,7 @@ package org.grakovne.lissen.channel.audiobookshelf.podcast.model data class PodcastResponse( val id: String, val ino: String, + val libraryId: String, val media: PodcastMedia, ) diff --git a/app/src/main/java/org/grakovne/lissen/content/cache/LocalCacheModule.kt b/app/src/main/java/org/grakovne/lissen/content/cache/LocalCacheModule.kt index 3266116f..7f33f14f 100644 --- a/app/src/main/java/org/grakovne/lissen/content/cache/LocalCacheModule.kt +++ b/app/src/main/java/org/grakovne/lissen/content/cache/LocalCacheModule.kt @@ -29,6 +29,7 @@ object LocalCacheModule { return database .addMigrations(MIGRATION_1_2) .addMigrations(MIGRATION_2_3) + .addMigrations(MIGRATION_3_4) .build() } diff --git a/app/src/main/java/org/grakovne/lissen/content/cache/LocalCacheStorage.kt b/app/src/main/java/org/grakovne/lissen/content/cache/LocalCacheStorage.kt index c8e6bfbe..31ab4c97 100644 --- a/app/src/main/java/org/grakovne/lissen/content/cache/LocalCacheStorage.kt +++ b/app/src/main/java/org/grakovne/lissen/content/cache/LocalCacheStorage.kt @@ -18,7 +18,7 @@ import org.grakovne.lissen.content.cache.entity.MediaProgressEntity MediaProgressEntity::class, CachedLibraryEntity::class, ], - version = 3, + version = 4, exportSchema = true, ) abstract class LocalCacheStorage : RoomDatabase() { diff --git a/app/src/main/java/org/grakovne/lissen/content/cache/Migrations.kt b/app/src/main/java/org/grakovne/lissen/content/cache/Migrations.kt index 848172e3..4fd757fb 100644 --- a/app/src/main/java/org/grakovne/lissen/content/cache/Migrations.kt +++ b/app/src/main/java/org/grakovne/lissen/content/cache/Migrations.kt @@ -62,3 +62,16 @@ val MIGRATION_2_3 = object : Migration(2, 3) { db.execSQL("ALTER TABLE libraries_new RENAME TO libraries") } } + +val MIGRATION_3_4 = object : Migration(3, 4) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE detailed_books ADD COLUMN libraryId TEXT") + + db.execSQL( + """ + UPDATE detailed_books + SET libraryId = NULL + """.trimIndent(), + ) + } +} diff --git a/app/src/main/java/org/grakovne/lissen/content/cache/api/CachedBookRepository.kt b/app/src/main/java/org/grakovne/lissen/content/cache/api/CachedBookRepository.kt index 5c978350..e5ac09a0 100644 --- a/app/src/main/java/org/grakovne/lissen/content/cache/api/CachedBookRepository.kt +++ b/app/src/main/java/org/grakovne/lissen/content/cache/api/CachedBookRepository.kt @@ -12,6 +12,7 @@ import org.grakovne.lissen.domain.Book import org.grakovne.lissen.domain.DetailedItem import org.grakovne.lissen.domain.PlaybackProgress import org.grakovne.lissen.domain.RecentBook +import org.grakovne.lissen.persistence.preferences.LissenSharedPreferences import java.io.File import java.time.Instant import javax.inject.Inject @@ -24,6 +25,7 @@ class CachedBookRepository @Inject constructor( private val cachedBookEntityConverter: CachedBookEntityConverter, private val cachedBookEntityDetailedConverter: CachedBookEntityDetailedConverter, private val cachedBookEntityRecentConverter: CachedBookEntityRecentConverter, + private val preferences: LissenSharedPreferences, ) { fun provideFileUri(bookId: String, fileId: String): Uri = properties @@ -42,25 +44,43 @@ class CachedBookRepository @Inject constructor( bookDao.upsertCachedBook(book) } - suspend fun fetchCachedBooksIds() = bookDao.fetchBookIds() + suspend fun fetchCachedBooksIds() = bookDao.fetchBookIds( + libraryId = preferences.getPreferredLibrary()?.id, + ) suspend fun fetchBooks( pageNumber: Int, pageSize: Int, ): List = bookDao - .fetchCachedBooks(pageNumber = pageNumber, pageSize = pageSize) + .fetchCachedBooks( + pageNumber = pageNumber, + pageSize = pageSize, + libraryId = preferences.getPreferredLibrary()?.id, + ) .map { cachedBookEntityConverter.apply(it) } suspend fun searchBooks( query: String, ): List = bookDao - .searchCachedBooks(searchQuery = query) + .searchCachedBooks( + searchQuery = query, + libraryId = preferences.getPreferredLibrary()?.id, + ) .map { cachedBookEntityConverter.apply(it) } - suspend fun fetchRecentBooks(): List = - bookDao - .fetchRecentlyListenedCachedBooks() - .map { cachedBookEntityRecentConverter.apply(it) } + suspend fun fetchRecentBooks(): List { + val recentBooks = bookDao.fetchRecentlyListenedCachedBooks( + libraryId = preferences.getPreferredLibrary()?.id, + ) + + val progress = recentBooks + .map { it.id } + .mapNotNull { bookDao.fetchMediaProgress(it) } + .associate { it.bookId to it.currentTime } + + return recentBooks + .map { cachedBookEntityRecentConverter.apply(it, progress[it.id]) } + } suspend fun fetchBook( bookId: String, diff --git a/app/src/main/java/org/grakovne/lissen/content/cache/converter/CachedBookEntityDetailedConverter.kt b/app/src/main/java/org/grakovne/lissen/content/cache/converter/CachedBookEntityDetailedConverter.kt index 92ed6c8f..007b4b8a 100644 --- a/app/src/main/java/org/grakovne/lissen/content/cache/converter/CachedBookEntityDetailedConverter.kt +++ b/app/src/main/java/org/grakovne/lissen/content/cache/converter/CachedBookEntityDetailedConverter.kt @@ -15,6 +15,7 @@ class CachedBookEntityDetailedConverter @Inject constructor() { id = entity.detailedBook.id, title = entity.detailedBook.title, author = entity.detailedBook.author, + libraryId = entity.detailedBook.libraryId, files = entity.files.map { fileEntity -> BookFile( id = fileEntity.bookFileId, diff --git a/app/src/main/java/org/grakovne/lissen/content/cache/converter/CachedBookEntityRecentConverter.kt b/app/src/main/java/org/grakovne/lissen/content/cache/converter/CachedBookEntityRecentConverter.kt index 99034723..7a807507 100644 --- a/app/src/main/java/org/grakovne/lissen/content/cache/converter/CachedBookEntityRecentConverter.kt +++ b/app/src/main/java/org/grakovne/lissen/content/cache/converter/CachedBookEntityRecentConverter.kt @@ -8,9 +8,13 @@ import javax.inject.Singleton @Singleton class CachedBookEntityRecentConverter @Inject constructor() { - fun apply(entity: BookEntity): RecentBook = RecentBook( + fun apply(entity: BookEntity, currentTime: Double?): RecentBook = RecentBook( id = entity.id, title = entity.title, author = entity.author, + listenedPercentage = currentTime + ?.let { it / entity.duration } + ?.let { it * 100 } + ?.toInt(), ) } diff --git a/app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt b/app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt index 4f2a0f2d..08f8c54b 100644 --- a/app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt +++ b/app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt @@ -25,6 +25,7 @@ interface CachedBookDao { title = book.title, author = book.author, duration = book.chapters.sumOf { it.duration }.toInt(), + libraryId = book.libraryId, ) val bookFiles = book @@ -70,15 +71,31 @@ interface CachedBookDao { } @Transaction - @Query("SELECT * FROM detailed_books ORDER BY title LIMIT :pageSize OFFSET :pageNumber * :pageSize") + @Query( + """ + SELECT * FROM detailed_books + WHERE (libraryId IS NULL OR libraryId = :libraryId) + ORDER BY title + LIMIT :pageSize OFFSET :pageNumber * :pageSize + """, + ) suspend fun fetchCachedBooks( + libraryId: String?, pageNumber: Int, pageSize: Int, ): List @Transaction - @Query("SELECT * FROM detailed_books WHERE title LIKE '%' || :searchQuery || '%' OR author LIKE '%' || :searchQuery || '%' ORDER BY title") + @Query( + """ + SELECT * FROM detailed_books + WHERE (libraryId IS NULL OR libraryId = :libraryId) + AND (title LIKE '%' || :searchQuery || '%' OR author LIKE '%' || :searchQuery || '%') + ORDER BY title + """, + ) suspend fun searchCachedBooks( + libraryId: String?, searchQuery: String, ): List @@ -86,13 +103,13 @@ interface CachedBookDao { @RewriteQueriesToDropUnusedColumns @Query( """ - SELECT * FROM detailed_books - INNER JOIN media_progress ON detailed_books.id = media_progress.bookId + SELECT * FROM detailed_books + INNER JOIN media_progress ON detailed_books.id = media_progress.bookId WHERE (libraryId IS NULL OR libraryId = :libraryId) ORDER BY media_progress.lastUpdate DESC LIMIT 10 """, ) - suspend fun fetchRecentlyListenedCachedBooks(): List + suspend fun fetchRecentlyListenedCachedBooks(libraryId: String?): List @Transaction @Query("SELECT * FROM detailed_books WHERE id = :bookId") @@ -103,8 +120,8 @@ interface CachedBookDao { suspend fun fetchBook(bookId: String): BookEntity? @Transaction - @Query("SELECT id FROM detailed_books") - suspend fun fetchBookIds(): List + @Query("SELECT id FROM detailed_books WHERE (libraryId IS NULL OR libraryId = :libraryId) ") + suspend fun fetchBookIds(libraryId: String?): List @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsertBook(book: BookEntity) diff --git a/app/src/main/java/org/grakovne/lissen/content/cache/entity/CachedBookEntity.kt b/app/src/main/java/org/grakovne/lissen/content/cache/entity/CachedBookEntity.kt index e751ce30..b056776a 100644 --- a/app/src/main/java/org/grakovne/lissen/content/cache/entity/CachedBookEntity.kt +++ b/app/src/main/java/org/grakovne/lissen/content/cache/entity/CachedBookEntity.kt @@ -36,6 +36,7 @@ data class BookEntity( val title: String, val author: String?, val duration: Int, + val libraryId: String?, ) : Serializable @Entity( diff --git a/app/src/main/java/org/grakovne/lissen/domain/DetailedItem.kt b/app/src/main/java/org/grakovne/lissen/domain/DetailedItem.kt index e2ce87c7..2ac06be1 100644 --- a/app/src/main/java/org/grakovne/lissen/domain/DetailedItem.kt +++ b/app/src/main/java/org/grakovne/lissen/domain/DetailedItem.kt @@ -9,6 +9,7 @@ data class DetailedItem( val files: List, val chapters: List, val progress: MediaProgress?, + val libraryId: String?, ) : Serializable data class BookFile( diff --git a/app/src/main/java/org/grakovne/lissen/domain/RecentBook.kt b/app/src/main/java/org/grakovne/lissen/domain/RecentBook.kt index 18d314b5..85979147 100644 --- a/app/src/main/java/org/grakovne/lissen/domain/RecentBook.kt +++ b/app/src/main/java/org/grakovne/lissen/domain/RecentBook.kt @@ -4,4 +4,5 @@ data class RecentBook( val id: String, val title: String, val author: String?, + val listenedPercentage: Int?, ) diff --git a/app/src/main/java/org/grakovne/lissen/ui/components/AsyncShimmeringImage.kt b/app/src/main/java/org/grakovne/lissen/ui/components/AsyncShimmeringImage.kt index 4e114b82..0b93127e 100644 --- a/app/src/main/java/org/grakovne/lissen/ui/components/AsyncShimmeringImage.kt +++ b/app/src/main/java/org/grakovne/lissen/ui/components/AsyncShimmeringImage.kt @@ -26,6 +26,7 @@ fun AsyncShimmeringImage( contentScale: ContentScale, modifier: Modifier = Modifier, error: Painter, + onLoadingStateChanged: (Boolean) -> Unit = {}, ) { var isLoading by remember { mutableStateOf(true) } @@ -48,8 +49,14 @@ fun AsyncShimmeringImage( contentDescription = contentDescription, contentScale = contentScale, modifier = Modifier.fillMaxSize(), - onSuccess = { isLoading = false }, - onError = { isLoading = false }, + onSuccess = { + isLoading = false + onLoadingStateChanged(false) + }, + onError = { + isLoading = false + onLoadingStateChanged(false) + }, error = error, ) } diff --git a/app/src/main/java/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt b/app/src/main/java/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt index aa9aefc8..58a7d84a 100644 --- a/app/src/main/java/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt +++ b/app/src/main/java/org/grakovne/lissen/ui/screens/library/LibraryScreen.kt @@ -272,6 +272,7 @@ fun LibraryScreen( navController = navController, recentBooks = showingRecentBooks, imageLoader = imageLoader, + libraryViewModel = libraryViewModel, ) Spacer(modifier = Modifier.height(20.dp)) @@ -323,6 +324,7 @@ fun LibraryScreen( searchRequested = searchRequested, cachingModelView = cachingModelView, networkQualityService = networkQualityService, + libraryViewModel = libraryViewModel, ) } } diff --git a/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/RecentBooksComposable.kt b/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/RecentBooksComposable.kt index 4f07cadf..be91178e 100644 --- a/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/RecentBooksComposable.kt +++ b/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/RecentBooksComposable.kt @@ -1,12 +1,16 @@ package org.grakovne.lissen.ui.screens.library.composables +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -16,9 +20,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext @@ -30,9 +39,12 @@ import androidx.compose.ui.unit.dp import coil.ImageLoader import coil.request.ImageRequest import org.grakovne.lissen.R +import org.grakovne.lissen.channel.common.LibraryType import org.grakovne.lissen.domain.RecentBook import org.grakovne.lissen.ui.components.AsyncShimmeringImage import org.grakovne.lissen.ui.navigation.AppNavigationService +import org.grakovne.lissen.ui.theme.FoxOrange +import org.grakovne.lissen.viewmodel.LibraryViewModel @Composable fun RecentBooksComposable( @@ -40,6 +52,7 @@ fun RecentBooksComposable( recentBooks: List, imageLoader: ImageLoader, modifier: Modifier = Modifier, + libraryViewModel: LibraryViewModel, ) { val configuration = LocalConfiguration.current val screenWidth = remember { configuration.screenWidthDp.dp } @@ -63,6 +76,7 @@ fun RecentBooksComposable( width = itemWidth, imageLoader = imageLoader, navController = navController, + libraryViewModel = libraryViewModel, ) } } @@ -74,6 +88,7 @@ fun RecentBookItemComposable( book: RecentBook, width: Dp, imageLoader: ImageLoader, + libraryViewModel: LibraryViewModel, ) { Column( modifier = Modifier @@ -81,6 +96,7 @@ fun RecentBookItemComposable( .clickable { navController.showPlayer(book.id, book.title) }, ) { val context = LocalContext.current + var coverLoading by remember { mutableStateOf(true) } val imageRequest = remember(book.id) { ImageRequest @@ -90,17 +106,46 @@ fun RecentBookItemComposable( .build() } - AsyncShimmeringImage( - imageRequest = imageRequest, - imageLoader = imageLoader, - contentDescription = "${book.title} cover", - contentScale = ContentScale.FillBounds, + Box( modifier = Modifier .fillMaxWidth() - .aspectRatio(1f) - .clip(RoundedCornerShape(8.dp)), - error = painterResource(R.drawable.cover_fallback), - ) + .clip(RoundedCornerShape(8.dp)) + .aspectRatio(1f), + ) { + AsyncShimmeringImage( + imageRequest = imageRequest, + imageLoader = imageLoader, + contentDescription = "${book.title} cover", + contentScale = ContentScale.FillBounds, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)), + error = painterResource(R.drawable.cover_fallback), + onLoadingStateChanged = { coverLoading = it }, + ) + + if (!coverLoading && shouldShowProgress(book, libraryViewModel.fetchPreferredLibraryType())) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(4.dp) + .align(Alignment.BottomCenter), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Gray.copy(alpha = 0.4f)), + ) + + Box( + modifier = Modifier + .fillMaxWidth(calculateProgress(book)) + .fillMaxHeight() + .background(FoxOrange), + ) + } + } + } Spacer(modifier = Modifier.height(8.dp)) @@ -129,3 +174,12 @@ fun RecentBookItemComposable( } } } + +private fun calculateProgress(book: RecentBook): Float { + return book.listenedPercentage?.div(100.0f) ?: 0.0f +} + +private fun shouldShowProgress(book: RecentBook, libraryType: LibraryType): Boolean = + book.listenedPercentage != null && + libraryType == LibraryType.LIBRARY && + book.listenedPercentage > 0 diff --git a/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/fallback/LibraryFallbackComposable.kt b/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/fallback/LibraryFallbackComposable.kt index 6ce7be33..d84a65c5 100644 --- a/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/fallback/LibraryFallbackComposable.kt +++ b/app/src/main/java/org/grakovne/lissen/ui/screens/library/composables/fallback/LibraryFallbackComposable.kt @@ -24,13 +24,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import org.grakovne.lissen.R +import org.grakovne.lissen.channel.common.LibraryType import org.grakovne.lissen.common.NetworkQualityService import org.grakovne.lissen.viewmodel.CachingModelView +import org.grakovne.lissen.viewmodel.LibraryViewModel @Composable fun LibraryFallbackComposable( searchRequested: Boolean, cachingModelView: CachingModelView, + libraryViewModel: LibraryViewModel, networkQualityService: NetworkQualityService, ) { val configuration = LocalConfiguration.current @@ -39,8 +42,7 @@ fun LibraryFallbackComposable( Box( modifier = Modifier .fillMaxWidth() - .height(screenHeight / 2) - .padding(horizontal = 16.dp), + .height(screenHeight / 2), contentAlignment = Alignment.Center, ) { Column( @@ -51,7 +53,10 @@ fun LibraryFallbackComposable( val text = when { searchRequested -> null - isLocalCache -> stringResource(R.string.the_offline_library_is_empty) + isLocalCache -> when (libraryViewModel.fetchPreferredLibraryType()) { + LibraryType.PODCAST -> stringResource(R.string.the_offline_podcasts_is_empty) + else -> stringResource(R.string.the_offline_library_is_empty) + } hasNetwork.not() -> stringResource(R.string.no_internet_connection) else -> stringResource(R.string.the_library_is_empty) } @@ -84,7 +89,7 @@ fun LibraryFallbackComposable( Text( textAlign = TextAlign.Center, text = it, - style = MaterialTheme.typography.headlineMedium, + style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(top = 36.dp), ) } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b60c02d0..7bcb789e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -61,4 +61,5 @@ Таймер Поиск по названию эпизода Нажмите чтобы запустить + Загрузка подкастов не поддерживается \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3767bbf..8542b774 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,4 +61,5 @@ Timer Search by chapter title Click to open the app + Saving podcasts is not available \ No newline at end of file