mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
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.
103 lines
3.6 KiB
Swift
103 lines
3.6 KiB
Swift
import SwiftUI
|
|
|
|
/// The type ramp, which is the desktop client's type ramp, under Material's
|
|
/// names so a view ported from the Android client asks for the same style.
|
|
///
|
|
/// Inter, Regular and SemiBold, as on the desktop and on Android: a `Medium`
|
|
/// role resolves to Regular. The rungs, largest first:
|
|
/// 34 / 26 / 20 / 18 / 17 / 15 / 14 / 13 / 12 / 11. The top one is the hero
|
|
/// line — the banner's name and the title page's — in Inter Display Bold and
|
|
/// set tighter. `TypeRampTests` pins them.
|
|
enum TypeRole: CaseIterable {
|
|
case displayLarge, displayMedium, displaySmall
|
|
case headlineLarge, headlineMedium, headlineSmall
|
|
case titleLarge, titleMedium, titleSmall
|
|
case bodyLarge, bodyMedium, bodySmall
|
|
case labelLarge, labelMedium, labelSmall
|
|
|
|
var size: CGFloat {
|
|
switch self {
|
|
case .displayLarge: Ramp.hero
|
|
case .displayMedium, .headlineLarge, .headlineMedium: Ramp.display
|
|
case .displaySmall, .titleLarge: Ramp.title
|
|
case .headlineSmall: Ramp.heading
|
|
case .titleMedium: Ramp.section
|
|
case .titleSmall: Ramp.subhead
|
|
case .bodyLarge: Ramp.body
|
|
case .bodyMedium, .labelLarge: Ramp.label
|
|
case .bodySmall, .labelMedium: Ramp.caption
|
|
case .labelSmall: Ramp.micro
|
|
}
|
|
}
|
|
|
|
var semibold: Bool {
|
|
switch self {
|
|
case .displayMedium, .displaySmall, .headlineLarge, .headlineMedium, .headlineSmall, .titleLarge:
|
|
true
|
|
default:
|
|
false
|
|
}
|
|
}
|
|
|
|
/// The Dynamic Type style each rung scales with.
|
|
var relative: Font.TextStyle {
|
|
switch size {
|
|
case Ramp.hero: .largeTitle
|
|
case Ramp.display: .title
|
|
case Ramp.title: .title2
|
|
case Ramp.heading: .title3
|
|
case Ramp.section: .headline
|
|
case Ramp.subhead: .subheadline
|
|
case Ramp.body: .body
|
|
case Ramp.label: .callout
|
|
case Ramp.caption: .footnote
|
|
default: .caption
|
|
}
|
|
}
|
|
|
|
/// Line height is derived rather than named per style: 1.35 of the size,
|
|
/// as on Android. Inter's own leading is about 1.21, so the rest is spacing.
|
|
var lineSpacing: CGFloat {
|
|
size * ((self == .displayLarge ? Ramp.heroLineHeight : Ramp.lineHeight) - 1.21)
|
|
}
|
|
|
|
/// The PostScript name of the face this style is set in.
|
|
func face(semibold: Bool? = nil) -> String {
|
|
if self == .displayLarge, semibold == nil { return "InterDisplay-Bold" }
|
|
return (semibold ?? self.semibold) ? "Inter-SemiBold" : "Inter-Regular"
|
|
}
|
|
}
|
|
|
|
enum Ramp {
|
|
static let hero: CGFloat = 34
|
|
static let display: CGFloat = 26
|
|
static let title: CGFloat = 20
|
|
static let heading: CGFloat = 18
|
|
static let section: CGFloat = 17
|
|
static let subhead: CGFloat = 15
|
|
static let body: CGFloat = 14
|
|
static let label: CGFloat = 13
|
|
static let caption: CGFloat = 12
|
|
static let micro: CGFloat = 11
|
|
static let lineHeight: CGFloat = 1.35
|
|
static let heroLineHeight: CGFloat = 1.1
|
|
}
|
|
|
|
extension Font {
|
|
static func inter(_ style: TypeRole, semibold: Bool? = nil) -> Font {
|
|
.custom(style.face(semibold: semibold), size: style.size, relativeTo: style.relative)
|
|
}
|
|
|
|
static func inter(size: CGFloat, semibold: Bool = false) -> Font {
|
|
.custom(semibold ? "Inter-SemiBold" : "Inter-Regular", size: size)
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
/// One rung of the ramp. `semibold` overrides the style's own weight, the
|
|
/// counterpart of Compose's `fontWeight = FontWeight.SemiBold`.
|
|
func textStyle(_ style: TypeRole, semibold: Bool? = nil) -> some View {
|
|
font(.inter(style, semibold: semibold)).lineSpacing(style.lineSpacing)
|
|
}
|
|
}
|