From 486024834d9bf187813121fc5ec97fae343eec5f Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 28 May 2026 23:26:05 +0200 Subject: [PATCH] Add position-based colorful Home shortcut palettes Colorful shortcut cards now color by grid position instead of shortcut identity, so colors stay in the same slot when shortcuts are reordered. Adds a Palette option (Classic default, plus Sunset, Meadow, Berry, Grape) and a Custom palette with a swatch-list editor (ColorPicker + hex fields) and a switchable comma-separated text field. Color is resolved per position and wraps by palette length, wired via a new homeShortcutColorfulColor environment value and a Color(hex:) extension. --- Yattee/Core/Settings/SettingsKey.swift | 3 + .../Core/Settings/SettingsManager+Home.swift | 34 +++ Yattee/Core/SettingsManager.swift | 4 + Yattee/Extensions/Color+Hex.swift | 71 +++++ Yattee/Localizable.xcstrings | 245 ++++++++++++++---- Yattee/Views/Home/HomeItem.swift | 61 +++++ Yattee/Views/Home/HomeSettingsView.swift | 18 +- Yattee/Views/Home/HomeShortcutCardView.swift | 4 +- Yattee/Views/Home/HomeShortcutStyleView.swift | 194 +++++++++++++- Yattee/Views/Home/HomeView.swift | 13 +- 10 files changed, 596 insertions(+), 51 deletions(-) create mode 100644 Yattee/Extensions/Color+Hex.swift diff --git a/Yattee/Core/Settings/SettingsKey.swift b/Yattee/Core/Settings/SettingsKey.swift index c393b48f..09e6ca6c 100644 --- a/Yattee/Core/Settings/SettingsKey.swift +++ b/Yattee/Core/Settings/SettingsKey.swift @@ -65,6 +65,8 @@ enum SettingsKey: String, CaseIterable { case homeShortcutVisibility case homeShortcutLayout case homeShortcutCardStyle + case homeShortcutColorfulPalette + case homeShortcutCustomPaletteColors case homeSectionOrder case homeSectionVisibility case homeSectionItemsLimit @@ -137,6 +139,7 @@ enum SettingsKey: String, CaseIterable { case .preferredQuality, .cellularQuality, .allowSoftwareDecodedFormats, .macPlayerMode, .listStyle, // Home layout — different UI paradigms per platform .homeShortcutOrder, .homeShortcutVisibility, .homeShortcutLayout, .homeShortcutCardStyle, + .homeShortcutColorfulPalette, .homeShortcutCustomPaletteColors, .homeSectionOrder, .homeSectionVisibility, .homeSectionItemsLimit, .homeSectionLayout, // Top Shelf — tvOS only .topShelfSections, diff --git a/Yattee/Core/Settings/SettingsManager+Home.swift b/Yattee/Core/Settings/SettingsManager+Home.swift index 5c5fb99f..6bffc3b4 100644 --- a/Yattee/Core/Settings/SettingsManager+Home.swift +++ b/Yattee/Core/Settings/SettingsManager+Home.swift @@ -110,6 +110,40 @@ extension SettingsManager { } } + /// Palette used by the "colorful" card style. Colors are applied by grid + /// position. Default is Classic. + var homeShortcutColorfulPalette: HomeShortcutColorfulPalette { + get { + if let cached = _homeShortcutColorfulPalette { return cached } + guard let rawValue = string(for: .homeShortcutColorfulPalette) else { + return .classic + } + return HomeShortcutColorfulPalette(rawValue: rawValue) ?? .classic + } + set { + _homeShortcutColorfulPalette = newValue + set(newValue.rawValue, for: .homeShortcutColorfulPalette) + } + } + + /// User-supplied hex colors for the custom "colorful" palette. Stored as a + /// single comma-separated string. Defaults to a starter set of colors. + var homeShortcutCustomPaletteColors: [String] { + get { + if let cached = _homeShortcutCustomPaletteColors { return cached } + guard let raw = string(for: .homeShortcutCustomPaletteColors) else { + return HomeShortcutColorfulPalette.customStarterColors + } + let colors = raw.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty } + return colors + } + set { + _homeShortcutCustomPaletteColors = newValue + let joined = newValue.map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }.joined(separator: ",") + set(joined, for: .homeShortcutCustomPaletteColors) + } + } + /// Layout mode for home sections (list or grid). Default is list on iOS/macOS, grid on tvOS. var homeSectionLayout: HomeSectionLayout { get { diff --git a/Yattee/Core/SettingsManager.swift b/Yattee/Core/SettingsManager.swift index c723840d..59ff69f5 100644 --- a/Yattee/Core/SettingsManager.swift +++ b/Yattee/Core/SettingsManager.swift @@ -109,6 +109,8 @@ final class SettingsManager { var _homeShortcutVisibility: [HomeShortcutItem: Bool]? var _homeShortcutLayout: HomeShortcutLayout? var _homeShortcutCardStyle: HomeShortcutCardStyle? + var _homeShortcutColorfulPalette: HomeShortcutColorfulPalette? + var _homeShortcutCustomPaletteColors: [String]? var _homeSectionOrder: [HomeSectionItem]? var _homeSectionVisibility: [HomeSectionItem: Bool]? var _homeSectionItemsLimit: Int? @@ -465,6 +467,8 @@ final class SettingsManager { _homeShortcutVisibility = nil _homeShortcutLayout = nil _homeShortcutCardStyle = nil + _homeShortcutColorfulPalette = nil + _homeShortcutCustomPaletteColors = nil _homeSectionOrder = nil _homeSectionVisibility = nil _homeSectionItemsLimit = nil diff --git a/Yattee/Extensions/Color+Hex.swift b/Yattee/Extensions/Color+Hex.swift new file mode 100644 index 00000000..06b61aea --- /dev/null +++ b/Yattee/Extensions/Color+Hex.swift @@ -0,0 +1,71 @@ +// +// Color+Hex.swift +// Yattee +// +// Hex-string parsing/formatting for SwiftUI Color, plus the environment value +// used to drive position-based colorful shortcut cards. +// + +import SwiftUI + +extension Color { + /// Creates a color from a `#RRGGBB` / `RRGGBB` hex string (also accepts + /// `#RGB` shorthand and an optional `AA`/`AARRGGBB`/`RRGGBBAA` alpha). + /// Returns `nil` for anything it can't parse so bad input is simply skipped. + init?(hex: String) { + var cleaned = hex.trimmingCharacters(in: .whitespacesAndNewlines) + if cleaned.hasPrefix("#") { + cleaned.removeFirst() + } + guard !cleaned.isEmpty, cleaned.allSatisfy({ $0.isHexDigit }) else { return nil } + + // Expand #RGB shorthand to #RRGGBB. + if cleaned.count == 3 { + cleaned = cleaned.map { "\($0)\($0)" }.joined() + } + + guard let value = UInt64(cleaned, radix: 16) else { return nil } + + let r, g, b, a: Double + switch cleaned.count { + case 6: // RRGGBB + r = Double((value & 0xFF0000) >> 16) / 255 + g = Double((value & 0x00FF00) >> 8) / 255 + b = Double(value & 0x0000FF) / 255 + a = 1 + case 8: // RRGGBBAA + r = Double((value & 0xFF00_0000) >> 24) / 255 + g = Double((value & 0x00FF_0000) >> 16) / 255 + b = Double((value & 0x0000_FF00) >> 8) / 255 + a = Double(value & 0x0000_00FF) / 255 + default: + return nil + } + + self.init(red: r, green: g, blue: b, opacity: a) + } + + /// Formats the resolved color as an uppercase `#RRGGBB` string. + func toHexString() -> String { + let resolved = resolve(in: EnvironmentValues()) + let r = Int((max(0, min(1, resolved.red)) * 255).rounded()) + let g = Int((max(0, min(1, resolved.green)) * 255).rounded()) + let b = Int((max(0, min(1, resolved.blue)) * 255).rounded()) + return String(format: "#%02X%02X%02X", r, g, b) + } +} + +// MARK: - Environment: position-based colorful color + +private struct HomeShortcutColorfulColorKey: EnvironmentKey { + static let defaultValue: Color? = nil +} + +extension EnvironmentValues { + /// The colorful-style fill color resolved from the shortcut's grid position. + /// When set, it overrides a card's fixed `colorfulColor`. + var homeShortcutColorfulColor: Color? { + get { self[HomeShortcutColorfulColorKey.self] } + set { self[HomeShortcutColorfulColorKey.self] = newValue } + } +} diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index b47acff6..eeae1ca4 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -9,6 +9,9 @@ }, "·" : { + }, + "#RRGGBB" : { + }, "%@" : { @@ -3674,17 +3677,6 @@ } } }, - "home.editShortcuts" : { - "comment" : "Context menu action on Home shortcuts to open Home settings", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Edit Shortcuts" - } - } - } - }, "home.downloads.notAvailable" : { "comment" : "Downloads not available on tvOS", "localizations" : { @@ -3707,6 +3699,17 @@ } } }, + "home.editShortcuts" : { + "comment" : "Context menu action on Home shortcuts to open Home settings", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit Shortcuts" + } + } + } + }, "home.empty.description" : { "comment" : "Library tab placeholder description", "localizations" : { @@ -4203,6 +4206,17 @@ } } }, + "home.settings.shortcuts.palette" : { + "comment" : "Label/header for the colorful palette picker", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Palette" + } + } + } + }, "home.settings.shortcuts.style" : { "comment" : "Label for shortcuts card style picker in library settings", "localizations" : { @@ -4225,39 +4239,6 @@ } } }, - "home.shortcuts.style.accent" : { - "comment" : "Accent card style option", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Accent" - } - } - } - }, - "home.shortcuts.style.colorful" : { - "comment" : "Colorful card style option", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Colorful" - } - } - } - }, - "home.shortcuts.style.plain" : { - "comment" : "Plain card style option", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plain" - } - } - } - }, "home.settings.sourceDisabled" : { }, @@ -4393,6 +4374,182 @@ } } }, + "home.shortcuts.palette.berry" : { + "comment" : "Colorful palette name", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berry" + } + } + } + }, + "home.shortcuts.palette.classic" : { + "comment" : "Colorful palette name", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Classic" + } + } + } + }, + "home.shortcuts.palette.custom" : { + "comment" : "Colorful palette name for the user-defined palette", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Custom" + } + } + } + }, + "home.shortcuts.palette.custom.add" : { + "comment" : "Button to add a color to the custom palette", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add Color" + } + } + } + }, + "home.shortcuts.palette.custom.footer" : { + "comment" : "Footer explaining the custom palette editor", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Colors are applied to cards by position and repeat once the list runs out." + } + } + } + }, + "home.shortcuts.palette.custom.header" : { + "comment" : "Header for the custom palette editor section", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Custom Colors" + } + } + } + }, + "home.shortcuts.palette.custom.mode" : { + "comment" : "Accessibility label for the custom palette edit-mode picker", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit Mode" + } + } + } + }, + "home.shortcuts.palette.custom.mode.list" : { + "comment" : "Custom palette edit mode: swatch list", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "List" + } + } + } + }, + "home.shortcuts.palette.custom.mode.text" : { + "comment" : "Custom palette edit mode: raw text", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Text" + } + } + } + }, + "home.shortcuts.palette.custom.placeholder" : { + "comment" : "Placeholder for the custom palette hex text field", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "#FF6B6B, #4D96FF, #6BCB77" + } + } + } + }, + "home.shortcuts.palette.grape" : { + "comment" : "Colorful palette name", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Grape" + } + } + } + }, + "home.shortcuts.palette.meadow" : { + "comment" : "Colorful palette name", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Meadow" + } + } + } + }, + "home.shortcuts.palette.sunset" : { + "comment" : "Colorful palette name", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sunset" + } + } + } + }, + "home.shortcuts.style.accent" : { + "comment" : "Accent card style option", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Accent" + } + } + } + }, + "home.shortcuts.style.colorful" : { + "comment" : "Colorful card style option", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Colorful" + } + } + } + }, + "home.shortcuts.style.plain" : { + "comment" : "Plain card style option", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plain" + } + } + } + }, "home.subscriptions.subtitle" : { "comment" : "Subscriptions card subtitle in library", "localizations" : { diff --git a/Yattee/Views/Home/HomeItem.swift b/Yattee/Views/Home/HomeItem.swift index cdf8a58f..88357be1 100644 --- a/Yattee/Views/Home/HomeItem.swift +++ b/Yattee/Views/Home/HomeItem.swift @@ -50,6 +50,67 @@ enum HomeShortcutCardStyle: String, CaseIterable, Sendable { } } +// MARK: - Home Shortcut Colorful Palette + +/// Palette used by the "colorful" card style. Colors are applied by grid +/// position (wrapping by palette length), so a card's color depends on where +/// it sits, not on which shortcut it is. +enum HomeShortcutColorfulPalette: String, CaseIterable, Sendable { + /// Default: reproduces the original per-position look (SwiftUI named colors). + case classic + case sunset + case meadow + case berry + case grape + /// User-supplied hex colors (stored in settings). + case custom + + var displayName: LocalizedStringKey { + switch self { + case .classic: return "home.shortcuts.palette.classic" + case .sunset: return "home.shortcuts.palette.sunset" + case .meadow: return "home.shortcuts.palette.meadow" + case .berry: return "home.shortcuts.palette.berry" + case .grape: return "home.shortcuts.palette.grape" + case .custom: return "home.shortcuts.palette.custom" + } + } + + /// Built-in colors for this palette; `nil` for `.custom` (resolved from + /// the user's stored hex list instead). + var builtInColors: [Color]? { + switch self { + case .classic: + // Ordered to match `HomeShortcutItem.defaultOrder` so the classic + // palette reproduces the app's original colorful appearance. + return [.blue, .teal, .indigo, .pink, .purple, .orange, .green, .red, .cyan, .brown] + case .sunset: + return ["#FF6B6B", "#F06595", "#CC5DE8", "#845EF7", "#FFA94D"].compactMap { Color(hex: $0) } + case .meadow: + return ["#2B8A3E", "#37B24D", "#66A80F", "#099268", "#0CA678"].compactMap { Color(hex: $0) } + case .berry: + return ["#E64980", "#C2255C", "#A61E4D", "#862E9C", "#5F3DC4"].compactMap { Color(hex: $0) } + case .grape: + return ["#7048E8", "#9C36B5", "#C2255C", "#E8590C", "#F08C00"].compactMap { Color(hex: $0) } + case .custom: + return nil + } + } + + /// Starter hex colors used to seed a new custom palette. + static let customStarterColors = ["#007AFF", "#30B0C7", "#5856D6", "#FF2D55", "#AF52DE"] + + /// Resolves the color for a shortcut at `position` in the grid, wrapping by + /// palette length. Falls back to the Classic palette if the selection yields + /// no usable colors (e.g. an empty or all-invalid custom list). + static func color(forPosition position: Int, palette: HomeShortcutColorfulPalette, customHex: [String]) -> Color { + let colors = palette == .custom ? customHex.compactMap { Color(hex: $0) } : (palette.builtInColors ?? []) + let usable = colors.isEmpty ? (classic.builtInColors ?? [.accentColor]) : colors + guard !usable.isEmpty else { return .accentColor } + return usable[position % usable.count] + } +} + // MARK: - Home Section Layout /// Layout mode for the configurable home sections (Continue Watching, Feed, etc.). diff --git a/Yattee/Views/Home/HomeSettingsView.swift b/Yattee/Views/Home/HomeSettingsView.swift index 15713f39..511ec96e 100644 --- a/Yattee/Views/Home/HomeSettingsView.swift +++ b/Yattee/Views/Home/HomeSettingsView.swift @@ -13,6 +13,8 @@ struct HomeSettingsView: View { // Local state for editing (copied from settings on appear, saved on dismiss) @State private var shortcutLayout: HomeShortcutLayout = .cards @State private var shortcutCardStyle: HomeShortcutCardStyle = .plain + @State private var shortcutColorfulPalette: HomeShortcutColorfulPalette = .classic + @State private var shortcutCustomPaletteColors: [String] = [] @State private var shortcutOrder: [HomeShortcutItem] = [] @State private var shortcutVisibility: [HomeShortcutItem: Bool] = [:] @State private var sectionOrder: [HomeSectionItem] = [] @@ -54,7 +56,11 @@ struct HomeSettingsView: View { #if os(macOS) .listStyle(.inset) .navigationDestination(isPresented: $showingStyleSettings) { - HomeShortcutStyleView(style: $shortcutCardStyle) + HomeShortcutStyleView( + style: $shortcutCardStyle, + palette: $shortcutColorfulPalette, + customColors: $shortcutCustomPaletteColors + ) } #endif #if !os(tvOS) @@ -111,7 +117,11 @@ struct HomeSettingsView: View { systemImage: "paintpalette", trailing: { Text(shortcutCardStyle.displayName) } ) { - HomeShortcutStyleView(style: $shortcutCardStyle) + HomeShortcutStyleView( + style: $shortcutCardStyle, + palette: $shortcutColorfulPalette, + customColors: $shortcutCustomPaletteColors + ) } #endif } @@ -311,6 +321,8 @@ struct HomeSettingsView: View { shortcutLayout = settings.homeShortcutLayout shortcutCardStyle = settings.homeShortcutCardStyle + shortcutColorfulPalette = settings.homeShortcutColorfulPalette + shortcutCustomPaletteColors = settings.homeShortcutCustomPaletteColors shortcutOrder = settings.homeShortcutOrder shortcutVisibility = settings.homeShortcutVisibility sectionOrder = settings.homeSectionOrder @@ -332,6 +344,8 @@ struct HomeSettingsView: View { guard let settings = settingsManager else { return } settings.homeShortcutLayout = shortcutLayout settings.homeShortcutCardStyle = shortcutCardStyle + settings.homeShortcutColorfulPalette = shortcutColorfulPalette + settings.homeShortcutCustomPaletteColors = shortcutCustomPaletteColors settings.homeShortcutOrder = shortcutOrder settings.homeShortcutVisibility = shortcutVisibility settings.homeSectionOrder = sectionOrder diff --git a/Yattee/Views/Home/HomeShortcutCardView.swift b/Yattee/Views/Home/HomeShortcutCardView.swift index 09a4a978..6335a3cb 100644 --- a/Yattee/Views/Home/HomeShortcutCardView.swift +++ b/Yattee/Views/Home/HomeShortcutCardView.swift @@ -10,6 +10,8 @@ import SwiftUI struct HomeShortcutCardView: View { @Environment(\.appEnvironment) private var appEnvironment @Environment(\.dynamicTypeSize) private var dynamicTypeSize + /// Position-resolved colorful color; when set it overrides `colorfulColor`. + @Environment(\.homeShortcutColorfulColor) private var positionColorfulColor #if os(tvOS) @Environment(\.isFocused) private var isFocused #endif @@ -67,7 +69,7 @@ struct HomeShortcutCardView: View { } private var fillColor: Color { - cardStyle == .colorful ? colorfulColor : accentColor + cardStyle == .colorful ? (positionColorfulColor ?? colorfulColor) : accentColor } private var iconColor: Color { diff --git a/Yattee/Views/Home/HomeShortcutStyleView.swift b/Yattee/Views/Home/HomeShortcutStyleView.swift index b6aa9c27..8b82e2ed 100644 --- a/Yattee/Views/Home/HomeShortcutStyleView.swift +++ b/Yattee/Views/Home/HomeShortcutStyleView.swift @@ -11,6 +11,23 @@ import SwiftUI struct HomeShortcutStyleView: View { @Binding var style: HomeShortcutCardStyle + @Binding var palette: HomeShortcutColorfulPalette + @Binding var customColors: [String] + + /// Editing mode for the custom palette. + private enum CustomEditMode: String, CaseIterable { + case list + case text + + var displayName: LocalizedStringKey { + switch self { + case .list: return "home.shortcuts.palette.custom.mode.list" + case .text: return "home.shortcuts.palette.custom.mode.text" + } + } + } + + @State private var customEditMode: CustomEditMode = .list private let previewColumns = [GridItem(.adaptive(minimum: 150), spacing: 16)] @@ -31,9 +48,17 @@ struct HomeShortcutStyleView: View { #endif } + if style == .colorful { + paletteSection + + if palette == .custom { + customColorsSection + } + } + Section { LazyVGrid(columns: previewColumns, spacing: 16) { - ForEach(previewItems) { item in + ForEach(Array(previewItems.enumerated()), id: \.element.id) { index, item in HomeShortcutCardView( icon: item.icon, title: item.localizedTitle, @@ -43,6 +68,10 @@ struct HomeShortcutStyleView: View { colorfulColor: item.cardColor, styleOverride: style ) + .environment( + \.homeShortcutColorfulColor, + HomeShortcutColorfulPalette.color(forPosition: index, palette: palette, customHex: customColors) + ) } } .padding(.vertical, 4) @@ -60,6 +89,161 @@ struct HomeShortcutStyleView: View { #endif } + // MARK: - Palette + + private var paletteSection: some View { + Section { + Picker(String(localized: "home.settings.shortcuts.palette"), selection: $palette) { + ForEach(HomeShortcutColorfulPalette.allCases, id: \.self) { palette in + Text(palette.displayName).tag(palette) + } + } + + // Swatch strip for the selected palette. + swatchStrip(for: paletteSwatchColors) + .padding(.vertical, 4) + } header: { + Text(String(localized: "home.settings.shortcuts.palette")) + } + } + + /// Colors shown in the selected-palette swatch strip. + private var paletteSwatchColors: [Color] { + if palette == .custom { + let parsed = customColors.compactMap { Color(hex: $0) } + return parsed.isEmpty ? (HomeShortcutColorfulPalette.classic.builtInColors ?? []) : parsed + } + return palette.builtInColors ?? [] + } + + private func swatchStrip(for colors: [Color]) -> some View { + HStack(spacing: 6) { + ForEach(Array(colors.enumerated()), id: \.offset) { _, color in + RoundedRectangle(cornerRadius: 5) + .fill(color) + .frame(height: 22) + } + } + } + + // MARK: - Custom Colors + + private var customColorsSection: some View { + Section { + Picker(String(localized: "home.shortcuts.palette.custom.mode"), selection: $customEditMode) { + ForEach(CustomEditMode.allCases, id: \.self) { mode in + Text(mode.displayName).tag(mode) + } + } + .pickerStyle(.segmented) + + switch customEditMode { + case .list: + ForEach(customColors.indices, id: \.self) { index in + customColorRow(index: index) + } + .onDelete { offsets in + customColors.remove(atOffsets: offsets) + } + + Button { + let seed = HomeShortcutColorfulPalette.customStarterColors.first ?? "#4D96FF" + customColors.append(seed) + } label: { + Label(String(localized: "home.shortcuts.palette.custom.add"), systemImage: "plus.circle.fill") + } + + case .text: + TextField( + String(localized: "home.shortcuts.palette.custom.placeholder"), + text: customColorsText, + axis: .vertical + ) + .lineLimit(3 ... 6) + #if os(iOS) + .textInputAutocapitalization(.characters) + .autocorrectionDisabled() + #endif + .font(.body.monospaced()) + } + } header: { + Text(String(localized: "home.shortcuts.palette.custom.header")) + } footer: { + Text(String(localized: "home.shortcuts.palette.custom.footer")) + } + } + + private func customColorRow(index: Int) -> some View { + HStack(spacing: 12) { + ColorPicker("", selection: colorBinding(at: index), supportsOpacity: false) + .labelsHidden() + + TextField("#RRGGBB", text: hexBinding(at: index)) + #if os(iOS) + .textInputAutocapitalization(.characters) + .autocorrectionDisabled() + #endif + .font(.body.monospaced()) + + Spacer(minLength: 0) + + Button(role: .destructive) { + guard customColors.indices.contains(index) else { return } + customColors.remove(at: index) + } label: { + Image(systemName: "minus.circle.fill") + .foregroundStyle(.red) + } + .buttonStyle(.plain) + } + } + + // MARK: - Bindings + + /// Two-way binding between a stored hex string and a SwiftUI Color for the + /// native ColorPicker. + private func colorBinding(at index: Int) -> Binding { + Binding( + get: { + guard customColors.indices.contains(index) else { return .gray } + return Color(hex: customColors[index]) ?? .gray + }, + set: { newColor in + guard customColors.indices.contains(index) else { return } + customColors[index] = newColor.toHexString() + } + ) + } + + /// Editable hex text for a single custom color. + private func hexBinding(at index: Int) -> Binding { + Binding( + get: { + guard customColors.indices.contains(index) else { return "" } + return customColors[index] + }, + set: { newValue in + guard customColors.indices.contains(index) else { return } + customColors[index] = newValue + } + ) + } + + /// Comma/newline separated hex list for the text-editing mode. + private var customColorsText: Binding { + Binding( + get: { customColors.joined(separator: ", ") }, + set: { newValue in + customColors = newValue + .split(whereSeparator: { $0 == "," || $0 == "\n" }) + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + } + ) + } + + // MARK: - Preview Helpers + /// Shortcuts without a meaningful count hide it in the filled layout, /// mirroring `HomeView`'s real card configuration. private func showsCount(for item: HomeShortcutItem) -> Bool { @@ -89,8 +273,12 @@ struct HomeShortcutStyleView: View { #Preview { NavigationStack { - HomeShortcutStyleView(style: .constant(.colorful)) - .appEnvironment(.preview) + HomeShortcutStyleView( + style: .constant(.colorful), + palette: .constant(.classic), + customColors: .constant(HomeShortcutColorfulPalette.customStarterColors) + ) + .appEnvironment(.preview) } } #endif diff --git a/Yattee/Views/Home/HomeView.swift b/Yattee/Views/Home/HomeView.swift index 241d3068..28d5f340 100644 --- a/Yattee/Views/Home/HomeView.swift +++ b/Yattee/Views/Home/HomeView.swift @@ -379,9 +379,12 @@ struct HomeView: View { let gridSpacing: CGFloat = 16 #endif + let shortcuts = settingsManager?.visibleShortcuts() ?? HomeShortcutItem.defaultOrder + return LazyVGrid(columns: columns, spacing: gridSpacing) { - ForEach(settingsManager?.visibleShortcuts() ?? HomeShortcutItem.defaultOrder) { shortcut in + ForEach(Array(shortcuts.enumerated()), id: \.element.id) { index, shortcut in shortcutCardView(for: shortcut) + .environment(\.homeShortcutColorfulColor, colorfulColor(atPosition: index)) #if !os(tvOS) .contextMenu { editShortcutsButton } #endif @@ -396,6 +399,14 @@ struct HomeView: View { #endif } + /// Resolves the colorful-style color for a shortcut at the given grid + /// position from the selected palette (and custom colors when applicable). + private func colorfulColor(atPosition position: Int) -> Color { + let palette = settingsManager?.homeShortcutColorfulPalette ?? .classic + let customHex = settingsManager?.homeShortcutCustomPaletteColors ?? [] + return HomeShortcutColorfulPalette.color(forPosition: position, palette: palette, customHex: customHex) + } + private var shortcutsList: some View { let shortcuts = settingsManager?.visibleShortcuts() ?? HomeShortcutItem.defaultOrder return ForEach(Array(shortcuts.enumerated()), id: \.element) { index, shortcut in