Eine Sekunde heisst nicht „Weiterschauen"

Die Schwelle aus den Einstellungen wirkt serverseitig, also sofort überall:
`/home/continue` liest die gespeicherte Spalte. Eine Stelle blieb trotzdem
widersprüchlich — die **Beschriftung**.

Web und Apps entschieden „Weiterschauen" gegen „Abspielen" selbst, allein
daran, ob eine Stelle über null steht. Nach einer Sekunde stand der Titel
damit nicht mehr in der Reihe, hiess aber weiter „Weiterschauen", trug
einen Fortschrittsbalken unter der Kachel und bot „Aus Weiterschauen
entfernen" an.

Der Server hatte diese Frage längst beantwortet. `ItemStateResponse` bringt
sie jetzt mit; `MediaItem.isResumable` und `.resumeProgress` sind die
einzigen Stellen, die sie noch stellen. Im Simulator nachgeprüft: nach
einer Sekunde heisst der Knopf „Abspielen", „Von vorn" fehlt, und der
Balken unter der Kachel ist weg.

Kennt ein Server das Feld nicht — Jellyfin, oder ein älterer Flaro —, gilt
wie früher die blosse Stelle.

**Und das Profilbild steht in den Apps.** `FlaroAccount` führt es mit,
`/auth/me` bringt es bei jedem Start frisch, und der Buchstabenkreis tritt
zurück, sobald eines da ist. Jellyfin bleibt beim Buchstaben — dort gibt es
kein Profilbild, und das ist keine Lücke, sondern der Stand der Dinge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-06 17:30:38 +02:00
co-authored by Claude Opus 5
parent 579a49b73b
commit e30158b84c
25 changed files with 155 additions and 39 deletions
@@ -443,17 +443,26 @@ public struct FlaroAccount: Sendable, Equatable {
public let userName: String
public let isAdministrator: Bool
/// Das eigene Bild, als Schlüssel in die Bildablage — `nil`: der Buchstabe.
///
/// **Mit Vorgabewert, damit Jellyfin unberührt bleibt.** Dort gibt es
/// kein Profilbild, und ein Pflichtfeld hier zwänge jeden Aufrufer, sich
/// dazu zu äussern.
public let avatarImage: String?
public init(
baseURL: URL,
serverName: String,
userID: String,
userName: String,
isAdministrator: Bool
isAdministrator: Bool,
avatarImage: String? = nil
) {
self.baseURL = baseURL
self.serverName = serverName
self.userID = userID
self.userName = userName
self.isAdministrator = isAdministrator
self.avatarImage = avatarImage
}
}
@@ -38,6 +38,8 @@ enum Wire {
let id: String
let userName: String
let isAdministrator: Bool
/// Das Profilbild, als Schlüssel in die Bildablage des Servers.
let avatarImage: String?
}
struct Library: Decodable, Sendable {
@@ -78,6 +80,12 @@ enum Wire {
let rating: Int?
let dismissed: Bool
let lastPlayedAt: Date?
/// Ob der Server den Titel in „Weiterschauen" führt.
///
/// **Nicht dasselbe wie „Stelle über null".** Seit es eine
/// einstellbare Untergrenze gibt, sind das zwei Fragen: eine Sekunde
/// wird gemerkt, zählt aber nicht als angefangen.
let inProgress: Bool?
}
struct Movie: Decodable, Sendable {
@@ -20,6 +20,7 @@ extension Wire.Movie {
resumeMilliseconds: state.map { Int($0.positionSeconds * 1000) },
lastPlayedAt: state?.lastPlayedAt,
isFavourite: state?.favourite ?? false,
isInProgress: state?.inProgress,
availableFrom: publishAt,
versions: [Wire.fassung(id: id, files: files)]
)
@@ -69,6 +70,7 @@ extension Wire.Episode {
resumeMilliseconds: state.map { Int($0.positionSeconds * 1000) },
lastPlayedAt: state?.lastPlayedAt,
isFavourite: state?.favourite ?? false,
isInProgress: state?.inProgress,
availableFrom: publishAt,
versions: [Wire.fassung(id: id, files: files)]
)
@@ -104,6 +106,7 @@ extension Wire.Started {
resumeMilliseconds: Int(state.positionSeconds * 1000),
lastPlayedAt: state.lastPlayedAt,
isFavourite: state.favourite,
isInProgress: state.inProgress,
availableFrom: publishAt
)
}
@@ -84,7 +84,8 @@ public enum FlaroSignIn {
serverName: gruss.serverName,
userID: antwort.user.id,
userName: antwort.user.userName,
isAdministrator: antwort.user.isAdministrator), client)
isAdministrator: antwort.user.isAdministrator,
avatarImage: antwort.user.avatarImage), client)
}
/// Nimmt ein gespeichertes Token wieder auf.
@@ -111,7 +112,8 @@ public enum FlaroSignIn {
serverName: gruss?.serverName ?? baseURL.host() ?? "Flaro",
userID: wer.id,
userName: wer.userName,
isAdministrator: wer.isAdministrator), client)
isAdministrator: wer.isAdministrator,
avatarImage: wer.avatarImage), client)
}
// MARK: - Der Weg über einen Code
@@ -179,7 +181,8 @@ public enum FlaroSignIn {
serverName: gruss?.serverName ?? baseURL.host() ?? "Flaro",
userID: anmeldung.user.id,
userName: anmeldung.user.userName,
isAdministrator: anmeldung.user.isAdministrator)), client)
isAdministrator: anmeldung.user.isAdministrator,
avatarImage: anmeldung.user.avatarImage)), client)
case "expired":
return (.expired, nil)
default:
@@ -71,6 +71,18 @@ public struct MediaItem: Sendable, Equatable, Identifiable, Codable {
/// eine Fähigkeit, die es nur auf einem gibt.
public let isFavourite: Bool
/// Ob der Server den Titel als angefangen führt.
///
/// **Die Entscheidung gehört dem Server, nicht der Beschriftung.** „Hat
/// eine Stelle über null" und „ist angefangen" waren dasselbe, bis es eine
/// einstellbare Untergrenze gab. Seitdem heisst ein Titel, in den jemand
/// eine Sekunde hineingeschaut hat, nicht mehr „Weiterschauen" — er steht
/// ja auch nicht mehr in der Reihe.
///
/// `nil` heisst: der Server sagt nichts dazu (Jellyfin, oder ein älterer
/// Flaro). Dann gilt wie früher die blosse Stelle.
public let isInProgress: Bool?
/// Ab wann es den Titel gibt — `nil` heisst: jetzt.
///
/// **Ein geplanter Titel steht in der Mediathek, läuft aber nicht.** Das
@@ -125,6 +137,7 @@ public struct MediaItem: Sendable, Equatable, Identifiable, Codable {
lastPlayedAt: Date? = nil,
addedAt: Date? = nil,
isFavourite: Bool = false,
isInProgress: Bool? = nil,
availableFrom: Date? = nil,
episodeCount: Int? = nil,
playedEpisodeCount: Int? = nil,
@@ -164,6 +177,7 @@ public struct MediaItem: Sendable, Equatable, Identifiable, Codable {
self.lastPlayedAt = lastPlayedAt
self.addedAt = addedAt
self.isFavourite = isFavourite
self.isInProgress = isInProgress
self.availableFrom = availableFrom
self.episodeCount = episodeCount
self.playedEpisodeCount = playedEpisodeCount
@@ -235,6 +249,20 @@ extension MediaItem {
public var isWatched: Bool { (playCount ?? 0) > 0 }
/// Ob die Beschriftung „Weiterschauen" heissen soll.
///
/// Eine Stelle, die beide Fälle kennt: was der Server sagt, und was ohne
/// seine Auskunft gilt. Stünde der Zweifel an jedem Knopf einzeln, hiesse
/// derselbe Titel auf der Startseite anders als auf seiner Seite.
public var isResumable: Bool { isInProgress ?? (watchProgress != nil) }
/// Der Balken unter der Kachel — nur, wenn der Titel wirklich angefangen ist.
///
/// **Sonst sagt die Kachel etwas anderes als der Knopf.** Ein Strich unter
/// dem Plakat heisst „du bist mittendrin". Für eine Sekunde, in die jemand
/// hineingeschaut hat, ist das keine Auskunft, sondern ein Fussel.
public var resumeProgress: Double? { isResumable ? watchProgress : nil }
/// Ob der Titel erst noch kommt.
///
/// Gefragt wird an einer Stelle, beantwortet an einer: sonst steht an