Files
Flaro/Flixy/Views/Home/MediaRow.swift
T
ScarriffleandClaude Opus 5 f8ada44a8c Mac: näher am iPad — Schrift, Seitenleiste, Formulare
Drei Gründe, warum der Mac anders aussah, und keiner davon war Geschmack.

**Die Schrift.** macOS rendert dieselben Textstile deutlich kleiner als iOS:
`largeTitle` misst dort 26 Punkt statt 34, Fließtext 13 statt 17. Eine
Oberfläche, die auf dem iPad stimmt, wirkt damit zusammengeschrumpft. Auf dem
Mac stehen jetzt feste Größen — die von iOS. Das kostet dort Dynamic Type;
vertretbar, weil macOS keine systemweite Textgröße kennt und man ein Fenster
stattdessen größer ziehen kann.

**Der Aufmacher wächst mit.** Das war der zweite, größere Teil: das Bild ist so
breit wie das Fenster, die Schrift darauf war es nicht. Bei 1280 Punkt Breite
wirkt ein 34-Punkt-Titel verloren, bei 834 auf dem iPad genau richtig. Titel,
Schriftzug, Angaben und Abstände richten sich jetzt nach der Breite des
Aufmachers — mit dem iPad als Bezug und einem Deckel, damit ein Fenster über
die ganze Wand nicht in Plakatschrift endet. Die Beschreibung bekommt
außerdem eine Zeilenbreite: ein Satz über 1280 Punkt ist unlesbar, das Auge
verliert die Zeile.

**Die Seitenleiste.** Sie richtet sich nach ihrem Inhalt, und bei Wörtern wie
„Start" fällt sie auf eine Breite zusammen, in der schon der Kontoname darunter
nicht mehr Platz hatte — daher das „S…". Die Beschriftungen haben jetzt eine
Mindestbreite.

**Die Formulare.** Auf dem Mac nahm SwiftUI den Spalten-Stil: Beschriftungen
rechts ausgerichtet, keine abgesetzten Abschnitte. Jetzt derselbe gruppierte
Stil wie auf iOS.

Dazu: Scrollbalken sind weg. Der graue Streifen unter dem Aufmacher kam daher,
dass in den Systemeinstellungen „Scrollbalken immer einblenden" steht —
dagegen reicht `.hidden` nicht, `.never` schon. Und das Fenster öffnet mit
1280 × 820 statt in Notizzettelgröße, ohne graue Titelleiste.

263 Tests grün, iOS baut unverändert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 15:59:22 +02:00

121 lines
4.3 KiB
Swift

import SwiftUI
import FlixyDesign
import MediaKit
/// Eine waagerechte Reihe von Titeln.
struct MediaRow: View {
let row: MediaRowGroup
@Environment(FlixySession.self) private var session
/// Breit genug, dass die übernächste Kachel angeschnitten ist — das zeigt,
/// dass es weitergeht, ohne einen Pfeil dafür zu brauchen.
private let landscapeWidth: CGFloat = 220
/// Schmaler, weil hochkant: bei gleicher Höhe passen so mehr Plakate
/// nebeneinander, und mehr Auswahl auf einen Blick ist genau der Zweck.
private let posterWidth: CGFloat = 116
var body: some View {
VStack(alignment: .leading, spacing: FlixySpacing.regular) {
Text(row.title)
.font(.flixyRowHeader)
.foregroundStyle(Color.flixyTextPrimary)
.padding(.horizontal, FlixySpacing.screenMargin)
ScrollView(.horizontal) {
LazyHStack(spacing: FlixySpacing.regular) {
ForEach(row.items) { item in
NavigationLink(value: MediaRoute(ratingKey: item.id, title: item.title)) {
card(for: item)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, FlixySpacing.screenMargin)
// Ohne dieses Ziel rasten die Kacheln nicht ein und die Reihe
// bleibt an beliebiger Stelle stehen.
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
.scrollIndicators(.never)
}
}
/// Szenenbild beim Weiterschauen, sonst das Plakat.
///
/// Angefangenes erkennt man an der Szene, an der man aufgehört hat —
/// dafür ist das Querformat richtig. Wer dagegen aussucht, was er als
/// Nächstes sehen will, sucht nach Plakaten.
@ViewBuilder
private func card(for item: MediaItem) -> some View {
if row.isContinueWatching {
LandscapeCard(
title: item.rowTitle,
subtitle: item.cardSubtitle,
artworkURL: session.backend?.artworkURL(
path: item.rowArtworkPath,
width: 440,
height: 248
),
progress: item.watchProgress
)
.frame(width: landscapeWidth)
} else {
PosterCard(
title: item.rowTitle,
subtitle: item.cardSubtitle,
artworkURL: session.backend?.artworkURL(
path: item.posterArtworkPath,
width: 300,
height: 450
),
progress: item.watchProgress
)
.frame(width: posterWidth)
}
}
}
extension MediaItem {
/// Der Titel auf einer Kachel in einer Reihe.
///
/// Groß steht, worum es geht: bei einer Folge ihr eigener Titel, bei
/// einer Staffel der Name der Serie. Die Einordnung — Serie, Staffel —
/// gehört darunter. „Staffel 10" allein als Überschrift sagt nichts
/// darüber, welche Serie gemeint ist.
var rowTitle: String {
switch kind {
case .episode: title
// Die Serie steht je nach Antwort mal unter `parentTitle`, mal unter
// `seriesTitle`. Fehlt beides, bleibt „Staffel 2" — besser als
// gar keine Beschriftung.
case .season: parentTitle ?? seriesTitle ?? title
default: title
}
}
/// Welches Bild auf die Querformat-Kachel gehört.
///
/// Bei Folgen ist `thumb` das Szenenbild der Folge — genau richtig. Bei
/// Filmen ist `thumb` das Hochkant-Plakat, das im Querformat beschnitten
/// würde; dort ist `art` (das Hintergrundbild) die bessere Wahl.
var rowArtworkPath: String? {
switch kind {
case .episode: thumb ?? seriesThumb
default: art ?? thumb
}
}
/// Welches Bild auf die Hochkant-Kachel gehört.
///
/// Bei Folgen das Plakat der Serie statt des Szenenbilds: ein 16:9-Bild
/// hochkant beschnitten zeigt einen zufälligen Ausschnitt und ist zum
/// Wiedererkennen wertlos.
var posterArtworkPath: String? {
switch kind {
case .episode: seriesThumb ?? parentThumb ?? thumb
default: thumb
}
}
}