mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 14:22:02 +00:00
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.
This commit is contained in:
@@ -65,6 +65,8 @@ enum SettingsKey: String, CaseIterable {
|
|||||||
case homeShortcutVisibility
|
case homeShortcutVisibility
|
||||||
case homeShortcutLayout
|
case homeShortcutLayout
|
||||||
case homeShortcutCardStyle
|
case homeShortcutCardStyle
|
||||||
|
case homeShortcutColorfulPalette
|
||||||
|
case homeShortcutCustomPaletteColors
|
||||||
case homeSectionOrder
|
case homeSectionOrder
|
||||||
case homeSectionVisibility
|
case homeSectionVisibility
|
||||||
case homeSectionItemsLimit
|
case homeSectionItemsLimit
|
||||||
@@ -137,6 +139,7 @@ enum SettingsKey: String, CaseIterable {
|
|||||||
case .preferredQuality, .cellularQuality, .allowSoftwareDecodedFormats, .macPlayerMode, .listStyle,
|
case .preferredQuality, .cellularQuality, .allowSoftwareDecodedFormats, .macPlayerMode, .listStyle,
|
||||||
// Home layout — different UI paradigms per platform
|
// Home layout — different UI paradigms per platform
|
||||||
.homeShortcutOrder, .homeShortcutVisibility, .homeShortcutLayout, .homeShortcutCardStyle,
|
.homeShortcutOrder, .homeShortcutVisibility, .homeShortcutLayout, .homeShortcutCardStyle,
|
||||||
|
.homeShortcutColorfulPalette, .homeShortcutCustomPaletteColors,
|
||||||
.homeSectionOrder, .homeSectionVisibility, .homeSectionItemsLimit, .homeSectionLayout,
|
.homeSectionOrder, .homeSectionVisibility, .homeSectionItemsLimit, .homeSectionLayout,
|
||||||
// Top Shelf — tvOS only
|
// Top Shelf — tvOS only
|
||||||
.topShelfSections,
|
.topShelfSections,
|
||||||
|
|||||||
@@ -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.
|
/// Layout mode for home sections (list or grid). Default is list on iOS/macOS, grid on tvOS.
|
||||||
var homeSectionLayout: HomeSectionLayout {
|
var homeSectionLayout: HomeSectionLayout {
|
||||||
get {
|
get {
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ final class SettingsManager {
|
|||||||
var _homeShortcutVisibility: [HomeShortcutItem: Bool]?
|
var _homeShortcutVisibility: [HomeShortcutItem: Bool]?
|
||||||
var _homeShortcutLayout: HomeShortcutLayout?
|
var _homeShortcutLayout: HomeShortcutLayout?
|
||||||
var _homeShortcutCardStyle: HomeShortcutCardStyle?
|
var _homeShortcutCardStyle: HomeShortcutCardStyle?
|
||||||
|
var _homeShortcutColorfulPalette: HomeShortcutColorfulPalette?
|
||||||
|
var _homeShortcutCustomPaletteColors: [String]?
|
||||||
var _homeSectionOrder: [HomeSectionItem]?
|
var _homeSectionOrder: [HomeSectionItem]?
|
||||||
var _homeSectionVisibility: [HomeSectionItem: Bool]?
|
var _homeSectionVisibility: [HomeSectionItem: Bool]?
|
||||||
var _homeSectionItemsLimit: Int?
|
var _homeSectionItemsLimit: Int?
|
||||||
@@ -465,6 +467,8 @@ final class SettingsManager {
|
|||||||
_homeShortcutVisibility = nil
|
_homeShortcutVisibility = nil
|
||||||
_homeShortcutLayout = nil
|
_homeShortcutLayout = nil
|
||||||
_homeShortcutCardStyle = nil
|
_homeShortcutCardStyle = nil
|
||||||
|
_homeShortcutColorfulPalette = nil
|
||||||
|
_homeShortcutCustomPaletteColors = nil
|
||||||
_homeSectionOrder = nil
|
_homeSectionOrder = nil
|
||||||
_homeSectionVisibility = nil
|
_homeSectionVisibility = nil
|
||||||
_homeSectionItemsLimit = nil
|
_homeSectionItemsLimit = nil
|
||||||
|
|||||||
71
Yattee/Extensions/Color+Hex.swift
Normal file
71
Yattee/Extensions/Color+Hex.swift
Normal file
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" : {
|
"home.downloads.notAvailable" : {
|
||||||
"comment" : "Downloads not available on tvOS",
|
"comment" : "Downloads not available on tvOS",
|
||||||
"localizations" : {
|
"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" : {
|
"home.empty.description" : {
|
||||||
"comment" : "Library tab placeholder description",
|
"comment" : "Library tab placeholder description",
|
||||||
"localizations" : {
|
"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" : {
|
"home.settings.shortcuts.style" : {
|
||||||
"comment" : "Label for shortcuts card style picker in library settings",
|
"comment" : "Label for shortcuts card style picker in library settings",
|
||||||
"localizations" : {
|
"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" : {
|
"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" : {
|
"home.subscriptions.subtitle" : {
|
||||||
"comment" : "Subscriptions card subtitle in library",
|
"comment" : "Subscriptions card subtitle in library",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
@@ -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
|
// MARK: - Home Section Layout
|
||||||
|
|
||||||
/// Layout mode for the configurable home sections (Continue Watching, Feed, etc.).
|
/// Layout mode for the configurable home sections (Continue Watching, Feed, etc.).
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ struct HomeSettingsView: View {
|
|||||||
// Local state for editing (copied from settings on appear, saved on dismiss)
|
// Local state for editing (copied from settings on appear, saved on dismiss)
|
||||||
@State private var shortcutLayout: HomeShortcutLayout = .cards
|
@State private var shortcutLayout: HomeShortcutLayout = .cards
|
||||||
@State private var shortcutCardStyle: HomeShortcutCardStyle = .plain
|
@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 shortcutOrder: [HomeShortcutItem] = []
|
||||||
@State private var shortcutVisibility: [HomeShortcutItem: Bool] = [:]
|
@State private var shortcutVisibility: [HomeShortcutItem: Bool] = [:]
|
||||||
@State private var sectionOrder: [HomeSectionItem] = []
|
@State private var sectionOrder: [HomeSectionItem] = []
|
||||||
@@ -54,7 +56,11 @@ struct HomeSettingsView: View {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.listStyle(.inset)
|
.listStyle(.inset)
|
||||||
.navigationDestination(isPresented: $showingStyleSettings) {
|
.navigationDestination(isPresented: $showingStyleSettings) {
|
||||||
HomeShortcutStyleView(style: $shortcutCardStyle)
|
HomeShortcutStyleView(
|
||||||
|
style: $shortcutCardStyle,
|
||||||
|
palette: $shortcutColorfulPalette,
|
||||||
|
customColors: $shortcutCustomPaletteColors
|
||||||
|
)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@@ -111,7 +117,11 @@ struct HomeSettingsView: View {
|
|||||||
systemImage: "paintpalette",
|
systemImage: "paintpalette",
|
||||||
trailing: { Text(shortcutCardStyle.displayName) }
|
trailing: { Text(shortcutCardStyle.displayName) }
|
||||||
) {
|
) {
|
||||||
HomeShortcutStyleView(style: $shortcutCardStyle)
|
HomeShortcutStyleView(
|
||||||
|
style: $shortcutCardStyle,
|
||||||
|
palette: $shortcutColorfulPalette,
|
||||||
|
customColors: $shortcutCustomPaletteColors
|
||||||
|
)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -311,6 +321,8 @@ struct HomeSettingsView: View {
|
|||||||
|
|
||||||
shortcutLayout = settings.homeShortcutLayout
|
shortcutLayout = settings.homeShortcutLayout
|
||||||
shortcutCardStyle = settings.homeShortcutCardStyle
|
shortcutCardStyle = settings.homeShortcutCardStyle
|
||||||
|
shortcutColorfulPalette = settings.homeShortcutColorfulPalette
|
||||||
|
shortcutCustomPaletteColors = settings.homeShortcutCustomPaletteColors
|
||||||
shortcutOrder = settings.homeShortcutOrder
|
shortcutOrder = settings.homeShortcutOrder
|
||||||
shortcutVisibility = settings.homeShortcutVisibility
|
shortcutVisibility = settings.homeShortcutVisibility
|
||||||
sectionOrder = settings.homeSectionOrder
|
sectionOrder = settings.homeSectionOrder
|
||||||
@@ -332,6 +344,8 @@ struct HomeSettingsView: View {
|
|||||||
guard let settings = settingsManager else { return }
|
guard let settings = settingsManager else { return }
|
||||||
settings.homeShortcutLayout = shortcutLayout
|
settings.homeShortcutLayout = shortcutLayout
|
||||||
settings.homeShortcutCardStyle = shortcutCardStyle
|
settings.homeShortcutCardStyle = shortcutCardStyle
|
||||||
|
settings.homeShortcutColorfulPalette = shortcutColorfulPalette
|
||||||
|
settings.homeShortcutCustomPaletteColors = shortcutCustomPaletteColors
|
||||||
settings.homeShortcutOrder = shortcutOrder
|
settings.homeShortcutOrder = shortcutOrder
|
||||||
settings.homeShortcutVisibility = shortcutVisibility
|
settings.homeShortcutVisibility = shortcutVisibility
|
||||||
settings.homeSectionOrder = sectionOrder
|
settings.homeSectionOrder = sectionOrder
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import SwiftUI
|
|||||||
struct HomeShortcutCardView<StatusIndicator: View>: View {
|
struct HomeShortcutCardView<StatusIndicator: View>: View {
|
||||||
@Environment(\.appEnvironment) private var appEnvironment
|
@Environment(\.appEnvironment) private var appEnvironment
|
||||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||||
|
/// Position-resolved colorful color; when set it overrides `colorfulColor`.
|
||||||
|
@Environment(\.homeShortcutColorfulColor) private var positionColorfulColor
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@Environment(\.isFocused) private var isFocused
|
@Environment(\.isFocused) private var isFocused
|
||||||
#endif
|
#endif
|
||||||
@@ -67,7 +69,7 @@ struct HomeShortcutCardView<StatusIndicator: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var fillColor: Color {
|
private var fillColor: Color {
|
||||||
cardStyle == .colorful ? colorfulColor : accentColor
|
cardStyle == .colorful ? (positionColorfulColor ?? colorfulColor) : accentColor
|
||||||
}
|
}
|
||||||
|
|
||||||
private var iconColor: Color {
|
private var iconColor: Color {
|
||||||
|
|||||||
@@ -11,6 +11,23 @@ import SwiftUI
|
|||||||
|
|
||||||
struct HomeShortcutStyleView: View {
|
struct HomeShortcutStyleView: View {
|
||||||
@Binding var style: HomeShortcutCardStyle
|
@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)]
|
private let previewColumns = [GridItem(.adaptive(minimum: 150), spacing: 16)]
|
||||||
|
|
||||||
@@ -31,9 +48,17 @@ struct HomeShortcutStyleView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if style == .colorful {
|
||||||
|
paletteSection
|
||||||
|
|
||||||
|
if palette == .custom {
|
||||||
|
customColorsSection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
LazyVGrid(columns: previewColumns, spacing: 16) {
|
LazyVGrid(columns: previewColumns, spacing: 16) {
|
||||||
ForEach(previewItems) { item in
|
ForEach(Array(previewItems.enumerated()), id: \.element.id) { index, item in
|
||||||
HomeShortcutCardView(
|
HomeShortcutCardView(
|
||||||
icon: item.icon,
|
icon: item.icon,
|
||||||
title: item.localizedTitle,
|
title: item.localizedTitle,
|
||||||
@@ -43,6 +68,10 @@ struct HomeShortcutStyleView: View {
|
|||||||
colorfulColor: item.cardColor,
|
colorfulColor: item.cardColor,
|
||||||
styleOverride: style
|
styleOverride: style
|
||||||
)
|
)
|
||||||
|
.environment(
|
||||||
|
\.homeShortcutColorfulColor,
|
||||||
|
HomeShortcutColorfulPalette.color(forPosition: index, palette: palette, customHex: customColors)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
@@ -60,6 +89,161 @@ struct HomeShortcutStyleView: View {
|
|||||||
#endif
|
#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<Color> {
|
||||||
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
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,
|
/// Shortcuts without a meaningful count hide it in the filled layout,
|
||||||
/// mirroring `HomeView`'s real card configuration.
|
/// mirroring `HomeView`'s real card configuration.
|
||||||
private func showsCount(for item: HomeShortcutItem) -> Bool {
|
private func showsCount(for item: HomeShortcutItem) -> Bool {
|
||||||
@@ -89,8 +273,12 @@ struct HomeShortcutStyleView: View {
|
|||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
HomeShortcutStyleView(style: .constant(.colorful))
|
HomeShortcutStyleView(
|
||||||
.appEnvironment(.preview)
|
style: .constant(.colorful),
|
||||||
|
palette: .constant(.classic),
|
||||||
|
customColors: .constant(HomeShortcutColorfulPalette.customStarterColors)
|
||||||
|
)
|
||||||
|
.appEnvironment(.preview)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -379,9 +379,12 @@ struct HomeView: View {
|
|||||||
let gridSpacing: CGFloat = 16
|
let gridSpacing: CGFloat = 16
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
let shortcuts = settingsManager?.visibleShortcuts() ?? HomeShortcutItem.defaultOrder
|
||||||
|
|
||||||
return LazyVGrid(columns: columns, spacing: gridSpacing) {
|
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)
|
shortcutCardView(for: shortcut)
|
||||||
|
.environment(\.homeShortcutColorfulColor, colorfulColor(atPosition: index))
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.contextMenu { editShortcutsButton }
|
.contextMenu { editShortcutsButton }
|
||||||
#endif
|
#endif
|
||||||
@@ -396,6 +399,14 @@ struct HomeView: View {
|
|||||||
#endif
|
#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 {
|
private var shortcutsList: some View {
|
||||||
let shortcuts = settingsManager?.visibleShortcuts() ?? HomeShortcutItem.defaultOrder
|
let shortcuts = settingsManager?.visibleShortcuts() ?? HomeShortcutItem.defaultOrder
|
||||||
return ForEach(Array(shortcuts.enumerated()), id: \.element) { index, shortcut in
|
return ForEach(Array(shortcuts.enumerated()), id: \.element) { index, shortcut in
|
||||||
|
|||||||
Reference in New Issue
Block a user