mirror of
https://github.com/yattee/yattee.git
synced 2026-05-13 10:55:03 +00:00
Use resizable sidebar layout for Subscriptions on macOS
Replaces the iOS-style floating channel strip with a dedicated channels column next to the feed, using HSplitView for a native draggable divider. Mirrors the tvOS two-column structure.
This commit is contained in:
77
Yattee/Views/Subscriptions/MacSubscriptionsSidebarRow.swift
Normal file
77
Yattee/Views/Subscriptions/MacSubscriptionsSidebarRow.swift
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//
|
||||||
|
// MacSubscriptionsSidebarRow.swift
|
||||||
|
// Yattee
|
||||||
|
//
|
||||||
|
// Compact row used in the macOS Subscriptions sidebar for the
|
||||||
|
// "All Channels" entry and each subscribed channel.
|
||||||
|
//
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
import SwiftUI
|
||||||
|
import NukeUI
|
||||||
|
|
||||||
|
struct MacSubscriptionsSidebarRow: View {
|
||||||
|
let name: String
|
||||||
|
let avatarURL: URL?
|
||||||
|
let serverURL: URL?
|
||||||
|
let authHeader: String?
|
||||||
|
let channelID: String?
|
||||||
|
let isAllChannels: Bool
|
||||||
|
|
||||||
|
private let avatarSize: CGFloat = 28
|
||||||
|
|
||||||
|
private var effectiveAvatarURL: URL? {
|
||||||
|
guard let channelID else { return nil }
|
||||||
|
return AvatarURLBuilder.avatarURL(
|
||||||
|
channelID: channelID,
|
||||||
|
directURL: avatarURL,
|
||||||
|
serverURL: serverURL,
|
||||||
|
size: Int(avatarSize * 2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
avatar
|
||||||
|
.frame(width: avatarSize, height: avatarSize)
|
||||||
|
.clipShape(Circle())
|
||||||
|
|
||||||
|
Text(name)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var avatar: some View {
|
||||||
|
if isAllChannels {
|
||||||
|
ZStack {
|
||||||
|
Circle().fill(.quaternary)
|
||||||
|
Image(systemName: "rectangle.stack.fill")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.padding(avatarSize * 0.25)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyImage(request: AvatarURLBuilder.imageRequest(url: effectiveAvatarURL, authHeader: authHeader)) { state in
|
||||||
|
if let image = state.image {
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
} else {
|
||||||
|
Circle()
|
||||||
|
.fill(.quaternary)
|
||||||
|
.overlay {
|
||||||
|
Text(String(name.prefix(1)))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -221,6 +221,23 @@ struct SubscriptionsView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.top, 20)
|
.padding(.top, 20)
|
||||||
|
#elseif os(macOS)
|
||||||
|
HSplitView {
|
||||||
|
if subscriptionsLoaded && subscriptions.count > 1 {
|
||||||
|
macOSChannelsSidebar
|
||||||
|
.frame(minWidth: 180, idealWidth: 240, maxWidth: 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
Group {
|
||||||
|
switch layout {
|
||||||
|
case .list:
|
||||||
|
listContent
|
||||||
|
case .grid:
|
||||||
|
gridContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
Group {
|
Group {
|
||||||
switch layout {
|
switch layout {
|
||||||
@@ -239,8 +256,8 @@ struct SubscriptionsView: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Bottom overlay for filter strip
|
// Bottom overlay for filter strip (iOS only)
|
||||||
#if !os(tvOS)
|
#if os(iOS)
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -311,7 +328,7 @@ struct SubscriptionsView: View {
|
|||||||
|
|
||||||
Toggle("viewOptions.hideWatched", isOn: $hideWatched)
|
Toggle("viewOptions.hideWatched", isOn: $hideWatched)
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if os(iOS)
|
||||||
Picker("viewOptions.channelStrip", selection: $channelStripSize) {
|
Picker("viewOptions.channelStrip", selection: $channelStripSize) {
|
||||||
ForEach(ChannelStripSize.allCases, id: \.self) { size in
|
ForEach(ChannelStripSize.allCases, id: \.self) { size in
|
||||||
Text(size.displayName).tag(size)
|
Text(size.displayName).tag(size)
|
||||||
@@ -435,10 +452,14 @@ struct SubscriptionsView: View {
|
|||||||
} content: {
|
} content: {
|
||||||
feedContentRows
|
feedContentRows
|
||||||
} footer: {
|
} footer: {
|
||||||
|
#if os(iOS)
|
||||||
// Bottom spacer for channel strip overlay (outside the card)
|
// Bottom spacer for channel strip overlay (outside the card)
|
||||||
if channelStripSize != .disabled && subscriptions.count > 1 && !isShowingFullScreenError {
|
if channelStripSize != .disabled && subscriptions.count > 1 && !isShowingFullScreenError {
|
||||||
Color.clear.frame(height: channelStripSize.totalHeight)
|
Color.clear.frame(height: channelStripSize.totalHeight)
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
EmptyView()
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,6 +742,70 @@ struct SubscriptionsView: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// MARK: - macOS Channels Sidebar
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
private static let macOSAllChannelsTag = "__all__"
|
||||||
|
|
||||||
|
private var macOSSidebarSelection: Binding<String?> {
|
||||||
|
Binding(
|
||||||
|
get: { selectedChannelID ?? Self.macOSAllChannelsTag },
|
||||||
|
set: { newValue in
|
||||||
|
selectedChannelID = (newValue == Self.macOSAllChannelsTag) ? nil : newValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var macOSChannelsSidebar: some View {
|
||||||
|
List(selection: macOSSidebarSelection) {
|
||||||
|
MacSubscriptionsSidebarRow(
|
||||||
|
name: String(localized: "subscriptions.allChannels"),
|
||||||
|
avatarURL: nil,
|
||||||
|
serverURL: nil,
|
||||||
|
authHeader: nil,
|
||||||
|
channelID: nil,
|
||||||
|
isAllChannels: true
|
||||||
|
)
|
||||||
|
.tag(Self.macOSAllChannelsTag)
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
appEnvironment?.navigationCoordinator.navigate(to: .manageChannels)
|
||||||
|
} label: {
|
||||||
|
Label(String(localized: "sidebar.manageChannels"), systemImage: "person.2.badge.gearshape")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(sortedSubscriptions, id: \.channelID) { subscription in
|
||||||
|
MacSubscriptionsSidebarRow(
|
||||||
|
name: subscription.name,
|
||||||
|
avatarURL: subscription.avatarURL,
|
||||||
|
serverURL: yatteeServerURL,
|
||||||
|
authHeader: yatteeServerAuthHeader,
|
||||||
|
channelID: subscription.channelID,
|
||||||
|
isAllChannels: false
|
||||||
|
)
|
||||||
|
.tag(Optional(subscription.channelID))
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
appEnvironment?.navigationCoordinator.navigate(
|
||||||
|
to: .channel(subscription.channelID, subscription.contentSource)
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
Label(String(localized: "subscriptions.goToChannel"), systemImage: "person.circle")
|
||||||
|
}
|
||||||
|
Button(role: .destructive) {
|
||||||
|
unsubscribeChannel(subscription.channelID)
|
||||||
|
} label: {
|
||||||
|
Label(String(localized: "channel.unsubscribe"), systemImage: "person.badge.minus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.sidebar)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// MARK: - Content Views
|
// MARK: - Content Views
|
||||||
|
|
||||||
private var subscriptionsQueueSource: QueueSource {
|
private var subscriptionsQueueSource: QueueSource {
|
||||||
|
|||||||
Reference in New Issue
Block a user