Skip to content

Commit

Permalink
Feature/98 listened items icon (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
GrakovNe authored Jan 26, 2025
1 parent b94349d commit 2d071b4
Show file tree
Hide file tree
Showing 11 changed files with 84 additions and 49 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ android {
applicationId = "org.grakovne.lissen"
minSdk = 28
targetSdk = 35
versionCode = 62
versionName = "1.2.1"
versionCode = 63
versionName = "1.2.2"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package org.grakovne.lissen.channel.audiobookshelf.library.converter
import org.grakovne.lissen.channel.audiobookshelf.common.model.MediaProgressResponse
import org.grakovne.lissen.channel.audiobookshelf.library.model.BookResponse
import org.grakovne.lissen.channel.audiobookshelf.library.model.LibraryAuthorResponse
import org.grakovne.lissen.domain.BookChapter
import org.grakovne.lissen.domain.BookFile
import org.grakovne.lissen.domain.DetailedItem
import org.grakovne.lissen.domain.MediaProgress
import org.grakovne.lissen.domain.PlayingChapter
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -22,31 +22,33 @@ class BookResponseConverter @Inject constructor() {
.chapters
?.takeIf { it.isNotEmpty() }
?.map {
BookChapter(
PlayingChapter(
start = it.start,
end = it.end,
title = it.title,
available = true,
id = it.id,
duration = it.end - it.start,
podcastEpisodeState = null,
)
}

val filesAsChapters: () -> List<BookChapter> = {
val filesAsChapters: () -> List<PlayingChapter> = {
item
.media
.audioFiles
?.sortedBy { it.index }
?.fold(0.0 to mutableListOf<BookChapter>()) { (accDuration, chapters), file ->
?.fold(0.0 to mutableListOf<PlayingChapter>()) { (accDuration, chapters), file ->
chapters.add(
BookChapter(
PlayingChapter(
available = true,
start = accDuration,
end = accDuration + file.duration,
title = file.metaTags?.tagTitle
?: file.metadata.filename.removeSuffix(file.metadata.ext),
duration = file.duration,
id = file.ino,
podcastEpisodeState = null,
),
)
accDuration + file.duration to chapters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,13 @@ class PodcastAudiobookshelfChannel @Inject constructor(

progress
.filter { it.libraryItemId == bookId }
.maxByOrNull { it.lastUpdate }
.filterNot { it.episodeId == null }
.sortedByDescending { it.lastUpdate }
.distinctBy { it.episodeId }
}

async { dataRepository.fetchPodcastItem(bookId) }
.await()
.map { podcastResponseConverter.apply(it, mediaProgress.await()) }
.map { podcastResponseConverter.apply(it, mediaProgress.await() ?: emptyList()) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package org.grakovne.lissen.channel.audiobookshelf.podcast.converter
import org.grakovne.lissen.channel.audiobookshelf.common.model.MediaProgressResponse
import org.grakovne.lissen.channel.audiobookshelf.podcast.model.PodcastEpisodeResponse
import org.grakovne.lissen.channel.audiobookshelf.podcast.model.PodcastResponse
import org.grakovne.lissen.domain.BookChapter
import org.grakovne.lissen.domain.BookChapterState
import org.grakovne.lissen.domain.BookFile
import org.grakovne.lissen.domain.DetailedItem
import org.grakovne.lissen.domain.MediaProgress
import org.grakovne.lissen.domain.PlayingChapter
import java.text.SimpleDateFormat
import java.util.Locale
import javax.inject.Inject
Expand All @@ -17,27 +18,40 @@ class PodcastResponseConverter @Inject constructor() {

fun apply(
item: PodcastResponse,
progressResponse: MediaProgressResponse? = null,
progressResponses: List<MediaProgressResponse> = emptyList(),
): DetailedItem {
val orderedEpisodes = item
.media
.episodes
?.orderEpisode()

val filesAsChapters: List<BookChapter> =
val latestEpisodeMediaProgress = progressResponses
.maxByOrNull { it.lastUpdate }
?.let {
MediaProgress(
currentTime = it.currentTime,
isFinished = it.isFinished,
lastUpdate = it.lastUpdate,
)
}

val filesAsChapters: List<PlayingChapter> =
orderedEpisodes
?.fold(0.0 to mutableListOf<BookChapter>()) { (accDuration, chapters), file ->
?.fold(0.0 to mutableListOf<PlayingChapter>()) { (accDuration, chapters), episode ->
chapters.add(
BookChapter(
PlayingChapter(
start = accDuration,
end = accDuration + file.audioFile.duration,
title = file.title,
duration = file.audioFile.duration,
id = file.id,
end = accDuration + episode.audioFile.duration,
title = episode.title,
duration = episode.audioFile.duration,
id = episode.id,
available = true,
podcastEpisodeState = progressResponses
.find { it.episodeId == episode.id }
?.let { hasFinished(it) },
),
)
accDuration + file.audioFile.duration to chapters
accDuration + episode.audioFile.duration to chapters
}
?.second
?: emptyList()
Expand All @@ -59,18 +73,20 @@ class PodcastResponseConverter @Inject constructor() {
}
?: emptyList(),
chapters = filesAsChapters,
progress = progressResponse
?.let {
MediaProgress(
currentTime = it.currentTime,
isFinished = it.isFinished,
lastUpdate = it.lastUpdate,
)
},
progress = latestEpisodeMediaProgress,
)
}

private fun hasFinished(progress: MediaProgressResponse): BookChapterState? {
return when (progress.isFinished || progress.progress > FINISHED_PROGRESS_THRESHOLD) {
true -> BookChapterState.FINISHED
false -> null
}
}

companion object {

private const val FINISHED_PROGRESS_THRESHOLD = 0.9
private val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH)

private fun List<PodcastEpisodeResponse>.orderEpisode() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import org.grakovne.lissen.channel.common.MediaChannel
import org.grakovne.lissen.content.cache.api.CachedBookRepository
import org.grakovne.lissen.content.cache.api.CachedLibraryRepository
import org.grakovne.lissen.domain.AllItemsDownloadOption
import org.grakovne.lissen.domain.BookChapter
import org.grakovne.lissen.domain.BookFile
import org.grakovne.lissen.domain.CurrentItemDownloadOption
import org.grakovne.lissen.domain.DetailedItem
import org.grakovne.lissen.domain.DownloadOption
import org.grakovne.lissen.domain.NumberItemDownloadOption
import org.grakovne.lissen.domain.PlayingChapter
import org.grakovne.lissen.playback.service.calculateChapterIndex
import org.grakovne.lissen.viewmodel.CacheProgress
import javax.inject.Inject
Expand Down Expand Up @@ -119,7 +119,7 @@ class ContentCachingService @Inject constructor(

private suspend fun cacheBookInfo(
book: DetailedItem,
fetchedChapters: List<BookChapter>,
fetchedChapters: List<PlayingChapter>,
) = bookRepository
.cacheBook(book, fetchedChapters)
.let { CacheProgress.Completed }
Expand Down Expand Up @@ -212,7 +212,7 @@ class ContentCachingService @Inject constructor(

private fun findRequestedFiles(
book: DetailedItem,
requestedChapters: List<BookChapter>,
requestedChapters: List<PlayingChapter>,
): List<BookFile> = requestedChapters
.flatMap { findRelatedFiles(it, book.files) }
.distinctBy { it.id }
Expand All @@ -221,7 +221,7 @@ class ContentCachingService @Inject constructor(
book: DetailedItem,
option: DownloadOption,
currentTotalPosition: Double,
): List<BookChapter> {
): List<PlayingChapter> {
val chapterIndex = calculateChapterIndex(book, currentTotalPosition)

return when (option) {
Expand All @@ -235,7 +235,7 @@ class ContentCachingService @Inject constructor(
}

private fun findRelatedFiles(
chapter: BookChapter,
chapter: PlayingChapter,
files: List<BookFile>,
): List<BookFile> {
val chapterStartRounded = chapter.start.round()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import org.grakovne.lissen.content.cache.converter.CachedBookEntityRecentConvert
import org.grakovne.lissen.content.cache.dao.CachedBookDao
import org.grakovne.lissen.content.cache.entity.MediaProgressEntity
import org.grakovne.lissen.domain.Book
import org.grakovne.lissen.domain.BookChapter
import org.grakovne.lissen.domain.DetailedItem
import org.grakovne.lissen.domain.PlaybackProgress
import org.grakovne.lissen.domain.PlayingChapter
import org.grakovne.lissen.domain.RecentBook
import org.grakovne.lissen.persistence.preferences.LissenSharedPreferences
import java.io.File
Expand Down Expand Up @@ -43,7 +43,7 @@ class CachedBookRepository @Inject constructor(

suspend fun cacheBook(
book: DetailedItem,
fetchedChapters: List<BookChapter>,
fetchedChapters: List<PlayingChapter>,
) {
bookDao.upsertCachedBook(book, fetchedChapters)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package org.grakovne.lissen.content.cache.converter

import org.grakovne.lissen.content.cache.entity.CachedBookEntity
import org.grakovne.lissen.domain.BookChapter
import org.grakovne.lissen.domain.BookFile
import org.grakovne.lissen.domain.DetailedItem
import org.grakovne.lissen.domain.MediaProgress
import org.grakovne.lissen.domain.PlayingChapter
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -26,13 +26,14 @@ class CachedBookEntityDetailedConverter @Inject constructor() {
)
},
chapters = entity.chapters.map { chapterEntity ->
BookChapter(
PlayingChapter(
duration = chapterEntity.duration,
start = chapterEntity.start,
end = chapterEntity.end,
title = chapterEntity.title,
available = chapterEntity.isCached,
id = chapterEntity.bookChapterId,
podcastEpisodeState = null, // currently state is not available for local mode
)
},
progress = entity.progress?.let { progressEntity ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ import org.grakovne.lissen.content.cache.entity.BookEntity
import org.grakovne.lissen.content.cache.entity.BookFileEntity
import org.grakovne.lissen.content.cache.entity.CachedBookEntity
import org.grakovne.lissen.content.cache.entity.MediaProgressEntity
import org.grakovne.lissen.domain.BookChapter
import org.grakovne.lissen.domain.DetailedItem
import org.grakovne.lissen.domain.PlayingChapter

@Dao
interface CachedBookDao {

@Transaction
suspend fun upsertCachedBook(
book: DetailedItem,
fetchedChapters: List<BookChapter>,
fetchedChapters: List<PlayingChapter>,
) {
val bookEntity = BookEntity(
id = book.id,
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/java/org/grakovne/lissen/domain/DetailedItem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ data class DetailedItem(
val title: String,
val author: String?,
val files: List<BookFile>,
val chapters: List<BookChapter>,
val chapters: List<PlayingChapter>,
val progress: MediaProgress?,
val libraryId: String?,
val localProvided: Boolean,
Expand All @@ -26,11 +26,16 @@ data class MediaProgress(
val lastUpdate: Long,
) : Serializable

data class BookChapter(
data class PlayingChapter(
val available: Boolean,
val podcastEpisodeState: BookChapterState?,
val duration: Double,
val start: Double,
val end: Double,
val title: String,
val id: String,
) : Serializable

enum class BookChapterState {
FINISHED,
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Audiotrack
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
Expand All @@ -22,12 +23,13 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import org.grakovne.lissen.R
import org.grakovne.lissen.domain.BookChapter
import org.grakovne.lissen.domain.BookChapterState
import org.grakovne.lissen.domain.PlayingChapter
import org.grakovne.lissen.ui.extensions.formatLeadingMinutes

@Composable
fun PlaylistItemComposable(
track: BookChapter,
track: PlayingChapter,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier,
Expand All @@ -41,14 +43,21 @@ fun PlaylistItemComposable(
interactionSource = remember { MutableInteractionSource() },
),
) {
if (isSelected) {
Icon(
imageVector = Icons.Outlined.Audiotrack,
when {
isSelected ->
Icon(
imageVector = Icons.Outlined.Audiotrack,
contentDescription = stringResource(R.string.player_screen_library_playing_title),
modifier = Modifier.size(16.dp),
)

track.podcastEpisodeState == BookChapterState.FINISHED -> Icon(
imageVector = Icons.Outlined.Check,
contentDescription = stringResource(R.string.player_screen_library_playing_title),
modifier = Modifier.size(16.dp),
)
} else {
Spacer(modifier = Modifier.size(16.dp))

else -> Spacer(modifier = Modifier.size(16.dp))
}

Spacer(modifier = Modifier.width(8.dp))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.grakovne.lissen.domain.BookChapter
import org.grakovne.lissen.domain.DetailedItem
import org.grakovne.lissen.domain.PlayingChapter
import org.grakovne.lissen.domain.TimerOption
import org.grakovne.lissen.widget.MediaRepository
import javax.inject.Inject
Expand Down Expand Up @@ -87,7 +87,7 @@ class PlayerViewModel @Inject constructor(
mediaRepository.setChapterPosition(chapterPosition)
}

fun setChapter(chapter: BookChapter) {
fun setChapter(chapter: PlayingChapter) {
if (chapter.available) {
val index = book.value?.chapters?.indexOf(chapter) ?: -1
mediaRepository.setChapter(index)
Expand Down

0 comments on commit 2d071b4

Please sign in to comment.