Skip to content

Commit 2d071b4

Browse files
authored
Feature/98 listened items icon (#109)
1 parent b94349d commit 2d071b4

File tree

11 files changed

+84
-49
lines changed

11 files changed

+84
-49
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ android {
2929
applicationId = "org.grakovne.lissen"
3030
minSdk = 28
3131
targetSdk = 35
32-
versionCode = 62
33-
versionName = "1.2.1"
32+
versionCode = 63
33+
versionName = "1.2.2"
3434

3535
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
3636
vectorDrawables {

app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/library/converter/BookResponseConverter.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ package org.grakovne.lissen.channel.audiobookshelf.library.converter
33
import org.grakovne.lissen.channel.audiobookshelf.common.model.MediaProgressResponse
44
import org.grakovne.lissen.channel.audiobookshelf.library.model.BookResponse
55
import org.grakovne.lissen.channel.audiobookshelf.library.model.LibraryAuthorResponse
6-
import org.grakovne.lissen.domain.BookChapter
76
import org.grakovne.lissen.domain.BookFile
87
import org.grakovne.lissen.domain.DetailedItem
98
import org.grakovne.lissen.domain.MediaProgress
9+
import org.grakovne.lissen.domain.PlayingChapter
1010
import javax.inject.Inject
1111
import javax.inject.Singleton
1212

@@ -22,31 +22,33 @@ class BookResponseConverter @Inject constructor() {
2222
.chapters
2323
?.takeIf { it.isNotEmpty() }
2424
?.map {
25-
BookChapter(
25+
PlayingChapter(
2626
start = it.start,
2727
end = it.end,
2828
title = it.title,
2929
available = true,
3030
id = it.id,
3131
duration = it.end - it.start,
32+
podcastEpisodeState = null,
3233
)
3334
}
3435

35-
val filesAsChapters: () -> List<BookChapter> = {
36+
val filesAsChapters: () -> List<PlayingChapter> = {
3637
item
3738
.media
3839
.audioFiles
3940
?.sortedBy { it.index }
40-
?.fold(0.0 to mutableListOf<BookChapter>()) { (accDuration, chapters), file ->
41+
?.fold(0.0 to mutableListOf<PlayingChapter>()) { (accDuration, chapters), file ->
4142
chapters.add(
42-
BookChapter(
43+
PlayingChapter(
4344
available = true,
4445
start = accDuration,
4546
end = accDuration + file.duration,
4647
title = file.metaTags?.tagTitle
4748
?: file.metadata.filename.removeSuffix(file.metadata.ext),
4849
duration = file.duration,
4950
id = file.ino,
51+
podcastEpisodeState = null,
5052
),
5153
)
5254
accDuration + file.duration to chapters

app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/podcast/PodcastAudiobookshelfChannel.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,13 @@ class PodcastAudiobookshelfChannel @Inject constructor(
121121

122122
progress
123123
.filter { it.libraryItemId == bookId }
124-
.maxByOrNull { it.lastUpdate }
124+
.filterNot { it.episodeId == null }
125+
.sortedByDescending { it.lastUpdate }
126+
.distinctBy { it.episodeId }
125127
}
126128

127129
async { dataRepository.fetchPodcastItem(bookId) }
128130
.await()
129-
.map { podcastResponseConverter.apply(it, mediaProgress.await()) }
131+
.map { podcastResponseConverter.apply(it, mediaProgress.await() ?: emptyList()) }
130132
}
131133
}

app/src/main/java/org/grakovne/lissen/channel/audiobookshelf/podcast/converter/PodcastResponseConverter.kt

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ package org.grakovne.lissen.channel.audiobookshelf.podcast.converter
33
import org.grakovne.lissen.channel.audiobookshelf.common.model.MediaProgressResponse
44
import org.grakovne.lissen.channel.audiobookshelf.podcast.model.PodcastEpisodeResponse
55
import org.grakovne.lissen.channel.audiobookshelf.podcast.model.PodcastResponse
6-
import org.grakovne.lissen.domain.BookChapter
6+
import org.grakovne.lissen.domain.BookChapterState
77
import org.grakovne.lissen.domain.BookFile
88
import org.grakovne.lissen.domain.DetailedItem
99
import org.grakovne.lissen.domain.MediaProgress
10+
import org.grakovne.lissen.domain.PlayingChapter
1011
import java.text.SimpleDateFormat
1112
import java.util.Locale
1213
import javax.inject.Inject
@@ -17,27 +18,40 @@ class PodcastResponseConverter @Inject constructor() {
1718

1819
fun apply(
1920
item: PodcastResponse,
20-
progressResponse: MediaProgressResponse? = null,
21+
progressResponses: List<MediaProgressResponse> = emptyList(),
2122
): DetailedItem {
2223
val orderedEpisodes = item
2324
.media
2425
.episodes
2526
?.orderEpisode()
2627

27-
val filesAsChapters: List<BookChapter> =
28+
val latestEpisodeMediaProgress = progressResponses
29+
.maxByOrNull { it.lastUpdate }
30+
?.let {
31+
MediaProgress(
32+
currentTime = it.currentTime,
33+
isFinished = it.isFinished,
34+
lastUpdate = it.lastUpdate,
35+
)
36+
}
37+
38+
val filesAsChapters: List<PlayingChapter> =
2839
orderedEpisodes
29-
?.fold(0.0 to mutableListOf<BookChapter>()) { (accDuration, chapters), file ->
40+
?.fold(0.0 to mutableListOf<PlayingChapter>()) { (accDuration, chapters), episode ->
3041
chapters.add(
31-
BookChapter(
42+
PlayingChapter(
3243
start = accDuration,
33-
end = accDuration + file.audioFile.duration,
34-
title = file.title,
35-
duration = file.audioFile.duration,
36-
id = file.id,
44+
end = accDuration + episode.audioFile.duration,
45+
title = episode.title,
46+
duration = episode.audioFile.duration,
47+
id = episode.id,
3748
available = true,
49+
podcastEpisodeState = progressResponses
50+
.find { it.episodeId == episode.id }
51+
?.let { hasFinished(it) },
3852
),
3953
)
40-
accDuration + file.audioFile.duration to chapters
54+
accDuration + episode.audioFile.duration to chapters
4155
}
4256
?.second
4357
?: emptyList()
@@ -59,18 +73,20 @@ class PodcastResponseConverter @Inject constructor() {
5973
}
6074
?: emptyList(),
6175
chapters = filesAsChapters,
62-
progress = progressResponse
63-
?.let {
64-
MediaProgress(
65-
currentTime = it.currentTime,
66-
isFinished = it.isFinished,
67-
lastUpdate = it.lastUpdate,
68-
)
69-
},
76+
progress = latestEpisodeMediaProgress,
7077
)
7178
}
7279

80+
private fun hasFinished(progress: MediaProgressResponse): BookChapterState? {
81+
return when (progress.isFinished || progress.progress > FINISHED_PROGRESS_THRESHOLD) {
82+
true -> BookChapterState.FINISHED
83+
false -> null
84+
}
85+
}
86+
7387
companion object {
88+
89+
private const val FINISHED_PROGRESS_THRESHOLD = 0.9
7490
private val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH)
7591

7692
private fun List<PodcastEpisodeResponse>.orderEpisode() =

app/src/main/java/org/grakovne/lissen/content/cache/ContentCachingService.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ import org.grakovne.lissen.channel.common.MediaChannel
2020
import org.grakovne.lissen.content.cache.api.CachedBookRepository
2121
import org.grakovne.lissen.content.cache.api.CachedLibraryRepository
2222
import org.grakovne.lissen.domain.AllItemsDownloadOption
23-
import org.grakovne.lissen.domain.BookChapter
2423
import org.grakovne.lissen.domain.BookFile
2524
import org.grakovne.lissen.domain.CurrentItemDownloadOption
2625
import org.grakovne.lissen.domain.DetailedItem
2726
import org.grakovne.lissen.domain.DownloadOption
2827
import org.grakovne.lissen.domain.NumberItemDownloadOption
28+
import org.grakovne.lissen.domain.PlayingChapter
2929
import org.grakovne.lissen.playback.service.calculateChapterIndex
3030
import org.grakovne.lissen.viewmodel.CacheProgress
3131
import javax.inject.Inject
@@ -119,7 +119,7 @@ class ContentCachingService @Inject constructor(
119119

120120
private suspend fun cacheBookInfo(
121121
book: DetailedItem,
122-
fetchedChapters: List<BookChapter>,
122+
fetchedChapters: List<PlayingChapter>,
123123
) = bookRepository
124124
.cacheBook(book, fetchedChapters)
125125
.let { CacheProgress.Completed }
@@ -212,7 +212,7 @@ class ContentCachingService @Inject constructor(
212212

213213
private fun findRequestedFiles(
214214
book: DetailedItem,
215-
requestedChapters: List<BookChapter>,
215+
requestedChapters: List<PlayingChapter>,
216216
): List<BookFile> = requestedChapters
217217
.flatMap { findRelatedFiles(it, book.files) }
218218
.distinctBy { it.id }
@@ -221,7 +221,7 @@ class ContentCachingService @Inject constructor(
221221
book: DetailedItem,
222222
option: DownloadOption,
223223
currentTotalPosition: Double,
224-
): List<BookChapter> {
224+
): List<PlayingChapter> {
225225
val chapterIndex = calculateChapterIndex(book, currentTotalPosition)
226226

227227
return when (option) {
@@ -235,7 +235,7 @@ class ContentCachingService @Inject constructor(
235235
}
236236

237237
private fun findRelatedFiles(
238-
chapter: BookChapter,
238+
chapter: PlayingChapter,
239239
files: List<BookFile>,
240240
): List<BookFile> {
241241
val chapterStartRounded = chapter.start.round()

app/src/main/java/org/grakovne/lissen/content/cache/api/CachedBookRepository.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import org.grakovne.lissen.content.cache.converter.CachedBookEntityRecentConvert
99
import org.grakovne.lissen.content.cache.dao.CachedBookDao
1010
import org.grakovne.lissen.content.cache.entity.MediaProgressEntity
1111
import org.grakovne.lissen.domain.Book
12-
import org.grakovne.lissen.domain.BookChapter
1312
import org.grakovne.lissen.domain.DetailedItem
1413
import org.grakovne.lissen.domain.PlaybackProgress
14+
import org.grakovne.lissen.domain.PlayingChapter
1515
import org.grakovne.lissen.domain.RecentBook
1616
import org.grakovne.lissen.persistence.preferences.LissenSharedPreferences
1717
import java.io.File
@@ -43,7 +43,7 @@ class CachedBookRepository @Inject constructor(
4343

4444
suspend fun cacheBook(
4545
book: DetailedItem,
46-
fetchedChapters: List<BookChapter>,
46+
fetchedChapters: List<PlayingChapter>,
4747
) {
4848
bookDao.upsertCachedBook(book, fetchedChapters)
4949
}

app/src/main/java/org/grakovne/lissen/content/cache/converter/CachedBookEntityDetailedConverter.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package org.grakovne.lissen.content.cache.converter
22

33
import org.grakovne.lissen.content.cache.entity.CachedBookEntity
4-
import org.grakovne.lissen.domain.BookChapter
54
import org.grakovne.lissen.domain.BookFile
65
import org.grakovne.lissen.domain.DetailedItem
76
import org.grakovne.lissen.domain.MediaProgress
7+
import org.grakovne.lissen.domain.PlayingChapter
88
import javax.inject.Inject
99
import javax.inject.Singleton
1010

@@ -26,13 +26,14 @@ class CachedBookEntityDetailedConverter @Inject constructor() {
2626
)
2727
},
2828
chapters = entity.chapters.map { chapterEntity ->
29-
BookChapter(
29+
PlayingChapter(
3030
duration = chapterEntity.duration,
3131
start = chapterEntity.start,
3232
end = chapterEntity.end,
3333
title = chapterEntity.title,
3434
available = chapterEntity.isCached,
3535
id = chapterEntity.bookChapterId,
36+
podcastEpisodeState = null, // currently state is not available for local mode
3637
)
3738
},
3839
progress = entity.progress?.let { progressEntity ->

app/src/main/java/org/grakovne/lissen/content/cache/dao/CachedBookDao.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@ import org.grakovne.lissen.content.cache.entity.BookEntity
1414
import org.grakovne.lissen.content.cache.entity.BookFileEntity
1515
import org.grakovne.lissen.content.cache.entity.CachedBookEntity
1616
import org.grakovne.lissen.content.cache.entity.MediaProgressEntity
17-
import org.grakovne.lissen.domain.BookChapter
1817
import org.grakovne.lissen.domain.DetailedItem
18+
import org.grakovne.lissen.domain.PlayingChapter
1919

2020
@Dao
2121
interface CachedBookDao {
2222

2323
@Transaction
2424
suspend fun upsertCachedBook(
2525
book: DetailedItem,
26-
fetchedChapters: List<BookChapter>,
26+
fetchedChapters: List<PlayingChapter>,
2727
) {
2828
val bookEntity = BookEntity(
2929
id = book.id,

app/src/main/java/org/grakovne/lissen/domain/DetailedItem.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ data class DetailedItem(
77
val title: String,
88
val author: String?,
99
val files: List<BookFile>,
10-
val chapters: List<BookChapter>,
10+
val chapters: List<PlayingChapter>,
1111
val progress: MediaProgress?,
1212
val libraryId: String?,
1313
val localProvided: Boolean,
@@ -26,11 +26,16 @@ data class MediaProgress(
2626
val lastUpdate: Long,
2727
) : Serializable
2828

29-
data class BookChapter(
29+
data class PlayingChapter(
3030
val available: Boolean,
31+
val podcastEpisodeState: BookChapterState?,
3132
val duration: Double,
3233
val start: Double,
3334
val end: Double,
3435
val title: String,
3536
val id: String,
3637
) : Serializable
38+
39+
enum class BookChapterState {
40+
FINISHED,
41+
}

app/src/main/java/org/grakovne/lissen/ui/screens/player/composable/PlaylistItemComposable.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.size
99
import androidx.compose.foundation.layout.width
1010
import androidx.compose.material.icons.Icons
1111
import androidx.compose.material.icons.outlined.Audiotrack
12+
import androidx.compose.material.icons.outlined.Check
1213
import androidx.compose.material3.Icon
1314
import androidx.compose.material3.MaterialTheme
1415
import androidx.compose.material3.MaterialTheme.colorScheme
@@ -22,12 +23,13 @@ import androidx.compose.ui.text.font.FontWeight
2223
import androidx.compose.ui.text.style.TextOverflow
2324
import androidx.compose.ui.unit.dp
2425
import org.grakovne.lissen.R
25-
import org.grakovne.lissen.domain.BookChapter
26+
import org.grakovne.lissen.domain.BookChapterState
27+
import org.grakovne.lissen.domain.PlayingChapter
2628
import org.grakovne.lissen.ui.extensions.formatLeadingMinutes
2729

2830
@Composable
2931
fun PlaylistItemComposable(
30-
track: BookChapter,
32+
track: PlayingChapter,
3133
isSelected: Boolean,
3234
onClick: () -> Unit,
3335
modifier: Modifier,
@@ -41,14 +43,21 @@ fun PlaylistItemComposable(
4143
interactionSource = remember { MutableInteractionSource() },
4244
),
4345
) {
44-
if (isSelected) {
45-
Icon(
46-
imageVector = Icons.Outlined.Audiotrack,
46+
when {
47+
isSelected ->
48+
Icon(
49+
imageVector = Icons.Outlined.Audiotrack,
50+
contentDescription = stringResource(R.string.player_screen_library_playing_title),
51+
modifier = Modifier.size(16.dp),
52+
)
53+
54+
track.podcastEpisodeState == BookChapterState.FINISHED -> Icon(
55+
imageVector = Icons.Outlined.Check,
4756
contentDescription = stringResource(R.string.player_screen_library_playing_title),
4857
modifier = Modifier.size(16.dp),
4958
)
50-
} else {
51-
Spacer(modifier = Modifier.size(16.dp))
59+
60+
else -> Spacer(modifier = Modifier.size(16.dp))
5261
}
5362

5463
Spacer(modifier = Modifier.width(8.dp))

app/src/main/java/org/grakovne/lissen/viewmodel/PlayerViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import androidx.lifecycle.ViewModel
66
import androidx.lifecycle.viewModelScope
77
import dagger.hilt.android.lifecycle.HiltViewModel
88
import kotlinx.coroutines.launch
9-
import org.grakovne.lissen.domain.BookChapter
109
import org.grakovne.lissen.domain.DetailedItem
10+
import org.grakovne.lissen.domain.PlayingChapter
1111
import org.grakovne.lissen.domain.TimerOption
1212
import org.grakovne.lissen.widget.MediaRepository
1313
import javax.inject.Inject
@@ -87,7 +87,7 @@ class PlayerViewModel @Inject constructor(
8787
mediaRepository.setChapterPosition(chapterPosition)
8888
}
8989

90-
fun setChapter(chapter: BookChapter) {
90+
fun setChapter(chapter: PlayingChapter) {
9191
if (chapter.available) {
9292
val index = book.value?.chapters?.indexOf(chapter) ?: -1
9393
mediaRepository.setChapter(index)

0 commit comments

Comments
 (0)