From 4770bbe3b908884d4b2cfb382e26b84d3d084846 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 8 Oct 2026 23:57:56 +0200 Subject: [PATCH] Remove dead code found by Periphery on all platforms Delete declarations that Periphery reports unused on every platform that compiles them (iOS, macOS, tvOS), including nine views used only by their own previews. Platform-gated code still used on another platform, device- only MPV render paths, migration models, CloudKit record names, App Intents metadata and unused parameters are kept. --- Yattee/Core/AccountsManager.swift | 5 - Yattee/Core/AppEnvironment.swift | 4 - Yattee/Core/HomeInstanceDiskCache.swift | 9 - Yattee/Core/InstancesManager.swift | 94 ---- Yattee/Core/MediaSourcesManager.swift | 64 --- .../Settings/SettingsManager+General.swift | 5 - .../Core/Settings/SettingsManager+Home.swift | 38 -- .../Settings/SettingsManager+Player.swift | 11 - Yattee/Core/Settings/SettingsTypes.swift | 5 - Yattee/Data/Bookmark.swift | 20 - ...aManager+ChannelNotificationSettings.swift | 35 -- Yattee/Data/DataManager+Subscriptions.swift | 125 ------ Yattee/Data/DataManagerNotifications.swift | 4 - Yattee/Data/WatchEntry.swift | 14 - Yattee/Extensions/View+Conditional.swift | 13 - Yattee/Extensions/View+LiquidGlassSheet.swift | 42 -- Yattee/Extensions/View+ZoomTransition.swift | 15 - Yattee/Models/Comment.swift | 2 - Yattee/Models/MediaSources/MediaFile.swift | 4 - Yattee/Models/Navigation/SidebarItem.swift | 16 - Yattee/Models/Player/VideoQueueContext.swift | 6 - .../ControlButtonConfiguration.swift | 10 - .../PlayerControls/ControlButtonType.swift | 5 - .../PlayerControls/GlobalLayoutSettings.swift | 12 - .../Models/PlayerControls/LayoutSection.swift | 32 -- Yattee/Models/Storyboard.swift | 9 - Yattee/Models/VideoListLayout.swift | 6 - Yattee/Services/API/ContentService.swift | 46 +- Yattee/Services/API/GitHubAPI.swift | 5 - Yattee/Services/API/InstanceAPI.swift | 1 - Yattee/Services/API/InstanceDetector.swift | 24 -- Yattee/Services/API/InvidiousAPI.swift | 27 +- Yattee/Services/API/PipedAPI.swift | 13 +- Yattee/Services/API/ProxyDetectionCache.swift | 15 - Yattee/Services/API/YatteeServerAPI.swift | 26 -- .../BackgroundRefreshManager.swift | 9 - .../CloudKit/CloudKitSyncEngine.swift | 15 - .../CloudKit/CloudKitZoneManager.swift | 5 - Yattee/Services/CloudKit/RecordTypes.swift | 42 -- .../AccountCredentialsManager.swift | 9 - .../BasicAuthCredentialsManager.swift | 10 - .../InstanceCredentialsManager.swift | 9 - .../InvidiousCredentialsManager.swift | 11 - .../Credentials/PipedCredentialsManager.swift | 1 - Yattee/Services/DeviceRotationManager.swift | 5 - .../Services/Downloads/DownloadManager.swift | 48 --- .../ImageLoading/ImageLoadingService.swift | 14 - Yattee/Services/Logging/LoggingService.swift | 5 - .../MediaSources/LocalFileClient.swift | 17 - .../NetworkShareDiscoveryService.swift | 8 - .../SMBBridge/SMBBridgeWrapper.swift | 18 - Yattee/Services/MediaSources/SMBClient.swift | 17 - .../Services/MediaSources/WebDAVClient.swift | 2 - .../MediaSources/WebDAVClientFactory.swift | 8 - .../Navigation/NavigationCoordinator.swift | 20 - .../Services/Navigation/SidebarManager.swift | 16 +- .../Networking/HTTPClientFactory.swift | 6 - Yattee/Services/Player/BackendSwitcher.swift | 408 ------------------ Yattee/Services/Player/DeArrowAPI.swift | 25 -- Yattee/Services/Player/MPV/MPVClient.swift | 251 ----------- Yattee/Services/Player/MPV/MPVOGLView.swift | 11 - .../Services/Player/MPV/MPVOpenGLLayer.swift | 6 +- Yattee/Services/Player/MPV/MPVPiPBridge.swift | 50 --- .../Services/Player/MPV/MPVRenderView.swift | 6 - .../Player/MPV/MPVSoftwareRenderView.swift | 2 - Yattee/Services/Player/MPVBackend.swift | 110 ----- .../Services/Player/NowPlayingService.swift | 1 - Yattee/Services/Player/PlayerBackend.swift | 98 ----- Yattee/Services/Player/PlayerService.swift | 135 ------ Yattee/Services/Player/PlayerState.swift | 12 - Yattee/Services/Player/QueueManager.swift | 10 - .../Player/ReturnYouTubeDislikeAPI.swift | 5 - .../PlayerControlsBackupService.swift | 28 -- .../PlayerControlsLayoutService.swift | 48 --- .../RemoteControlCoordinator.swift | 45 -- Yattee/Services/RemotePlaylistsCache.swift | 5 - Yattee/Services/SubscriptionService.swift | 19 - .../Subscriptions/DataContextValidator.swift | 2 - .../InvidiousSubscriptionProvider.swift | 3 - .../LocalSubscriptionProvider.swift | 1 - .../PipedSubscriptionProvider.swift | 3 - .../Subscriptions/SubscriptionProvider.swift | 7 - Yattee/Services/ToastManager.swift | 80 ---- Yattee/ViewModels/OpenedLinksViewModel.swift | 7 - .../PlayerControlsSettingsViewModel.swift | 176 -------- Yattee/Views/Channel/ChannelView.swift | 22 - Yattee/Views/Components/BookmarkRowView.swift | 6 - .../Components/ExpandedCommentsView.swift | 55 --- Yattee/Views/Components/LoadStateView.swift | 81 ---- Yattee/Views/Components/ShareSheet.swift | 30 -- .../Components/SourceListContainer.swift | 144 ------- .../Components/SwipeActionModifier.swift | 1 - .../Components/TextTruncationEffect.swift | 236 ---------- .../Views/Components/VideoListContainer.swift | 16 - .../Components/VideoNavigationButton.swift | 77 ---- .../VideoSwipeActionsModifier.swift | 1 - .../Views/Components/ViewOptionsSheet.swift | 1 - Yattee/Views/Downloads/DownloadsView.swift | 12 - Yattee/Views/Home/OpenLinkSheet.swift | 2 - .../Views/Instances/InstanceBrowseView.swift | 6 - .../Views/MediaBrowser/MediaBrowserView.swift | 2 - .../Navigation/SidebarAdaptableModifier.swift | 25 -- .../Views/Navigation/SidebarChannelRow.swift | 69 --- .../Views/Navigation/SidebarPlaylistRow.swift | 63 --- .../Player/ExpandedPlayerSheet+Layouts.swift | 104 ----- .../ExpandedPlayerSheet+VideoInfo.swift | 17 - Yattee/Views/Player/ExpandedPlayerSheet.swift | 22 - .../Views/Player/ExpandedPlayerWindow.swift | 24 -- .../Player/ExpandedPlayerWindowManager.swift | 24 -- .../Gestures/GestureSeekPreviewView.swift | 7 +- .../Gestures/TapGestureFeedbackView.swift | 2 - .../Player/Gestures/TapZoneCalculator.swift | 83 ---- Yattee/Views/Player/MiniPlayerView.swift | 1 - .../Panels/PlayerPanelContentView.swift | 14 +- .../Views/Player/Panels/QueuePanelPage.swift | 1 - .../Views/Player/PlayerControlsActions.swift | 7 - Yattee/Views/Player/PlayerControlsView.swift | 13 +- Yattee/Views/Player/PlayerHelperViews.swift | 1 - .../Views/Player/PortraitDetailsPanel.swift | 2 - .../QualitySelectorTypes.swift | 12 - .../QualitySelectorView+StreamHelpers.swift | 73 ---- .../Views/Player/QueueManagementSheet.swift | 3 - Yattee/Views/Player/SeekPreviewView.swift | 1 - .../Views/Player/SegmentedProgressBar.swift | 4 - .../Views/Player/macOS/MacOSControlBar.swift | 8 +- Yattee/Views/Player/tvOS/TVDetailsPanel.swift | 8 +- .../Player/tvOS/TVPlayerProgressBar.swift | 10 +- .../Views/Playlist/PlaylistVideoRowView.swift | 12 - .../Playlist/TappablePlaylistVideoRow.swift | 52 --- .../RemoteControl/RemoteControlView.swift | 5 - Yattee/Views/Search/SearchView.swift | 17 - Yattee/Views/Settings/EditSourceView.swift | 7 - .../PlayerControls/MiniPlayerEditorView.swift | 4 +- .../PlayerControlsPreviewView.swift | 1 - .../Settings/RemoteControlSettingsView.swift | 27 -- Yattee/Views/Settings/SourceRow.swift | 161 ------- .../Subscriptions/ManageChannelsView.swift | 1 - YatteeTests/AccountSyncTests.swift | 4 - YatteeTests/AccountsScreenTests.swift | 2 - YatteeTests/Integration/TestConstants.swift | 6 - YatteeTests/LocalNamespaceImportTests.swift | 5 - 141 files changed, 14 insertions(+), 4191 deletions(-) delete mode 100644 Yattee/Views/Components/SourceListContainer.swift delete mode 100644 Yattee/Views/Components/TextTruncationEffect.swift delete mode 100644 Yattee/Views/Components/VideoNavigationButton.swift delete mode 100644 Yattee/Views/Navigation/SidebarAdaptableModifier.swift delete mode 100644 Yattee/Views/Navigation/SidebarChannelRow.swift delete mode 100644 Yattee/Views/Navigation/SidebarPlaylistRow.swift delete mode 100644 Yattee/Views/Playlist/TappablePlaylistVideoRow.swift delete mode 100644 Yattee/Views/Settings/RemoteControlSettingsView.swift delete mode 100644 Yattee/Views/Settings/SourceRow.swift diff --git a/Yattee/Core/AccountsManager.swift b/Yattee/Core/AccountsManager.swift index f8fea004..a6950097 100644 --- a/Yattee/Core/AccountsManager.swift +++ b/Yattee/Core/AccountsManager.swift @@ -161,11 +161,6 @@ final class AccountsManager { } } - func setInstancesManager(_ manager: InstancesManager) { - instancesManager = manager - reconcileLegacyAccounts() - } - // MARK: - Lookup func account(id: UUID) -> Account? { diff --git a/Yattee/Core/AppEnvironment.swift b/Yattee/Core/AppEnvironment.swift index 83c12130..22f95eae 100644 --- a/Yattee/Core/AppEnvironment.swift +++ b/Yattee/Core/AppEnvironment.swift @@ -36,7 +36,6 @@ final class AppEnvironment { let webDAVClientFactory: WebDAVClientFactory let smbClient: SMBClient let localFileClient: LocalFileClient - let urlSessionFactory: URLSessionFactory let httpClientFactory: HTTPClientFactory let localNetworkService: LocalNetworkService let remoteControlCoordinator: RemoteControlCoordinator @@ -57,7 +56,6 @@ final class AppEnvironment { let localNamespaceImporter: LocalNamespaceImporter let homeInstanceCache: HomeInstanceCache let invidiousAPI: InvidiousAPI - let pipedAPI: PipedAPI let credentialProbeService: CredentialProbeService let accountService: AccountService let remotePlaylistsCache: RemotePlaylistsCache @@ -262,7 +260,6 @@ final class AppEnvironment { // Initialize Piped API (used by SubscriptionService and SubscriptionFeedCache) let pipedAPI = PipedAPI(httpClient: client) - self.pipedAPI = pipedAPI // A 401 or 403 from an ordinary request is never allowed to reject a // login on its own — it is re-asked here against an endpoint that @@ -399,7 +396,6 @@ final class AppEnvironment { // Initialize URL Session and Client Factories let sessionFactory = URLSessionFactory.shared - self.urlSessionFactory = sessionFactory self.httpClientFactory = HTTPClientFactory(sessionFactory: sessionFactory) self.webDAVClientFactory = WebDAVClientFactory(sessionFactory: sessionFactory) diff --git a/Yattee/Core/HomeInstanceDiskCache.swift b/Yattee/Core/HomeInstanceDiskCache.swift index 5313c73a..9d8f750e 100644 --- a/Yattee/Core/HomeInstanceDiskCache.swift +++ b/Yattee/Core/HomeInstanceDiskCache.swift @@ -99,15 +99,6 @@ actor HomeInstanceDiskCache { } } } - - /// Clears the cache from both memory and disk. - func clear() async { - cachedData = nil - try? fileManager.removeItem(at: cacheFileURL) - await MainActor.run { - LoggingService.shared.debug("HomeInstanceDiskCache.clear: Cache cleared", category: .general) - } - } } // MARK: - Cache Data Model diff --git a/Yattee/Core/InstancesManager.swift b/Yattee/Core/InstancesManager.swift index 66fef417..d41a55c5 100644 --- a/Yattee/Core/InstancesManager.swift +++ b/Yattee/Core/InstancesManager.swift @@ -12,8 +12,6 @@ import SwiftUI enum InstanceStatus: Equatable { /// Instance is online and working. case online - /// Instance is offline or unreachable. - case offline /// Instance requires authentication but credentials are not provided. case authRequired /// Instance authentication failed (wrong credentials). @@ -172,11 +170,6 @@ final class InstancesManager { } } - /// Alias for add method to maintain consistency. - func addInstance(_ instance: Instance) { - add(instance) - } - /// Toggles the enabled state of an instance. func toggleEnabled(_ instance: Instance) { if let index = instances.firstIndex(where: { $0.id == instance.id }) { @@ -211,39 +204,12 @@ final class InstancesManager { saveInstances() } - /// Sets the given instance as the primary (first) instance. - func setPrimary(_ instance: Instance) { - LoggingService.shared.debug("[InstancesManager] setPrimary called for: \(instance.displayName)", category: .general) - LoggingService.shared.debug("[InstancesManager] Current instances: \(instances.map { $0.displayName })", category: .general) - - guard let index = instances.firstIndex(where: { $0.id == instance.id }) else { - LoggingService.shared.debug("[InstancesManager] Instance not found in list", category: .general) - return - } - - if index == 0 { - LoggingService.shared.debug("[InstancesManager] Instance already at index 0, skipping", category: .general) - return - } - - LoggingService.shared.debug("[InstancesManager] Moving instance from index \(index) to 0", category: .general) - // Move to front - let removed = instances.remove(at: index) - instances.insert(removed, at: 0) - saveInstances() - LoggingService.shared.debug("[InstancesManager] After move: \(instances.map { $0.displayName })", category: .general) - } - // MARK: - Computed Properties var enabledInstances: [Instance] { instances.filter(\.isEnabled) } - var youtubeInstances: [Instance] { - instances.filter(\.isYouTubeInstance) - } - var peertubeInstances: [Instance] { instances.filter(\.isPeerTubeInstance) } @@ -360,42 +326,6 @@ final class InstancesManager { instanceStatuses[instance.id] ?? .online } - /// Updates the status of an instance. - func updateStatus(_ status: InstanceStatus, for instance: Instance) { - instanceStatuses[instance.id] = status - } - - /// Updates status based on an API error. - func updateStatusFromError(_ error: Error, for instance: Instance) { - if let apiError = error as? APIError { - switch apiError { - case .unauthorized: - updateStatus(.authFailed, for: instance) - case .noConnection, .timeout, .connectionFailed: - updateStatus(.offline, for: instance) - default: - // Don't change status for other errors - break - } - } else { - // Generic network errors - let nsError = error as NSError - if nsError.domain == NSURLErrorDomain { - updateStatus(.offline, for: instance) - } - } - } - - /// Clears the status of an instance (resets to online). - func clearStatus(for instance: Instance) { - instanceStatuses.removeValue(forKey: instance.id) - } - - /// Instances that have auth issues (need attention). - var instancesWithAuthIssues: [Instance] { - instances.filter { instanceStatuses[$0.id] == .authFailed || instanceStatuses[$0.id] == .authRequired } - } - /// The currently active instance for browsing content. /// Falls back to the first enabled instance if no active instance is set. var activeInstance: Instance? { @@ -406,21 +336,6 @@ final class InstancesManager { return enabledInstances.first } - /// Sets the given instance as the active instance for browsing. - func setActive(_ instance: Instance) { - guard enabledInstances.contains(where: { $0.id == instance.id }) else { return } - activeInstanceID = instance.id - saveActiveInstance() - NotificationCenter.default.post(name: .activeInstanceDidChange, object: nil) - } - - /// Clears the active instance, falling back to the first enabled instance. - func clearActiveInstance() { - activeInstanceID = nil - localDefaults.removeObject(forKey: activeInstanceKey) - NotificationCenter.default.post(name: .activeInstanceDidChange, object: nil) - } - // MARK: - Private Methods private func loadInstances() { @@ -453,19 +368,10 @@ final class InstancesManager { activeInstanceID = uuid } } - - private func saveActiveInstance() { - if let id = activeInstanceID { - localDefaults.set(id.uuidString, forKey: activeInstanceKey) - } else { - localDefaults.removeObject(forKey: activeInstanceKey) - } - } } // MARK: - Notifications extension Notification.Name { static let instancesDidChange = Notification.Name("stream.yattee.instancesDidChange") - static let activeInstanceDidChange = Notification.Name("stream.yattee.activeInstanceDidChange") } diff --git a/Yattee/Core/MediaSourcesManager.swift b/Yattee/Core/MediaSourcesManager.swift index 8d29653a..0aae5bb9 100644 --- a/Yattee/Core/MediaSourcesManager.swift +++ b/Yattee/Core/MediaSourcesManager.swift @@ -89,11 +89,6 @@ final class MediaSourcesManager { } } - /// Sets the settings manager reference (for dependency injection after init). - func configure(settingsManager: SettingsManager) { - self.settingsManager = settingsManager - } - /// Sets the data manager reference (for cleanup when sources are deleted). func setDataManager(_ manager: DataManager) { self.dataManager = manager @@ -344,51 +339,6 @@ final class MediaSourcesManager { passwordStoredSourceIDs.remove(source.id) } - // MARK: - Bookmarks (Local Folders) - - /// Updates the bookmark data for a local folder source. - func updateBookmark(_ bookmarkData: Data, for source: MediaSource) { - if let index = sources.firstIndex(where: { $0.id == source.id }) { - var updated = sources[index] - updated.bookmarkData = bookmarkData - sources[index] = updated - saveSources() - } - } - - /// Resolves and accesses a local folder source. - /// - Parameter source: The local folder source. - /// - Returns: The resolved URL, or nil if bookmark resolution failed. - func resolveLocalFolderURL(for source: MediaSource) -> URL? { - guard source.type == .localFolder, - let bookmarkData = source.bookmarkData else { - return source.url - } - - var isStale = false - - #if os(macOS) - let options: URL.BookmarkResolutionOptions = [.withSecurityScope] - #else - let options: URL.BookmarkResolutionOptions = [] - #endif - - do { - let url = try URL( - resolvingBookmarkData: bookmarkData, - options: options, - relativeTo: nil, - bookmarkDataIsStale: &isStale - ) - - // If bookmark is stale, we should re-create it - // but we can't do that without user interaction - return url - } catch { - return nil - } - } - // MARK: - iCloud Sync /// Whether iCloud sync is enabled and media sources sync is enabled. @@ -501,17 +451,3 @@ final class MediaSourcesManager { ubiquitousStore.synchronize() } } - -// MARK: - Preview Support - -extension MediaSourcesManager { - /// Preview manager with sample data. - static var preview: MediaSourcesManager { - let manager = MediaSourcesManager() - manager.sources = [ - .webdav(name: "My NAS", url: URL(string: "https://nas.local:5006")!, username: "user"), - .localFolder(name: "Downloads", url: URL(fileURLWithPath: "/Users/user/Downloads")) - ] - return manager - } -} diff --git a/Yattee/Core/Settings/SettingsManager+General.swift b/Yattee/Core/Settings/SettingsManager+General.swift index 8580ea85..234ae339 100644 --- a/Yattee/Core/Settings/SettingsManager+General.swift +++ b/Yattee/Core/Settings/SettingsManager+General.swift @@ -264,11 +264,6 @@ extension SettingsManager { // MARK: - Custom User-Agent - /// Generates a new random User-Agent string. - static func generateRandomUserAgent() -> String { - UserAgentGenerator.generateRandom() - } - /// Returns the current effective User-Agent string. /// If randomize per request is enabled, generates a new random UA. /// Otherwise, returns the stored custom user agent. diff --git a/Yattee/Core/Settings/SettingsManager+Home.swift b/Yattee/Core/Settings/SettingsManager+Home.swift index 51fd628c..d59bd206 100644 --- a/Yattee/Core/Settings/SettingsManager+Home.swift +++ b/Yattee/Core/Settings/SettingsManager+Home.swift @@ -337,25 +337,6 @@ extension SettingsManager { homeSectionVisibility = sectionVis } - /// Checks if an instance content item is in Home (either as card or section). - func isInHome(instanceID: UUID, contentType: InstanceContentType) -> (inCards: Bool, inSections: Bool) { - let inCards = homeShortcutOrder.contains { item in - if case .instanceContent(let id, let type) = item { - return id == instanceID && type == contentType - } - return false - } - - let inSections = homeSectionOrder.contains { item in - if case .instanceContent(let id, let type) = item { - return id == instanceID && type == contentType - } - return false - } - - return (inCards, inSections) - } - /// Removes all Home items for instances that no longer exist. func cleanupOrphanedHomeInstanceItems(validInstanceIDs: Set) { // Collect orphaned instance IDs for cache cleanup @@ -560,25 +541,6 @@ extension SettingsManager { homeSectionVisibility = sectionVis } - /// Checks if a media source is in Home (either as card or section). - func isInHome(sourceID: UUID) -> (inCards: Bool, inSections: Bool) { - let inCards = homeShortcutOrder.contains { item in - if case .mediaSource(let id) = item { - return id == sourceID - } - return false - } - - let inSections = homeSectionOrder.contains { item in - if case .mediaSource(let id) = item { - return id == sourceID - } - return false - } - - return (inCards, inSections) - } - /// Returns all available card items for a media source that are NOT already added. func availableShortcuts(for source: MediaSource) -> [HomeShortcutItem] { let card = HomeShortcutItem.mediaSource(sourceID: source.id) diff --git a/Yattee/Core/Settings/SettingsManager+Player.swift b/Yattee/Core/Settings/SettingsManager+Player.swift index 30023df9..45184960 100644 --- a/Yattee/Core/Settings/SettingsManager+Player.swift +++ b/Yattee/Core/Settings/SettingsManager+Player.swift @@ -13,17 +13,6 @@ import UIKit extension SettingsManager { // MARK: - Player Settings - var keepPlayerPinnedEnabled: Bool { - get { - if let cached = _keepPlayerPinnedEnabled { return cached } - return bool(for: .keepPlayerPinned, default: false) - } - set { - _keepPlayerPinnedEnabled = newValue - set(newValue, for: .keepPlayerPinned) - } - } - #if os(iOS) /// Whether in-app orientation lock is enabled. /// When enabled, ignores accelerometer rotation detection and stays in current orientation. diff --git a/Yattee/Core/Settings/SettingsTypes.swift b/Yattee/Core/Settings/SettingsTypes.swift index d405417a..c2916700 100644 --- a/Yattee/Core/Settings/SettingsTypes.swift +++ b/Yattee/Core/Settings/SettingsTypes.swift @@ -603,11 +603,6 @@ enum VideoSwipeAction: String, CaseIterable, Codable, Identifiable { } } - /// Default order with only download and share enabled. - static var defaultOrder: [VideoSwipeAction] { - [.download, .share] - } - /// Default visibility: only download and share are enabled by default. static var defaultVisibility: [VideoSwipeAction: Bool] { var visibility = [VideoSwipeAction: Bool]() diff --git a/Yattee/Data/Bookmark.swift b/Yattee/Data/Bookmark.swift index ee6d78ba..2d5aee16 100644 --- a/Yattee/Data/Bookmark.swift +++ b/Yattee/Data/Bookmark.swift @@ -172,26 +172,6 @@ final class Bookmark { } } - /// Formatted view count string. - var formattedViewCount: String? { - guard let viewCount else { return nil } - return CountFormatter.compact(viewCount) - } - - /// Formatted published date, preferring parsed Date over API-provided text. - /// - /// A date that would render as "in 0 seconds" is shown as nothing at all — - /// see ``Video/formattedPublishedDate``. - var formattedPublishedDate: String? { - if let publishedAt { - guard !RelativeDateFormatter.isEffectivelyNow(publishedAt) else { return nil } - let formatter = RelativeDateTimeFormatter() - formatter.unitsStyle = .full - return formatter.localizedString(for: publishedAt, relativeTo: Date()) - } - return publishedText - } - /// Converts this bookmark to a Video model for playback. func toVideo() -> Video { let videoIDObj: VideoID diff --git a/Yattee/Data/DataManager+ChannelNotificationSettings.swift b/Yattee/Data/DataManager+ChannelNotificationSettings.swift index 36fb546f..803e76de 100644 --- a/Yattee/Data/DataManager+ChannelNotificationSettings.swift +++ b/Yattee/Data/DataManager+ChannelNotificationSettings.swift @@ -131,41 +131,6 @@ extension DataManager { save() } - /// Deletes notification settings for a channel. - /// - Parameter channelID: The channel ID to delete settings for. - func deleteNotificationSettings(for channelID: String) { - let descriptor = FetchDescriptor( - predicate: #Predicate { $0.channelID == channelID } - ) - - do { - let results = try modelContext.fetch(descriptor) - guard !results.isEmpty else { return } - - // Capture scopes before deleting - let scopes = results.map { - SourceScope.from( - sourceRawValue: $0.sourceRawValue, - globalProvider: $0.globalProvider, - instanceURLString: $0.instanceURLString, - externalExtractor: nil - ) - } - - for settings in results { - modelContext.delete(settings) - } - save() - - // Queue scoped CloudKit deletions - for scope in scopes { - cloudKitSync?.queueChannelNotificationSettingsDelete(channelID: channelID, scope: scope) - } - } catch { - LoggingService.shared.logCloudKitError("Failed to delete notification settings", error: error) - } - } - /// Inserts or updates notification settings from CloudKit sync. /// - Parameter settings: The notification settings to upsert. func upsertChannelNotificationSettings(_ settings: ChannelNotificationSettings) { diff --git a/Yattee/Data/DataManager+Subscriptions.swift b/Yattee/Data/DataManager+Subscriptions.swift index e66a1236..587afda0 100644 --- a/Yattee/Data/DataManager+Subscriptions.swift +++ b/Yattee/Data/DataManager+Subscriptions.swift @@ -43,61 +43,6 @@ extension DataManager { } } - /// Subscribes to a channel from an Author. - /// - Parameters: - /// - author: The author/channel to subscribe to. - /// - source: The content source (YouTube or PeerTube). - func subscribe(to author: Author, source: ContentSource) { - let channelID = author.id - let descriptor = FetchDescriptor( - predicate: #Predicate { $0.channelID == channelID } - ) - - do { - let existing = try modelContext.fetch(descriptor) - guard existing.isEmpty else { - return - } - - let sourceRaw: String - var instanceURL: String? - - switch source { - case .global: - sourceRaw = "global" - case .federated(_, let instance): - sourceRaw = "federated" - instanceURL = instance.absoluteString - case .extracted: - // Extracted sources don't support subscriptions - return - } - - let subscription = Subscription( - channelID: channelID, - sourceRawValue: sourceRaw, - instanceURLString: instanceURL, - name: author.name, - subscriberCount: author.subscriberCount, - avatarURLString: author.thumbnailURL?.absoluteString - ) - modelContext.insert(subscription) - save() - - // Queue for CloudKit sync - cloudKitSync?.queueSubscriptionSave(subscription) - - let change = SubscriptionChange(addedSubscriptions: [subscription], removedChannelIDs: []) - NotificationCenter.default.post( - name: .subscriptionsDidChange, - object: nil, - userInfo: [SubscriptionChange.userInfoKey: change] - ) - } catch { - LoggingService.shared.logCloudKitError("Failed to subscribe", error: error) - } - } - /// Unsubscribes from a channel. func unsubscribe(from channelID: String) { let descriptor = FetchDescriptor( @@ -137,42 +82,6 @@ extension DataManager { } } - /// Bulk adds subscriptions from channel data (for testing). - func bulkAddSubscriptions(_ channels: [(id: String, name: String)]) { - var addedCount = 0 - - for channel in channels { - let channelID = channel.id - let descriptor = FetchDescriptor( - predicate: #Predicate { $0.channelID == channelID } - ) - - do { - let existing = try modelContext.fetch(descriptor) - guard existing.isEmpty else { - continue - } - - let subscription = Subscription( - channelID: channel.id, - sourceRawValue: "youtube", - instanceURLString: nil, - name: channel.name - ) - modelContext.insert(subscription) - addedCount += 1 - } catch { - continue - } - } - - if addedCount > 0 { - save() - SubscriptionFeedCache.shared.invalidate() - LoggingService.shared.info("Bulk added \(addedCount) subscriptions", category: .general) - } - } - /// Checks if subscribed to a channel. func isSubscribed(to channelID: String) -> Bool { let descriptor = FetchDescriptor( @@ -257,40 +166,6 @@ extension DataManager { // Note: caller is responsible for calling save() after batch operations } - /// Removes subscriptions matching the given channel IDs. - func removeSubscriptions(matching channelIDs: Set) { - let allSubscriptions = subscriptions() - var removedCount = 0 - var deleteInfo: [(channelID: String, scope: SourceScope)] = [] - - for subscription in allSubscriptions { - if channelIDs.contains(subscription.channelID) { - // Capture scope before deleting - let scope = SourceScope.from( - sourceRawValue: subscription.sourceRawValue, - globalProvider: subscription.providerName, - instanceURLString: subscription.instanceURLString, - externalExtractor: nil - ) - deleteInfo.append((subscription.channelID, scope)) - modelContext.delete(subscription) - removedCount += 1 - } - } - - if removedCount > 0 { - save() - - // Queue CloudKit deletions - for info in deleteInfo { - cloudKitSync?.queueSubscriptionDelete(channelID: info.channelID, scope: info.scope) - } - - SubscriptionFeedCache.shared.invalidate() - LoggingService.shared.info("Removed \(removedCount) test subscriptions", category: .general) - } - } - /// Deletes all locally stored subscriptions, including their iCloud copies. /// Server-account subscriptions (Invidious/Piped) are unaffected. func deleteAllSubscriptions() { diff --git a/Yattee/Data/DataManagerNotifications.swift b/Yattee/Data/DataManagerNotifications.swift index dbf74a44..ea8be07a 100644 --- a/Yattee/Data/DataManagerNotifications.swift +++ b/Yattee/Data/DataManagerNotifications.swift @@ -40,8 +40,4 @@ struct SubscriptionChange { let removedChannelIDs: [String] static let userInfoKey = "subscriptionChange" - - var isEmpty: Bool { - addedSubscriptions.isEmpty && removedChannelIDs.isEmpty - } } diff --git a/Yattee/Data/WatchEntry.swift b/Yattee/Data/WatchEntry.swift index f34eceeb..173b707e 100644 --- a/Yattee/Data/WatchEntry.swift +++ b/Yattee/Data/WatchEntry.swift @@ -149,20 +149,6 @@ final class WatchEntry { return min(watchedSeconds / duration, 1.0) } - /// Formatted total duration. - var formattedDuration: String { - guard duration > 0 else { return "" } - let hours = Int(duration) / 3600 - let minutes = (Int(duration) % 3600) / 60 - let seconds = Int(duration) % 60 - - if hours > 0 { - return String(format: "%d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%d:%02d", minutes, seconds) - } - } - /// Formatted remaining time. var remainingTime: String { let remaining = max(0, duration - watchedSeconds) diff --git a/Yattee/Extensions/View+Conditional.swift b/Yattee/Extensions/View+Conditional.swift index a1d2ed0a..7ae30663 100644 --- a/Yattee/Extensions/View+Conditional.swift +++ b/Yattee/Extensions/View+Conditional.swift @@ -34,17 +34,4 @@ extension View { falseTransform(self) } } - - /// Conditionally applies a transformation when an optional value is present. - @ViewBuilder - func ifLet( - _ value: T?, - transform: (Self, T) -> Transform - ) -> some View { - if let value { - transform(self, value) - } else { - self - } - } } diff --git a/Yattee/Extensions/View+LiquidGlassSheet.swift b/Yattee/Extensions/View+LiquidGlassSheet.swift index 27b86667..62305236 100644 --- a/Yattee/Extensions/View+LiquidGlassSheet.swift +++ b/Yattee/Extensions/View+LiquidGlassSheet.swift @@ -106,45 +106,3 @@ extension View { modifier(LiquidGlassSheetContentModifier(sourceID: sourceID, namespace: namespace)) } } - -// MARK: - ToolbarContent Extension - -extension ToolbarContent { - /// Marks this toolbar content as the source for a Liquid Glass morphing sheet transition. - /// - /// Apply this modifier to a `ToolbarItem` that presents a sheet. On iOS 26+, - /// the sheet will morph from this toolbar item when presented. - /// - /// - Parameters: - /// - id: Unique identifier for the transition (must match the sheet content's sourceID). - /// - namespace: A namespace for the matched geometry effect. - /// - /// Example: - /// ```swift - /// @Namespace private var sheetTransition - /// - /// .toolbar { - /// ToolbarItem(placement: .primaryAction) { - /// Button { showSheet = true } label: { - /// Image(systemName: "gear") - /// } - /// } - /// .liquidGlassTransitionSource(id: "settings", in: sheetTransition) - /// } - /// ``` - @ToolbarContentBuilder - func liquidGlassTransitionSource( - id: String, - in namespace: Namespace.ID - ) -> some ToolbarContent { - #if os(iOS) - if #available(iOS 26, *) { - self.matchedTransitionSource(id: id, in: namespace) - } else { - self - } - #else - self - #endif - } -} diff --git a/Yattee/Extensions/View+ZoomTransition.swift b/Yattee/Extensions/View+ZoomTransition.swift index 5a65e91a..05fdf448 100644 --- a/Yattee/Extensions/View+ZoomTransition.swift +++ b/Yattee/Extensions/View+ZoomTransition.swift @@ -159,21 +159,6 @@ extension View { environment(\.zoomTransitionNamespace, namespace) } - /// Injects the zoom transition namespace into the environment (optional overload). - /// - /// If the namespace is nil, the view is returned unchanged. - /// - /// - Parameter namespace: The optional namespace to use for matched transitions. - /// - Returns: A view with the namespace injected into the environment, or unchanged if nil. - @ViewBuilder - func zoomTransitionNamespace(_ namespace: Namespace.ID?) -> some View { - if let namespace { - environment(\.zoomTransitionNamespace, namespace) - } else { - self - } - } - /// Sets whether zoom transitions are enabled. /// /// Apply this at a high level in the view hierarchy (e.g., ContentView) to control diff --git a/Yattee/Models/Comment.swift b/Yattee/Models/Comment.swift index d17b7f8e..5dce7d12 100644 --- a/Yattee/Models/Comment.swift +++ b/Yattee/Models/Comment.swift @@ -94,6 +94,4 @@ struct CommentsPage: Sendable { /// Continuation token for loading more comments. Nil if no more pages. let continuation: String? - /// Whether there are more comments to load. - var hasMore: Bool { continuation != nil } } diff --git a/Yattee/Models/MediaSources/MediaFile.swift b/Yattee/Models/MediaSources/MediaFile.swift index 12f87efa..b4ce66cf 100644 --- a/Yattee/Models/MediaSources/MediaFile.swift +++ b/Yattee/Models/MediaSources/MediaFile.swift @@ -36,9 +36,6 @@ struct MediaFile: Identifiable, Hashable, Sendable { /// Creation date. let createdDate: Date? - /// MIME type if known. - let mimeType: String? - // MARK: - Initialization init( @@ -60,7 +57,6 @@ struct MediaFile: Identifiable, Hashable, Sendable { self.size = size self.modifiedDate = modifiedDate self.createdDate = createdDate - self.mimeType = mimeType } // MARK: - Computed Properties diff --git a/Yattee/Models/Navigation/SidebarItem.swift b/Yattee/Models/Navigation/SidebarItem.swift index 11c21df1..7d9c4ed5 100644 --- a/Yattee/Models/Navigation/SidebarItem.swift +++ b/Yattee/Models/Navigation/SidebarItem.swift @@ -215,22 +215,6 @@ enum SidebarItem: Hashable, Identifiable { // MARK: - Item Categories - /// Whether this is a fixed navigation item (always visible). - var isFixedNavigation: Bool { - switch self { - case .home, .search, .sources, .settings, .nowPlaying, .openURL, .remoteControl: - return true - default: - return false - } - } - - /// Whether this is a dynamic channel item. - var isChannel: Bool { - if case .channel = self { return true } - return false - } - /// Whether this is a dynamic playlist item. var isPlaylist: Bool { switch self { diff --git a/Yattee/Models/Player/VideoQueueContext.swift b/Yattee/Models/Player/VideoQueueContext.swift index f52c2648..b6d1b7fc 100644 --- a/Yattee/Models/Player/VideoQueueContext.swift +++ b/Yattee/Models/Player/VideoQueueContext.swift @@ -57,12 +57,6 @@ struct VideoQueueContext { videoList != nil && videoIndex != nil } - /// Number of videos that will be queued after the current one - var remainingVideosCount: Int { - guard let list = videoList, let index = videoIndex else { return 0 } - return max(0, list.count - index - 1) - } - /// Whether more videos can be loaded via continuation var canLoadMore: Bool { let hasCallback = loadMoreVideos != nil diff --git a/Yattee/Models/PlayerControls/ControlButtonConfiguration.swift b/Yattee/Models/PlayerControls/ControlButtonConfiguration.swift index b8d71002..a69ae354 100644 --- a/Yattee/Models/PlayerControls/ControlButtonConfiguration.swift +++ b/Yattee/Models/PlayerControls/ControlButtonConfiguration.swift @@ -59,16 +59,6 @@ struct ControlButtonConfiguration: Identifiable, Codable, Hashable, Sendable { ) } - /// Creates a configuration for a fixed-width spacer. - /// - Parameter width: The fixed width in points. - /// - Returns: A spacer configuration with fixed width. - static func fixedSpacer(width: Int) -> ControlButtonConfiguration { - ControlButtonConfiguration( - buttonType: .spacer, - settings: .spacer(SpacerSettings(isFlexible: false, fixedWidth: width)) - ) - } - // MARK: - Convenience Accessors /// Returns the spacer settings if this is a spacer button. diff --git a/Yattee/Models/PlayerControls/ControlButtonType.swift b/Yattee/Models/PlayerControls/ControlButtonType.swift index 72cbf948..cf123e8a 100644 --- a/Yattee/Models/PlayerControls/ControlButtonType.swift +++ b/Yattee/Models/PlayerControls/ControlButtonType.swift @@ -294,11 +294,6 @@ enum ControlButtonType: String, Codable, Hashable, Sendable, CaseIterable { #endif } - /// Button types for center section (play/pause, seek). - static var availableForCenterSection: [ControlButtonType] { - [.playPause, .seekBackward, .seekForward] - } - /// Button types available for the player pill (curated subset). static var availableForPill: [ControlButtonType] { [ diff --git a/Yattee/Models/PlayerControls/GlobalLayoutSettings.swift b/Yattee/Models/PlayerControls/GlobalLayoutSettings.swift index bdb2017d..d8a980ae 100644 --- a/Yattee/Models/PlayerControls/GlobalLayoutSettings.swift +++ b/Yattee/Models/PlayerControls/GlobalLayoutSettings.swift @@ -294,18 +294,6 @@ extension SponsorBlockSegmentSettings { func settings(for category: SponsorBlockCategory) -> SponsorBlockCategorySettings { settings(forKey: category.rawValue) } - - /// Returns a copy with updated settings for a specific category. - /// - Parameters: - /// - category: The category to update. - /// - settings: The new settings. - /// - Returns: Updated SponsorBlockSegmentSettings. - func withUpdatedSettings( - for category: SponsorBlockCategory, - _ settings: SponsorBlockCategorySettings - ) -> SponsorBlockSegmentSettings { - withUpdatedSettings(forKey: category.rawValue, settings) - } } // MARK: - Font Style diff --git a/Yattee/Models/PlayerControls/LayoutSection.swift b/Yattee/Models/PlayerControls/LayoutSection.swift index a43c4002..11de198c 100644 --- a/Yattee/Models/PlayerControls/LayoutSection.swift +++ b/Yattee/Models/PlayerControls/LayoutSection.swift @@ -34,37 +34,12 @@ struct LayoutSection: Codable, Hashable, Sendable { buttons.append(button) } - /// Adds a button with the given type to the end of the section. - /// - Parameter type: The button type to add with default configuration. - mutating func add(buttonType type: ControlButtonType) { - buttons.append(.defaultConfiguration(for: type)) - } - - /// Removes a button at the specified index. - /// - Parameter index: The index of the button to remove. - mutating func remove(at index: Int) { - guard buttons.indices.contains(index) else { return } - buttons.remove(at: index) - } - /// Removes a button with the specified ID. /// - Parameter id: The ID of the button to remove. mutating func remove(id: UUID) { buttons.removeAll { $0.id == id } } - /// Moves a button from one position to another. - /// - Parameters: - /// - source: The current index of the button. - /// - destination: The target index for the button. - mutating func move(from source: Int, to destination: Int) { - guard buttons.indices.contains(source) else { return } - let button = buttons.remove(at: source) - let targetIndex = destination > source ? destination - 1 : destination - let clampedIndex = max(0, min(buttons.count, targetIndex)) - buttons.insert(button, at: clampedIndex) - } - /// Moves buttons from source indices to a destination index. /// Compatible with SwiftUI's `onMove` modifier. /// - Parameters: @@ -97,13 +72,6 @@ struct LayoutSection: Codable, Hashable, Sendable { buttons.map(\.buttonType) } - /// Checks if a button type is already in this section. - /// - Parameter type: The button type to check. - /// - Returns: True if the type is already present. - func contains(buttonType type: ControlButtonType) -> Bool { - buttons.contains { $0.buttonType == type } - } - /// Returns buttons filtered by visibility for the given layout state. /// - Parameter isWideLayout: Whether the current layout is wide/landscape. /// - Returns: Buttons that should be visible. diff --git a/Yattee/Models/Storyboard.swift b/Yattee/Models/Storyboard.swift index 43d44e11..0ee31750 100644 --- a/Yattee/Models/Storyboard.swift +++ b/Yattee/Models/Storyboard.swift @@ -170,15 +170,6 @@ extension Storyboard { // MARK: - Storyboard Selection extension Array where Element == Storyboard { - /// Selects the preferred storyboard based on desired width. - /// Prefers the largest storyboard that doesn't exceed maxWidth. - /// - Parameter maxWidth: Maximum preferred width (default 160) - /// - Returns: Best matching storyboard, or nil if array is empty - func preferred(maxWidth: Int = 160) -> Storyboard? { - let suitable = filter { $0.width <= maxWidth } - return suitable.max(by: { $0.width < $1.width }) ?? first - } - /// Returns the highest quality storyboard (largest width). func highest() -> Storyboard? { self.max(by: { $0.width < $1.width }) diff --git a/Yattee/Models/VideoListLayout.swift b/Yattee/Models/VideoListLayout.swift index a00eb619..2bdcc2e5 100644 --- a/Yattee/Models/VideoListLayout.swift +++ b/Yattee/Models/VideoListLayout.swift @@ -113,7 +113,6 @@ func makeGridColumns(count: Int) -> [GridItem] { /// // Then use: /// // gridConfig.effectiveColumns - actual column count /// // gridConfig.isCompactCards - whether to use compact card styling -/// // gridConfig.columns - GridItem array for LazyVGrid /// // gridConfig.maxColumns - for ViewOptionsSheet /// ``` struct GridLayoutConfiguration { @@ -134,9 +133,4 @@ struct GridLayoutConfiguration { var isCompactCards: Bool { effectiveColumns >= GridConstants.compactThreshold } - - /// GridItem array for LazyVGrid columns parameter. - var columns: [GridItem] { - makeGridColumns(count: effectiveColumns) - } } diff --git a/Yattee/Services/API/ContentService.swift b/Yattee/Services/API/ContentService.swift index fbbb1f4e..571ad450 100644 --- a/Yattee/Services/API/ContentService.swift +++ b/Yattee/Services/API/ContentService.swift @@ -8,25 +8,7 @@ import Foundation /// Protocol defining the common content fetching interface. -protocol ContentServiceProtocol: Sendable { - func trending(for instance: Instance) async throws -> [Video] - func popular(for instance: Instance) async throws -> [Video] - func feed(for instance: Instance, credential: String) async throws -> [Video] - func subscriptions(for instance: Instance, credential: String) async throws -> [Channel] - func search(query: String, instance: Instance, page: Int, filters: SearchFilters) async throws -> SearchResult - func searchSuggestions(query: String, instance: Instance) async throws -> [String] - func video(id: String, instance: Instance) async throws -> Video - func channel(id: String, instance: Instance) async throws -> Channel - func channelVideos(id: String, instance: Instance, continuation: String?) async throws -> ChannelVideosPage - func channelPlaylists(id: String, instance: Instance, continuation: String?) async throws -> ChannelPlaylistsPage - func channelShorts(id: String, instance: Instance, continuation: String?) async throws -> ChannelVideosPage - func channelStreams(id: String, instance: Instance, continuation: String?) async throws -> ChannelVideosPage - func playlist(id: String, instance: Instance) async throws -> Playlist - func comments(videoID: String, instance: Instance, continuation: String?) async throws -> CommentsPage - func streams(videoID: String, instance: Instance) async throws -> [Stream] - func captions(videoID: String, instance: Instance) async throws -> [Caption] - func channelSearch(id: String, query: String, instance: Instance, page: Int) async throws -> ChannelSearchPage -} +protocol ContentServiceProtocol: Sendable {} /// Unified content service that routes requests to the appropriate API based on instance type. actor ContentService: ContentServiceProtocol { @@ -55,17 +37,6 @@ actor ContentService: ContentServiceProtocol { self.basicAuthCredentialsManager = basicAuthCredentialsManager } - init(httpClientFactory: HTTPClientFactory, basicAuthCredentialsManager: BasicAuthCredentialsManager? = nil) { - self.httpClientFactory = httpClientFactory - // Create default client for instances that don't need insecure SSL - self.defaultHTTPClient = httpClientFactory.createClient(allowInvalidCertificates: false) - self.defaultInvidiousAPI = InvidiousAPI(httpClient: defaultHTTPClient) - self.defaultPipedAPI = PipedAPI(httpClient: defaultHTTPClient) - self.defaultPeerTubeAPI = PeerTubeAPI(httpClient: defaultHTTPClient) - self.defaultYatteeServerAPI = YatteeServerAPI(httpClient: defaultHTTPClient) - self.basicAuthCredentialsManager = basicAuthCredentialsManager - } - // MARK: - Routing /// Builds a per-instance HTTPClient with the basic-auth `Authorization` header baked in, @@ -256,10 +227,6 @@ actor ContentService: ContentServiceProtocol { try await api(for: instance).comments(videoID: videoID, instance: instance, continuation: continuation) } - func streams(videoID: String, instance: Instance) async throws -> [Stream] { - try await api(for: instance).streams(videoID: videoID, instance: instance) - } - func captions(videoID: String, instance: Instance) async throws -> [Caption] { try await api(for: instance).captions(videoID: videoID, instance: instance) } @@ -590,17 +557,6 @@ actor ContentService: ContentServiceProtocol { } } - /// Fetches streams with proxy URLs for downloads (Yattee Server: hits /proxy/fast/ - /// so the server caches the file on disk; other backends apply Invidious URL rewriting). - func proxyStreams(videoID: String, instance: Instance) async throws -> [Stream] { - if instance.type == .yatteeServer { - return try await yatteeServerAPI(for: instance) - .proxyStreams(videoID: videoID, instance: instance, mode: .download) - } - let fetchedStreams = try await streams(videoID: videoID, instance: instance) - return await InvidiousAPI.proxyStreamsIfNeeded(fetchedStreams, instance: instance) - } - /// Fetches video details + streams for downloads. For Yattee Server this routes /// through `/proxy/fast/`, which caches the file on disk on the server. func videoWithProxyStreamsAndCaptionsAndStoryboards(id: String, instance: Instance) async throws -> (video: Video, streams: [Stream], captions: [Caption], storyboards: [Storyboard]) { diff --git a/Yattee/Services/API/GitHubAPI.swift b/Yattee/Services/API/GitHubAPI.swift index 0683b596..fe99ff70 100644 --- a/Yattee/Services/API/GitHubAPI.swift +++ b/Yattee/Services/API/GitHubAPI.swift @@ -77,9 +77,4 @@ actor GitHubAPI { throw APIError.decodingError(error) } } - - /// Clears the cache. - func clearCache() { - contributorsCache = nil - } } diff --git a/Yattee/Services/API/InstanceAPI.swift b/Yattee/Services/API/InstanceAPI.swift index bc5a694e..f546395d 100644 --- a/Yattee/Services/API/InstanceAPI.swift +++ b/Yattee/Services/API/InstanceAPI.swift @@ -21,7 +21,6 @@ protocol InstanceAPI: Sendable { func channelStreams(id: String, instance: Instance, continuation: String?) async throws -> ChannelVideosPage func playlist(id: String, instance: Instance) async throws -> Playlist func comments(videoID: String, instance: Instance, continuation: String?) async throws -> CommentsPage - func streams(videoID: String, instance: Instance) async throws -> [Stream] func captions(videoID: String, instance: Instance) async throws -> [Caption] func channelSearch(id: String, query: String, instance: Instance, page: Int) async throws -> ChannelSearchPage } diff --git a/Yattee/Services/API/InstanceDetector.swift b/Yattee/Services/API/InstanceDetector.swift index fde71832..0ca79dda 100644 --- a/Yattee/Services/API/InstanceDetector.swift +++ b/Yattee/Services/API/InstanceDetector.swift @@ -50,12 +50,9 @@ enum DetectionError: Error, Sendable { /// Result of instance detection including type and authentication requirements. struct InstanceDetectionResult: Sendable { let type: InstanceType - /// Whether this instance requires authentication (Basic Auth for Yattee Server). - let requiresAuth: Bool init(type: InstanceType, requiresAuth: Bool = false) { self.type = type - self.requiresAuth = requiresAuth } } @@ -67,27 +64,6 @@ actor InstanceDetector { self.httpClient = httpClient } - /// Detects the instance type for a given URL. - /// - Parameter url: The base URL of the instance. - /// - Returns: The detected instance type, or nil if detection failed. - func detect(url: URL) async -> InstanceType? { - let result = await detectWithAuth(url: url) - return result?.type - } - - /// Detects the instance type and authentication requirements for a given URL. - /// - Parameter url: The base URL of the instance. - /// - Returns: The detection result including type and auth requirements, or nil if detection failed. - func detectWithAuth(url: URL) async -> InstanceDetectionResult? { - let result = await detectWithResult(url: url) - switch result { - case .success(let detectionResult): - return detectionResult - case .failure: - return nil - } - } - /// Detects the instance type with detailed error reporting. /// - Parameters: /// - url: The base URL of the instance. diff --git a/Yattee/Services/API/InvidiousAPI.swift b/Yattee/Services/API/InvidiousAPI.swift index 13dc28c8..29eb04b4 100644 --- a/Yattee/Services/API/InvidiousAPI.swift +++ b/Yattee/Services/API/InvidiousAPI.swift @@ -234,22 +234,11 @@ actor InvidiousAPI: InstanceAPI { ).toPlaylist(baseURL: instance.url) } - /// Fetches a user's playlist using authenticated endpoint. - /// Required for private playlists (IVPL* IDs). - /// - Parameters: - /// - id: The playlist ID - /// - instance: The Invidious instance - /// - sid: The session ID from login - /// - Returns: The playlist with videos - func userPlaylist(id: String, instance: Instance, sid: String) async throws -> Playlist { - try await userPlaylistSnapshot(id: id, instance: instance, sid: sid).playlist - } - /// Fetches a user's playlist along with the per-entry identities needed to /// mutate it. /// - /// Same request as ``userPlaylist(id:instance:sid:)`` — that one is a thin - /// wrapper over this — but it keeps the `index`/`indexId` tokens Invidious + /// Uses the authenticated endpoint (required for private IVPL* playlists) + /// and keeps the `index`/`indexId` tokens Invidious /// hands out for every row instead of dropping them on conversion. /// - Parameters: /// - id: The playlist ID @@ -455,14 +444,6 @@ actor InvidiousAPI: InstanceAPI { return ChannelSearchPage(items: items, nextPage: hasResults ? page + 1 : nil) } - /// Fetches video details, streams, and captions in a single API call. - /// This is more efficient than calling video(), streams(), and captions() separately - /// since they all fetch from the same endpoint. - func videoWithStreamsAndCaptions(id: String, instance: Instance) async throws -> (video: Video, streams: [Stream], captions: [Caption]) { - let result = try await videoWithStreamsAndCaptionsAndStoryboards(id: id, instance: instance) - return (video: result.video, streams: result.streams, captions: result.captions) - } - func videoWithStreamsAndCaptionsAndStoryboards(id: String, instance: Instance) async throws -> (video: Video, streams: [Stream], captions: [Caption], storyboards: [Storyboard]) { let endpoint = GenericEndpoint.get("/api/v1/videos/\(id)") let response: InvidiousVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) @@ -1854,10 +1835,6 @@ private struct InvidiousChannel: Decodable, Sendable { } } -private struct InvidiousChannelVideos: Decodable, Sendable { - let videos: [InvidiousVideo] -} - private struct InvidiousChannelVideosWithContinuation: Decodable, Sendable { let videos: [InvidiousVideo] let continuation: String? diff --git a/Yattee/Services/API/PipedAPI.swift b/Yattee/Services/API/PipedAPI.swift index 51a1aefa..cffd3e87 100644 --- a/Yattee/Services/API/PipedAPI.swift +++ b/Yattee/Services/API/PipedAPI.swift @@ -385,21 +385,10 @@ actor PipedAPI: InstanceAPI { return response.map { $0.toPlaylist() } } - /// Fetches a user playlist with its videos. - /// - Parameters: - /// - id: The playlist ID (UUID) - /// - instance: The Piped instance - /// - authToken: The auth token from login - /// - Returns: Playlist with videos - func userPlaylist(id: String, instance: Instance, authToken: String) async throws -> Playlist { - try await userPlaylistSnapshot(id: id, instance: instance, authToken: authToken).playlist - } - /// Fetches a user playlist along with the per-entry identities needed to /// mutate it. /// - /// Same request as ``userPlaylist(id:instance:authToken:)`` — that one is a - /// thin wrapper over this. Piped has no per-entry token, so entries are + /// Piped has no per-entry token, so entries are /// addressed by their position in this fetch and are only valid until the /// playlist next changes. /// - Parameters: diff --git a/Yattee/Services/API/ProxyDetectionCache.swift b/Yattee/Services/API/ProxyDetectionCache.swift index 97ec0ca2..e04028fd 100644 --- a/Yattee/Services/API/ProxyDetectionCache.swift +++ b/Yattee/Services/API/ProxyDetectionCache.swift @@ -45,13 +45,6 @@ actor ProxyDetectionCache { return entry.decision } - /// Most-recently-seen CDN URL for this instance. Used by ``prewarm`` so we - /// don't have to wait for the current video's API response just to learn a - /// URL to probe. - func lastSampleURL(for instance: Instance) -> URL? { - entries[instance.id]?.sampleURL - } - /// Records a verdict (decision + the URL we probed against, kept as a /// future probe sample). Refreshes the TTL. func record(decision: Bool, sampleURL: URL?, for instance: Instance) { @@ -115,12 +108,4 @@ actor ProxyDetectionCache { inFlight[instance.id]?.cancel() inFlight.removeValue(forKey: instance.id) } - - /// Drop all entries — e.g. after a network reachability change or when the - /// instances collection mutates broadly. - func invalidateAll() { - entries.removeAll() - for (_, task) in inFlight { task.cancel() } - inFlight.removeAll() - } } diff --git a/Yattee/Services/API/YatteeServerAPI.swift b/Yattee/Services/API/YatteeServerAPI.swift index 7533637e..debef250 100644 --- a/Yattee/Services/API/YatteeServerAPI.swift +++ b/Yattee/Services/API/YatteeServerAPI.swift @@ -206,12 +206,6 @@ actor YatteeServerAPI: InstanceAPI { } } - func streams(videoID: String, instance: Instance) async throws -> [Stream] { - let endpoint = GenericEndpoint.get("/api/v1/videos/\(videoID)") - let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) - return response.toStreams() - } - func captions(videoID: String, instance: Instance) async throws -> [Caption] { let endpoint = GenericEndpoint.get("/api/v1/videos/\(videoID)") let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) @@ -237,12 +231,6 @@ actor YatteeServerAPI: InstanceAPI { return ChannelSearchPage(items: items, nextPage: hasResults ? page + 1 : nil) } - /// Fetches video details, streams, and captions in a single API call. - func videoWithStreamsAndCaptions(id: String, instance: Instance) async throws -> (video: Video, streams: [Stream], captions: [Caption]) { - let result = try await videoWithStreamsAndCaptionsAndStoryboards(id: id, instance: instance) - return (video: result.video, streams: result.streams, captions: result.captions) - } - func videoWithStreamsAndCaptionsAndStoryboards(id: String, instance: Instance) async throws -> (video: Video, streams: [Stream], captions: [Caption], storyboards: [Storyboard]) { let endpoint = GenericEndpoint.get("/api/v1/videos/\(id)") let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) @@ -268,16 +256,6 @@ actor YatteeServerAPI: InstanceAPI { case download } - /// Fetches streams with URLs that proxy through the Yattee Server. - func proxyStreams(videoID: String, instance: Instance, mode: ProxyMode) async throws -> [Stream] { - let endpoint = GenericEndpoint.get( - "/api/v1/videos/\(videoID)", - query: ["proxy": "true", "proxy_mode": mode.rawValue] - ) - let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) - return response.toStreams() - } - /// Fetches video details, proxy streams, captions, and storyboards in a single API call. func videoWithProxyStreamsAndCaptionsAndStoryboards(id: String, instance: Instance, mode: ProxyMode) async throws -> (video: Video, streams: [Stream], captions: [Caption], storyboards: [Storyboard]) { let endpoint = GenericEndpoint.get( @@ -1324,10 +1302,6 @@ private struct YatteeChannel: Decodable, Sendable { } } -private struct YatteeChannelVideos: Decodable, Sendable { - let videos: [YatteeVideo] -} - private struct YatteeChannelSearchResponse: Decodable, Sendable { let videos: [YatteeVideo] } diff --git a/Yattee/Services/BackgroundRefresh/BackgroundRefreshManager.swift b/Yattee/Services/BackgroundRefresh/BackgroundRefreshManager.swift index c071df40..a1e864c2 100644 --- a/Yattee/Services/BackgroundRefresh/BackgroundRefreshManager.swift +++ b/Yattee/Services/BackgroundRefresh/BackgroundRefreshManager.swift @@ -23,19 +23,15 @@ final class BackgroundRefreshManager { // MARK: - Dependencies - private weak var appEnvironment: AppEnvironment? private let backgroundRefresher: BackgroundFeedRefresher - private let notificationManager: NotificationManager // MARK: - Initialization init(notificationManager: NotificationManager) { - self.notificationManager = notificationManager self.backgroundRefresher = BackgroundFeedRefresher(notificationManager: notificationManager) } func setAppEnvironment(_ environment: AppEnvironment) { - self.appEnvironment = environment backgroundRefresher.setAppEnvironment(environment) } @@ -148,11 +144,6 @@ final class BackgroundRefreshManager { activityScheduler = nil LoggingService.shared.debug("Invalidated macOS background scheduler", category: .notifications) } - - func restartMacOSScheduler() { - invalidateMacOSScheduler() - registerMacOSBackgroundActivity() - } #endif // MARK: - Enable/Disable diff --git a/Yattee/Services/CloudKit/CloudKitSyncEngine.swift b/Yattee/Services/CloudKit/CloudKitSyncEngine.swift index 93a6a3c6..4c5ed6c4 100644 --- a/Yattee/Services/CloudKit/CloudKitSyncEngine.swift +++ b/Yattee/Services/CloudKit/CloudKitSyncEngine.swift @@ -1225,21 +1225,6 @@ final class CloudKitSyncEngine: @unchecked Sendable { LoggingService.shared.logCloudKit("Queued channel notification settings save: \(settings.channelID)") } - /// Queue channel notification settings deletion for sync (debounced). - func queueChannelNotificationSettingsDelete(channelID: String, scope: SourceScope) { - guard canSyncSubscriptions else { return } - - let recordID = SyncableRecordType.channelNotificationSettings(channelID: channelID, scope: scope).recordID(in: zone) - addPendingChanges([.deleteRecord(recordID)]) - LoggingService.shared.logCloudKit("Queued channel notification settings delete: \(channelID)") - } - - /// Upload all existing channel notification settings to CloudKit (for initial sync). - func uploadAllLocalChannelNotificationSettings() async { - guard enqueueAllLocalChannelNotificationSettings() else { return } - await sync() - } - /// Registers all channel notification settings for upload without sending. /// - Returns: whether anything was registered. @discardableResult diff --git a/Yattee/Services/CloudKit/CloudKitZoneManager.swift b/Yattee/Services/CloudKit/CloudKitZoneManager.swift index 78a39195..a8c76ca5 100644 --- a/Yattee/Services/CloudKit/CloudKitZoneManager.swift +++ b/Yattee/Services/CloudKit/CloudKitZoneManager.swift @@ -82,9 +82,4 @@ actor CloudKitZoneManager { throw error } } - - /// Fetches all zones in the private database. - func fetchAllZones() async throws -> [CKRecordZone] { - try await database.allRecordZones() - } } diff --git a/Yattee/Services/CloudKit/RecordTypes.swift b/Yattee/Services/CloudKit/RecordTypes.swift index 0757e7da..872c1d6c 100644 --- a/Yattee/Services/CloudKit/RecordTypes.swift +++ b/Yattee/Services/CloudKit/RecordTypes.swift @@ -109,29 +109,6 @@ enum SyncableRecordType: Sendable { case dataContext case accountDeletion(accountID: UUID) - /// The CloudKit record type name. - var recordTypeName: String { - switch self { - case .subscription: RecordType.subscription - case .watchEntry: RecordType.watchEntry - case .bookmark: RecordType.bookmark - case .localPlaylist: RecordType.localPlaylist - case .localPlaylistItem: RecordType.localPlaylistItem - case .searchHistory: RecordType.searchHistory - case .recentChannel: RecordType.recentChannel - case .recentPlaylist: RecordType.recentPlaylist - case .appSettings: RecordType.appSettings - case .instance: RecordType.instance - case .mediaSource: RecordType.mediaSource - case .channelNotificationSettings: RecordType.channelNotificationSettings - case .controlsPreset: RecordType.controlsPreset - case .account: RecordType.account - case .legacyAccountMigration: RecordType.legacyAccountMigration - case .dataContext: RecordType.dataContext - case .accountDeletion: RecordType.accountDeletion - } - } - /// CloudKit rejects record names longer than this with a fatal /// `CKException` at `CKRecord.ID` construction time. static let maxRecordNameLength = 255 @@ -290,22 +267,3 @@ enum AccountRecordName { || recordName.hasPrefix(accountPrefix) } } - -/// Sync operation type. -enum SyncOperation: Sendable { - case save - case delete -} - -/// Pending sync change to be uploaded to CloudKit. -struct PendingSyncChange: Sendable { - let recordType: SyncableRecordType - let operation: SyncOperation - let timestamp: Date - - init(recordType: SyncableRecordType, operation: SyncOperation) { - self.recordType = recordType - self.operation = operation - self.timestamp = Date() - } -} diff --git a/Yattee/Services/Credentials/AccountCredentialsManager.swift b/Yattee/Services/Credentials/AccountCredentialsManager.swift index 1385088c..80c4b0dc 100644 --- a/Yattee/Services/Credentials/AccountCredentialsManager.swift +++ b/Yattee/Services/Credentials/AccountCredentialsManager.swift @@ -17,9 +17,6 @@ import Foundation /// proxy credential covers every account on that instance. @MainActor protocol AccountCredentialsManager: AnyObject, Observable { - /// Accounts with a stored credential under the **current** identity scope. - var loggedInAccountIDs: Set { get } - /// Whether the identity scope has been resolved yet. /// /// While `false`, every read below reports "no credential" because the app @@ -41,12 +38,6 @@ protocol AccountCredentialsManager: AnyObject, Observable { /// key needs only the account's ID. func deleteCredential(forAccountID accountID: UUID) - /// Rewrites stored credentials so they carry the current iCloud sync - /// setting. Whether a Keychain item syncs is decided when it is written, so - /// enabling sync later would otherwise leave existing logins local — and - /// every other device with an account whose credential never arrives. - func resyncCredentialsWithCurrentSetting() - /// The login identifier the user typed. Kept in the Keychain beside the /// credential so it never enters synced metadata. func setUsername(_ username: String, for account: Account) diff --git a/Yattee/Services/Credentials/BasicAuthCredentialsManager.swift b/Yattee/Services/Credentials/BasicAuthCredentialsManager.swift index fa955759..4c7b2f13 100644 --- a/Yattee/Services/Credentials/BasicAuthCredentialsManager.swift +++ b/Yattee/Services/Credentials/BasicAuthCredentialsManager.swift @@ -179,16 +179,6 @@ final class BasicAuthCredentialsManager: InstanceCredentialsManager { return hasCreds } - /// Refreshes the logged-in status for an instance. - /// Call this when the view appears to ensure UI is in sync. - func refreshLoginStatus(for instance: Instance) { - if credentials(for: instance) != nil { - loggedInInstanceIDs.insert(instance.id) - } else { - loggedInInstanceIDs.remove(instance.id) - } - } - // MARK: - InstanceCredentialsManager Protocol /// Stores a credential for an instance (protocol conformance). diff --git a/Yattee/Services/Credentials/InstanceCredentialsManager.swift b/Yattee/Services/Credentials/InstanceCredentialsManager.swift index 65b74f1a..57899f66 100644 --- a/Yattee/Services/Credentials/InstanceCredentialsManager.swift +++ b/Yattee/Services/Credentials/InstanceCredentialsManager.swift @@ -13,10 +13,6 @@ import Foundation /// across different instance types (Invidious, Piped, etc.). @MainActor protocol InstanceCredentialsManager: AnyObject, Observable { - /// Set of instance IDs that are currently logged in. - /// Used for reactive UI updates when login state changes. - var loggedInInstanceIDs: Set { get } - /// Stores a credential (SID for Invidious, token for Piped) for an instance. /// - Parameters: /// - credential: The credential value to store @@ -36,9 +32,4 @@ protocol InstanceCredentialsManager: AnyObject, Observable { /// - Parameter instance: The instance to check /// - Returns: true if logged in, false otherwise func isLoggedIn(for instance: Instance) -> Bool - - /// Refreshes the login status for an instance from the Keychain. - /// Call this when a view appears to ensure UI is in sync with stored state. - /// - Parameter instance: The instance to refresh status for - func refreshLoginStatus(for instance: Instance) } diff --git a/Yattee/Services/Credentials/InvidiousCredentialsManager.swift b/Yattee/Services/Credentials/InvidiousCredentialsManager.swift index b7476cb3..9ec39106 100644 --- a/Yattee/Services/Credentials/InvidiousCredentialsManager.swift +++ b/Yattee/Services/Credentials/InvidiousCredentialsManager.swift @@ -236,16 +236,6 @@ final class InvidiousCredentialsManager: InstanceCredentialsManager { return hasSession } - /// Refreshes the logged-in status for an instance. - /// Call this when the view appears to ensure UI is in sync. - func refreshLoginStatus(for instance: Instance) { - if sid(for: instance) != nil { - loggedInInstanceIDs.insert(instance.id) - } else { - loggedInInstanceIDs.remove(instance.id) - } - } - // MARK: - InstanceCredentialsManager Protocol /// Stores a credential (SID) for an Invidious instance. @@ -270,7 +260,6 @@ final class InvidiousCredentialsManager: InstanceCredentialsManager { // MARK: - AccountCredentialsManager extension InvidiousCredentialsManager: AccountCredentialsManager { - var loggedInAccountIDs: Set { accountCredentials.loggedInAccountIDs } var isScopeResolved: Bool { accountCredentials.isScopeResolved } func setCredential(_ credential: String, for account: Account) { diff --git a/Yattee/Services/Credentials/PipedCredentialsManager.swift b/Yattee/Services/Credentials/PipedCredentialsManager.swift index 8d3ec6f9..fada499c 100644 --- a/Yattee/Services/Credentials/PipedCredentialsManager.swift +++ b/Yattee/Services/Credentials/PipedCredentialsManager.swift @@ -199,7 +199,6 @@ final class PipedCredentialsManager: InstanceCredentialsManager { // MARK: - AccountCredentialsManager extension PipedCredentialsManager: AccountCredentialsManager { - var loggedInAccountIDs: Set { accountCredentials.loggedInAccountIDs } var isScopeResolved: Bool { accountCredentials.isScopeResolved } func setCredential(_ credential: String, for account: Account) { diff --git a/Yattee/Services/DeviceRotationManager.swift b/Yattee/Services/DeviceRotationManager.swift index aca6c45a..44accabe 100644 --- a/Yattee/Services/DeviceRotationManager.swift +++ b/Yattee/Services/DeviceRotationManager.swift @@ -252,11 +252,6 @@ final class DeviceRotationManager { } } - /// Check if device is currently in landscape orientation - var isLandscape: Bool { - detectedOrientation == .landscapeLeft || detectedOrientation == .landscapeRight - } - /// Check if device is currently in portrait orientation var isPortrait: Bool { detectedOrientation == .portrait || detectedOrientation == .portraitUpsideDown diff --git a/Yattee/Services/Downloads/DownloadManager.swift b/Yattee/Services/Downloads/DownloadManager.swift index d6ace4c3..b7278933 100644 --- a/Yattee/Services/Downloads/DownloadManager.swift +++ b/Yattee/Services/Downloads/DownloadManager.swift @@ -866,20 +866,6 @@ final class DownloadManager: NSObject { // MARK: - Batch Operations - /// Pause all active downloads. - func pauseAll() async { - for download in activeDownloads where download.status == .downloading { - await pause(download) - } - } - - /// Resume all paused downloads. - func resumeAll() async { - for download in activeDownloads where download.status == .paused { - await resume(download) - } - } - /// Delete all completed downloads. func deleteAllCompleted() async { for download in completedDownloads { @@ -1024,16 +1010,6 @@ final class DownloadManager: NSObject { return (video, stream, audioStream, captionURL, download.dislikeCount) } - /// Creates a Video and Stream for playing a downloaded video by video ID. - /// Returns nil if the video is not downloaded or the file doesn't exist. - /// Also returns the stored dislike count, audio stream, and caption URL if available. - func videoAndStream(for videoID: VideoID) -> (video: Video, stream: Stream, audioStream: Stream?, captionURL: URL?, dislikeCount: Int?)? { - guard let download = completedDownloads.first(where: { $0.videoID == videoID }) else { - return nil - } - return videoAndStream(for: download) - } - // MARK: - Private Helpers func sortQueue() { @@ -1226,36 +1202,12 @@ final class DownloadManager { private(set) var storageUsed: Int64 = 0 var maxConcurrentDownloads: Int { 2 } - func enqueue(_ video: Video, quality: String, formatID: String, streamURL: URL, audioStreamURL: URL? = nil, captionURL: URL? = nil, audioLanguage: String? = nil, captionLanguage: String? = nil, httpHeaders: [String: String]? = nil, storyboard: Storyboard? = nil, dislikeCount: Int? = nil, priority: DownloadPriority = .normal, videoCodec: String? = nil, audioCodec: String? = nil, videoBitrate: Int? = nil, audioBitrate: Int? = nil, suppressStartToast: Bool = false) async throws { - throw DownloadError.notSupported - } - - func autoEnqueue(_ video: Video, preferredQuality: DownloadQuality, preferredAudioLanguage: String?, preferredSubtitlesLanguage: String?, includeSubtitles: Bool, contentService: ContentService, instance: Instance, suppressStartToast: Bool = false) async throws { - throw DownloadError.notSupported - } - - func autoEnqueueMediaSource(_ video: Video, mediaSourcesManager: MediaSourcesManager, webDAVClient: WebDAVClient, smbClient: SMBClient) async throws { - throw DownloadError.notSupported - } - - func pause(_ download: Download) async {} - func resume(_ download: Download) async {} - func cancel(_ download: Download) async {} - func delete(_ download: Download) async {} - func moveInQueue(_ download: Download, to position: Int) async {} - func pauseAll() async {} - func resumeAll() async {} - func deleteAllCompleted() async {} - func calculateStorageUsed() async -> Int64 { 0 } func getAvailableStorage() -> Int64 { 0 } - func deleteWatchedDownloads(watchedVideoIDs: Set) async {} func isDownloaded(_ videoID: VideoID) -> Bool { false } func isDownloading(_ videoID: VideoID) -> Bool { false } func download(for videoID: VideoID) -> Download? { nil } func localURL(for videoID: VideoID) -> URL? { nil } - func resolveLocalURL(for download: Download) -> URL? { nil } func videoAndStream(for download: Download) -> (video: Video, stream: Stream, audioStream: Stream?, captionURL: URL?, dislikeCount: Int?)? { nil } - func videoAndStream(for videoID: VideoID) -> (video: Video, stream: Stream, audioStream: Stream?, captionURL: URL?, dislikeCount: Int?)? { nil } func setToastManager(_ manager: ToastManager) {} func setDownloadSettings(_ settings: DownloadSettings) {} func downloadsDirectory() -> URL { URL(fileURLWithPath: NSTemporaryDirectory()) } diff --git a/Yattee/Services/ImageLoading/ImageLoadingService.swift b/Yattee/Services/ImageLoading/ImageLoadingService.swift index 8eabac7d..474fbc09 100644 --- a/Yattee/Services/ImageLoading/ImageLoadingService.swift +++ b/Yattee/Services/ImageLoading/ImageLoadingService.swift @@ -118,20 +118,6 @@ final class ImageLoadingService: Sendable { LoggingService.shared.info("Image cache cleared", category: .imageLoading) } - - /// Returns the disk cache size in bytes. - func diskCacheSize() -> Int { - guard let dataCache = ImagePipeline.shared.configuration.dataCache as? DataCache else { - return 0 - } - return dataCache.totalSize - } - - /// Returns a formatted string of the disk cache size. - func formattedDiskCacheSize() -> String { - let size = diskCacheSize() - return ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .file) - } } /// Pipeline delegate that normalizes Nuke's cache key by stripping per-request diff --git a/Yattee/Services/Logging/LoggingService.swift b/Yattee/Services/Logging/LoggingService.swift index 4ba3061e..c9c583b1 100644 --- a/Yattee/Services/Logging/LoggingService.swift +++ b/Yattee/Services/Logging/LoggingService.swift @@ -313,11 +313,6 @@ extension LoggingService { self.error(message, category: .downloads, details: error?.localizedDescription) } - /// Log a notification event. - nonisolated func logNotification(_ message: String, details: String? = nil) { - info(message, category: .notifications, details: details) - } - /// Log a notification error. nonisolated func logNotificationError(_ message: String, error: Error? = nil) { self.error(message, category: .notifications, details: error?.localizedDescription) diff --git a/Yattee/Services/MediaSources/LocalFileClient.swift b/Yattee/Services/MediaSources/LocalFileClient.swift index 2550b463..67901532 100644 --- a/Yattee/Services/MediaSources/LocalFileClient.swift +++ b/Yattee/Services/MediaSources/LocalFileClient.swift @@ -47,14 +47,6 @@ enum LocalFolderURLResolver { lock.lock(); defer { lock.unlock() } return cache[sourceID] } - - static func clear(_ sourceID: UUID) { - lock.lock(); defer { lock.unlock() } - cache.removeValue(forKey: sourceID) - if let previous = persistentAccess.removeValue(forKey: sourceID) { - previous.stopAccessingSecurityScopedResource() - } - } } /// Actor-based client for local file system operations. @@ -279,15 +271,6 @@ actor LocalFileClient { return url } - /// Resolves bookmark and starts accessing the security-scoped resource. - /// - Parameter bookmarkData: The stored bookmark data. - /// - Returns: Tuple of (URL, didStartAccessing) - caller must call stopAccessingSecurityScopedResource when done. - func resolveAndAccessBookmark(_ bookmarkData: Data) throws -> (URL, Bool) { - let url = try resolveBookmark(bookmarkData) - let didStart = url.startAccessingSecurityScopedResource() - return (url, didStart) - } - // MARK: - Private Methods private func createMediaFile( diff --git a/Yattee/Services/MediaSources/NetworkShareDiscoveryService.swift b/Yattee/Services/MediaSources/NetworkShareDiscoveryService.swift index d6f8d4cd..1f24f3fd 100644 --- a/Yattee/Services/MediaSources/NetworkShareDiscoveryService.swift +++ b/Yattee/Services/MediaSources/NetworkShareDiscoveryService.swift @@ -22,14 +22,6 @@ struct DiscoveredShare: Identifiable, Hashable, Sendable { case webdavs // _webdavs._tcp (HTTPS) case smb // _smb._tcp - var displayName: String { - switch self { - case .webdav: String(localized: "discovery.shareType.webdav") - case .webdavs: String(localized: "discovery.shareType.webdavs") - case .smb: String(localized: "discovery.shareType.smb") - } - } - var systemImage: String { switch self { case .webdav: "globe" diff --git a/Yattee/Services/MediaSources/SMBBridge/SMBBridgeWrapper.swift b/Yattee/Services/MediaSources/SMBBridge/SMBBridgeWrapper.swift index 5e5dc23a..52c9ebd5 100644 --- a/Yattee/Services/MediaSources/SMBBridge/SMBBridgeWrapper.swift +++ b/Yattee/Services/MediaSources/SMBBridge/SMBBridgeWrapper.swift @@ -39,24 +39,6 @@ enum SMBBridgeError: Error, LocalizedError, Sendable { return "Invalid parameters provided" } } - - /// User-friendly error messages based on common SMB errors - var userFriendlyMessage: String { - switch self { - case .connectionFailed(let msg) where msg.contains("errno: 13"): - return "Permission denied. Check username and password." - case .connectionFailed(let msg) where msg.contains("errno: 2"): - return "Share or path not found." - case .connectionFailed(let msg) where msg.contains("errno: 110"): - return "Connection timed out. Check server address and network." - case .connectionFailed(let msg) where msg.contains("errno: 111"): - return "Cannot reach server. Check server address." - case .listingFailed(let msg) where msg.contains("errno: 13"): - return "Access denied to this folder." - default: - return errorDescription ?? "Unknown error" - } - } } /// SMB protocol version for connection preferences (Swift wrapper). diff --git a/Yattee/Services/MediaSources/SMBClient.swift b/Yattee/Services/MediaSources/SMBClient.swift index 4f098499..31c1a76f 100644 --- a/Yattee/Services/MediaSources/SMBClient.swift +++ b/Yattee/Services/MediaSources/SMBClient.swift @@ -343,29 +343,12 @@ actor SMBClient { // MARK: - Helper Methods - /// Validates SMB URL format. - private func validateSMBURL(_ url: URL) throws { - guard url.scheme?.lowercased() == "smb" else { - throw MediaSourceError.unknown("URL must use smb:// scheme") - } - - guard url.host != nil else { - throw MediaSourceError.unknown("SMB URL missing host") - } - } - /// Clears cached SMB context for a specific source. /// Call this when source credentials change or connection fails. func clearCache(for source: MediaSource) { contextCache.removeValue(forKey: source.id) LoggingService.shared.logMediaSourcesDebug("Cleared SMB context cache for source: \(source.id)") } - - /// Clears all cached SMB contexts. - func clearAllCaches() { - contextCache.removeAll() - LoggingService.shared.logMediaSourcesDebug("Cleared all SMB context caches") - } } // MARK: - File Download diff --git a/Yattee/Services/MediaSources/WebDAVClient.swift b/Yattee/Services/MediaSources/WebDAVClient.swift index 4307aab6..b07f299a 100644 --- a/Yattee/Services/MediaSources/WebDAVClient.swift +++ b/Yattee/Services/MediaSources/WebDAVClient.swift @@ -167,7 +167,6 @@ private final class WebDAVResponseParser: NSObject, XMLParserDelegate { private var files: [MediaFile] = [] private var currentResponse: ResponseData? - private var currentElement: String = "" private var currentText: String = "" // Temporary storage for current response properties @@ -209,7 +208,6 @@ private final class WebDAVResponseParser: NSObject, XMLParserDelegate { qualifiedName: String?, attributes: [String: String] ) { - currentElement = elementName currentText = "" if elementName == "response" { diff --git a/Yattee/Services/MediaSources/WebDAVClientFactory.swift b/Yattee/Services/MediaSources/WebDAVClientFactory.swift index db08678c..09bd9eee 100644 --- a/Yattee/Services/MediaSources/WebDAVClientFactory.swift +++ b/Yattee/Services/MediaSources/WebDAVClientFactory.swift @@ -22,12 +22,4 @@ final class WebDAVClientFactory: Sendable { let session = sessionFactory.session(allowInvalidCertificates: source.allowInvalidCertificates) return WebDAVClient(session: session) } - - /// Creates a WebDAVClient with explicit SSL settings. - /// - Parameter allowInvalidCertificates: Whether to bypass SSL certificate validation. - /// - Returns: A WebDAVClient with the specified SSL settings. - func createClient(allowInvalidCertificates: Bool) -> WebDAVClient { - let session = sessionFactory.session(allowInvalidCertificates: allowInvalidCertificates) - return WebDAVClient(session: session) - } } diff --git a/Yattee/Services/Navigation/NavigationCoordinator.swift b/Yattee/Services/Navigation/NavigationCoordinator.swift index d8d1f3e6..76fdd094 100644 --- a/Yattee/Services/Navigation/NavigationCoordinator.swift +++ b/Yattee/Services/Navigation/NavigationCoordinator.swift @@ -288,19 +288,6 @@ final class NavigationCoordinator { } } - /// Scrolls the player into view (uncovered position). - func scrollPlayerIntoView() { - isPlayerScrollAnimating = true - scrollPlayerIntoViewTrigger += 1 - } - - /// Waits until player scroll animation completes. - func waitForPlayerScrollAnimation() async { - while isPlayerScrollAnimating { - try? await Task.sleep(for: .milliseconds(50)) - } - } - // MARK: - Convenience Navigation /// Navigate to the subscriptions tab/feed. @@ -322,13 +309,6 @@ final class NavigationCoordinator { navigate(to: destination) } - /// Navigate to a video by its ID string. - /// Creates a VideoID assuming YouTube source. - func navigateToVideo(videoID: String) { - let id = VideoID(source: .global(provider: ContentSource.youtubeProvider), videoID: videoID) - navigate(to: .video(.id(id))) - } - /// Navigate to a video's channel/source. /// For media source videos (SMB, WebDAV, local), navigates to the parent directory. /// For extracted videos, navigates to the external channel URL. diff --git a/Yattee/Services/Navigation/SidebarManager.swift b/Yattee/Services/Navigation/SidebarManager.swift index 04b3ea06..c777a3fc 100644 --- a/Yattee/Services/Navigation/SidebarManager.swift +++ b/Yattee/Services/Navigation/SidebarManager.swift @@ -244,7 +244,7 @@ final class SidebarManager { private func loadPlaylists() { guard let dataManager else { return } - // Cache playlists for use in videoCount(for:) + // Cache playlists for use in thumbnailURL(for:) cachedPlaylists = dataManager.playlists() let sortOrder = settingsManager?.sidebarPlaylistSort ?? .alphabetical let limitEnabled = settingsManager?.sidebarPlaylistsLimitEnabled ?? false @@ -424,20 +424,6 @@ final class SidebarManager { // MARK: - Playlist Data Access - /// Returns video count for a playlist sidebar item. - /// Uses cached playlists to avoid repeated DB queries during layout. - func videoCount(for item: SidebarItem) -> Int { - 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? { diff --git a/Yattee/Services/Networking/HTTPClientFactory.swift b/Yattee/Services/Networking/HTTPClientFactory.swift index 3d5f34b9..ea06d7dd 100644 --- a/Yattee/Services/Networking/HTTPClientFactory.swift +++ b/Yattee/Services/Networking/HTTPClientFactory.swift @@ -66,12 +66,6 @@ final class HTTPClientFactory: Sendable { makeClient(session: sessionFactory.session(allowInvalidCertificates: allowInvalidCertificates)) } - /// Creates an HTTPClient with low network priority for background/prefetch work. - /// - Returns: An HTTPClient configured with `.background` network service type. - func createLowPriorityClient() -> HTTPClient { - makeClient(session: sessionFactory.lowPrioritySession()) - } - /// Builds the client with the current User-Agent baked in. /// /// Passed through the initializer rather than set afterwards: these clients diff --git a/Yattee/Services/Player/BackendSwitcher.swift b/Yattee/Services/Player/BackendSwitcher.swift index 36918d13..73c26d7c 100644 --- a/Yattee/Services/Player/BackendSwitcher.swift +++ b/Yattee/Services/Player/BackendSwitcher.swift @@ -7,63 +7,11 @@ import Foundation -#if canImport(UIKit) -import UIKit -#elseif canImport(AppKit) -import AppKit -#endif - -// MARK: - Switch Animation - -/// Animation style for backend switching. -enum BackendSwitchAnimation: Sendable { - case instant // No animation, immediate swap - case crossfade // Crossfade between views - case slide // Slide transition - - var duration: TimeInterval { - switch self { - case .instant: return 0 - case .crossfade: return 0.3 - case .slide: return 0.4 - } - } -} - -// MARK: - Switch Result - -/// Result of a backend switch operation. -struct BackendSwitchResult: Sendable { - let success: Bool - let sourceBackend: PlayerBackendType - let targetBackend: PlayerBackendType - let timeDrift: TimeInterval // Difference between expected and actual time after switch - let switchDuration: TimeInterval // How long the switch took -} - -// MARK: - Backend Switcher Delegate - -/// Delegate for switch progress callbacks. -@MainActor -protocol BackendSwitcherDelegate: AnyObject { - func switcherWillBeginSwitch(from source: PlayerBackendType, to target: PlayerBackendType) - func switcherDidPrepareTarget(_ switcher: BackendSwitcher) - func switcherDidCompleteSwitch(_ result: BackendSwitchResult) - func switcherDidFailSwitch(_ error: Error) -} - // MARK: - Backend Switcher /// Manages seamless hot-swapping between player backends. @MainActor final class BackendSwitcher { - // MARK: - Properties - - weak var delegate: BackendSwitcherDelegate? - - /// Whether a switch is currently in progress. - private(set) var isSwitching: Bool = false - // MARK: - Dependencies /// Factory for creating backend instances. @@ -76,362 +24,6 @@ final class BackendSwitcher { self.backendFactory = backendFactory self.settingsManager = settingsManager } - - // MARK: - Public Methods - - /// Switch from one backend to another during active playback. - /// - /// This method: - /// 1. Captures the current playback state from the source backend - /// 2. Selects a compatible stream for the target backend - /// 3. Initializes the target backend and loads the stream - /// 4. Seeks to the captured position - /// 5. Waits for the target to be ready - /// 6. Performs a smooth visual transition - /// 7. Resumes playback on the target backend - /// 8. Cleans up the source backend - /// - /// - Parameters: - /// - source: The currently active backend - /// - targetType: The type of backend to switch to - /// - streams: Available streams for the current video - /// - animation: Animation style for the transition - /// - Returns: The new active backend - /// - Throws: BackendError if the switch fails - func switchBackend( - from source: any PlayerBackend, - to targetType: PlayerBackendType, - streams: [Stream], - animation: BackendSwitchAnimation = .crossfade - ) async throws -> any PlayerBackend { - guard !isSwitching else { - throw BackendError.switchFailed("Switch already in progress") - } - - let startTime = Date() - isSwitching = true - - defer { isSwitching = false } - - LoggingService.shared.logPlayer("Backend switch starting", details: "From \(source.backendType.rawValue) to \(targetType.rawValue)") - delegate?.switcherWillBeginSwitch(from: source.backendType, to: targetType) - - // Step 1: Capture current state - let capturedState = source.captureState() - - // Step 2: Find compatible stream for target backend - guard let selection = selectStream(for: targetType, from: streams) else { - throw BackendError.switchFailed("No compatible stream found for \(targetType.displayName)") - } - let targetStream = selection.video - let targetAudioStream = selection.audio - - // Step 3: Prepare source for handoff (pause but keep state) - source.prepareForHandoff() - - // Step 4: Create and initialize target backend - let target = try backendFactory.createBackend(type: targetType) - - // Step 5: Load stream on target (without autoplay) - do { - let useEDL = settingsManager?.mpvUseEDLStreams ?? true - try await target.load(stream: targetStream, audioStream: targetAudioStream, autoplay: false, useEDL: useEDL) - } catch { - // Rollback: resume source backend - if capturedState.isPlaying { - source.play() - } - throw BackendError.switchFailed("Failed to load stream on target: \(error.localizedDescription)") - } - - delegate?.switcherDidPrepareTarget(self) - - // Step 6: Seek to captured position - if capturedState.currentTime > 0 { - await target.seek(to: capturedState.currentTime, showLoading: false) - } - - // Step 7: Restore other state (volume, rate, mute) - target.volume = capturedState.volume - target.isMuted = capturedState.isMuted - target.rate = capturedState.rate - - // Step 8: Perform visual transition - await performTransition( - from: source, - to: target, - animation: animation - ) - - // Step 9: Resume playback if was playing - if capturedState.isPlaying { - target.play() - } - - // Step 10: Stop source backend - source.stop() - - // Calculate result metrics - let switchDuration = Date().timeIntervalSince(startTime) - let timeDrift = abs(target.currentTime - capturedState.currentTime) - - let result = BackendSwitchResult( - success: true, - sourceBackend: source.backendType, - targetBackend: targetType, - timeDrift: timeDrift, - switchDuration: switchDuration - ) - - LoggingService.shared.logPlayer("Backend switch completed", details: "Duration: \(String(format: "%.2f", switchDuration))s, drift: \(String(format: "%.3f", timeDrift))s") - delegate?.switcherDidCompleteSwitch(result) - - return target - } - - /// Check if switching to a given backend type is possible. - func canSwitch(to targetType: PlayerBackendType, streams: [Stream]) -> Bool { - // Check if we have a compatible stream - selectStream(for: targetType, from: streams) != nil - } - - // MARK: - Private Methods - - /// Select the best compatible stream for a backend type. - private func selectStream(for backendType: PlayerBackendType, from streams: [Stream]) -> (video: Stream, audio: Stream?)? { - let supportedFormats = backendType.supportedFormats - let preferredQuality = settingsManager?.preferredQuality ?? .auto - - // Separate streams by type - let videoOnlyStreams = streams.filter { stream in - guard !stream.isAudioOnly && stream.isVideoOnly else { return false } - let format = StreamFormat.detect(from: stream) - return supportedFormats.contains(format) - } - - let muxedStreams = streams.filter { stream in - let format = StreamFormat.detect(from: stream) - guard supportedFormats.contains(format) else { return false } - return stream.isMuxed || format == .hls || format == .dash - } - - let audioStreams = streams.filter { $0.isAudioOnly } - - // Get the maximum resolution based on user's quality preference - let maxResolution = preferredQuality.maxResolution - - // For live streams, always prefer HLS/DASH (designed for live streaming) - let isLiveStream = streams.contains(where: { $0.isLive }) - if isLiveStream { - if let hlsStream = muxedStreams.first(where: { StreamFormat.detect(from: $0) == .hls }) { - return (hlsStream, nil) - } - if let dashStream = muxedStreams.first(where: { StreamFormat.detect(from: $0) == .dash }) { - return (dashStream, nil) - } - } - - // Note: For non-live videos, we prefer progressive formats (MP4/WebM) over HLS/DASH - // because they typically offer better quality. HLS/DASH are only used as last resort. - - // Try to find the best video-only stream + audio - if !videoOnlyStreams.isEmpty && !audioStreams.isEmpty { - let filteredVideoStreams: [Stream] - if let maxRes = maxResolution { - filteredVideoStreams = videoOnlyStreams.filter { stream in - guard let resolution = stream.resolution else { return true } - return resolution <= maxRes - } - } else { - filteredVideoStreams = videoOnlyStreams - } - - // Sort by resolution first, then by codec quality (AV1 > VP9 > H.264) - let sortedVideo = filteredVideoStreams.sorted { s1, s2 in - let res1 = s1.resolution ?? .p360 - let res2 = s2.resolution ?? .p360 - if res1 != res2 { - return res1 > res2 - } - // Same resolution - prefer better codec - return videoCodecPriority(s1.videoCodec) > videoCodecPriority(s2.videoCodec) - } - - if let bestVideo = sortedVideo.first { - // Select best audio stream based on preferred language, codec, and bitrate - let preferredAudioLanguage = settingsManager?.preferredAudioLanguage - let bestAudio = audioStreams - .sorted { stream1, stream2 in - // First priority: preferred language or original audio - if let preferred = preferredAudioLanguage { - // User selected a specific language - let lang1 = stream1.audioLanguage ?? "" - let lang2 = stream2.audioLanguage ?? "" - let matches1 = lang1.hasPrefix(preferred) - let matches2 = lang2.hasPrefix(preferred) - if matches1 != matches2 { return matches1 } - } else { - // No preference set - prefer original audio track - if stream1.isOriginalAudio != stream2.isOriginalAudio { - return stream1.isOriginalAudio - } - } - - // Second priority: prefer Opus > AAC for MPV (better quality/compression) - let codecPriority1 = audioCodecPriority(stream1.audioCodec) - let codecPriority2 = audioCodecPriority(stream2.audioCodec) - if codecPriority1 != codecPriority2 { - return codecPriority1 > codecPriority2 - } - - // Third priority: higher bitrate - return (stream1.bitrate ?? 0) > (stream2.bitrate ?? 0) - } - .first - - if let audio = bestAudio { - return (bestVideo, audio) - } - } - } - - // Fallback to muxed streams - prefer progressive formats over HLS/DASH for non-live content - let filteredMuxed: [Stream] - if let maxRes = maxResolution { - filteredMuxed = muxedStreams.filter { stream in - guard let resolution = stream.resolution else { return true } - return resolution <= maxRes - } - } else { - filteredMuxed = muxedStreams - } - - // Sort: prefer progressive formats, then HLS, and only fall back to DASH as a last - // resort (DASH should never be chosen when any other format is available). - let sortedMuxed = filteredMuxed.sorted { s1, s2 in - let rank1 = muxedFormatRank(StreamFormat.detect(from: s1)) - let rank2 = muxedFormatRank(StreamFormat.detect(from: s2)) - if rank1 != rank2 { - return rank1 < rank2 - } - return (s1.resolution ?? .p360) > (s2.resolution ?? .p360) - } - - if let bestMuxed = sortedMuxed.first { - return (bestMuxed, nil) - } - - // Last resort: any muxed stream (HLS/DASH will be selected here if nothing else available) - if let anyMuxed = muxedStreams.sorted(by: { ($0.resolution ?? .p360) > ($1.resolution ?? .p360) }).first { - return (anyMuxed, nil) - } - - return nil - } - - /// Ranks muxed formats for fallback selection (lower = preferred). - /// Progressive formats win, then HLS; DASH is the last resort and is only - /// selected when it is the only format available. - private func muxedFormatRank(_ format: StreamFormat) -> Int { - switch format { - case .dash: return 2 - case .hls: return 1 - default: return 0 // progressive - } - } - - /// Returns codec priority for video streams (higher = better for MPV). - /// AV1 > VP9 > H.264/AVC - private func videoCodecPriority(_ codec: String?) -> Int { - guard let codec = codec?.lowercased() else { return 0 } - if codec.contains("av1") || codec.contains("av01") { - return 3 // Best compression, modern codec - } else if codec.contains("vp9") || codec.contains("vp09") { - return 2 // Good compression, widely supported - } else if codec.contains("avc") || codec.contains("h264") || codec.contains("h.264") { - return 1 // Most compatible, less efficient - } - return 0 - } - - /// Returns codec priority for audio streams (higher = better for MPV). - /// Opus > AAC - private func audioCodecPriority(_ codec: String?) -> Int { - guard let codec = codec?.lowercased() else { return 0 } - if codec.contains("opus") { - return 2 // Best quality/compression ratio - } else if codec.contains("aac") || codec.contains("mp4a") { - return 1 // Good compatibility - } - return 0 - } - - /// Perform visual transition between backends. - private func performTransition( - from source: any PlayerBackend, - to target: any PlayerBackend, - animation: BackendSwitchAnimation - ) async { - guard animation != .instant else { return } - - #if canImport(UIKit) - guard let sourceView = source.playerView, - let targetView = target.playerView, - let containerView = sourceView.superview else { - return - } - - // Add target view behind source - targetView.frame = containerView.bounds - targetView.alpha = 0 - containerView.insertSubview(targetView, belowSubview: sourceView) - - // Animate transition - await withCheckedContinuation { (continuation: CheckedContinuation) in - UIView.animate(withDuration: animation.duration, animations: { - switch animation { - case .crossfade: - sourceView.alpha = 0 - targetView.alpha = 1 - - case .slide: - sourceView.transform = CGAffineTransform(translationX: -sourceView.bounds.width, y: 0) - targetView.alpha = 1 - - case .instant: - break - } - }, completion: { _ in - sourceView.removeFromSuperview() - continuation.resume() - }) - } - - #elseif canImport(AppKit) - guard let sourceView = source.playerView, - let targetView = target.playerView, - let containerView = sourceView.superview else { - return - } - - // Add target view - targetView.frame = containerView.bounds - targetView.alphaValue = 0 - containerView.addSubview(targetView, positioned: .below, relativeTo: sourceView) - - // Animate transition - await withCheckedContinuation { (continuation: CheckedContinuation) in - NSAnimationContext.runAnimationGroup({ context in - context.duration = animation.duration - sourceView.animator().alphaValue = 0 - targetView.animator().alphaValue = 1 - }, completionHandler: { - sourceView.removeFromSuperview() - continuation.resume() - }) - } - #endif - } } // MARK: - Backend Factory diff --git a/Yattee/Services/Player/DeArrowAPI.swift b/Yattee/Services/Player/DeArrowAPI.swift index 181cf061..3a1e857a 100644 --- a/Yattee/Services/Player/DeArrowAPI.swift +++ b/Yattee/Services/Player/DeArrowAPI.swift @@ -138,18 +138,6 @@ actor DeArrowAPI { } } - /// Returns the current thumbnail base URL. - nonisolated func currentThumbnailBaseURL() -> URL { - // Note: This returns the default URL when called from nonisolated context. - // For dynamic URL access, use the async version. - Self.defaultThumbnailURL - } - - /// Returns the current thumbnail base URL (async version for isolation). - func getThumbnailBaseURL() -> URL { - thumbnailBaseURL - } - /// Fetches branding data for a YouTube video. /// - Parameter videoID: The YouTube video ID. /// - Returns: The branding data, or nil if not available. @@ -220,19 +208,6 @@ actor DeArrowAPI { return components.url! } - /// Generates a thumbnail URL using the default thumbnail base URL. - /// Use this when you need a URL synchronously without actor isolation. - nonisolated static func defaultThumbnailURL(for videoID: String, timestamp: Double? = nil) -> URL { - var components = URLComponents(url: defaultThumbnailURL, resolvingAgainstBaseURL: false)! - components.path = "/api/v1/getThumbnail" - var queryItems = [URLQueryItem(name: "videoID", value: videoID)] - if let timestamp { - queryItems.append(URLQueryItem(name: "time", value: String(format: "%.2f", timestamp))) - } - components.queryItems = queryItems - return components.url! - } - /// Result of fetching a thumbnail with timestamp verification. struct ThumbnailFetchResult: Sendable { let imageData: Data? diff --git a/Yattee/Services/Player/MPV/MPVClient.swift b/Yattee/Services/Player/MPV/MPVClient.swift index 47e1e52b..a01b9a22 100644 --- a/Yattee/Services/Player/MPV/MPVClient.swift +++ b/Yattee/Services/Player/MPV/MPVClient.swift @@ -653,16 +653,6 @@ final class MPVClient: @unchecked Sendable { observeProperty("sid", format: MPV_FORMAT_STRING) } - // MARK: - Options - - /// Set an MPV option (must be called before initialize or after load). - func setOption(_ name: String, _ value: String) { - mpvQueue.async { [weak self] in - guard let mpv = self?.mpv else { return } - mpv_set_option_string(mpv, name, value) - } - } - // MARK: - Playback Control /// Load a file/URL for playback. @@ -780,39 +770,14 @@ final class MPVClient: @unchecked Sendable { command(["stop"]) } - /// Seek to a specific time in seconds. - func seek(to time: Double, mode: String = "absolute") { - command(["seek", String(time), mode]) - } - /// Seek to a specific time in seconds asynchronously (non-blocking). /// Use this to avoid blocking the calling thread during seeks. func seekAsync(to time: Double, mode: String = "absolute") { commandAsync(["seek", String(time), mode]) } - /// Seek by a relative offset in seconds. - func seekRelative(_ offset: Double) { - command(["seek", String(offset), "relative"]) - } - - /// Seek by a relative offset in seconds asynchronously (non-blocking). - func seekRelativeAsync(_ offset: Double) { - commandAsync(["seek", String(offset), "relative"]) - } - // MARK: - Subtitles - /// Add an external subtitle file. - /// - Parameters: - /// - url: The subtitle URL (VTT, SRT, etc.) - /// - select: Whether to automatically select this subtitle - func addSubtitle(_ url: URL, select: Bool = true) { - // sub-add [ [ [<lang>]]] - let flags = select ? "select" : "auto" - command(["sub-add", url.absoluteString, flags]) - } - /// Add an external subtitle file asynchronously (does not block caller). /// Use this for remote URLs to avoid blocking the UI during download. /// - Parameters: @@ -828,16 +793,6 @@ final class MPVClient: @unchecked Sendable { setProperty("sid", "no") } - /// Enable subtitles by selecting the first track (if any). - func enableSubtitles() { - setProperty("sid", "auto") - } - - /// Remove all external subtitle tracks. - func removeAllSubtitles() { - command(["sub-remove"]) - } - /// Remove all external subtitle tracks asynchronously. func removeAllSubtitlesAsync() { commandAsync(["sub-remove"]) @@ -936,13 +891,6 @@ final class MPVClient: @unchecked Sendable { } } - /// Set a property value synchronously. Use when timing is critical. - func setPropertySync(_ name: String, _ value: Any) { - mpvQueue.sync { [weak self] in - self?.setPropertyUnsafe(name, value) - } - } - /// Internal property setter (must be called on mpvQueue). private func setPropertyUnsafe(_ name: String, _ value: Any) { guard let mpv, !isDestroyed else { return } @@ -1009,116 +957,6 @@ final class MPVClient: @unchecked Sendable { } } - /// Get a double property value. - /// **⚠️ BLOCKS CALLING THREAD** - Use getDoubleAsync() from @MainActor code. - func getDouble(_ name: String) -> Double? { - mpvQueue.sync { - guard let mpv, !isDestroyed else { return nil } - - var value: Double = 0 - let result = mpv_get_property(mpv, name, MPV_FORMAT_DOUBLE, &value) - return result >= 0 ? value : nil - } - } - - /// Get a boolean property value. - /// **⚠️ BLOCKS CALLING THREAD** - Use getFlagAsync() from @MainActor code. - func getFlag(_ name: String) -> Bool? { - mpvQueue.sync { - guard let mpv, !isDestroyed else { return nil } - - var value: Int32 = 0 - let result = mpv_get_property(mpv, name, MPV_FORMAT_FLAG, &value) - return result >= 0 ? (value != 0) : nil - } - } - - /// Get a string property value. - /// **⚠️ BLOCKS CALLING THREAD** - Use getStringAsync() from @MainActor code. - func getString(_ name: String) -> String? { - mpvQueue.sync { - guard let mpv, !isDestroyed else { return nil } - - guard let cString = mpv_get_property_string(mpv, name) else { - return nil - } - let string = String(cString: cString) - mpv_free(cString) - return string - } - } - - /// Get an integer property value. - /// **⚠️ BLOCKS CALLING THREAD** - Use getIntAsync() from @MainActor code. - func getInt(_ name: String) -> Int? { - mpvQueue.sync { - guard let mpv, !isDestroyed else { return nil } - - var value: Int64 = 0 - let result = mpv_get_property(mpv, name, MPV_FORMAT_INT64, &value) - return result >= 0 ? Int(value) : nil - } - } - - /// Get the demuxer cache state. - /// **⚠️ BLOCKS CALLING THREAD** - Use getCacheStateAsync() from @MainActor code. - func getCacheState() -> MPVCacheState? { - mpvQueue.sync { - guard let mpv, !isDestroyed else { return nil } - - var node = mpv_node() - let result = mpv_get_property(mpv, "demuxer-cache-state", MPV_FORMAT_NODE, &node) - guard result >= 0 else { - return nil - } - - defer { - mpv_free_node_contents(&node) - } - - return parseCacheStateSync(node) - } - } - - /// Get MPV version and build information. - /// **⚠️ BLOCKS CALLING THREAD** - Use getVersionInfoAsync() from @MainActor code. - func getVersionInfo() -> MPVVersionInfo? { - mpvQueue.sync { - guard let mpv, !isDestroyed else { return nil } - - // Get mpv-version property - var mpvVersion: String? - if let cString = mpv_get_property_string(mpv, "mpv-version") { - mpvVersion = String(cString: cString) - mpv_free(cString) - } - - // Get ffmpeg-version property - var ffmpegVersion: String? - if let cString = mpv_get_property_string(mpv, "ffmpeg-version") { - ffmpegVersion = String(cString: cString) - mpv_free(cString) - } - - // Get mpv-configuration property (compilation flags) - var configuration: String? - if let cString = mpv_get_property_string(mpv, "mpv-configuration") { - configuration = String(cString: cString) - mpv_free(cString) - } - - // Get libmpv API version - let apiVersion = mpv_client_api_version() - - return MPVVersionInfo( - mpvVersion: mpvVersion, - ffmpegVersion: ffmpegVersion, - configuration: configuration, - apiVersion: UInt(apiVersion) - ) - } - } - /// Raw debug properties fetched in a single sync block to minimize lock contention. struct DebugProperties { var videoCodec: String? @@ -1258,89 +1096,6 @@ final class MPVClient: @unchecked Sendable { } } - /// Get a double property value asynchronously (non-blocking). - /// Use this from MainActor code instead of getDouble() to avoid blocking the UI. - /// - Parameter name: The property name (e.g., "time-pos", "duration") - /// - Returns: The double value, or nil if the property doesn't exist or MPV is destroyed - func getDoubleAsync(_ name: String) async -> Double? { - await withCheckedContinuation { continuation in - mpvQueue.async { [weak self] in - guard let self, let mpv = self.mpv, !self.isDestroyed else { - continuation.resume(returning: nil) - return - } - var value: Double = 0 - let result = mpv_get_property(mpv, name, MPV_FORMAT_DOUBLE, &value) - continuation.resume(returning: result >= 0 ? value : nil) - } - } - } - - /// Get a string property value asynchronously (non-blocking). - /// Use this from MainActor code instead of getString() to avoid blocking the UI. - /// - Parameter name: The property name (e.g., "video-codec", "hwdec-current") - /// - Returns: The string value, or nil if the property doesn't exist or MPV is destroyed - func getStringAsync(_ name: String) async -> String? { - await withCheckedContinuation { continuation in - mpvQueue.async { [weak self] in - guard let self, let mpv = self.mpv, !self.isDestroyed else { - continuation.resume(returning: nil) - return - } - guard let cString = mpv_get_property_string(mpv, name) else { - continuation.resume(returning: nil) - return - } - let string = String(cString: cString) - mpv_free(cString) - continuation.resume(returning: string) - } - } - } - - /// Get an integer property value asynchronously (non-blocking). - /// Use this from MainActor code instead of getInt() to avoid blocking the UI. - /// - Parameter name: The property name (e.g., "width", "height") - /// - Returns: The integer value, or nil if the property doesn't exist or MPV is destroyed - func getIntAsync(_ name: String) async -> Int? { - await withCheckedContinuation { continuation in - mpvQueue.async { [weak self] in - guard let self, let mpv = self.mpv, !self.isDestroyed else { - continuation.resume(returning: nil) - return - } - var value: Int64 = 0 - let result = mpv_get_property(mpv, name, MPV_FORMAT_INT64, &value) - continuation.resume(returning: result >= 0 ? Int(value) : nil) - } - } - } - - /// Get the demuxer cache state asynchronously (non-blocking). - /// Use this from MainActor code instead of getCacheState() to avoid blocking the UI. - /// - Returns: The cache state information, or nil if unavailable or MPV is destroyed - func getCacheStateAsync() async -> MPVCacheState? { - await withCheckedContinuation { continuation in - mpvQueue.async { [weak self] in - guard let self, let mpv = self.mpv, !self.isDestroyed else { - continuation.resume(returning: nil) - return - } - - var node = mpv_node() - let result = mpv_get_property(mpv, "demuxer-cache-state", MPV_FORMAT_NODE, &node) - guard result >= 0 else { - continuation.resume(returning: nil) - return - } - - let cacheState = self.parseCacheStateSync(node) - mpv_free_node_contents(&node) - continuation.resume(returning: cacheState) - } - } - } - /// Get MPV version and build information asynchronously (non-blocking). /// Use this from MainActor code instead of getVersionInfo() to avoid blocking the UI. /// - Returns: The version information, or nil if unavailable or MPV is destroyed @@ -1858,12 +1613,6 @@ final class MPVClient: @unchecked Sendable { // MARK: - Render Context - /// Get the MPV handle for render context creation. - /// Must be called on mpvQueue. - var mpvHandle: OpaquePointer? { - mpvQueue.sync { mpv } - } - /// Create a render context for OpenGL rendering. /// - Parameter getProcAddress: Function to get OpenGL proc addresses /// - Returns: Whether creation succeeded diff --git a/Yattee/Services/Player/MPV/MPVOGLView.swift b/Yattee/Services/Player/MPV/MPVOGLView.swift index fb146f2a..38d462d8 100644 --- a/Yattee/Services/Player/MPV/MPVOGLView.swift +++ b/Yattee/Services/Player/MPV/MPVOGLView.swift @@ -57,11 +57,6 @@ final class MPVOGLView: NSView { /// Actual display link frame rate. var displayLinkActualFPS: Double = 60.0 - /// Current display link target frame rate (for debug overlay). - var displayLinkTargetFPS: Double { - displayLinkActualFPS - } - // MARK: - PiP Properties (forwarded to layer) /// Whether to capture frames for PiP. @@ -253,12 +248,6 @@ final class MPVOGLView: NSView { mpvClient?.reportSwap() } - /// Update display link for the current display. - func updateDisplayLink() { - // With CADisplayLink, we just need to update the refresh rate info - updateDisplayRefreshRate() - } - /// Update the cached display refresh rate. private func updateDisplayRefreshRate() { guard let screen = window?.screen else { diff --git a/Yattee/Services/Player/MPV/MPVOpenGLLayer.swift b/Yattee/Services/Player/MPV/MPVOpenGLLayer.swift index 54928e9a..97bda493 100644 --- a/Yattee/Services/Player/MPV/MPVOpenGLLayer.swift +++ b/Yattee/Services/Player/MPV/MPVOpenGLLayer.swift @@ -59,7 +59,6 @@ struct MPVRenderHealth { var draws: UInt64 var skips: UInt64 var droppedDraws: UInt64 - var lastDrawAge: TimeInterval? } // MARK: - MPVOpenGLLayer @@ -113,7 +112,6 @@ final class MPVOpenGLLayer: CAOpenGLLayer { private var successfulDrawCount: UInt64 = 0 private var skipRenderCount: UInt64 = 0 private var droppedDrawCount: UInt64 = 0 - private var lastSuccessfulDrawTime: CFTimeInterval? private var skipsSinceLastDraw: UInt64 = 0 private let renderHealthLock = NSLock() @@ -377,7 +375,6 @@ final class MPVOpenGLLayer: CAOpenGLLayer { renderHealthLock.withLock { successfulDrawCount += 1 skipsSinceLastDraw = 0 - lastSuccessfulDrawTime = CACurrentMediaTime() } // Clear the buffer @@ -699,8 +696,7 @@ final class MPVOpenGLLayer: CAOpenGLLayer { MPVRenderHealth( draws: successfulDrawCount, skips: skipRenderCount, - droppedDraws: droppedDrawCount, - lastDrawAge: lastSuccessfulDrawTime.map { CACurrentMediaTime() - $0 } + droppedDraws: droppedDrawCount ) } } diff --git a/Yattee/Services/Player/MPV/MPVPiPBridge.swift b/Yattee/Services/Player/MPV/MPVPiPBridge.swift index 3a6e84f6..68b7141b 100644 --- a/Yattee/Services/Player/MPV/MPVPiPBridge.swift +++ b/Yattee/Services/Player/MPV/MPVPiPBridge.swift @@ -67,9 +67,6 @@ final class MPVPiPBridge: NSObject { /// KVO observation for isPictureInPicturePossible private var pipPossibleObservation: NSKeyValueObservation? - /// Current PiP render size from AVPictureInPictureController - private var currentPiPRenderSize: CMVideoDimensions? - /// Current video aspect ratio (width / height) private var videoAspectRatio: CGFloat = 16.0 / 9.0 @@ -396,36 +393,6 @@ final class MPVPiPBridge: NSObject { } #endif - /// Move the sample buffer layer to a new container view. - /// This is needed when transitioning from fullscreen to PiP, - /// as the layer must be in a visible window hierarchy. - func moveLayer(to containerView: PlatformView) { - sampleBufferLayer.removeFromSuperlayer() - self.containerView = containerView - if let frame = sanitizedGeometry(containerView.bounds, from: "moveLayer(to:)") { - sampleBufferLayer.frame = frame - } - #if os(iOS) - containerView.layer.addSublayer(sampleBufferLayer) - #elseif os(macOS) - containerView.wantsLayer = true - containerView.layer?.addSublayer(sampleBufferLayer) - #endif - } - - /// Clean up and release resources. - func cleanup() { - NotificationCenter.default.removeObserver(self) - pipPossibleObservation?.invalidate() - pipPossibleObservation = nil - lastPixelBuffer = nil - pipController?.stopPictureInPicture() - pipController = nil - sampleBufferLayer.removeFromSuperlayer() - sampleBufferLayer.sampleBufferRenderer.flush(removingDisplayedImage: true, completionHandler: nil) - mpvBackend = nil - } - /// Flush the sample buffer to clear any displayed frame. /// Call this when stopping playback to prevent stale frames when reusing backend. func flushBuffer() { @@ -633,20 +600,6 @@ final class MPVPiPBridge: NSObject { notifyPiPPossibleState() } #endif - - /// Toggle Picture-in-Picture. - func togglePiP() { - if isPiPActive { - stopPiP() - } else { - startPiP() - } - } - - /// Invalidate and update the playback state in PiP window. - func invalidatePlaybackState() { - pipController?.invalidatePlaybackState() - } } // MARK: - AVPictureInPictureSampleBufferPlaybackDelegate @@ -720,7 +673,6 @@ extension MPVPiPBridge: AVPictureInPictureSampleBufferPlaybackDelegate { didTransitionToRenderSize newRenderSize: CMVideoDimensions ) { Task { @MainActor in - currentPiPRenderSize = newRenderSize onPiPRenderSizeChanged?(newRenderSize) LoggingService.shared.debug("MPVPiPBridge: PiP render size changed to \(newRenderSize.width)x\(newRenderSize.height)", category: .mpv) } @@ -734,8 +686,6 @@ extension MPVPiPBridge: AVPictureInPictureControllerDelegate { _ pictureInPictureController: AVPictureInPictureController ) { Task { @MainActor in - // Reset PiP render size - will be updated by didTransitionToRenderSize - currentPiPRenderSize = nil // Reset restore flag - will be set if user clicks restore button restoreWasRequested = false // Show the sample buffer layer when PiP starts diff --git a/Yattee/Services/Player/MPV/MPVRenderView.swift b/Yattee/Services/Player/MPV/MPVRenderView.swift index 6b0b0ced..2318c3f2 100644 --- a/Yattee/Services/Player/MPV/MPVRenderView.swift +++ b/Yattee/Services/Player/MPV/MPVRenderView.swift @@ -104,12 +104,8 @@ final class MPVRenderView: UIView { private var renderWidth: GLint = 0 private var renderHeight: GLint = 0 - /// Last known stable size (for throttling resize events) - private var lastStableSize: CGSize = .zero /// Pending resize work item (for debouncing) private var pendingResizeWorkItem: DispatchWorkItem? - /// Minimum time between framebuffer recreations - private let resizeDebounceInterval: TimeInterval = 0.1 /// Dedicated queue for OpenGL rendering (off main thread). private let renderQueue = DispatchQueue(label: "stream.yattee.mpv.render", qos: .userInteractive) @@ -507,7 +503,6 @@ final class MPVRenderView: UIView { // Always recreate framebuffer immediately to ensure video is properly sized // This is important during rotation animations - lastStableSize = bounds.size renderQueue.sync { destroyFramebuffer() createFramebuffer() @@ -565,7 +560,6 @@ final class MPVRenderView: UIView { // Create framebuffer only if view has valid bounds // (createFramebuffer makes the context current itself and unbinds on exit) - lastStableSize = bounds.size if bounds.size != .zero { renderQueue.sync { createFramebuffer() diff --git a/Yattee/Services/Player/MPV/MPVSoftwareRenderView.swift b/Yattee/Services/Player/MPV/MPVSoftwareRenderView.swift index 4924355e..1fd8362e 100644 --- a/Yattee/Services/Player/MPV/MPVSoftwareRenderView.swift +++ b/Yattee/Services/Player/MPV/MPVSoftwareRenderView.swift @@ -77,8 +77,6 @@ final class MPVSoftwareRenderView: UIView { /// Dedicated queue for rendering operations private let renderQueue = DispatchQueue(label: "stream.yattee.mpv.software-render", qos: .userInitiated) - /// Lock for buffer recreation - private let bufferLock = NSLock() private var isRecreatingBuffer = false // MARK: - Initialization diff --git a/Yattee/Services/Player/MPVBackend.swift b/Yattee/Services/Player/MPVBackend.swift index 94bc0979..6e08f32d 100644 --- a/Yattee/Services/Player/MPVBackend.swift +++ b/Yattee/Services/Player/MPVBackend.swift @@ -60,19 +60,6 @@ final class MPVBackend: PlayerBackend { } } - /// Panscan value (0.0 = aspect fit with black bars, 1.0 = aspect fill/crop) - var panscan: Double { - get { _panscan } - set { - _panscan = max(0, min(1, newValue)) - mpvClient?.setProperty("panscan", _panscan) - } - } - - var supportedFormats: Set<StreamFormat> { - Set(StreamFormat.allCases) // MPV supports all formats - } - // MARK: - Public Properties /// MPV version and build information (available after initialization). @@ -92,15 +79,9 @@ final class MPVBackend: PlayerBackend { private var _rate: Float = 1.0 private var _volume: Float = 1.0 private var _isMuted: Bool = false - private var _panscan: Double = 0.0 private var currentStream: Stream? - private var currentAudioStream: Stream? private var currentCaption: Caption? - private var pendingAutoplay: Bool = false - - /// Tracks reported by mpv for the loaded file (embedded and external). - private(set) var currentTracks: [MPVTrack] = [] // Retry mechanism: 4 attempts with increasing timeouts and delays // Attempt 1: 3s timeout, 1s delay | Attempt 2: 3s timeout, 3s delay @@ -447,9 +428,6 @@ final class MPVBackend: PlayerBackend { // Store for potential retries currentStream = stream - currentAudioStream = audioStream - pendingAutoplay = autoplay - pendingUseEDL = actualUseEDL retryCount = 0 // Reset retry state in UI @@ -459,8 +437,6 @@ final class MPVBackend: PlayerBackend { try await loadWithRetry(stream: stream, audioStream: audioStream, autoplay: autoplay, useEDL: actualUseEDL, loadingID: loadingID) } - private var pendingUseEDL: Bool = true - private func loadWithRetry(stream: Stream, audioStream: Stream?, autoplay: Bool, useEDL: Bool, loadingID: UUID) async throws { // Check if this load operation was cancelled (a new load started) guard currentLoadingID == loadingID else { @@ -495,7 +471,6 @@ final class MPVBackend: PlayerBackend { mpvClient?.removeAllSubtitlesAsync() // Clear stale track info; the observer repopulates it once the new file loads - currentTracks = [] delegate?.backend(self, didUpdateTracks: []) // Reset first-frame tracking for new content @@ -618,8 +593,6 @@ final class MPVBackend: PlayerBackend { private func resetRetryState() { retryCount = 0 currentStream = nil - currentAudioStream = nil - pendingAutoplay = false delegate?.backend(self, didUpdateRetryState: 0, maxRetries: maxRetries, isRetrying: false, exhausted: false) } @@ -863,43 +836,6 @@ final class MPVBackend: PlayerBackend { private var pendingInitialSeek = false - // MARK: - Backend Switching - - func captureState() -> BackendState { - BackendState( - currentTime: currentTime, - duration: duration, - rate: _rate, - volume: _volume, - isMuted: _isMuted, - isPlaying: isPlaying - ) - } - - func restore(state: BackendState) async { - _rate = state.rate - _volume = state.volume - _isMuted = state.isMuted - - // Apply to MPV - mpvClient?.setProperty("speed", Double(state.rate)) - mpvClient?.setProperty("volume", Double(state.volume * 100)) - mpvClient?.setProperty("mute", state.isMuted) - - if state.currentTime > 0 { - await seek(to: state.currentTime) - } - - if state.isPlaying { - play() - } - } - - func prepareForHandoff() { - mpvClient?.pause() - isPlaying = false - } - // MARK: - Subtitles /// Load and display a caption/subtitle track. @@ -922,11 +858,6 @@ final class MPVBackend: PlayerBackend { currentCaption = caption } - /// Get the currently loaded caption. - func getCurrentCaption() -> Caption? { - currentCaption - } - // MARK: - Embedded Tracks /// Select an embedded audio track by mpv track id (live, no reload). @@ -955,16 +886,6 @@ final class MPVBackend: PlayerBackend { mpvClient?.updateAudioDelay(milliseconds: milliseconds) } - /// Get the actual video track dimensions from MPV. - /// Returns (width, height) or nil if not available. - func getVideoSize() -> (width: Int, height: Int)? { - // Use cached values (updated via property observation) to avoid sync fetch - guard videoWidth > 0, videoHeight > 0 else { - return nil - } - return (videoWidth, videoHeight) - } - /// Get debug statistics from MPV for the debug overlay. /// Uses batch fetch to minimize lock contention (single sync block instead of ~25 separate calls). func getDebugStats() -> MPVDebugStats { @@ -1254,22 +1175,6 @@ final class MPVBackend: PlayerBackend { pipBridge.enqueueFrame(pixelBuffer, presentationTime: presentationTime) } - /// Clean up PiP resources. - func cleanupPiP() { - _playerView?.captureFramesForPiP = false - _playerView?.onFrameReady = nil - pipBridge?.cleanup() - pipBridge = nil - isPiPSetUp = false - } - - /// Move the PiP layer to a new container view. - /// Call this before starting PiP from fullscreen to ensure the layer - /// is in the main window's view hierarchy. - func movePiPLayer(to containerView: UIView) { - pipBridge?.moveLayer(to: containerView) - } - #elseif os(macOS) // MARK: - Picture-in-Picture (macOS) @@ -1437,20 +1342,6 @@ final class MPVBackend: PlayerBackend { pipBridge.enqueueFrame(pixelBuffer, presentationTime: presentationTime) } - - /// Clean up PiP resources. - func cleanupPiP() { - _playerView?.captureFramesForPiP = false - _playerView?.onFrameReady = nil - pipBridge?.cleanup() - pipBridge = nil - isPiPSetUp = false - } - - /// Move the PiP layer to a new container view. - func movePiPLayer(to containerView: NSView) { - pipBridge?.moveLayer(to: containerView) - } #endif // MARK: - Private Methods @@ -1609,7 +1500,6 @@ extension MPVBackend: MPVClientDelegate { nonisolated func mpvClient(_ client: MPVClient, didUpdateTrackList tracks: [MPVTrack]) { Task { @MainActor [weak self] in guard let self else { return } - self.currentTracks = tracks self.delegate?.backend(self, didUpdateTracks: tracks) } } diff --git a/Yattee/Services/Player/NowPlayingService.swift b/Yattee/Services/Player/NowPlayingService.swift index 975edd17..0b816c0d 100644 --- a/Yattee/Services/Player/NowPlayingService.swift +++ b/Yattee/Services/Player/NowPlayingService.swift @@ -37,7 +37,6 @@ final class NowPlayingService { weak var playerService: PlayerService? weak var deArrowBrandingProvider: DeArrowBrandingProvider? - weak var settingsManager: SettingsManager? weak var playerControlsLayoutService: PlayerControlsLayoutService? // MARK: - Initialization diff --git a/Yattee/Services/Player/PlayerBackend.swift b/Yattee/Services/Player/PlayerBackend.swift index d83e1180..ccb653a4 100644 --- a/Yattee/Services/Player/PlayerBackend.swift +++ b/Yattee/Services/Player/PlayerBackend.swift @@ -9,40 +9,6 @@ import Foundation import AVFoundation import SwiftUI -#if os(iOS) || os(tvOS) -import UIKit -#elseif os(macOS) -import AppKit -#endif - -// MARK: - Backend State - -/// Captured state for backend switching. -struct BackendState: Sendable { - let currentTime: TimeInterval - let duration: TimeInterval - let rate: Float - let volume: Float - let isMuted: Bool - let isPlaying: Bool - - init( - currentTime: TimeInterval = 0, - duration: TimeInterval = 0, - rate: Float = 1.0, - volume: Float = 1.0, - isMuted: Bool = false, - isPlaying: Bool = false - ) { - self.currentTime = currentTime - self.duration = duration - self.rate = rate - self.volume = volume - self.isMuted = isMuted - self.isPlaying = isPlaying - } -} - // MARK: - Stream Format /// Stream format categories for backend compatibility. @@ -57,12 +23,6 @@ enum StreamFormat: String, CaseIterable, Sendable { case audioOpus // Opus audio case audioMP3 // MP3 audio - /// Whether MPV can play this format. - var isMPVCompatible: Bool { - // MPV supports all formats - true - } - /// Detect format from stream properties. static func detect(from stream: Stream) -> StreamFormat { let format = stream.format.lowercased() @@ -148,7 +108,6 @@ protocol PlayerBackendDelegate: AnyObject { func backend(_ backend: any PlayerBackend, didChangeState state: PlaybackState) func backend(_ backend: any PlayerBackend, didUpdateBufferedTime time: TimeInterval) func backend(_ backend: any PlayerBackend, didUpdateBufferProgress progress: Int) - func backend(_ backend: any PlayerBackend, didEncounterError error: Error) func backend(_ backend: any PlayerBackend, didUpdateVideoSize width: Int, height: Int) func backend(_ backend: any PlayerBackend, didUpdateRetryState currentRetry: Int, maxRetries: Int, isRetrying: Bool, exhausted: Bool) func backend(_ backend: any PlayerBackend, didRequestStreamRefresh atTime: TimeInterval?) @@ -169,21 +128,6 @@ protocol PlayerBackend: AnyObject { /// Delegate for callbacks. var delegate: PlayerBackendDelegate? { get set } - /// Current playback time in seconds. - var currentTime: TimeInterval { get } - - /// Total duration in seconds. - var duration: TimeInterval { get } - - /// Buffered time in seconds. - var bufferedTime: TimeInterval { get } - - /// Whether the backend is ready to play. - var isReady: Bool { get } - - /// Whether playback is currently active. - var isPlaying: Bool { get } - /// Current playback rate (1.0 = normal). var rate: Float { get set } @@ -193,9 +137,6 @@ protocol PlayerBackend: AnyObject { /// Whether audio is muted. var isMuted: Bool { get set } - /// Formats this backend can play. - var supportedFormats: Set<StreamFormat> { get } - // MARK: - Playback Control /// Load a stream for playback. @@ -226,27 +167,6 @@ protocol PlayerBackend: AnyObject { /// preventing a flash of the video at position 0 before jumping to resume position. func prepareForInitialSeek() - // MARK: - Backend Switching - - /// Capture current state for switching. - func captureState() -> BackendState - - /// Restore state after switching. - func restore(state: BackendState) async - - /// Prepare for handoff to another backend. - func prepareForHandoff() - - // MARK: - View - - #if os(iOS) || os(tvOS) - /// The view displaying video content (UIKit). - var playerView: UIView? { get } - #elseif os(macOS) - /// The view displaying video content (AppKit). - var playerView: NSView? { get } - #endif - // MARK: - Background Playback /// Handle scene phase changes for background playback. @@ -261,24 +181,6 @@ protocol PlayerBackend: AnyObject { // MARK: - Default Implementations extension PlayerBackend { - /// Check if this backend can play a given stream. - func canPlay(stream: Stream) -> Bool { - let format = StreamFormat.detect(from: stream) - return supportedFormats.contains(format) - } - - /// Capture current state with all properties. - func captureState() -> BackendState { - BackendState( - currentTime: currentTime, - duration: duration, - rate: rate, - volume: volume, - isMuted: isMuted, - isPlaying: isPlaying - ) - } - /// Default implementation does nothing. func handleScenePhase(_ phase: ScenePhase, isPiPActive: Bool) { // No-op by default diff --git a/Yattee/Services/Player/PlayerService.swift b/Yattee/Services/Player/PlayerService.swift index 2556b922..7a49f4f7 100644 --- a/Yattee/Services/Player/PlayerService.swift +++ b/Yattee/Services/Player/PlayerService.swift @@ -103,17 +103,11 @@ final class PlayerService { /// The current download being played, if any. private(set) var currentDownload: Download? - /// Whether we're currently playing downloaded content. - var isPlayingDownloadedContent: Bool { - currentDownload != nil - } - /// Whether online streams are currently being loaded. private(set) var isLoadingOnlineStreams: Bool = false // MARK: - Dependencies - private let httpClient: HTTPClient private let contentService: ContentService private let sponsorBlockAPI: SponsorBlockAPI private let returnYouTubeDislikeAPI: ReturnYouTubeDislikeAPI @@ -184,7 +178,6 @@ final class PlayerService { dataManager: DataManager, backendFactory: BackendFactory? = nil ) { - self.httpClient = httpClient self.contentService = contentService self.dataManager = dataManager self.sponsorBlockAPI = SponsorBlockAPI(httpClient: httpClient) @@ -227,22 +220,6 @@ final class PlayerService { // MARK: - Public Methods - /// Prepares a video for playback without loading streams. - /// Shows the video in the player with a play button overlay. - /// Call `play(video:)` to actually start loading and playing. - /// - Parameter video: The video to prepare - func prepare(video: Video) { - LoggingService.shared.logPlayer("Preparing video: \(video.id.id)") - - // Clear streams - availableStreams = [] - originalStreams = [] - - // Set video in state but keep idle (not loading) - state.setCurrentVideo(video, stream: nil) - state.setPlaybackState(.idle) - } - /// Plays a video with optional starting time. /// - Parameters: /// - video: The video to play @@ -1785,113 +1762,6 @@ final class PlayerService { // MARK: - Media Browser Stream Resolution - /// Resolves stream and captions for a media browser video on-demand. - /// - Parameters: - /// - video: The video to resolve - /// - context: Media browser context with source and folder files - /// - Returns: Tuple of (stream, captions) ready for playback - func resolveMediaBrowserStream( - for video: Video, - context: MediaBrowserQueueContext - ) async throws -> (stream: Stream, captions: [Caption]) { - // Find the MediaFile for this video in the folder - // videoID format: "sourceUUID:path" - guard let mediaFile = context.allFilesInFolder.first(where: { $0.id == video.id.videoID }) else { - throw MediaSourceError.pathNotFound("Video file not found in folder context") - } - - // Find matching subtitle files - let subtitleFiles = mediaFile.findMatchingSubtitles(in: context.allFilesInFolder) - - let source = context.source - let password = mediaSourcesManager?.password(for: source) - - switch source.type { - case .webdav: - // Get auth headers for WebDAV - var authHeaders: [String: String]? - if let webDAVClient { - authHeaders = await webDAVClient.authHeaders(for: source, password: password) - } - let stream = mediaFile.toStream(authHeaders: authHeaders) - // Subtitles can use remote URLs directly for WebDAV - return (stream, subtitleFiles) - - case .smb: - guard let smbClient else { - throw MediaSourceError.unknown("SMB client not available") - } - - let folderKey = source.id.uuidString + ":" + context.folderPath - - // Pre-download ALL subtitles in folder on first access. - // Must complete before MPV opens any SMB connection. - // For queued videos from the same folder, subtitles will already be cached. - if !preDownloadedSubtitleFolders.contains(folderKey) { - let allSubtitleFiles = context.allFilesInFolder.filter { $0.isSubtitle } - LoggingService.shared.logMediaSources("Pre-downloading \(allSubtitleFiles.count) subtitle(s) from folder: \(context.folderPath)") - - for subtitleFile in allSubtitleFiles { - do { - let localURL = try await smbClient.downloadSubtitleToTemp( - file: subtitleFile, - source: source, - password: password, - videoID: folderKey - ) - downloadedSubtitlesCache[subtitleFile.id] = localURL - LoggingService.shared.logMediaSources("Pre-downloaded subtitle: \(subtitleFile.name) → \(localURL.lastPathComponent)") - } catch { - LoggingService.shared.error( - "Failed to pre-download subtitle \(subtitleFile.name): \(error.localizedDescription)", - category: .general - ) - } - } - - preDownloadedSubtitleFolders.insert(folderKey) - - // Release app SMB context before MPV opens its connection. - // libsmbclient uses process-global talloc state that corrupts - // when two contexts access the same server concurrently. - await smbClient.clearCache(for: source) - LoggingService.shared.logMediaSources("Cleared SMB context cache before playback") - } - - // Construct video playback URL (pure string manipulation, no libsmbclient) - let playbackURL = try await smbClient.constructPlaybackURL( - for: mediaFile, - source: source, - password: password - ) - let stream = Stream(url: playbackURL, resolution: nil, format: mediaFile.fileExtension) - - // Build captions from pre-downloaded local files - var localCaptions: [Caption] = [] - for subtitle in subtitleFiles { - if let subtitleFile = context.allFilesInFolder.first(where: { $0.url == subtitle.url }), - let localURL = downloadedSubtitlesCache[subtitleFile.id] { - localCaptions.append(Caption( - label: subtitle.label, - languageCode: subtitle.languageCode, - url: localURL - )) - LoggingService.shared.info( - "Using pre-downloaded subtitle: \(subtitleFile.name)", - category: .general - ) - } - } - - return (stream, localCaptions) - - case .localFolder: - // Local folder - no auth needed, use file:// URLs directly - let stream = mediaFile.toStream(authHeaders: nil) - return (stream, subtitleFiles) - } - } - /// Resolves stream and subtitles for a media source video by fetching folder contents on-demand. /// Works for all media source types (WebDAV, SMB, local folders) from any playback source /// (Media Browser, Continue Watching, etc.). @@ -2688,7 +2558,6 @@ final class PlayerService { func setSettingsManager(_ manager: SettingsManager) { self.settingsManager = manager self.backendSwitcher.settingsManager = manager - self.nowPlayingService.settingsManager = manager // Reconfigure remote commands now that we have settings nowPlayingService.configureRemoteCommands() // Note: preferredBackendType is now computed directly from settingsManager @@ -3184,10 +3053,6 @@ extension PlayerService: PlayerBackendDelegate { } } - func backend(_ backend: any PlayerBackend, didEncounterError error: Error) { - delegate?.playerService(self, didEncounterError: error) - } - func backend(_ backend: any PlayerBackend, didUpdateVideoSize width: Int, height: Int) { guard width > 0, height > 0 else { return } let aspectRatio = Double(width) / Double(height) diff --git a/Yattee/Services/Player/PlayerState.swift b/Yattee/Services/Player/PlayerState.swift index 1af8d9a2..09e0ec87 100644 --- a/Yattee/Services/Player/PlayerState.swift +++ b/Yattee/Services/Player/PlayerState.swift @@ -49,12 +49,6 @@ struct RetryState: Equatable, Sendable { let exhausted: Bool static let idle = RetryState(currentRetry: 0, maxRetries: 5, isRetrying: false, exhausted: false) - - /// Text to display during retries (e.g., "Retrying... (1/5)"). - var displayText: String? { - guard isRetrying, currentRetry > 0 else { return nil } - return String(localized: "player.retry.status \(currentRetry) \(maxRetries)") - } } /// Represents a queued video for playback. @@ -442,12 +436,6 @@ final class PlayerState { /// nil when not buffering or when using AVPlayer backend. var bufferProgress: Int? - /// Whether the video is vertical (portrait orientation). - var isVerticalVideo: Bool { - guard let ratio = videoAspectRatio else { return false } - return ratio < 1.0 - } - /// Display aspect ratio for UI - falls back to 16:9 if unknown. var displayAspectRatio: Double { videoAspectRatio ?? (16.0 / 9.0) diff --git a/Yattee/Services/Player/QueueManager.swift b/Yattee/Services/Player/QueueManager.swift index 7a5f78ea..3c3cfe92 100644 --- a/Yattee/Services/Player/QueueManager.swift +++ b/Yattee/Services/Player/QueueManager.swift @@ -283,22 +283,12 @@ final class QueueManager { playerService?.openVideo(videoToPlay) } - /// Clears the media browser context when switching to a different queue source. - func clearMediaBrowserContext() { - mediaBrowserContext = nil - } - /// Whether there are more items that can be loaded from the queue source. func hasMoreItems() -> Bool { guard let source = currentQueueSource else { return false } return source.supportsContinuation } - /// Sets the queue source for continuation loading. - func setQueueSource(_ source: QueueSource?) { - currentQueueSource = source - } - // MARK: - Proactive Continuation Loading /// Called when a video starts playing to trigger proactive loading. diff --git a/Yattee/Services/Player/ReturnYouTubeDislikeAPI.swift b/Yattee/Services/Player/ReturnYouTubeDislikeAPI.swift index 7b23452d..b508d8b6 100644 --- a/Yattee/Services/Player/ReturnYouTubeDislikeAPI.swift +++ b/Yattee/Services/Player/ReturnYouTubeDislikeAPI.swift @@ -78,9 +78,4 @@ actor ReturnYouTubeDislikeAPI { throw error } } - - /// Clears the cache. - func clearCache() { - votesCache.removeAll() - } } diff --git a/Yattee/Services/PlayerControls/PlayerControlsBackupService.swift b/Yattee/Services/PlayerControls/PlayerControlsBackupService.swift index 0e43a175..f95cc32f 100644 --- a/Yattee/Services/PlayerControls/PlayerControlsBackupService.swift +++ b/Yattee/Services/PlayerControls/PlayerControlsBackupService.swift @@ -63,24 +63,6 @@ actor PlayerControlsBackupService { return presets } - /// Checks if a valid backup exists. - /// - Returns: True if a valid backup file exists. - func hasValidBackup() async -> Bool { - guard fileManager.fileExists(atPath: backupFileURL.path) else { - return false - } - - // Try to decode to verify it's valid - do { - let data = try Data(contentsOf: backupFileURL) - _ = try decoder.decode([LayoutPreset].self, from: data) - return true - } catch { - LoggingService.shared.error("Backup file exists but is invalid: \(error.localizedDescription)") - return false - } - } - /// Attempts to create a recovered preset from corrupted data. /// - Parameter data: The corrupted data. /// - Returns: A recovered preset if partial recovery is possible. @@ -117,16 +99,6 @@ actor PlayerControlsBackupService { return nil } - /// Deletes the backup file. - func deleteBackup() async throws { - guard fileManager.fileExists(atPath: backupFileURL.path) else { - return - } - - try fileManager.removeItem(at: backupFileURL) - LoggingService.shared.info("Deleted player controls backup") - } - // MARK: - Helpers private func formattedDate() -> String { diff --git a/Yattee/Services/PlayerControls/PlayerControlsLayoutService.swift b/Yattee/Services/PlayerControls/PlayerControlsLayoutService.swift index e496da31..40c0af77 100644 --- a/Yattee/Services/PlayerControls/PlayerControlsLayoutService.swift +++ b/Yattee/Services/PlayerControls/PlayerControlsLayoutService.swift @@ -179,22 +179,6 @@ actor PlayerControlsLayoutService { // MARK: - Preset Management - /// Returns all presets for the current device class. - func allPresets() async -> [LayoutPreset] { - if !isLoaded { - _ = try? await loadPresets() - } - return presets.filter { $0.deviceClass == .current } - } - - /// Returns a preset by ID. - func preset(forID id: UUID) async -> LayoutPreset? { - if !isLoaded { - _ = try? await loadPresets() - } - return presets.first { $0.id == id } - } - /// Saves a new or updated preset. func savePreset(_ preset: LayoutPreset) async throws { if !isLoaded { @@ -290,13 +274,6 @@ actor PlayerControlsLayoutService { } } - /// Duplicates a preset with a new name. - func duplicatePreset(_ preset: LayoutPreset, newName: String) async throws -> LayoutPreset { - let duplicate = preset.duplicate(name: newName) - try await savePreset(duplicate) - return duplicate - } - /// Renames a preset. func renamePreset(_ presetID: UUID, to newName: String) async throws { if !isLoaded { @@ -380,31 +357,6 @@ actor PlayerControlsLayoutService { return layout } - // MARK: - Validation - - /// Checks if a preset can be deleted. - func canDeletePreset(id: UUID) async -> Bool { - if !isLoaded { - _ = try? await loadPresets() - } - - guard let preset = presets.first(where: { $0.id == id }) else { - return false - } - - // Cannot delete built-in presets - if preset.isBuiltIn { - return false - } - - // Cannot delete active preset - if activePresetID() == id { - return false - } - - return true - } - // MARK: - NEW Badge Support /// Returns the last seen button version. diff --git a/Yattee/Services/RemoteControl/RemoteControlCoordinator.swift b/Yattee/Services/RemoteControl/RemoteControlCoordinator.swift index 37fcedca..b3b40a11 100644 --- a/Yattee/Services/RemoteControl/RemoteControlCoordinator.swift +++ b/Yattee/Services/RemoteControl/RemoteControlCoordinator.swift @@ -79,17 +79,6 @@ final class RemoteControlCoordinator { /// The device that's controlling us, if any. private(set) var controllingDevice: DiscoveredDevice? - /// This device's name for display. - var deviceName: String { - get { networkService.deviceName } - set { networkService.deviceName = newValue } - } - - /// This device's ID. - var deviceID: String { - networkService.deviceID - } - // MARK: - Dependencies private let networkService: LocalNetworkService @@ -337,25 +326,8 @@ final class RemoteControlCoordinator { } } - /// Send a command to all connected devices. - func broadcastCommand(_ command: RemoteControlCommand) async { - let commandDesc = String(describing: command).prefix(50) - rcDebug("COMMAND", "Broadcasting: \(commandDesc)") - await networkService.broadcast(command: command) - } - // MARK: - Convenience Commands - /// Send play command to a device. - func play(on device: DiscoveredDevice) async { - await sendCommand(.play, to: device) - } - - /// Send pause command to a device. - func pause(on device: DiscoveredDevice) async { - await sendCommand(.pause, to: device) - } - /// Send toggle play/pause command to a device. func togglePlayPause(on device: DiscoveredDevice) async { await sendCommand(.togglePlayPause, to: device) @@ -441,11 +413,6 @@ final class RemoteControlCoordinator { await sendCommand(.closeVideo, to: device) } - /// Toggle fullscreen on a device. - func toggleFullscreen(on device: DiscoveredDevice) async { - await sendCommand(.toggleFullscreen, to: device) - } - /// Play next video in queue on a device. func playNext(on device: DiscoveredDevice) async { await sendCommand(.playNext, to: device) @@ -1013,18 +980,6 @@ final class RemoteControlCoordinator { } } - private func expandPlayerIfNeeded() { - #if os(iOS) || os(macOS) - let isPiPActive = (playerService?.currentBackend as? MPVBackend)?.isPiPActive ?? false - rcDebug("PLAYER", "expandPlayerIfNeeded - isPiPActive=\(isPiPActive)") - if !isPiPActive { - navigationCoordinator?.expandPlayer() - } - #else - navigationCoordinator?.expandPlayer() - #endif - } - // MARK: - State Broadcasting private func startStateBroadcast() { diff --git a/Yattee/Services/RemotePlaylistsCache.swift b/Yattee/Services/RemotePlaylistsCache.swift index dbbdca1a..27c8edbb 100644 --- a/Yattee/Services/RemotePlaylistsCache.swift +++ b/Yattee/Services/RemotePlaylistsCache.swift @@ -105,11 +105,6 @@ final class RemotePlaylistsCache { lists[accountID] } - /// When the account's list was last fetched. - func listFetchDate(for accountID: UUID) -> Date? { - listFetchDates[accountID] - } - /// One playlist as last fetched, or `nil` if it never was. func snapshot(id: String, for accountID: UUID) -> RemotePlaylistSnapshot? { snapshots[accountID]?[id] diff --git a/Yattee/Services/SubscriptionService.swift b/Yattee/Services/SubscriptionService.swift index 5e2d5db9..4136e893 100644 --- a/Yattee/Services/SubscriptionService.swift +++ b/Yattee/Services/SubscriptionService.swift @@ -377,25 +377,6 @@ final class SubscriptionService { } } - /// Refreshes the subscription cache from the current provider. - func refreshCache() async throws { - guard let provider = readProvider else { - throw SubscriptionError.providerNotAvailable - } - - isLoading = true - lastError = nil - - do { - try await provider.refreshCache() - isLoading = false - } catch { - lastError = error - isLoading = false - throw error - } - } - // MARK: - Import /// Imports parsed external subscriptions (YouTube CSV / OPML) into the active account. diff --git a/Yattee/Services/Subscriptions/DataContextValidator.swift b/Yattee/Services/Subscriptions/DataContextValidator.swift index b706a0bf..91ecceaa 100644 --- a/Yattee/Services/Subscriptions/DataContextValidator.swift +++ b/Yattee/Services/Subscriptions/DataContextValidator.swift @@ -59,7 +59,6 @@ enum DataContextValidity: Hashable, Sendable { @Observable final class DataContextValidator { private let accountsManager: AccountsManager - private let instancesManager: InstancesManager private let toastManager: ToastManager private weak var appEnvironment: AppEnvironment? @@ -69,7 +68,6 @@ final class DataContextValidator { toastManager: ToastManager ) { self.accountsManager = accountsManager - self.instancesManager = instancesManager self.toastManager = toastManager } diff --git a/Yattee/Services/Subscriptions/InvidiousSubscriptionProvider.swift b/Yattee/Services/Subscriptions/InvidiousSubscriptionProvider.swift index 30853219..043d3f80 100644 --- a/Yattee/Services/Subscriptions/InvidiousSubscriptionProvider.swift +++ b/Yattee/Services/Subscriptions/InvidiousSubscriptionProvider.swift @@ -18,7 +18,6 @@ final class InvidiousSubscriptionProvider: SubscriptionProvider { /// The account this provider is bound to. One provider per account, so two /// logins on the same instance never share a subscription cache. let account: Account - var context: DataContext { .account(account.id) } private let invidiousAPI: InvidiousAPI private let credentialsManager: InvidiousCredentialsManager @@ -36,7 +35,6 @@ final class InvidiousSubscriptionProvider: SubscriptionProvider { accountsManager.recordServerOutcome(error, for: account) } - private let settingsManager: SettingsManager /// In-memory cache of subscribed channel IDs for fast lookup. private var cachedChannelIDs: Set<String> = [] @@ -60,7 +58,6 @@ final class InvidiousSubscriptionProvider: SubscriptionProvider { self.invidiousAPI = invidiousAPI self.credentialsManager = credentialsManager self.accountsManager = accountsManager - self.settingsManager = settingsManager } // MARK: - SubscriptionProvider diff --git a/Yattee/Services/Subscriptions/LocalSubscriptionProvider.swift b/Yattee/Services/Subscriptions/LocalSubscriptionProvider.swift index 1d27b41c..4798bc7f 100644 --- a/Yattee/Services/Subscriptions/LocalSubscriptionProvider.swift +++ b/Yattee/Services/Subscriptions/LocalSubscriptionProvider.swift @@ -14,7 +14,6 @@ import Foundation final class LocalSubscriptionProvider: SubscriptionProvider { // MARK: - Properties - let context: DataContext = .local private let dataManager: DataManager // MARK: - Initialization diff --git a/Yattee/Services/Subscriptions/PipedSubscriptionProvider.swift b/Yattee/Services/Subscriptions/PipedSubscriptionProvider.swift index 311468fa..78dc404d 100644 --- a/Yattee/Services/Subscriptions/PipedSubscriptionProvider.swift +++ b/Yattee/Services/Subscriptions/PipedSubscriptionProvider.swift @@ -18,7 +18,6 @@ final class PipedSubscriptionProvider: SubscriptionProvider { /// The account this provider is bound to. One provider per account, so two /// logins on the same instance never share a subscription cache. let account: Account - var context: DataContext { .account(account.id) } private let pipedAPI: PipedAPI private let credentialsManager: PipedCredentialsManager @@ -36,7 +35,6 @@ final class PipedSubscriptionProvider: SubscriptionProvider { accountsManager.recordServerOutcome(error, for: account) } - private let settingsManager: SettingsManager /// In-memory cache of subscribed channel IDs for fast lookup. private var cachedChannelIDs: Set<String> = [] @@ -60,7 +58,6 @@ final class PipedSubscriptionProvider: SubscriptionProvider { self.pipedAPI = pipedAPI self.credentialsManager = credentialsManager self.accountsManager = accountsManager - self.settingsManager = settingsManager } // MARK: - SubscriptionProvider diff --git a/Yattee/Services/Subscriptions/SubscriptionProvider.swift b/Yattee/Services/Subscriptions/SubscriptionProvider.swift index 7ab9d368..a71ecfb2 100644 --- a/Yattee/Services/Subscriptions/SubscriptionProvider.swift +++ b/Yattee/Services/Subscriptions/SubscriptionProvider.swift @@ -12,13 +12,6 @@ import Foundation /// Each provider implementation handles a specific subscription source. @MainActor protocol SubscriptionProvider: Sendable { - /// The data context this provider serves. - /// - /// A context rather than a backend type: an instance can hold several - /// accounts, so "which Invidious" is no longer enough to identify a - /// provider — each account gets its own, with its own cache. - var context: DataContext { get } - /// Fetches all subscriptions from the provider. /// - Returns: Array of channels the user is subscribed to. func fetchSubscriptions() async throws -> [Channel] diff --git a/Yattee/Services/ToastManager.swift b/Yattee/Services/ToastManager.swift index 71fa1280..8a0e812c 100644 --- a/Yattee/Services/ToastManager.swift +++ b/Yattee/Services/ToastManager.swift @@ -258,50 +258,6 @@ final class ToastManager { dismissTasks[id] = task } - // MARK: - Player Scope Convenience Methods - - /// Show a loading toast (player-only scope, longer auto-dismiss delay). - @discardableResult - func showPlayerLoading(_ title: String, subtitle: String? = nil) -> UUID { - show( - scopes: [.player], - category: .loading, - title: title, - subtitle: subtitle, - icon: nil, // Will show ProgressView - iconColor: nil, - autoDismissDelay: 30.0 // Long timeout for loading states - ) - } - - /// Show a retry toast (player-only scope). - @discardableResult - func showPlayerRetry(_ title: String, subtitle: String? = nil) -> UUID { - show( - scopes: [.player], - category: .retry, - title: title, - subtitle: subtitle, - icon: "arrow.clockwise", - iconColor: .orange, - autoDismissDelay: 5.0 - ) - } - - /// Show an error toast (player-only scope). - @discardableResult - func showPlayerError(_ title: String, subtitle: String? = nil) -> UUID { - show( - scopes: [.player], - category: .error, - title: title, - subtitle: subtitle, - icon: "exclamationmark.triangle.fill", - iconColor: .red, - autoDismissDelay: 5.0 - ) - } - // MARK: - General Convenience Methods /// Show an error toast (shows in player if visible, otherwise main). @@ -350,42 +306,6 @@ final class ToastManager { dismissInternal(id: id, animated: animated) } - /// Dismiss all toasts in a category (across all scopes). - func dismissCategory(_ category: ToastCategory) { - let toastsToDismiss = activeToasts.filter { $0.category == category } - for toast in toastsToDismiss { - dismissInternal(id: toast.id, animated: true) - } - } - - /// Dismiss all toasts in a category for a specific active scope. - func dismissCategory(_ category: ToastCategory, scope: ToastScope) { - let toastsToDismiss = activeToasts.filter { $0.category == category && $0.activeScope == scope } - for toast in toastsToDismiss { - dismissInternal(id: toast.id, animated: true) - } - } - - /// Dismiss all toasts in a specific active scope. - func dismissScope(_ scope: ToastScope) { - let toastsToDismiss = activeToasts.filter { $0.activeScope == scope } - for toast in toastsToDismiss { - dismissInternal(id: toast.id, animated: true) - } - } - - /// Dismiss all toasts. - func dismissAll() { - for task in dismissTasks.values { - task.cancel() - } - dismissTasks.removeAll() - - withAnimation(.easeInOut(duration: 0.25)) { - activeToasts.removeAll() - } - } - // MARK: - Private private func dismissInternal(id: UUID, animated: Bool) { diff --git a/Yattee/ViewModels/OpenedLinksViewModel.swift b/Yattee/ViewModels/OpenedLinksViewModel.swift index afd2ec12..94b94a2c 100644 --- a/Yattee/ViewModels/OpenedLinksViewModel.swift +++ b/Yattee/ViewModels/OpenedLinksViewModel.swift @@ -72,13 +72,6 @@ final class OpenedLinksViewModel { } } - var isLoading: Bool { - links.contains { link in - if case .loading = link.state { return true } - return false - } - } - /// Starts resolving once. Owned by the model rather than the view's /// `.task`, which is cancelled when a result's screen is pushed and would /// fail the links still loading. diff --git a/Yattee/ViewModels/PlayerControls/PlayerControlsSettingsViewModel.swift b/Yattee/ViewModels/PlayerControls/PlayerControlsSettingsViewModel.swift index 9528db3b..ac2bae0a 100644 --- a/Yattee/ViewModels/PlayerControls/PlayerControlsSettingsViewModel.swift +++ b/Yattee/ViewModels/PlayerControls/PlayerControlsSettingsViewModel.swift @@ -16,7 +16,6 @@ final class PlayerControlsSettingsViewModel { // MARK: - Dependencies private let layoutService: PlayerControlsLayoutService - let settingsManager: SettingsManager /// Observer for preset changes from CloudKit sync. @ObservationIgnored private var presetsChangedObserver: NSObjectProtocol? @@ -54,12 +53,6 @@ final class PlayerControlsSettingsViewModel { activePreset?.id } - /// Whether the active preset can be deleted (not built-in and not active). - var canDeleteActivePreset: Bool { - guard let activePreset else { return false } - return !activePreset.isBuiltIn - } - /// Whether the active preset can be edited (not built-in). var canEditActivePreset: Bool { guard let activePreset else { return false } @@ -76,16 +69,6 @@ final class PlayerControlsSettingsViewModel { activePreset?.layout.centerSettings ?? .default } - /// Seek backward seconds - exposed for direct observation. - var seekBackwardSeconds: Int { - activePreset?.layout.centerSettings.seekBackwardSeconds ?? 10 - } - - /// Seek forward seconds - exposed for direct observation. - var seekForwardSeconds: Int { - activePreset?.layout.centerSettings.seekForwardSeconds ?? 10 - } - /// Current progress bar settings. var progressBarSettings: ProgressBarSettings { activePreset?.layout.progressBarSettings ?? .default @@ -101,15 +84,6 @@ final class PlayerControlsSettingsViewModel { presets.filter { !$0.isBuiltIn }.sorted { $0.name < $1.name } } - /// Button types that are new since last seen. - var newButtonTypes: [ControlButtonType] { - Task.detached { [layoutService] in - await layoutService.newButtonTypes() - } - // Return empty for now; actual implementation will load async - return [] - } - // MARK: - Initialization init( @@ -117,7 +91,6 @@ final class PlayerControlsSettingsViewModel { settingsManager: SettingsManager ) { self.layoutService = layoutService - self.settingsManager = settingsManager // Observe preset changes from CloudKit sync presetsChangedObserver = NotificationCenter.default.addObserver( @@ -204,13 +177,6 @@ final class PlayerControlsSettingsViewModel { LoggingService.shared.info("Selected preset: \(preset.name)") } - /// Selects a preset by ID. - /// - Parameter id: The ID of the preset to activate. - func selectPreset(id: UUID) { - guard let preset = presets.first(where: { $0.id == id }) else { return } - selectPreset(preset) - } - // MARK: - Preset Management /// Creates a new custom preset with the given name. @@ -290,30 +256,6 @@ final class PlayerControlsSettingsViewModel { // MARK: - Layout Editing - /// Updates the top section of the current layout. - /// - Parameter section: The new top section configuration. - func updateTopSection(_ section: LayoutSection) async { - await updateLayout { layout in - layout.topSection = section - } - } - - /// Updates the bottom section of the current layout. - /// - Parameter section: The new bottom section configuration. - func updateBottomSection(_ section: LayoutSection) async { - await updateLayout { layout in - layout.bottomSection = section - } - } - - /// Updates the center settings of the current layout. - /// - Parameter settings: The new center section settings. - func updateCenterSettings(_ settings: CenterSectionSettings) async { - await updateLayout { layout in - layout.centerSettings = settings - } - } - /// Synchronously updates center settings with immediate UI feedback. /// Saves are debounced to avoid rapid disk writes. /// - Parameter mutation: A closure that mutates the center settings. @@ -338,14 +280,6 @@ final class PlayerControlsSettingsViewModel { queueDebouncedSave(preset: preset) } - /// Updates the global settings of the current layout. - /// - Parameter settings: The new global settings. - func updateGlobalSettings(_ settings: GlobalLayoutSettings) async { - await updateLayout { layout in - layout.globalSettings = settings - } - } - /// Synchronously updates global settings with immediate UI feedback. /// Saves are debounced to avoid rapid disk writes. /// - Parameter mutation: A closure that mutates the global settings. @@ -382,21 +316,6 @@ final class PlayerControlsSettingsViewModel { queueDebouncedSave(preset: preset) } - /// Updates a button configuration in the specified section. - /// - Parameters: - /// - config: The updated button configuration. - /// - section: The section containing the button. - func updateButtonConfiguration(_ config: ControlButtonConfiguration, in section: LayoutSectionType) async { - await updateLayout { layout in - switch section { - case .top: - layout.topSection.update(button: config) - case .bottom: - layout.bottomSection.update(button: config) - } - } - } - /// Synchronously updates a button configuration with immediate UI feedback. /// Saves are debounced to avoid rapid disk writes. /// - Parameters: @@ -422,22 +341,6 @@ final class PlayerControlsSettingsViewModel { queueDebouncedSave(preset: preset) } - /// Adds a button to the specified section. - /// - Parameters: - /// - buttonType: The type of button to add. - /// - section: The section to add the button to. - func addButton(_ buttonType: ControlButtonType, to section: LayoutSectionType) async { - await updateLayout { layout in - let config = ControlButtonConfiguration.defaultConfiguration(for: buttonType) - switch section { - case .top: - layout.topSection.add(button: config) - case .bottom: - layout.bottomSection.add(button: config) - } - } - } - /// Synchronously adds a button with immediate UI feedback. /// - Parameters: /// - buttonType: The type of button to add. @@ -462,21 +365,6 @@ final class PlayerControlsSettingsViewModel { queueDebouncedSave(preset: preset) } - /// Removes a button from the specified section. - /// - Parameters: - /// - buttonID: The ID of the button to remove. - /// - section: The section containing the button. - func removeButton(_ buttonID: UUID, from section: LayoutSectionType) async { - await updateLayout { layout in - switch section { - case .top: - layout.topSection.remove(id: buttonID) - case .bottom: - layout.bottomSection.remove(id: buttonID) - } - } - } - /// Synchronously removes a button with immediate UI feedback. /// - Parameters: /// - buttonID: The ID of the button to remove. @@ -500,22 +388,6 @@ final class PlayerControlsSettingsViewModel { queueDebouncedSave(preset: preset) } - /// Moves a button within a section. - /// - Parameters: - /// - source: Source indices. - /// - destination: Destination index. - /// - section: The section containing the buttons. - func moveButton(fromOffsets source: IndexSet, toOffset destination: Int, in section: LayoutSectionType) async { - await updateLayout { layout in - switch section { - case .top: - layout.topSection.move(fromOffsets: source, toOffset: destination) - case .bottom: - layout.bottomSection.move(fromOffsets: source, toOffset: destination) - } - } - } - /// Synchronously moves a button with immediate UI feedback. /// - Parameters: /// - source: Source indices. @@ -850,13 +722,6 @@ final class PlayerControlsSettingsViewModel { } } - /// Moves pages within the panel page order. - func movePanelPages(fromOffsets source: IndexSet, toOffset destination: Int) { - updatePanelSettingsSync { settings in - settings.movePages(fromOffsets: source, toOffset: destination) - } - } - /// Reorders the pages a settings list displays; offsets are relative to /// `visible` (see `PanelSettings.moveVisiblePages`). func movePanelPages(visible: [PanelPageType], fromOffsets source: IndexSet, toOffset destination: Int) { @@ -981,32 +846,6 @@ final class PlayerControlsSettingsViewModel { // MARK: - Private Helpers - /// Updates the layout and saves to storage. - /// - Parameter mutation: A closure that mutates the layout. - private func updateLayout(_ mutation: (inout PlayerControlsLayout) -> Void) async { - guard let activePreset, !activePreset.isBuiltIn else { - error = "Cannot edit built-in presets. Duplicate it first." - return - } - - var layout = activePreset.layout - mutation(&layout) - - // Update local state immediately for responsive UI - self.activePreset = activePreset.withUpdatedLayout(layout) - - // Save to storage - isSaving = true - do { - try await layoutService.updatePresetLayout(activePreset.id, layout: layout) - await refreshPresets() - } catch { - self.error = error.localizedDescription - LoggingService.shared.error("Failed to save layout: \(error.localizedDescription)") - } - isSaving = false - } - /// Queues a debounced save operation. /// Cancels any pending save and waits before persisting to avoid rapid disk writes. /// - Parameter preset: The preset to save. @@ -1029,21 +868,6 @@ final class PlayerControlsSettingsViewModel { } } - // MARK: - NEW Badge Support - - /// Marks all button types as seen, hiding NEW badges. - func markAllButtonsAsSeen() async { - await layoutService.markAllButtonsAsSeen() - } - - /// Returns whether a button type should show the NEW badge. - /// - Parameter type: The button type to check. - /// - Returns: True if the button was added after the last seen version. - func isNewButton(_ type: ControlButtonType) async -> Bool { - let newTypes = await layoutService.newButtonTypes() - return newTypes.contains(type) - } - // MARK: - Error Handling /// Clears the current error. diff --git a/Yattee/Views/Channel/ChannelView.swift b/Yattee/Views/Channel/ChannelView.swift index 4db5d715..a76f3d44 100644 --- a/Yattee/Views/Channel/ChannelView.swift +++ b/Yattee/Views/Channel/ChannelView.swift @@ -307,11 +307,6 @@ struct ChannelView: View { avatarSize - (avatarSize - collapsedAvatarSize) * collapseProgress } - /// Opacity for expanded content (fades out as we scroll) - private var expandedContentOpacity: CGFloat { - 1 - min(collapseProgress * 2, 1) - } - /// Opacity for collapsed title (fades in as we scroll) private var collapsedTitleOpacity: CGFloat { max((collapseProgress - 0.5) * 2, 0) @@ -785,11 +780,6 @@ struct ChannelView: View { .animation(.easeInOut(duration: 0.25), value: isDescriptionScrollLocked) } - @ViewBuilder - private func tvOSAvatar(for channel: Channel) -> some View { - tvOSAvatar(name: channel.name, thumbnailURL: channel.thumbnailURL) - } - @ViewBuilder private func tvOSAvatar(name: String, thumbnailURL: URL?) -> some View { LazyImage(url: thumbnailURL) { state in @@ -2246,13 +2236,6 @@ struct ChannelView: View { isSearchLoading = false } - private func clearSearch() { - isSearchActive = false - hasSearched = false - searchResults = .empty - searchText = "" - } - // MARK: - Error View private func errorView(_ error: String) -> some View { @@ -2272,11 +2255,6 @@ struct ChannelView: View { // MARK: - Helpers - private func formatSubscribers(_ count: Int) -> String { - let formatted = CountFormatter.compact(count) - return String(localized: "channel.subscriberCount \(formatted)") - } - /// Returns the appropriate instance for the channel's content source private func instanceForSource() -> Instance? { appEnvironment?.instancesManager.instance(for: source) diff --git a/Yattee/Views/Components/BookmarkRowView.swift b/Yattee/Views/Components/BookmarkRowView.swift index c76612e8..ef5416b1 100644 --- a/Yattee/Views/Components/BookmarkRowView.swift +++ b/Yattee/Views/Components/BookmarkRowView.swift @@ -13,8 +13,6 @@ import SwiftUI /// Automatically handles DeArrow integration. /// Supports optional queue context for auto-play functionality. struct BookmarkRowView: View { - @Environment(\.appEnvironment) private var appEnvironment - let bookmark: Bookmark var style: VideoRowStyle = .regular var watchProgress: Double? = nil @@ -27,10 +25,6 @@ struct BookmarkRowView: View { var videoIndex: Int? = nil var loadMoreVideos: LoadMoreVideosCallback? = nil - private var accentColor: Color { - appEnvironment?.settingsManager.resolvedAccentColor ?? .accentColor - } - private var isBookmarkValid: Bool { bookmark.modelContext != nil && !bookmark.isDeleted } diff --git a/Yattee/Views/Components/ExpandedCommentsView.swift b/Yattee/Views/Components/ExpandedCommentsView.swift index 6f0cbd76..b0d617f2 100644 --- a/Yattee/Views/Components/ExpandedCommentsView.swift +++ b/Yattee/Views/Components/ExpandedCommentsView.swift @@ -27,14 +27,6 @@ struct ExpandedCommentsView: View { @State private var scrollBounceOffset: CGFloat = 0 @State private var isDismissThresholdReached = false - private var playerState: PlayerState? { appEnvironment?.playerService.state } - private var contentService: ContentService? { appEnvironment?.contentService } - private var instancesManager: InstancesManager? { appEnvironment?.instancesManager } - - private var comments: [Comment] { playerState?.comments ?? [] } - private var commentsState: CommentsLoadState { playerState?.commentsState ?? .idle } - private var videoSource: ContentSource? { playerState?.currentVideo?.id.source } - private var backgroundColor: Color { #if os(iOS) Color(.systemBackground) @@ -363,53 +355,6 @@ struct CommentsListContent: View { } } -// MARK: - Scroll Fade Overlay - -/// A view modifier that applies a gradient fade overlay to the top and bottom edges of a scroll view. -private struct ScrollFadeOverlayModifier: ViewModifier { - let fadeHeight: CGFloat - - func body(content: Content) -> some View { - content - .overlay(alignment: .top) { - // Top gradient fade - LinearGradient( - colors: [backgroundColor, .clear], - startPoint: .top, - endPoint: .bottom - ) - .frame(height: fadeHeight) - .allowsHitTesting(false) - } - .overlay(alignment: .bottom) { - // Bottom gradient fade - LinearGradient( - colors: [.clear, backgroundColor], - startPoint: .top, - endPoint: .bottom - ) - .frame(height: fadeHeight) - .allowsHitTesting(false) - } - } - - private var backgroundColor: Color { - #if os(iOS) - Color(.systemBackground) - #elseif os(macOS) - Color(nsColor: .windowBackgroundColor) - #else - Color.black - #endif - } -} - -private extension View { - func scrollFadeMask(fadeHeight: CGFloat = 24) -> some View { - modifier(ScrollFadeOverlayModifier(fadeHeight: fadeHeight)) - } -} - // MARK: - Scroll Detection #if os(iOS) diff --git a/Yattee/Views/Components/LoadStateView.swift b/Yattee/Views/Components/LoadStateView.swift index 5be39d25..ec508bad 100644 --- a/Yattee/Views/Components/LoadStateView.swift +++ b/Yattee/Views/Components/LoadStateView.swift @@ -83,52 +83,6 @@ struct ErrorStateView: View { } } -// MARK: - Load State View (Generic Container) - -/// A generic container that handles loading, error, and content states. -/// -/// Example usage: -/// ```swift -/// LoadStateView( -/// isLoading: viewModel.isLoading, -/// errorMessage: viewModel.error, -/// data: viewModel.data, -/// loadingMessage: "Loading...", -/// errorTitle: "Error", -/// onRetry: { await viewModel.load() } -/// ) { data in -/// ContentView(data: data) -/// } -/// ``` -struct LoadStateView<Content: View, Data>: View { - let isLoading: Bool - let errorMessage: String? - let data: Data? - var loadingMessage: String? = nil - var loadingSubtext: String? = nil - var errorTitle: String = "Error" - var onRetry: (() async -> Void)? = nil - var onDismiss: (() -> Void)? = nil - @ViewBuilder let content: (Data) -> Content - - var body: some View { - Group { - if isLoading && data == nil { - LoadingView(message: loadingMessage, subtext: loadingSubtext) - } else if let error = errorMessage, data == nil { - ErrorStateView( - title: errorTitle, - message: error, - onRetry: onRetry, - onDismiss: onDismiss - ) - } else if let data { - content(data) - } - } - } -} - // MARK: - Previews #Preview("Loading") { @@ -143,38 +97,3 @@ struct LoadStateView<Content: View, Data>: View { onDismiss: {} ) } - -#Preview("LoadStateView - Loading") { - LoadStateView( - isLoading: true, - errorMessage: nil, - data: nil as String?, - loadingMessage: "Fetching data..." - ) { data in - Text(data) - } -} - -#Preview("LoadStateView - Error") { - LoadStateView( - isLoading: false, - errorMessage: "Network error occurred", - data: nil as String?, - errorTitle: "Failed to Load", - onRetry: {} - ) { data in - Text(data) - } -} - -#Preview("LoadStateView - Content") { - LoadStateView( - isLoading: false, - errorMessage: nil, - data: "Hello, World!", - loadingMessage: "Loading..." - ) { data in - Text(data) - .font(.largeTitle) - } -} diff --git a/Yattee/Views/Components/ShareSheet.swift b/Yattee/Views/Components/ShareSheet.swift index 4c1e4d20..592eec94 100644 --- a/Yattee/Views/Components/ShareSheet.swift +++ b/Yattee/Views/Components/ShareSheet.swift @@ -24,34 +24,4 @@ struct ShareSheet: UIViewControllerRepresentable { func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} } -#elseif os(macOS) -struct ShareSheet: View { - let items: [Any] - - var body: some View { - VStack(spacing: 12) { - if let text = items.first as? String { - ScrollView { - Text(text) - .font(.caption) - .fontDesign(.monospaced) - .textSelection(.enabled) - .frame(maxWidth: .infinity, alignment: .leading) - .padding() - } - .frame(maxHeight: 400) - } - - Button(String(localized: "settings.advanced.logs.export.copy")) { - if let text = items.first as? String { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(text, forType: .string) - } - } - .keyboardShortcut(.defaultAction) - } - .padding() - .frame(minWidth: 420, minHeight: 320) - } -} #endif diff --git a/Yattee/Views/Components/SourceListContainer.swift b/Yattee/Views/Components/SourceListContainer.swift deleted file mode 100644 index 20fd700e..00000000 --- a/Yattee/Views/Components/SourceListContainer.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// SourceListContainer.swift -// Yattee -// -// Container for source list views with inset/plain styling. -// - -import SwiftUI - -/// A container that handles inset/plain list styling for source views. -/// -/// Simplified version of VideoListContainer for simpler source rows. -struct SourceListContainer<Header: View, Content: View>: View { - let listStyle: VideoListStyle - - @ViewBuilder let header: () -> Header - @ViewBuilder let content: () -> Content - - /// Corner radius for card container in inset style. - private let cardCornerRadius: CGFloat = 10 - - /// Horizontal padding for card container. - private let cardHorizontalPadding: CGFloat = 16 - - /// Bottom padding for card container. - private let cardBottomPadding: CGFloat = 16 - - var body: some View { - #if os(tvOS) - // tvOS: Simple ScrollView without background overlay - ScrollView { - LazyVStack(spacing: 0) { - header() - content() - } - } - #else - // iOS/macOS: Background overlay pattern - backgroundStyle.color - .ignoresSafeArea() - .overlay( - ScrollView { - LazyVStack(spacing: 0) { - header() - - if listStyle == .inset { - insetCardContent - } else { - plainContent - } - } - } - ) - #endif - } - - // MARK: - Private - - private var backgroundStyle: ListBackgroundStyle { - listStyle == .inset ? .grouped : .plain - } - - private var insetCardContent: some View { - LazyVStack(spacing: 0) { - content() - } - .background(ListBackgroundStyle.card.color) - .clipShape(RoundedRectangle(cornerRadius: cardCornerRadius)) - .padding(.horizontal, cardHorizontalPadding) - .padding(.bottom, cardBottomPadding) - } - - private var plainContent: some View { - LazyVStack(spacing: 0) { - content() - } - .padding(.bottom, cardBottomPadding) - } -} - -// MARK: - Convenience Initializers - -extension SourceListContainer where Header == EmptyView { - /// Creates a source list container without a header. - init( - listStyle: VideoListStyle, - @ViewBuilder content: @escaping () -> Content - ) { - self.listStyle = listStyle - self.header = { EmptyView() } - self.content = content - } -} - -// MARK: - Preview - -#Preview("Inset Style") { - SourceListContainer(listStyle: .inset) { - Text("Header") - .padding() - } content: { - ForEach(0..<5) { index in - SourceListRow(isLast: index == 4, listStyle: .inset) { - HStack(spacing: 12) { - Image(systemName: "server.rack") - .font(.title2) - .foregroundStyle(.blue) - .frame(width: 32) - VStack(alignment: .leading) { - Text("Source \(index + 1)") - .font(.headline) - Text("example.com") - .font(.caption) - .foregroundStyle(.secondary) - } - Spacer() - } - } - } - } -} - -#Preview("Plain Style") { - SourceListContainer(listStyle: .plain) { - ForEach(0..<5) { index in - SourceListRow(isLast: index == 4, listStyle: .plain) { - HStack(spacing: 12) { - Image(systemName: "server.rack") - .font(.title2) - .foregroundStyle(.blue) - .frame(width: 32) - VStack(alignment: .leading) { - Text("Source \(index + 1)") - .font(.headline) - Text("example.com") - .font(.caption) - .foregroundStyle(.secondary) - } - Spacer() - } - } - } - } -} diff --git a/Yattee/Views/Components/SwipeActionModifier.swift b/Yattee/Views/Components/SwipeActionModifier.swift index 6c49924b..ceba497b 100644 --- a/Yattee/Views/Components/SwipeActionModifier.swift +++ b/Yattee/Views/Components/SwipeActionModifier.swift @@ -76,7 +76,6 @@ struct SwipeActionModifier: ViewModifier { @State private var progress: CGFloat = 0 // Scroll tracking for auto-close on scroll - @State private var currentScrollOffset: CGFloat = 0 @State private var storedScrollOffset: CGFloat? // Shared state reference (computed to avoid inclusion in memberwise init) diff --git a/Yattee/Views/Components/TextTruncationEffect.swift b/Yattee/Views/Components/TextTruncationEffect.swift deleted file mode 100644 index 41ba7e61..00000000 --- a/Yattee/Views/Components/TextTruncationEffect.swift +++ /dev/null @@ -1,236 +0,0 @@ -// -// TextTruncationEffect.swift -// Yattee -// -// Animated text truncation effect with "...more" indicator. -// Adapted from Balaji Venkatesh's TruncationEffect implementation. -// - -import SwiftUI - -#if os(macOS) -import AppKit -#else -import UIKit -#endif - -extension Text { - /// Applies an animated truncation effect that shows "...more" when collapsed. - /// - Parameters: - /// - length: Number of lines to show when truncated - /// - isEnabled: Whether truncation is enabled (false = expanded) - /// - animation: Animation to use for expand/collapse transitions - @ViewBuilder - func truncationEffect(length: Int, isEnabled: Bool, animation: Animation) -> some View { - self.modifier( - TruncationEffectViewModifier( - length: length, - isEnabled: isEnabled, - animation: animation - ) - ) - } -} - -// MARK: - View Modifier - -private struct TruncationEffectViewModifier: ViewModifier { - var length: Int - var isEnabled: Bool - var animation: Animation - - @State private var limitedSize: CGSize = .zero - @State private var fullSize: CGSize = .zero - @State private var animatedProgress: CGFloat = 0 - - func body(content: Content) -> some View { - content - .lineLimit(length) - .opacity(0) - .fixedSize(horizontal: false, vertical: true) - .frame(maxWidth: .infinity, alignment: .leading) - .onGeometryChange(for: CGSize.self) { - $0.size - } action: { newValue in - limitedSize = newValue - } - .frame(height: isExpanded ? fullSize.height : nil) - .overlay { - // Full content with animation - GeometryReader { proxy in - let contentSize = proxy.size - - content - .textRenderer( - TruncationTextRenderer( - length: length, - progress: animatedProgress - ) - ) - .fixedSize(horizontal: false, vertical: true) - .onGeometryChange(for: CGSize.self) { - $0.size - } action: { newValue in - fullSize = newValue - } - .frame( - width: contentSize.width, - height: contentSize.height, - alignment: isExpanded ? .leading : .topLeading - ) - } - } - .contentShape(.rect) - .onChange(of: isEnabled) { _, newValue in - withAnimation(animation) { - animatedProgress = !newValue ? 1 : 0 - } - } - .onAppear { - // Set initial value without animation - animatedProgress = !isEnabled ? 1 : 0 - } - } - - var isExpanded: Bool { - animatedProgress == 1 - } -} - -// MARK: - Text Renderer - -@Animatable -private struct TruncationTextRenderer: TextRenderer { - @AnimatableIgnored var length: Int - var progress: CGFloat - - /// Minimum number of hidden characters to show "...more" indicator. - /// If less than this, just show the full text without truncation indicator. - private let minHiddenCharacters = 10 - - func draw(layout: Text.Layout, in ctx: inout GraphicsContext) { - let totalLines = layout.count - let hasExtraLines = totalLines > length - - // Count characters in lines beyond the limit - let hiddenCharacterCount: Int = { - guard hasExtraLines else { return 0 } - var count = 0 - for index in length..<totalLines { - count += layout[index].flatMap { $0 }.count - } - return count - }() - - // If hidden content is too short, don't show "...more" - just draw normally - let shouldShowMoreIndicator = hasExtraLines && hiddenCharacterCount >= minHiddenCharacters - - for (index, line) in layout.enumerated() { - var copyContext = ctx - if index == length - 1 && shouldShowMoreIndicator { - drawMoreTextAtEnd(line: line, context: ©Context) - } else { - if index < length { - // Draw all visible lines - copyContext.draw(line) - } else if shouldShowMoreIndicator { - drawLinesWithBlurEffect(index: index, layout: layout, in: ©Context) - } else { - // No "...more" indicator - just draw with fade based on progress - copyContext.opacity = progress - copyContext.draw(line) - } - } - } - } - - private func drawLinesWithBlurEffect(index: Int, layout: Text.Layout, in ctx: inout GraphicsContext) { - let line = layout[index] - - let lineIndex = Double(index - length) - let totalExtraLines = Double(layout.count - length) - - // Divide the animation progress among all extra lines - let lineStartProgress = lineIndex / max(1, totalExtraLines) - let lineEndProgress = (lineIndex + 1) / max(1, totalExtraLines) - - // Calculate this specific line's progress - let lineProgress = max(0, min(1, (progress - lineStartProgress) / (lineEndProgress - lineStartProgress))) - - ctx.opacity = lineProgress - ctx.addFilter(.blur(radius: blurRadius - (blurRadius * lineProgress))) - ctx.draw(line) - } - - private func drawMoreTextAtEnd(line: Text.Layout.Element, context: inout GraphicsContext) { - let runs = line.flatMap { $0 } - let runsCount = runs.count - let text = " ..." + String(localized: "common.more") - let textCount = text.count - - // Draw runs until the text count - for index in 0..<max(runsCount - textCount, 0) { - let run = runs[index] - context.draw(run) - } - - // Draw remaining runs with opacity filter - for index in max(runsCount - textCount, 0)..<runsCount { - let run = runs[index] - context.opacity = progress - context.draw(run) - } - - // Draw "...more" text - let textRunIndex = max(runsCount - textCount, 0) - var typography: Text.Layout.TypographicBounds - if !runs.isEmpty { - typography = runs[textRunIndex].typographicBounds - } else { - typography = line.typographicBounds - } - - let fontSize: CGFloat = typography.ascent - let font = platformFont(ofSize: fontSize) - - let spacing: CGFloat = textWidth(text, font: font) / 2 - - let swiftUIText = Text(text) - .font(Font(font)) - .foregroundStyle(.secondary) - - let origin = CGPoint( - x: typography.rect.minX + spacing, - y: typography.rect.midY - ) - - context.opacity = 1 - progress - context.draw(swiftUIText, at: origin) - } - - private var blurRadius: CGFloat { - 5 - } - - // MARK: - Cross-Platform Font Helpers - - #if os(macOS) - private func platformFont(ofSize size: CGFloat) -> NSFont { - NSFont.systemFont(ofSize: size) - } - - private func textWidth(_ text: String, font: NSFont) -> CGFloat { - let attributes: [NSAttributedString.Key: Any] = [.font: font] - return (text as NSString).size(withAttributes: attributes).width - } - #else - private func platformFont(ofSize size: CGFloat) -> UIFont { - UIFont.systemFont(ofSize: size) - } - - private func textWidth(_ text: String, font: UIFont) -> CGFloat { - let attributes: [NSAttributedString.Key: Any] = [.font: font] - return (text as NSString).size(withAttributes: attributes).width - } - #endif -} diff --git a/Yattee/Views/Components/VideoListContainer.swift b/Yattee/Views/Components/VideoListContainer.swift index 52f6d331..a477de5b 100644 --- a/Yattee/Views/Components/VideoListContainer.swift +++ b/Yattee/Views/Components/VideoListContainer.swift @@ -138,22 +138,6 @@ extension VideoListContainer where Footer == EmptyView { } } -extension VideoListContainer where Header == EmptyView { - /// Creates a video list container with a footer but no header. - init( - listStyle: VideoListStyle, - rowStyle: VideoRowStyle, - @ViewBuilder content: @escaping () -> Content, - @ViewBuilder footer: @escaping () -> Footer - ) { - self.listStyle = listStyle - self.rowStyle = rowStyle - self.header = { EmptyView() } - self.content = content - self.footer = footer - } -} - // MARK: - Preview #Preview("Inset Style") { diff --git a/Yattee/Views/Components/VideoNavigationButton.swift b/Yattee/Views/Components/VideoNavigationButton.swift deleted file mode 100644 index f10d3426..00000000 --- a/Yattee/Views/Components/VideoNavigationButton.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// VideoNavigationButton.swift -// Yattee -// -// Navigation button for switching between videos in VideoInfoView. -// - -import SwiftUI - -/// A floating circular button for navigating between videos in a queue. -/// Used in VideoInfoView to switch to previous/next video. -struct VideoNavigationButton: View { - let direction: Direction - let action: () -> Void - var isLoading: Bool = false - var hasError: Bool = false - - enum Direction { - case previous - case next - - var icon: String { - switch self { - case .previous: return "chevron.left" - case .next: return "chevron.right" - } - } - - var accessibilityLabel: String { - switch self { - case .previous: return String(localized: "video.navigation.previous") - case .next: return String(localized: "video.navigation.next") - } - } - } - - var body: some View { - Button(action: action) { - ZStack { - if isLoading { - ProgressView() - .controlSize(.small) - .tint(.primary) - } else { - Image(systemName: direction.icon) - .font(.title2) - .fontWeight(.semibold) - } - } - .foregroundStyle(.primary) - .frame(width: 44, height: 44) - .glassBackground(.regular, in: .circle) - .shadow(color: .black.opacity(0.1), radius: 8, y: 4) - .overlay { - if hasError { - Circle() - .stroke(Color.red, lineWidth: 2) - } - } - } - .buttonStyle(.plain) - .disabled(isLoading) - .accessibilityLabel(direction.accessibilityLabel) - } -} - -// MARK: - Preview - -#Preview("Previous") { - VideoNavigationButton(direction: .previous) {} - .padding() -} - -#Preview("Next") { - VideoNavigationButton(direction: .next) {} - .padding() -} diff --git a/Yattee/Views/Components/VideoSwipeActionsModifier.swift b/Yattee/Views/Components/VideoSwipeActionsModifier.swift index 9cb0545d..7e6f4128 100644 --- a/Yattee/Views/Components/VideoSwipeActionsModifier.swift +++ b/Yattee/Views/Components/VideoSwipeActionsModifier.swift @@ -11,7 +11,6 @@ import SwiftUI /// View modifier that applies user-configurable swipe actions plus fixed context-specific actions. struct VideoSwipeActionsModifier: ViewModifier { @Environment(\.appEnvironment) private var appEnvironment - @Environment(\.videoQueueContext) private var queueContext @Environment(\.presentAppSheet) private var presentAppSheet let video: Video diff --git a/Yattee/Views/Components/ViewOptionsSheet.swift b/Yattee/Views/Components/ViewOptionsSheet.swift index 5aae42a6..a1be0555 100644 --- a/Yattee/Views/Components/ViewOptionsSheet.swift +++ b/Yattee/Views/Components/ViewOptionsSheet.swift @@ -12,7 +12,6 @@ import SwiftUI /// Shows layout picker (list/grid), size options, and filter toggles. /// Reusable across different views with their own storage bindings. struct ViewOptionsSheet: View { - @Environment(\.appEnvironment) private var appEnvironment @Binding var layout: VideoListLayout @Binding var rowStyle: VideoRowStyle @Binding var gridColumns: Int diff --git a/Yattee/Views/Downloads/DownloadsView.swift b/Yattee/Views/Downloads/DownloadsView.swift index cd0f392f..4804eca3 100644 --- a/Yattee/Views/Downloads/DownloadsView.swift +++ b/Yattee/Views/Downloads/DownloadsView.swift @@ -297,18 +297,6 @@ private struct ActiveDownloadsSectionContentView: View { } } - // MARK: - Section Header - - private func sectionHeader(_ text: String) -> some View { - Text(text) - .font(.subheadline.weight(.semibold)) - .foregroundStyle(.secondary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, listStyle == .inset ? 16 : 16) - .padding(.top, 8) - .padding(.bottom, 8) - } - // MARK: - Swipe Actions private func swipeActionsFor(_ download: Download) -> [SwipeAction] { diff --git a/Yattee/Views/Home/OpenLinkSheet.swift b/Yattee/Views/Home/OpenLinkSheet.swift index 696f177d..f572605a 100644 --- a/Yattee/Views/Home/OpenLinkSheet.swift +++ b/Yattee/Views/Home/OpenLinkSheet.swift @@ -83,7 +83,6 @@ struct OpenLinkView: View { struct OpenLinkFormView: View { @Environment(\.appEnvironment) private var appEnvironment - let prefilledURL: URL? let onRequestDismiss: (() -> Void)? @State private var urlText: String @@ -103,7 +102,6 @@ struct OpenLinkFormView: View { fileprivate static let maxURLs = 20 init(prefilledURL: URL?, onRequestDismiss: (() -> Void)?) { - self.prefilledURL = prefilledURL self.onRequestDismiss = onRequestDismiss _urlText = State(initialValue: prefilledURL?.absoluteString ?? "") } diff --git a/Yattee/Views/Instances/InstanceBrowseView.swift b/Yattee/Views/Instances/InstanceBrowseView.swift index ec2c9a90..0599bfe5 100644 --- a/Yattee/Views/Instances/InstanceBrowseView.swift +++ b/Yattee/Views/Instances/InstanceBrowseView.swift @@ -87,12 +87,6 @@ struct InstanceBrowseView: View { } private var yatteeServerURL: URL? { yatteeServer?.url } - /// Auth header for Yattee Server instances (when browsing a Yattee Server directly) - private var yatteeServerAuthHeader: String? { - guard instance.type == .yatteeServer else { return nil } - return appEnvironment?.basicAuthCredentialsManager.basicAuthHeader(for: instance) - } - /// Auth header for avatar loading (uses Yattee Server for YouTube channel avatars) private var avatarAuthHeader: String? { guard let server = yatteeServer else { return nil } diff --git a/Yattee/Views/MediaBrowser/MediaBrowserView.swift b/Yattee/Views/MediaBrowser/MediaBrowserView.swift index dd234c7f..f96fbb70 100644 --- a/Yattee/Views/MediaBrowser/MediaBrowserView.swift +++ b/Yattee/Views/MediaBrowser/MediaBrowserView.swift @@ -11,7 +11,6 @@ struct MediaBrowserView: View { @Environment(\.appEnvironment) private var appEnvironment let source: MediaSource - let initialPath: String @Namespace private var sheetTransition @State private var currentPath: String @@ -33,7 +32,6 @@ struct MediaBrowserView: View { init(source: MediaSource, path: String = "/", showOnlyPlayable: Bool = false) { self.source = source - self.initialPath = path _currentPath = State(initialValue: path) let defaults = UserDefaults.standard diff --git a/Yattee/Views/Navigation/SidebarAdaptableModifier.swift b/Yattee/Views/Navigation/SidebarAdaptableModifier.swift deleted file mode 100644 index 93b5835b..00000000 --- a/Yattee/Views/Navigation/SidebarAdaptableModifier.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// SidebarAdaptableModifier.swift -// Yattee -// -// Navigation-related view modifiers. -// - -import SwiftUI - -// MARK: - Sidebar Adaptable Modifier - -#if os(iOS) -struct SidebarAdaptableModifier: ViewModifier { - func body(content: Content) -> some View { - content.tabViewStyle(.sidebarAdaptable) - } -} - -extension View { - /// Applies sidebar adaptable style. - func sidebarAdaptable() -> some View { - modifier(SidebarAdaptableModifier()) - } -} -#endif diff --git a/Yattee/Views/Navigation/SidebarChannelRow.swift b/Yattee/Views/Navigation/SidebarChannelRow.swift deleted file mode 100644 index a2f05c9d..00000000 --- a/Yattee/Views/Navigation/SidebarChannelRow.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// SidebarChannelRow.swift -// Yattee -// -// Compact channel row for sidebar display. -// - -import SwiftUI -import NukeUI - -struct SidebarChannelRow: View { - let name: String - let avatarURL: URL? - var authHeader: String? - - var body: some View { - HStack(spacing: 8) { - // Avatar - LazyImage(request: AvatarURLBuilder.imageRequest(url: avatarURL, authHeader: authHeader)) { state in - if let image = state.image { - image - .resizable() - .aspectRatio(contentMode: .fill) - } else { - avatarPlaceholder - } - } - .frame(width: 24, height: 24) - .clipShape(Circle()) - - // Name - Text(name) - .lineLimit(1) - .truncationMode(.tail) - } - } - - private var avatarPlaceholder: some View { - Circle() - .fill(.quaternary) - .overlay { - Text(String(name.prefix(1)).uppercased()) - .font(.caption2.bold()) - .foregroundStyle(.secondary) - } - } -} - -// MARK: - Preview - -#if !os(tvOS) -#Preview { - List { - SidebarChannelRow( - name: "Technology Reviews", - avatarURL: nil - ) - SidebarChannelRow( - name: "Music & Sound Design", - avatarURL: nil - ) - SidebarChannelRow( - name: "A Very Long Channel Name That Should Be Truncated", - avatarURL: nil - ) - } - .listStyle(.sidebar) -} -#endif diff --git a/Yattee/Views/Navigation/SidebarPlaylistRow.swift b/Yattee/Views/Navigation/SidebarPlaylistRow.swift deleted file mode 100644 index 2dd3474f..00000000 --- a/Yattee/Views/Navigation/SidebarPlaylistRow.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// SidebarPlaylistRow.swift -// Yattee -// -// Compact playlist row for sidebar display. -// - -import SwiftUI - -struct SidebarPlaylistRow: View { - let title: String - let videoCount: Int - - var body: some View { - HStack(spacing: 8) { - // Icon - Image(systemName: "list.bullet.rectangle") - .font(.system(size: 14)) - .foregroundStyle(.secondary) - .frame(width: 24, height: 24) - - // Title and count - Text(title) - .lineLimit(1) - .truncationMode(.tail) - - Spacer() - - // Video count badge - if videoCount > 0 { - Text("\(videoCount)") - .font(.caption2.monospacedDigit()) - .foregroundStyle(.secondary) - } - } - } -} - -// MARK: - Preview - -#if !os(tvOS) -#Preview { - List { - SidebarPlaylistRow( - title: "Watch Later", - videoCount: 12 - ) - SidebarPlaylistRow( - title: "Favorites", - videoCount: 45 - ) - SidebarPlaylistRow( - title: "A Very Long Playlist Name That Should Be Truncated", - videoCount: 100 - ) - SidebarPlaylistRow( - title: "Empty Playlist", - videoCount: 0 - ) - } - .listStyle(.sidebar) -} -#endif diff --git a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift index 08a42c61..d3f7de2f 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift @@ -13,9 +13,6 @@ import NukeUI extension ExpandedPlayerSheet { // MARK: - Layout Constants - /// Minimum width required to show widescreen layout with floating panel. - static let minWidthForWidescreen: CGFloat = 700 - /// Minimum width required for the corner layout; below this the corner /// preset setting falls back to the stacked widescreen layout (Split View, /// narrow windows). @@ -98,91 +95,6 @@ extension ExpandedPlayerSheet { } #endif - // MARK: - Download Action Handler - - #if !os(tvOS) - /// Handles download button action based on current state. - func handleDownloadAction(for video: Video) { - let isDownloaded = downloadManager?.isDownloaded(video.id) ?? false - let download = downloadManager?.download(for: video.id) - let isDownloading = download?.status == .downloading || download?.status == .queued - - if isDownloading, let download { - Task { - await downloadManager?.cancel(download) - } - } else if isDownloaded { - showingDeleteDownloadAlert = true - } else { - startDownload(for: video) - } - } - - /// Starts a download either automatically or by showing the quality sheet. - private func startDownload(for video: Video) { - guard let appEnvironment else { - presentDownloadOptions() - return - } - - // Media source videos (SMB/WebDAV/local) use direct file URLs - no API call needed - if video.isFromMediaSource { - Task { - do { - try await appEnvironment.downloadManager.autoEnqueueMediaSource( - video, - mediaSourcesManager: appEnvironment.mediaSourcesManager, - webDAVClient: appEnvironment.webDAVClient, - smbClient: appEnvironment.smbClient - ) - } catch { - appEnvironment.toastManager.show( - category: .error, - title: String(localized: "download.error.title"), - subtitle: error.localizedDescription, - icon: "exclamationmark.triangle", - iconColor: .red - ) - } - } - return - } - - let downloadSettings = appEnvironment.downloadSettings - - // Check if auto-download mode - if downloadSettings.preferredDownloadQuality != .ask, - let instance = appEnvironment.instancesManager.instance(for: video) { - Task { - do { - try await appEnvironment.downloadManager.autoEnqueue( - video, - preferredQuality: downloadSettings.preferredDownloadQuality, - preferredAudioLanguage: appEnvironment.settingsManager.preferredAudioLanguage, - preferredSubtitlesLanguage: appEnvironment.settingsManager.preferredSubtitlesLanguage, - includeSubtitles: downloadSettings.includeSubtitlesInAutoDownload, - contentService: appEnvironment.contentService, - instance: instance - ) - } catch { - appEnvironment.toastManager.show( - category: .error, - title: String(localized: "download.error.title"), - subtitle: error.localizedDescription, - icon: "exclamationmark.triangle", - iconColor: .red - ) - } - } - } else { - presentDownloadOptions() - } - } - #else - /// No-op on tvOS. - func handleDownloadAction(for video: Video) {} - #endif - // MARK: - Standard/Portrait Layout /// Standard player content for portrait mode with video at top and panel below. @@ -768,20 +680,6 @@ extension ExpandedPlayerSheet { appEnvironment?.settingsManager.portraitDetailsPanelLastVisible = newValue } - /// Hide floating panel with animation - private func hideFloatingPanel() { - withAnimation(.easeInOut(duration: 0.3)) { - isPortraitPanelVisible = false - } - navigationCoordinator?.isPortraitPanelVisible = false - appEnvironment?.settingsManager.portraitDetailsPanelLastVisible = false - - // Reset main window transform when hiding panel to fix safe area clipping (iOS only) - #if os(iOS) - ExpandedPlayerWindowManager.shared.resetMainWindowImmediate() - #endif - } - // MARK: - Portrait Player Area /// Player area for portrait layout that separates video (panscan-scaled) from controls. @@ -1688,8 +1586,6 @@ extension ExpandedPlayerSheet { panelSide: panelSide, isPanelPinned: isPanelPinned, isCornerLayout: appEnvironment?.navigationCoordinator.isCornerLayoutActive == true, - layoutLeadingSafeArea: 0.01, // Non-zero indicates safe areas handled externally - layoutTrailingSafeArea: 0.01, onToggleOrientationLock: { [weak appEnvironment] in appEnvironment?.settingsManager.inAppOrientationLock.toggle() }, diff --git a/Yattee/Views/Player/ExpandedPlayerSheet+VideoInfo.swift b/Yattee/Views/Player/ExpandedPlayerSheet+VideoInfo.swift index 86ad4f8d..7eb8e927 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet+VideoInfo.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet+VideoInfo.swift @@ -12,23 +12,6 @@ import SwiftUI extension ExpandedPlayerSheet { // MARK: - Scroll & Comments - /// Scrolls the content to the top. - func scrollToTop() { - withAnimation(.easeOut(duration: 0.25)) { - scrollPosition.scrollTo(y: 0) - } - } - - /// Expands the comments overlay. - func expandComments() { - // Scroll player to top so video is fully visible - scrollPosition.scrollTo(y: 0) - // Use same animation as player sheet expand (0.3s, no bounce) - withAnimation(.smooth(duration: 0.3)) { - isCommentsExpanded = true - } - } - /// Collapses the comments overlay. func collapseComments() { navigationCoordinator?.commentsFrame = .zero diff --git a/Yattee/Views/Player/ExpandedPlayerSheet.swift b/Yattee/Views/Player/ExpandedPlayerSheet.swift index 8c69435f..9316d789 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet.swift @@ -13,7 +13,6 @@ import SwiftUI struct ExpandedPlayerSheet: View { @Environment(\.appEnvironment) var appEnvironment - @Environment(\.dismiss) private var dismiss // MARK: - Sheet State @@ -23,9 +22,7 @@ struct ExpandedPlayerSheet: View { @State var commentsDismissOffset: CGFloat = 0 /// Direct scroll position control - use scrollTo(y:) to scroll to top @State var scrollPosition: ScrollPosition = .init(y: 0) - @State private var currentScrollOffset: CGFloat = 0 @State var currentPlayerHeight: CGFloat = 0 - @State private var isBottomOverscroll: Bool = false @State var isPortraitPanelVisible: Bool = true /// Temporarily hide controls during fullscreen transition to avoid animation glitch @State var hideControlsDuringTransition: Bool = false @@ -95,9 +92,6 @@ struct ExpandedPlayerSheet: View { /// Task for preloading comments - cancelled when video changes @State var commentsPreloadTask: Task<Void, Never>? - /// Namespace for matchedGeometryEffect transitions - @Namespace var playerNamespace - // MARK: - Layout State (iOS) #if os(iOS) @@ -120,9 +114,7 @@ struct ExpandedPlayerSheet: View { var playerService: PlayerService? { appEnvironment?.playerService } var playerState: PlayerState? { playerService?.state } - var downloadManager: DownloadManager? { appEnvironment?.downloadManager } var navigationCoordinator: NavigationCoordinator? { appEnvironment?.navigationCoordinator } - var accentColor: Color { appEnvironment?.settingsManager.resolvedAccentColor ?? .accentColor } #if os(iOS) var inAppOrientationLock: Bool { appEnvironment?.settingsManager.inAppOrientationLock ?? false } @@ -136,7 +128,6 @@ struct ExpandedPlayerSheet: View { // MARK: - Queue Helpers var queue: [QueuedVideo] { playerState?.queue ?? [] } - var history: [QueuedVideo] { playerState?.history ?? [] } var isQueueEnabled: Bool { appEnvironment?.settingsManager.queueEnabled ?? true } var hasQueueItems: Bool { !queue.isEmpty && isQueueEnabled } @@ -165,7 +156,6 @@ struct ExpandedPlayerSheet: View { let isFailed = if case .failed = state { true } else { false } let hasBackend = playerService?.currentBackend != nil && !isLoading && !isIdle && !isEnded && !isFailed return PlaybackInfo( - state: state, isLoading: isLoading, isIdle: isIdle, isEnded: isEnded, @@ -188,18 +178,6 @@ struct ExpandedPlayerSheet: View { presentSheet(.addToPlaylist(video)) } - #if !os(tvOS) - func presentDownloadOptions() { - guard let video = playerState?.currentVideo, let playerService = appEnvironment?.playerService else { return } - presentSheet(.downloadOptions( - video, - streams: playerService.availableStreams, - captions: playerService.availableCaptions, - dislikeCount: playerState?.dislikeCount - )) - } - #endif - // MARK: - Body var body: some View { diff --git a/Yattee/Views/Player/ExpandedPlayerWindow.swift b/Yattee/Views/Player/ExpandedPlayerWindow.swift index 29e1b38f..9a010b1f 100644 --- a/Yattee/Views/Player/ExpandedPlayerWindow.swift +++ b/Yattee/Views/Player/ExpandedPlayerWindow.swift @@ -187,14 +187,12 @@ private final class DragToDismissGestureHandler: NSObject, UIGestureRecognizerDe // Main window scaling callbacks (Apple Music-style effect) var onMainWindowScaleChanged: ((CGFloat) -> Void)? var onMainWindowScaleAnimated: ((CGFloat) -> Void)? // Animated scale to progress - var onMainWindowScaleReset: ((Bool) -> Void)? // Bool = animated private let dismissThreshold: CGFloat = 100 private var isDismissing = false private var scrollViewAtTop = false private weak var trackedScrollView: UIScrollView? private var scrollObservation: NSKeyValueObservation? - private var originalBackgroundColor: UIColor? // Panscan gesture state private var basePanscan: Double = 0.0 @@ -632,12 +630,6 @@ final class ExpandedPlayerWindowManager { return window } - /// The view to apply transforms to (first subview of main window) - /// Note: For actual transformations, use scaledPresentingView which caches the reference - private var presentingView: UIView? { - mainWindow?.subviews.first - } - /// Applies scale and corner radius to main window based on progress (0 = normal, 1 = fully scaled) func scaleMainWindow(progress: CGFloat) { // Capture the presenting view reference on first call @@ -689,18 +681,6 @@ final class ExpandedPlayerWindowManager { } } - /// Resets main window to normal state with animation - func resetMainWindowAnimated(duration: TimeInterval = 0.3) { - let view = scaledPresentingView ?? mainWindow?.subviews.first - UIView.animate(withDuration: duration) { - view?.layer.cornerRadius = 0 - view?.transform = .identity - } completion: { [weak self] _ in - view?.layer.masksToBounds = false - self?.scaledPresentingView = nil - } - } - /// Resets main window to normal state immediately func resetMainWindowImmediate() { // Reset ALL subviews of the main window to handle rotation edge cases @@ -764,10 +744,6 @@ final class ExpandedPlayerWindowManager { } } - var isPresented: Bool { - expandedWindow != nil - } - func show(with appEnvironment: AppEnvironment, animated: Bool = true) { LoggingService.shared.logPlayer("[ExpandedPlayerWindowManager] show called, expandedWindow=\(expandedWindow != nil), animated=\(animated), retryCount=\(showRetryCount)") guard expandedWindow == nil else { return } diff --git a/Yattee/Views/Player/ExpandedPlayerWindowManager.swift b/Yattee/Views/Player/ExpandedPlayerWindowManager.swift index 0dfd7865..9e191c1c 100644 --- a/Yattee/Views/Player/ExpandedPlayerWindowManager.swift +++ b/Yattee/Views/Player/ExpandedPlayerWindowManager.swift @@ -660,30 +660,6 @@ final class ExpandedPlayerWindowManager: NSObject { LoggingService.shared.debug("ExpandedPlayerWindowManager: restored corner panels after fullscreen", category: .player) } - /// Restores a window that was hidden for PiP mode. - /// Call this when returning from PiP to show the player window again. - func restoreFromPiP(animated: Bool = true) { - guard let window = playerWindow else { - LoggingService.shared.debug("ExpandedPlayerWindowManager: restoreFromPiP - no window to restore", category: .player) - return - } - - LoggingService.shared.debug("ExpandedPlayerWindowManager: restoreFromPiP called", category: .player) - - if animated { - window.alphaValue = 0 - window.makeKeyAndOrderFront(nil) - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.25 - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - window.animator().alphaValue = 1 - } - } else { - window.alphaValue = 1 - window.makeKeyAndOrderFront(nil) - } - } - /// Cleans up a window that was hidden for PiP when PiP ends without restoring. /// Call this when PiP is closed via the X button (not restore). func cleanupAfterPiP() { diff --git a/Yattee/Views/Player/Gestures/GestureSeekPreviewView.swift b/Yattee/Views/Player/Gestures/GestureSeekPreviewView.swift index 3cd233be..40ec8b24 100644 --- a/Yattee/Views/Player/Gestures/GestureSeekPreviewView.swift +++ b/Yattee/Views/Player/Gestures/GestureSeekPreviewView.swift @@ -13,10 +13,8 @@ import SwiftUI struct GestureSeekPreviewView: View { let storyboard: Storyboard? let seekTime: TimeInterval - let duration: TimeInterval let storyboardService: StoryboardService let buttonBackground: ButtonBackgroundStyle - let theme: ControlsTheme let chapters: [VideoChapter] let isActive: Bool var availableWidth: CGFloat = 320 @@ -42,8 +40,7 @@ struct GestureSeekPreviewView: View { storyboard: storyboard, seekTime: seekTime, storyboardService: storyboardService, - buttonBackground: buttonBackground, - theme: theme + buttonBackground: buttonBackground ) } } @@ -70,10 +67,8 @@ struct GestureSeekPreviewView: View { GestureSeekPreviewView( storyboard: nil, seekTime: 180, - duration: 600, storyboardService: StoryboardService(), buttonBackground: .regularGlass, - theme: .dark, chapters: [], isActive: true ) diff --git a/Yattee/Views/Player/Gestures/TapGestureFeedbackView.swift b/Yattee/Views/Player/Gestures/TapGestureFeedbackView.swift index 9ae81eb5..3fe83b71 100644 --- a/Yattee/Views/Player/Gestures/TapGestureFeedbackView.swift +++ b/Yattee/Views/Player/Gestures/TapGestureFeedbackView.swift @@ -203,7 +203,6 @@ struct SeekFeedbackView: View { // Ripple circles ForEach(0..<3) { index in SeekRipple( - isForward: isForward, delay: Double(index) * 0.1, isActive: rippleCount > index ) @@ -252,7 +251,6 @@ struct SeekFeedbackView: View { } private struct SeekRipple: View { - let isForward: Bool let delay: Double let isActive: Bool diff --git a/Yattee/Views/Player/Gestures/TapZoneCalculator.swift b/Yattee/Views/Player/Gestures/TapZoneCalculator.swift index d554e934..695e8667 100644 --- a/Yattee/Views/Player/Gestures/TapZoneCalculator.swift +++ b/Yattee/Views/Player/Gestures/TapZoneCalculator.swift @@ -63,88 +63,5 @@ enum TapZoneCalculator { } } } - - /// Returns the frame for a specific zone position within bounds. - /// - Parameters: - /// - position: The zone position. - /// - bounds: The total gesture-recognizable area bounds. - /// - layout: The current tap zone layout. - /// - Returns: The frame for the zone, or nil if position doesn't match layout. - static func frame( - for position: TapZonePosition, - in bounds: CGRect, - layout: TapZoneLayout - ) -> CGRect? { - // Use safe area for calculations - let safeArea = bounds.insetBy(dx: safeMargin, dy: safeMargin) - - switch layout { - case .single: - guard position == .full else { return nil } - return safeArea - - case .horizontalSplit: - let halfWidth = safeArea.width / 2 - switch position { - case .left: - return CGRect(x: safeArea.minX, y: safeArea.minY, - width: halfWidth, height: safeArea.height) - case .right: - return CGRect(x: safeArea.minX + halfWidth, y: safeArea.minY, - width: halfWidth, height: safeArea.height) - default: - return nil - } - - case .verticalSplit: - let halfHeight = safeArea.height / 2 - switch position { - case .top: - return CGRect(x: safeArea.minX, y: safeArea.minY, - width: safeArea.width, height: halfHeight) - case .bottom: - return CGRect(x: safeArea.minX, y: safeArea.minY + halfHeight, - width: safeArea.width, height: halfHeight) - default: - return nil - } - - case .threeColumns: - let thirdWidth = safeArea.width / 3 - switch position { - case .leftThird: - return CGRect(x: safeArea.minX, y: safeArea.minY, - width: thirdWidth, height: safeArea.height) - case .center: - return CGRect(x: safeArea.minX + thirdWidth, y: safeArea.minY, - width: thirdWidth, height: safeArea.height) - case .rightThird: - return CGRect(x: safeArea.minX + thirdWidth * 2, y: safeArea.minY, - width: thirdWidth, height: safeArea.height) - default: - return nil - } - - case .quadrants: - let halfWidth = safeArea.width / 2 - let halfHeight = safeArea.height / 2 - switch position { - case .topLeft: - return CGRect(x: safeArea.minX, y: safeArea.minY, - width: halfWidth, height: halfHeight) - case .topRight: - return CGRect(x: safeArea.minX + halfWidth, y: safeArea.minY, - width: halfWidth, height: halfHeight) - case .bottomLeft: - return CGRect(x: safeArea.minX, y: safeArea.minY + halfHeight, - width: halfWidth, height: halfHeight) - case .bottomRight: - return CGRect(x: safeArea.minX + halfWidth, y: safeArea.minY + halfHeight, - width: halfWidth, height: halfHeight) - default: - return nil - } - } - } } #endif diff --git a/Yattee/Views/Player/MiniPlayerView.swift b/Yattee/Views/Player/MiniPlayerView.swift index 38c402c8..d3d7e224 100644 --- a/Yattee/Views/Player/MiniPlayerView.swift +++ b/Yattee/Views/Player/MiniPlayerView.swift @@ -25,7 +25,6 @@ struct MiniPlayerView: View { private var deArrowProvider: DeArrowBrandingProvider? { appEnvironment?.deArrowBrandingProvider } private var queueManager: QueueManager? { appEnvironment?.queueManager } private var playerControlsLayoutService: PlayerControlsLayoutService? { appEnvironment?.playerControlsLayoutService } - private var settingsManager: SettingsManager? { appEnvironment?.settingsManager } private var currentVideo: Video? { playerState?.currentVideo } diff --git a/Yattee/Views/Player/Panels/PlayerPanelContentView.swift b/Yattee/Views/Player/Panels/PlayerPanelContentView.swift index 649148cd..d44174ad 100644 --- a/Yattee/Views/Player/Panels/PlayerPanelContentView.swift +++ b/Yattee/Views/Player/Panels/PlayerPanelContentView.swift @@ -71,18 +71,6 @@ struct PlayerPanelContentView: View { } } - /// Enabled pages that currently have content, in configured order. - static func visiblePages( - panelSettings: PanelSettings, - region: PanelRegion?, - video: Video, - appEnvironment: AppEnvironment? - ) -> [PanelPageConfiguration] { - panelSettings.enabledPages(in: region).filter { - isRuntimeVisible($0.pageType, video: video, appEnvironment: appEnvironment) - } - } - /// Enabled pages for this host in configured order, before runtime /// visibility filtering. The selection tracks this list so a page that /// loads late (e.g. related) still wins once its content arrives. @@ -194,7 +182,7 @@ struct PlayerPanelContentView: View { ChaptersPanelPage(context: context, settings: page.chaptersSettings, scrollConfig: scrollConfig) .id(PanelPageType.chapters) case .queue: - QueuePanelPage(context: context, settings: page.queueSettings, bottomInset: scrollConfig.bottomInset) + QueuePanelPage(settings: page.queueSettings, bottomInset: scrollConfig.bottomInset) .id(PanelPageType.queue) } } diff --git a/Yattee/Views/Player/Panels/QueuePanelPage.swift b/Yattee/Views/Player/Panels/QueuePanelPage.swift index 345654da..9db072ab 100644 --- a/Yattee/Views/Player/Panels/QueuePanelPage.swift +++ b/Yattee/Views/Player/Panels/QueuePanelPage.swift @@ -10,7 +10,6 @@ import SwiftUI #if os(iOS) || os(macOS) struct QueuePanelPage: View { - let context: PanelPageContext var settings: QueuePageSettings = QueuePageSettings() var bottomInset: CGFloat = 0 diff --git a/Yattee/Views/Player/PlayerControlsActions.swift b/Yattee/Views/Player/PlayerControlsActions.swift index f018adfa..31aea796 100644 --- a/Yattee/Views/Player/PlayerControlsActions.swift +++ b/Yattee/Views/Player/PlayerControlsActions.swift @@ -95,9 +95,7 @@ struct PlayerControlsActions { let currentAudioStream: Stream? /// Embedded (in-container) tracks reported by mpv for the loaded file - var embeddedAudioTracks: [MPVTrack] = [] var embeddedSubtitleTracks: [MPVTrack] = [] - var currentEmbeddedAudioTrackID: Int? = nil var currentEmbeddedSubtitleTrackID: Int? = nil #if os(iOS) @@ -234,11 +232,6 @@ struct PlayerControlsActions { return String(format: "%.2gx", rate.rawValue) } - /// Whether share is available (has video with share URL) - var canShare: Bool { - currentVideo != nil - } - /// Whether add to playlist is available var canAddToPlaylist: Bool { guard let video = currentVideo else { return false } diff --git a/Yattee/Views/Player/PlayerControlsView.swift b/Yattee/Views/Player/PlayerControlsView.swift index b17b3732..637175eb 100644 --- a/Yattee/Views/Player/PlayerControlsView.swift +++ b/Yattee/Views/Player/PlayerControlsView.swift @@ -45,9 +45,6 @@ struct PlayerControlsView: View { var isPanelPinned: Bool = false /// Corner layout: fullscreen button toggles the panels var isCornerLayout: Bool = false - /// Safe area padding from layout (when panel is pinned) - var layoutLeadingSafeArea: CGFloat = 0 - var layoutTrailingSafeArea: CGFloat = 0 var onToggleOrientationLock: (() -> Void)? = nil var isOrientationLocked: Bool = false var onToggleDetailsVisibility: (() -> Void)? = nil @@ -140,11 +137,8 @@ struct PlayerControlsView: View { @State private var targetSeekProgress: Double = 0 @State private var hideTimer: Timer? @State private var showControls: Bool? - @State private var isAdjustingVolume = false - @State private var isAdjustingBrightness = false /// Tracks brightness value for side slider (needed because UIScreen.main.brightness isn't observable) @State private var currentBrightness: Double = UIScreen.main.brightness - @State private var playNextTapCount = 0 /// Track safe area to force re-render when it changes @State private var currentSafeArea: UIEdgeInsets = .zero /// Window hosting these controls, captured by `HostWindowReader`. Safe @@ -440,9 +434,7 @@ struct PlayerControlsView: View { availableStreams: availableStreams, currentStream: currentStream, currentAudioStream: currentAudioStream, - embeddedAudioTracks: embeddedAudioTracks, embeddedSubtitleTracks: embeddedSubtitleTracks, - currentEmbeddedAudioTrackID: currentEmbeddedAudioTrackID, currentEmbeddedSubtitleTrackID: currentEmbeddedSubtitleTrackID, panscanValue: panscanValue, isPanscanAllowed: isPanscanAllowed, @@ -1044,8 +1036,7 @@ struct PlayerControlsView: View { storyboard: storyboard, seekTime: seekTime, storyboardService: StoryboardService.shared, - buttonBackground: activeLayout.globalSettings.buttonBackground, - theme: activeLayout.globalSettings.theme + buttonBackground: activeLayout.globalSettings.buttonBackground ) .position(x: xPosition, y: yPosition) .transition(.opacity.combined(with: .scale(scale: 0.9))) @@ -1212,10 +1203,8 @@ struct PlayerControlsView: View { GestureSeekPreviewView( storyboard: playerState.preferredStoryboard, seekTime: seekGesturePreviewTime, - duration: playerState.duration, storyboardService: StoryboardService.shared, buttonBackground: activeLayout.globalSettings.buttonBackground, - theme: activeLayout.globalSettings.theme, chapters: playerState.chapters, isActive: isSeekGestureActive, availableWidth: gestureGeometry.size.width diff --git a/Yattee/Views/Player/PlayerHelperViews.swift b/Yattee/Views/Player/PlayerHelperViews.swift index 2e055d1f..86a57998 100644 --- a/Yattee/Views/Player/PlayerHelperViews.swift +++ b/Yattee/Views/Player/PlayerHelperViews.swift @@ -13,7 +13,6 @@ import SwiftUI /// Holds computed playback state flags to avoid duplicating these checks struct PlaybackInfo { - let state: PlaybackState let isLoading: Bool let isIdle: Bool let isEnded: Bool diff --git a/Yattee/Views/Player/PortraitDetailsPanel.swift b/Yattee/Views/Player/PortraitDetailsPanel.swift index 5393817c..8b4d9e89 100644 --- a/Yattee/Views/Player/PortraitDetailsPanel.swift +++ b/Yattee/Views/Player/PortraitDetailsPanel.swift @@ -59,8 +59,6 @@ struct PortraitDetailsPanel: View { // Queue helpers private var queue: [QueuedVideo] { playerState?.queue ?? [] } - private var history: [QueuedVideo] { playerState?.history ?? [] } - private var isQueueEnabled: Bool { settingsManager?.queueEnabled ?? true } // Player pill helpers private var playerPillSettings: PlayerPillSettings { diff --git a/Yattee/Views/Player/QualitySelector/QualitySelectorTypes.swift b/Yattee/Views/Player/QualitySelector/QualitySelectorTypes.swift index 311adb7f..61fc1736 100644 --- a/Yattee/Views/Player/QualitySelector/QualitySelectorTypes.swift +++ b/Yattee/Views/Player/QualitySelector/QualitySelectorTypes.swift @@ -12,18 +12,6 @@ enum QualitySelectorTab: String, CaseIterable, Sendable { case video case audio case subtitles - - /// Localized display label for the tab. - var label: String { - switch self { - case .video: - String(localized: "player.quality.video") - case .audio: - String(localized: "stream.audio") - case .subtitles: - String(localized: "stream.subtitles") - } - } } /// Navigation destination for quality selector detail pages. diff --git a/Yattee/Views/Player/QualitySelectorView+StreamHelpers.swift b/Yattee/Views/Player/QualitySelectorView+StreamHelpers.swift index bf4427ae..be853427 100644 --- a/Yattee/Views/Player/QualitySelectorView+StreamHelpers.swift +++ b/Yattee/Views/Player/QualitySelectorView+StreamHelpers.swift @@ -349,77 +349,4 @@ extension QualitySelectorView { } return allRates[currentIndex + 1] } - - // MARK: - Formatting Helpers - - /// Formats a file size from bytes. - func formatFileSize(_ bytes: Int64) -> String { - ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file) - } - - /// Formats stream details (bitrate and file size). - func formatStreamDetails(bitrate: Int?, fileSize: String?) -> String { - var parts: [String] = [] - if let bitrate { - parts.append(formatBitrate(bitrate)) - } - if let fileSize { - parts.append(fileSize) - } - return parts.joined(separator: " · ") - } - - /// Formats audio details (track type, bitrate, file size). - func formatAudioDetails(trackType: String?, bitrate: Int?, fileSize: String?) -> String { - var parts: [String] = [] - if let trackType { - parts.append(trackType) - } - if let bitrate { - parts.append(formatBitrate(bitrate)) - } - if let fileSize { - parts.append(fileSize) - } - return parts.joined(separator: " · ") - } - - /// Formats a codec string for display. - func formatCodec(_ codec: String) -> String { - let lowercased: String = codec.lowercased() - if lowercased.contains("avc") || lowercased.contains("h264") { - return "H.264" - } else if lowercased.contains("hev") || lowercased.contains("h265") || lowercased.contains("hevc") { - return "HEVC" - } else if lowercased.contains("vp9") || lowercased.contains("vp09") { - return "VP9" - } else if lowercased.contains("av1") || lowercased.contains("av01") { - return "AV1" - } - return codec.uppercased() - } - - /// Returns a color for a codec badge. - func codecColor(_ codec: String) -> Color { - let lowercased: String = codec.lowercased() - if lowercased.contains("av1") || lowercased.contains("av01") { - return .blue - } else if lowercased.contains("vp9") || lowercased.contains("vp09") { - return .orange - } else if lowercased.contains("avc") || lowercased.contains("h264") { - return .red - } else if lowercased.contains("hev") || lowercased.contains("h265") || lowercased.contains("hevc") { - return .green - } - return .gray - } - - /// Formats a bitrate for display. - func formatBitrate(_ bitrate: Int) -> String { - if bitrate >= 1_000_000 { - return String(format: "%.1f Mbps", Double(bitrate) / 1_000_000) - } else { - return "\(bitrate / 1000) kbps" - } - } } diff --git a/Yattee/Views/Player/QueueManagementSheet.swift b/Yattee/Views/Player/QueueManagementSheet.swift index 0e1fea10..9ac5a9d4 100644 --- a/Yattee/Views/Player/QueueManagementSheet.swift +++ b/Yattee/Views/Player/QueueManagementSheet.swift @@ -26,12 +26,9 @@ struct QueueManagementSheet: View { private var queueManager: QueueManager? { appEnvironment?.queueManager } private var playerState: PlayerState? { appEnvironment?.playerService.state } - private var navigationCoordinator: NavigationCoordinator? { appEnvironment?.navigationCoordinator } private var queue: [QueuedVideo] { playerState?.queue ?? [] } private var history: [QueuedVideo] { playerState?.history ?? [] } - private var isLoadingMore: Bool { queueManager?.isLoadingMore ?? false } - private var hasMoreItems: Bool { queueManager?.hasMoreItems() ?? false } private var queueSourceLabel: String? { queueManager?.currentQueueSourceLabel } var body: some View { diff --git a/Yattee/Views/Player/SeekPreviewView.swift b/Yattee/Views/Player/SeekPreviewView.swift index ad1b83f3..9240bd0f 100644 --- a/Yattee/Views/Player/SeekPreviewView.swift +++ b/Yattee/Views/Player/SeekPreviewView.swift @@ -50,7 +50,6 @@ struct SeekPreviewView: View { let seekTime: TimeInterval let storyboardService: StoryboardService let buttonBackground: ButtonBackgroundStyle - let theme: ControlsTheme @State private var thumbnail: PlatformImage? @State private var loadTask: Task<Void, Never>? diff --git a/Yattee/Views/Player/SegmentedProgressBar.swift b/Yattee/Views/Player/SegmentedProgressBar.swift index 32c62f90..5d99ec27 100644 --- a/Yattee/Views/Player/SegmentedProgressBar.swift +++ b/Yattee/Views/Player/SegmentedProgressBar.swift @@ -102,10 +102,8 @@ struct SegmentedProgressBar: View { ChapterSegmentView( chapter: chapter, nextChapterStart: chapterEnd, - duration: duration, currentTime: currentTime, bufferedTime: bufferedTime, - segmentWidth: segmentWidth, playedColor: playedColor, bufferedColor: bufferedColor, backgroundColor: backgroundColor, @@ -182,10 +180,8 @@ struct SegmentedProgressBar: View { private struct ChapterSegmentView: View { let chapter: VideoChapter let nextChapterStart: TimeInterval - let duration: TimeInterval let currentTime: TimeInterval let bufferedTime: TimeInterval - let segmentWidth: CGFloat let playedColor: Color let bufferedColor: Color let backgroundColor: Color diff --git a/Yattee/Views/Player/macOS/MacOSControlBar.swift b/Yattee/Views/Player/macOS/MacOSControlBar.swift index 2ea8bccd..e406879c 100644 --- a/Yattee/Views/Player/macOS/MacOSControlBar.swift +++ b/Yattee/Views/Player/macOS/MacOSControlBar.swift @@ -57,11 +57,6 @@ struct MacOSControlBar: View { isDragging ? dragProgress : playerState.progress } - private var bufferedProgress: Double { - guard playerState.duration > 0 else { return 0 } - return playerState.bufferedTime / playerState.duration - } - // MARK: - Body var body: some View { @@ -251,8 +246,7 @@ struct MacOSControlBar: View { storyboard: storyboard, seekTime: seekTime, storyboardService: StoryboardService.shared, - buttonBackground: .none, - theme: .dark + buttonBackground: .none ) .transition(.opacity.combined(with: .scale(scale: 0.9))) .animation(.easeInOut(duration: 0.15), value: isDragging || isHoveringProgress) diff --git a/Yattee/Views/Player/tvOS/TVDetailsPanel.swift b/Yattee/Views/Player/tvOS/TVDetailsPanel.swift index 53cc83cd..1cbb8ce0 100644 --- a/Yattee/Views/Player/tvOS/TVDetailsPanel.swift +++ b/Yattee/Views/Player/tvOS/TVDetailsPanel.swift @@ -12,7 +12,6 @@ import NukeUI /// Details panel that slides up from the bottom showing video information. struct TVDetailsPanel: View { let video: Video? - let initialTab: TVDetailsTab let onDismiss: () -> Void @Environment(\.appEnvironment) private var appEnvironment @@ -22,7 +21,6 @@ struct TVDetailsPanel: View { init(video: Video?, initialTab: TVDetailsTab = .info, onDismiss: @escaping () -> Void) { self.video = video - self.initialTab = initialTab self.onDismiss = onDismiss _selectedTab = State(initialValue: initialTab) } @@ -302,7 +300,6 @@ enum TVDetailsTab: String, CaseIterable { enum TVDetailsFocusItem: Hashable { case tabPicker case channelButton - case description } /// Scrollable description view with click-to-lock scrolling. @@ -615,7 +612,7 @@ struct TVFocusableCommentView: View { } label: { commentContent } - .buttonStyle(TVCommentButtonStyle(hasReplies: comment.replyCount > 0)) + .buttonStyle(TVCommentButtonStyle()) // Replies section if showReplies && comment.replyCount > 0 { @@ -791,7 +788,7 @@ struct TVReplyView: View { } label: { replyContent } - .buttonStyle(TVCommentButtonStyle(hasReplies: false)) + .buttonStyle(TVCommentButtonStyle()) } private var replyContent: some View { @@ -865,7 +862,6 @@ struct TVReplyView: View { /// Button style for focusable comments. struct TVCommentButtonStyle: ButtonStyle { - let hasReplies: Bool @Environment(\.isFocused) private var isFocused func makeBody(configuration: Configuration) -> some View { diff --git a/Yattee/Views/Player/tvOS/TVPlayerProgressBar.swift b/Yattee/Views/Player/tvOS/TVPlayerProgressBar.swift index bdcf74f5..7f5b3595 100644 --- a/Yattee/Views/Player/tvOS/TVPlayerProgressBar.swift +++ b/Yattee/Views/Player/tvOS/TVPlayerProgressBar.swift @@ -76,12 +76,6 @@ struct TVPlayerProgressBar: View { return min(max(displayTime / duration, 0), 1) } - /// Buffered progress as a fraction (0-1). - private var bufferedProgress: Double { - guard duration > 0 else { return 0 } - return min(bufferedTime / duration, 1) - } - var body: some View { Button { if isScrubbing { @@ -92,7 +86,7 @@ struct TVPlayerProgressBar: View { } label: { progressContent } - .buttonStyle(TVProgressBarButtonStyle(isFocused: isFocused)) + .buttonStyle(TVProgressBarButtonStyle()) .disabled(isLive) .overlay { // Gesture capture layer (only when scrubbing). Siri Remote pan @@ -602,8 +596,6 @@ class TVPanGestureUIView: UIView { /// Button style for the progress bar. struct TVProgressBarButtonStyle: ButtonStyle { - let isFocused: Bool - func makeBody(configuration: Configuration) -> some View { configuration.label .scaleEffect(configuration.isPressed ? 0.98 : 1.0) diff --git a/Yattee/Views/Playlist/PlaylistVideoRowView.swift b/Yattee/Views/Playlist/PlaylistVideoRowView.swift index 21f5f309..ba1a1473 100644 --- a/Yattee/Views/Playlist/PlaylistVideoRowView.swift +++ b/Yattee/Views/Playlist/PlaylistVideoRowView.swift @@ -38,18 +38,6 @@ struct PlaylistVideoRowView: View { } } -// MARK: - Convenience Initializers - -extension PlaylistVideoRowView { - /// Initialize from a LocalPlaylistItem model. - init(item: LocalPlaylistItem, index: Int, watchProgress: Double? = nil, onRemove: @escaping () -> Void) { - self.index = index - self.video = item.toVideo() - self.watchProgress = watchProgress - self.onRemove = onRemove - } -} - // MARK: - Preview #Preview { diff --git a/Yattee/Views/Playlist/TappablePlaylistVideoRow.swift b/Yattee/Views/Playlist/TappablePlaylistVideoRow.swift deleted file mode 100644 index 2c7a3208..00000000 --- a/Yattee/Views/Playlist/TappablePlaylistVideoRow.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// TappablePlaylistVideoRow.swift -// Yattee -// -// Tappable row view for playlist videos. -// - -import SwiftUI - -/// A playlist video row that plays the video when tapped. -/// -/// Note: For new code, consider using `PlaylistVideoRowView(...).tappableVideo(video, includeContextMenu: false)` directly. -struct TappablePlaylistVideoRow: View { - let video: Video - let index: Int - - var body: some View { - PlaylistVideoRowView( - index: index, - video: video - ) - .padding(.horizontal) - .padding(.vertical, 8) - .tappableVideo(video, includeContextMenu: false) - } -} - -// MARK: - Preview - -#Preview { - List { - TappablePlaylistVideoRow( - video: Video( - id: .global("test"), - title: "Test Video", - description: nil, - author: Author(id: "author", name: "Test Channel"), - duration: 360, - publishedAt: nil, - publishedText: "2 days ago", - viewCount: 10000, - likeCount: nil, - thumbnails: [], - isLive: false, - isUpcoming: false, - scheduledStartTime: nil - ), - index: 1 - ) - } - .appEnvironment(.preview) -} diff --git a/Yattee/Views/RemoteControl/RemoteControlView.swift b/Yattee/Views/RemoteControl/RemoteControlView.swift index 1c9d82f1..7bb93ec9 100644 --- a/Yattee/Views/RemoteControl/RemoteControlView.swift +++ b/Yattee/Views/RemoteControl/RemoteControlView.swift @@ -804,11 +804,6 @@ struct RemoteControlView: View { isConnecting = false } - private func disconnect() { - remoteControl?.disconnect(from: device) - isConnected = false - } - /// Returns the previous playback rate, or nil if at minimum. private func previousRate() -> PlaybackRate? { let allRates = PlaybackRate.allCases diff --git a/Yattee/Views/Search/SearchView.swift b/Yattee/Views/Search/SearchView.swift index 999f67ed..9ccd1a69 100644 --- a/Yattee/Views/Search/SearchView.swift +++ b/Yattee/Views/Search/SearchView.swift @@ -89,10 +89,6 @@ struct SearchView: View { searchViewModel?.hasResults ?? false } - private var hasSearched: Bool { - searchViewModel?.hasSearched ?? false - } - /// Initialize SearchView with optional external search text binding init(searchText: Binding<String>? = nil, initialQuery: String? = nil) { self.externalSearchText = searchText @@ -1372,11 +1368,6 @@ struct SearchView: View { appEnvironment?.dataManager.deleteSearchQuery(history) } - private func clearAllHistory() { - appEnvironment?.dataManager.clearSearchHistory() - isSearchHistoryExpanded = false - } - private func executeSearch(_ query: String) { dismissKeyboard() suppressNextTextChangeSearch = (query != searchTextBinding.wrappedValue) @@ -1417,14 +1408,6 @@ struct SearchView: View { appEnvironment?.dataManager.deleteRecentPlaylist(playlist) } - private func clearAllRecentChannels() { - appEnvironment?.dataManager.clearRecentChannels() - } - - private func clearAllRecentPlaylists() { - appEnvironment?.dataManager.clearRecentPlaylists() - } - private func clearAllRecents() { appEnvironment?.dataManager.clearSearchHistory() appEnvironment?.dataManager.clearRecentChannels() diff --git a/Yattee/Views/Settings/EditSourceView.swift b/Yattee/Views/Settings/EditSourceView.swift index 2f901d0a..5fb2d00a 100644 --- a/Yattee/Views/Settings/EditSourceView.swift +++ b/Yattee/Views/Settings/EditSourceView.swift @@ -10,9 +10,6 @@ import SwiftUI struct EditSourceView: View { let source: UnifiedSource - @Environment(\.dismiss) private var dismiss - @Environment(\.appEnvironment) private var appEnvironment - var body: some View { switch source { case .remoteServer(let instance): @@ -400,10 +397,6 @@ private struct EditRemoteServerContent: View { // MARK: - Computed Properties - private var hasYatteeServer: Bool { - appEnvironment?.instancesManager.hasYatteeServerInstances ?? false - } - private func invidiousDisplayValue(_ info: InstanceDetectorModels.YatteeServerFullInfo) -> String { guard let invidiousInstance = info.config?.invidiousInstance else { return String(localized: "sources.serverInfo.notConfigured") diff --git a/Yattee/Views/Settings/PlayerControls/MiniPlayerEditorView.swift b/Yattee/Views/Settings/PlayerControls/MiniPlayerEditorView.swift index a0268ffc..22721ac3 100644 --- a/Yattee/Views/Settings/PlayerControls/MiniPlayerEditorView.swift +++ b/Yattee/Views/Settings/PlayerControls/MiniPlayerEditorView.swift @@ -17,8 +17,7 @@ struct MiniPlayerEditorView: View { var body: some View { List { MiniPlayerPreviewView( - buttons: viewModel.miniPlayerButtons, - showVideo: showVideo + buttons: viewModel.miniPlayerButtons ) .listRowInsets(EdgeInsets()) .listRowBackground(Color.clear) @@ -213,7 +212,6 @@ struct MiniPlayerEditorView: View { private struct MiniPlayerPreviewView: View { let buttons: [ControlButtonConfiguration] - let showVideo: Bool private let buttonSize: CGFloat = 28 private let buttonSpacing: CGFloat = 4 diff --git a/Yattee/Views/Settings/PlayerControls/PlayerControlsPreviewView.swift b/Yattee/Views/Settings/PlayerControls/PlayerControlsPreviewView.swift index 32812b59..e3fb4363 100644 --- a/Yattee/Views/Settings/PlayerControls/PlayerControlsPreviewView.swift +++ b/Yattee/Views/Settings/PlayerControls/PlayerControlsPreviewView.swift @@ -12,7 +12,6 @@ import SwiftUI /// A view modifier that applies the controls theme color scheme to preview. struct PreviewThemeModifier: ViewModifier { let theme: ControlsTheme - @Environment(\.colorScheme) private var systemColorScheme func body(content: Content) -> some View { if let forcedScheme = theme.colorScheme { diff --git a/Yattee/Views/Settings/RemoteControlSettingsView.swift b/Yattee/Views/Settings/RemoteControlSettingsView.swift deleted file mode 100644 index 1de3ad90..00000000 --- a/Yattee/Views/Settings/RemoteControlSettingsView.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// RemoteControlSettingsView.swift -// Yattee -// -// Settings for remote control configuration. -// - -import SwiftUI - -struct RemoteControlSettingsView: View { - var body: some View { - RemoteControlContentView(navigationStyle: .link) - .navigationTitle(String(localized: "remoteControl.title")) - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif - } -} - -// MARK: - Preview - -#Preview { - NavigationStack { - RemoteControlSettingsView() - } - .appEnvironment(.preview) -} diff --git a/Yattee/Views/Settings/SourceRow.swift b/Yattee/Views/Settings/SourceRow.swift deleted file mode 100644 index e5354f6e..00000000 --- a/Yattee/Views/Settings/SourceRow.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// SourceRow.swift -// Yattee -// -// Unified row view for displaying any source type in the sources list. -// - -import SwiftUI - -struct SourceRow: View { - let source: UnifiedSource - let onEdit: () -> Void - - @Environment(\.appEnvironment) private var appEnvironment - - /// Computed property that checks password status reactively for WebDAV sources - private var needsPassword: Bool { - guard case .fileSource(let mediaSource) = source, - mediaSource.type == .webdav else { return false } - return appEnvironment?.mediaSourcesManager.needsPassword(for: mediaSource) ?? false - } - - /// Computed property that checks instance status for auth issues - private var instanceStatus: InstanceStatus? { - guard case .remoteServer(let instance) = source else { return nil } - return appEnvironment?.instancesManager.status(for: instance) - } - - /// Whether this instance has auth issues (failed or required) - private var hasAuthIssue: Bool { - guard let status = instanceStatus else { return false } - return status == .authFailed || status == .authRequired - } - - /// Whether a remote server instance requires credentials that are missing from - /// the Keychain (e.g. after reinstalling and importing sources from iCloud). - private var needsCredentials: Bool { - guard case .remoteServer(let instance) = source else { return false } - return appEnvironment?.needsCredentials(for: instance) ?? false - } - - /// Whether one of this source's logins is being refused by the server. - /// - /// The instance-level statuses know nothing about accounts, so a rejected - /// login would otherwise show up nowhere the user looks for their sources. - private var hasRejectedAccount: Bool { - guard case .remoteServer(let instance) = source, - let accountsManager = appEnvironment?.accountsManager else { return false } - - return accountsManager.accounts(forInstance: instance.id).contains { - accountsManager.status(for: $0.id) == .credentialRejected - } - } - - var body: some View { - #if os(tvOS) - Button(action: onEdit) { - rowContent - } - .buttonStyle(.card) - .accessibilityIdentifier(accessibilityId) - #else - rowContent - .contentShape(Rectangle()) - .onTapGesture(perform: onEdit) - .accessibilityElement(children: .combine) - .accessibilityIdentifier(accessibilityId) - #endif - } - - private var rowContent: some View { - HStack { - VStack(alignment: .leading, spacing: 4) { - HStack { - Text(source.name) - .font(.headline) - - if !source.isEnabled { - Text(String(localized: "sources.status.disabled")) - .font(.caption) - .foregroundStyle(.secondary) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(.quaternary) - .clipShape(Capsule()) - } - } - - Text(source.urlDisplayString) - .font(.caption) - .foregroundStyle(.secondary) - - statusView - } - - Spacer() - } - } - - /// Generates a unique accessibility identifier for the source row. - /// Format: sources.row.<type>.<host> - private var accessibilityId: String { - switch source { - case .remoteServer(let instance): - let host = instance.url.host ?? "unknown" - return "sources.row.\(instance.type.rawValue).\(host)" - case .fileSource(let mediaSource): - let identifier = mediaSource.url.host ?? mediaSource.name.replacingOccurrences(of: " ", with: "_") - return "sources.row.\(mediaSource.type.rawValue).\(identifier)" - } - } - - @ViewBuilder - private var statusView: some View { - // A refused login comes first: it names one thing the user has to do, - // and two warnings stacked on one row say less than either alone. - if hasRejectedAccount { - Label( - AccountStatusBadge.text(for: .credentialRejected), - systemImage: "person.crop.circle.badge.exclamationmark" - ) - .font(.caption2) - .foregroundStyle(AccountStatusBadge.color(for: .credentialRejected)) - } else if needsPassword || needsCredentials { - Label(String(localized: "sources.status.authRequired"), systemImage: "key.fill") - .font(.caption2) - .foregroundStyle(.orange) - } else if hasAuthIssue { - // Show auth issue for remote servers (Yattee Server auth failed) - if instanceStatus == .authFailed { - Label(String(localized: "sources.status.authFailed"), systemImage: "exclamationmark.triangle.fill") - .font(.caption2) - .foregroundStyle(.orange) - } else { - Label(String(localized: "sources.status.authRequired"), systemImage: "key.fill") - .font(.caption2) - .foregroundStyle(.orange) - } - } - } -} - -// MARK: - Preview - -#Preview { - List { - SourceRow( - source: .remoteServer(Instance(type: .invidious, url: URL(string: "https://invidious.example.com")!)), - onEdit: {} - ) - SourceRow( - source: .fileSource(.webdav(name: "My NAS", url: URL(string: "https://nas.local:5006")!)), - onEdit: {} - ) - SourceRow( - source: .fileSource(.localFolder(name: "Movies", url: URL(fileURLWithPath: "/Users/user/Movies"))), - onEdit: {} - ) - } - .appEnvironment(.preview) -} diff --git a/Yattee/Views/Subscriptions/ManageChannelsView.swift b/Yattee/Views/Subscriptions/ManageChannelsView.swift index 29f37133..a7941119 100644 --- a/Yattee/Views/Subscriptions/ManageChannelsView.swift +++ b/Yattee/Views/Subscriptions/ManageChannelsView.swift @@ -131,7 +131,6 @@ struct ManageChannelsView: View { private var dataManager: DataManager? { appEnvironment?.dataManager } private var subscriptionService: SubscriptionService? { appEnvironment?.subscriptionService } - private var settingsManager: SettingsManager? { appEnvironment?.settingsManager } private var toastManager: ToastManager? { appEnvironment?.toastManager } private var yatteeServer: Instance? { appEnvironment?.instancesManager.enabledYatteeServerInstances.first diff --git a/YatteeTests/AccountSyncTests.swift b/YatteeTests/AccountSyncTests.swift index 590352a5..cf2d9be6 100644 --- a/YatteeTests/AccountSyncTests.swift +++ b/YatteeTests/AccountSyncTests.swift @@ -55,7 +55,6 @@ struct AccountSyncTests { } private struct Harness { - let defaults: UserDefaults let accounts: AccountsManager let store: AccountSyncStore let coordinator: AccountSyncCoordinator @@ -109,7 +108,6 @@ struct AccountSyncTests { coordinator.attach(database: nil) { recorder.record($0) } return Harness( - defaults: defaults, accounts: accounts, store: store, coordinator: coordinator, @@ -1152,7 +1150,6 @@ struct AccountSyncTests { let scope: CredentialIdentityScope let instances: InstancesManager let accounts: AccountsManager - let store: AccountSyncStore let coordinator: AccountSyncCoordinator /// Held for the length of the test: the manager's activation hook /// only borrows it. @@ -1261,7 +1258,6 @@ struct AccountSyncTests { scope: scope, instances: instances, accounts: accounts, - store: store, coordinator: coordinator, importer: importer, recorder: recorder, diff --git a/YatteeTests/AccountsScreenTests.swift b/YatteeTests/AccountsScreenTests.swift index faa17b7f..20bf162c 100644 --- a/YatteeTests/AccountsScreenTests.swift +++ b/YatteeTests/AccountsScreenTests.swift @@ -231,7 +231,6 @@ private final class DeletionOrderStore: AccountCredentialsManager { self.accountsManager = accountsManager } - var loggedInAccountIDs: Set<UUID> { [] } var isScopeResolved: Bool { true } func setCredential(_: String, for _: Account) {} @@ -245,7 +244,6 @@ private final class DeletionOrderStore: AccountCredentialsManager { func migrateCredential(forAccountID _: UUID, fromScope _: String) -> Bool { true } func deleteCredential(forAccountID _: UUID, inScope _: String) -> Bool { true } - func resyncCredentialsWithCurrentSetting() {} func setUsername(_: String, for _: Account) {} func username(for _: Account) -> String? { nil } func isLoggedIn(for _: Account) -> Bool { false } diff --git a/YatteeTests/Integration/TestConstants.swift b/YatteeTests/Integration/TestConstants.swift index 7adf7736..9f6d2365 100644 --- a/YatteeTests/Integration/TestConstants.swift +++ b/YatteeTests/Integration/TestConstants.swift @@ -26,12 +26,6 @@ enum IntegrationTestConstants { /// Rick Astley's channel ID. static let testChannelID = "UCuAXFkgsw1L7xaCfnd5JJOw" - /// A popular music playlist ID. - static let testPlaylistID = "PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf" - /// A stable search query. static let testSearchQuery = "never gonna give you up" - - /// Timeout for network requests (30 seconds). - static let networkTimeout: TimeInterval = 30 } diff --git a/YatteeTests/LocalNamespaceImportTests.swift b/YatteeTests/LocalNamespaceImportTests.swift index f16b6f13..3a4657d8 100644 --- a/YatteeTests/LocalNamespaceImportTests.swift +++ b/YatteeTests/LocalNamespaceImportTests.swift @@ -849,7 +849,6 @@ private final class TestCredentialsManager: AccountCredentialsManager { ) } - var loggedInAccountIDs: Set<UUID> { credentials.loggedInAccountIDs } var isScopeResolved: Bool { credentials.isScopeResolved } func setCredential(_ credential: String, for account: Account) { @@ -864,8 +863,6 @@ private final class TestCredentialsManager: AccountCredentialsManager { credentials.deleteCredential(forAccountID: accountID) } - func resyncCredentialsWithCurrentSetting() { credentials.resyncCredentialsWithCurrentSetting() } - func setUsername(_ username: String, for account: Account) { credentials.setUsername(username, for: account) } @@ -916,14 +913,12 @@ private final class FlakyCredentialsManager: AccountCredentialsManager { private(set) var migrateCalls = 0 private(set) var deleteCalls = 0 - var loggedInAccountIDs: Set<UUID> { [] } var isScopeResolved: Bool { true } func setCredential(_: String, for _: Account) {} func credential(for _: Account) -> String? { nil } func deleteCredential(for _: Account) {} func deleteCredential(forAccountID _: UUID) {} - func resyncCredentialsWithCurrentSetting() {} func setUsername(_: String, for _: Account) {} func username(for _: Account) -> String? { nil } func isLoggedIn(for _: Account) -> Bool { false }