mirror of
https://github.com/yattee/yattee.git
synced 2026-05-13 02:45:03 +00:00
Add channels sidebar to Subscriptions on iPad regular width
On iPad in regular horizontal size class, the Subscriptions view now shows a channels column next to the feed (mirroring macOS/tvOS) instead of the floating channel strip. iPhone and compact-width iPad keep the existing strip. - Renames MacSubscriptionsSidebarRow to SubscriptionsSidebarRow and shares it across macOS and iOS. - Uses a custom ScrollView-based sidebar on iPad to avoid iOS 26's sidebar background extension bleeding into the selection highlight. - Forces inline toolbar title on iPad regular so scrolling either column behaves consistently.
This commit is contained in:
@@ -1,22 +1,22 @@
|
||||
//
|
||||
// MacSubscriptionsSidebarRow.swift
|
||||
// SubscriptionsSidebarRow.swift
|
||||
// Yattee
|
||||
//
|
||||
// Compact row used in the macOS Subscriptions sidebar for the
|
||||
// Compact row used in the Subscriptions sidebar (macOS and iPad) for the
|
||||
// "All Channels" entry and each subscribed channel.
|
||||
//
|
||||
|
||||
#if os(macOS)
|
||||
import SwiftUI
|
||||
import NukeUI
|
||||
|
||||
struct MacSubscriptionsSidebarRow: View {
|
||||
struct SubscriptionsSidebarRow: View {
|
||||
let name: String
|
||||
let avatarURL: URL?
|
||||
let serverURL: URL?
|
||||
let authHeader: String?
|
||||
let channelID: String?
|
||||
let isAllChannels: Bool
|
||||
var isSelected: Bool = false
|
||||
|
||||
private let avatarSize: CGFloat = 28
|
||||
|
||||
@@ -37,6 +37,7 @@ struct MacSubscriptionsSidebarRow: View {
|
||||
.clipShape(Circle())
|
||||
|
||||
Text(name)
|
||||
.fontWeight(isSelected ? .semibold : .regular)
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
@@ -74,4 +75,3 @@ struct MacSubscriptionsSidebarRow: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -13,6 +13,9 @@ struct SubscriptionsView: View {
|
||||
#if os(tvOS)
|
||||
@FocusState private var focusedVideoID: String?
|
||||
#endif
|
||||
#if os(iOS)
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
#endif
|
||||
@State private var feedCache = SubscriptionFeedCache.shared
|
||||
@State private var subscriptions: [Subscription] = []
|
||||
@State private var subscriptionsLoaded = false
|
||||
@@ -239,13 +242,15 @@ struct SubscriptionsView: View {
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
#else
|
||||
Group {
|
||||
switch layout {
|
||||
case .list:
|
||||
listContent
|
||||
case .grid:
|
||||
gridContent
|
||||
}
|
||||
if isIPadRegular && subscriptionsLoaded && subscriptions.count > 1 {
|
||||
HStack(spacing: 0) {
|
||||
iOSChannelsSidebar
|
||||
.frame(width: 260)
|
||||
|
||||
Divider()
|
||||
|
||||
feedLayout
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.refreshable {
|
||||
guard let appEnvironment else { return }
|
||||
@@ -254,14 +259,24 @@ struct SubscriptionsView: View {
|
||||
await feedCache.refresh(using: appEnvironment)
|
||||
LoggingService.shared.info("Pull-to-refresh completed", category: .general)
|
||||
}
|
||||
} else {
|
||||
feedLayout
|
||||
.refreshable {
|
||||
guard let appEnvironment else { return }
|
||||
LoggingService.shared.info("User initiated pull-to-refresh in Subscriptions view", category: .general)
|
||||
await loadSubscriptionsAsync()
|
||||
await feedCache.refresh(using: appEnvironment)
|
||||
LoggingService.shared.info("Pull-to-refresh completed", category: .general)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Bottom overlay for filter strip (iOS only)
|
||||
// Bottom overlay for filter strip (iOS only, compact width)
|
||||
#if os(iOS)
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
if subscriptionsLoaded && subscriptions.count > 1 && channelStripSize != .disabled && !isShowingFullScreenError {
|
||||
if subscriptionsLoaded && subscriptions.count > 1 && channelStripSize != .disabled && !isShowingFullScreenError && !isIPadRegular {
|
||||
bottomFloatingFilterStrip
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
}
|
||||
@@ -270,7 +285,11 @@ struct SubscriptionsView: View {
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.navigationTitle(String(localized: "tabs.subscriptions"))
|
||||
#if os(iOS)
|
||||
.toolbarTitleDisplayMode(isIPadRegular ? .inline : .inlineLarge)
|
||||
#else
|
||||
.toolbarTitleDisplayMode(.inlineLarge)
|
||||
#endif
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
@@ -742,23 +761,23 @@ struct SubscriptionsView: View {
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - macOS Channels Sidebar
|
||||
// MARK: - Channels Sidebar (macOS / iPad)
|
||||
|
||||
#if os(macOS)
|
||||
private static let macOSAllChannelsTag = "__all__"
|
||||
#if !os(tvOS)
|
||||
private static let sidebarAllChannelsTag = "__all__"
|
||||
|
||||
private var macOSSidebarSelection: Binding<String?> {
|
||||
private var sidebarSelection: Binding<String?> {
|
||||
Binding(
|
||||
get: { selectedChannelID ?? Self.macOSAllChannelsTag },
|
||||
get: { selectedChannelID ?? Self.sidebarAllChannelsTag },
|
||||
set: { newValue in
|
||||
selectedChannelID = (newValue == Self.macOSAllChannelsTag) ? nil : newValue
|
||||
selectedChannelID = (newValue == Self.sidebarAllChannelsTag) ? nil : newValue
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var macOSChannelsSidebar: some View {
|
||||
List(selection: macOSSidebarSelection) {
|
||||
MacSubscriptionsSidebarRow(
|
||||
@ViewBuilder
|
||||
private var channelsSidebarContent: some View {
|
||||
SubscriptionsSidebarRow(
|
||||
name: String(localized: "subscriptions.allChannels"),
|
||||
avatarURL: nil,
|
||||
serverURL: nil,
|
||||
@@ -766,7 +785,7 @@ struct SubscriptionsView: View {
|
||||
channelID: nil,
|
||||
isAllChannels: true
|
||||
)
|
||||
.tag(Self.macOSAllChannelsTag)
|
||||
.tag(Self.sidebarAllChannelsTag)
|
||||
.contextMenu {
|
||||
Button {
|
||||
appEnvironment?.navigationCoordinator.navigate(to: .manageChannels)
|
||||
@@ -776,7 +795,7 @@ struct SubscriptionsView: View {
|
||||
}
|
||||
|
||||
ForEach(sortedSubscriptions, id: \.channelID) { subscription in
|
||||
MacSubscriptionsSidebarRow(
|
||||
SubscriptionsSidebarRow(
|
||||
name: subscription.name,
|
||||
avatarURL: subscription.avatarURL,
|
||||
serverURL: yatteeServerURL,
|
||||
@@ -801,11 +820,126 @@ struct SubscriptionsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
private var macOSChannelsSidebar: some View {
|
||||
List(selection: sidebarSelection) {
|
||||
channelsSidebarContent
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
.scrollContentBackground(.hidden)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
private var isIPadRegular: Bool {
|
||||
UIDevice.current.userInterfaceIdiom == .pad && horizontalSizeClass == .regular
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var feedLayout: some View {
|
||||
Group {
|
||||
switch layout {
|
||||
case .list:
|
||||
listContent
|
||||
case .grid:
|
||||
gridContent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var iOSChannelsSidebar: some View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 2) {
|
||||
iOSSidebarRow(
|
||||
name: String(localized: "subscriptions.allChannels"),
|
||||
avatarURL: nil,
|
||||
serverURL: nil,
|
||||
authHeader: nil,
|
||||
channelID: nil,
|
||||
isAllChannels: true,
|
||||
isSelected: selectedChannelID == nil
|
||||
) {
|
||||
if selectedChannelID != nil { selectedChannelID = nil }
|
||||
}
|
||||
.contextMenu {
|
||||
Button {
|
||||
appEnvironment?.navigationCoordinator.navigate(to: .manageChannels)
|
||||
} label: {
|
||||
Label(String(localized: "sidebar.manageChannels"), systemImage: "person.2.badge.gearshape")
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(sortedSubscriptions, id: \.channelID) { subscription in
|
||||
iOSSidebarRow(
|
||||
name: subscription.name,
|
||||
avatarURL: subscription.avatarURL,
|
||||
serverURL: yatteeServerURL,
|
||||
authHeader: yatteeServerAuthHeader,
|
||||
channelID: subscription.channelID,
|
||||
isAllChannels: false,
|
||||
isSelected: selectedChannelID == subscription.channelID
|
||||
) {
|
||||
if selectedChannelID == subscription.channelID {
|
||||
selectedChannelID = nil
|
||||
} else {
|
||||
selectedChannelID = 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func iOSSidebarRow(
|
||||
name: String,
|
||||
avatarURL: URL?,
|
||||
serverURL: URL?,
|
||||
authHeader: String?,
|
||||
channelID: String?,
|
||||
isAllChannels: Bool,
|
||||
isSelected: Bool,
|
||||
action: @escaping () -> Void
|
||||
) -> some View {
|
||||
Button(action: action) {
|
||||
SubscriptionsSidebarRow(
|
||||
name: name,
|
||||
avatarURL: avatarURL,
|
||||
serverURL: serverURL,
|
||||
authHeader: authHeader,
|
||||
channelID: channelID,
|
||||
isAllChannels: isAllChannels,
|
||||
isSelected: isSelected
|
||||
)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
.fill(isSelected ? accentColor.opacity(0.2) : Color.clear)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Content Views
|
||||
|
||||
private var subscriptionsQueueSource: QueueSource {
|
||||
|
||||
Reference in New Issue
Block a user