mirror of
https://github.com/yattee/yattee.git
synced 2026-05-13 10:55:03 +00:00
Make sources list feel native on macOS
Drop the iOS-grouped rounded card, per-row chevron, and oversized metrics on macOS. Use tighter padding, smaller icon/title fonts, uppercase section headers, and top/bottom dividers so the list reads like a native grouped Mac list. Force .buttonStyle(.plain) on row buttons/NavigationLinks and add .contentShape(Rectangle()) so the full row is hit-testable without picking up macOS's default link styling. iOS and tvOS unchanged.
This commit is contained in:
@@ -17,13 +17,25 @@ struct SourceListRow<Content: View>: View {
|
|||||||
@ViewBuilder let content: () -> Content
|
@ViewBuilder let content: () -> Content
|
||||||
|
|
||||||
/// Horizontal padding for row content.
|
/// Horizontal padding for row content.
|
||||||
|
#if os(macOS)
|
||||||
|
private let horizontalPadding: CGFloat = 12
|
||||||
|
#else
|
||||||
private let horizontalPadding: CGFloat = 16
|
private let horizontalPadding: CGFloat = 16
|
||||||
|
#endif
|
||||||
|
|
||||||
/// Vertical padding for row content.
|
/// Vertical padding for row content.
|
||||||
|
#if os(macOS)
|
||||||
|
private let verticalPadding: CGFloat = 8
|
||||||
|
#else
|
||||||
private let verticalPadding: CGFloat = 12
|
private let verticalPadding: CGFloat = 12
|
||||||
|
#endif
|
||||||
|
|
||||||
/// Width of the icon column.
|
/// Width of the icon column.
|
||||||
|
#if os(macOS)
|
||||||
|
private let iconWidth: CGFloat = 24
|
||||||
|
#else
|
||||||
private let iconWidth: CGFloat = 32
|
private let iconWidth: CGFloat = 32
|
||||||
|
#endif
|
||||||
|
|
||||||
/// Spacing between icon and text.
|
/// Spacing between icon and text.
|
||||||
private let iconTextSpacing: CGFloat = 12
|
private let iconTextSpacing: CGFloat = 12
|
||||||
|
|||||||
@@ -170,6 +170,16 @@ struct MediaSourcesView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func sectionHeader(_ title: String) -> some View {
|
private func sectionHeader(_ title: String) -> some View {
|
||||||
|
#if os(macOS)
|
||||||
|
Text(title)
|
||||||
|
.font(.subheadline)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.top, 12)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
#else
|
||||||
Text(title)
|
Text(title)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -177,6 +187,7 @@ struct MediaSourcesView: View {
|
|||||||
.padding(.horizontal, listStyle == .inset ? 32 : 16)
|
.padding(.horizontal, listStyle == .inset ? 32 : 16)
|
||||||
.padding(.top, 16)
|
.padding(.top, 16)
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sourcesList: some View {
|
private var sourcesList: some View {
|
||||||
@@ -333,6 +344,16 @@ struct MediaSourcesView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func sectionCard<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
private func sectionCard<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
||||||
|
#if os(macOS)
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Divider()
|
||||||
|
LazyVStack(spacing: 0) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
.padding(.bottom, 12)
|
||||||
|
#else
|
||||||
if listStyle == .inset {
|
if listStyle == .inset {
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
content()
|
content()
|
||||||
@@ -347,6 +368,7 @@ struct MediaSourcesView: View {
|
|||||||
}
|
}
|
||||||
.padding(.bottom, 16)
|
.padding(.bottom, 16)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -364,7 +386,11 @@ struct MediaSourcesView: View {
|
|||||||
NavigationLink(value: NavigationDestination.instanceBrowse(instance)) {
|
NavigationLink(value: NavigationDestination.instanceBrowse(instance)) {
|
||||||
instanceRow(instance)
|
instanceRow(instance)
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
#else
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.modifier(FirstRowFocusModifier(isFirst: isFirst, focus: $firstSourceFocused))
|
.modifier(FirstRowFocusModifier(isFirst: isFirst, focus: $firstSourceFocused))
|
||||||
@@ -416,12 +442,20 @@ struct MediaSourcesView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
mediaSourceRow(source, needsPassword: true)
|
mediaSourceRow(source, needsPassword: true)
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
#else
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
NavigationLink(value: NavigationDestination.mediaBrowser(source, path: "/")) {
|
NavigationLink(value: NavigationDestination.mediaBrowser(source, path: "/")) {
|
||||||
mediaSourceRow(source, needsPassword: false)
|
mediaSourceRow(source, needsPassword: false)
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
#else
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@@ -460,15 +494,24 @@ struct MediaSourcesView: View {
|
|||||||
#else
|
#else
|
||||||
let rowSpacing: CGFloat = 12
|
let rowSpacing: CGFloat = 12
|
||||||
#endif
|
#endif
|
||||||
|
#if os(macOS)
|
||||||
|
let iconFont: Font = .title3
|
||||||
|
let iconFrameWidth: CGFloat = 24
|
||||||
|
let titleFont: Font = .body
|
||||||
|
#else
|
||||||
|
let iconFont: Font = .title2
|
||||||
|
let iconFrameWidth: CGFloat = 32
|
||||||
|
let titleFont: Font = .headline
|
||||||
|
#endif
|
||||||
return HStack(spacing: rowSpacing) {
|
return HStack(spacing: rowSpacing) {
|
||||||
Image(systemName: instance.type.systemImage)
|
Image(systemName: instance.type.systemImage)
|
||||||
.font(.title2)
|
.font(iconFont)
|
||||||
.foregroundStyle(.tint)
|
.foregroundStyle(.tint)
|
||||||
.frame(width: 32)
|
.frame(width: iconFrameWidth)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(instance.displayName)
|
Text(instance.displayName)
|
||||||
.font(.headline)
|
.font(titleFont)
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
Text("\(instance.type.displayName) - \(instance.url.host ?? instance.url.absoluteString)")
|
Text("\(instance.type.displayName) - \(instance.url.host ?? instance.url.absoluteString)")
|
||||||
@@ -478,10 +521,13 @@ struct MediaSourcesView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|
||||||
private func mediaSourceRow(_ source: MediaSource, needsPassword: Bool) -> some View {
|
private func mediaSourceRow(_ source: MediaSource, needsPassword: Bool) -> some View {
|
||||||
@@ -490,15 +536,24 @@ struct MediaSourcesView: View {
|
|||||||
#else
|
#else
|
||||||
let rowSpacing: CGFloat = 12
|
let rowSpacing: CGFloat = 12
|
||||||
#endif
|
#endif
|
||||||
|
#if os(macOS)
|
||||||
|
let iconFont: Font = .title3
|
||||||
|
let iconFrameWidth: CGFloat = 24
|
||||||
|
let titleFont: Font = .body
|
||||||
|
#else
|
||||||
|
let iconFont: Font = .title2
|
||||||
|
let iconFrameWidth: CGFloat = 32
|
||||||
|
let titleFont: Font = .headline
|
||||||
|
#endif
|
||||||
return HStack(spacing: rowSpacing) {
|
return HStack(spacing: rowSpacing) {
|
||||||
Image(systemName: source.type.systemImage)
|
Image(systemName: source.type.systemImage)
|
||||||
.font(.title2)
|
.font(iconFont)
|
||||||
.foregroundStyle(.tint)
|
.foregroundStyle(.tint)
|
||||||
.frame(width: 32)
|
.frame(width: iconFrameWidth)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(source.name)
|
Text(source.name)
|
||||||
.font(.headline)
|
.font(titleFont)
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
Text("\(source.type.displayName) - \(source.urlDisplayString)")
|
Text("\(source.type.displayName) - \(source.urlDisplayString)")
|
||||||
@@ -514,10 +569,13 @@ struct MediaSourcesView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,16 @@ struct SourcesListView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func sectionHeader(_ title: String) -> some View {
|
private func sectionHeader(_ title: String) -> some View {
|
||||||
|
#if os(macOS)
|
||||||
|
Text(title)
|
||||||
|
.font(.subheadline)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.top, 12)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
#else
|
||||||
Text(title)
|
Text(title)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -181,12 +191,23 @@ struct SourcesListView: View {
|
|||||||
.padding(.horizontal, listStyle == .inset ? 32 : 16)
|
.padding(.horizontal, listStyle == .inset ? 32 : 16)
|
||||||
.padding(.top, 16)
|
.padding(.top, 16)
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Section Card
|
// MARK: - Section Card
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func sectionCard<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
private func sectionCard<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
||||||
|
#if os(macOS)
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Divider()
|
||||||
|
LazyVStack(spacing: 0) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
}
|
||||||
|
.padding(.bottom, 12)
|
||||||
|
#else
|
||||||
if listStyle == .inset {
|
if listStyle == .inset {
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
content()
|
content()
|
||||||
@@ -205,6 +226,7 @@ struct SourcesListView: View {
|
|||||||
}
|
}
|
||||||
.padding(.bottom, 16)
|
.padding(.bottom, 16)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Remote Servers Section
|
// MARK: - Remote Servers Section
|
||||||
@@ -244,7 +266,11 @@ struct SourcesListView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
instanceRow(instance)
|
instanceRow(instance)
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
#else
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.swipeActions {
|
.swipeActions {
|
||||||
SwipeAction(symbolImage: "pencil", tint: .white, background: .orange) { reset in
|
SwipeAction(symbolImage: "pencil", tint: .white, background: .orange) { reset in
|
||||||
@@ -280,16 +306,25 @@ struct SourcesListView: View {
|
|||||||
#else
|
#else
|
||||||
let rowSpacing: CGFloat = 12
|
let rowSpacing: CGFloat = 12
|
||||||
#endif
|
#endif
|
||||||
|
#if os(macOS)
|
||||||
|
let iconFont: Font = .title3
|
||||||
|
let iconFrameWidth: CGFloat = 24
|
||||||
|
let titleFont: Font = .body
|
||||||
|
#else
|
||||||
|
let iconFont: Font = .title2
|
||||||
|
let iconFrameWidth: CGFloat = 32
|
||||||
|
let titleFont: Font = .headline
|
||||||
|
#endif
|
||||||
return HStack(spacing: rowSpacing) {
|
return HStack(spacing: rowSpacing) {
|
||||||
Image(systemName: instance.type.systemImage)
|
Image(systemName: instance.type.systemImage)
|
||||||
.font(.title2)
|
.font(iconFont)
|
||||||
.foregroundStyle(.tint)
|
.foregroundStyle(.tint)
|
||||||
.frame(width: 32)
|
.frame(width: iconFrameWidth)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(instance.displayName)
|
Text(instance.displayName)
|
||||||
.font(.headline)
|
.font(titleFont)
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
if !instance.isEnabled {
|
if !instance.isEnabled {
|
||||||
@@ -306,10 +341,13 @@ struct SourcesListView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.tertiary)
|
.foregroundStyle(.tertiary)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -380,7 +418,11 @@ struct SourcesListView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
mediaSourceRow(source, needsPassword: needsPassword)
|
mediaSourceRow(source, needsPassword: needsPassword)
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
#else
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.swipeActions {
|
.swipeActions {
|
||||||
SwipeAction(symbolImage: "pencil", tint: .white, background: .orange) { reset in
|
SwipeAction(symbolImage: "pencil", tint: .white, background: .orange) { reset in
|
||||||
@@ -416,16 +458,25 @@ struct SourcesListView: View {
|
|||||||
#else
|
#else
|
||||||
let rowSpacing: CGFloat = 12
|
let rowSpacing: CGFloat = 12
|
||||||
#endif
|
#endif
|
||||||
|
#if os(macOS)
|
||||||
|
let iconFont: Font = .title3
|
||||||
|
let iconFrameWidth: CGFloat = 24
|
||||||
|
let titleFont: Font = .body
|
||||||
|
#else
|
||||||
|
let iconFont: Font = .title2
|
||||||
|
let iconFrameWidth: CGFloat = 32
|
||||||
|
let titleFont: Font = .headline
|
||||||
|
#endif
|
||||||
return HStack(spacing: rowSpacing) {
|
return HStack(spacing: rowSpacing) {
|
||||||
Image(systemName: source.type.systemImage)
|
Image(systemName: source.type.systemImage)
|
||||||
.font(.title2)
|
.font(iconFont)
|
||||||
.foregroundStyle(.tint)
|
.foregroundStyle(.tint)
|
||||||
.frame(width: 32)
|
.frame(width: iconFrameWidth)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(source.name)
|
Text(source.name)
|
||||||
.font(.headline)
|
.font(titleFont)
|
||||||
.foregroundStyle(.primary)
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
if !source.isEnabled {
|
if !source.isEnabled {
|
||||||
@@ -446,10 +497,13 @@ struct SourcesListView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.tertiary)
|
.foregroundStyle(.tertiary)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Disabled Badge
|
// MARK: - Disabled Badge
|
||||||
|
|||||||
Reference in New Issue
Block a user