Files
yattee/Yattee/Services/Subscriptions/SubscriptionProvider.swift
Arkadiusz Fal cd01c08b5a 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
2026-08-03 23:34:06 +02:00

84 lines
2.9 KiB
Swift

//
// SubscriptionProvider.swift
// Yattee
//
// Protocol defining subscription management operations.
// Implementations handle different subscription sources (local, Invidious, Piped).
//
import Foundation
/// Protocol for subscription management providers.
/// Each provider implementation handles a specific subscription source.
@MainActor
protocol SubscriptionProvider: Sendable {
/// The type of subscription account this provider handles.
var accountType: SubscriptionAccountType { get }
/// Fetches all subscriptions from the provider.
/// - Returns: Array of channels the user is subscribed to.
func fetchSubscriptions() async throws -> [Channel]
/// Subscribes to a channel.
/// - Parameter channel: The channel to subscribe to.
func subscribe(to channel: Channel) async throws
/// Unsubscribes from a channel.
/// - Parameter channelID: The channel ID to unsubscribe from.
func unsubscribe(from channelID: String) async throws
/// Checks if subscribed to a channel.
/// - Parameter channelID: The channel ID to check.
/// - 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
}
// MARK: - Default Implementations
extension SubscriptionProvider {
/// Default implementation for providers that don't need cache refresh.
func refreshCache() async throws {
// No-op by default
}
}
// MARK: - Subscription Provider Error
/// Errors that can occur during subscription provider operations.
enum SubscriptionProviderError: Error, LocalizedError, Equatable, Sendable {
case notAuthenticated
case networkError(String)
case channelNotFound
case alreadySubscribed
case notSubscribed
case instanceNotConfigured
case operationFailed(String)
var errorDescription: String? {
switch self {
case .notAuthenticated:
return String(localized: "subscription.error.notAuthenticated")
case .networkError(let message):
return String(localized: "subscription.error.network \(message)")
case .channelNotFound:
return String(localized: "subscription.error.channelNotFound")
case .alreadySubscribed:
return String(localized: "subscription.error.alreadySubscribed")
case .notSubscribed:
return String(localized: "subscription.error.notSubscribed")
case .instanceNotConfigured:
return String(localized: "subscription.error.instanceNotConfigured")
case .operationFailed(let message):
return String(localized: "subscription.error.operationFailed \(message)")
}
}
}