mirror of
https://github.com/yattee/yattee.git
synced 2026-08-05 23:01:28 +00:00
Fix #960: scope subscription counts, import/export to active account
When an Invidious/Piped account is active, several views read the local SwiftData subscription store instead of the account, so counts contradicted each other (Home tile said 10 while the Channels list showed the server's 2): - Home Channels tile now uses the provider's count via new SubscriptionService.cachedSubscriptionCount (fetched once in the background when the server cache isn't populated yet) - Export footer and export content use the active account's list; export runs async with a spinner and surfaces fetch errors as a toast - CSV/OPML import routes through SubscriptionService.importSubscriptions, subscribing on the server for server accounts instead of silently writing to the invisible local store - ChannelView subscribe-state is corrected via the provider after the optimistic local-store read; unused *Sync write helpers removed - Server-account subscribe/unsubscribe/import now post subscriptionsDidChange so other views refresh - New "Delete Local Subscription Data" section in Subscriptions settings (visible with a server account) clears the local store and queues CloudKit deletions so iCloud doesn't restore it
This commit is contained in:
@@ -114,6 +114,12 @@ final class SubscriptionService {
|
||||
settingsManager.subscriptionAccount.type
|
||||
}
|
||||
|
||||
/// Subscription count for the active account, when known without a network fetch.
|
||||
/// Always available for local accounts; nil for server accounts until their cache is populated.
|
||||
var cachedSubscriptionCount: Int? {
|
||||
currentProvider?.cachedSubscriptionCount
|
||||
}
|
||||
|
||||
// MARK: - Subscribe
|
||||
|
||||
/// Subscribes to a channel using the current provider.
|
||||
@@ -129,6 +135,7 @@ final class SubscriptionService {
|
||||
|
||||
do {
|
||||
try await provider.subscribe(to: channel)
|
||||
postChangeNotificationForServerAccount()
|
||||
LoggingService.shared.info(
|
||||
"Subscribed to \(channel.name) via \(provider.accountType)",
|
||||
category: .general
|
||||
@@ -174,6 +181,7 @@ final class SubscriptionService {
|
||||
|
||||
do {
|
||||
try await provider.unsubscribe(from: channelID)
|
||||
postChangeNotificationForServerAccount()
|
||||
LoggingService.shared.info(
|
||||
"Unsubscribed from \(channelID) via \(provider.accountType)",
|
||||
category: .general
|
||||
@@ -262,27 +270,53 @@ final class SubscriptionService {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Synchronous Helpers (for backwards compatibility)
|
||||
// MARK: - Import
|
||||
|
||||
/// Synchronously checks if subscribed to a channel.
|
||||
/// Uses cached data from DataManager for instant response.
|
||||
/// - Parameter channelID: The channel ID to check.
|
||||
/// - Returns: `true` if subscribed (based on local cache), `false` otherwise.
|
||||
func isSubscribedSync(to channelID: String) -> Bool {
|
||||
dataManager.isSubscribed(to: channelID)
|
||||
/// Imports parsed external subscriptions (YouTube CSV / OPML) into the active account.
|
||||
/// Local accounts write to SwiftData; server accounts subscribe on the server,
|
||||
/// one channel at a time. Channels that are already subscribed are skipped.
|
||||
func importSubscriptions(_ channels: [(channelID: String, name: String)]) async -> (imported: Int, skipped: Int) {
|
||||
guard currentAccountType != .local else {
|
||||
return dataManager.importSubscriptionsFromExternal(channels)
|
||||
}
|
||||
|
||||
guard let provider = currentProvider else { return (0, channels.count) }
|
||||
|
||||
// Populate the cache so already-subscribed channels can be skipped
|
||||
try? await provider.refreshCache()
|
||||
|
||||
var imported = 0
|
||||
var skipped = 0
|
||||
for entry in channels {
|
||||
if await provider.isSubscribed(to: entry.channelID) {
|
||||
skipped += 1
|
||||
continue
|
||||
}
|
||||
do {
|
||||
try await provider.subscribe(to: Channel(id: .global(entry.channelID), name: entry.name))
|
||||
imported += 1
|
||||
} catch {
|
||||
skipped += 1
|
||||
LoggingService.shared.error(
|
||||
"Failed to import subscription \(entry.channelID): \(error.localizedDescription)",
|
||||
category: .general
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if imported > 0 {
|
||||
postChangeNotificationForServerAccount()
|
||||
}
|
||||
|
||||
return (imported, skipped)
|
||||
}
|
||||
|
||||
/// Synchronously subscribes to a channel (local provider only).
|
||||
/// For Invidious provider, this will only update local cache.
|
||||
/// - Parameter channel: The channel to subscribe to.
|
||||
func subscribeSync(to channel: Channel) {
|
||||
dataManager.subscribe(to: channel)
|
||||
}
|
||||
// MARK: - Private Helpers
|
||||
|
||||
/// Synchronously unsubscribes from a channel (local provider only).
|
||||
/// For Invidious provider, this will only update local cache.
|
||||
/// - Parameter channelID: The channel ID to unsubscribe from.
|
||||
func unsubscribeSync(from channelID: String) {
|
||||
dataManager.unsubscribe(from: channelID)
|
||||
/// Posts a subscriptions-changed notification for server accounts.
|
||||
/// Local accounts already post it from DataManager with a change payload.
|
||||
private func postChangeNotificationForServerAccount() {
|
||||
guard currentAccountType != .local else { return }
|
||||
NotificationCenter.default.post(name: .subscriptionsDidChange, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,10 @@ final class InvidiousSubscriptionProvider: SubscriptionProvider {
|
||||
_ = try await fetchSubscriptions()
|
||||
}
|
||||
|
||||
var cachedSubscriptionCount: Int? {
|
||||
cachePopulated ? cachedChannels.count : nil
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
/// Gets the authenticated Invidious instance and session ID from account settings.
|
||||
|
||||
@@ -63,4 +63,8 @@ final class LocalSubscriptionProvider: SubscriptionProvider {
|
||||
func refreshCache() async throws {
|
||||
// Local provider doesn't need cache refresh - data is already local
|
||||
}
|
||||
|
||||
var cachedSubscriptionCount: Int? {
|
||||
dataManager.subscriptionCount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,10 @@ final class PipedSubscriptionProvider: SubscriptionProvider {
|
||||
_ = try await fetchSubscriptions()
|
||||
}
|
||||
|
||||
var cachedSubscriptionCount: Int? {
|
||||
cachePopulated ? cachedChannels.count : nil
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
/// Gets the authenticated Piped instance and auth token from account settings.
|
||||
|
||||
@@ -32,6 +32,10 @@ protocol SubscriptionProvider: Sendable {
|
||||
/// - Returns: `true` if subscribed, `false` otherwise.
|
||||
func isSubscribed(to channelID: String) async -> Bool
|
||||
|
||||
/// The subscription count known without a network fetch.
|
||||
/// Returns nil when the provider hasn't populated its cache yet.
|
||||
var cachedSubscriptionCount: Int? { get }
|
||||
|
||||
/// Refreshes the local cache of subscriptions from the remote source.
|
||||
/// For local provider, this is a no-op.
|
||||
func refreshCache() async throws
|
||||
|
||||
Reference in New Issue
Block a user