mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
feat(android,ios): mark watched up to an episode, and undo an episode's tick
A long press on an episode row offers "Mark watched up to here", as a right click on the desktop's tick does. It is for someone joining a show partway. Ticking one episode now goes through the same path as a season, so it gets the same Undo.
This commit is contained in:
1 parent
5c15f45ace
commit
5c0519a226
4 files changed
+49
-39
No files matched your search
@@ -811,24 +811,8 @@ class AppViewModel(context: Context, private val workManager: WorkManager) : Vie
|
||||
* at its duration where one is known, so the progress bar agrees with the
|
||||
* tick.
|
||||
*/
|
||||
fun setWatched(episode: EpisodeRecord, watched: Boolean) {
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
val engine = NativeRuntime.engine()
|
||||
val duration = engine.watchState(episode.shareId, episode.linkId)?.durationSecs
|
||||
engine.saveWatchState(
|
||||
episode.shareId,
|
||||
episode.linkId,
|
||||
if (watched) duration ?: 0.0 else 0.0,
|
||||
duration,
|
||||
watched,
|
||||
)
|
||||
}
|
||||
}.onFailure(::reportError)
|
||||
reload()
|
||||
}
|
||||
}
|
||||
/** One episode's tick, with the same Undo a season's has. */
|
||||
fun setWatched(episode: EpisodeRecord, watched: Boolean) = markEpisodes(listOf(episode), watched, episode.label)
|
||||
|
||||
fun removeAllOffline() {
|
||||
viewModelScope.launch {
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -30,6 +31,8 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
@@ -391,6 +394,14 @@ internal fun TitleScreen(
|
||||
onPause = { DownloadCoordinator.pause(context, it) },
|
||||
onResume = { DownloadCoordinator.resume(context, it) },
|
||||
onSetWatched = { onSetWatched(episode, it) },
|
||||
onWatchedUpTo = {
|
||||
val upTo = playlist.indexOfFirst { it.linkId == episode.linkId }
|
||||
onMarkEpisodes(
|
||||
playlist.take(upTo + 1),
|
||||
true,
|
||||
"${title.displayName} up to ${episode.label}",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -538,16 +549,35 @@ private fun EpisodeRow(
|
||||
onPause: (RetainedDownload) -> Unit,
|
||||
onResume: (RetainedDownload) -> Unit,
|
||||
onSetWatched: (Boolean) -> Unit,
|
||||
onWatchedUpTo: () -> Unit = {},
|
||||
) {
|
||||
// A long press, for someone joining a show partway: everything before
|
||||
// this one ticked at once rather than one by one.
|
||||
var menu by remember { mutableStateOf(false) }
|
||||
val running = download?.status == RetainedDownload.STATUS_RUNNING ||
|
||||
download?.status == RetainedDownload.STATUS_QUEUED
|
||||
val heading = episode.heading(position)
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = playerReady, onClick = onPlay)
|
||||
.combinedClickable(
|
||||
enabled = playerReady,
|
||||
onClick = onPlay,
|
||||
onLongClick = { menu = true },
|
||||
onLongClickLabel = "More for this episode",
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
) {
|
||||
DropdownMenu(expanded = menu, onDismissRequest = { menu = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Mark watched up to here") },
|
||||
leadingIcon = { Icon(Icons.Default.DoneAll, contentDescription = null) },
|
||||
onClick = {
|
||||
menu = false
|
||||
onWatchedUpTo()
|
||||
},
|
||||
)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// With metadata off the provider has no still, and Proton's own
|
||||
// thumbnail is a frame of this very episode. Where it was left sits
|
||||
|
||||
@@ -737,25 +737,9 @@ final class AppModel {
|
||||
/// Unwatching rewinds, matching the desktop client. Marking one seen puts
|
||||
/// the position at its duration where one is known, so the progress bar
|
||||
/// agrees with the tick.
|
||||
/// One episode's tick, with the same Undo a season's has.
|
||||
func setWatched(_ episode: EpisodeRecord, _ watched: Bool) {
|
||||
showWatched([episode], watched)
|
||||
Task {
|
||||
do {
|
||||
try await run { engine in
|
||||
let duration = try engine.watchState(shareId: episode.shareId, linkId: episode.linkId)?.durationSecs
|
||||
try engine.saveWatchState(
|
||||
shareId: episode.shareId,
|
||||
linkId: episode.linkId,
|
||||
positionSecs: watched ? duration ?? 0 : 0,
|
||||
durationSecs: duration,
|
||||
watched: watched
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
reportError(error)
|
||||
}
|
||||
reload()
|
||||
}
|
||||
markEpisodes([episode], watched, episode.label)
|
||||
}
|
||||
|
||||
/// Draw a watched mark the moment it is tapped, before the bridge has
|
||||
|
||||
@@ -72,7 +72,11 @@ struct TitleView: View {
|
||||
position: position,
|
||||
download: downloads.first { $0.shareId == episode.shareId && $0.linkId == episode.linkId },
|
||||
onPlay: { onPlay(playlist, playlist.firstIndex { $0.linkId == episode.linkId } ?? 0) },
|
||||
onSetWatched: { model.setWatched(episode, $0) }
|
||||
onSetWatched: { model.setWatched(episode, $0) },
|
||||
onWatchedUpTo: {
|
||||
let upTo = playlist.firstIndex { $0.linkId == episode.linkId } ?? 0
|
||||
model.markEpisodes(Array(playlist.prefix(upTo + 1)), true, "\(title.displayName) up to \(episode.label)")
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, side)
|
||||
}
|
||||
@@ -393,7 +397,11 @@ private struct EpisodeRow: View {
|
||||
let download: RetainedDownload?
|
||||
let onPlay: () -> Void
|
||||
let onSetWatched: (Bool) -> Void
|
||||
/// Everything up to and including this one, for someone joining a show
|
||||
/// partway.
|
||||
let onWatchedUpTo: () -> Void
|
||||
@Environment(\.scheme) private var scheme
|
||||
@State private var holding = false
|
||||
|
||||
private var subline: String {
|
||||
let size: String? = episode.size.map { formatBytes($0) }
|
||||
@@ -460,7 +468,11 @@ private struct EpisodeRow: View {
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 6)
|
||||
.contentShape(Rectangle())
|
||||
.tapAndHold(onPlay)
|
||||
.tapAndHold(onPlay) { holding = true }
|
||||
.confirmationDialog(episode.heading(position), isPresented: $holding) {
|
||||
Button("Mark watched up to here", action: onWatchedUpTo)
|
||||
}
|
||||
.accessibilityAction(named: "Mark watched up to here", onWatchedUpTo)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user