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
|
* at its duration where one is known, so the progress bar agrees with the
|
||||||
* tick.
|
* tick.
|
||||||
*/
|
*/
|
||||||
fun setWatched(episode: EpisodeRecord, watched: Boolean) {
|
/** One episode's tick, with the same Undo a season's has. */
|
||||||
viewModelScope.launch {
|
fun setWatched(episode: EpisodeRecord, watched: Boolean) = markEpisodes(listOf(episode), watched, episode.label)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAllOffline() {
|
fun removeAllOffline() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableFloatStateOf
|
|||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -391,6 +394,14 @@ internal fun TitleScreen(
|
|||||||
onPause = { DownloadCoordinator.pause(context, it) },
|
onPause = { DownloadCoordinator.pause(context, it) },
|
||||||
onResume = { DownloadCoordinator.resume(context, it) },
|
onResume = { DownloadCoordinator.resume(context, it) },
|
||||||
onSetWatched = { onSetWatched(episode, 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,
|
onPause: (RetainedDownload) -> Unit,
|
||||||
onResume: (RetainedDownload) -> Unit,
|
onResume: (RetainedDownload) -> Unit,
|
||||||
onSetWatched: (Boolean) -> 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 ||
|
val running = download?.status == RetainedDownload.STATUS_RUNNING ||
|
||||||
download?.status == RetainedDownload.STATUS_QUEUED
|
download?.status == RetainedDownload.STATUS_QUEUED
|
||||||
val heading = episode.heading(position)
|
val heading = episode.heading(position)
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.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),
|
.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) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
// With metadata off the provider has no still, and Proton's own
|
// 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
|
// 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
|
/// Unwatching rewinds, matching the desktop client. Marking one seen puts
|
||||||
/// the position at its duration where one is known, so the progress bar
|
/// the position at its duration where one is known, so the progress bar
|
||||||
/// agrees with the tick.
|
/// agrees with the tick.
|
||||||
|
/// One episode's tick, with the same Undo a season's has.
|
||||||
func setWatched(_ episode: EpisodeRecord, _ watched: Bool) {
|
func setWatched(_ episode: EpisodeRecord, _ watched: Bool) {
|
||||||
showWatched([episode], watched)
|
markEpisodes([episode], watched, episode.label)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw a watched mark the moment it is tapped, before the bridge has
|
/// Draw a watched mark the moment it is tapped, before the bridge has
|
||||||
|
|||||||
@@ -72,7 +72,11 @@ struct TitleView: View {
|
|||||||
position: position,
|
position: position,
|
||||||
download: downloads.first { $0.shareId == episode.shareId && $0.linkId == episode.linkId },
|
download: downloads.first { $0.shareId == episode.shareId && $0.linkId == episode.linkId },
|
||||||
onPlay: { onPlay(playlist, playlist.firstIndex { $0.linkId == episode.linkId } ?? 0) },
|
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)
|
.padding(.horizontal, side)
|
||||||
}
|
}
|
||||||
@@ -393,7 +397,11 @@ private struct EpisodeRow: View {
|
|||||||
let download: RetainedDownload?
|
let download: RetainedDownload?
|
||||||
let onPlay: () -> Void
|
let onPlay: () -> Void
|
||||||
let onSetWatched: (Bool) -> 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
|
@Environment(\.scheme) private var scheme
|
||||||
|
@State private var holding = false
|
||||||
|
|
||||||
private var subline: String {
|
private var subline: String {
|
||||||
let size: String? = episode.size.map { formatBytes($0) }
|
let size: String? = episode.size.map { formatBytes($0) }
|
||||||
@@ -460,7 +468,11 @@ private struct EpisodeRow: View {
|
|||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
.contentShape(Rectangle())
|
.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