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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d54954b9c2
commit
f8ada44a8c
@@ -67,6 +67,12 @@ struct FlixyApp: App {
|
|||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
RootView()
|
RootView()
|
||||||
|
// Eine Untergrenze, unter der die Oberfläche nicht mehr
|
||||||
|
// aufgeht: schmaler als das hier klappt die Seitenleiste zu
|
||||||
|
// und die Reihen zeigen anderthalb Kacheln.
|
||||||
|
#if os(macOS)
|
||||||
|
.frame(minWidth: 900, minHeight: 620)
|
||||||
|
#endif
|
||||||
.environment(session)
|
.environment(session)
|
||||||
.environment(playback)
|
.environment(playback)
|
||||||
.environment(playbackSettings)
|
.environment(playbackSettings)
|
||||||
@@ -102,6 +108,17 @@ struct FlixyApp: App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modelContainer(modelContainer)
|
.modelContainer(modelContainer)
|
||||||
|
#if os(macOS)
|
||||||
|
// Ohne diese Angabe öffnet macOS ein Fenster von der Größe eines
|
||||||
|
// Notizzettels — und darin fällt der Anmeldebildschirm in sich
|
||||||
|
// zusammen, weil seine Abstände dehnbar sind.
|
||||||
|
.defaultSize(width: 1280, height: 820)
|
||||||
|
// Das Fenster darf nicht kleiner werden, als der Inhalt verträgt.
|
||||||
|
.windowResizability(.contentMinSize)
|
||||||
|
// Keine graue Titelleiste über einem dunklen Bild: die Ampelknöpfe
|
||||||
|
// schweben über dem Inhalt, wie man es von Fernseh-Apps kennt.
|
||||||
|
.windowStyle(.hiddenTitleBar)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ struct ParentLinks: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, FlixySpacing.screenMargin)
|
.padding(.horizontal, FlixySpacing.screenMargin)
|
||||||
}
|
}
|
||||||
.scrollIndicators(.hidden)
|
.scrollIndicators(.never)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ struct PlayableDetailView: View {
|
|||||||
}
|
}
|
||||||
.padding(.bottom, FlixySpacing.section)
|
.padding(.bottom, FlixySpacing.section)
|
||||||
}
|
}
|
||||||
.scrollIndicators(.hidden)
|
.scrollIndicators(.never)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var currentMedia: MediaVersion? {
|
private var currentMedia: MediaVersion? {
|
||||||
@@ -162,7 +162,7 @@ struct TagStrip: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, FlixySpacing.screenMargin)
|
.padding(.horizontal, FlixySpacing.screenMargin)
|
||||||
}
|
}
|
||||||
.scrollIndicators(.hidden)
|
.scrollIndicators(.never)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ struct CastRow: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, FlixySpacing.screenMargin)
|
.padding(.horizontal, FlixySpacing.screenMargin)
|
||||||
}
|
}
|
||||||
.scrollIndicators(.hidden)
|
.scrollIndicators(.never)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ struct SeriesDetailView: View {
|
|||||||
}
|
}
|
||||||
.padding(.bottom, FlixySpacing.section)
|
.padding(.bottom, FlixySpacing.section)
|
||||||
}
|
}
|
||||||
.scrollIndicators(.hidden)
|
.scrollIndicators(.never)
|
||||||
.task(id: item.id) { await loadChildren() }
|
.task(id: item.id) { await loadChildren() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ struct HeroCarousel: View {
|
|||||||
}
|
}
|
||||||
.scrollTargetBehavior(.paging)
|
.scrollTargetBehavior(.paging)
|
||||||
.scrollPosition(id: $shownID)
|
.scrollPosition(id: $shownID)
|
||||||
.scrollIndicators(.hidden)
|
// `.never`, nicht `.hidden`: wer in den Systemeinstellungen
|
||||||
|
// „Scrollbalken immer einblenden" gewählt hat, bekam sonst einen
|
||||||
|
// grauen Streifen quer unter den Aufmacher gelegt.
|
||||||
|
.scrollIndicators(.never)
|
||||||
.aspectRatio(FlixyAspectRatio.hero, contentMode: .fit)
|
.aspectRatio(FlixyAspectRatio.hero, contentMode: .fit)
|
||||||
// Jede Berührung hält das Weiterwandern an — auch die, die nur ein
|
// Jede Berührung hält das Weiterwandern an — auch die, die nur ein
|
||||||
// Antippen wird. Gleichzeitig und ohne Mindestweg, damit weder dem
|
// Antippen wird. Gleichzeitig und ohne Mindestweg, damit weder dem
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ struct HomeView: View {
|
|||||||
}
|
}
|
||||||
.padding(.bottom, FlixySpacing.section)
|
.padding(.bottom, FlixySpacing.section)
|
||||||
}
|
}
|
||||||
.scrollIndicators(.hidden)
|
.scrollIndicators(.never)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bringt die Reihen vom Server in die Form, die eingestellt ist.
|
/// Bringt die Reihen vom Server in die Form, die eingestellt ist.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ struct MediaRow: View {
|
|||||||
.scrollTargetLayout()
|
.scrollTargetLayout()
|
||||||
}
|
}
|
||||||
.scrollTargetBehavior(.viewAligned)
|
.scrollTargetBehavior(.viewAligned)
|
||||||
.scrollIndicators(.hidden)
|
.scrollIndicators(.never)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ struct LibraryFilterSheet: View {
|
|||||||
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
|
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
|
||||||
// leuchteten, während die Schalter daneben schon die gewählte trugen.
|
// leuchteten, während die Schalter daneben schon die gewählte trugen.
|
||||||
.tint(accent)
|
.tint(accent)
|
||||||
|
.flixyForm()
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color.flixyCanvas)
|
.background(Color.flixyCanvas)
|
||||||
.navigationTitle(Text("filter.title"))
|
.navigationTitle(Text("filter.title"))
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ struct ProgramGuide: View {
|
|||||||
lanes
|
lanes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.scrollIndicators(.hidden)
|
.scrollIndicators(.never)
|
||||||
}
|
}
|
||||||
.background(Color.flixyCanvas)
|
.background(Color.flixyCanvas)
|
||||||
.sheet(item: $selection) { selection in
|
.sheet(item: $selection) { selection in
|
||||||
@@ -193,7 +193,7 @@ struct ProgramGuide: View {
|
|||||||
}
|
}
|
||||||
.frame(width: layout.totalWidth, alignment: .topLeading)
|
.frame(width: layout.totalWidth, alignment: .topLeading)
|
||||||
}
|
}
|
||||||
.scrollIndicators(.hidden)
|
.scrollIndicators(.never)
|
||||||
.frame(height: CGFloat(channels.count) * rowHeight)
|
.frame(height: CGFloat(channels.count) * rowHeight)
|
||||||
.onScrollGeometryChange(for: CGFloat.self) { geometry in
|
.onScrollGeometryChange(for: CGFloat.self) { geometry in
|
||||||
geometry.contentOffset.x
|
geometry.contentOffset.x
|
||||||
|
|||||||
@@ -94,8 +94,11 @@ struct AccountBar: View {
|
|||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Ohne Vorrang staucht SwiftUI den Text vor dem Zwischenraum:
|
||||||
|
// in einer schmalen Leiste stand vom Namen nur noch „S…" da.
|
||||||
|
.layoutPriority(1)
|
||||||
|
|
||||||
Spacer(minLength: FlixySpacing.snug)
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
// Zeigt nach oben: dorthin geht die Karte auf.
|
// Zeigt nach oben: dorthin geht die Karte auf.
|
||||||
Image(systemName: "chevron.up")
|
Image(systemName: "chevron.up")
|
||||||
|
|||||||
@@ -187,11 +187,11 @@ struct RootView: View {
|
|||||||
TabSection {
|
TabSection {
|
||||||
ForEach(session.libraries) { library in
|
ForEach(session.libraries) { library in
|
||||||
Tab(
|
Tab(
|
||||||
library.title,
|
|
||||||
systemImage: symbol(for: library.kind),
|
|
||||||
value: SidebarDestination.library(key: library.id, title: library.title)
|
value: SidebarDestination.library(key: library.id, title: library.title)
|
||||||
) {
|
) {
|
||||||
mediaStack { LibraryView(library: library) }
|
mediaStack { LibraryView(library: library) }
|
||||||
|
} label: {
|
||||||
|
tabLabel(library.title, symbol: symbol(for: library.kind))
|
||||||
}
|
}
|
||||||
.customizationID("flixy.tab.library.\(library.id)")
|
.customizationID("flixy.tab.library.\(library.id)")
|
||||||
}
|
}
|
||||||
@@ -201,37 +201,33 @@ struct RootView: View {
|
|||||||
.customizationID("flixy.tab.libraries")
|
.customizationID("flixy.tab.libraries")
|
||||||
|
|
||||||
if session.supportsLiveTV {
|
if session.supportsLiveTV {
|
||||||
Tab(
|
Tab(value: SidebarDestination.liveTV) {
|
||||||
"sidebar.liveTV",
|
|
||||||
systemImage: "dot.radiowaves.left.and.right",
|
|
||||||
value: SidebarDestination.liveTV
|
|
||||||
) {
|
|
||||||
mediaStack { LiveTVView() }
|
mediaStack { LiveTVView() }
|
||||||
|
} label: {
|
||||||
|
tabLabel("sidebar.liveTV", symbol: "dot.radiowaves.left.and.right")
|
||||||
}
|
}
|
||||||
.customizationID("flixy.tab.liveTV")
|
.customizationID("flixy.tab.liveTV")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Tab(
|
Tab(value: SidebarDestination.libraryHub) {
|
||||||
"sidebar.library",
|
|
||||||
systemImage: "books.vertical",
|
|
||||||
value: SidebarDestination.libraryHub
|
|
||||||
) {
|
|
||||||
mediaStack { LibraryHubView() }
|
mediaStack { LibraryHubView() }
|
||||||
|
} label: {
|
||||||
|
tabLabel("sidebar.library", symbol: "books.vertical")
|
||||||
}
|
}
|
||||||
.customizationID("flixy.tab.libraryHub")
|
.customizationID("flixy.tab.libraryHub")
|
||||||
}
|
}
|
||||||
|
|
||||||
Tab(
|
Tab(value: SidebarDestination.downloads) {
|
||||||
"sidebar.downloads",
|
|
||||||
systemImage: "arrow.down.circle",
|
|
||||||
value: SidebarDestination.downloads
|
|
||||||
) {
|
|
||||||
mediaStack { DownloadsView() }
|
mediaStack { DownloadsView() }
|
||||||
|
} label: {
|
||||||
|
tabLabel("sidebar.downloads", symbol: "arrow.down.circle")
|
||||||
}
|
}
|
||||||
.customizationID("flixy.tab.downloads")
|
.customizationID("flixy.tab.downloads")
|
||||||
|
|
||||||
Tab("sidebar.settings", systemImage: "gearshape", value: SidebarDestination.settings) {
|
Tab(value: SidebarDestination.settings) {
|
||||||
mediaStack { SettingsView() }
|
mediaStack { SettingsView() }
|
||||||
|
} label: {
|
||||||
|
tabLabel("sidebar.settings", symbol: "gearshape")
|
||||||
}
|
}
|
||||||
.customizationID("flixy.tab.settings")
|
.customizationID("flixy.tab.settings")
|
||||||
|
|
||||||
@@ -274,6 +270,32 @@ struct RootView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Die Beschriftung eines Eintrags.
|
||||||
|
///
|
||||||
|
/// Auf dem Mac mit einer Mindestbreite. Die Seitenleiste richtet sich nach
|
||||||
|
/// ihrem Inhalt, und bei kurzen Wörtern wie „Start" fällt sie auf eine
|
||||||
|
/// Breite zusammen, in der schon der Kontoname darunter nicht mehr Platz
|
||||||
|
/// hat — auf dem iPad ist sie dreimal so breit, und genau dieser
|
||||||
|
/// Unterschied macht die beiden Oberflächen ungleich.
|
||||||
|
private func tabLabel(_ title: LocalizedStringKey, symbol: String) -> some View {
|
||||||
|
let label = Label(title, systemImage: symbol)
|
||||||
|
#if os(macOS)
|
||||||
|
return label.frame(minWidth: 170, alignment: .leading)
|
||||||
|
#else
|
||||||
|
return label
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dieselbe Beschriftung, wenn der Titel erst vom Server kommt.
|
||||||
|
private func tabLabel(_ title: String, symbol: String) -> some View {
|
||||||
|
let label = Label(title, systemImage: symbol)
|
||||||
|
#if os(macOS)
|
||||||
|
return label.frame(minWidth: 170, alignment: .leading)
|
||||||
|
#else
|
||||||
|
return label
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
private func symbol(for kind: MediaLibrary.Kind) -> String {
|
private func symbol(for kind: MediaLibrary.Kind) -> String {
|
||||||
switch kind {
|
switch kind {
|
||||||
case .movie: "film"
|
case .movie: "film"
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ struct AppearanceView: View {
|
|||||||
PreviewRow()
|
PreviewRow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.flixyForm()
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color.flixyCanvas)
|
.background(Color.flixyCanvas)
|
||||||
.navigationTitle(Text("appearance.title"))
|
.navigationTitle(Text("appearance.title"))
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ struct DiagnosticsView: View {
|
|||||||
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
|
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
|
||||||
// leuchteten, während die Schalter daneben schon die gewählte trugen.
|
// leuchteten, während die Schalter daneben schon die gewählte trugen.
|
||||||
.tint(accent)
|
.tint(accent)
|
||||||
|
.flixyForm()
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color.flixyCanvas)
|
.background(Color.flixyCanvas)
|
||||||
.navigationTitle(Text("diagnostics.title"))
|
.navigationTitle(Text("diagnostics.title"))
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ struct DownloadSettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(accent)
|
.tint(accent)
|
||||||
|
.flixyForm()
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color.flixyCanvas)
|
.background(Color.flixyCanvas)
|
||||||
.navigationTitle(Text("sidebar.downloads"))
|
.navigationTitle(Text("sidebar.downloads"))
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ struct LanguagesView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(accent)
|
.tint(accent)
|
||||||
|
.flixyForm()
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color.flixyCanvas)
|
.background(Color.flixyCanvas)
|
||||||
.navigationTitle(Text("languages.title"))
|
.navigationTitle(Text("languages.title"))
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ struct PlaybackSettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(accent)
|
.tint(accent)
|
||||||
|
.flixyForm()
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color.flixyCanvas)
|
.background(Color.flixyCanvas)
|
||||||
.navigationTitle(Text("settings.playback"))
|
.navigationTitle(Text("settings.playback"))
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ struct ServerSettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(accent)
|
.tint(accent)
|
||||||
|
.flixyForm()
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color.flixyCanvas)
|
.background(Color.flixyCanvas)
|
||||||
.navigationTitle(Text("settings.server"))
|
.navigationTitle(Text("settings.server"))
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ struct SettingsView: View {
|
|||||||
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
|
// gehen können — sichtbar an Picker-Werten, die in der Standardfarbe
|
||||||
// leuchteten, während die Schalter daneben schon die gewählte trugen.
|
// leuchteten, während die Schalter daneben schon die gewählte trugen.
|
||||||
.tint(accent)
|
.tint(accent)
|
||||||
|
.flixyForm()
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.background(Color.flixyCanvas)
|
.background(Color.flixyCanvas)
|
||||||
.navigationTitle(Text("sidebar.settings"))
|
.navigationTitle(Text("sidebar.settings"))
|
||||||
|
|||||||
@@ -39,13 +39,25 @@ public struct HeroBanner<Actions: View>: View {
|
|||||||
self.actions = actions()
|
self.actions = actions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wie stark Schrift und Schriftzug mitwachsen.
|
||||||
|
///
|
||||||
|
/// Der Aufmacher ist so breit wie das Fenster. Bei fester Schriftgröße
|
||||||
|
/// wirkt derselbe Titel auf einem iPad groß und in einem breiten
|
||||||
|
/// Mac-Fenster verloren — das Bild wächst, die Schrift nicht. Bezugsgröße
|
||||||
|
/// ist die Breite eines iPads im Hochformat; alles darüber wächst mit,
|
||||||
|
/// aber gedeckelt, damit ein Fenster über die ganze Wand nicht in
|
||||||
|
/// Plakatschrift endet.
|
||||||
|
private func scale(for width: CGFloat) -> CGFloat {
|
||||||
|
min(max(width / 834, 1), 1.7)
|
||||||
|
}
|
||||||
|
|
||||||
/// Schriftzug, wenn vorhanden — sonst gesetzter Titel.
|
/// Schriftzug, wenn vorhanden — sonst gesetzter Titel.
|
||||||
///
|
///
|
||||||
/// Der Schriftzug wird ohne Bild-Transcoder geladen; er ist ein
|
/// Der Schriftzug wird ohne Bild-Transcoder geladen; er ist ein
|
||||||
/// freigestelltes PNG, und der Transcoder liefert JPEG. Aus dem
|
/// freigestelltes PNG, und der Transcoder liefert JPEG. Aus dem
|
||||||
/// freigestellten Logo würde sonst ein schwarzer Kasten.
|
/// freigestellten Logo würde sonst ein schwarzer Kasten.
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var titleView: some View {
|
private func titleView(scale: CGFloat) -> some View {
|
||||||
if let logoURL {
|
if let logoURL {
|
||||||
AsyncImage(url: logoURL) { phase in
|
AsyncImage(url: logoURL) { phase in
|
||||||
switch phase {
|
switch phase {
|
||||||
@@ -56,23 +68,23 @@ public struct HeroBanner<Actions: View>: View {
|
|||||||
// Nach oben begrenzt: manche Schriftzüge sind fast
|
// Nach oben begrenzt: manche Schriftzüge sind fast
|
||||||
// quadratisch, andere sehr breit. Ohne Deckel schöbe
|
// quadratisch, andere sehr breit. Ohne Deckel schöbe
|
||||||
// ein hohes Logo die Knöpfe aus dem Bild.
|
// ein hohes Logo die Knöpfe aus dem Bild.
|
||||||
.frame(maxWidth: 260, maxHeight: 76, alignment: .leading)
|
.frame(maxWidth: 260 * scale, maxHeight: 76 * scale, alignment: .leading)
|
||||||
default:
|
default:
|
||||||
// Solange geladen wird — und wenn es scheitert — bleibt es
|
// Solange geladen wird — und wenn es scheitert — bleibt es
|
||||||
// beim gesetzten Titel. Ein leerer Platz wäre schlimmer
|
// beim gesetzten Titel. Ein leerer Platz wäre schlimmer
|
||||||
// als eine Systemschrift.
|
// als eine Systemschrift.
|
||||||
plainTitle
|
plainTitle(scale: scale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.accessibilityLabel(Text(title))
|
.accessibilityLabel(Text(title))
|
||||||
} else {
|
} else {
|
||||||
plainTitle
|
plainTitle(scale: scale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var plainTitle: some View {
|
private func plainTitle(scale: CGFloat) -> some View {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.flixyHero)
|
.font(.system(size: 34 * scale, weight: .bold))
|
||||||
.foregroundStyle(Color.flixyTextPrimary)
|
.foregroundStyle(Color.flixyTextPrimary)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
}
|
}
|
||||||
@@ -106,20 +118,28 @@ public struct HeroBanner<Actions: View>: View {
|
|||||||
.allowsHitTesting(false)
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
.overlay(alignment: .bottomLeading) {
|
.overlay(alignment: .bottomLeading) {
|
||||||
VStack(alignment: .leading, spacing: FlixySpacing.snug) {
|
GeometryReader { geometry in
|
||||||
titleView
|
let scale = scale(for: geometry.size.width)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: FlixySpacing.snug * scale) {
|
||||||
|
titleView(scale: scale)
|
||||||
|
|
||||||
if let metadata {
|
if let metadata {
|
||||||
Text(metadata)
|
Text(metadata)
|
||||||
.font(.flixyCaption)
|
.font(.system(size: 12 * scale))
|
||||||
.foregroundStyle(Color.flixyTextSecondary)
|
.foregroundStyle(Color.flixyTextSecondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let summary {
|
if let summary {
|
||||||
Text(summary)
|
Text(summary)
|
||||||
.font(.flixyCaption)
|
.font(.system(size: 12 * scale))
|
||||||
.foregroundStyle(Color.flixyTextSecondary)
|
.foregroundStyle(Color.flixyTextSecondary)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
|
// Ein Satz über die ganze Breite eines großen
|
||||||
|
// Fensters ist unlesbar; das Auge verliert die
|
||||||
|
// Zeile. Achtzig Zeichen sind die Grenze, ab
|
||||||
|
// der Satzspiegel unangenehm werden.
|
||||||
|
.frame(maxWidth: 640 * scale, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: FlixySpacing.regular) {
|
HStack(spacing: FlixySpacing.regular) {
|
||||||
@@ -127,8 +147,14 @@ public struct HeroBanner<Actions: View>: View {
|
|||||||
}
|
}
|
||||||
.padding(.top, FlixySpacing.tight)
|
.padding(.top, FlixySpacing.tight)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, FlixySpacing.screenMargin)
|
.padding(.horizontal, FlixySpacing.screenMargin * scale)
|
||||||
.padding(.bottom, FlixySpacing.loose)
|
.padding(.bottom, FlixySpacing.loose * scale)
|
||||||
|
.frame(
|
||||||
|
width: geometry.size.width,
|
||||||
|
height: geometry.size.height,
|
||||||
|
alignment: .bottomLeading
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,33 @@ import SwiftUI
|
|||||||
|
|
||||||
/// Die Schriftskala der App.
|
/// Die Schriftskala der App.
|
||||||
///
|
///
|
||||||
/// Durchgehend die Systemschrift mit `Font.TextStyle` als Grundlage, damit
|
/// Auf iOS die Systemstile, damit Dynamic Type wirkt. Die Rolle steht im
|
||||||
/// Dynamic Type wirkt. Die Rolle steht im Namen, nicht die Größe — sonst
|
/// Namen, nicht die Größe — sonst führt jede Anpassung zu einer Suche quer
|
||||||
/// führt jede Anpassung zu einer Suche quer durch alle Views.
|
/// durch alle Views.
|
||||||
|
///
|
||||||
|
/// **Auf dem Mac stehen feste Größen.** Dieselben Stile sind dort deutlich
|
||||||
|
/// kleiner: `largeTitle` misst 26 Punkt statt 34, `body` 13 statt 17. Eine
|
||||||
|
/// Oberfläche, die auf dem iPad stimmt, wirkt damit auf dem Mac zusammen-
|
||||||
|
/// geschrumpft — und genau das war sie. Die Zahlen hier sind die von iOS.
|
||||||
|
///
|
||||||
|
/// Das kostet auf dem Mac Dynamic Type. Das ist vertretbar: macOS hat keine
|
||||||
|
/// systemweite Textgrößen-Einstellung, auf die Apps reagieren müssten, und ein
|
||||||
|
/// Fenster lässt sich stattdessen größer ziehen.
|
||||||
extension Font {
|
extension Font {
|
||||||
|
#if os(macOS)
|
||||||
|
/// Der Titel im Aufmacher-Bild.
|
||||||
|
public static let flixyHero = Font.system(size: 34, weight: .bold)
|
||||||
|
/// Titel auf Detailseiten.
|
||||||
|
public static let flixyTitle = Font.system(size: 22, weight: .semibold)
|
||||||
|
/// Überschrift einer Reihe auf der Startseite.
|
||||||
|
public static let flixyRowHeader = Font.system(size: 17, weight: .semibold)
|
||||||
|
/// Titel unter einem Poster.
|
||||||
|
public static let flixyCardTitle = Font.system(size: 15, weight: .medium)
|
||||||
|
/// Fließtext, Beschreibungen.
|
||||||
|
public static let flixyBody = Font.system(size: 17)
|
||||||
|
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
|
||||||
|
public static let flixyCaption = Font.system(size: 12)
|
||||||
|
#else
|
||||||
/// Der Titel im Aufmacher-Bild.
|
/// Der Titel im Aufmacher-Bild.
|
||||||
public static let flixyHero = Font.system(.largeTitle, design: .default, weight: .bold)
|
public static let flixyHero = Font.system(.largeTitle, design: .default, weight: .bold)
|
||||||
/// Titel auf Detailseiten.
|
/// Titel auf Detailseiten.
|
||||||
@@ -18,6 +41,7 @@ extension Font {
|
|||||||
public static let flixyBody = Font.system(.body)
|
public static let flixyBody = Font.system(.body)
|
||||||
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
|
/// Jahreszahl, Laufzeit, Auflösung — alles, was neben dem Titel steht.
|
||||||
public static let flixyCaption = Font.system(.caption)
|
public static let flixyCaption = Font.system(.caption)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Abstände.
|
/// Abstände.
|
||||||
|
|||||||
@@ -43,6 +43,21 @@ extension View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ein Formular, das auf beiden Seiten gleich aussieht.
|
||||||
|
///
|
||||||
|
/// Auf dem Mac setzt SwiftUI sonst den Spalten-Stil: Beschriftungen rechts
|
||||||
|
/// ausgerichtet, keine abgesetzten Abschnitte, alles eng. Der gruppierte
|
||||||
|
/// Stil ist derselbe, den iOS ohnehin nimmt — und der Grund, warum die
|
||||||
|
/// Einstellungen auf dem Mac wie ein Systemdialog aus einem anderen
|
||||||
|
/// Jahrzehnt aussahen.
|
||||||
|
public func flixyForm() -> some View {
|
||||||
|
#if os(macOS)
|
||||||
|
formStyle(.grouped)
|
||||||
|
#else
|
||||||
|
self
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
/// Eingabefelder für Adressen und Namen: keine Großschreibung, keine
|
/// Eingabefelder für Adressen und Namen: keine Großschreibung, keine
|
||||||
/// Korrektur — auf dem Mac gibt es beides ohnehin nicht als Tastatur.
|
/// Korrektur — auf dem Mac gibt es beides ohnehin nicht als Tastatur.
|
||||||
public func flixyPlainEntry(isAddress: Bool = false) -> some View {
|
public func flixyPlainEntry(isAddress: Bool = false) -> some View {
|
||||||
|
|||||||
Reference in New Issue
Block a user