mirror of
https://github.com/yattee/yattee.git
synced 2026-05-13 10:55: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
|
// 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.
|
// "All Channels" entry and each subscribed channel.
|
||||||
//
|
//
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import NukeUI
|
import NukeUI
|
||||||
|
|
||||||
struct MacSubscriptionsSidebarRow: View {
|
struct SubscriptionsSidebarRow: View {
|
||||||
let name: String
|
let name: String
|
||||||
let avatarURL: URL?
|
let avatarURL: URL?
|
||||||
let serverURL: URL?
|
let serverURL: URL?
|
||||||
let authHeader: String?
|
let authHeader: String?
|
||||||
let channelID: String?
|
let channelID: String?
|
||||||
let isAllChannels: Bool
|
let isAllChannels: Bool
|
||||||
|
var isSelected: Bool = false
|
||||||
|
|
||||||
private let avatarSize: CGFloat = 28
|
private let avatarSize: CGFloat = 28
|
||||||
|
|
||||||
@@ -37,6 +37,7 @@ struct MacSubscriptionsSidebarRow: View {
|
|||||||
.clipShape(Circle())
|
.clipShape(Circle())
|
||||||
|
|
||||||
Text(name)
|
Text(name)
|
||||||
|
.fontWeight(isSelected ? .semibold : .regular)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
@@ -74,4 +75,3 @@ struct MacSubscriptionsSidebarRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
@@ -13,6 +13,9 @@ struct SubscriptionsView: View {
|
|||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
@FocusState private var focusedVideoID: String?
|
@FocusState private var focusedVideoID: String?
|
||||||
#endif
|
#endif
|
||||||
|
#if os(iOS)
|
||||||
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
|
#endif
|
||||||
@State private var feedCache = SubscriptionFeedCache.shared
|
@State private var feedCache = SubscriptionFeedCache.shared
|
||||||
@State private var subscriptions: [Subscription] = []
|
@State private var subscriptions: [Subscription] = []
|
||||||
@State private var subscriptionsLoaded = false
|
@State private var subscriptionsLoaded = false
|
||||||
@@ -239,13 +242,15 @@ struct SubscriptionsView: View {
|
|||||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: .infinity)
|
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
Group {
|
if isIPadRegular && subscriptionsLoaded && subscriptions.count > 1 {
|
||||||
switch layout {
|
HStack(spacing: 0) {
|
||||||
case .list:
|
iOSChannelsSidebar
|
||||||
listContent
|
.frame(width: 260)
|
||||||
case .grid:
|
|
||||||
gridContent
|
Divider()
|
||||||
}
|
|
||||||
|
feedLayout
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
guard let appEnvironment else { return }
|
guard let appEnvironment else { return }
|
||||||
@@ -254,14 +259,24 @@ struct SubscriptionsView: View {
|
|||||||
await feedCache.refresh(using: appEnvironment)
|
await feedCache.refresh(using: appEnvironment)
|
||||||
LoggingService.shared.info("Pull-to-refresh completed", category: .general)
|
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
|
#endif
|
||||||
|
|
||||||
// Bottom overlay for filter strip (iOS only)
|
// Bottom overlay for filter strip (iOS only, compact width)
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if subscriptionsLoaded && subscriptions.count > 1 && channelStripSize != .disabled && !isShowingFullScreenError {
|
if subscriptionsLoaded && subscriptions.count > 1 && channelStripSize != .disabled && !isShowingFullScreenError && !isIPadRegular {
|
||||||
bottomFloatingFilterStrip
|
bottomFloatingFilterStrip
|
||||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||||
}
|
}
|
||||||
@@ -270,7 +285,11 @@ struct SubscriptionsView: View {
|
|||||||
}
|
}
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.navigationTitle(String(localized: "tabs.subscriptions"))
|
.navigationTitle(String(localized: "tabs.subscriptions"))
|
||||||
|
#if os(iOS)
|
||||||
|
.toolbarTitleDisplayMode(isIPadRegular ? .inline : .inlineLarge)
|
||||||
|
#else
|
||||||
.toolbarTitleDisplayMode(.inlineLarge)
|
.toolbarTitleDisplayMode(.inlineLarge)
|
||||||
|
#endif
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
Button {
|
Button {
|
||||||
@@ -742,23 +761,23 @@ struct SubscriptionsView: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// MARK: - macOS Channels Sidebar
|
// MARK: - Channels Sidebar (macOS / iPad)
|
||||||
|
|
||||||
#if os(macOS)
|
#if !os(tvOS)
|
||||||
private static let macOSAllChannelsTag = "__all__"
|
private static let sidebarAllChannelsTag = "__all__"
|
||||||
|
|
||||||
private var macOSSidebarSelection: Binding<String?> {
|
private var sidebarSelection: Binding<String?> {
|
||||||
Binding(
|
Binding(
|
||||||
get: { selectedChannelID ?? Self.macOSAllChannelsTag },
|
get: { selectedChannelID ?? Self.sidebarAllChannelsTag },
|
||||||
set: { newValue in
|
set: { newValue in
|
||||||
selectedChannelID = (newValue == Self.macOSAllChannelsTag) ? nil : newValue
|
selectedChannelID = (newValue == Self.sidebarAllChannelsTag) ? nil : newValue
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var macOSChannelsSidebar: some View {
|
@ViewBuilder
|
||||||
List(selection: macOSSidebarSelection) {
|
private var channelsSidebarContent: some View {
|
||||||
MacSubscriptionsSidebarRow(
|
SubscriptionsSidebarRow(
|
||||||
name: String(localized: "subscriptions.allChannels"),
|
name: String(localized: "subscriptions.allChannels"),
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
serverURL: nil,
|
serverURL: nil,
|
||||||
@@ -766,7 +785,7 @@ struct SubscriptionsView: View {
|
|||||||
channelID: nil,
|
channelID: nil,
|
||||||
isAllChannels: true
|
isAllChannels: true
|
||||||
)
|
)
|
||||||
.tag(Self.macOSAllChannelsTag)
|
.tag(Self.sidebarAllChannelsTag)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button {
|
Button {
|
||||||
appEnvironment?.navigationCoordinator.navigate(to: .manageChannels)
|
appEnvironment?.navigationCoordinator.navigate(to: .manageChannels)
|
||||||
@@ -776,7 +795,7 @@ struct SubscriptionsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ForEach(sortedSubscriptions, id: \.channelID) { subscription in
|
ForEach(sortedSubscriptions, id: \.channelID) { subscription in
|
||||||
MacSubscriptionsSidebarRow(
|
SubscriptionsSidebarRow(
|
||||||
name: subscription.name,
|
name: subscription.name,
|
||||||
avatarURL: subscription.avatarURL,
|
avatarURL: subscription.avatarURL,
|
||||||
serverURL: yatteeServerURL,
|
serverURL: yatteeServerURL,
|
||||||
@@ -801,11 +820,126 @@ struct SubscriptionsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
private var macOSChannelsSidebar: some View {
|
||||||
|
List(selection: sidebarSelection) {
|
||||||
|
channelsSidebarContent
|
||||||
|
}
|
||||||
.listStyle(.sidebar)
|
.listStyle(.sidebar)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
}
|
}
|
||||||
#endif
|
#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
|
// MARK: - Content Views
|
||||||
|
|
||||||
private var subscriptionsQueueSource: QueueSource {
|
private var subscriptionsQueueSource: QueueSource {
|
||||||
|
|||||||
Reference in New Issue
Block a user