From c6012cc71fdfed15a3383205fa0183a5d4932132 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Fri, 4 Oct 2024 17:11:46 -0500 Subject: [PATCH 1/3] Rework how core services are created --- BookPlayer.xcodeproj/project.pbxproj | 24 +- BookPlayer/AppDelegate.swift | 256 +++++------------- .../AppIntents/CustomRewindIntent.swift | 36 +-- .../AppIntents/CustomSkipForwardIntent.swift | 36 +-- .../LastBookStartPlaybackIntent.swift | 34 +-- .../AppIntents/PausePlaybackIntent.swift | 23 +- .../DataInitializerCoordinator.swift | 75 ++--- .../Coordinators/ItemListCoordinator.swift | 37 ++- .../Coordinators/LibraryListCoordinator.swift | 98 ++++--- .../Coordinators/LoadingCoordinator.swift | 4 +- BookPlayer/Coordinators/MainCoordinator.swift | 42 ++- BookPlayer/Loading/LoadingViewModel.swift | 4 +- BookPlayer/Player/BPPlayerError.swift | 13 + BookPlayer/Player/PlayerLoaderService.swift | 76 ++++++ BookPlayer/Player/PlayerManager.swift | 34 --- BookPlayer/Player/PlayerManagerProtocol.swift | 46 ++++ BookPlayer/Services/ActionParserService.swift | 18 +- BookPlayer/Services/CarPlayManager.swift | 160 ++++++----- .../Services/ListSyncRefreshService.swift | 54 +++- .../PlayerSettingsViewController.swift | 29 +- BookPlayer/Utils/CoreServices.swift | 11 +- .../Phone/BookPlaybackToggleIntent.swift | 25 +- .../Phone/BookStartPlaybackIntent.swift | 45 --- Shared/Constants.swift | 3 - .../Extensions/UserDefaults+BookPlayer.swift | 8 - Shared/Services/LibraryService.swift | 2 +- 26 files changed, 613 insertions(+), 580 deletions(-) create mode 100644 BookPlayer/Player/BPPlayerError.swift create mode 100644 BookPlayer/Player/PlayerLoaderService.swift create mode 100644 BookPlayer/Player/PlayerManagerProtocol.swift delete mode 100644 BookPlayerWidgets/Phone/BookStartPlaybackIntent.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index e858a138..9e80bf0d 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -358,7 +358,6 @@ 634BA5A52C176B5A0015314D /* StoryActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5A42C176B5A0015314D /* StoryActionView.swift */; }; 634BA5A72C1777BB0015314D /* PricingBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5A62C1777BA0015314D /* PricingBoxView.swift */; }; 634BA5AD2C180F5E0015314D /* StoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5AC2C180F5E0015314D /* StoryViewModel.swift */; }; - 634E67462AFC7DEF00595BAC /* BookStartPlaybackIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634E67432AFB2DF500595BAC /* BookStartPlaybackIntent.swift */; }; 6354CD9C2B4902CE006D9551 /* DebugInformationActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6354CD9B2B4902CE006D9551 /* DebugInformationActivityItemSource.swift */; }; 6356D48C2C584EFD00994B71 /* CustomSkipForwardIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6356D48B2C584EFD00994B71 /* CustomSkipForwardIntent.swift */; }; 6356F9B52AC7CC5600B7A027 /* CancelSleepTimerIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6356F9B42AC7CC5600B7A027 /* CancelSleepTimerIntent.swift */; }; @@ -417,7 +416,6 @@ 63B760F72C32734000AA98C7 /* SecondOnboardingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B760F62C32734000AA98C7 /* SecondOnboardingResponse.swift */; }; 63B760F92C32738E00AA98C7 /* StoryAccountSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B760F82C32738E00AA98C7 /* StoryAccountSubscriptionService.swift */; }; 63B760FC2C33B77F00AA98C7 /* SupportProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B760FB2C33B77F00AA98C7 /* SupportProfileView.swift */; }; - 63C1A8AF2B09158600C4B418 /* BookStartPlaybackIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634E67432AFB2DF500595BAC /* BookStartPlaybackIntent.swift */; }; 63C1A8B02B0915EE00C4B418 /* WidgetUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 418445C2258AE11E0072DD13 /* WidgetUtils.swift */; }; 63C1A8B22B09166F00C4B418 /* WidgetEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 637DAB092AEB3E0D006DC2D1 /* WidgetEntries.swift */; }; 63C6C2E62B5029BC00FFE0D8 /* SettingsAutolockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C6C2E52B5029BC00FFE0D8 /* SettingsAutolockView.swift */; }; @@ -428,6 +426,12 @@ 63C6C3122B54F16800FFE0D8 /* LibraryItemSyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C6C30F2B54F14800FFE0D8 /* LibraryItemSyncOperation.swift */; }; 63C6C3192B5E102200FFE0D8 /* SyncTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C6C3172B5E0FE700FFE0D8 /* SyncTask.swift */; }; 63C6C31A2B5E102200FFE0D8 /* SyncTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C6C3172B5E0FE700FFE0D8 /* SyncTask.swift */; }; + 63E893922CAFA89000946CD4 /* BPPlayerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893912CAFA89000946CD4 /* BPPlayerError.swift */; }; + 63E893932CAFA89000946CD4 /* BPPlayerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893912CAFA89000946CD4 /* BPPlayerError.swift */; }; + 63E893952CAFAB8F00946CD4 /* PlayerLoaderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893942CAFAB8F00946CD4 /* PlayerLoaderService.swift */; }; + 63E893962CAFAB8F00946CD4 /* PlayerLoaderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893942CAFAB8F00946CD4 /* PlayerLoaderService.swift */; }; + 63E893982CAFAC7500946CD4 /* PlayerManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893972CAFAC7500946CD4 /* PlayerManagerProtocol.swift */; }; + 63E893992CAFAC7500946CD4 /* PlayerManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893972CAFAC7500946CD4 /* PlayerManagerProtocol.swift */; }; 63F1C7892BB91260006B164C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 63F1C7882BB91259006B164C /* PrivacyInfo.xcprivacy */; }; 63F1C78B2BB91E21006B164C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 63F1C78A2BB91E1B006B164C /* PrivacyInfo.xcprivacy */; }; 63F828572AED56FA00B5CE0C /* CornerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F828562AED56FA00B5CE0C /* CornerView.swift */; }; @@ -1121,7 +1125,6 @@ 634BA5A42C176B5A0015314D /* StoryActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryActionView.swift; sourceTree = ""; }; 634BA5A62C1777BA0015314D /* PricingBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PricingBoxView.swift; sourceTree = ""; }; 634BA5AC2C180F5E0015314D /* StoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryViewModel.swift; sourceTree = ""; }; - 634E67432AFB2DF500595BAC /* BookStartPlaybackIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookStartPlaybackIntent.swift; sourceTree = ""; }; 6354CD9B2B4902CE006D9551 /* DebugInformationActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugInformationActivityItemSource.swift; sourceTree = ""; }; 6356D48B2C584EFD00994B71 /* CustomSkipForwardIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSkipForwardIntent.swift; sourceTree = ""; }; 6356F9B42AC7CC5600B7A027 /* CancelSleepTimerIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelSleepTimerIntent.swift; sourceTree = ""; }; @@ -1192,6 +1195,9 @@ 63C6C30B2B538B7A00FFE0D8 /* SyncTasksStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncTasksStorage.swift; sourceTree = ""; }; 63C6C30F2B54F14800FFE0D8 /* LibraryItemSyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryItemSyncOperation.swift; sourceTree = ""; }; 63C6C3172B5E0FE700FFE0D8 /* SyncTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncTask.swift; sourceTree = ""; }; + 63E893912CAFA89000946CD4 /* BPPlayerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPPlayerError.swift; sourceTree = ""; }; + 63E893942CAFAB8F00946CD4 /* PlayerLoaderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerLoaderService.swift; sourceTree = ""; }; + 63E893972CAFAC7500946CD4 /* PlayerManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerManagerProtocol.swift; sourceTree = ""; }; 63F1C7882BB91259006B164C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 63F1C78A2BB91E1B006B164C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 63F828562AED56FA00B5CE0C /* CornerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerView.swift; sourceTree = ""; }; @@ -2303,7 +2309,6 @@ 63E5D6A92AECB8AB00A67B32 /* Phone */ = { isa = PBXGroup; children = ( - 634E67432AFB2DF500595BAC /* BookStartPlaybackIntent.swift */, 6309F1252B0CF1C1002B86A4 /* BookPlaybackToggleIntent.swift */, 6397208B2CAC95040045A4DB /* LaunchAppControlWidgetView.swift */, 4106413E258725F1008EB8D0 /* TimeListened */, @@ -2752,6 +2757,9 @@ 9F64C6222793C37C00B2493C /* Controls Screen */, 9F3D0CE328C2BF2A00E9E8A3 /* ButtonFree Screen */, 410D0FF01EDF659900A52EB9 /* PlayerManager.swift */, + 63E893972CAFAC7500946CD4 /* PlayerManagerProtocol.swift */, + 63E893912CAFA89000946CD4 /* BPPlayerError.swift */, + 63E893942CAFAB8F00946CD4 /* PlayerLoaderService.swift */, 6308260C2AF6C312002ACE0D /* WidgetReloadService.swift */, 9F5011F82A6580800075FEBA /* ShakeMotionService.swift */, C3EC372D206EE0650094B4E8 /* SleepTimer.swift */, @@ -3463,6 +3471,7 @@ 6397208A2CAC5C870045A4DB /* LastPlayedModel.swift in Sources */, 417D9994256DE3FB00C3B753 /* LastPlayedWidgetView.swift in Sources */, 41A359C7276232E00020D5F5 /* MappingModel_v7_to_v8.xcmappingmodel in Sources */, + 63E893992CAFAC7500946CD4 /* PlayerManagerProtocol.swift in Sources */, 416A29AA2569658100605395 /* BookPlayerWidgets.swift in Sources */, 630826052AF522FF002ACE0D /* RectangularView.swift in Sources */, 41D20DB425D5F5A100AAEE30 /* MappingModel_v1_to_v2.xcmappingmodel in Sources */, @@ -3471,16 +3480,17 @@ 630826032AF5225F002ACE0D /* CircularView.swift in Sources */, 6397208C2CAC95040045A4DB /* LaunchAppControlWidgetView.swift in Sources */, 630826042AF522EA002ACE0D /* SharedWidgetEntry.swift in Sources */, + 63E893962CAFAB8F00946CD4 /* PlayerLoaderService.swift in Sources */, 41064152258726D2008EB8D0 /* TimeListenedMediumView.swift in Sources */, 41C3396A25E04112003ED2B0 /* MappingModel_v2_to_v3.xcmappingmodel in Sources */, 416A29DA256AB6A900605395 /* BookPlayer.xcdatamodeld in Sources */, 630826112AF6CA44002ACE0D /* SharedIconWidget.swift in Sources */, 637DAB0B2AEB3F6D006DC2D1 /* WidgetEntries.swift in Sources */, 418CABB625EF28FC00D8C878 /* MappingModel_v3_to_v4.xcmappingmodel in Sources */, + 63E893932CAFA89000946CD4 /* BPPlayerError.swift in Sources */, 41ADD6DA2570AC6300660C64 /* RecentBooksWidgetView.swift in Sources */, 630826162AF6CABD002ACE0D /* SharedIconWidgetEntry.swift in Sources */, 9FF383D52A40F97000BBAC11 /* MappingModel_v8_to_v9.xcmappingmodel in Sources */, - 634E67462AFC7DEF00595BAC /* BookStartPlaybackIntent.swift in Sources */, 639720832CAB0C380045A4DB /* LastPlayedView.swift in Sources */, 639720852CABB0D00045A4DB /* RecentBooksProvider.swift in Sources */, 630826072AF52831002ACE0D /* SharedWidget.swift in Sources */, @@ -3501,6 +3511,7 @@ 9F4691F02800C97000A8F0E8 /* CompleteAccountViewModel.swift in Sources */, 9F2681AD2888B26100359BD3 /* LoginBenefitView.swift in Sources */, 634BA5A72C1777BB0015314D /* PricingBoxView.swift in Sources */, + 63E893982CAFAC7500946CD4 /* PlayerManagerProtocol.swift in Sources */, 9F588DBF2902C798000DA799 /* ComposedButton.swift in Sources */, 4151A6B326E491A800E49DBE /* NibLoadableView.swift in Sources */, 4197FAFC267E480100811CC8 /* ImportViewController.swift in Sources */, @@ -3584,6 +3595,7 @@ 634BA5A12C174F9D0015314D /* StoryViewerViewModel.swift in Sources */, 4142964921F2E2BA004356DA /* ThemeCellView.swift in Sources */, 9F4691F72800F85600A8F0E8 /* AccountViewModel.swift in Sources */, + 63E893922CAFA89000946CD4 /* BPPlayerError.swift in Sources */, 4160A0A123F304530039166B /* LocalizableLabel.swift in Sources */, 4137BBCC272DAF2E009ED9FE /* MVVMControllerProtocol.swift in Sources */, C318D3AD208CF624000666F8 /* PlayerJumpIcon.swift in Sources */, @@ -3601,6 +3613,7 @@ 4138CE1C26E5B42F0014F11E /* BookmarksViewModel.swift in Sources */, 9F3D0CE528C2BF5C00E9E8A3 /* ButtonFreeViewController.swift in Sources */, 4197240021874D5F00AB1190 /* UserActivityManager.swift in Sources */, + 63E893952CAFAB8F00946CD4 /* PlayerLoaderService.swift in Sources */, 634BA5972C161FBE0015314D /* StoryView.swift in Sources */, 6356F9C52AC86D9200B7A027 /* BPAppShortcuts.swift in Sources */, 69343D332133844D000C425E /* VoiceOverService.swift in Sources */, @@ -3630,7 +3643,6 @@ 415B274421FAE47200F9D9B7 /* LoadingView.swift in Sources */, 41E79BED26C61D2B00EA9FFF /* PlayPauseIconView.swift in Sources */, 9F4691F52800F84800A8F0E8 /* AccountViewController.swift in Sources */, - 63C1A8AF2B09158600C4B418 /* BookStartPlaybackIntent.swift in Sources */, 9FEC87B027FA9F0F006C71D5 /* LoginViewController.swift in Sources */, 9F00A5FA294F8BFE005EA316 /* ClearableTextField.swift in Sources */, 9F5FBB08293EDCD8009F4B0E /* ItemDetailsViewController.swift in Sources */, diff --git a/BookPlayer/AppDelegate.swift b/BookPlayer/AppDelegate.swift index bddec417..b28bcb4b 100644 --- a/BookPlayer/AppDelegate.swift +++ b/BookPlayer/AppDelegate.swift @@ -7,6 +7,7 @@ // import AVFoundation +import AppIntents import BackgroundTasks import BookPlayerKit import Combine @@ -30,13 +31,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { var documentFolderWatcher: DirectoryWatcher? var sharedFolderWatcher: DirectoryWatcher? - var dataManager: DataManager? - var accountService: AccountServiceProtocol? - var syncService: SyncServiceProtocol? - var libraryService: LibraryService? - var playbackService: PlaybackServiceProtocol? - var playerManager: PlayerManagerProtocol? - var watchConnectivityService: PhoneWatchConnectivityService? + let databaseInitializer = DatabaseInitializer() + var coreServices: CoreServices? + /// Internal property used as a fallback in ``activeSceneDelegate`` var lastSceneToResignActive: SceneDelegate? /// Access the current (or last) active scene delegate to present VCs or alerts @@ -53,11 +50,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { } /// Reference for observers private var crashReportsAccessObserver: NSKeyValueObservation? - private var sharedWidgetActionURLObserver: NSKeyValueObservation? /// Background refresh task identifier private lazy var refreshTaskIdentifier = "\(Bundle.main.configurationString(for: .bundleIdentifier)).background.refresh" + /// Reference to the task that creates the core services + var setupCoreServicesTask: Task<(), Error>? + var errorCoreServicesSetup: Error? + func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -83,8 +83,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { self.setupSentry() // Setup Realm self.setupRealm() - // Setup observer for interactive widgets - self.setupSharedWidgetActionObserver() + // Setup core services + self.setupCoreServices() return true } @@ -104,62 +104,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { completionHandler(response) } - // swiftlint:disable:next function_body_length func createCoreServicesIfNeeded(from stack: CoreDataStack) -> CoreServices { - let dataManager: DataManager - - if let sharedDataManager = AppDelegate.shared?.dataManager { - dataManager = sharedDataManager - } else { - dataManager = DataManager(coreDataStack: stack) - AppDelegate.shared?.dataManager = dataManager - } - - let accountService: AccountServiceProtocol - - if let sharedAccountService = AppDelegate.shared?.accountService { - accountService = sharedAccountService + if let coreServices = self.coreServices { + return coreServices } else { - accountService = AccountService(dataManager: dataManager) - AppDelegate.shared?.accountService = accountService - } - - let libraryService: LibraryService - - if let sharedLibraryService = AppDelegate.shared?.libraryService { - libraryService = sharedLibraryService - } else { - libraryService = LibraryService(dataManager: dataManager) - AppDelegate.shared?.libraryService = libraryService - } - - let syncService: SyncServiceProtocol - - if let sharedSyncService = AppDelegate.shared?.syncService { - syncService = sharedSyncService - } else { - syncService = SyncService( + let dataManager = DataManager(coreDataStack: stack) + let accountService = AccountService(dataManager: dataManager) + let libraryService = LibraryService(dataManager: dataManager) + let syncService = SyncService( isActive: accountService.hasSyncEnabled(), libraryService: libraryService ) - AppDelegate.shared?.syncService = syncService - } - - let playbackService: PlaybackServiceProtocol - - if let sharedPlaybackService = AppDelegate.shared?.playbackService { - playbackService = sharedPlaybackService - } else { - playbackService = PlaybackService(libraryService: libraryService) - AppDelegate.shared?.playbackService = playbackService - } - - let playerManager: PlayerManagerProtocol - - if let sharedPlayerManager = AppDelegate.shared?.playerManager { - playerManager = sharedPlayerManager - } else { - playerManager = PlayerManager( + let playbackService = PlaybackService(libraryService: libraryService) + let playerManager = PlayerManager( libraryService: libraryService, playbackService: playbackService, syncService: syncService, @@ -167,101 +124,31 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { shakeMotionService: ShakeMotionService(), widgetReloadService: WidgetReloadService() ) - AppDelegate.shared?.playerManager = playerManager - } - - let watchService: PhoneWatchConnectivityService - - if let sharedWatchService = AppDelegate.shared?.watchConnectivityService { - watchService = sharedWatchService - } else { - watchService = PhoneWatchConnectivityService( + let watchService = PhoneWatchConnectivityService( libraryService: libraryService, playbackService: playbackService, playerManager: playerManager ) - AppDelegate.shared?.watchConnectivityService = watchService - } - - return CoreServices( - dataManager: dataManager, - accountService: accountService, - syncService: syncService, - libraryService: libraryService, - playbackService: playbackService, - playerManager: playerManager, - watchService: watchService - ) - } - - func loadPlayer( - _ relativePath: String, - autoplay: Bool, - showPlayer: (() -> Void)?, - alertPresenter: AlertPresenter, - recordAsLastBook: Bool = true - ) { - Task { @MainActor in - let fileURL = DataManager.getProcessedFolderURL().appendingPathComponent(relativePath) - - if syncService?.isActive == false, - !FileManager.default.fileExists(atPath: fileURL.path) - { - alertPresenter.showAlert( - "file_missing_title".localized, - message: - "\("file_missing_description".localized)\n\(fileURL.lastPathComponent)", - completion: nil - ) - return - } - - // Only load if loaded book is a different one - if playerManager?.hasLoadedBook() == true, - relativePath == playerManager?.currentItem?.relativePath - { - if autoplay { - playerManager?.play() - } - showPlayer?() - return - } - - guard let libraryItem = self.libraryService?.getSimpleItem(with: relativePath) else { return } - - var item: PlayableItem? - - do { - /// If the selected item is a bound book, check that the contents are loaded - if syncService?.isActive == true, - libraryItem.type == .bound, - let contents = libraryService?.getMaxItemsCount(at: relativePath), - contents == 0 - { - _ = try await syncService?.syncListContents(at: relativePath) - } - - item = try self.playbackService?.getPlayableItem(from: libraryItem) - } catch { - alertPresenter.showAlert( - "error_title".localized, - message: error.localizedDescription, - completion: nil - ) - return - } - - guard let item = item else { return } - - playerManager?.load(item, autoplay: autoplay) + let playerLoaderService = PlayerLoaderService( + syncService: syncService, + libraryService: libraryService, + playbackService: playbackService, + playerManager: playerManager + ) + let coreServices = CoreServices( + dataManager: dataManager, + accountService: accountService, + syncService: syncService, + libraryService: libraryService, + playbackService: playbackService, + playerManager: playerManager, + playerLoaderService: playerLoaderService, + watchService: watchService + ) - if recordAsLastBook { - await MainActor.run { - libraryService?.setLibraryLastBook(with: item.relativePath) - } - } + self.coreServices = coreServices - showPlayer?() + return coreServices } } @@ -280,7 +167,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { override func accessibilityPerformMagicTap() -> Bool { guard - let playerManager = self.playerManager, + let playerManager = self.coreServices?.playerManager, playerManager.currentItem != nil else { UIAccessibility.post( @@ -306,7 +193,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { // Play / Pause center.togglePlayPauseCommand.isEnabled = true center.togglePlayPauseCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in - guard let playerManager = self?.playerManager else { + guard let playerManager = self?.coreServices?.playerManager else { return .commandFailed } @@ -323,7 +210,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { center.playCommand.isEnabled = true center.playCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in - guard let playerManager = self?.playerManager else { + guard let playerManager = self?.coreServices?.playerManager else { return .commandFailed } @@ -333,7 +220,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { center.pauseCommand.isEnabled = true center.pauseCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in - guard let playerManager = self?.playerManager else { + guard let playerManager = self?.coreServices?.playerManager else { return .commandFailed } @@ -349,7 +236,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { center.changePlaybackPositionCommand.isEnabled = true center.changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in guard - let playerManager = self?.playerManager, + let playerManager = self?.coreServices?.playerManager, let currentItem = playerManager.currentItem, let event = remoteEvent as? MPChangePlaybackPositionCommandEvent else { return .commandFailed } @@ -374,14 +261,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { // Forward center.skipForwardCommand.preferredIntervals = [NSNumber(value: PlayerManager.forwardInterval)] center.skipForwardCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in - guard let playerManager = self?.playerManager else { return .commandFailed } + guard let playerManager = self?.coreServices?.playerManager else { return .commandFailed } playerManager.forward() return .success } center.nextTrackCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in - guard let playerManager = self?.playerManager else { return .commandFailed } + guard let playerManager = self?.coreServices?.playerManager else { return .commandFailed } playerManager.forward() return .success @@ -394,7 +281,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { return .success } - guard let playerManager = self?.playerManager else { return .success } + guard let playerManager = self?.coreServices?.playerManager else { return .success } // End seeking playerManager.forward() @@ -404,14 +291,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { // Rewind center.skipBackwardCommand.preferredIntervals = [NSNumber(value: PlayerManager.rewindInterval)] center.skipBackwardCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in - guard let playerManager = self?.playerManager else { return .commandFailed } + guard let playerManager = self?.coreServices?.playerManager else { return .commandFailed } playerManager.rewind() return .success } center.previousTrackCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in - guard let playerManager = self?.playerManager else { return .commandFailed } + guard let playerManager = self?.coreServices?.playerManager else { return .commandFailed } playerManager.rewind() return .success @@ -425,7 +312,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { return .success } - guard let playerManager = self?.playerManager else { return .success } + guard let playerManager = self?.coreServices?.playerManager else { return .success } // End seeking playerManager.rewind() @@ -468,30 +355,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { } } - func setupSharedWidgetActionObserver() { - let sharedDefaults = UserDefaults.sharedDefaults - - if let actionURL = sharedDefaults.sharedWidgetActionURL { - ActionParserService.process(actionURL) - sharedDefaults.removeObject( - forKey: Constants.UserDefaults.sharedWidgetActionURL - ) - } - - sharedWidgetActionURLObserver = sharedDefaults.observe( - \.sharedWidgetActionURL - ) { defaults, _ in - DispatchQueue.main.async { - guard let actionURL = defaults.sharedWidgetActionURL else { return } - - ActionParserService.process(actionURL) - sharedDefaults.removeObject( - forKey: Constants.UserDefaults.sharedWidgetActionURL - ) + func setupCoreServices() { + setupCoreServicesTask = Task { + do { + let stack = try await databaseInitializer.loadCoreDataStack() + let coreServices = createCoreServicesIfNeeded(from: stack) + if #available(iOS 16.0, *) { + AppDependencyManager.shared.add(dependency: coreServices.playerLoaderService) + AppDependencyManager.shared.add(dependency: coreServices.libraryService) + } + } catch { + errorCoreServicesSetup = error } } } + func resetCoreServices() { + setupCoreServicesTask?.cancel() + setupCoreServicesTask = nil + errorCoreServicesSetup = nil + databaseInitializer.cleanupStoreFiles() + setupCoreServices() + } + /// Setup or stop Sentry based on flag /// - Parameter isDisabled: Determines user preference for crash reports private func handleSentryPreference(isDisabled: Bool) { @@ -546,7 +432,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { func playLastBook() { guard - let playerManager, + let playerManager = coreServices?.playerManager, playerManager.hasLoadedBook() else { UserDefaults.standard.set(true, forKey: Constants.UserActivityPlayback) @@ -558,7 +444,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger { func showPlayer() { guard - let playerManager, + let playerManager = coreServices?.playerManager, playerManager.hasLoadedBook() else { UserDefaults.standard.set(true, forKey: Constants.UserDefaults.showPlayer) @@ -583,7 +469,7 @@ extension AppDelegate { object: nil, queue: nil ) { _ in - if self.playerManager?.isPlaying != true { + if self.coreServices?.playerManager.isPlaying != true { self.scheduleAppRefresh() } } @@ -606,7 +492,7 @@ extension AppDelegate { } func handleAppRefresh(task: BGAppRefreshTask) { - guard let syncService else { return } + guard let syncService = coreServices?.syncService else { return } let refreshOperation = RefreshTaskOperation(syncService: syncService) diff --git a/BookPlayer/AppIntents/CustomRewindIntent.swift b/BookPlayer/AppIntents/CustomRewindIntent.swift index 50fdd936..8f2bd650 100644 --- a/BookPlayer/AppIntents/CustomRewindIntent.swift +++ b/BookPlayer/AppIntents/CustomRewindIntent.swift @@ -6,12 +6,12 @@ // Copyright © 2024 Tortuga Power. All rights reserved. // +import AppIntents import BookPlayerKit import Foundation -import AppIntents -@available(iOS 16.4, macOS 14.0, watchOS 10.0, tvOS 16.0, *) -struct CustomRewindIntent: AudioStartingIntent, ForegroundContinuableIntent { +@available(iOS 16.0, macOS 14.0, watchOS 10.0, tvOS 16.0, *) +struct CustomRewindIntent: AudioStartingIntent { static var title: LocalizedStringResource = "intent_custom_skiprewind_title" @Parameter( @@ -24,30 +24,22 @@ struct CustomRewindIntent: AudioStartingIntent, ForegroundContinuableIntent { Summary("Rewind \(\.$interval)") } - func perform() async throws -> some IntentResult { - let seconds = interval.converted(to: .seconds).value - let stack = try await DatabaseInitializer().loadCoreDataStack() - - let continuation: (@MainActor () async throws -> Void) = { - let actionString = CommandParser.createActionString( - from: .skipRewind, - parameters: [URLQueryItem(name: "interval", value: "\(seconds)")] - ) - let actionURL = URL(string: actionString)! - UIApplication.shared.open(actionURL) - } + @Dependency + var playerLoaderService: PlayerLoaderService - guard let appDelegate = await AppDelegate.shared else { - throw needsToContinueInForegroundError(continuation: continuation) - } + @Dependency + var libraryService: LibraryService - let coreServices = await appDelegate.createCoreServicesIfNeeded(from: stack) + func perform() async throws -> some IntentResult { + let seconds = interval.converted(to: .seconds).value - guard coreServices.playerManager.hasLoadedBook() else { - throw needsToContinueInForegroundError(continuation: continuation) + if !playerLoaderService.playerManager.hasLoadedBook(), + let book = libraryService.getLastPlayedItems(limit: 1)?.first + { + try await playerLoaderService.loadPlayer(book.relativePath, autoplay: false) } - coreServices.playerManager.skip(-seconds) + playerLoaderService.playerManager.skip(-seconds) return .result() } diff --git a/BookPlayer/AppIntents/CustomSkipForwardIntent.swift b/BookPlayer/AppIntents/CustomSkipForwardIntent.swift index f354c887..092d8d13 100644 --- a/BookPlayer/AppIntents/CustomSkipForwardIntent.swift +++ b/BookPlayer/AppIntents/CustomSkipForwardIntent.swift @@ -6,12 +6,12 @@ // Copyright © 2024 Tortuga Power. All rights reserved. // +import AppIntents import BookPlayerKit import Foundation -import AppIntents -@available(iOS 16.4, macOS 14.0, watchOS 10.0, tvOS 16.0, *) -struct CustomSkipForwardIntent: AudioStartingIntent, ForegroundContinuableIntent { +@available(iOS 16.0, macOS 14.0, watchOS 10.0, tvOS 16.0, *) +struct CustomSkipForwardIntent: AudioStartingIntent { static var title: LocalizedStringResource = "intent_custom_skipforward_title" @Parameter( @@ -24,30 +24,22 @@ struct CustomSkipForwardIntent: AudioStartingIntent, ForegroundContinuableIntent Summary("Skip forward \(\.$interval)") } - func perform() async throws -> some IntentResult { - let seconds = interval.converted(to: .seconds).value - let stack = try await DatabaseInitializer().loadCoreDataStack() - - let continuation: (@MainActor () async throws -> Void) = { - let actionString = CommandParser.createActionString( - from: .skipForward, - parameters: [URLQueryItem(name: "interval", value: "\(seconds)")] - ) - let actionURL = URL(string: actionString)! - UIApplication.shared.open(actionURL) - } + @Dependency + var playerLoaderService: PlayerLoaderService - guard let appDelegate = await AppDelegate.shared else { - throw needsToContinueInForegroundError(continuation: continuation) - } + @Dependency + var libraryService: LibraryService - let coreServices = await appDelegate.createCoreServicesIfNeeded(from: stack) + func perform() async throws -> some IntentResult { + let seconds = interval.converted(to: .seconds).value - guard coreServices.playerManager.hasLoadedBook() else { - throw needsToContinueInForegroundError(continuation: continuation) + if !playerLoaderService.playerManager.hasLoadedBook(), + let book = libraryService.getLastPlayedItems(limit: 1)?.first + { + try await playerLoaderService.loadPlayer(book.relativePath, autoplay: false) } - coreServices.playerManager.skip(seconds) + playerLoaderService.playerManager.skip(seconds) return .result() } diff --git a/BookPlayer/AppIntents/LastBookStartPlaybackIntent.swift b/BookPlayer/AppIntents/LastBookStartPlaybackIntent.swift index 5abdf911..7c3659ea 100644 --- a/BookPlayer/AppIntents/LastBookStartPlaybackIntent.swift +++ b/BookPlayer/AppIntents/LastBookStartPlaybackIntent.swift @@ -6,40 +6,26 @@ // Copyright © 2023 Tortuga Power. All rights reserved. // -import Foundation import AppIntents import BookPlayerKit +import Foundation -@available(iOS 16.4, macOS 14.0, watchOS 10.0, *) -struct LastBookStartPlaybackIntent: AudioStartingIntent, ForegroundContinuableIntent { +@available(iOS 16.0, macOS 14.0, watchOS 10.0, *) +struct LastBookStartPlaybackIntent: AudioStartingIntent { static var title: LocalizedStringResource = "intent_lastbook_play_title" - func perform() async throws -> some IntentResult { - let stack = try await DatabaseInitializer().loadCoreDataStack() - - guard let appDelegate = await AppDelegate.shared else { - throw needsToContinueInForegroundError { - let actionString = CommandParser.createActionString( - from: .play, - parameters: [URLQueryItem(name: "autoplay", value: "true")] - ) - let actionURL = URL(string: actionString)! - UIApplication.shared.open(actionURL) - } - } + @Dependency + var playerLoaderService: PlayerLoaderService - let coreServices = await appDelegate.createCoreServicesIfNeeded(from: stack) + @Dependency + var libraryService: LibraryService - guard let book = coreServices.libraryService.getLastPlayedItems(limit: 1)?.first else { + func perform() async throws -> some IntentResult { + guard let book = libraryService.getLastPlayedItems(limit: 1)?.first else { throw "intent_lastbook_empty_error".localized } - await appDelegate.loadPlayer( - book.relativePath, - autoplay: true, - showPlayer: nil, - alertPresenter: VoidAlertPresenter() - ) + try await playerLoaderService.loadPlayer(book.relativePath, autoplay: true) return .result() } diff --git a/BookPlayer/AppIntents/PausePlaybackIntent.swift b/BookPlayer/AppIntents/PausePlaybackIntent.swift index 210a78d9..f911ddbf 100644 --- a/BookPlayer/AppIntents/PausePlaybackIntent.swift +++ b/BookPlayer/AppIntents/PausePlaybackIntent.swift @@ -6,28 +6,19 @@ // Copyright © 2023 Tortuga Power. All rights reserved. // -import Foundation import AppIntents import BookPlayerKit +import Foundation -@available(iOS 16.4, macOS 14.0, watchOS 10.0, *) -struct PausePlaybackIntent: AudioStartingIntent, ForegroundContinuableIntent { +@available(iOS 16.0, macOS 14.0, watchOS 10.0, *) +struct PausePlaybackIntent: AudioStartingIntent { static var title: LocalizedStringResource = "intent_playback_pause_title" - func perform() async throws -> some IntentResult { - let stack = try await DatabaseInitializer().loadCoreDataStack() - - guard let appDelegate = await AppDelegate.shared else { - throw needsToContinueInForegroundError { - let actionString = CommandParser.createActionString(from: .pause, parameters: []) - let actionURL = URL(string: actionString)! - UIApplication.shared.open(actionURL) - } - } - - let coreServices = await appDelegate.createCoreServicesIfNeeded(from: stack) + @Dependency + var playerLoaderService: PlayerLoaderService - coreServices.playerManager.pause() + func perform() async throws -> some IntentResult { + playerLoaderService.playerManager.pause() return .result() } diff --git a/BookPlayer/Coordinators/DataInitializerCoordinator.swift b/BookPlayer/Coordinators/DataInitializerCoordinator.swift index 76d4e2a9..3ea4d86d 100644 --- a/BookPlayer/Coordinators/DataInitializerCoordinator.swift +++ b/BookPlayer/Coordinators/DataInitializerCoordinator.swift @@ -15,7 +15,7 @@ class DataInitializerCoordinator: BPLogger { let databaseInitializer: DatabaseInitializer = DatabaseInitializer() let alertPresenter: AlertPresenter - var onFinish: ((CoreDataStack) -> Void)? + var onFinish: (() -> Void)? init(alertPresenter: AlertPresenter) { self.alertPresenter = alertPresenter @@ -28,13 +28,22 @@ class DataInitializerCoordinator: BPLogger { } func initializeLibrary(isRecoveryAttempt: Bool) async { - do { - let stack = try await databaseInitializer.loadCoreDataStack() - finishLibrarySetup(stack, fromRecovery: isRecoveryAttempt) - } catch let error as NSError where error.domain == NSPOSIXErrorDomain && ( - error.code == ENOSPC - || error.code == NSFileWriteOutOfSpaceError - ) { + let appDelegate = await AppDelegate.shared! + _ = await appDelegate.setupCoreServicesTask?.result + + if let errorCoreServicesSetup = await appDelegate.errorCoreServicesSetup { + await handleError(errorCoreServicesSetup as NSError) + return + } + + await finishLibrarySetup(fromRecovery: isRecoveryAttempt) + } + + func handleError(_ error: NSError) async { + if error.domain == NSPOSIXErrorDomain + && (error.code == ENOSPC + || error.code == NSFileWriteOutOfSpaceError) + { // CoreData may fail if device doesn't have space await MainActor.run { alertPresenter.showAlert( @@ -43,19 +52,12 @@ class DataInitializerCoordinator: BPLogger { completion: nil ) } - } catch let error as NSError where ( - error.code == NSMigrationError || - error.code == NSMigrationConstraintViolationError || - error.code == NSMigrationCancelledError || - error.code == NSMigrationMissingSourceModelError || - error.code == NSMigrationMissingMappingModelError || - error.code == NSMigrationManagerSourceStoreError || - error.code == NSMigrationManagerDestinationStoreError || - error.code == NSEntityMigrationPolicyError || - error.code == NSValidationMultipleErrorsError || - error.code == NSValidationMissingMandatoryPropertyError - ) { - // TODO: We can handle `isRecoveryAttempt` to show a different error message + } else if error.code == NSMigrationError || error.code == NSMigrationConstraintViolationError + || error.code == NSMigrationCancelledError || error.code == NSMigrationMissingSourceModelError + || error.code == NSMigrationMissingMappingModelError || error.code == NSMigrationManagerSourceStoreError + || error.code == NSMigrationManagerDestinationStoreError || error.code == NSEntityMigrationPolicyError + || error.code == NSValidationMultipleErrorsError || error.code == NSValidationMissingMandatoryPropertyError + { Self.logger.warning("Failed to perform migration, attempting recovery with the loading library sequence") await MainActor.run { alertPresenter.showAlert( @@ -65,35 +67,33 @@ class DataInitializerCoordinator: BPLogger { recoverLibraryFromFailedMigration() } } - } catch { - let error = error as NSError + } else { fatalError("Unresolved error \(error), \(error.userInfo)") } } func recoverLibraryFromFailedMigration() { Task { - databaseInitializer.cleanupStoreFiles() + await AppDelegate.shared?.resetCoreServices() await initializeLibrary(isRecoveryAttempt: true) } } - func finishLibrarySetup(_ stack: CoreDataStack, fromRecovery: Bool) { - let dataManager = DataManager(coreDataStack: stack) - let libraryService = LibraryService(dataManager: dataManager) + func finishLibrarySetup(fromRecovery: Bool) async { + let coreServices = await AppDelegate.shared!.coreServices! setupDefaultState( - libraryService: libraryService, - dataManager: dataManager + libraryService: coreServices.libraryService, + dataManager: coreServices.dataManager ) if fromRecovery { let files = getLibraryFiles() - libraryService.insertItems(from: files) + coreServices.libraryService.insertItems(from: files) } - DispatchQueue.main.async { - self.onFinish?(stack) + await MainActor.run { + self.onFinish?() } } @@ -101,10 +101,12 @@ class DataInitializerCoordinator: BPLogger { let enumerator = FileManager.default.enumerator( at: DataManager.getProcessedFolderURL(), includingPropertiesForKeys: [.isDirectoryKey], - options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: { (url, error) -> Bool in + options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], + errorHandler: { (url, error) -> Bool in print("directoryEnumerator error at \(url): ", error) return true - })! + } + )! var files = [URL]() for case let fileURL as URL in enumerator { files.append(fileURL) @@ -124,8 +126,9 @@ class DataInitializerCoordinator: BPLogger { let storedIconId = UserDefaults.standard.string(forKey: Constants.UserDefaults.appIcon) sharedDefaults.set(storedIconId, forKey: Constants.UserDefaults.appIcon) } else if let sharedAppIcon = sharedDefaults.string(forKey: Constants.UserDefaults.appIcon), - let localAppIcon = UserDefaults.standard.string(forKey: Constants.UserDefaults.appIcon), - sharedAppIcon != localAppIcon { + let localAppIcon = UserDefaults.standard.string(forKey: Constants.UserDefaults.appIcon), + sharedAppIcon != localAppIcon + { sharedDefaults.set(localAppIcon, forKey: Constants.UserDefaults.appIcon) UserDefaults.standard.removeObject(forKey: Constants.UserDefaults.appIcon) } diff --git a/BookPlayer/Coordinators/ItemListCoordinator.swift b/BookPlayer/Coordinators/ItemListCoordinator.swift index 13e2e90e..3bd00d6c 100644 --- a/BookPlayer/Coordinators/ItemListCoordinator.swift +++ b/BookPlayer/Coordinators/ItemListCoordinator.swift @@ -70,6 +70,7 @@ class ItemListCoordinator: NSObject, Coordinator, AlertPresenter, BPLogger { flow.navigationController.present(nav, animated: true) } + @MainActor func showPlayer() { let playerCoordinator = PlayerCoordinator( flow: .modalOnlyFlow(presentingController: flow.navigationController, modalPresentationStyle: .overFullScreen), @@ -103,14 +104,29 @@ class ItemListCoordinator: NSObject, Coordinator, AlertPresenter, BPLogger { } func loadPlayer(_ relativePath: String) { - AppDelegate.shared?.loadPlayer( - relativePath, - autoplay: true, - showPlayer: { [weak self] in - self?.showPlayer() - }, - alertPresenter: self - ) + Task { + let alertPresenter: AlertPresenter = self + do { + try await AppDelegate.shared?.coreServices?.playerLoaderService.loadPlayer( + relativePath, + autoplay: true + ) + await self.showPlayer() + } catch BPPlayerError.fileMissing { + alertPresenter.showAlert( + "file_missing_title".localized, + message: + "\("file_missing_description".localized)\n\(relativePath)", + completion: nil + ) + } catch { + alertPresenter.showAlert( + "error_title".localized, + message: error.localizedDescription, + completion: nil + ) + } + } } func showMiniPlayer(flag: Bool) { @@ -131,7 +147,7 @@ extension ItemListCoordinator { UTType.audio, UTType.movie, UTType.zip, - UTType.folder + UTType.folder, ], asCopy: true ) @@ -151,7 +167,8 @@ extension ItemListCoordinator { shareController.excludedActivityTypes = [.copyToPasteboard] if let popoverPresentationController = shareController.popoverPresentationController, - let view = flow.navigationController.topViewController?.view { + let view = flow.navigationController.topViewController?.view + { popoverPresentationController.permittedArrowDirections = [] popoverPresentationController.sourceView = view popoverPresentationController.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0) diff --git a/BookPlayer/Coordinators/LibraryListCoordinator.swift b/BookPlayer/Coordinators/LibraryListCoordinator.swift index 8f22401a..7ad00f90 100644 --- a/BookPlayer/Coordinators/LibraryListCoordinator.swift +++ b/BookPlayer/Coordinators/LibraryListCoordinator.swift @@ -57,7 +57,7 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat networkClient: NetworkClient(), libraryService: self.libraryService, playbackService: self.playbackService, - syncService: self.syncService, + syncService: self.syncService, importManager: importManager, listRefreshService: listRefreshService, themeAccent: ThemeManager.shared.currentTheme.linkColor @@ -104,7 +104,7 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat self.documentPickerDelegate = vc - AppDelegate.shared?.watchConnectivityService?.startSession() + AppDelegate.shared?.coreServices?.watchService.startSession() } func handleLibraryLoaded() { @@ -145,15 +145,16 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat fileSubscription = importManager.observeFiles() .receive(on: DispatchQueue.main) .sink { [weak self] files in - guard let self = self, - !files.isEmpty, - self.shouldShowImportScreen() else { return } + guard let self = self, + !files.isEmpty, + self.shouldShowImportScreen() + else { return } - self.showImport() - } + self.showImport() + } importOperationSubscription = importManager.operationPublisher.sink(receiveValue: { [weak self] operation in - guard + guard let self, let lastItemListViewController = self.flow.navigationController.viewControllers.last as? ItemListViewController else { @@ -161,7 +162,10 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat } lastItemListViewController.setEditing(false, animated: false) - let loadingTitle = String.localizedStringWithFormat("import_processing_description".localized, operation.files.count) + let loadingTitle = String.localizedStringWithFormat( + "import_processing_description".localized, + operation.files.count + ) lastItemListViewController.showLoadView(true, title: loadingTitle) operation.completionBlock = { @@ -189,29 +193,32 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat @MainActor func notifyPendingFiles() { // Get reference of all the files located inside the Documents, Shared and Inbox folders - let documentsURLs = ((try? FileManager.default.contentsOfDirectory( - at: DataManager.getDocumentsFolderURL(), - includingPropertiesForKeys: nil, - options: .skipsSubdirectoryDescendants - )) ?? []) + let documentsURLs = + ((try? FileManager.default.contentsOfDirectory( + at: DataManager.getDocumentsFolderURL(), + includingPropertiesForKeys: nil, + options: .skipsSubdirectoryDescendants + )) ?? []) .filter { $0.lastPathComponent != DataManager.processedFolderName - && $0.lastPathComponent != DataManager.inboxFolderName - && $0.lastPathComponent != DataManager.backupFolderName - && $0.lastPathComponent != DataManager.trashFolderName + && $0.lastPathComponent != DataManager.inboxFolderName + && $0.lastPathComponent != DataManager.backupFolderName + && $0.lastPathComponent != DataManager.trashFolderName } - let sharedURLs = (try? FileManager.default.contentsOfDirectory( - at: DataManager.getSharedFilesFolderURL(), - includingPropertiesForKeys: nil, - options: .skipsSubdirectoryDescendants - )) ?? [] + let sharedURLs = + (try? FileManager.default.contentsOfDirectory( + at: DataManager.getSharedFilesFolderURL(), + includingPropertiesForKeys: nil, + options: .skipsSubdirectoryDescendants + )) ?? [] - let inboxURLs = (try? FileManager.default.contentsOfDirectory( - at: DataManager.getInboxFolderURL(), - includingPropertiesForKeys: nil, - options: .skipsSubdirectoryDescendants - )) ?? [] + let inboxURLs = + (try? FileManager.default.contentsOfDirectory( + at: DataManager.getInboxFolderURL(), + includingPropertiesForKeys: nil, + options: .skipsSubdirectoryDescendants + )) ?? [] let urls = documentsURLs + sharedURLs + inboxURLs @@ -225,23 +232,38 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat let libraryItem = libraryService.getLibraryLastItem() else { return } - AppDelegate.shared?.loadPlayer( - libraryItem.relativePath, - autoplay: false, - showPlayer: { [weak self] in + Task { + let alertPresenter: AlertPresenter = self + do { + try await AppDelegate.shared?.coreServices?.playerLoaderService.loadPlayer( + libraryItem.relativePath, + autoplay: false, + recordAsLastBook: false + ) if UserDefaults.standard.bool(forKey: Constants.UserActivityPlayback) { UserDefaults.standard.removeObject(forKey: Constants.UserActivityPlayback) - self?.playerManager.play() + self.playerManager.play() } if UserDefaults.standard.bool(forKey: Constants.UserDefaults.showPlayer) { UserDefaults.standard.removeObject(forKey: Constants.UserDefaults.showPlayer) - self?.showPlayer() + self.showPlayer() } - }, - alertPresenter: self, - recordAsLastBook: false - ) + } catch BPPlayerError.fileMissing { + alertPresenter.showAlert( + "file_missing_title".localized, + message: + "\("file_missing_description".localized)\n\(libraryItem.relativePath)", + completion: nil + ) + } catch { + alertPresenter.showAlert( + "error_title".localized, + message: error.localizedDescription, + completion: nil + ) + } + } } func processFiles(urls: [URL]) { @@ -263,7 +285,7 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat } func showImport() { - guard + guard let topVC = AppDelegate.shared?.activeSceneDelegate?.startingNavigationController.getTopVisibleViewController() else { return } diff --git a/BookPlayer/Coordinators/LoadingCoordinator.swift b/BookPlayer/Coordinators/LoadingCoordinator.swift index 8a21ec0a..31841cbb 100644 --- a/BookPlayer/Coordinators/LoadingCoordinator.swift +++ b/BookPlayer/Coordinators/LoadingCoordinator.swift @@ -25,8 +25,8 @@ class LoadingCoordinator: Coordinator, AlertPresenter { flow.startPresentation(vc, animated: false) } - func didFinishLoadingSequence(coreDataStack: CoreDataStack) { - let coreServices = AppDelegate.shared!.createCoreServicesIfNeeded(from: coreDataStack) + func didFinishLoadingSequence() { + let coreServices = AppDelegate.shared!.coreServices! let coordinator = MainCoordinator( navigationController: flow.navigationController, diff --git a/BookPlayer/Coordinators/MainCoordinator.swift b/BookPlayer/Coordinators/MainCoordinator.swift index dc16b861..204e2c24 100644 --- a/BookPlayer/Coordinators/MainCoordinator.swift +++ b/BookPlayer/Coordinators/MainCoordinator.swift @@ -91,7 +91,7 @@ class MainCoordinator: NSObject { listRefreshService: ListSyncRefreshService( playerManager: playerManager, syncService: syncService - ), + ), accountService: self.accountService ) playerManager.syncProgressDelegate = libraryCoordinator @@ -115,7 +115,7 @@ class MainCoordinator: NSObject { func startSettingsCoordinator(with tabBarController: UITabBarController) { let settingsCoordinator = SettingsCoordinator( flow: .pushFlow(navigationController: AppNavigationController.instantiate(from: .Settings)), - libraryService: self.libraryService, + libraryService: self.libraryService, syncService: self.syncService, accountService: self.accountService ) @@ -148,16 +148,31 @@ class MainCoordinator: NSObject { } func loadPlayer(_ relativePath: String, autoplay: Bool, showPlayer: Bool) { - AppDelegate.shared?.loadPlayer( - relativePath, - autoplay: autoplay, - showPlayer: { [weak self] in + Task { + let alertPresenter: AlertPresenter = getLibraryCoordinator() ?? self + do { + try await AppDelegate.shared?.coreServices?.playerLoaderService.loadPlayer( + relativePath, + autoplay: autoplay + ) if showPlayer { - self?.showPlayer() + self.showPlayer() } - }, - alertPresenter: (getLibraryCoordinator() ?? self) - ) + } catch BPPlayerError.fileMissing { + alertPresenter.showAlert( + "file_missing_title".localized, + message: + "\("file_missing_description".localized)\n\(relativePath)", + completion: nil + ) + } catch { + alertPresenter.showAlert( + "error_title".localized, + message: error.localizedDescription, + completion: nil + ) + } + } } func showPlayer() { @@ -211,9 +226,10 @@ extension MainCoordinator: Themeable { return } // This fixes native components like alerts having the proper color theme - AppDelegate.shared?.activeSceneDelegate?.window?.overrideUserInterfaceStyle = theme.useDarkVariant - ? .dark - : .light + AppDelegate.shared?.activeSceneDelegate?.window?.overrideUserInterfaceStyle = + theme.useDarkVariant + ? .dark + : .light } } diff --git a/BookPlayer/Loading/LoadingViewModel.swift b/BookPlayer/Loading/LoadingViewModel.swift index d28f5855..be228c8f 100644 --- a/BookPlayer/Loading/LoadingViewModel.swift +++ b/BookPlayer/Loading/LoadingViewModel.swift @@ -16,8 +16,8 @@ class LoadingViewModel: ViewModelProtocol { func initializeDataIfNeeded() { let dataInitializerCoordinator = DataInitializerCoordinator(alertPresenter: self.coordinator) - dataInitializerCoordinator.onFinish = { stack in - self.coordinator.didFinishLoadingSequence(coreDataStack: stack) + dataInitializerCoordinator.onFinish = { + self.coordinator.didFinishLoadingSequence() } dataInitializerCoordinator.start() diff --git a/BookPlayer/Player/BPPlayerError.swift b/BookPlayer/Player/BPPlayerError.swift new file mode 100644 index 00000000..56361f88 --- /dev/null +++ b/BookPlayer/Player/BPPlayerError.swift @@ -0,0 +1,13 @@ +// +// BPPlayerError.swift +// BookPlayer +// +// Created by Gianni Carlo on 3/10/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import Foundation + +enum BPPlayerError: Error { + case fileMissing +} diff --git a/BookPlayer/Player/PlayerLoaderService.swift b/BookPlayer/Player/PlayerLoaderService.swift new file mode 100644 index 00000000..31ba3a0c --- /dev/null +++ b/BookPlayer/Player/PlayerLoaderService.swift @@ -0,0 +1,76 @@ +// +// PlayerLoaderService.swift +// BookPlayer +// +// Created by Gianni Carlo on 3/10/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerKit +import Foundation + +final class PlayerLoaderService: @unchecked Sendable { + var syncService: SyncServiceProtocol + var libraryService: LibraryServiceProtocol + var playbackService: PlaybackServiceProtocol + var playerManager: PlayerManagerProtocol + + init( + syncService: SyncServiceProtocol, + libraryService: LibraryServiceProtocol, + playbackService: PlaybackServiceProtocol, + playerManager: PlayerManagerProtocol + ) { + self.syncService = syncService + self.libraryService = libraryService + self.playbackService = playbackService + self.playerManager = playerManager + } + + @MainActor + func loadPlayer( + _ relativePath: String, + autoplay: Bool, + recordAsLastBook: Bool = true + ) async throws { + let fileURL = DataManager.getProcessedFolderURL().appendingPathComponent(relativePath) + + if syncService.isActive == false, + !FileManager.default.fileExists(atPath: fileURL.path) + { + throw BPPlayerError.fileMissing + } + + // Only load if loaded book is a different one + if playerManager.hasLoadedBook() == true, + relativePath == playerManager.currentItem?.relativePath + { + if autoplay { + playerManager.play() + } + return + } + + guard + let libraryItem = self.libraryService.getSimpleItem(with: relativePath) + else { return } + + /// If the selected item is a bound book, check that the contents are loaded + if syncService.isActive == true, + libraryItem.type == .bound, + libraryService.getMaxItemsCount(at: relativePath) == 0 + { + _ = try await syncService.syncListContents(at: relativePath) + } + + let item = try self.playbackService.getPlayableItem(from: libraryItem) + + playerManager.load(item, autoplay: autoplay) + + if recordAsLastBook { + await MainActor.run { + libraryService.setLibraryLastBook(with: item.relativePath) + } + } + } +} diff --git a/BookPlayer/Player/PlayerManager.swift b/BookPlayer/Player/PlayerManager.swift index 24025033..7d70f24a 100755 --- a/BookPlayer/Player/PlayerManager.swift +++ b/BookPlayer/Player/PlayerManager.swift @@ -13,40 +13,6 @@ import Foundation import MediaPlayer // swiftlint:disable:next file_length -/// sourcery: AutoMockable -public protocol PlayerManagerProtocol: AnyObject { - var currentItem: PlayableItem? { get set } - var currentSpeed: Float { get set } - var isPlaying: Bool { get } - var syncProgressDelegate: PlaybackSyncProgressDelegate? { get set } - - func load(_ item: PlayableItem, autoplay: Bool) - func hasLoadedBook() -> Bool - - func playPreviousItem() - func playNextItem(autoPlayed: Bool, shouldAutoplay: Bool) - func play() - func playPause() - func pause() - func stop() - func rewind() - func forward() - func skip(_ interval: TimeInterval) - func jumpTo(_ time: Double, recordBookmark: Bool) - func jumpToChapter(_ chapter: PlayableChapter) - func markAsCompleted(_ flag: Bool) - func setSpeed(_ newValue: Float) - func setBoostVolume(_ newValue: Bool) - - func currentSpeedPublisher() -> AnyPublisher - func isPlayingPublisher() -> AnyPublisher - func currentItemPublisher() -> AnyPublisher -} - -/// Delegate that hooks into the playback sequence -public protocol PlaybackSyncProgressDelegate: AnyObject { - func waitForSyncInProgress() async -} final class PlayerManager: NSObject, PlayerManagerProtocol { private let libraryService: LibraryServiceProtocol diff --git a/BookPlayer/Player/PlayerManagerProtocol.swift b/BookPlayer/Player/PlayerManagerProtocol.swift new file mode 100644 index 00000000..f9e1c84d --- /dev/null +++ b/BookPlayer/Player/PlayerManagerProtocol.swift @@ -0,0 +1,46 @@ +// +// PlayerManagerProtocol.swift +// BookPlayer +// +// Created by Gianni Carlo on 3/10/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerKit +import Combine +import Foundation + +/// sourcery: AutoMockable +public protocol PlayerManagerProtocol: AnyObject { + var currentItem: PlayableItem? { get set } + var currentSpeed: Float { get set } + var isPlaying: Bool { get } + var syncProgressDelegate: PlaybackSyncProgressDelegate? { get set } + + func load(_ item: PlayableItem, autoplay: Bool) + func hasLoadedBook() -> Bool + + func playPreviousItem() + func playNextItem(autoPlayed: Bool, shouldAutoplay: Bool) + func play() + func playPause() + func pause() + func stop() + func rewind() + func forward() + func skip(_ interval: TimeInterval) + func jumpTo(_ time: Double, recordBookmark: Bool) + func jumpToChapter(_ chapter: PlayableChapter) + func markAsCompleted(_ flag: Bool) + func setSpeed(_ newValue: Float) + func setBoostVolume(_ newValue: Bool) + + func currentSpeedPublisher() -> AnyPublisher + func isPlayingPublisher() -> AnyPublisher + func currentItemPublisher() -> AnyPublisher +} + +/// Delegate that hooks into the playback sequence +public protocol PlaybackSyncProgressDelegate: AnyObject { + func waitForSyncInProgress() async +} diff --git a/BookPlayer/Services/ActionParserService.swift b/BookPlayer/Services/ActionParserService.swift index 8acb4574..46d5d4c5 100644 --- a/BookPlayer/Services/ActionParserService.swift +++ b/BookPlayer/Services/ActionParserService.swift @@ -41,7 +41,7 @@ class ActionParserService { appDelegate.pendingURLActions.append(action) guard - let watchConnectivityService = appDelegate.watchConnectivityService + let watchConnectivityService = appDelegate.coreServices?.watchService else { return } switch action.command { @@ -77,7 +77,7 @@ class ActionParserService { private class func handleRewindAction(_ action: Action) { guard - let playerManager = AppDelegate.shared?.playerManager + let playerManager = AppDelegate.shared?.coreServices?.playerManager else { return } @@ -93,7 +93,7 @@ class ActionParserService { private class func handleForwardAction(_ action: Action) { guard - let playerManager = AppDelegate.shared?.playerManager + let playerManager = AppDelegate.shared?.coreServices?.playerManager else { return } @@ -111,7 +111,7 @@ class ActionParserService { guard let valueString = action.getQueryValue(for: "start"), let chapterStart = Double(valueString), - let playerManager = AppDelegate.shared?.playerManager + let playerManager = AppDelegate.shared?.coreServices?.playerManager else { return } @@ -130,7 +130,7 @@ class ActionParserService { let roundedValue = round(speedRate * 100) / 100.0 guard - let playerManager = AppDelegate.shared?.playerManager + let playerManager = AppDelegate.shared?.coreServices?.playerManager else { return } @@ -144,7 +144,7 @@ class ActionParserService { let isOn = valueString == "true" guard - let playerManager = AppDelegate.shared?.playerManager + let playerManager = AppDelegate.shared?.coreServices?.playerManager else { return } @@ -188,7 +188,7 @@ class ActionParserService { private class func handlePlaybackToggleAction(_ action: Action) { guard - let playerManager = AppDelegate.shared?.playerManager + let playerManager = AppDelegate.shared?.coreServices?.playerManager else { return } @@ -208,7 +208,7 @@ class ActionParserService { private class func handlePauseAction(_ action: Action) { guard - let playerManager = AppDelegate.shared?.playerManager + let playerManager = AppDelegate.shared?.coreServices?.playerManager else { return } @@ -219,7 +219,7 @@ class ActionParserService { private class func handlePlayAction(_ action: Action) { guard - let playerManager = AppDelegate.shared?.playerManager + let playerManager = AppDelegate.shared?.coreServices?.playerManager else { return } diff --git a/BookPlayer/Services/CarPlayManager.swift b/BookPlayer/Services/CarPlayManager.swift index 3c21598e..746292c4 100644 --- a/BookPlayer/Services/CarPlayManager.swift +++ b/BookPlayer/Services/CarPlayManager.swift @@ -7,8 +7,8 @@ // import BookPlayerKit -import Combine import CarPlay +import Combine class CarPlayManager: NSObject { var interfaceController: CPInterfaceController? @@ -27,7 +27,7 @@ class CarPlayManager: NSObject { // MARK: - Lifecycle - @MainActor + @MainActor func connect(_ interfaceController: CPInterfaceController) { self.interfaceController = interfaceController self.interfaceController?.delegate = self @@ -45,18 +45,14 @@ class CarPlayManager: NSObject { @MainActor func initializeDataIfNeeded() { guard - AppDelegate.shared?.dataManager == nil, AppDelegate.shared?.activeSceneDelegate == nil else { return } let dataInitializerCoordinator = DataInitializerCoordinator(alertPresenter: self) - dataInitializerCoordinator.onFinish = { [weak self] stack in - let services = AppDelegate.shared?.createCoreServicesIfNeeded(from: stack) - + dataInitializerCoordinator.onFinish = { [weak self] in self?.setRootTemplate() - - services?.watchService.startSession() + AppDelegate.shared?.coreServices?.watchService.startSession() } dataInitializerCoordinator.start() @@ -94,13 +90,14 @@ class CarPlayManager: NSObject { object: self, userInfo: [ "command": Command.boostVolume.rawValue, - "isOn": "\(!flag)" + "isOn": "\(!flag)", ] ) - let boostTitle = !flag - ? "\("settings_boostvolume_title".localized): \("active_title".localized)" - : "\("settings_boostvolume_title".localized): \("sleep_off_title".localized)" + let boostTitle = + !flag + ? "\("settings_boostvolume_title".localized): \("active_title".localized)" + : "\("settings_boostvolume_title".localized): \("sleep_off_title".localized)" self?.boostVolumeItem.setText(boostTitle) completion() @@ -109,7 +106,7 @@ class CarPlayManager: NSObject { func loadLibraryItems(at relativePath: String?) -> [SimpleLibraryItem] { guard - let libraryService = AppDelegate.shared?.libraryService + let libraryService = AppDelegate.shared?.coreServices?.libraryService else { return [] } return libraryService.fetchContents(at: relativePath, limit: nil, offset: nil) ?? [] @@ -118,10 +115,12 @@ class CarPlayManager: NSObject { // swiftlint:disable:next function_body_length func setupNowPlayingTemplate() { guard - let libraryService = AppDelegate.shared?.libraryService, - let playerManager = AppDelegate.shared?.playerManager + let coreServices = AppDelegate.shared?.coreServices else { return } + let libraryService = coreServices.libraryService + let playerManager = coreServices.playerManager + let prevButton = self.getPreviousChapterButton() let nextButton = self.getNextChapterButton() @@ -158,7 +157,7 @@ class CarPlayManager: NSObject { relativePath: currentItem.relativePath, type: .user ) { - AppDelegate.shared?.syncService?.scheduleSetBookmark( + coreServices.syncService.scheduleSetBookmark( relativePath: currentItem.relativePath, time: currentTime, note: nil @@ -177,7 +176,9 @@ class CarPlayManager: NSObject { self.interfaceController?.presentTemplate(alertTemplate, animated: true, completion: nil) } - CPNowPlayingTemplate.shared.updateNowPlayingButtons([prevButton, controlsButton, bookmarksButton, listButton, nextButton]) + CPNowPlayingTemplate.shared.updateNowPlayingButtons([ + prevButton, controlsButton, bookmarksButton, listButton, nextButton, + ]) } /// Setup root Tab bar template with the Recent and Library tabs @@ -213,7 +214,7 @@ class CarPlayManager: NSObject { /// Returns the library contents at a specified level func getLibraryContents(at relativePath: String? = nil) -> [CPListItem] { guard - let libraryService = AppDelegate.shared?.libraryService + let libraryService = AppDelegate.shared?.coreServices?.libraryService else { return [] } let items = libraryService.fetchContents(at: relativePath, limit: nil, offset: nil) ?? [] @@ -247,7 +248,7 @@ class CarPlayManager: NSObject { /// Reloads the recent items tab func reloadRecentItems() { guard - let libraryService = AppDelegate.shared?.libraryService + let libraryService = AppDelegate.shared?.coreServices?.libraryService else { return } let items = libraryService.getLastPlayedItems(limit: 20) ?? [] @@ -262,17 +263,32 @@ class CarPlayManager: NSObject { /// Handle playing the selected item func playItem(with relativePath: String) { - AppDelegate.shared?.loadPlayer( - relativePath, - autoplay: true, - showPlayer: { [weak self] in + Task { + let alertPresenter: AlertPresenter = self + do { + try await AppDelegate.shared?.coreServices?.playerLoaderService.loadPlayer( + relativePath, + autoplay: true + ) /// Avoid trying to show the now playing screen if it's already shown - if self?.interfaceController?.topTemplate != CPNowPlayingTemplate.shared { - self?.interfaceController?.pushTemplate(CPNowPlayingTemplate.shared, animated: true, completion: nil) + if self.interfaceController?.topTemplate != CPNowPlayingTemplate.shared { + self.interfaceController?.pushTemplate(CPNowPlayingTemplate.shared, animated: true, completion: nil) } - }, - alertPresenter: self - ) + } catch BPPlayerError.fileMissing { + alertPresenter.showAlert( + "file_missing_title".localized, + message: + "\("file_missing_description".localized)\n\(relativePath)", + completion: nil + ) + } catch { + alertPresenter.showAlert( + "error_title".localized, + message: error.localizedDescription, + completion: nil + ) + } + } } func formatSpeed(_ speed: Float) -> String { @@ -285,7 +301,7 @@ class CarPlayManager: NSObject { extension CarPlayManager { func hasChapter(before chapter: PlayableChapter?) -> Bool { guard - let playerManager = AppDelegate.shared?.playerManager, + let playerManager = AppDelegate.shared?.coreServices?.playerManager, let chapter = chapter else { return false } @@ -294,7 +310,7 @@ extension CarPlayManager { func hasChapter(after chapter: PlayableChapter?) -> Bool { guard - let playerManager = AppDelegate.shared?.playerManager, + let playerManager = AppDelegate.shared?.coreServices?.playerManager, let chapter = chapter else { return false } @@ -302,17 +318,19 @@ extension CarPlayManager { } func getPreviousChapterButton() -> CPNowPlayingImageButton { - let prevChapterImageName = self.hasChapter(before: AppDelegate.shared?.playerManager?.currentItem?.currentChapter) - ? "carplay.chevron.left" - : "carplay.chevron.left.2" + let prevChapterImageName = + self.hasChapter(before: AppDelegate.shared?.coreServices?.playerManager.currentItem?.currentChapter) + ? "carplay.chevron.left" + : "carplay.chevron.left.2" return CPNowPlayingImageButton( image: UIImage(named: prevChapterImageName)! ) { _ in - guard let playerManager = AppDelegate.shared?.playerManager else { return } + guard let playerManager = AppDelegate.shared?.coreServices?.playerManager else { return } if let currentChapter = playerManager.currentItem?.currentChapter, - let previousChapter = playerManager.currentItem?.previousChapter(before: currentChapter) { + let previousChapter = playerManager.currentItem?.previousChapter(before: currentChapter) + { playerManager.jumpToChapter(previousChapter) } else { playerManager.playPreviousItem() @@ -321,17 +339,19 @@ extension CarPlayManager { } func getNextChapterButton() -> CPNowPlayingImageButton { - let nextChapterImageName = self.hasChapter(after: AppDelegate.shared?.playerManager?.currentItem?.currentChapter) - ? "carplay.chevron.right" - : "carplay.chevron.right.2" + let nextChapterImageName = + self.hasChapter(after: AppDelegate.shared?.coreServices?.playerManager.currentItem?.currentChapter) + ? "carplay.chevron.right" + : "carplay.chevron.right.2" return CPNowPlayingImageButton( image: UIImage(named: nextChapterImageName)! ) { _ in - guard let playerManager = AppDelegate.shared?.playerManager else { return } + guard let playerManager = AppDelegate.shared?.coreServices?.playerManager else { return } if let currentChapter = playerManager.currentItem?.currentChapter, - let nextChapter = playerManager.currentItem?.nextChapter(after: currentChapter) { + let nextChapter = playerManager.currentItem?.nextChapter(after: currentChapter) + { playerManager.jumpToChapter(nextChapter) } else { playerManager.playNextItem(autoPlayed: false, shouldAutoplay: true) @@ -345,21 +365,27 @@ extension CarPlayManager { extension CarPlayManager { func showChapterListTemplate() { guard - let playerManager = AppDelegate.shared?.playerManager, + let playerManager = AppDelegate.shared?.coreServices?.playerManager, let chapters = playerManager.currentItem?.chapters else { return } let chapterItems = chapters.enumerated().map({ [weak self, playerManager] (index, chapter) -> CPListItem in - let chapterTitle = chapter.title == "" - ? String.localizedStringWithFormat("chapter_number_title".localized, index + 1) - : chapter.title - - let chapterDetail = String.localizedStringWithFormat("chapters_item_description".localized, TimeParser.formatTime(chapter.start), TimeParser.formatTime(chapter.duration)) + let chapterTitle = + chapter.title == "" + ? String.localizedStringWithFormat("chapter_number_title".localized, index + 1) + : chapter.title + + let chapterDetail = String.localizedStringWithFormat( + "chapters_item_description".localized, + TimeParser.formatTime(chapter.start), + TimeParser.formatTime(chapter.duration) + ) let item = CPListItem(text: chapterTitle, detailText: chapterDetail) if let currentChapter = playerManager.currentItem?.currentChapter, - currentChapter.index == chapter.index { + currentChapter.index == chapter.index + { item.isPlaying = true } @@ -369,7 +395,7 @@ extension CarPlayManager { object: self, userInfo: [ "command": Command.chapter.rawValue, - "start": "\(chapter.start)" + "start": "\(chapter.start)", ] ) completion() @@ -405,7 +431,7 @@ extension CarPlayManager { object: self, userInfo: [ "command": Command.chapter.rawValue, - "start": "\(bookmark.time)" + "start": "\(bookmark.time)", ] ) completion() @@ -417,11 +443,12 @@ extension CarPlayManager { func showBookmarkListTemplate() { guard - let playerManager = AppDelegate.shared?.playerManager, - let libraryService = AppDelegate.shared?.libraryService, - let currentItem = playerManager.currentItem + let coreServices = AppDelegate.shared?.coreServices, + let currentItem = coreServices.playerManager.currentItem else { return } + let libraryService = coreServices.libraryService + let playBookmarks = libraryService.getBookmarks(of: .play, relativePath: currentItem.relativePath) ?? [] let skipBookmarks = libraryService.getBookmarks(of: .skip, relativePath: currentItem.relativePath) ?? [] @@ -439,7 +466,11 @@ extension CarPlayManager { return self?.createBookmarkCPItem(from: bookmark, includeImage: false) } - let section1 = CPListSection(items: automaticItems, header: "bookmark_type_automatic_title".localized, sectionIndexTitle: nil) + let section1 = CPListSection( + items: automaticItems, + header: "bookmark_type_automatic_title".localized, + sectionIndexTitle: nil + ) let section2 = CPListSection(items: userItems, header: "bookmark_type_user_title".localized, sectionIndexTitle: nil) @@ -453,15 +484,16 @@ extension CarPlayManager { extension CarPlayManager { func showPlaybackControlsTemplate() { - let boostTitle = UserDefaults.standard.bool(forKey: Constants.UserDefaults.boostVolumeEnabled) - ? "\("settings_boostvolume_title".localized): \("active_title".localized)" - : "\("settings_boostvolume_title".localized): \("sleep_off_title".localized)" + let boostTitle = + UserDefaults.standard.bool(forKey: Constants.UserDefaults.boostVolumeEnabled) + ? "\("settings_boostvolume_title".localized): \("active_title".localized)" + : "\("settings_boostvolume_title".localized): \("sleep_off_title".localized)" boostVolumeItem.setText(boostTitle) let section1 = CPListSection(items: [boostVolumeItem]) - let currentSpeed = AppDelegate.shared?.playerManager?.currentSpeed ?? 1 + let currentSpeed = AppDelegate.shared?.coreServices?.playerManager.currentSpeed ?? 1 let formattedSpeed = formatSpeed(currentSpeed) let speedItems = self.getSpeedOptions() @@ -475,7 +507,7 @@ extension CarPlayManager { object: self, userInfo: [ "command": Command.speed.rawValue, - "rate": "\(roundedValue)" + "rate": "\(roundedValue)", ] ) @@ -485,7 +517,11 @@ extension CarPlayManager { return item }) - let section2 = CPListSection(items: speedItems, header: "\("player_speed_title".localized): \(formattedSpeed)", sectionIndexTitle: nil) + let section2 = CPListSection( + items: speedItems, + header: "\("player_speed_title".localized): \(formattedSpeed)", + sectionIndexTitle: nil + ) let listTemplate = CPListTemplate(title: "settings_controls_title".localized, sections: [section1, section2]) @@ -498,7 +534,7 @@ extension CarPlayManager { 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, - 4.0 + 4.0, ] } } @@ -506,8 +542,8 @@ extension CarPlayManager { extension CarPlayManager: CPInterfaceControllerDelegate {} extension CarPlayManager: AlertPresenter { - func showLoader() { } - func stopLoader() { } + func showLoader() {} + func stopLoader() {} public func showAlert(_ title: String? = nil, message: String? = nil, completion: (() -> Void)? = nil) { let okAction = CPAlertAction(title: "ok_button".localized, style: .default) { _ in diff --git a/BookPlayer/Services/ListSyncRefreshService.swift b/BookPlayer/Services/ListSyncRefreshService.swift index 9bbd703a..8bd671b6 100644 --- a/BookPlayer/Services/ListSyncRefreshService.swift +++ b/BookPlayer/Services/ListSyncRefreshService.swift @@ -49,12 +49,28 @@ class ListSyncRefreshService: BPLogger { private func reloadLastBook(relativePath: String, alertPresenter: AlertPresenter) { let wasPlaying = playerManager.isPlaying playerManager.stop() - AppDelegate.shared?.loadPlayer( - relativePath, - autoplay: wasPlaying, - showPlayer: nil, - alertPresenter: alertPresenter - ) + + Task { + do { + try await AppDelegate.shared?.coreServices?.playerLoaderService.loadPlayer( + relativePath, + autoplay: wasPlaying + ) + } catch BPPlayerError.fileMissing { + alertPresenter.showAlert( + "file_missing_title".localized, + message: + "\("file_missing_description".localized)\n\(relativePath)", + completion: nil + ) + } catch { + alertPresenter.showAlert( + "error_title".localized, + message: error.localizedDescription, + completion: nil + ) + } + } } @MainActor @@ -63,11 +79,25 @@ class ListSyncRefreshService: BPLogger { guard playerManager.isPlaying == false else { return } await syncService.setLibraryLastBook(with: relativePath) - AppDelegate.shared?.loadPlayer( - relativePath, - autoplay: false, - showPlayer: nil, - alertPresenter: alertPresenter - ) + + do { + try await AppDelegate.shared?.coreServices?.playerLoaderService.loadPlayer( + relativePath, + autoplay: false + ) + } catch BPPlayerError.fileMissing { + alertPresenter.showAlert( + "file_missing_title".localized, + message: + "\("file_missing_description".localized)\n\(relativePath)", + completion: nil + ) + } catch { + alertPresenter.showAlert( + "error_title".localized, + message: error.localizedDescription, + completion: nil + ) + } } } diff --git a/BookPlayer/Settings/Player Settings Screen/PlayerSettingsViewController.swift b/BookPlayer/Settings/Player Settings Screen/PlayerSettingsViewController.swift index 25be3e81..dfd0f99e 100644 --- a/BookPlayer/Settings/Player Settings Screen/PlayerSettingsViewController.swift +++ b/BookPlayer/Settings/Player Settings Screen/PlayerSettingsViewController.swift @@ -27,7 +27,8 @@ class PlayerSettingsViewController: UITableViewController, Storyboarded { private var disposeBag = Set() enum SettingsSection: Int { - case intervals = 0, rewind, sleepTimer, volume, speed, playerList, progressLabels + case intervals = 0 + case rewind, sleepTimer, volume, speed, playerList, progressLabels } let playerListPreferencePath = IndexPath(row: 0, section: SettingsSection.playerList.rawValue) @@ -51,19 +52,19 @@ class PlayerSettingsViewController: UITableViewController, Storyboarded { // Set initial switch positions smartRewindSwitch.setOn( - UserDefaults.standard.bool(forKey: Constants.UserDefaults.smartRewindEnabled), + UserDefaults.standard.bool(forKey: Constants.UserDefaults.smartRewindEnabled), animated: false ) autoSleepTimerSwitch.setOn( - UserDefaults.standard.bool(forKey: Constants.UserDefaults.autoTimerEnabled), + UserDefaults.standard.bool(forKey: Constants.UserDefaults.autoTimerEnabled), animated: false ) boostVolumeSwitch.setOn( - UserDefaults.standard.bool(forKey: Constants.UserDefaults.boostVolumeEnabled), + UserDefaults.standard.bool(forKey: Constants.UserDefaults.boostVolumeEnabled), animated: false ) globalSpeedSwitch.setOn( - UserDefaults.standard.bool(forKey: Constants.UserDefaults.globalSpeedEnabled), + UserDefaults.standard.bool(forKey: Constants.UserDefaults.globalSpeedEnabled), animated: false ) chapterTimeSwitch.setOn( @@ -112,13 +113,17 @@ class PlayerSettingsViewController: UITableViewController, Storyboarded { func showPlayerListOptionAlert(indexPath: IndexPath) { let sheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - sheet.addAction(UIAlertAction(title: "chapters_title".localized, style: .default) { [weak self] _ in - self?.viewModel.handleOptionSelected(.chapters) - }) + sheet.addAction( + UIAlertAction(title: "chapters_title".localized, style: .default) { [weak self] _ in + self?.viewModel.handleOptionSelected(.chapters) + } + ) - sheet.addAction(UIAlertAction(title: "bookmarks_title".localized, style: .default) { [weak self] _ in - self?.viewModel.handleOptionSelected(.bookmarks) - }) + sheet.addAction( + UIAlertAction(title: "bookmarks_title".localized, style: .default) { [weak self] _ in + self?.viewModel.handleOptionSelected(.bookmarks) + } + ) sheet.addAction(UIAlertAction(title: "cancel_button".localized, style: .cancel)) @@ -217,7 +222,7 @@ class PlayerSettingsViewController: UITableViewController, Storyboarded { @objc func boostVolumeToggleDidChange() { UserDefaults.standard.set(self.boostVolumeSwitch.isOn, forKey: Constants.UserDefaults.boostVolumeEnabled) - guard let playerManager = AppDelegate.shared?.playerManager else { return } + guard let playerManager = AppDelegate.shared?.coreServices?.playerManager else { return } playerManager.setBoostVolume(self.boostVolumeSwitch.isOn) } diff --git a/BookPlayer/Utils/CoreServices.swift b/BookPlayer/Utils/CoreServices.swift index 63e6910d..7da91f31 100644 --- a/BookPlayer/Utils/CoreServices.swift +++ b/BookPlayer/Utils/CoreServices.swift @@ -11,10 +11,11 @@ import Foundation struct CoreServices { let dataManager: DataManager - let accountService: AccountServiceProtocol - let syncService: SyncServiceProtocol - let libraryService: LibraryServiceProtocol - let playbackService: PlaybackServiceProtocol - let playerManager: PlayerManagerProtocol + let accountService: AccountService + let syncService: SyncService + let libraryService: LibraryService + let playbackService: PlaybackService + let playerManager: PlayerManager + let playerLoaderService: PlayerLoaderService let watchService: PhoneWatchConnectivityService } diff --git a/BookPlayerWidgets/Phone/BookPlaybackToggleIntent.swift b/BookPlayerWidgets/Phone/BookPlaybackToggleIntent.swift index 77003c7b..11c1edef 100644 --- a/BookPlayerWidgets/Phone/BookPlaybackToggleIntent.swift +++ b/BookPlayerWidgets/Phone/BookPlaybackToggleIntent.swift @@ -6,10 +6,10 @@ // Copyright © 2023 Tortuga Power. All rights reserved. // -import Foundation +import AVFoundation import AppIntents import BookPlayerKit -import AVFoundation +import Foundation @available(iOS 17.0, macOS 14.0, watchOS 10.0, *) struct BookPlaybackToggleIntent: AudioPlaybackIntent { @@ -17,10 +17,13 @@ struct BookPlaybackToggleIntent: AudioPlaybackIntent { static var title: LocalizedStringResource = .init("Toggle playback of book") @Parameter(title: "relativePath") - var relativePath: String? + var relativePath: String + + @Dependency + var playerLoaderService: PlayerLoaderService init() { - relativePath = nil + self.relativePath = "" } init(relativePath: String) { @@ -28,15 +31,11 @@ struct BookPlaybackToggleIntent: AudioPlaybackIntent { } func perform() async throws -> some IntentResult { - let url = WidgetUtils.getWidgetActionURL( - with: relativePath, - playbackToggle: true - ).absoluteString - - UserDefaults.sharedDefaults.set( - url, - forKey: Constants.UserDefaults.sharedWidgetActionURL - ) + if playerLoaderService.playerManager.currentItem?.relativePath == relativePath { + playerLoaderService.playerManager.playPause() + } else { + try await playerLoaderService.loadPlayer(relativePath, autoplay: true) + } return .result() } diff --git a/BookPlayerWidgets/Phone/BookStartPlaybackIntent.swift b/BookPlayerWidgets/Phone/BookStartPlaybackIntent.swift deleted file mode 100644 index 8fc2513e..00000000 --- a/BookPlayerWidgets/Phone/BookStartPlaybackIntent.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// BookStartPlaybackIntent.swift -// BookPlayer -// -// Created by Gianni Carlo on 7/11/23. -// Copyright © 2023 Tortuga Power. All rights reserved. -// - -import Foundation -import AppIntents -import BookPlayerKit -import AVFoundation - -@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) -struct BookStartPlaybackIntent: AudioPlaybackIntent { - - static var title: LocalizedStringResource = .init("Start playback of book") - - @Parameter(title: "relativePath") - var relativePath: String? - - init() { - relativePath = nil - } - - init(relativePath: String) { - self.relativePath = relativePath - } - - func perform() async throws -> some IntentResult { - let url = WidgetUtils.getWidgetActionURL( - with: relativePath, - autoplay: true, - timerSeconds: nil - ).absoluteString - - UserDefaults.sharedDefaults.set( - url, - forKey: Constants.UserDefaults.sharedWidgetActionURL - ) - - return .result() - } -} - diff --git a/Shared/Constants.swift b/Shared/Constants.swift index 7a6b7c05..0d89d3b7 100644 --- a/Shared/Constants.swift +++ b/Shared/Constants.swift @@ -50,9 +50,6 @@ public enum Constants { // One-time migrations public static let fileProtectionMigration = "userFileProtectionMigration" - /// Shared widget action URL - public static let sharedWidgetActionURL = "sharedWidgetActionURL" - /// Shared widget currently playing relative path public static let sharedWidgetNowPlayingPath = "sharedWidgetNowPlayingPath" diff --git a/Shared/Extensions/UserDefaults+BookPlayer.swift b/Shared/Extensions/UserDefaults+BookPlayer.swift index b74f03be..9fe3329e 100644 --- a/Shared/Extensions/UserDefaults+BookPlayer.swift +++ b/Shared/Extensions/UserDefaults+BookPlayer.swift @@ -11,14 +11,6 @@ import Foundation public extension UserDefaults { static var sharedDefaults = UserDefaults(suiteName: Constants.ApplicationGroupIdentifier)! - @objc dynamic var sharedWidgetActionURL: URL? { - guard - let widgetActionString = string(forKey: Constants.UserDefaults.sharedWidgetActionURL) - else { return nil } - - return URL(string: widgetActionString) - } - @objc dynamic var userSettingsAppIcon: String? { return string(forKey: Constants.UserDefaults.appIcon) } diff --git a/Shared/Services/LibraryService.swift b/Shared/Services/LibraryService.swift index 93c473a4..38b50ddc 100644 --- a/Shared/Services/LibraryService.swift +++ b/Shared/Services/LibraryService.swift @@ -136,7 +136,7 @@ public protocol LibraryServiceProtocol { } // swiftlint:disable force_cast -public final class LibraryService: LibraryServiceProtocol { +public final class LibraryService: LibraryServiceProtocol, @unchecked Sendable { let dataManager: DataManager /// Internal passthrough publisher for emitting metadata update events From 5f015fe4a920b78ea1200747b73df143989fdc88 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 5 Oct 2024 11:56:52 -0500 Subject: [PATCH 2/3] Replace launch widget for last played widget --- BookPlayer.xcodeproj/project.pbxproj | 16 ++-- .../bookplayer.icon.symbolset/Contents.json | 12 --- .../bookplayer.icon.svg | 94 ------------------- BookPlayerWidgets/BookPlayerWidgets.swift | 2 +- .../Phone/LaunchAppControlWidgetView.swift | 43 --------- .../Phone/PlayLastControlWidgetView.swift | 26 +++++ 6 files changed, 37 insertions(+), 156 deletions(-) delete mode 100644 BookPlayerWidgets/Assets.xcassets/bookplayer.icon.symbolset/Contents.json delete mode 100644 BookPlayerWidgets/Assets.xcassets/bookplayer.icon.symbolset/bookplayer.icon.svg delete mode 100644 BookPlayerWidgets/Phone/LaunchAppControlWidgetView.swift create mode 100644 BookPlayerWidgets/Phone/PlayLastControlWidgetView.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index 9e80bf0d..450691dc 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -388,8 +388,6 @@ 639720832CAB0C380045A4DB /* LastPlayedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639720822CAB0C380045A4DB /* LastPlayedView.swift */; }; 639720852CABB0D00045A4DB /* RecentBooksProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639720842CABB0D00045A4DB /* RecentBooksProvider.swift */; }; 6397208A2CAC5C870045A4DB /* LastPlayedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639720892CAC5C870045A4DB /* LastPlayedModel.swift */; }; - 6397208C2CAC95040045A4DB /* LaunchAppControlWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6397208B2CAC95040045A4DB /* LaunchAppControlWidgetView.swift */; }; - 6397208D2CAC95040045A4DB /* LaunchAppControlWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6397208B2CAC95040045A4DB /* LaunchAppControlWidgetView.swift */; }; 6399F94D2AA03C6C00A5C8EA /* BPSKANManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6399F94C2AA03C6C00A5C8EA /* BPSKANManager.swift */; }; 639AC9892AD9F1D50053AFC6 /* BPDownloadURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639AC9882AD9F1D50053AFC6 /* BPDownloadURLSession.swift */; }; 639AC98A2AD9F1D50053AFC6 /* BPDownloadURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639AC9882AD9F1D50053AFC6 /* BPDownloadURLSession.swift */; }; @@ -432,6 +430,10 @@ 63E893962CAFAB8F00946CD4 /* PlayerLoaderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893942CAFAB8F00946CD4 /* PlayerLoaderService.swift */; }; 63E893982CAFAC7500946CD4 /* PlayerManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893972CAFAC7500946CD4 /* PlayerManagerProtocol.swift */; }; 63E893992CAFAC7500946CD4 /* PlayerManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893972CAFAC7500946CD4 /* PlayerManagerProtocol.swift */; }; + 63E893B02CB0AA7100946CD4 /* LastBookStartPlaybackIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6356F9C02AC823EE00B7A027 /* LastBookStartPlaybackIntent.swift */; }; + 63E893B22CB0AACE00946CD4 /* PlayLastControlWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893B12CB0AACE00946CD4 /* PlayLastControlWidgetView.swift */; }; + 63E893B32CB0AACE00946CD4 /* PlayLastControlWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893B12CB0AACE00946CD4 /* PlayLastControlWidgetView.swift */; }; + 63E893B42CB1775500946CD4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 419B375423B8D5A500128A8F /* Localizable.strings */; }; 63F1C7892BB91260006B164C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 63F1C7882BB91259006B164C /* PrivacyInfo.xcprivacy */; }; 63F1C78B2BB91E21006B164C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 63F1C78A2BB91E1B006B164C /* PrivacyInfo.xcprivacy */; }; 63F828572AED56FA00B5CE0C /* CornerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F828562AED56FA00B5CE0C /* CornerView.swift */; }; @@ -1175,7 +1177,6 @@ 639720822CAB0C380045A4DB /* LastPlayedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastPlayedView.swift; sourceTree = ""; }; 639720842CABB0D00045A4DB /* RecentBooksProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentBooksProvider.swift; sourceTree = ""; }; 639720892CAC5C870045A4DB /* LastPlayedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastPlayedModel.swift; sourceTree = ""; }; - 6397208B2CAC95040045A4DB /* LaunchAppControlWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAppControlWidgetView.swift; sourceTree = ""; }; 6399F94C2AA03C6C00A5C8EA /* BPSKANManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPSKANManager.swift; sourceTree = ""; }; 639AC9882AD9F1D50053AFC6 /* BPDownloadURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPDownloadURLSession.swift; sourceTree = ""; }; 639E12C52B85AACF00C875F7 /* SyncTasksObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncTasksObject.swift; sourceTree = ""; }; @@ -1198,6 +1199,7 @@ 63E893912CAFA89000946CD4 /* BPPlayerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPPlayerError.swift; sourceTree = ""; }; 63E893942CAFAB8F00946CD4 /* PlayerLoaderService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerLoaderService.swift; sourceTree = ""; }; 63E893972CAFAC7500946CD4 /* PlayerManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerManagerProtocol.swift; sourceTree = ""; }; + 63E893B12CB0AACE00946CD4 /* PlayLastControlWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayLastControlWidgetView.swift; sourceTree = ""; }; 63F1C7882BB91259006B164C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 63F1C78A2BB91E1B006B164C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 63F828562AED56FA00B5CE0C /* CornerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerView.swift; sourceTree = ""; }; @@ -2310,7 +2312,7 @@ isa = PBXGroup; children = ( 6309F1252B0CF1C1002B86A4 /* BookPlaybackToggleIntent.swift */, - 6397208B2CAC95040045A4DB /* LaunchAppControlWidgetView.swift */, + 63E893B12CB0AACE00946CD4 /* PlayLastControlWidgetView.swift */, 4106413E258725F1008EB8D0 /* TimeListened */, 637DAB092AEB3E0D006DC2D1 /* WidgetEntries.swift */, 418445C2258AE11E0072DD13 /* WidgetUtils.swift */, @@ -3173,6 +3175,7 @@ buildActionMask = 2147483647; files = ( 63F1C78B2BB91E21006B164C /* PrivacyInfo.xcprivacy in Resources */, + 63E893B42CB1775500946CD4 /* Localizable.strings in Resources */, 416A29AC2569658300605395 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3465,6 +3468,7 @@ files = ( 418445C3258AE11E0072DD13 /* WidgetUtils.swift in Sources */, 4106414025872614008EB8D0 /* BarView.swift in Sources */, + 63E893B02CB0AA7100946CD4 /* LastBookStartPlaybackIntent.swift in Sources */, 630826062AF525F1002ACE0D /* SharedWidgetContainerView.swift in Sources */, 4106414925872699008EB8D0 /* TimeListenedSmallView.swift in Sources */, 417D996F256D73B400C3B753 /* Intents.intentdefinition in Sources */, @@ -3478,7 +3482,6 @@ 410641282579AA2F008EB8D0 /* TimeListenedWidgetView.swift in Sources */, 6309F1272B0CF658002B86A4 /* BookPlaybackToggleIntent.swift in Sources */, 630826032AF5225F002ACE0D /* CircularView.swift in Sources */, - 6397208C2CAC95040045A4DB /* LaunchAppControlWidgetView.swift in Sources */, 630826042AF522EA002ACE0D /* SharedWidgetEntry.swift in Sources */, 63E893962CAFAB8F00946CD4 /* PlayerLoaderService.swift in Sources */, 41064152258726D2008EB8D0 /* TimeListenedMediumView.swift in Sources */, @@ -3494,6 +3497,7 @@ 639720832CAB0C380045A4DB /* LastPlayedView.swift in Sources */, 639720852CABB0D00045A4DB /* RecentBooksProvider.swift in Sources */, 630826072AF52831002ACE0D /* SharedWidget.swift in Sources */, + 63E893B32CB0AACE00946CD4 /* PlayLastControlWidgetView.swift in Sources */, 639720722CAAF8290045A4DB /* LastPlayedProvider.swift in Sources */, 630826022AF295AE002ACE0D /* CornerView.swift in Sources */, ); @@ -3605,6 +3609,7 @@ D6BA8F162A4CA94800C2BD9A /* StorageRowView.swift in Sources */, 9F3C436A284181690066D99A /* DataInitializerCoordinator.swift in Sources */, 9F3C436B284181C70066D99A /* AlertPresenter.swift in Sources */, + 63E893B22CB0AACE00946CD4 /* PlayLastControlWidgetView.swift in Sources */, 63B760FC2C33B77F00AA98C7 /* SupportProfileView.swift in Sources */, 9F00A6212950F44B005EA316 /* ImagePicker.swift in Sources */, 9F00A6242951F2F3005EA316 /* ItemDetailsFormViewModel.swift in Sources */, @@ -3657,7 +3662,6 @@ 9F5F13682978D9E100F061A0 /* ProfileSyncTasksStatusView.swift in Sources */, 41AD3DA7221C850F00DC41E1 /* IconCellView.swift in Sources */, 9FF710B92A213084006490E0 /* QueuedSyncTaskRowView.swift in Sources */, - 6397208D2CAC95040045A4DB /* LaunchAppControlWidgetView.swift in Sources */, 9FAB93742A53117C005B92B2 /* CompleteAccountView.swift in Sources */, 6304CF6A2B4C2AE800055285 /* SettingsAutoplayView.swift in Sources */, 4158388326EBD76A00F4A12B /* LibraryListCoordinator.swift in Sources */, diff --git a/BookPlayerWidgets/Assets.xcassets/bookplayer.icon.symbolset/Contents.json b/BookPlayerWidgets/Assets.xcassets/bookplayer.icon.symbolset/Contents.json deleted file mode 100644 index 35488c0f..00000000 --- a/BookPlayerWidgets/Assets.xcassets/bookplayer.icon.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "bookplayer.icon.svg", - "idiom" : "universal" - } - ] -} diff --git a/BookPlayerWidgets/Assets.xcassets/bookplayer.icon.symbolset/bookplayer.icon.svg b/BookPlayerWidgets/Assets.xcassets/bookplayer.icon.symbolset/bookplayer.icon.svg deleted file mode 100644 index 6ec43527..00000000 --- a/BookPlayerWidgets/Assets.xcassets/bookplayer.icon.symbolset/bookplayer.icon.svg +++ /dev/null @@ -1,94 +0,0 @@ - - - - - -Weight/Scale Variations -Ultralight -Thin -Light -Regular -Medium -Semibold -Bold -Heavy -Black - - - - - - - - - - -Design Variations -Symbols are supported in up to nine weights and three scales. -For optimal layout with text and other symbols, vertically align -symbols with the adjacent text. - - - - - -Margins -Leading and trailing margins on the left and right side of each symbol -can be adjusted by modifying the x-location of the margin guidelines. -Modifications are automatically applied proportionally to all -scales and weights. - - - -Exporting -Symbols should be outlined when exporting to ensure the -design is preserved when submitting to Xcode. -Template v.6.0 -Requires Xcode 16 or greater -Generated from square.and.arrow.up.circle -Typeset at 100.0 points -Small -Medium -Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/BookPlayerWidgets/BookPlayerWidgets.swift b/BookPlayerWidgets/BookPlayerWidgets.swift index 9b11fe90..16aa7f70 100644 --- a/BookPlayerWidgets/BookPlayerWidgets.swift +++ b/BookPlayerWidgets/BookPlayerWidgets.swift @@ -46,7 +46,7 @@ struct BookPlayerBundle: WidgetBundle { SharedIconWidget() } if #available(iOSApplicationExtension 18.0, *) { - LaunchAppButton() + PlayLastControlWidgetView() } #elseif os(watchOS) SharedWidget() diff --git a/BookPlayerWidgets/Phone/LaunchAppControlWidgetView.swift b/BookPlayerWidgets/Phone/LaunchAppControlWidgetView.swift deleted file mode 100644 index 8b1a66e6..00000000 --- a/BookPlayerWidgets/Phone/LaunchAppControlWidgetView.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// LaunchAppControlWidgetView.swift -// BookPlayerWidgetsPhone -// -// Created by Gianni Carlo on 1/10/24. -// Copyright © 2024 Tortuga Power. All rights reserved. -// - -import AppIntents -import Foundation -import SwiftUI -import WidgetKit - -@available(iOSApplicationExtension 18.0, iOS 18.0, *) -struct LaunchAppButton: ControlWidget { - var body: some ControlWidgetConfiguration { - StaticControlConfiguration( - kind: "com.bookplayer.controlcenter.launchapp" - ) { - ControlWidgetButton(action: LaunchAppIntent()) { - Label("BookPlayer", image: "bookplayer.icon") - } - } - .displayName("BookPlayer") - } -} - -@available(iOSApplicationExtension 16, iOS 16.0, *) -struct LaunchAppIntent: OpenIntent { - static var title: LocalizedStringResource = "Launch App" - @Parameter(title: "Target") - var target: LaunchAppEnum -} - -@available(iOSApplicationExtension 16.0, iOS 16.0, *) -enum LaunchAppEnum: String, AppEnum { - case home - - static var typeDisplayRepresentation = TypeDisplayRepresentation("BookPlayer Home") - static var caseDisplayRepresentations = [ - LaunchAppEnum.home: DisplayRepresentation("Home") - ] -} diff --git a/BookPlayerWidgets/Phone/PlayLastControlWidgetView.swift b/BookPlayerWidgets/Phone/PlayLastControlWidgetView.swift new file mode 100644 index 00000000..c311e3c6 --- /dev/null +++ b/BookPlayerWidgets/Phone/PlayLastControlWidgetView.swift @@ -0,0 +1,26 @@ +// +// LastPlayControlWidgetView.swift +// BookPlayer +// +// Created by Gianni Carlo on 4/10/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import AppIntents +import Foundation +import SwiftUI +import WidgetKit + +@available(iOSApplicationExtension 18.0, iOS 18.0, *) +struct PlayLastControlWidgetView: ControlWidget { + var body: some ControlWidgetConfiguration { + StaticControlConfiguration( + kind: "com.bookplayer.controlcenter.lastplayed" + ) { + ControlWidgetButton(action: LastBookStartPlaybackIntent()) { + Label("intent_lastbook_play_title", systemImage: "play.circle") + } + } + .displayName("intent_lastbook_play_title") + } +} From ef8d36d8d00fe192bc7be8062d2f58868cd4db5b Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 5 Oct 2024 16:20:10 -0500 Subject: [PATCH 3/3] Update tests --- BookPlayer/Utils/CoreServices.swift | 8 ++--- .../ItemListCoordinatorTests.swift | 1 + .../LoadingCoordinatorTests.swift | 2 +- .../Services/AccountServiceTests.swift | 6 +++- BookPlayerTests/StorageViewModelTests.swift | 30 ++++++++++++++++--- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/BookPlayer/Utils/CoreServices.swift b/BookPlayer/Utils/CoreServices.swift index 7da91f31..4c67c759 100644 --- a/BookPlayer/Utils/CoreServices.swift +++ b/BookPlayer/Utils/CoreServices.swift @@ -11,11 +11,11 @@ import Foundation struct CoreServices { let dataManager: DataManager - let accountService: AccountService - let syncService: SyncService + let accountService: AccountServiceProtocol + let syncService: SyncServiceProtocol let libraryService: LibraryService - let playbackService: PlaybackService - let playerManager: PlayerManager + let playbackService: PlaybackServiceProtocol + let playerManager: PlayerManagerProtocol let playerLoaderService: PlayerLoaderService let watchService: PhoneWatchConnectivityService } diff --git a/BookPlayerTests/Coordinators/ItemListCoordinatorTests.swift b/BookPlayerTests/Coordinators/ItemListCoordinatorTests.swift index 0de8dab8..15be2ebc 100644 --- a/BookPlayerTests/Coordinators/ItemListCoordinatorTests.swift +++ b/BookPlayerTests/Coordinators/ItemListCoordinatorTests.swift @@ -66,6 +66,7 @@ class LibraryListCoordinatorTests: XCTestCase { XCTAssertTrue(presentingController.horizontalStack == ["ItemListViewController", "ItemListViewController"]) } + @MainActor func testShowPlayer() { self.libraryListCoordinator.showPlayer() XCTAssert(presentingController.verticalStack == ["PlayerViewController"]) diff --git a/BookPlayerTests/Coordinators/LoadingCoordinatorTests.swift b/BookPlayerTests/Coordinators/LoadingCoordinatorTests.swift index cf065a64..e3489347 100644 --- a/BookPlayerTests/Coordinators/LoadingCoordinatorTests.swift +++ b/BookPlayerTests/Coordinators/LoadingCoordinatorTests.swift @@ -25,7 +25,7 @@ class LoadingCoordinatorTests: XCTestCase { } func testFinishedLoadingSequence() { - self.loadingCoordinator.didFinishLoadingSequence(coreDataStack: CoreDataStack(testPath: "/dev/null")) + self.loadingCoordinator.didFinishLoadingSequence() XCTAssertNotNil(self.loadingCoordinator.getMainCoordinator()) } } diff --git a/BookPlayerTests/Services/AccountServiceTests.swift b/BookPlayerTests/Services/AccountServiceTests.swift index 603077d6..3f0d47c3 100644 --- a/BookPlayerTests/Services/AccountServiceTests.swift +++ b/BookPlayerTests/Services/AccountServiceTests.swift @@ -21,7 +21,11 @@ class AccountServiceTests: XCTestCase { DataTestUtils.clearFolderContents(url: DataManager.getProcessedFolderURL()) let dataManager = DataManager(coreDataStack: CoreDataStack(testPath: "/dev/null")) self.mockKeychain = KeychainServiceProtocolMock() - self.sut = AccountService(dataManager: dataManager, keychain: self.mockKeychain) + self.sut = AccountService( + dataManager: dataManager, + client: NetworkClientMock(mockedResponse: Empty()), + keychain: self.mockKeychain + ) } private func setupBlankAccount() { diff --git a/BookPlayerTests/StorageViewModelTests.swift b/BookPlayerTests/StorageViewModelTests.swift index e3ea906b..b84efe3d 100644 --- a/BookPlayerTests/StorageViewModelTests.swift +++ b/BookPlayerTests/StorageViewModelTests.swift @@ -46,9 +46,6 @@ final class StorageViewModelMissingFileTests: XCTestCase { } func testSetup(with filename: String) { - /// Avoid making the second onboarding network call - AppDelegate.shared?.accountService = AccountServiceMock(account: nil) - let bookContents = "bookcontents".data(using: .utf8)! let documentsURL = DataManager.getDocumentsFolderURL() @@ -68,9 +65,34 @@ final class StorageViewModelMissingFileTests: XCTestCase { let dataManager = DataManager(coreDataStack: CoreDataStack(testPath: self.testPath)) let libraryService = LibraryService(dataManager: dataManager) _ = libraryService.getLibrary() + let syncService = SyncServiceProtocolMock() + let playbackService = PlaybackServiceProtocolMock() + let playerManager = PlayerManagerProtocolMock() + + /// Avoid making the second onboarding network call + AppDelegate.shared?.coreServices = CoreServices( + dataManager: dataManager, + accountService: AccountServiceMock(account: nil), + syncService: syncService, + libraryService: libraryService, + playbackService: playbackService, + playerManager: playerManager, + playerLoaderService: PlayerLoaderService( + syncService: syncService, + libraryService: libraryService, + playbackService: playbackService, + playerManager: playerManager + ), + watchService: PhoneWatchConnectivityService( + libraryService: libraryService, + playbackService: playbackService, + playerManager: playerManager + ) + ) + self.viewModel = StorageViewModel( libraryService: libraryService, - syncService: SyncServiceProtocolMock(), + syncService: syncService, folderURL: self.directoryURL ) }