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() + } + } + } +}