Skip to content

Commit 22d18dc

Browse files
committed
Add cache error state in UI.
1 parent 9269686 commit 22d18dc

10 files changed

Lines changed: 229 additions & 199 deletions

File tree

app/shared/app-data/src/commonMain/kotlin/domain/media/cache/LocalFileMediaCache.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2024-2025 OpenAni and contributors.
2+
* Copyright (C) 2024-2026 OpenAni and contributors.
33
*
44
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
55
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
@@ -48,7 +48,7 @@ class LocalFileMediaCache(
4848
private val backedMediaSourceId: String = MediaCacheManager.LOCAL_FS_MEDIA_SOURCE_ID,
4949
private val onCloseAndDeleteFiles: LocalFileMediaCache.(SystemPath) -> Unit = { file.deleteRecursively() },
5050
) : MediaCache {
51-
override val state: Flow<MediaCacheState> = MutableStateFlow(MediaCacheState.IN_PROGRESS)
51+
override val state: Flow<MediaCacheState> = MutableStateFlow(MediaCacheState.COMPLETED)
5252
override val canPlay: Flow<Boolean> = MutableStateFlow(true)
5353

5454
private val fileSize = file.length().bytes
@@ -91,4 +91,4 @@ class LocalFileMediaCache(
9191
_isDeleted.value = true
9292
onCloseAndDeleteFiles(file)
9393
}
94-
}
94+
}

app/shared/app-data/src/commonMain/kotlin/domain/media/cache/MediaCache.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2024-2025 OpenAni and contributors.
2+
* Copyright (C) 2024-2026 OpenAni and contributors.
33
*
44
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
55
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
@@ -244,11 +244,13 @@ interface MediaCache {
244244
}
245245
}
246246

247-
suspend inline fun MediaCache.isFinished(): Boolean = sessionStats.first().downloadProgress.isFinished
247+
suspend inline fun MediaCache.isFinished(): Boolean = state.first() == MediaCacheState.COMPLETED
248248

249249
enum class MediaCacheState {
250250
IN_PROGRESS,
251251
PAUSED,
252+
FAILED,
253+
COMPLETED,
252254
}
253255

