feat(core,app,android,ios,cli): look up new titles after every crawl, show presses and work on iOS

Metadata was only fetched after adding a share (mobile) or from the
Settings buttons (desktop), so a show or episode that turned up in an
existing share stayed bare. Every crawl is now followed by a match run
when enrichment is on; `plan` makes no requests for a library that is
already matched. A stored episode guide is fetched again when a file
newer than it has no named entry, and daily for two weeks while an
aired episode is still unnamed.

Removing a share now carries its watch state to any other share holding
the same file, `pstr remove` drops the share's catalog rows, and the app
forgets catalog rows of shares no longer configured — an older `pstr
remove` left them, and every episode showed twice.

iOS, for a slow iPad:
- swipe back on a settings page uses its own edge strip; on the whole
  page it also pressed the row under the finger and opened the metadata
  sheet
- "Change match" from the tile menu opens once the menu has dismissed,
  not after a guessed 350 ms that a slow device missed
- the activity banner tracks each running line by itself (also Android),
  so overlapping work no longer leaves it spinning forever
- buttons, tiles and rows draw their press; marking watched shows at
  once; remove/repair share, sign out and deletions show the banner;
  the player says "Opening…" while the stream opens
- posters are read into a reserved buffer instead of a byte at a time
This commit is contained in:
nvrl committed 2026-10-12 18:36:33 +02:00
1 parent 2f3be1d193
commit 73973841c2
19 files changed
+692 -113

No files matched your search

+7 -5
View File
@@ -73,10 +73,12 @@ struct PlayerView: View {
// Only while nothing else explains the still picture: an error
// is the more useful thing to say.
if player.stalled && host.playbackError == nil {
if (host.opening || player.stalled) && host.playbackError == nil {
VStack(spacing: 12) {
ProgressView().controlSize(.large).tint(scheme.primary)
if player.buffering {
if host.opening {
Text("Opening…").textStyle(.labelMedium).foregroundStyle(scheme.onSurface)
} else if player.buffering {
Text(player.cachePercent > 0 ? "Buffering… \(Int(player.cachePercent.rounded()))%" : "Buffering…")
.textStyle(.labelMedium).foregroundStyle(scheme.onSurface)
}
@@ -295,7 +297,7 @@ private struct CompactIcon: View {
.frame(width: 40, height: 40)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.buttonStyle(PressableStyle(circle: true, wash: .white))
.disabled(!enabled)
.accessibilityLabel(label)
}
@@ -342,7 +344,7 @@ private struct UpNextCard: View {
}
.clipShape(RoundedRectangle(cornerRadius: Corner.standard))
}
.buttonStyle(.plain)
.buttonStyle(PressableStyle(wash: .white))
.accessibilityLabel("Play now")
VStack(alignment: .leading, spacing: 2) {
Text("Up next in \(remaining)s").textStyle(.labelMedium).foregroundStyle(scheme.onSurfaceVariant)
@@ -515,7 +517,7 @@ private struct EpisodeSheet: View {
.background(current ? scheme.secondaryContainer : .clear)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.buttonStyle(.pressable)
}
}