mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 13:12:53 +00:00
Let the sidebar's channels and playlists follow the data context
This commit is contained in:
@@ -24,6 +24,11 @@ enum SidebarItem: Hashable, Identifiable {
|
||||
// MARK: - Dynamic Playlist Items
|
||||
case playlist(id: UUID, title: String)
|
||||
|
||||
/// A server-side playlist of the account the data context is on.
|
||||
/// `accountID` names the account it was listed for, which is what makes
|
||||
/// the detail view treat it as owned (editable).
|
||||
case remotePlaylist(id: PlaylistID, title: String, accountID: UUID)
|
||||
|
||||
// MARK: - Dynamic Media Source Items
|
||||
case mediaSource(id: UUID, name: String, type: MediaSourceType)
|
||||
|
||||
@@ -61,6 +66,8 @@ enum SidebarItem: Hashable, Identifiable {
|
||||
return "channel-\(source.provider)-\(channelID)"
|
||||
case .playlist(let id, _):
|
||||
return "playlist-\(id.uuidString)"
|
||||
case .remotePlaylist(let id, _, let accountID):
|
||||
return "remote-playlist-\(accountID.uuidString)-\(id.id)"
|
||||
case .mediaSource(let id, _, _):
|
||||
return "mediasource-\(id.uuidString)"
|
||||
case .instance(let id, _, _):
|
||||
@@ -104,6 +111,8 @@ enum SidebarItem: Hashable, Identifiable {
|
||||
return name
|
||||
case .playlist(_, let title):
|
||||
return title
|
||||
case .remotePlaylist(_, let title, _):
|
||||
return title
|
||||
case .mediaSource(_, let name, _):
|
||||
return name
|
||||
case .instance(_, let name, _):
|
||||
@@ -143,7 +152,7 @@ enum SidebarItem: Hashable, Identifiable {
|
||||
return "antenna.radiowaves.left.and.right"
|
||||
case .channel:
|
||||
return "person.circle"
|
||||
case .playlist:
|
||||
case .playlist, .remotePlaylist:
|
||||
return "list.bullet.rectangle"
|
||||
case .mediaSource(_, _, let type):
|
||||
return type.systemImage
|
||||
@@ -179,6 +188,9 @@ enum SidebarItem: Hashable, Identifiable {
|
||||
return .channel(channelID, source)
|
||||
case .playlist(let id, let title):
|
||||
return .playlist(.local(id, title: title))
|
||||
case .remotePlaylist(let id, let title, let accountID):
|
||||
// No instance here: the detail view resolves it from the account.
|
||||
return .playlist(.remote(id, instance: nil, title: title, accountID: accountID))
|
||||
case .mediaSource(let id, _, _):
|
||||
return .mediaSource(id)
|
||||
case .instance:
|
||||
@@ -221,8 +233,10 @@ enum SidebarItem: Hashable, Identifiable {
|
||||
|
||||
/// Whether this is a dynamic playlist item.
|
||||
var isPlaylist: Bool {
|
||||
if case .playlist = self { return true }
|
||||
return false
|
||||
switch self {
|
||||
case .playlist, .remotePlaylist: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this is a dynamic media source item.
|
||||
@@ -255,6 +269,20 @@ extension SidebarItem {
|
||||
.playlist(id: playlist.id, title: playlist.title)
|
||||
}
|
||||
|
||||
/// Creates a SidebarItem from a server account's Channel.
|
||||
static func from(channel: Channel) -> SidebarItem {
|
||||
.channel(
|
||||
channelID: channel.id.channelID,
|
||||
name: channel.name,
|
||||
source: channel.id.source
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a SidebarItem from a server account's Playlist.
|
||||
static func from(remotePlaylist playlist: Playlist, accountID: UUID) -> SidebarItem {
|
||||
.remotePlaylist(id: playlist.id, title: playlist.title, accountID: accountID)
|
||||
}
|
||||
|
||||
/// Creates a SidebarItem from a MediaSource.
|
||||
static func from(mediaSource: MediaSource) -> SidebarItem {
|
||||
.mediaSource(id: mediaSource.id, name: mediaSource.name, type: mediaSource.type)
|
||||
|
||||
@@ -14,15 +14,37 @@ import Combine
|
||||
final class SidebarManager {
|
||||
// MARK: - Published Items
|
||||
|
||||
/// Channel items for the Channels section.
|
||||
private(set) var channelItems: [SidebarItem] = []
|
||||
/// Channel items for the Channels section, following the data context the
|
||||
/// same way the Subscriptions screen does: local subscriptions under the
|
||||
/// local context, the account's server subscriptions under an account one.
|
||||
var channelItems: [SidebarItem] {
|
||||
if case .account = effectiveContext { return remoteChannelItems }
|
||||
return localChannelItems
|
||||
}
|
||||
|
||||
/// Playlist items for the Collections section.
|
||||
///
|
||||
/// Local playlists only, whatever data context is selected — the playlists
|
||||
/// screen follows the context but the sidebar does not. A known
|
||||
/// inconsistency, kept for now rather than half-solved.
|
||||
private(set) var playlistItems: [SidebarItem] = []
|
||||
/// Playlist items for the Playlists section, following the data context the
|
||||
/// same way the Playlists screen does. Remote lists are read live from the
|
||||
/// cache so a create/rename/delete made anywhere shows up here too.
|
||||
var playlistItems: [SidebarItem] {
|
||||
if case let .account(accountID) = effectiveContext {
|
||||
guard var playlists = remotePlaylistsCache?.playlists(for: accountID) else { return [] }
|
||||
if settingsManager?.sidebarPlaylistsLimitEnabled ?? false {
|
||||
let maxPlaylists = settingsManager?.sidebarMaxPlaylists ?? SettingsManager.defaultSidebarMaxPlaylists
|
||||
playlists = Array(playlists.prefix(maxPlaylists))
|
||||
}
|
||||
return playlists.map { SidebarItem.from(remotePlaylist: $0, accountID: accountID) }
|
||||
}
|
||||
return localPlaylistItems
|
||||
}
|
||||
|
||||
/// The local halves behind the computed sections above.
|
||||
private var localChannelItems: [SidebarItem] = []
|
||||
private var localPlaylistItems: [SidebarItem] = []
|
||||
|
||||
/// The current account's server subscriptions, rebuilt from the provider
|
||||
/// cache — providers are not observable, so this is notification-driven.
|
||||
private var remoteChannelItems: [SidebarItem] = []
|
||||
private var remoteChannelAvatars: [String: URL] = [:]
|
||||
|
||||
/// Media source items for the Media Sources section.
|
||||
private(set) var mediaSourceItems: [SidebarItem] = []
|
||||
@@ -45,7 +67,22 @@ final class SidebarManager {
|
||||
private weak var settingsManager: SettingsManager?
|
||||
private weak var mediaSourcesManager: MediaSourcesManager?
|
||||
private weak var instancesManager: InstancesManager?
|
||||
private weak var accountsManager: AccountsManager?
|
||||
private weak var subscriptionService: SubscriptionService?
|
||||
private weak var remotePlaylistsCache: RemotePlaylistsCache?
|
||||
private weak var contextValidator: DataContextValidator?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
/// The context the sections follow — the same fallback the data screens
|
||||
/// apply, so an unavailable account leaves the sidebar on local data too.
|
||||
private var effectiveContext: DataContext {
|
||||
contextValidator?.effectiveContext ?? accountsManager?.currentContext ?? .local
|
||||
}
|
||||
|
||||
/// One fetch per account per visit: a cold cache triggers a load, a failed
|
||||
/// one stays empty until the context is switched again.
|
||||
private var attemptedChannelFetches: Set<UUID> = []
|
||||
private var attemptedPlaylistFetches: Set<UUID> = []
|
||||
|
||||
// MARK: - Cached Data (to avoid repeated DB queries during layout)
|
||||
|
||||
@@ -63,12 +100,20 @@ final class SidebarManager {
|
||||
dataManager: DataManager,
|
||||
settingsManager: SettingsManager,
|
||||
mediaSourcesManager: MediaSourcesManager? = nil,
|
||||
instancesManager: InstancesManager? = nil
|
||||
instancesManager: InstancesManager? = nil,
|
||||
accountsManager: AccountsManager? = nil,
|
||||
subscriptionService: SubscriptionService? = nil,
|
||||
remotePlaylistsCache: RemotePlaylistsCache? = nil,
|
||||
contextValidator: DataContextValidator? = nil
|
||||
) {
|
||||
self.dataManager = dataManager
|
||||
self.settingsManager = settingsManager
|
||||
self.mediaSourcesManager = mediaSourcesManager
|
||||
self.instancesManager = instancesManager
|
||||
self.accountsManager = accountsManager
|
||||
self.subscriptionService = subscriptionService
|
||||
self.remotePlaylistsCache = remotePlaylistsCache
|
||||
self.contextValidator = contextValidator
|
||||
loadData()
|
||||
}
|
||||
|
||||
@@ -81,8 +126,14 @@ final class SidebarManager {
|
||||
loadSources()
|
||||
}
|
||||
|
||||
/// Loads channel items from subscriptions.
|
||||
/// Loads channel items from subscriptions — the local store always, plus
|
||||
/// the account provider's cache when an account context is selected.
|
||||
private func loadChannels() {
|
||||
loadLocalChannels()
|
||||
refreshRemoteChannels()
|
||||
}
|
||||
|
||||
private func loadLocalChannels() {
|
||||
guard let dataManager else { return }
|
||||
|
||||
// Cache subscriptions for use in avatarURL(for:)
|
||||
@@ -111,15 +162,84 @@ final class SidebarManager {
|
||||
|
||||
// Apply limit (if enabled) and convert to sidebar items
|
||||
if limitEnabled {
|
||||
channelItems = sortedSubscriptions
|
||||
localChannelItems = sortedSubscriptions
|
||||
.prefix(maxChannels)
|
||||
.map { SidebarItem.from(subscription: $0) }
|
||||
} else {
|
||||
channelItems = sortedSubscriptions
|
||||
localChannelItems = sortedSubscriptions
|
||||
.map { SidebarItem.from(subscription: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuilds the remote channel items from the current account provider's
|
||||
/// cache, fetching once when the cache is cold.
|
||||
private func refreshRemoteChannels() {
|
||||
guard case let .account(accountID) = effectiveContext,
|
||||
let subscriptionService,
|
||||
let provider = subscriptionService.provider(for: .account(accountID))
|
||||
else {
|
||||
remoteChannelItems = []
|
||||
remoteChannelAvatars = [:]
|
||||
return
|
||||
}
|
||||
|
||||
guard let channels = provider.cachedSubscriptions else {
|
||||
remoteChannelItems = []
|
||||
remoteChannelAvatars = [:]
|
||||
fetchRemoteChannelsOnce(accountID: accountID)
|
||||
return
|
||||
}
|
||||
|
||||
// Alphabetical regardless of the sidebar sort setting: server
|
||||
// subscriptions carry neither a subscribed-at nor a last-upload date.
|
||||
var sorted = channels.sorted {
|
||||
$0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending
|
||||
}
|
||||
|
||||
if settingsManager?.sidebarChannelsLimitEnabled ?? true {
|
||||
let maxChannels = settingsManager?.sidebarMaxChannels ?? SettingsManager.defaultSidebarMaxChannels
|
||||
sorted = Array(sorted.prefix(maxChannels))
|
||||
}
|
||||
|
||||
remoteChannelItems = sorted.map { SidebarItem.from(channel: $0) }
|
||||
remoteChannelAvatars = channels.reduce(into: [:]) { map, channel in
|
||||
map[channel.id.channelID] = channel.thumbnailURL
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchRemoteChannelsOnce(accountID: UUID) {
|
||||
guard let subscriptionService,
|
||||
!attemptedChannelFetches.contains(accountID) else { return }
|
||||
attemptedChannelFetches.insert(accountID)
|
||||
|
||||
Task { [weak self] in
|
||||
// The service guards the fetch with its own generation counter, so
|
||||
// a result landing after a context switch is discarded there.
|
||||
_ = try? await subscriptionService.fetchSubscriptions()
|
||||
guard let self, case .account(accountID) = self.effectiveContext else { return }
|
||||
self.refreshRemoteChannels()
|
||||
}
|
||||
}
|
||||
|
||||
/// Warms the remote playlists cache for the current account once; the
|
||||
/// computed `playlistItems` reads the cache live, so nothing to apply here.
|
||||
private func fetchRemotePlaylistsIfNeeded() {
|
||||
guard case let .account(accountID) = effectiveContext,
|
||||
let remotePlaylistsCache,
|
||||
remotePlaylistsCache.playlists(for: accountID) == nil,
|
||||
let accountsManager,
|
||||
let account = accountsManager.account(id: accountID),
|
||||
let instance = accountsManager.instance(for: account),
|
||||
instance.type.accountCapabilities.supportsPlaylistReads,
|
||||
!attemptedPlaylistFetches.contains(accountID)
|
||||
else { return }
|
||||
attemptedPlaylistFetches.insert(accountID)
|
||||
|
||||
Task {
|
||||
try? await remotePlaylistsCache.refreshList(for: account)
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads playlist items from local playlists.
|
||||
private func loadPlaylists() {
|
||||
guard let dataManager else { return }
|
||||
@@ -141,13 +261,15 @@ final class SidebarManager {
|
||||
|
||||
// Apply limit if enabled
|
||||
if limitEnabled {
|
||||
playlistItems = sortedPlaylists
|
||||
localPlaylistItems = sortedPlaylists
|
||||
.prefix(maxPlaylists)
|
||||
.map { SidebarItem.from(playlist: $0) }
|
||||
} else {
|
||||
playlistItems = sortedPlaylists
|
||||
localPlaylistItems = sortedPlaylists
|
||||
.map { SidebarItem.from(playlist: $0) }
|
||||
}
|
||||
|
||||
fetchRemotePlaylistsIfNeeded()
|
||||
}
|
||||
|
||||
/// Loads instance and media source items with sorting and limiting.
|
||||
@@ -258,6 +380,18 @@ final class SidebarManager {
|
||||
self?.loadData()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
NotificationCenter.default.publisher(for: .dataContextDidChange)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
// A fresh visit deserves a fresh attempt, even at a cache that
|
||||
// failed to fill last time.
|
||||
self?.attemptedChannelFetches.removeAll()
|
||||
self?.attemptedPlaylistFetches.removeAll()
|
||||
self?.refreshRemoteChannels()
|
||||
self?.fetchRemotePlaylistsIfNeeded()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Channel Data Access
|
||||
@@ -273,7 +407,12 @@ final class SidebarManager {
|
||||
func avatarURL(for item: SidebarItem) -> URL? {
|
||||
guard case .channel(let channelID, _, _) = item else { return nil }
|
||||
|
||||
let directURL = cachedSubscriptions.first { $0.channelID == channelID }?.avatarURL
|
||||
let directURL: URL?
|
||||
if case .account = effectiveContext {
|
||||
directURL = remoteChannelAvatars[channelID]
|
||||
} else {
|
||||
directURL = cachedSubscriptions.first { $0.channelID == channelID }?.avatarURL
|
||||
}
|
||||
|
||||
return AvatarURLBuilder.avatarURL(
|
||||
channelID: channelID,
|
||||
@@ -288,16 +427,29 @@ final class SidebarManager {
|
||||
/// Returns video count for a playlist sidebar item.
|
||||
/// Uses cached playlists to avoid repeated DB queries during layout.
|
||||
func videoCount(for item: SidebarItem) -> Int {
|
||||
guard case .playlist(let id, _) = item else { return 0 }
|
||||
|
||||
return cachedPlaylists.first { $0.id == id }?.videoCount ?? 0
|
||||
switch item {
|
||||
case .playlist(let id, _):
|
||||
return cachedPlaylists.first { $0.id == id }?.videoCount ?? 0
|
||||
case .remotePlaylist(let id, _, let accountID):
|
||||
return remotePlaylistsCache?.playlists(for: accountID)?
|
||||
.first { $0.id == id }?.videoCount ?? 0
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns thumbnail URL for a playlist sidebar item.
|
||||
/// Uses cached playlists to avoid repeated DB queries during layout.
|
||||
func thumbnailURL(for item: SidebarItem) -> URL? {
|
||||
guard case .playlist(let id, _) = item else { return nil }
|
||||
return cachedPlaylists.first { $0.id == id }?.thumbnailURL
|
||||
switch item {
|
||||
case .playlist(let id, _):
|
||||
return cachedPlaylists.first { $0.id == id }?.thumbnailURL
|
||||
case .remotePlaylist(let id, _, let accountID):
|
||||
return remotePlaylistsCache?.playlists(for: accountID)?
|
||||
.first { $0.id == id }?.thumbnailURL
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,10 @@ final class InvidiousSubscriptionProvider: SubscriptionProvider {
|
||||
cachePopulated ? cachedChannels.count : nil
|
||||
}
|
||||
|
||||
var cachedSubscriptions: [Channel]? {
|
||||
cachePopulated ? cachedChannels : nil
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
/// Gets the authenticated Invidious instance and session ID from account settings.
|
||||
|
||||
@@ -26,7 +26,12 @@ final class LocalSubscriptionProvider: SubscriptionProvider {
|
||||
// MARK: - SubscriptionProvider
|
||||
|
||||
func fetchSubscriptions() async throws -> [Channel] {
|
||||
// Convert Subscription models to Channel models
|
||||
subscriptionChannels()
|
||||
}
|
||||
|
||||
/// Converts Subscription models to Channel models, synchronously — the
|
||||
/// store is local, so there is nothing to await.
|
||||
private func subscriptionChannels() -> [Channel] {
|
||||
dataManager.subscriptions().map { subscription in
|
||||
Channel(
|
||||
id: ChannelID(source: subscription.contentSource, channelID: subscription.channelID),
|
||||
@@ -67,4 +72,9 @@ final class LocalSubscriptionProvider: SubscriptionProvider {
|
||||
var cachedSubscriptionCount: Int? {
|
||||
dataManager.subscriptionCount
|
||||
}
|
||||
|
||||
var cachedSubscriptions: [Channel]? {
|
||||
// Local data needs no cache warm-up — the store is always readable.
|
||||
subscriptionChannels()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,10 @@ final class PipedSubscriptionProvider: SubscriptionProvider {
|
||||
cachePopulated ? cachedChannels.count : nil
|
||||
}
|
||||
|
||||
var cachedSubscriptions: [Channel]? {
|
||||
cachePopulated ? cachedChannels : nil
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
/// Gets the authenticated Piped instance and auth token from account settings.
|
||||
|
||||
@@ -40,6 +40,10 @@ protocol SubscriptionProvider: Sendable {
|
||||
/// Returns nil when the provider hasn't populated its cache yet.
|
||||
var cachedSubscriptionCount: Int? { get }
|
||||
|
||||
/// The subscribed channels known without a network fetch.
|
||||
/// Returns nil when the provider hasn't populated its cache yet.
|
||||
var cachedSubscriptions: [Channel]? { get }
|
||||
|
||||
/// Refreshes the local cache of subscriptions from the remote source.
|
||||
/// For local provider, this is a no-op.
|
||||
func refreshCache() async throws
|
||||
|
||||
@@ -26,6 +26,7 @@ struct UnifiedTabView: View {
|
||||
@State private var searchPath = NavigationPath()
|
||||
@State private var channelPaths: [String: NavigationPath] = [:]
|
||||
@State private var playlistPaths: [UUID: NavigationPath] = [:]
|
||||
@State private var remotePlaylistPaths: [String: NavigationPath] = [:]
|
||||
@State private var mediaSourcePaths: [UUID: NavigationPath] = [:]
|
||||
@State private var instancePaths: [UUID: NavigationPath] = [:]
|
||||
@State private var bookmarksPath = NavigationPath()
|
||||
@@ -355,6 +356,7 @@ struct UnifiedTabView: View {
|
||||
@State private var searchPath = NavigationPath()
|
||||
@State private var channelPaths: [String: NavigationPath] = [:]
|
||||
@State private var playlistPaths: [UUID: NavigationPath] = [:]
|
||||
@State private var remotePlaylistPaths: [String: NavigationPath] = [:]
|
||||
@State private var mediaSourcePaths: [UUID: NavigationPath] = [:]
|
||||
@State private var instancePaths: [UUID: NavigationPath] = [:]
|
||||
@State private var bookmarksPath = NavigationPath()
|
||||
@@ -549,7 +551,7 @@ struct UnifiedTabView: View {
|
||||
}
|
||||
case .channel:
|
||||
channelContent(for: selection)
|
||||
case .playlist:
|
||||
case .playlist, .remotePlaylist:
|
||||
playlistContent(for: selection)
|
||||
case .mediaSource, .instance:
|
||||
sourceContent(for: selection)
|
||||
@@ -583,6 +585,7 @@ struct UnifiedTabView: View {
|
||||
@State private var searchPath = NavigationPath()
|
||||
@State private var channelPaths: [String: NavigationPath] = [:]
|
||||
@State private var playlistPaths: [UUID: NavigationPath] = [:]
|
||||
@State private var remotePlaylistPaths: [String: NavigationPath] = [:]
|
||||
@State private var instancePaths: [UUID: NavigationPath] = [:]
|
||||
@State private var mediaSourcePaths: [UUID: NavigationPath] = [:]
|
||||
@State private var bookmarksPath = NavigationPath()
|
||||
@@ -675,6 +678,8 @@ struct UnifiedTabView: View {
|
||||
channelPaths[channelID] = NavigationPath()
|
||||
case let .playlist(id, _):
|
||||
playlistPaths[id] = NavigationPath()
|
||||
case .remotePlaylist:
|
||||
remotePlaylistPaths[item.id] = NavigationPath()
|
||||
case let .instance(id, _, _):
|
||||
instancePaths[id] = NavigationPath()
|
||||
case let .mediaSource(id, _, _):
|
||||
@@ -934,7 +939,11 @@ extension UnifiedTabView {
|
||||
dataManager: appEnvironment.dataManager,
|
||||
settingsManager: appEnvironment.settingsManager,
|
||||
mediaSourcesManager: appEnvironment.mediaSourcesManager,
|
||||
instancesManager: appEnvironment.instancesManager
|
||||
instancesManager: appEnvironment.instancesManager,
|
||||
accountsManager: appEnvironment.accountsManager,
|
||||
subscriptionService: appEnvironment.subscriptionService,
|
||||
remotePlaylistsCache: appEnvironment.remotePlaylistsCache,
|
||||
contextValidator: appEnvironment.dataContextValidator
|
||||
)
|
||||
}
|
||||
|
||||
@@ -951,6 +960,8 @@ extension UnifiedTabView {
|
||||
channelPaths[selection.id, default: NavigationPath()].append(destination)
|
||||
case .playlist(let id, _):
|
||||
playlistPaths[id, default: NavigationPath()].append(destination)
|
||||
case .remotePlaylist:
|
||||
remotePlaylistPaths[selection.id, default: NavigationPath()].append(destination)
|
||||
case .mediaSource(let id, _, _):
|
||||
mediaSourcePaths[id, default: NavigationPath()].append(destination)
|
||||
case .instance(let id, _, _):
|
||||
@@ -1047,20 +1058,38 @@ extension UnifiedTabView {
|
||||
|
||||
@ViewBuilder
|
||||
func playlistContent(for item: SidebarItem) -> some View {
|
||||
if case .playlist(let id, _) = item {
|
||||
switch item {
|
||||
case .playlist(let id, _):
|
||||
NavigationStack(path: playlistPathBinding(for: id)) {
|
||||
UnifiedPlaylistDetailView(source: .local(id))
|
||||
.withNavigationDestinations()
|
||||
}
|
||||
.id(item.id)
|
||||
case .remotePlaylist(let id, let title, let accountID):
|
||||
NavigationStack(path: remotePlaylistPathBinding(for: item)) {
|
||||
// No instance passed: the detail view resolves it from the
|
||||
// account, same as SidebarItem.navigationDestination().
|
||||
UnifiedPlaylistDetailView(source: .remote(id, instance: nil, title: title, accountID: accountID))
|
||||
.withNavigationDestinations()
|
||||
}
|
||||
.id(item.id)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
func remotePlaylistPathBinding(for item: SidebarItem) -> Binding<NavigationPath> {
|
||||
Binding(
|
||||
get: { remotePlaylistPaths[item.id] ?? NavigationPath() },
|
||||
set: { remotePlaylistPaths[item.id] = $0 }
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func playlistLabel(for item: SidebarItem) -> some View {
|
||||
if case .playlist(_, let title) = item {
|
||||
if item.isPlaylist {
|
||||
Label {
|
||||
Text(title)
|
||||
Text(item.title)
|
||||
} icon: {
|
||||
SidebarPlaylistIcon(url: sidebarManager.thumbnailURL(for: item))
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@ private final class ScriptedSubscriptionProvider: SubscriptionProvider {
|
||||
}
|
||||
|
||||
var cachedSubscriptionCount: Int? { subscribed.count }
|
||||
|
||||
var cachedSubscriptions: [Channel]? {
|
||||
subscribed.map { Channel(id: .global($0), name: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Hands out the scripted providers instead of building ones that would need a
|
||||
|
||||
Reference in New Issue
Block a user