mirror of
https://github.com/yattee/yattee.git
synced 2026-07-21 06:42:01 +00:00
Add card style option for Home shortcut cards
Adds a Style picker (Plain / Accent / Colorful) to the Customize Home sheet, shown when the shortcuts layout is Cards. Defaults to Plain. - Plain: current look (light accent tint + accent border) - Accent: solid accent fill, white icon/text - Colorful: solid fill with a fixed color per shortcut type The filled styles use a Reminders-style layout (icon top-leading, count top-trailing, title anchored to the bottom) with a subtle gradient sheen over the fill color. Cards without a meaningful count (Open Link, Remote, Subscriptions, instance content, media sources) hide the number; Remote shows its status dot in the top-right slot instead. Persisted via SettingsKey.homeShortcutCardStyle, mirroring the existing homeShortcutLayout accessor.
This commit is contained in:
@@ -64,6 +64,7 @@ enum SettingsKey: String, CaseIterable {
|
|||||||
case homeShortcutOrder
|
case homeShortcutOrder
|
||||||
case homeShortcutVisibility
|
case homeShortcutVisibility
|
||||||
case homeShortcutLayout
|
case homeShortcutLayout
|
||||||
|
case homeShortcutCardStyle
|
||||||
case homeSectionOrder
|
case homeSectionOrder
|
||||||
case homeSectionVisibility
|
case homeSectionVisibility
|
||||||
case homeSectionItemsLimit
|
case homeSectionItemsLimit
|
||||||
@@ -135,7 +136,7 @@ enum SettingsKey: String, CaseIterable {
|
|||||||
switch self {
|
switch self {
|
||||||
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,
|
.homeShortcutOrder, .homeShortcutVisibility, .homeShortcutLayout, .homeShortcutCardStyle,
|
||||||
.homeSectionOrder, .homeSectionVisibility, .homeSectionItemsLimit, .homeSectionLayout,
|
.homeSectionOrder, .homeSectionVisibility, .homeSectionItemsLimit, .homeSectionLayout,
|
||||||
// Top Shelf — tvOS only
|
// Top Shelf — tvOS only
|
||||||
.topShelfSections,
|
.topShelfSections,
|
||||||
|
|||||||
@@ -95,6 +95,21 @@ extension SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Visual style for home shortcut cards (plain, accent-filled, or colorful). Default is plain.
|
||||||
|
var homeShortcutCardStyle: HomeShortcutCardStyle {
|
||||||
|
get {
|
||||||
|
if let cached = _homeShortcutCardStyle { return cached }
|
||||||
|
guard let rawValue = string(for: .homeShortcutCardStyle) else {
|
||||||
|
return .plain
|
||||||
|
}
|
||||||
|
return HomeShortcutCardStyle(rawValue: rawValue) ?? .plain
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
_homeShortcutCardStyle = newValue
|
||||||
|
set(newValue.rawValue, for: .homeShortcutCardStyle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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 {
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ final class SettingsManager {
|
|||||||
var _homeShortcutOrder: [HomeShortcutItem]?
|
var _homeShortcutOrder: [HomeShortcutItem]?
|
||||||
var _homeShortcutVisibility: [HomeShortcutItem: Bool]?
|
var _homeShortcutVisibility: [HomeShortcutItem: Bool]?
|
||||||
var _homeShortcutLayout: HomeShortcutLayout?
|
var _homeShortcutLayout: HomeShortcutLayout?
|
||||||
|
var _homeShortcutCardStyle: HomeShortcutCardStyle?
|
||||||
var _homeSectionOrder: [HomeSectionItem]?
|
var _homeSectionOrder: [HomeSectionItem]?
|
||||||
var _homeSectionVisibility: [HomeSectionItem: Bool]?
|
var _homeSectionVisibility: [HomeSectionItem: Bool]?
|
||||||
var _homeSectionItemsLimit: Int?
|
var _homeSectionItemsLimit: Int?
|
||||||
@@ -463,6 +464,7 @@ final class SettingsManager {
|
|||||||
_homeShortcutOrder = nil
|
_homeShortcutOrder = nil
|
||||||
_homeShortcutVisibility = nil
|
_homeShortcutVisibility = nil
|
||||||
_homeShortcutLayout = nil
|
_homeShortcutLayout = nil
|
||||||
|
_homeShortcutCardStyle = nil
|
||||||
_homeSectionOrder = nil
|
_homeSectionOrder = nil
|
||||||
_homeSectionVisibility = nil
|
_homeSectionVisibility = nil
|
||||||
_homeSectionItemsLimit = nil
|
_homeSectionItemsLimit = nil
|
||||||
|
|||||||
@@ -4192,6 +4192,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"home.settings.shortcuts.style" : {
|
||||||
|
"comment" : "Label for shortcuts card style picker in library settings",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Style"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,6 +30,26 @@ enum HomeShortcutLayout: String, CaseIterable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Home Shortcut Card Style
|
||||||
|
|
||||||
|
/// Visual style for shortcut cards in the Home view (cards layout only).
|
||||||
|
enum HomeShortcutCardStyle: String, CaseIterable, Sendable {
|
||||||
|
/// Current look: light accent tint background with an accent border.
|
||||||
|
case plain
|
||||||
|
/// Solid fill in the user's accent color, white icon/text.
|
||||||
|
case accent
|
||||||
|
/// Solid fill with a fixed color per shortcut type, white icon/text (Reminders-style).
|
||||||
|
case colorful
|
||||||
|
|
||||||
|
var displayName: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .plain: return "home.shortcuts.style.plain"
|
||||||
|
case .accent: return "home.shortcuts.style.accent"
|
||||||
|
case .colorful: return "home.shortcuts.style.colorful"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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.).
|
||||||
@@ -190,6 +210,42 @@ enum HomeShortcutItem: Codable, Hashable, Identifiable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fixed color for this shortcut used by the "colorful" card style.
|
||||||
|
/// Dynamic items (instance content / media sources) derive a stable color
|
||||||
|
/// from their identifier so multiple items get varied hues.
|
||||||
|
var cardColor: Color {
|
||||||
|
switch self {
|
||||||
|
case .openURL: return .blue
|
||||||
|
case .remoteControl: return .teal
|
||||||
|
case .playlists: return .indigo
|
||||||
|
case .bookmarks: return .pink
|
||||||
|
case .continueWatching: return .purple
|
||||||
|
case .history: return .orange
|
||||||
|
case .downloads: return .green
|
||||||
|
case .channels: return .red
|
||||||
|
case .subscriptions: return .cyan
|
||||||
|
case .mediaSources: return .brown
|
||||||
|
case .instanceContent(let instanceID, _):
|
||||||
|
return Self.colorfulPalette[Self.stableIndex(for: instanceID.uuidString, count: Self.colorfulPalette.count)]
|
||||||
|
case .mediaSource(let sourceID):
|
||||||
|
return Self.colorfulPalette[Self.stableIndex(for: sourceID.uuidString, count: Self.colorfulPalette.count)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Palette used to assign stable colors to dynamic shortcut items.
|
||||||
|
private static let colorfulPalette: [Color] = [
|
||||||
|
.blue, .red, .orange, .pink, .teal, .green, .indigo, .purple, .cyan, .brown
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Deterministic (launch-stable) hash of a string into a palette index.
|
||||||
|
private static func stableIndex(for key: String, count: Int) -> Int {
|
||||||
|
var hash = 5381
|
||||||
|
for byte in key.utf8 {
|
||||||
|
hash = (hash &* 33) &+ Int(byte)
|
||||||
|
}
|
||||||
|
return abs(hash) % max(count, 1)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Codable Implementation
|
// MARK: - Codable Implementation
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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 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] = []
|
||||||
@@ -73,6 +74,15 @@ struct HomeSettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.segmented)
|
||||||
|
|
||||||
|
// Card style picker (Plain / Accent / Colorful) — only for cards layout
|
||||||
|
if shortcutLayout == .cards {
|
||||||
|
Picker(String(localized: "home.settings.shortcuts.style"), selection: $shortcutCardStyle) {
|
||||||
|
ForEach(HomeShortcutCardStyle.allCases, id: \.self) { style in
|
||||||
|
Text(style.displayName).tag(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -268,6 +278,7 @@ struct HomeSettingsView: View {
|
|||||||
let env = appEnvironment else { return }
|
let env = appEnvironment else { return }
|
||||||
|
|
||||||
shortcutLayout = settings.homeShortcutLayout
|
shortcutLayout = settings.homeShortcutLayout
|
||||||
|
shortcutCardStyle = settings.homeShortcutCardStyle
|
||||||
shortcutOrder = settings.homeShortcutOrder
|
shortcutOrder = settings.homeShortcutOrder
|
||||||
shortcutVisibility = settings.homeShortcutVisibility
|
shortcutVisibility = settings.homeShortcutVisibility
|
||||||
sectionOrder = settings.homeSectionOrder
|
sectionOrder = settings.homeSectionOrder
|
||||||
@@ -288,6 +299,7 @@ struct HomeSettingsView: View {
|
|||||||
private func saveSettings() {
|
private func saveSettings() {
|
||||||
guard let settings = settingsManager else { return }
|
guard let settings = settingsManager else { return }
|
||||||
settings.homeShortcutLayout = shortcutLayout
|
settings.homeShortcutLayout = shortcutLayout
|
||||||
|
settings.homeShortcutCardStyle = shortcutCardStyle
|
||||||
settings.homeShortcutOrder = shortcutOrder
|
settings.homeShortcutOrder = shortcutOrder
|
||||||
settings.homeShortcutVisibility = shortcutVisibility
|
settings.homeShortcutVisibility = shortcutVisibility
|
||||||
settings.homeSectionOrder = sectionOrder
|
settings.homeSectionOrder = sectionOrder
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ struct HomeShortcutCardView<StatusIndicator: View>: View {
|
|||||||
let title: String
|
let title: String
|
||||||
let count: Int
|
let count: Int
|
||||||
let subtitle: String
|
let subtitle: String
|
||||||
|
/// Whether to show the count number in the filled (Reminders) layout.
|
||||||
|
/// Cards without a meaningful count (Open Link, Remote, Subscriptions) hide it.
|
||||||
|
var showsCount: Bool
|
||||||
|
/// Fixed color used when the "colorful" card style is active.
|
||||||
|
var colorfulColor: Color
|
||||||
var statusIndicator: StatusIndicator?
|
var statusIndicator: StatusIndicator?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@@ -29,15 +34,49 @@ struct HomeShortcutCardView<StatusIndicator: View>: View {
|
|||||||
title: String,
|
title: String,
|
||||||
count: Int,
|
count: Int,
|
||||||
subtitle: String,
|
subtitle: String,
|
||||||
|
showsCount: Bool = true,
|
||||||
|
colorfulColor: Color = .accentColor,
|
||||||
statusIndicator: StatusIndicator?
|
statusIndicator: StatusIndicator?
|
||||||
) {
|
) {
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.title = title
|
self.title = title
|
||||||
self.count = count
|
self.count = count
|
||||||
self.subtitle = subtitle
|
self.subtitle = subtitle
|
||||||
|
self.showsCount = showsCount
|
||||||
|
self.colorfulColor = colorfulColor
|
||||||
self.statusIndicator = statusIndicator
|
self.statusIndicator = statusIndicator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Card Style
|
||||||
|
|
||||||
|
private var cardStyle: HomeShortcutCardStyle {
|
||||||
|
#if os(tvOS)
|
||||||
|
return .plain
|
||||||
|
#else
|
||||||
|
return appEnvironment?.settingsManager.homeShortcutCardStyle ?? .plain
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isFilled: Bool {
|
||||||
|
cardStyle != .plain
|
||||||
|
}
|
||||||
|
|
||||||
|
private var fillColor: Color {
|
||||||
|
cardStyle == .colorful ? colorfulColor : accentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
private var iconColor: Color {
|
||||||
|
isFilled ? .white : accentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
private var titleColor: Color {
|
||||||
|
isFilled ? .white : .primary
|
||||||
|
}
|
||||||
|
|
||||||
|
private var subtitleColor: Color {
|
||||||
|
isFilled ? Color.white.opacity(0.85) : .secondary
|
||||||
|
}
|
||||||
|
|
||||||
// Platform-specific styling
|
// Platform-specific styling
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
private let iconSize: CGFloat = 36
|
private let iconSize: CGFloat = 36
|
||||||
@@ -70,18 +109,30 @@ struct HomeShortcutCardView<StatusIndicator: View>: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Filled styles (accent/colorful) use a Reminders-style layout:
|
||||||
|
/// icon top-leading, count top-trailing, title at the bottom.
|
||||||
|
private var useRemindersLayout: Bool {
|
||||||
|
#if os(tvOS)
|
||||||
|
return false
|
||||||
|
#else
|
||||||
|
return isFilled
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
private var hasSubtitle: Bool {
|
private var hasSubtitle: Bool {
|
||||||
!subtitle.isEmpty
|
!subtitle.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if needsVerticalLayout {
|
if useRemindersLayout {
|
||||||
|
remindersLayout
|
||||||
|
} else if needsVerticalLayout {
|
||||||
// Vertical layout for tvOS and accessibility sizes
|
// Vertical layout for tvOS and accessibility sizes
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.foregroundStyle(accentColor)
|
.foregroundStyle(iconColor)
|
||||||
.frame(width: iconSize)
|
.frame(width: iconSize)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
@@ -89,7 +140,7 @@ struct HomeShortcutCardView<StatusIndicator: View>: View {
|
|||||||
Text(title)
|
Text(title)
|
||||||
.font(titleFont)
|
.font(titleFont)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(titleColor)
|
||||||
if let statusIndicator {
|
if let statusIndicator {
|
||||||
statusIndicator.padding(.leading, 4)
|
statusIndicator.padding(.leading, 4)
|
||||||
}
|
}
|
||||||
@@ -97,7 +148,7 @@ struct HomeShortcutCardView<StatusIndicator: View>: View {
|
|||||||
|
|
||||||
Text(hasSubtitle ? subtitle : " ")
|
Text(hasSubtitle ? subtitle : " ")
|
||||||
.font(subtitleFont)
|
.font(subtitleFont)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(subtitleColor)
|
||||||
.opacity(hasSubtitle ? 1 : 0)
|
.opacity(hasSubtitle ? 1 : 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +158,7 @@ struct HomeShortcutCardView<StatusIndicator: View>: View {
|
|||||||
HStack(alignment: .center, spacing: 8) {
|
HStack(alignment: .center, spacing: 8) {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.foregroundStyle(accentColor)
|
.foregroundStyle(iconColor)
|
||||||
.frame(width: iconSize)
|
.frame(width: iconSize)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
@@ -115,7 +166,7 @@ struct HomeShortcutCardView<StatusIndicator: View>: View {
|
|||||||
Text(title)
|
Text(title)
|
||||||
.font(titleFont)
|
.font(titleFont)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(titleColor)
|
||||||
.allowsTightening(true)
|
.allowsTightening(true)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
if let statusIndicator {
|
if let statusIndicator {
|
||||||
@@ -126,7 +177,7 @@ struct HomeShortcutCardView<StatusIndicator: View>: View {
|
|||||||
if hasSubtitle {
|
if hasSubtitle {
|
||||||
Text(subtitle)
|
Text(subtitle)
|
||||||
.font(subtitleFont)
|
.font(subtitleFont)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(subtitleColor)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,11 +192,48 @@ struct HomeShortcutCardView<StatusIndicator: View>: View {
|
|||||||
.background(cardBackground)
|
.background(cardBackground)
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: cornerRadius)
|
RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
.strokeBorder(accentColor.opacity(0.3), lineWidth: 1)
|
.strokeBorder(borderColor, lineWidth: 1)
|
||||||
)
|
)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reminders-style layout used by the accent/colorful filled styles:
|
||||||
|
/// icon top-leading, count top-trailing, title anchored to the bottom.
|
||||||
|
private var remindersLayout: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack(alignment: .top, spacing: 4) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundStyle(iconColor)
|
||||||
|
|
||||||
|
Spacer(minLength: 4)
|
||||||
|
|
||||||
|
if let statusIndicator {
|
||||||
|
statusIndicator
|
||||||
|
}
|
||||||
|
|
||||||
|
if showsCount {
|
||||||
|
Text(count, format: .number)
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.monospacedDigit()
|
||||||
|
.foregroundStyle(titleColor)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 8)
|
||||||
|
|
||||||
|
Text(title)
|
||||||
|
.font(titleFont)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundStyle(titleColor)
|
||||||
|
.lineLimit(1)
|
||||||
|
.allowsTightening(true)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, minHeight: remindersMinHeight, alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
/// Minimum height for the text content area to ensure consistent card heights
|
/// Minimum height for the text content area to ensure consistent card heights
|
||||||
private var subtitleMinHeight: CGFloat {
|
private var subtitleMinHeight: CGFloat {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -160,11 +248,41 @@ struct HomeShortcutCardView<StatusIndicator: View>: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var cardBackground: some ShapeStyle {
|
/// Minimum content height for the Reminders-style layout so the title sits
|
||||||
#if os(tvOS)
|
/// well below the icon/count row (matches Apple's roomier card proportions).
|
||||||
isFocused ? Color.white.opacity(0.2) : Color.gray.opacity(0.3)
|
private var remindersMinHeight: CGFloat {
|
||||||
|
#if os(macOS)
|
||||||
|
64
|
||||||
#else
|
#else
|
||||||
|
60
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var cardBackground: some View {
|
||||||
|
#if os(tvOS)
|
||||||
|
(isFocused ? Color.white.opacity(0.2) : Color.gray.opacity(0.3))
|
||||||
|
#else
|
||||||
|
if isFilled {
|
||||||
|
// Solid fill plus a subtle top-to-bottom sheen for a gentle gradient.
|
||||||
|
fillColor.overlay(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [Color.white.opacity(0.28), Color.clear, Color.black.opacity(0.10)],
|
||||||
|
startPoint: .top,
|
||||||
|
endPoint: .bottom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
accentColor.opacity(0.1)
|
accentColor.opacity(0.1)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var borderColor: Color {
|
||||||
|
#if os(tvOS)
|
||||||
|
return accentColor.opacity(0.3)
|
||||||
|
#else
|
||||||
|
return isFilled ? .clear : accentColor.opacity(0.3)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,12 +294,16 @@ extension HomeShortcutCardView where StatusIndicator == EmptyView {
|
|||||||
icon: String,
|
icon: String,
|
||||||
title: String,
|
title: String,
|
||||||
count: Int,
|
count: Int,
|
||||||
subtitle: String
|
subtitle: String,
|
||||||
|
showsCount: Bool = true,
|
||||||
|
colorfulColor: Color = .accentColor
|
||||||
) {
|
) {
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.title = title
|
self.title = title
|
||||||
self.count = count
|
self.count = count
|
||||||
self.subtitle = subtitle
|
self.subtitle = subtitle
|
||||||
|
self.showsCount = showsCount
|
||||||
|
self.colorfulColor = colorfulColor
|
||||||
self.statusIndicator = nil
|
self.statusIndicator = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -485,7 +485,9 @@ struct HomeView: View {
|
|||||||
icon: "link",
|
icon: "link",
|
||||||
title: String(localized: "home.shortcut.openURL"),
|
title: String(localized: "home.shortcut.openURL"),
|
||||||
count: 0,
|
count: 0,
|
||||||
subtitle: ""
|
subtitle: "",
|
||||||
|
showsCount: false,
|
||||||
|
colorfulColor: HomeShortcutItem.openURL.cardColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -507,6 +509,8 @@ struct HomeView: View {
|
|||||||
title: String(localized: "home.shortcut.remoteControl"),
|
title: String(localized: "home.shortcut.remoteControl"),
|
||||||
count: discoveredDevicesCount,
|
count: discoveredDevicesCount,
|
||||||
subtitle: "",
|
subtitle: "",
|
||||||
|
showsCount: false,
|
||||||
|
colorfulColor: HomeShortcutItem.remoteControl.cardColor,
|
||||||
statusIndicator: Circle()
|
statusIndicator: Circle()
|
||||||
.fill(isHosting ? Color.green : Color.red)
|
.fill(isHosting ? Color.green : Color.red)
|
||||||
.frame(width: 8, height: 8)
|
.frame(width: 8, height: 8)
|
||||||
@@ -528,7 +532,8 @@ struct HomeView: View {
|
|||||||
icon: "list.bullet.rectangle",
|
icon: "list.bullet.rectangle",
|
||||||
title: String(localized: "home.playlists.title"),
|
title: String(localized: "home.playlists.title"),
|
||||||
count: playlists.count,
|
count: playlists.count,
|
||||||
subtitle: formatCount(playlists.count, singular: "home.count.playlist", plural: "home.count.playlists")
|
subtitle: formatCount(playlists.count, singular: "home.count.playlist", plural: "home.count.playlists"),
|
||||||
|
colorfulColor: HomeShortcutItem.playlists.cardColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -547,7 +552,8 @@ struct HomeView: View {
|
|||||||
icon: "bookmark",
|
icon: "bookmark",
|
||||||
title: String(localized: "home.bookmarks.title"),
|
title: String(localized: "home.bookmarks.title"),
|
||||||
count: bookmarksCount,
|
count: bookmarksCount,
|
||||||
subtitle: formatCount(bookmarksCount, singular: "home.count.bookmark", plural: "home.count.bookmarks")
|
subtitle: formatCount(bookmarksCount, singular: "home.count.bookmark", plural: "home.count.bookmarks"),
|
||||||
|
colorfulColor: HomeShortcutItem.bookmarks.cardColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -566,7 +572,8 @@ struct HomeView: View {
|
|||||||
icon: "play.circle",
|
icon: "play.circle",
|
||||||
title: String(localized: "home.shortcut.continueWatching"),
|
title: String(localized: "home.shortcut.continueWatching"),
|
||||||
count: continueWatchingCount,
|
count: continueWatchingCount,
|
||||||
subtitle: formatCount(continueWatchingCount, singular: "home.count.video", plural: "home.count.videos")
|
subtitle: formatCount(continueWatchingCount, singular: "home.count.video", plural: "home.count.videos"),
|
||||||
|
colorfulColor: HomeShortcutItem.continueWatching.cardColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -585,7 +592,8 @@ struct HomeView: View {
|
|||||||
icon: "clock",
|
icon: "clock",
|
||||||
title: String(localized: "home.history.title"),
|
title: String(localized: "home.history.title"),
|
||||||
count: historyCount,
|
count: historyCount,
|
||||||
subtitle: formatCount(historyCount, singular: "home.count.video", plural: "home.count.videos")
|
subtitle: formatCount(historyCount, singular: "home.count.video", plural: "home.count.videos"),
|
||||||
|
colorfulColor: HomeShortcutItem.history.cardColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -606,7 +614,8 @@ struct HomeView: View {
|
|||||||
icon: "arrow.down.circle",
|
icon: "arrow.down.circle",
|
||||||
title: String(localized: "home.downloads.title"),
|
title: String(localized: "home.downloads.title"),
|
||||||
count: count,
|
count: count,
|
||||||
subtitle: formatCount(count, singular: "home.count.video", plural: "home.count.videos")
|
subtitle: formatCount(count, singular: "home.count.video", plural: "home.count.videos"),
|
||||||
|
colorfulColor: HomeShortcutItem.downloads.cardColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -622,7 +631,8 @@ struct HomeView: View {
|
|||||||
icon: "person.2",
|
icon: "person.2",
|
||||||
title: String(localized: "home.channels.title"),
|
title: String(localized: "home.channels.title"),
|
||||||
count: channelsCount,
|
count: channelsCount,
|
||||||
subtitle: formatCount(channelsCount, singular: "home.count.channel", plural: "home.count.channels")
|
subtitle: formatCount(channelsCount, singular: "home.count.channel", plural: "home.count.channels"),
|
||||||
|
colorfulColor: HomeShortcutItem.channels.cardColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -641,7 +651,9 @@ struct HomeView: View {
|
|||||||
icon: "play.square.stack",
|
icon: "play.square.stack",
|
||||||
title: String(localized: "home.subscriptions.title"),
|
title: String(localized: "home.subscriptions.title"),
|
||||||
count: 0,
|
count: 0,
|
||||||
subtitle: String(localized: "home.subscriptions.subtitle")
|
subtitle: String(localized: "home.subscriptions.subtitle"),
|
||||||
|
showsCount: false,
|
||||||
|
colorfulColor: HomeShortcutItem.subscriptions.cardColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -663,7 +675,8 @@ struct HomeView: View {
|
|||||||
icon: "externaldrive.connected.to.line.below",
|
icon: "externaldrive.connected.to.line.below",
|
||||||
title: "Sources",
|
title: "Sources",
|
||||||
count: count,
|
count: count,
|
||||||
subtitle: count == 1 ? "1 source" : "\(count) sources"
|
subtitle: count == 1 ? "1 source" : "\(count) sources",
|
||||||
|
colorfulColor: HomeShortcutItem.mediaSources.cardColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -686,7 +699,9 @@ struct HomeView: View {
|
|||||||
icon: contentType.icon,
|
icon: contentType.icon,
|
||||||
title: contentType.localizedTitle,
|
title: contentType.localizedTitle,
|
||||||
count: 0,
|
count: 0,
|
||||||
subtitle: instance.displayName
|
subtitle: instance.displayName,
|
||||||
|
showsCount: false,
|
||||||
|
colorfulColor: HomeShortcutItem.instanceContent(instanceID: instanceID, contentType: contentType).cardColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -709,7 +724,9 @@ struct HomeView: View {
|
|||||||
icon: source.type.systemImage,
|
icon: source.type.systemImage,
|
||||||
title: source.name,
|
title: source.name,
|
||||||
count: 0,
|
count: 0,
|
||||||
subtitle: source.type.displayName
|
subtitle: source.type.displayName,
|
||||||
|
showsCount: false,
|
||||||
|
colorfulColor: HomeShortcutItem.mediaSource(sourceID: sourceID).cardColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
|
|||||||
Reference in New Issue
Block a user