254256
open class TestMediaCache(
@@ -287,6 +289,7 @@ open class TestMediaCache(
287289
fun getResumeCalled() = resumeCalled.value
288290

289291
override suspend fun pause() {
292+
state.value = MediaCacheState.PAUSED
290293
println("pause")
291294
}
292295

@@ -296,6 +299,7 @@ open class TestMediaCache(
296299

297300
override suspend fun resume() {
298301
resumeCalled.incrementAndGet()
302+
state.value = MediaCacheState.IN_PROGRESS
299303
println("resume")
300304
}
301305

app/shared/app-data/src/commonMain/kotlin/domain/media/cache/MediaCacheManager.kt

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2024-2025 OpenAni and contributors.
2+
* Copyright (C) 2024-2026 OpenAni and contributors.
33
*
44
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
55
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
@@ -69,9 +69,13 @@ abstract class MediaCacheManager(
6969

7070
for (mediaCache in list) {
7171
if (mediaCache.metadata.subjectId == subjectIdString && mediaCache.metadata.episodeId == episodeIdString) {
72-
hasAnyCaching = mediaCache
73-
if (mediaCache.isFinished()) {
74-
hasAnyCached = mediaCache
72+
when (mediaCache.state.first()) {
73+
MediaCacheState.COMPLETED -> hasAnyCached = mediaCache
74+
MediaCacheState.IN_PROGRESS,
75+
MediaCacheState.PAUSED,
76+
-> hasAnyCaching = mediaCache
77+
78+
MediaCacheState.FAILED -> Unit
7579
}
7680
}
7781
}
@@ -81,11 +85,17 @@ abstract class MediaCacheManager(
8185
emit(EpisodeCacheStatus.NotCached)
8286
} else {
8387
emitAll(
84-
target.fileStats.map {
85-
if (it.downloadProgress.isFinished) {
86-
EpisodeCacheStatus.Cached(totalSize = it.totalSize)
87-
} else {
88-
EpisodeCacheStatus.Caching(progress = it.downloadProgress, totalSize = it.totalSize)
88+
target.state.combine(target.fileStats) { state, stats ->
89+
when (state) {
90+
MediaCacheState.COMPLETED -> EpisodeCacheStatus.Cached(totalSize = stats.totalSize)
91+
MediaCacheState.IN_PROGRESS,
92+
MediaCacheState.PAUSED,
93+
-> EpisodeCacheStatus.Caching(
94+
progress = stats.downloadProgress,
95+
totalSize = stats.totalSize,
96+
)
97+
98+
MediaCacheState.FAILED -> EpisodeCacheStatus.NotCached
8999
}
90100
},
91101
)

app/shared/app-data/src/commonMain/kotlin/domain/media/cache/engine/HttpMediaCacheEngine.kt

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -207,22 +207,8 @@ class HttpMediaCacheEngine(
207207
internal val downloadId: DownloadId,
208208
override val metadata: MediaCacheMetadata,
209209
) : MediaCache {
210-
override val state: Flow<MediaCacheState> = downloader.getProgressFlow(downloadId).map {
211-
when (it.status) {
212-
DownloadStatus.DOWNLOADING,
213-
DownloadStatus.MERGING,
214-
DownloadStatus.COMPLETED,
215-
-> MediaCacheState.IN_PROGRESS
216-
217-
DownloadStatus.INITIALIZING,
218-
DownloadStatus.PAUSED,
219-
-> MediaCacheState.PAUSED
220-
221-
DownloadStatus.FAILED,
222-
DownloadStatus.CANCELED,
223-
-> MediaCacheState.PAUSED
224-
}
225-
}
210+
override val state: Flow<MediaCacheState> =
211+
downloader.getProgressFlow(downloadId).map { it.status.toMediaCacheState() }
226212

227213
override val canPlay: Flow<Boolean>
228214
get() = downloader.getProgressFlow(downloadId).map {
@@ -361,6 +347,25 @@ class HttpMediaCacheEngine(
361347
}
362348
}
363349

350+
internal fun DownloadStatus.toMediaCacheState(): MediaCacheState {
351+
return when (this) {
352+
DownloadStatus.DOWNLOADING,
353+
DownloadStatus.MERGING,
354+
-> MediaCacheState.IN_PROGRESS
355+
356+
DownloadStatus.INITIALIZING,
357+
DownloadStatus.PAUSED,
358+
-> MediaCacheState.PAUSED
359+
360+
DownloadStatus.FAILED,
361+
DownloadStatus.CANCELED,
362+
-> MediaCacheState.FAILED
363+
364+
DownloadStatus.COMPLETED,
365+
-> MediaCacheState.COMPLETED
366+
}
367+
}
368+
364369
internal fun DownloadProgress.toHttpCacheProgress(): Progress {
365370
if (status == DownloadStatus.COMPLETED) {
366371
return 1f.toProgress()

app/shared/app-data/src/commonMain/kotlin/domain/media/cache/engine/TorrentMediaCacheEngine.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2024-2025 OpenAni and contributors.
2+
* Copyright (C) 2024-2026 OpenAni and contributors.
33
*
44
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
55
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
@@ -129,7 +129,7 @@ class TorrentMediaCacheEngine(
129129
override val metadata: MediaCacheMetadata, // 注意, 我们不能写 check 检查这些属性, 因为可能会有旧版本的数据
130130
val fileHandle: FileHandle
131131
) : MediaCache, SynchronizedObject() {
132-
override val state: MutableStateFlow<MediaCacheState> = MutableStateFlow(
132+
private val desiredState = MutableStateFlow(
133133
MediaCacheState.IN_PROGRESS,
134134
)
135135

@@ -188,10 +188,20 @@ class TorrentMediaCacheEngine(
188188
}
189189
}.flowOn(flowDispatcher)
190190

191+
override val state: Flow<MediaCacheState> =
192+
combine(desiredState, fileHandle.state, fileStats) { currentState, handleState, stats ->
193+
when {
194+
handleState == null -> MediaCacheState.FAILED
195+
stats.isDownloadFinished -> MediaCacheState.COMPLETED
196+
currentState == MediaCacheState.PAUSED -> MediaCacheState.PAUSED
197+
else -> MediaCacheState.IN_PROGRESS
198+
}
199+
}.flowOn(flowDispatcher)
200+
191201
override suspend fun pause() {
192202
if (isDeleted.value) return
203+
desiredState.value = MediaCacheState.PAUSED
193204
fileHandle.handle.first()?.pause()
194-
state.value = MediaCacheState.PAUSED
195205
}
196206

197207
override suspend fun close() {
@@ -202,7 +212,7 @@ class TorrentMediaCacheEngine(
202212
override suspend fun resume() {
203213
if (isDeleted.value) return
204214
val file = fileHandle.handle.first()
205-
state.value = MediaCacheState.IN_PROGRESS
215+
desiredState.value = MediaCacheState.IN_PROGRESS
206216
logger.info { "Resuming file: $file" }
207217
file?.resume(FilePriority.NORMAL)
208218
}
@@ -590,4 +600,4 @@ class TorrentMediaCacheEngine(
590600

591601
return file
592602
}
593-
}
603+
}

app/shared/app-data/src/commonMain/kotlin/domain/media/cache/storage/HttpMediaCacheStorage.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2024-2025 OpenAni and contributors.
2+
* Copyright (C) 2024-2026 OpenAni and contributors.
33
*
44
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
55
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
@@ -152,6 +152,9 @@ private class PendingHttpMediaCache(
152152
when (desiredState.value) {
153153
MediaCacheState.IN_PROGRESS -> if (resume) cache.resume()
154154
MediaCacheState.PAUSED -> cache.pause()
155+
MediaCacheState.FAILED,
156+
MediaCacheState.COMPLETED,
157+
-> Unit
155158
}
156159
}
157160

app/shared/ui-cache/src/commonMain/kotlin/ui/cache/CacheManagementScreen.kt

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ import androidx.compose.material.icons.filled.Language
3737
import androidx.compose.material.icons.filled.SelectAll
3838
import androidx.compose.material.icons.rounded.Close
3939
import androidx.compose.material.icons.rounded.Delete
40-
import androidx.compose.material.icons.rounded.DownloadDone
41-
import androidx.compose.material.icons.rounded.Downloading
4240
import androidx.compose.material.icons.rounded.Info
4341
import androidx.compose.material.icons.rounded.MoreVert
4442
import androidx.compose.material.icons.rounded.Pause
@@ -95,7 +93,7 @@ import me.him188.ani.app.ui.cache.components.CacheFilterAndSortState
9593
import me.him188.ani.app.ui.cache.components.CacheGroupState
9694
import me.him188.ani.app.ui.cache.components.CacheManagementOverallStats
9795
import me.him188.ani.app.ui.cache.components.CacheSelectionState
98-
import me.him188.ani.app.ui.cache.components.CacheStatusFilter
96+
import me.him188.ani.app.ui.cache.components.DownloadStateIcon
9997
import me.him188.ani.app.ui.cache.components.TestCacheGroupSates
10098
import me.him188.ani.app.ui.cache.components.createTestMediaStats
10199
import me.him188.ani.app.ui.cache.components.rememberCacheFilterAndSortState
@@ -570,7 +568,7 @@ private fun CacheManagementTopBar(
570568

571569

572570
@Composable
573-
private fun DeleteActionDialog(
571+
internal fun DeleteActionDialog(
574572
onDismiss: () -> Unit,
575573
onConfirm: () -> Unit,
576574
) {
@@ -681,8 +679,6 @@ private fun CacheListItem(
681679
) {
682680
var showMenu by rememberSaveable { mutableStateOf(false) }
683681
var showConfirm by rememberSaveable { mutableStateOf(false) }
684-
val statusIcon =
685-
if (entry.status == CacheStatusFilter.Finished) Icons.Rounded.DownloadDone else Icons.Rounded.Downloading
686682

687683
if (showConfirm) {
688684
DeleteActionDialog(
@@ -747,7 +743,7 @@ private fun CacheListItem(
747743
horizontalArrangement = Arrangement.spacedBy(4.dp),
748744
verticalAlignment = Alignment.CenterVertically,
749745
) {
750-
Icon(statusIcon, null)
746+
DownloadStateIcon(entry.state)
751747
if (selectionMode) {
752748
Checkbox(
753749
checked = selected,
@@ -820,58 +816,86 @@ private fun renderEngineIcon(key: MediaCacheEngineKey) = when (key) {
820816
}
821817

822818
@Composable
823-
private fun CacheActionDropdown(
819+
internal fun CacheActionDropdown(
824820
show: Boolean,
825821
onDismiss: () -> Unit,
826822
episode: CacheEpisodeState,
827823
onPlay: () -> Unit,
828824
onResume: () -> Unit,
829825
onPause: () -> Unit,
830826
onDelete: () -> Unit,
831-
onViewDetail: () -> Unit,
827+
onViewDetail: (() -> Unit)? = null,
828+
modifier: Modifier = Modifier,
829+
offset: DpOffset = DpOffset.Zero,
832830
) {
833831
val toaster = LocalToaster.current
834832
DropdownMenu(
835833
expanded = show,
836834
onDismissRequest = onDismiss,
837-
offset = DpOffset(x = (-20).dp, y = 0.dp),
835+
modifier = modifier,
836+
offset = offset,
838837
) {
839838
if (!episode.isFinished) {
840839
if (episode.isPaused) {
841840
DropdownMenuItem(
842841
text = { Text("继续下载") },
843842
leadingIcon = { Icon(Icons.Rounded.Restore, null) },
844-
onClick = onResume,
843+
onClick = {
844+
onResume()
845+
onDismiss()
846+
},
845847
)
846-
} else {
848+
} else if (!episode.isFailed) {
847849
DropdownMenuItem(
848850
text = { Text("暂停下载") },
849851
leadingIcon = { Icon(Icons.Rounded.Pause, null) },
850-
onClick = onPause,
852+
onClick = {
853+
onPause()
854+
onDismiss()
855+
},
851856
)
852857
}
853858
}
854-
DropdownMenuItem(
855-
text = { Text("播放") },
856-
leadingIcon = { Icon(Icons.Rounded.PlayArrow, null) },
857-
onClick = {
858-
when (episode.playability) {
859-
CacheEpisodeState.Playability.PLAYABLE -> onPlay()
860-
CacheEpisodeState.Playability.INVALID_SUBJECT_EPISODE_ID -> toaster.toast("缓存信息无效,无法播放")
861-
CacheEpisodeState.Playability.STREAMING_NOT_SUPPORTED -> toaster.toast("此资源不支持边下边播,请等待下载完成")
862-
}
863-
},
864-
)
865-
DropdownMenuItem(
866-
text = { Text("更多信息") },
867-
leadingIcon = { Icon(Icons.Rounded.Info, null) },
868-
onClick = onViewDetail,
869-
)
859+
if (!episode.isFailed) {
860+
DropdownMenuItem(
861+
text = { Text("播放") },
862+
leadingIcon = { Icon(Icons.Rounded.PlayArrow, null) },
863+
onClick = {
864+
when (episode.playability) {
865+
CacheEpisodeState.Playability.PLAYABLE -> {
866+
onPlay()
867+
onDismiss()
868+
}
869+
870+
CacheEpisodeState.Playability.INVALID_SUBJECT_EPISODE_ID -> {
871+
toaster.toast("缓存信息无效,无法播放")
872+
}
873+
874+
CacheEpisodeState.Playability.STREAMING_NOT_SUPPORTED -> {
875+
toaster.toast("此资源不支持边下边播,请等待下载完成")
876+
}
877+
}
878+
},
879+
)
880+
}
881+
onViewDetail?.let {
882+
DropdownMenuItem(
883+
text = { Text("更多信息") },
884+
leadingIcon = { Icon(Icons.Rounded.Info, null) },
885+
onClick = {
886+
it()
887+
onDismiss()
888+
},
889+
)
890+
}
870891

871892
DropdownMenuItem(
872893
text = { Text("删除", color = MaterialTheme.colorScheme.error) },
873894
leadingIcon = { Icon(Icons.Rounded.Delete, null, tint = MaterialTheme.colorScheme.error) },
874-
onClick = onDelete,
895+
onClick = {
896+
onDelete()
897+
onDismiss()
898+
},
875899
)
876900
}
877901
}

0 commit comments

Comments
 (0)