feat(app,android,ios): display type, artwork fade-in, shimmer, lifting tiles and paged shelves

A first pass at making the three clients look like a streaming app rather
than a file browser, with no new data behind it.

- Type: a Hero rung (34) set in Inter Display Bold, the optical size Inter
  draws for large text, for the banner's name and a title page's name where
  no logo stands in. Desktop `Role::Hero` and `theme::display`; Android and
  iOS `displayLarge`, at a tighter 1.1 leading. The font has its private use
  mappings stripped like the other two cuts.
- Artwork fades in over its skeleton on Android and iOS, as it already did
  on desktop cards; art already decoded draws at once. Desktop episode
  stills and logos fade in too.
- Skeletons shimmer: a soft diagonal highlight instead of a pulse.
- Desktop tiles lift towards the viewer under the pointer, capped so a
  lifted tile never reaches its neighbour, and keyboard focus gets a ring
  of its own.
- Desktop shelves share one `ui::shelf`: edges fade into the page and, under
  the pointer, round buttons page the row along.
- Genres are chips on Android and iOS; quality (4K, HDR, 10-bit, Dual audio)
  moves out of the facts line into small outlined marks on all three.
- Desktop title page and hero buttons use a tonal style beside the accent
  one, and the player's top bar a dark glass style, instead of egui's stock
  buttons. Episode numbers leave monospace for the type ramp.
- The phone players' top and bottom bars are gradients rather than flat
  bands, and a double-tap seek is felt as well as seen.
This commit is contained in:
nvrl committed 2026-10-30 20:36:09 +01:00
1 parent 3ebfd1a57b
commit 255e463fc8
33 files changed
+886 -290

No files matched your search

+18 -5
View File
@@ -123,7 +123,7 @@ struct PlayerView: View {
}
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.trailing, 16)
.padding(.bottom, showControls ? 132 : 24)
.padding(.bottom, showControls ? 160 : 24)
// Only while nothing else explains the still picture: an error
// is the more useful thing to say.
@@ -224,8 +224,15 @@ struct PlayerView: View {
}
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(scheme.scrim.opacity(0.45).ignoresSafeArea(edges: [.top, .horizontal]))
.padding(.top, 4)
.padding(.bottom, 28)
// A gradient rather than a band: dark where the text is, and gone by
// the time it reaches the picture. Black whatever the theme, since
// what is under it is a video frame.
.background(
LinearGradient(colors: [.black.opacity(0.75), .clear], startPoint: .top, endPoint: .bottom)
.ignoresSafeArea(edges: [.top, .horizontal])
)
}
/// Show or hide the controls; shown and playing, they hide themselves.
@@ -242,6 +249,8 @@ struct PlayerView: View {
private func doubleTap(at x: CGFloat, width: CGFloat) {
let third = width / 3
let seconds = x < third ? -doubleTapSeconds : x > width - third ? doubleTapSeconds : 0
// Felt as well as seen: the bubble is under the thumb that asked for it.
UIImpactFeedbackGenerator(style: .light).impactOccurred()
if seconds == 0 {
host.setPaused(!player.paused)
} else {
@@ -355,8 +364,12 @@ private struct PlayerControls: View {
}
}
.padding(.horizontal, 12)
.padding(.vertical, 4)
.background(scheme.scrim.opacity(0.68).ignoresSafeArea(edges: [.bottom, .horizontal]))
.padding(.top, 32)
.padding(.bottom, 4)
.background(
LinearGradient(colors: [.clear, .black.opacity(0.85)], startPoint: .top, endPoint: .bottom)
.ignoresSafeArea(edges: [.bottom, .horizontal])
)
}
private func toggleMute() {