From fd29df51cf26d45d056986af8d60d1dd02032cce Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 5 Sep 2026 21:32:24 +0200 Subject: [PATCH] =?UTF-8?q?Im=20Band=20ist=20Platz=20f=C3=BCr=20mehr=20als?= =?UTF-8?q?=20einen=20Wert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Begründung für „ein Wert je Seite" war falsch gerechnet: der Arm ist bei fünf Kacheln je 376 Punkte breit, und ein Wert braucht davon knapp sechzig. Die Zahl im Kopf war die der schmalen Insel, nicht die des ausgefahrenen Arms. Jetzt bis zu vier Elemente je Seite, in wählbarer Reihenfolge — Uhr, Wetter, Akku, CPU, Speicher, Temperatur. Kein Ziehen zum Umsortieren: bei höchstens vier Einträgen ist Herausnehmen und wieder Hinzufügen schneller als eine Ziehgeste, die man erst finden muss. „Nichts" ist keine Wahl mehr, sondern die leere Liste — zwei Wege, dasselbe zu sagen, waren einer zu viel. Co-Authored-By: Claude Opus 5 --- Onyx/AppModel.swift | 28 ++++++++++----- Onyx/Localizable.xcstrings | 20 ++--------- Onyx/NotchAccessories.swift | 31 +++++++++-------- Onyx/SettingsView.swift | 68 +++++++++++++++++++++++++++++-------- 4 files changed, 92 insertions(+), 55 deletions(-) diff --git a/Onyx/AppModel.swift b/Onyx/AppModel.swift index e7efbe7..5249926 100644 --- a/Onyx/AppModel.swift +++ b/Onyx/AppModel.swift @@ -85,22 +85,32 @@ final class AppModel { } } - /// Was links neben der Notch steht. - var bandLeading: BandAccessory { + /// Was links neben der Notch steht, in dieser Reihenfolge. + /// + /// Eine Liste und keine einzelne Wahl: der Arm ist bei fünf Kacheln rund + /// 376 Punkte breit, und ein Wert braucht davon knapp sechzig. + var bandLeading: [BandAccessory] { didSet { guard bandLeading != oldValue else { return } - defaults.set(bandLeading.rawValue, forKey: Keys.bandLeading) + defaults.set(bandLeading.map(\.rawValue), forKey: Keys.bandLeading) } } /// Was rechts daneben steht. - var bandTrailing: BandAccessory { + var bandTrailing: [BandAccessory] { didSet { guard bandTrailing != oldValue else { return } - defaults.set(bandTrailing.rawValue, forKey: Keys.bandTrailing) + defaults.set(bandTrailing.map(\.rawValue), forKey: Keys.bandTrailing) } } + /// Wie viele Elemente je Seite höchstens. + /// + /// Vier passen bei fünf Kacheln bequem. Wer weniger Kacheln zeigt, hat + /// schmalere Arme — dann wird das Letzte abgeschnitten, und das ist die + /// ehrlichere Grenze als eine Zahl, die für jede Panelbreite stimmen müsste. + static let bandLimit = 4 + /// Notch oder Insel — die Form, die Onyx oben annimmt. var notchStyle: NotchStyle { didSet { @@ -206,10 +216,10 @@ final class AppModel { // Die bisherige feste Verdrahtung als Voreinstellung: wer nichts // einstellt, sieht, was vorher auch dastand. - self.bandLeading = defaults.string(forKey: Keys.bandLeading) - .flatMap(BandAccessory.init(rawValue:)) ?? .weather - self.bandTrailing = defaults.string(forKey: Keys.bandTrailing) - .flatMap(BandAccessory.init(rawValue:)) ?? .battery + self.bandLeading = (defaults.stringArray(forKey: Keys.bandLeading) + .map { $0.compactMap(BandAccessory.init(rawValue:)) }) ?? [.weather] + self.bandTrailing = (defaults.stringArray(forKey: Keys.bandTrailing) + .map { $0.compactMap(BandAccessory.init(rawValue:)) }) ?? [.battery] self.notchWidths = (defaults.dictionary(forKey: Keys.notchWidths) as? [String: Double] ?? [:]) .reduce(into: [CGDirectDisplayID: CGFloat]()) { result, entry in guard let id = CGDirectDisplayID(entry.key) else { return } diff --git a/Onyx/Localizable.xcstrings b/Onyx/Localizable.xcstrings index 1ef043e..ddafb33 100644 --- a/Onyx/Localizable.xcstrings +++ b/Onyx/Localizable.xcstrings @@ -217,22 +217,6 @@ } } }, - "band.none": { - "localizations": { - "de": { - "stringUnit": { - "state": "translated", - "value": "Nichts" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Nothing" - } - } - } - }, "band.temperature": { "localizations": { "de": { @@ -2893,13 +2877,13 @@ "de": { "stringUnit": { "state": "translated", - "value": "Was auf Menüleistenhöhe links und rechts der Notch steht, wenn das Panel offen ist. Je Seite ein Wert — für mehr ist der Streifen zu schmal." + "value": "Was auf Menüleistenhöhe links und rechts der Notch steht, wenn das Panel offen ist. Bis zu vier je Seite — bei wenigen Kacheln sind die Arme schmaler, dann wird das Letzte abgeschnitten." } }, "en": { "stringUnit": { "state": "translated", - "value": "What sits at menu bar height to the left and right of the notch while the panel is open. One value per side — the strip is too narrow for more." + "value": "What sits at menu bar height left and right of the notch while the panel is open. Up to four per side — with few tiles the arms are narrower and the last one gets clipped." } } } diff --git a/Onyx/NotchAccessories.swift b/Onyx/NotchAccessories.swift index 77fb945..4d7f49e 100644 --- a/Onyx/NotchAccessories.swift +++ b/Onyx/NotchAccessories.swift @@ -202,12 +202,10 @@ struct BrightnessActivity: NotchActivity { /// Die Elemente, die links und rechts der Notch sitzen können. /// -/// **Eine Wahl je Seite, keine Liste.** Der Arm ist bei fünf Kacheln rund 200 -/// Punkte breit und liegt auf Menüleistenhöhe — dort passt ein Wert, nicht drei. -/// Eine Mehrfachauswahl anzubieten hieße, ein Gedränge zu ermöglichen, das man -/// hinterher wieder wegräumen muss. +/// Mehrere je Seite: der Arm ist bei fünf Kacheln rund 376 Punkte breit, und ein +/// Wert braucht davon knapp sechzig. Vier nebeneinander sind kein Gedränge, +/// sondern eine Zeile. enum BandAccessory: String, Codable, CaseIterable, Sendable { - case none case clock case weather case battery @@ -217,7 +215,6 @@ enum BandAccessory: String, Codable, CaseIterable, Sendable { var localizedName: LocalizedStringKey { switch self { - case .none: "band.none" case .clock: "band.clock" case .weather: "band.weather" case .battery: "band.battery" @@ -240,22 +237,28 @@ struct BandSlotView: View { let weather: WeatherModel? let metrics: MetricsModel? - private var kind: BandAccessory { + private var kinds: [BandAccessory] { edge == .leading ? model.bandLeading : model.bandTrailing } var body: some View { - content - .padding(.leading, edge == .leading ? Onyx.Space.xl : Onyx.Space.lg) - .padding(.trailing, edge == .leading ? Onyx.Space.lg : Onyx.Space.xl) - .frame(maxWidth: .infinity, alignment: edge == .leading ? .leading : .trailing) + HStack(spacing: Onyx.Space.xl) { + ForEach(kinds, id: \.self) { kind in + content(kind) + } + } + .padding(.leading, edge == .leading ? Onyx.Space.xl : Onyx.Space.lg) + .padding(.trailing, edge == .leading ? Onyx.Space.lg : Onyx.Space.xl) + .frame(maxWidth: .infinity, alignment: edge == .leading ? .leading : .trailing) + // Was nicht mehr hineinpasst, wird abgeschnitten statt den Arm über die + // Notch hinauszuschieben. Die Obergrenze in den Einstellungen sorgt + // dafür, dass es dazu im Normalfall nicht kommt. + .fixedSize(horizontal: true, vertical: false) } @ViewBuilder - private var content: some View { + private func content(_ kind: BandAccessory) -> some View { switch kind { - case .none: - EmptyView() case .clock: // Die eine Angabe, die keine Quelle braucht. TimelineView(.everyMinute) { context in diff --git a/Onyx/SettingsView.swift b/Onyx/SettingsView.swift index 3da3cd7..0ac47e8 100644 --- a/Onyx/SettingsView.swift +++ b/Onyx/SettingsView.swift @@ -203,22 +203,10 @@ private struct NotchSettings: View { OnyxFormCard(String(localized: "settings.band"), hint: String(localized: "settings.band.hint")) { OnyxFormRow(String(localized: "settings.band.leading")) { - Picker("", selection: $model.bandLeading) { - ForEach(BandAccessory.allCases, id: \.self) { kind in - Text(kind.localizedName).tag(kind) - } - } - .labelsHidden() - .frame(width: 200) + BandEditor(items: $model.bandLeading) } OnyxFormRow(String(localized: "settings.band.trailing")) { - Picker("", selection: $model.bandTrailing) { - ForEach(BandAccessory.allCases, id: \.self) { kind in - Text(kind.localizedName).tag(kind) - } - } - .labelsHidden() - .frame(width: 200) + BandEditor(items: $model.bandTrailing) } } @@ -735,3 +723,55 @@ private struct NotchWidthSettings: View { set: { model.setNotchWidth($0, for: id) }) } } + + +/// Die Elemente einer Bandseite: was drin ist, in welcher Reihenfolge, und was +/// sich hinzufügen lässt. +/// +/// Kein Ziehen zum Umsortieren. Die Liste hat höchstens vier Einträge, und für +/// vier Einträge ist Herausnehmen und wieder Hinzufügen schneller als jede +/// Ziehgeste — die man außerdem erst finden muss. +private struct BandEditor: View { + @Binding var items: [BandAccessory] + + private var available: [BandAccessory] { + BandAccessory.allCases.filter { !items.contains($0) } + } + + var body: some View { + HStack(spacing: Onyx.Space.sm) { + ForEach(items, id: \.self) { item in + HStack(spacing: Onyx.Space.xs) { + Text(item.localizedName) + .font(Onyx.Font.footnote) + Button { + items.removeAll { $0 == item } + } label: { + Image(systemName: "xmark") + .font(.system(size: Onyx.Symbol.caption)) + } + .buttonStyle(.plain) + .foregroundStyle(.secondary) + } + .padding(.horizontal, Onyx.Space.sm) + .padding(.vertical, Onyx.Space.xs) + .background(.quaternary, in: .capsule) + } + + if items.count < AppModel.bandLimit, !available.isEmpty { + Menu { + ForEach(available, id: \.self) { candidate in + Button { items.append(candidate) } label: { + Text(candidate.localizedName) + } + } + } label: { + Image(systemName: "plus") + } + .menuStyle(.borderlessButton) + .menuIndicator(.hidden) + .fixedSize() + } + } + } +}