From 52533a7c80a3a7d3600429b2f9727f9caa565099 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 16 Nov 2024 22:56:52 -0500 Subject: [PATCH 01/31] Add Apple Sign In to Watch app --- BookPlayer.xcodeproj/project.pbxproj | 64 ++++++++++++++++-- BookPlayerWatch/BP+ErrorAlerts.swift | 48 +++++++++++++ BookPlayerWatch/BookPlayerApp.swift | 9 +++ BookPlayerWatch/BookPlayerWatch.entitlements | 4 ++ BookPlayerWatch/Info.plist | 10 +++ .../ItemList/ContainerItemListView.swift | 29 +++++++- .../Settings/Login/LoginView.swift | 67 +++++++++++++++++++ .../Settings/Login/LoginViewModel.swift | 37 ++++++++++ .../Settings/Profile/ProfileView.swift | 63 +++++++++++++++++ .../Settings/Profile/ProfileViewModel.swift | 23 +++++++ BookPlayerWatch/Settings/SettingsView.swift | 38 +++++++++++ Shared/Network/NetworkClient.swift | 3 + Shared/Network/NetworkProvider.swift | 2 +- Shared/Services/Account/AccountAPI.swift | 6 +- 14 files changed, 390 insertions(+), 13 deletions(-) create mode 100644 BookPlayerWatch/BP+ErrorAlerts.swift create mode 100644 BookPlayerWatch/Settings/Login/LoginView.swift create mode 100644 BookPlayerWatch/Settings/Login/LoginViewModel.swift create mode 100644 BookPlayerWatch/Settings/Profile/ProfileView.swift create mode 100644 BookPlayerWatch/Settings/Profile/ProfileViewModel.swift create mode 100644 BookPlayerWatch/Settings/SettingsView.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index d4a00c72f..4124fda46 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -423,6 +423,14 @@ 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 */; }; + 63CD851D2CE2964900EDBEA8 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD851C2CE2964900EDBEA8 /* SettingsView.swift */; }; + 63CD851E2CE2EEBC00EDBEA8 /* UIFont+BookPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F22DE3D288D812900056FCD /* UIFont+BookPlayer.swift */; }; + 63CD851F2CE2EEE200EDBEA8 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F22DE2D288CB1A900056FCD /* Fonts.swift */; }; + 63CD85232CE302D200EDBEA8 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85222CE302D200EDBEA8 /* LoginView.swift */; }; + 63CD85252CE3046500EDBEA8 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85242CE3046500EDBEA8 /* LoginViewModel.swift */; }; + 63CD85272CE3064600EDBEA8 /* BP+ErrorAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85262CE3064600EDBEA8 /* BP+ErrorAlerts.swift */; }; + 63CD85432CE3105300EDBEA8 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85422CE3105300EDBEA8 /* ProfileView.swift */; }; + 63CD85452CE3109000EDBEA8 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85442CE3109000EDBEA8 /* ProfileViewModel.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 */; }; @@ -1194,6 +1202,12 @@ 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 = ""; }; + 63CD851C2CE2964900EDBEA8 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 63CD85222CE302D200EDBEA8 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; + 63CD85242CE3046500EDBEA8 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; + 63CD85262CE3064600EDBEA8 /* BP+ErrorAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BP+ErrorAlerts.swift"; sourceTree = ""; }; + 63CD85422CE3105300EDBEA8 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; + 63CD85442CE3109000EDBEA8 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.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 = ""; }; @@ -1584,11 +1598,13 @@ 4140EA46227288EF0009F794 /* ComplicationController.swift */, 9F82DF7027DF8203001B0EA8 /* WatchConnectivityService.swift */, 9FA334AE27C05EBB0064E8EA /* ContextManager.swift */, + 63CD85262CE3064600EDBEA8 /* BP+ErrorAlerts.swift */, 9FA334B427C156DB0064E8EA /* ItemList */, 9FA334B727C163510064E8EA /* NowPlaying */, 9F197EFC27C325150029C989 /* ChapterList */, 9F197EFB27C324FF0029C989 /* PlaybackControls */, 9F197F0327C3E1680029C989 /* Utils */, + 63CD851B2CE2963600EDBEA8 /* Settings */, 4140EA34227288EF0009F794 /* Assets.xcassets */, 4140EA36227288EF0009F794 /* Info.plist */, 419B375B23B8D6DB00128A8F /* Localizable.strings */, @@ -2297,6 +2313,34 @@ path = Autolock; sourceTree = ""; }; + 63CD851B2CE2963600EDBEA8 /* Settings */ = { + isa = PBXGroup; + children = ( + 63CD851C2CE2964900EDBEA8 /* SettingsView.swift */, + 63CD85412CE3102500EDBEA8 /* Profile */, + 63CD85402CE3101C00EDBEA8 /* Login */, + ); + path = Settings; + sourceTree = ""; + }; + 63CD85402CE3101C00EDBEA8 /* Login */ = { + isa = PBXGroup; + children = ( + 63CD85222CE302D200EDBEA8 /* LoginView.swift */, + 63CD85242CE3046500EDBEA8 /* LoginViewModel.swift */, + ); + path = Login; + sourceTree = ""; + }; + 63CD85412CE3102500EDBEA8 /* Profile */ = { + isa = PBXGroup; + children = ( + 63CD85422CE3105300EDBEA8 /* ProfileView.swift */, + 63CD85442CE3109000EDBEA8 /* ProfileViewModel.swift */, + ); + path = Profile; + sourceTree = ""; + }; 63E5D6A92AECB8AB00A67B32 /* Phone */ = { isa = PBXGroup; children = ( @@ -3331,17 +3375,23 @@ 41A8BAFE227E6C88003C9895 /* Notification+BookPlayerWatchApp.swift in Sources */, 41D20DB125D5F5A100AAEE30 /* MappingModel_v1_to_v2.xcmappingmodel in Sources */, 9F82DF6D27DE985A001B0EA8 /* NowPlayingPlaybackControlsView.swift in Sources */, + 63CD85452CE3109000EDBEA8 /* ProfileViewModel.swift in Sources */, 9FF383D22A40F97000BBAC11 /* MappingModel_v8_to_v9.xcmappingmodel in Sources */, 9FA334C327C2833B0064E8EA /* ResizeableImageView.swift in Sources */, 9F82DF6F27DEE83E001B0EA8 /* SkipDirection.swift in Sources */, 9FA334AB27C058210064E8EA /* ItemListView.swift in Sources */, + 63CD85432CE3105300EDBEA8 /* ProfileView.swift in Sources */, 9FA334AF27C05EBB0064E8EA /* ContextManager.swift in Sources */, 9FE07E1B27C522DE007591F7 /* ContainerItemListView.swift in Sources */, 9F82DF6B27DE9792001B0EA8 /* NowPlayingMediaControlsView.swift in Sources */, + 63CD85252CE3046500EDBEA8 /* LoginViewModel.swift in Sources */, 9FA334A927C0577E0064E8EA /* BookPlayerApp.swift in Sources */, + 63CD85272CE3064600EDBEA8 /* BP+ErrorAlerts.swift in Sources */, + 63CD85232CE302D200EDBEA8 /* LoginView.swift in Sources */, 41C3396225E04103003ED2B0 /* MappingModel_v2_to_v3.xcmappingmodel in Sources */, 9FA334C727C28D650064E8EA /* PlaybackControlsView.swift in Sources */, 4140EA8522728A160009F794 /* BookPlayer.xcdatamodeld in Sources */, + 63CD851D2CE2964900EDBEA8 /* SettingsView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3366,6 +3416,7 @@ 4140EA78227289BC0009F794 /* PlaybackRecord+CoreDataClass.swift in Sources */, 4140EA79227289BF0009F794 /* PlaybackRecord+CoreDataProperties.swift in Sources */, 639AC98A2AD9F1D50053AFC6 /* BPDownloadURLSession.swift in Sources */, + 63CD851F2CE2EEE200EDBEA8 /* Fonts.swift in Sources */, 41A90C4927564DAA00C30394 /* BookPlayerError.swift in Sources */, 41C23402272E1960006BC7B8 /* SimpleTheme.swift in Sources */, 638E64CF2B8E1CFD00DCFA3B /* SyncTasksCountService.swift in Sources */, @@ -3379,6 +3430,7 @@ 9FDDD2DF289BEE590020C428 /* SyncJobScheduler.swift in Sources */, 4140EA76227289B50009F794 /* Chapter+CoreDataClass.swift in Sources */, 41A359C6276232E00020D5F5 /* MappingModel_v7_to_v8.xcmappingmodel in Sources */, + 63CD851E2CE2EEBC00EDBEA8 /* UIFont+BookPlayer.swift in Sources */, 62CADBAE2725FE2900A4A98F /* AVAudioAssetImageDataProvider.swift in Sources */, 412AB71627017ABE00969618 /* DBVersion.swift in Sources */, 9FC1E45A2814E0B000522FA8 /* NetworkUtils.swift in Sources */, @@ -4268,7 +4320,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 7.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Debug; }; @@ -4306,7 +4358,7 @@ SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 7.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Release; }; @@ -4344,7 +4396,7 @@ SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 7.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Beta; }; @@ -4389,7 +4441,7 @@ TARGETED_DEVICE_FAMILY = 4; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 7.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Debug; }; @@ -4432,7 +4484,7 @@ TARGETED_DEVICE_FAMILY = 4; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 7.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Release; }; @@ -4475,7 +4527,7 @@ TARGETED_DEVICE_FAMILY = 4; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; - WATCHOS_DEPLOYMENT_TARGET = 7.0; + WATCHOS_DEPLOYMENT_TARGET = 8.0; }; name = Beta; }; diff --git a/BookPlayerWatch/BP+ErrorAlerts.swift b/BookPlayerWatch/BP+ErrorAlerts.swift new file mode 100644 index 000000000..4a86f29da --- /dev/null +++ b/BookPlayerWatch/BP+ErrorAlerts.swift @@ -0,0 +1,48 @@ +// +// BP+ErrorAlerts.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 11/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import Foundation +import SwiftUI + +extension View { + func errorAlert( + error: Binding, + buttonTitle: String = "OK" + ) + -> some View + { + let localizedAlertError = LocalizedAlertError(error: error.wrappedValue) + return alert( + isPresented: .constant(localizedAlertError != nil), + error: localizedAlertError + ) { _ in + Button(buttonTitle) { + error.wrappedValue = nil + } + } message: { error in + Text(error.recoverySuggestion ?? "") + } + } +} + +struct LocalizedAlertError: LocalizedError { + var errorDescription: String? + var recoverySuggestion: String? + + init?(error: Error?) { + if let localizedError = error as? LocalizedError { + self.errorDescription = localizedError.errorDescription + self.recoverySuggestion = localizedError.recoverySuggestion + } else if let error { + self.errorDescription = error.localizedDescription + self.recoverySuggestion = nil + } else { + return nil + } + } +} diff --git a/BookPlayerWatch/BookPlayerApp.swift b/BookPlayerWatch/BookPlayerApp.swift index 47e215a7f..8f0ea89e8 100644 --- a/BookPlayerWatch/BookPlayerApp.swift +++ b/BookPlayerWatch/BookPlayerApp.swift @@ -6,6 +6,7 @@ // Copyright © 2022 Tortuga Power. All rights reserved. // +import RevenueCat import SwiftUI @main @@ -13,6 +14,14 @@ struct BookPlayerApp: App { // swiftlint:disable:next weak_delegate @WKApplicationDelegateAdaptor var extensionDelegate: ExtensionDelegate + init() { + let revenueCatApiKey: String = Bundle.main.configurationValue( + for: .revenueCat + ) + Purchases.logLevel = .error + Purchases.configure(withAPIKey: revenueCatApiKey) + } + @SceneBuilder var body: some Scene { WindowGroup { NavigationView { diff --git a/BookPlayerWatch/BookPlayerWatch.entitlements b/BookPlayerWatch/BookPlayerWatch.entitlements index 51dba7a53..00a548652 100644 --- a/BookPlayerWatch/BookPlayerWatch.entitlements +++ b/BookPlayerWatch/BookPlayerWatch.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.applesignin + + Default + com.apple.security.application-groups group.$(BP_BUNDLE_IDENTIFIER).files diff --git a/BookPlayerWatch/Info.plist b/BookPlayerWatch/Info.plist index 89a895121..ed78b7d04 100644 --- a/BookPlayerWatch/Info.plist +++ b/BookPlayerWatch/Info.plist @@ -2,6 +2,16 @@ + BP_API_DOMAIN + $(BP_API_DOMAIN) + BP_API_PORT + $(BP_API_PORT) + BP_API_SCHEME + $(BP_API_SCHEME) + BP_BUNDLE_IDENTIFIER + $(BP_BUNDLE_IDENTIFIER) + BP_REVENUECAT_KEY + $(BP_REVENUECAT_KEY) BP_BUNDLE_IDENTIFIER $(BP_BUNDLE_IDENTIFIER) CFBundleDevelopmentRegion diff --git a/BookPlayerWatch/ItemList/ContainerItemListView.swift b/BookPlayerWatch/ItemList/ContainerItemListView.swift index f2e85d228..e8053d797 100644 --- a/BookPlayerWatch/ItemList/ContainerItemListView.swift +++ b/BookPlayerWatch/ItemList/ContainerItemListView.swift @@ -6,19 +6,42 @@ // Copyright © 2022 Tortuga Power. All rights reserved. // -import SwiftUI import BookPlayerWatchKit +import SwiftUI struct ContainerItemListView: View { @ObservedObject var contextManager = ExtensionDelegate.contextManager @State var showPlayer = false + @State var showSettings = false var body: some View { VStack { if contextManager.items.isEmpty, - contextManager.isConnecting { + contextManager.isConnecting + { ProgressView() - } else if #available(watchOS 8.0, *) { + } else { + itemList + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + print("Settings") + showSettings = true + } label: { + Image(systemName: "gear") + } + } + } + .fullScreenCover(isPresented: $showSettings) { + SettingsView() + } + } + } + } + + var itemList: some View { + Group { + if #available(watchOS 8.0, *) { ItemListView() .navigationTitle("recent_title") .navigationBarTitleDisplayMode(.inline) diff --git a/BookPlayerWatch/Settings/Login/LoginView.swift b/BookPlayerWatch/Settings/Login/LoginView.swift new file mode 100644 index 000000000..510e653ee --- /dev/null +++ b/BookPlayerWatch/Settings/Login/LoginView.swift @@ -0,0 +1,67 @@ +// +// LoginView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 11/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import AuthenticationServices +import BookPlayerWatchKit +import SwiftUI + +struct LoginView: View { + @StateObject var model = LoginViewModel() + @State private var isLoading = false + @State private var error: Error? + @Environment(\.dismiss) var dismiss + + var body: some View { + VStack(spacing: Spacing.S1) { + Text("BookPlayer Pro") + .font(Font(Fonts.titleLarge)) + Text("Stream your recent books to your Apple Watch, or download them to listen offline on the go.") + .font(Font(Fonts.body)) + .multilineTextAlignment(.center) + SignInWithAppleButton(.signIn) { request in + request.requestedScopes = [.email] + } onCompletion: { result in + switch result { + case .success(let authorization): + Task { + do { + isLoading = true + try await model.handleSignIn(authorization) + isLoading = false + } catch { + isLoading = false + self.error = error + } + } + case .failure(let error): + self.error = error + } + } + .frame(maxHeight: 45) + } + .errorAlert(error: $error) + .overlay { + Group { + if isLoading { + ProgressView() + .tint(.white) + .padding() + .background( + Color.black + .opacity(0.9) + .clipShape(RoundedRectangle(cornerRadius: 10)) + ) + } + } + } + } +} + +#Preview { + LoginView() +} diff --git a/BookPlayerWatch/Settings/Login/LoginViewModel.swift b/BookPlayerWatch/Settings/Login/LoginViewModel.swift new file mode 100644 index 000000000..4ef36fdf1 --- /dev/null +++ b/BookPlayerWatch/Settings/Login/LoginViewModel.swift @@ -0,0 +1,37 @@ +// +// LoginViewModel.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 11/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import AuthenticationServices +import BookPlayerWatchKit +import Foundation +import RevenueCat + +@MainActor +class LoginViewModel: ObservableObject { + private let provider: NetworkProvider = NetworkProvider(client: NetworkClient()) + private let keychain = KeychainService() + + func handleSignIn(_ authorization: ASAuthorization) async throws { + switch authorization.credential { + case let appleIDCredential as ASAuthorizationAppleIDCredential: + guard + let tokenData = appleIDCredential.identityToken, + let token = String(data: tokenData, encoding: .utf8) + else { + throw AccountError.missingToken + } + + let response: LoginResponse = try await provider.request(.login(token: token)) + try self.keychain.setAccessToken(response.token) + _ = try await Purchases.shared.logIn(appleIDCredential.user) + UserDefaults.standard.set(response.email, forKey: "userEmail") + default: + break + } + } +} diff --git a/BookPlayerWatch/Settings/Profile/ProfileView.swift b/BookPlayerWatch/Settings/Profile/ProfileView.swift new file mode 100644 index 000000000..e5a0b4d6d --- /dev/null +++ b/BookPlayerWatch/Settings/Profile/ProfileView.swift @@ -0,0 +1,63 @@ +// +// ProfileView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 11/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import SwiftUI + +struct ProfileView: View { + @StateObject var model = ProfileViewModel() + @State private var isLoading = false + @State private var error: Error? + @AppStorage("userEmail") + var email: String? + + var body: some View { + VStack { + Image(systemName: "person.crop.circle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 45, height: 45) + if let email { + Text(verbatim: email) + } + Spacer() + Button("Log Out") { + Task { + do { + isLoading = true + try await model.handleLogOut() + isLoading = false + } catch { + isLoading = false + self.error = error + } + } + } + .buttonStyle(PlainButtonStyle()) + .foregroundStyle(.red) + } + .errorAlert(error: $error) + .overlay { + Group { + if isLoading { + ProgressView() + .tint(.white) + .padding() + .background( + Color.black + .opacity(0.9) + .clipShape(RoundedRectangle(cornerRadius: 10)) + ) + } + } + } + } +} + +#Preview { + ProfileView() +} diff --git a/BookPlayerWatch/Settings/Profile/ProfileViewModel.swift b/BookPlayerWatch/Settings/Profile/ProfileViewModel.swift new file mode 100644 index 000000000..f60d28009 --- /dev/null +++ b/BookPlayerWatch/Settings/Profile/ProfileViewModel.swift @@ -0,0 +1,23 @@ +// +// ProfileViewModel.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 11/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerWatchKit +import Foundation +import RevenueCat + +@MainActor +class ProfileViewModel: ObservableObject { + private let keychain = KeychainService() + + func handleLogOut() async throws { + try keychain.removeAccessToken() + _ = try await Purchases.shared.logOut() + UserDefaults.standard.removeObject(forKey: "userEmail") + /// Delete downloaded files + } +} diff --git a/BookPlayerWatch/Settings/SettingsView.swift b/BookPlayerWatch/Settings/SettingsView.swift new file mode 100644 index 000000000..3f67fbe3c --- /dev/null +++ b/BookPlayerWatch/Settings/SettingsView.swift @@ -0,0 +1,38 @@ +// +// SettingsView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 11/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import AuthenticationServices +import BookPlayerWatchKit +import SwiftUI + +struct SettingsView: View { + @AppStorage("userEmail") + var email: String? + + var body: some View { + GeometryReader { geometry in + ScrollView { + Group { + if email != nil { + ProfileView() + } else { + LoginView() + .fixedSize(horizontal: false, vertical: true) + } + } + .padding(.horizontal, Spacing.S3) + .frame(width: geometry.size.width, height: geometry.size.height) + } + .frame(width: geometry.size.width, height: geometry.size.height) + } + } +} + +#Preview { + SettingsView() +} diff --git a/Shared/Network/NetworkClient.swift b/Shared/Network/NetworkClient.swift index 2725b3b90..9ff437f26 100644 --- a/Shared/Network/NetworkClient.swift +++ b/Shared/Network/NetworkClient.swift @@ -217,6 +217,9 @@ public class NetworkClient: NetworkClientProtocol, BPLogger { var request = URLRequest(url: url) request.httpMethod = method.rawValue request.setValue("application/json", forHTTPHeaderField: "Content-Type") +#if os(watchOS) + request.setValue("watch.bookplayer.app", forHTTPHeaderField: "origin") +#endif if useKeychain, let accessToken: String = try? keychain.get(.token) { diff --git a/Shared/Network/NetworkProvider.swift b/Shared/Network/NetworkProvider.swift index 7feca359f..dea15fcbe 100644 --- a/Shared/Network/NetworkProvider.swift +++ b/Shared/Network/NetworkProvider.swift @@ -8,7 +8,7 @@ import Foundation -class NetworkProvider { +public class NetworkProvider { public let client: NetworkClientProtocol public init(client: NetworkClientProtocol = NetworkClient()) { diff --git a/Shared/Services/Account/AccountAPI.swift b/Shared/Services/Account/AccountAPI.swift index 05aff5fc5..2a48abe64 100644 --- a/Shared/Services/Account/AccountAPI.swift +++ b/Shared/Services/Account/AccountAPI.swift @@ -53,9 +53,9 @@ extension AccountAPI: Endpoint { } } -struct LoginResponse: Decodable { - let email: String - let token: String +public struct LoginResponse: Decodable { + public let email: String + public let token: String } struct DeleteResponse: Decodable { From 3830cf02e1a0a6c6159b327c03add17c78469f9d Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Mon, 18 Nov 2024 10:51:31 -0500 Subject: [PATCH 02/31] Update keychain usage --- BookPlayerWatch/Settings/Login/LoginViewModel.swift | 2 +- BookPlayerWatch/Settings/Profile/ProfileViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BookPlayerWatch/Settings/Login/LoginViewModel.swift b/BookPlayerWatch/Settings/Login/LoginViewModel.swift index 4ef36fdf1..6aa0f7381 100644 --- a/BookPlayerWatch/Settings/Login/LoginViewModel.swift +++ b/BookPlayerWatch/Settings/Login/LoginViewModel.swift @@ -27,7 +27,7 @@ class LoginViewModel: ObservableObject { } let response: LoginResponse = try await provider.request(.login(token: token)) - try self.keychain.setAccessToken(response.token) + try self.keychain.set(response.token, key: .token) _ = try await Purchases.shared.logIn(appleIDCredential.user) UserDefaults.standard.set(response.email, forKey: "userEmail") default: diff --git a/BookPlayerWatch/Settings/Profile/ProfileViewModel.swift b/BookPlayerWatch/Settings/Profile/ProfileViewModel.swift index f60d28009..024fbcef8 100644 --- a/BookPlayerWatch/Settings/Profile/ProfileViewModel.swift +++ b/BookPlayerWatch/Settings/Profile/ProfileViewModel.swift @@ -15,7 +15,7 @@ class ProfileViewModel: ObservableObject { private let keychain = KeychainService() func handleLogOut() async throws { - try keychain.removeAccessToken() + try keychain.remove(.token) _ = try await Purchases.shared.logOut() UserDefaults.standard.removeObject(forKey: "userEmail") /// Delete downloaded files From abd15e242ae5faa0e9a9a4e28eca93dbc7f5274d Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 23 Nov 2024 00:08:51 -0500 Subject: [PATCH 03/31] Setup library --- BookPlayer.xcodeproj/project.pbxproj | 46 ++++-- .../Base.lproj/Localizable.strings | 1 + BookPlayerWatch/BookPlayerApp.swift | 15 +- BookPlayerWatch/CoreServices.swift | 39 +++++ BookPlayerWatch/ExtensionDelegate.swift | 78 +++++++++- BookPlayerWatch/ForcedEnvironment.swift | 26 ++++ BookPlayerWatch/Info.plist | 2 + BookPlayerWatch/LoadingView.swift | 25 +++ .../RemoteItemListCellView.swift | 31 ++++ .../RemoteItemList/RemoteItemListView.swift | 147 ++++++++++++++++++ ...ainerItemListView.swift => RootView.swift} | 50 +++--- .../Settings/Login/LoginView.swift | 43 ++++- .../Settings/Login/LoginViewModel.swift | 37 ----- .../Settings/Profile/ProfileView.swift | 18 +-- .../Settings/Profile/ProfileViewModel.swift | 23 --- BookPlayerWatch/Settings/SettingsView.swift | 12 +- BookPlayerWatch/ar.lproj/Localizable.strings | 1 + BookPlayerWatch/cs.lproj/Localizable.strings | 1 + BookPlayerWatch/da.lproj/Localizable.strings | 1 + BookPlayerWatch/de.lproj/Localizable.strings | 1 + BookPlayerWatch/el.lproj/Localizable.strings | 1 + BookPlayerWatch/en.lproj/Localizable.strings | 1 + BookPlayerWatch/es.lproj/Localizable.strings | 1 + BookPlayerWatch/fi.lproj/Localizable.strings | 1 + BookPlayerWatch/fr.lproj/Localizable.strings | 1 + BookPlayerWatch/hu.lproj/Localizable.strings | 1 + BookPlayerWatch/it.lproj/Localizable.strings | 1 + BookPlayerWatch/nb.lproj/Localizable.strings | 1 + BookPlayerWatch/nl.lproj/Localizable.strings | 1 + BookPlayerWatch/pl.lproj/Localizable.strings | 1 + .../pt-BR.lproj/Localizable.strings | 1 + .../pt-PT.lproj/Localizable.strings | 1 + BookPlayerWatch/ro.lproj/Localizable.strings | 1 + BookPlayerWatch/ru.lproj/Localizable.strings | 1 + .../sk-SK.lproj/Localizable.strings | 1 + BookPlayerWatch/sv.lproj/Localizable.strings | 1 + BookPlayerWatch/tr.lproj/Localizable.strings | 1 + BookPlayerWatch/uk.lproj/Localizable.strings | 1 + .../zh-Hans.lproj/Localizable.strings | 1 + .../Backed-Models/Account+CoreDataClass.swift | 4 +- Shared/Services/Account/AccountService.swift | 3 + 41 files changed, 490 insertions(+), 133 deletions(-) create mode 100644 BookPlayerWatch/CoreServices.swift create mode 100644 BookPlayerWatch/ForcedEnvironment.swift create mode 100644 BookPlayerWatch/LoadingView.swift create mode 100644 BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift create mode 100644 BookPlayerWatch/RemoteItemList/RemoteItemListView.swift rename BookPlayerWatch/{ItemList/ContainerItemListView.swift => RootView.swift} (51%) delete mode 100644 BookPlayerWatch/Settings/Login/LoginViewModel.swift delete mode 100644 BookPlayerWatch/Settings/Profile/ProfileViewModel.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index 4124fda46..4853f1df9 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -326,6 +326,7 @@ 630826162AF6CABD002ACE0D /* SharedIconWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630826132AF6CA81002ACE0D /* SharedIconWidgetEntry.swift */; }; 6309F1262B0CF1C1002B86A4 /* BookPlaybackToggleIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6309F1252B0CF1C1002B86A4 /* BookPlaybackToggleIntent.swift */; }; 6309F1272B0CF658002B86A4 /* BookPlaybackToggleIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6309F1252B0CF1C1002B86A4 /* BookPlaybackToggleIntent.swift */; }; + 630EC82F2CEF9F0700C19411 /* ForcedEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630EC82E2CEF9F0700C19411 /* ForcedEnvironment.swift */; }; 630F115E2AE7EEBA000A997A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 419B375B23B8D6DB00128A8F /* Localizable.strings */; }; 630F115F2AE7EECA000A997A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4140EA34227288EF0009F794 /* Assets.xcassets */; }; 63125D0E2C36D84E00D35533 /* EventsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63125D0D2C36D84E00D35533 /* EventsAPI.swift */; }; @@ -358,6 +359,7 @@ 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 */; }; + 6350E4642CF004160077CDC1 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6350E4632CF004160077CDC1 /* LoadingView.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 */; }; @@ -387,6 +389,9 @@ 639720752CAAFB010045A4DB /* WidgetLibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639720732CAAFB010045A4DB /* WidgetLibraryItem.swift */; }; 639720832CAB0C380045A4DB /* LastPlayedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639720822CAB0C380045A4DB /* LastPlayedView.swift */; }; 6397208A2CAC5C870045A4DB /* LastPlayedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639720892CAC5C870045A4DB /* LastPlayedModel.swift */; }; + 6399D0702CEBA35D00A2E278 /* RemoteItemListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6399D06F2CEBA35D00A2E278 /* RemoteItemListView.swift */; }; + 6399D0722CEBA37C00A2E278 /* RemoteItemListCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6399D0712CEBA37C00A2E278 /* RemoteItemListCellView.swift */; }; + 6399D0762CECFFA900A2E278 /* CoreServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6399D0752CECFFA900A2E278 /* CoreServices.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 */; }; @@ -427,10 +432,8 @@ 63CD851E2CE2EEBC00EDBEA8 /* UIFont+BookPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F22DE3D288D812900056FCD /* UIFont+BookPlayer.swift */; }; 63CD851F2CE2EEE200EDBEA8 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F22DE2D288CB1A900056FCD /* Fonts.swift */; }; 63CD85232CE302D200EDBEA8 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85222CE302D200EDBEA8 /* LoginView.swift */; }; - 63CD85252CE3046500EDBEA8 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85242CE3046500EDBEA8 /* LoginViewModel.swift */; }; 63CD85272CE3064600EDBEA8 /* BP+ErrorAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85262CE3064600EDBEA8 /* BP+ErrorAlerts.swift */; }; 63CD85432CE3105300EDBEA8 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85422CE3105300EDBEA8 /* ProfileView.swift */; }; - 63CD85452CE3109000EDBEA8 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85442CE3109000EDBEA8 /* ProfileViewModel.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 */; }; @@ -613,7 +616,7 @@ 9FDDD2DF289BEE590020C428 /* SyncJobScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDDD2DD289BEE590020C428 /* SyncJobScheduler.swift */; }; 9FDDD2E1289BFCE20020C428 /* LibraryService+Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDDD2E0289BFCE20020C428 /* LibraryService+Sync.swift */; }; 9FDDD2E2289BFCE20020C428 /* LibraryService+Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDDD2E0289BFCE20020C428 /* LibraryService+Sync.swift */; }; - 9FE07E1B27C522DE007591F7 /* ContainerItemListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE07E1A27C522DE007591F7 /* ContainerItemListView.swift */; }; + 9FE07E1B27C522DE007591F7 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE07E1A27C522DE007591F7 /* RootView.swift */; }; 9FE86B8E2A544CB200B5450E /* Binding+BookPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE86B8D2A544CB200B5450E /* Binding+BookPlayer.swift */; }; 9FEC87AE27FA9E98006C71D5 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEC87AD27FA9E98006C71D5 /* LoginCoordinator.swift */; }; 9FEC87B027FA9F0F006C71D5 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEC87AF27FA9F0F006C71D5 /* LoginViewController.swift */; }; @@ -1107,6 +1110,7 @@ 6308260F2AF6C9B0002ACE0D /* SharedIconWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedIconWidget.swift; sourceTree = ""; }; 630826132AF6CA81002ACE0D /* SharedIconWidgetEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedIconWidgetEntry.swift; sourceTree = ""; }; 6309F1252B0CF1C1002B86A4 /* BookPlaybackToggleIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookPlaybackToggleIntent.swift; sourceTree = ""; }; + 630EC82E2CEF9F0700C19411 /* ForcedEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForcedEnvironment.swift; sourceTree = ""; }; 63125D0D2C36D84E00D35533 /* EventsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsAPI.swift; sourceTree = ""; }; 63125D112C36D97400D35533 /* EventsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsService.swift; sourceTree = ""; }; 631B360B2ABE8ACC001F4C1C /* BPModalOnlyPresentationFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPModalOnlyPresentationFlow.swift; sourceTree = ""; }; @@ -1134,6 +1138,7 @@ 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 = ""; }; + 6350E4632CF004160077CDC1 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.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 = ""; }; @@ -1183,6 +1188,9 @@ 639720732CAAFB010045A4DB /* WidgetLibraryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetLibraryItem.swift; sourceTree = ""; }; 639720822CAB0C380045A4DB /* LastPlayedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastPlayedView.swift; sourceTree = ""; }; 639720892CAC5C870045A4DB /* LastPlayedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastPlayedModel.swift; sourceTree = ""; }; + 6399D06F2CEBA35D00A2E278 /* RemoteItemListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteItemListView.swift; sourceTree = ""; }; + 6399D0712CEBA37C00A2E278 /* RemoteItemListCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteItemListCellView.swift; sourceTree = ""; }; + 6399D0752CECFFA900A2E278 /* CoreServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreServices.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 = ""; }; @@ -1204,10 +1212,8 @@ 63C6C3172B5E0FE700FFE0D8 /* SyncTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncTask.swift; sourceTree = ""; }; 63CD851C2CE2964900EDBEA8 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 63CD85222CE302D200EDBEA8 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; - 63CD85242CE3046500EDBEA8 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; 63CD85262CE3064600EDBEA8 /* BP+ErrorAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BP+ErrorAlerts.swift"; sourceTree = ""; }; 63CD85422CE3105300EDBEA8 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; - 63CD85442CE3109000EDBEA8 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.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 = ""; }; @@ -1368,7 +1374,7 @@ 9FDDD2D7289B64440020C428 /* SyncStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStatus.swift; sourceTree = ""; }; 9FDDD2DD289BEE590020C428 /* SyncJobScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncJobScheduler.swift; sourceTree = ""; }; 9FDDD2E0289BFCE20020C428 /* LibraryService+Sync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LibraryService+Sync.swift"; sourceTree = ""; }; - 9FE07E1A27C522DE007591F7 /* ContainerItemListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerItemListView.swift; sourceTree = ""; }; + 9FE07E1A27C522DE007591F7 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; 9FE786AD292D0FA8002B4A8D /* BookPlayerShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BookPlayerShareExtension.entitlements; sourceTree = ""; }; 9FE86B8D2A544CB200B5450E /* Binding+BookPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+BookPlayer.swift"; sourceTree = ""; }; 9FEC87AD27FA9E98006C71D5 /* LoginCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = ""; }; @@ -1599,12 +1605,17 @@ 9F82DF7027DF8203001B0EA8 /* WatchConnectivityService.swift */, 9FA334AE27C05EBB0064E8EA /* ContextManager.swift */, 63CD85262CE3064600EDBEA8 /* BP+ErrorAlerts.swift */, + 6399D0752CECFFA900A2E278 /* CoreServices.swift */, + 6350E4632CF004160077CDC1 /* LoadingView.swift */, + 9FE07E1A27C522DE007591F7 /* RootView.swift */, + 630EC82E2CEF9F0700C19411 /* ForcedEnvironment.swift */, + 63CD851B2CE2963600EDBEA8 /* Settings */, + 6399D06E2CEBA1F900A2E278 /* RemoteItemList */, 9FA334B427C156DB0064E8EA /* ItemList */, 9FA334B727C163510064E8EA /* NowPlaying */, 9F197EFC27C325150029C989 /* ChapterList */, 9F197EFB27C324FF0029C989 /* PlaybackControls */, 9F197F0327C3E1680029C989 /* Utils */, - 63CD851B2CE2963600EDBEA8 /* Settings */, 4140EA34227288EF0009F794 /* Assets.xcassets */, 4140EA36227288EF0009F794 /* Info.plist */, 419B375B23B8D6DB00128A8F /* Localizable.strings */, @@ -2275,6 +2286,15 @@ path = RecentBooks; sourceTree = ""; }; + 6399D06E2CEBA1F900A2E278 /* RemoteItemList */ = { + isa = PBXGroup; + children = ( + 6399D06F2CEBA35D00A2E278 /* RemoteItemListView.swift */, + 6399D0712CEBA37C00A2E278 /* RemoteItemListCellView.swift */, + ); + path = RemoteItemList; + sourceTree = ""; + }; 63B2303B2B8CCDDB00AEECED /* Realm */ = { isa = PBXGroup; children = ( @@ -2327,7 +2347,6 @@ isa = PBXGroup; children = ( 63CD85222CE302D200EDBEA8 /* LoginView.swift */, - 63CD85242CE3046500EDBEA8 /* LoginViewModel.swift */, ); path = Login; sourceTree = ""; @@ -2336,7 +2355,6 @@ isa = PBXGroup; children = ( 63CD85422CE3105300EDBEA8 /* ProfileView.swift */, - 63CD85442CE3109000EDBEA8 /* ProfileViewModel.swift */, ); path = Profile; sourceTree = ""; @@ -2564,7 +2582,6 @@ 9FA334B427C156DB0064E8EA /* ItemList */ = { isa = PBXGroup; children = ( - 9FE07E1A27C522DE007591F7 /* ContainerItemListView.swift */, 9FA334AA27C058210064E8EA /* ItemListView.swift */, 9FA334B027C1499A0064E8EA /* ItemCellView.swift */, ); @@ -3362,10 +3379,12 @@ buildActionMask = 2147483647; files = ( 9F82DF6927DE93A2001B0EA8 /* SkipIntervalView.swift in Sources */, + 6399D0722CEBA37C00A2E278 /* RemoteItemListCellView.swift in Sources */, 9FA334B627C15DE30064E8EA /* VolumeView.swift in Sources */, 9FA334B927C1B8450064E8EA /* NowPlayingTitleView.swift in Sources */, 9FA334B327C156CB0064E8EA /* NowPlayingView.swift in Sources */, 9FA334C527C285650064E8EA /* ChapterListView.swift in Sources */, + 6399D0762CECFFA900A2E278 /* CoreServices.swift in Sources */, 4140EA47227288EF0009F794 /* ComplicationController.swift in Sources */, 9FA334B127C1499A0064E8EA /* ItemCellView.swift in Sources */, 4140EA43227288EF0009F794 /* ExtensionDelegate.swift in Sources */, @@ -3373,21 +3392,22 @@ 418CABB325EF28FC00D8C878 /* MappingModel_v3_to_v4.xcmappingmodel in Sources */, 9F82DF7127DF8203001B0EA8 /* WatchConnectivityService.swift in Sources */, 41A8BAFE227E6C88003C9895 /* Notification+BookPlayerWatchApp.swift in Sources */, + 630EC82F2CEF9F0700C19411 /* ForcedEnvironment.swift in Sources */, + 6350E4642CF004160077CDC1 /* LoadingView.swift in Sources */, 41D20DB125D5F5A100AAEE30 /* MappingModel_v1_to_v2.xcmappingmodel in Sources */, 9F82DF6D27DE985A001B0EA8 /* NowPlayingPlaybackControlsView.swift in Sources */, - 63CD85452CE3109000EDBEA8 /* ProfileViewModel.swift in Sources */, 9FF383D22A40F97000BBAC11 /* MappingModel_v8_to_v9.xcmappingmodel in Sources */, 9FA334C327C2833B0064E8EA /* ResizeableImageView.swift in Sources */, 9F82DF6F27DEE83E001B0EA8 /* SkipDirection.swift in Sources */, 9FA334AB27C058210064E8EA /* ItemListView.swift in Sources */, 63CD85432CE3105300EDBEA8 /* ProfileView.swift in Sources */, 9FA334AF27C05EBB0064E8EA /* ContextManager.swift in Sources */, - 9FE07E1B27C522DE007591F7 /* ContainerItemListView.swift in Sources */, + 9FE07E1B27C522DE007591F7 /* RootView.swift in Sources */, 9F82DF6B27DE9792001B0EA8 /* NowPlayingMediaControlsView.swift in Sources */, - 63CD85252CE3046500EDBEA8 /* LoginViewModel.swift in Sources */, 9FA334A927C0577E0064E8EA /* BookPlayerApp.swift in Sources */, 63CD85272CE3064600EDBEA8 /* BP+ErrorAlerts.swift in Sources */, 63CD85232CE302D200EDBEA8 /* LoginView.swift in Sources */, + 6399D0702CEBA35D00A2E278 /* RemoteItemListView.swift in Sources */, 41C3396225E04103003ED2B0 /* MappingModel_v2_to_v3.xcmappingmodel in Sources */, 9FA334C727C28D650064E8EA /* PlaybackControlsView.swift in Sources */, 4140EA8522728A160009F794 /* BookPlayer.xcdatamodeld in Sources */, diff --git a/BookPlayerWatch/Base.lproj/Localizable.strings b/BookPlayerWatch/Base.lproj/Localizable.strings index 7f54dad13..e8bd22af1 100644 --- a/BookPlayerWatch/Base.lproj/Localizable.strings +++ b/BookPlayerWatch/Base.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Chapters"; "settings_controls_title" = "Player Controls"; "settings_boostvolume_title" = "Boost Volume"; +"logout_title" = "Log out"; diff --git a/BookPlayerWatch/BookPlayerApp.swift b/BookPlayerWatch/BookPlayerApp.swift index 8f0ea89e8..29b3507da 100644 --- a/BookPlayerWatch/BookPlayerApp.swift +++ b/BookPlayerWatch/BookPlayerApp.swift @@ -6,7 +6,6 @@ // Copyright © 2022 Tortuga Power. All rights reserved. // -import RevenueCat import SwiftUI @main @@ -14,19 +13,15 @@ struct BookPlayerApp: App { // swiftlint:disable:next weak_delegate @WKApplicationDelegateAdaptor var extensionDelegate: ExtensionDelegate - init() { - let revenueCatApiKey: String = Bundle.main.configurationValue( - for: .revenueCat - ) - Purchases.logLevel = .error - Purchases.configure(withAPIKey: revenueCatApiKey) - } - @SceneBuilder var body: some Scene { WindowGroup { NavigationView { - ContainerItemListView() + LoadingView() } } } } + +extension EnvironmentValues { + @Entry var coreServices: CoreServices? +} diff --git a/BookPlayerWatch/CoreServices.swift b/BookPlayerWatch/CoreServices.swift new file mode 100644 index 000000000..15f28505b --- /dev/null +++ b/BookPlayerWatch/CoreServices.swift @@ -0,0 +1,39 @@ +// +// CoreServices.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 19/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerWatchKit +import Foundation + +class CoreServices: ObservableObject { + let dataManager: DataManager + let accountService: AccountServiceProtocol + let syncService: SyncServiceProtocol + let libraryService: LibraryService + let playbackService: PlaybackServiceProtocol + + @Published var hasSyncEnabled = false + + init( + dataManager: DataManager, + accountService: AccountServiceProtocol, + syncService: SyncServiceProtocol, + libraryService: LibraryService, + playbackService: PlaybackServiceProtocol + ) { + self.dataManager = dataManager + self.accountService = accountService + self.syncService = syncService + self.libraryService = libraryService + self.playbackService = playbackService + self.hasSyncEnabled = accountService.hasSyncEnabled() + } + + func checkAndReloadIfSyncIsEnabled() { + self.hasSyncEnabled = accountService.hasSyncEnabled() + } +} diff --git a/BookPlayerWatch/ExtensionDelegate.swift b/BookPlayerWatch/ExtensionDelegate.swift index 6606253c2..d6355583c 100644 --- a/BookPlayerWatch/ExtensionDelegate.swift +++ b/BookPlayerWatch/ExtensionDelegate.swift @@ -6,11 +6,75 @@ // Copyright © 2019 Tortuga Power. All rights reserved. // +import BookPlayerWatchKit +import RevenueCat import SwiftUI import WatchKit -class ExtensionDelegate: NSObject, WKApplicationDelegate { +class ExtensionDelegate: NSObject, WKApplicationDelegate, ObservableObject { static var contextManager = ContextManager() + let databaseInitializer = DatabaseInitializer() + @Published var coreServices: CoreServices? + + /// Reference to the task that creates the core services + var setupCoreServicesTask: Task<(), Error>? + var errorCoreServicesSetup: Error? + + func applicationDidFinishLaunching() { + setupRevenueCat() + setupCoreServices() + } + + func setupRevenueCat() { + let revenueCatApiKey: String = Bundle.main.configurationValue( + for: .revenueCat + ) + Purchases.logLevel = .error + let rcUserId = UserDefaults.standard.string(forKey: "rcUserId") + Purchases.configure(withAPIKey: revenueCatApiKey, appUserID: rcUserId) + Purchases.shared.delegate = self + } + + func setupCoreServices() { + setupCoreServicesTask = Task { + do { + let stack = try await databaseInitializer.loadCoreDataStack() + let coreServices = createCoreServicesIfNeeded(from: stack) + self.coreServices = coreServices + /// setup blank account if needed + guard !coreServices.accountService.hasAccount() else { return } + coreServices.accountService.createAccount(donationMade: false) + } catch { + errorCoreServicesSetup = error + } + } + } + + func createCoreServicesIfNeeded(from stack: CoreDataStack) -> CoreServices { + if let coreServices = self.coreServices { + return coreServices + } else { + let dataManager = DataManager(coreDataStack: stack) + let accountService = AccountService(dataManager: dataManager) + let libraryService = LibraryService(dataManager: dataManager) + let syncService = SyncService( + isActive: accountService.hasSyncEnabled(), + libraryService: libraryService + ) + let playbackService = PlaybackService(libraryService: libraryService) + let coreServices = CoreServices( + dataManager: dataManager, + accountService: accountService, + syncService: syncService, + libraryService: libraryService, + playbackService: playbackService + ) + + self.coreServices = coreServices + + return coreServices + } + } /// For some reason this never gets called func handleRemoteNowPlayingActivity() {} @@ -25,7 +89,11 @@ class ExtensionDelegate: NSObject, WKApplicationDelegate { backgroundTask.setTaskCompletedWithSnapshot(false) case let snapshotTask as WKSnapshotRefreshBackgroundTask: // Snapshot tasks have a unique completion call, make sure to set your expiration date - snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil) + snapshotTask.setTaskCompleted( + restoredDefaultState: true, + estimatedSnapshotExpiration: Date.distantFuture, + userInfo: nil + ) case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask: // Be sure to complete the connectivity task once you’re done. connectivityTask.setTaskCompletedWithSnapshot(false) @@ -45,3 +113,9 @@ class ExtensionDelegate: NSObject, WKApplicationDelegate { } } } + +extension ExtensionDelegate: PurchasesDelegate { + func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) { + coreServices?.hasSyncEnabled = customerInfo.entitlements.all["pro"]?.isActive == true + } +} diff --git a/BookPlayerWatch/ForcedEnvironment.swift b/BookPlayerWatch/ForcedEnvironment.swift new file mode 100644 index 000000000..5f3e1b96a --- /dev/null +++ b/BookPlayerWatch/ForcedEnvironment.swift @@ -0,0 +1,26 @@ +// +// ForcedEnvironment.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 21/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import SwiftUI + +@propertyWrapper +struct ForcedEnvironment: DynamicProperty { + @Environment private var env: Value? + + init(_ keyPath: KeyPath) { + _env = Environment(keyPath) + } + + var wrappedValue: Value { + if let env = env { + return env + } else { + fatalError("\(Value.self) not provided") + } + } +} diff --git a/BookPlayerWatch/Info.plist b/BookPlayerWatch/Info.plist index ed78b7d04..6c7bf7f4d 100644 --- a/BookPlayerWatch/Info.plist +++ b/BookPlayerWatch/Info.plist @@ -14,6 +14,8 @@ $(BP_REVENUECAT_KEY) BP_BUNDLE_IDENTIFIER $(BP_BUNDLE_IDENTIFIER) + BP_MOCKED_BEARER_TOKEN + $(BP_MOCKED_BEARER_TOKEN) CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName diff --git a/BookPlayerWatch/LoadingView.swift b/BookPlayerWatch/LoadingView.swift new file mode 100644 index 000000000..8f3cf4f2e --- /dev/null +++ b/BookPlayerWatch/LoadingView.swift @@ -0,0 +1,25 @@ +// +// LoadingView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 21/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import SwiftUI + +struct LoadingView: View { + @EnvironmentObject private var appDelegate: ExtensionDelegate + + var body: some View { + if let coreServices = appDelegate.coreServices { + RootView(coreServices: coreServices) + } else { + ProgressView() + } + } +} + +#Preview { + LoadingView() +} diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift new file mode 100644 index 000000000..2699c8db1 --- /dev/null +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift @@ -0,0 +1,31 @@ +// +// RemoteItemListCellView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 18/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerWatchKit +import SwiftUI + +struct RemoteItemListCellView: View { + let item: SimpleLibraryItem + + var body: some View { + HStack { + VStack(alignment: .leading) { + Text(item.title) + .lineLimit(2) + Text(item.details) + .font(.footnote) + .foregroundColor(Color.secondary) + .lineLimit(1) + } + Spacer() + if item.type == .folder { + Image(systemName: "chevron.forward") + } + } + } +} diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift new file mode 100644 index 000000000..ec5a052d1 --- /dev/null +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -0,0 +1,147 @@ +// +// RemoteItemListView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 18/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerWatchKit +import SwiftUI + +struct RemoteItemListView: View { + @ObservedObject var coreServices: CoreServices + @State var items: [SimpleLibraryItem] + @State var lastPlayedItem: SimpleLibraryItem? + @State var playingItemParentPath: String? + @State var error: Error? + + let folderRelativePath: String? + + init( + coreServices: CoreServices, + folderRelativePath: String? = nil + ) { + self.coreServices = coreServices + let fetchedItems = + coreServices.libraryService.fetchContents( + at: folderRelativePath, + limit: nil, + offset: nil + ) ?? [] + self._items = .init(initialValue: fetchedItems) + let lastItem = coreServices.libraryService.getLastPlayedItems(limit: 1)?.first + self._lastPlayedItem = .init(initialValue: lastItem) + self.folderRelativePath = folderRelativePath + + if let lastItem { + self._playingItemParentPath = .init( + initialValue: getPathForParentOfItem(currentPlayingPath: lastItem.relativePath) + ) + } else { + self._playingItemParentPath = .init(initialValue: nil) + } + } + + func getForegroundColor(for item: SimpleLibraryItem) -> Color { + guard let lastPlayedItem else { return .primary } + + if item.relativePath == lastPlayedItem.relativePath { + return .accentColor + } + + return item.relativePath == playingItemParentPath ? .accentColor : .primary + } + + func getPathForParentOfItem(currentPlayingPath: String) -> String? { + let parentFolders: [String] = currentPlayingPath.allRanges(of: "/") + .map { String(currentPlayingPath.prefix(upTo: $0.lowerBound)) } + .reversed() + + guard let folderRelativePath = self.folderRelativePath else { + return parentFolders.last + } + + guard let index = parentFolders.firstIndex(of: folderRelativePath) else { + return nil + } + + let elementIndex = index - 1 + + guard elementIndex >= 0 else { + return nil + } + + return parentFolders[elementIndex] + } + + var body: some View { + List { + if folderRelativePath == nil { + Section { + if let lastPlayedItem { + RemoteItemListCellView(item: lastPlayedItem) + } + } header: { + Text(verbatim: "watchapp_last_played_title".localized) + .foregroundStyle(Color.accentColor) + } + } + + Section { + ForEach(items) { item in + if item.type == .folder { + NavigationLink { + RemoteItemListView( + coreServices: coreServices, + folderRelativePath: item.relativePath + ) + } label: { + RemoteItemListCellView(item: item) + .foregroundColor(getForegroundColor(for: item)) + } + } else { + RemoteItemListCellView(item: item) + .foregroundColor(getForegroundColor(for: item)) + } + } + } header: { + Text(verbatim: folderRelativePath?.components(separatedBy: "/").last ?? "library_title".localized) + .foregroundStyle(Color.accentColor) + } + } + .errorAlert(error: $error) + .onAppear { + Task { + guard + await coreServices.syncService.canSyncListContents( + at: folderRelativePath, + ignoreLastTimestamp: false + ) + else { return } + + do { + try await coreServices.syncService.syncListContents(at: folderRelativePath) + } catch BPSyncError.differentLastBook(let relativePath), BPSyncError.reloadLastBook(let relativePath) { + await coreServices.syncService.setLibraryLastBook(with: relativePath) + } catch { + self.error = error + } + + items = + coreServices.libraryService.fetchContents( + at: folderRelativePath, + limit: nil, + offset: nil + ) ?? [] + + lastPlayedItem = coreServices.libraryService.getLastPlayedItems(limit: 1)?.first + if let lastPlayedItem { + playingItemParentPath = getPathForParentOfItem(currentPlayingPath: lastPlayedItem.relativePath) + } else { + playingItemParentPath = nil + } + } + } + } +} diff --git a/BookPlayerWatch/ItemList/ContainerItemListView.swift b/BookPlayerWatch/RootView.swift similarity index 51% rename from BookPlayerWatch/ItemList/ContainerItemListView.swift rename to BookPlayerWatch/RootView.swift index e8053d797..8eb5de989 100644 --- a/BookPlayerWatch/ItemList/ContainerItemListView.swift +++ b/BookPlayerWatch/RootView.swift @@ -1,5 +1,5 @@ // -// ContainerItemListView.swift +// RootView.swift // BookPlayerWatch Extension // // Created by gianni.carlo on 22/2/22. @@ -9,34 +9,40 @@ import BookPlayerWatchKit import SwiftUI -struct ContainerItemListView: View { +struct RootView: View { + @ObservedObject var coreServices: CoreServices @ObservedObject var contextManager = ExtensionDelegate.contextManager - @State var showPlayer = false @State var showSettings = false var body: some View { VStack { - if contextManager.items.isEmpty, - contextManager.isConnecting - { + if coreServices.hasSyncEnabled { + RemoteItemListView(coreServices: coreServices) + } else if contextManager.items.isEmpty && contextManager.isConnecting { ProgressView() } else { itemList - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button { - print("Settings") - showSettings = true - } label: { - Image(systemName: "gear") - } - } - } - .fullScreenCover(isPresented: $showSettings) { - SettingsView() - } } } + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + showSettings = true + } label: { + Image(systemName: "gear") + } + } + } + .fullScreenCover(isPresented: $showSettings) { + let account = coreServices.accountService.getAccount() + SettingsView( + account: + account?.hasId == true + ? account + : nil + ) + .environment(\.coreServices, coreServices) + } } var itemList: some View { @@ -54,9 +60,3 @@ struct ContainerItemListView: View { } } } - -struct ContainerItemListView_Previews: PreviewProvider { - static var previews: some View { - ContainerItemListView() - } -} diff --git a/BookPlayerWatch/Settings/Login/LoginView.swift b/BookPlayerWatch/Settings/Login/LoginView.swift index 510e653ee..035a9aa30 100644 --- a/BookPlayerWatch/Settings/Login/LoginView.swift +++ b/BookPlayerWatch/Settings/Login/LoginView.swift @@ -11,10 +11,10 @@ import BookPlayerWatchKit import SwiftUI struct LoginView: View { - @StateObject var model = LoginViewModel() + @ForcedEnvironment(\.coreServices) var coreServices + @Binding var account: Account? @State private var isLoading = false @State private var error: Error? - @Environment(\.dismiss) var dismiss var body: some View { VStack(spacing: Spacing.S1) { @@ -23,6 +23,24 @@ struct LoginView: View { Text("Stream your recent books to your Apple Watch, or download them to listen offline on the go.") .font(Font(Fonts.body)) .multilineTextAlignment(.center) + #if targetEnvironment(simulator) + Button("Test Login") { + Task { + let token: String = Bundle.main.configurationValue(for: .mockedBearerToken) + print(token) + do { + isLoading = true + try await coreServices.accountService.loginTestAccount(token: token) + isLoading = false + account = coreServices.accountService.getAccount() + coreServices.checkAndReloadIfSyncIsEnabled() + } catch { + isLoading = false + self.error = error + } + } + } + #endif SignInWithAppleButton(.signIn) { request in request.requestedScopes = [.email] } onCompletion: { result in @@ -31,8 +49,23 @@ struct LoginView: View { Task { do { isLoading = true - try await model.handleSignIn(authorization) + + guard + let creds = authorization.credential as? ASAuthorizationAppleIDCredential, + let tokenData = creds.identityToken, + let token = String(data: tokenData, encoding: .utf8) + else { + throw AccountError.missingToken + } + + let account = try await coreServices.accountService.login( + with: token, + userId: creds.user + ) + isLoading = false + self.account = account + coreServices.checkAndReloadIfSyncIsEnabled() } catch { isLoading = false self.error = error @@ -61,7 +94,3 @@ struct LoginView: View { } } } - -#Preview { - LoginView() -} diff --git a/BookPlayerWatch/Settings/Login/LoginViewModel.swift b/BookPlayerWatch/Settings/Login/LoginViewModel.swift deleted file mode 100644 index 6aa0f7381..000000000 --- a/BookPlayerWatch/Settings/Login/LoginViewModel.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// LoginViewModel.swift -// BookPlayerWatch -// -// Created by Gianni Carlo on 11/11/24. -// Copyright © 2024 Tortuga Power. All rights reserved. -// - -import AuthenticationServices -import BookPlayerWatchKit -import Foundation -import RevenueCat - -@MainActor -class LoginViewModel: ObservableObject { - private let provider: NetworkProvider = NetworkProvider(client: NetworkClient()) - private let keychain = KeychainService() - - func handleSignIn(_ authorization: ASAuthorization) async throws { - switch authorization.credential { - case let appleIDCredential as ASAuthorizationAppleIDCredential: - guard - let tokenData = appleIDCredential.identityToken, - let token = String(data: tokenData, encoding: .utf8) - else { - throw AccountError.missingToken - } - - let response: LoginResponse = try await provider.request(.login(token: token)) - try self.keychain.set(response.token, key: .token) - _ = try await Purchases.shared.logIn(appleIDCredential.user) - UserDefaults.standard.set(response.email, forKey: "userEmail") - default: - break - } - } -} diff --git a/BookPlayerWatch/Settings/Profile/ProfileView.swift b/BookPlayerWatch/Settings/Profile/ProfileView.swift index e5a0b4d6d..bd7448125 100644 --- a/BookPlayerWatch/Settings/Profile/ProfileView.swift +++ b/BookPlayerWatch/Settings/Profile/ProfileView.swift @@ -6,14 +6,14 @@ // Copyright © 2024 Tortuga Power. All rights reserved. // +import BookPlayerWatchKit import SwiftUI struct ProfileView: View { - @StateObject var model = ProfileViewModel() + @ForcedEnvironment(\.coreServices) var coreServices + @Binding var account: Account? @State private var isLoading = false @State private var error: Error? - @AppStorage("userEmail") - var email: String? var body: some View { VStack { @@ -21,16 +21,18 @@ struct ProfileView: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 45, height: 45) - if let email { + if let email = coreServices.accountService.getAccount()?.email { Text(verbatim: email) } Spacer() - Button("Log Out") { + Button("logout_title".localized) { Task { do { isLoading = true - try await model.handleLogOut() + try coreServices.accountService.logout() isLoading = false + account = nil + coreServices.hasSyncEnabled = false } catch { isLoading = false self.error = error @@ -57,7 +59,3 @@ struct ProfileView: View { } } } - -#Preview { - ProfileView() -} diff --git a/BookPlayerWatch/Settings/Profile/ProfileViewModel.swift b/BookPlayerWatch/Settings/Profile/ProfileViewModel.swift deleted file mode 100644 index 024fbcef8..000000000 --- a/BookPlayerWatch/Settings/Profile/ProfileViewModel.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ProfileViewModel.swift -// BookPlayerWatch -// -// Created by Gianni Carlo on 11/11/24. -// Copyright © 2024 Tortuga Power. All rights reserved. -// - -import BookPlayerWatchKit -import Foundation -import RevenueCat - -@MainActor -class ProfileViewModel: ObservableObject { - private let keychain = KeychainService() - - func handleLogOut() async throws { - try keychain.remove(.token) - _ = try await Purchases.shared.logOut() - UserDefaults.standard.removeObject(forKey: "userEmail") - /// Delete downloaded files - } -} diff --git a/BookPlayerWatch/Settings/SettingsView.swift b/BookPlayerWatch/Settings/SettingsView.swift index 3f67fbe3c..7164fede9 100644 --- a/BookPlayerWatch/Settings/SettingsView.swift +++ b/BookPlayerWatch/Settings/SettingsView.swift @@ -11,17 +11,19 @@ import BookPlayerWatchKit import SwiftUI struct SettingsView: View { - @AppStorage("userEmail") - var email: String? + @ForcedEnvironment(\.coreServices) var coreServices + @State var account: Account? var body: some View { GeometryReader { geometry in ScrollView { Group { - if email != nil { - ProfileView() + if account?.id != nil, + account?.id.isEmpty == false + { + ProfileView(account: $account) } else { - LoginView() + LoginView(account: $account) .fixedSize(horizontal: false, vertical: true) } } diff --git a/BookPlayerWatch/ar.lproj/Localizable.strings b/BookPlayerWatch/ar.lproj/Localizable.strings index 856ea7316..82fb281ed 100644 --- a/BookPlayerWatch/ar.lproj/Localizable.strings +++ b/BookPlayerWatch/ar.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "الفصول"; "settings_controls_title" = "ضبط المشغل"; "settings_boostvolume_title" = "زيادة مستوى الصوت"; +"logout_title" = "تسجيل خروج"; diff --git a/BookPlayerWatch/cs.lproj/Localizable.strings b/BookPlayerWatch/cs.lproj/Localizable.strings index 65cda52f7..661ae8468 100644 --- a/BookPlayerWatch/cs.lproj/Localizable.strings +++ b/BookPlayerWatch/cs.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Kapitoly"; "settings_controls_title" = "Ovládací prvky přehrávače"; "settings_boostvolume_title" = "Zvýšení hlasitosti"; +"logout_title" = "Odhlásit se"; diff --git a/BookPlayerWatch/da.lproj/Localizable.strings b/BookPlayerWatch/da.lproj/Localizable.strings index 4bdd93c98..a6ed0af54 100644 --- a/BookPlayerWatch/da.lproj/Localizable.strings +++ b/BookPlayerWatch/da.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Kapitler"; "settings_controls_title" = "Afspiller kontrol"; "settings_boostvolume_title" = "Boost lydstyrken"; +"logout_title" = "Log ud"; diff --git a/BookPlayerWatch/de.lproj/Localizable.strings b/BookPlayerWatch/de.lproj/Localizable.strings index 83c62c936..48c0f2ebf 100644 --- a/BookPlayerWatch/de.lproj/Localizable.strings +++ b/BookPlayerWatch/de.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Kapitel"; "settings_controls_title" = "Steuerung"; "settings_boostvolume_title" = "Lautstärke erhöhen"; +"logout_title" = "Ausloggen"; diff --git a/BookPlayerWatch/el.lproj/Localizable.strings b/BookPlayerWatch/el.lproj/Localizable.strings index 397bd5b2d..d9f10148b 100644 --- a/BookPlayerWatch/el.lproj/Localizable.strings +++ b/BookPlayerWatch/el.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Κεφάλαια"; "settings_controls_title" = "Χειριστήρια αναπαραγωγού"; "settings_boostvolume_title" = "Ενίσχυση όγκου"; +"logout_title" = "Αποσύνδεση"; diff --git a/BookPlayerWatch/en.lproj/Localizable.strings b/BookPlayerWatch/en.lproj/Localizable.strings index 7f54dad13..e8bd22af1 100644 --- a/BookPlayerWatch/en.lproj/Localizable.strings +++ b/BookPlayerWatch/en.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Chapters"; "settings_controls_title" = "Player Controls"; "settings_boostvolume_title" = "Boost Volume"; +"logout_title" = "Log out"; diff --git a/BookPlayerWatch/es.lproj/Localizable.strings b/BookPlayerWatch/es.lproj/Localizable.strings index 3d16d33d0..8e4ae3a57 100644 --- a/BookPlayerWatch/es.lproj/Localizable.strings +++ b/BookPlayerWatch/es.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Capítulos"; "settings_controls_title" = "Controles Del Reproductor"; "settings_boostvolume_title" = "Aumentar el volumen"; +"logout_title" = "Cerrar sesión"; diff --git a/BookPlayerWatch/fi.lproj/Localizable.strings b/BookPlayerWatch/fi.lproj/Localizable.strings index 2edd0ca49..f2e4f7029 100644 --- a/BookPlayerWatch/fi.lproj/Localizable.strings +++ b/BookPlayerWatch/fi.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Kappaleet"; "settings_controls_title" = "Soitinsäätimet"; "settings_boostvolume_title" = "Lisää äänenvoimakkuutta"; +"logout_title" = "Kirjautua ulos"; diff --git a/BookPlayerWatch/fr.lproj/Localizable.strings b/BookPlayerWatch/fr.lproj/Localizable.strings index 456bec07e..a5294d72f 100644 --- a/BookPlayerWatch/fr.lproj/Localizable.strings +++ b/BookPlayerWatch/fr.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Chapitres"; "settings_controls_title" = "Contrôles de lecture"; "settings_boostvolume_title" = "Augmenter le volume"; +"logout_title" = "Se déconnecter"; diff --git a/BookPlayerWatch/hu.lproj/Localizable.strings b/BookPlayerWatch/hu.lproj/Localizable.strings index 5351c1476..bef049db7 100644 --- a/BookPlayerWatch/hu.lproj/Localizable.strings +++ b/BookPlayerWatch/hu.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Fejezetek"; "settings_controls_title" = "Lejátszóvezérlők"; "settings_boostvolume_title" = "Hangerő növelése"; +"logout_title" = "Kijelentkezés"; diff --git a/BookPlayerWatch/it.lproj/Localizable.strings b/BookPlayerWatch/it.lproj/Localizable.strings index 0012cd8cf..c2af83d0b 100644 --- a/BookPlayerWatch/it.lproj/Localizable.strings +++ b/BookPlayerWatch/it.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Capitoli"; "settings_controls_title" = "Controlli della Riproduzione"; "settings_boostvolume_title" = "Aumenta il volume massimo"; +"logout_title" = "Disconnettersi"; diff --git a/BookPlayerWatch/nb.lproj/Localizable.strings b/BookPlayerWatch/nb.lproj/Localizable.strings index 94c358962..5f2c3ef66 100644 --- a/BookPlayerWatch/nb.lproj/Localizable.strings +++ b/BookPlayerWatch/nb.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Kapitler"; "settings_controls_title" = "Avspillingskontroller"; "settings_boostvolume_title" = "Boost volumet"; +"logout_title" = "Logg ut"; diff --git a/BookPlayerWatch/nl.lproj/Localizable.strings b/BookPlayerWatch/nl.lproj/Localizable.strings index 3c0a1f535..c7168548a 100644 --- a/BookPlayerWatch/nl.lproj/Localizable.strings +++ b/BookPlayerWatch/nl.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Hoofdstukken"; "settings_controls_title" = "Spelerbediening"; "settings_boostvolume_title" = "Boost volume"; +"logout_title" = "Uitloggen"; diff --git a/BookPlayerWatch/pl.lproj/Localizable.strings b/BookPlayerWatch/pl.lproj/Localizable.strings index dd9f609e3..7969f0a61 100644 --- a/BookPlayerWatch/pl.lproj/Localizable.strings +++ b/BookPlayerWatch/pl.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Rozdziały"; "settings_controls_title" = "Elementy sterujące odtwarzacza"; "settings_boostvolume_title" = "Zwiększ głośność"; +"logout_title" = "Wyloguj"; diff --git a/BookPlayerWatch/pt-BR.lproj/Localizable.strings b/BookPlayerWatch/pt-BR.lproj/Localizable.strings index 01109a75b..5de7a0416 100644 --- a/BookPlayerWatch/pt-BR.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-BR.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Capítulos"; "settings_controls_title" = "Controles do Reprodutor"; "settings_boostvolume_title" = "Aumentar volume"; +"logout_title" = "Sair"; diff --git a/BookPlayerWatch/pt-PT.lproj/Localizable.strings b/BookPlayerWatch/pt-PT.lproj/Localizable.strings index fa056cfd0..f4b1ce57c 100644 --- a/BookPlayerWatch/pt-PT.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-PT.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Capítulos"; "settings_controls_title" = "Controles do Reprodutor"; "settings_boostvolume_title" = "Aumentar volume"; +"logout_title" = "Sair"; diff --git a/BookPlayerWatch/ro.lproj/Localizable.strings b/BookPlayerWatch/ro.lproj/Localizable.strings index a7a939c75..037be9c9d 100644 --- a/BookPlayerWatch/ro.lproj/Localizable.strings +++ b/BookPlayerWatch/ro.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Capitole"; "settings_controls_title" = "Controlul Playerului"; "settings_boostvolume_title" = "Creșterea volumului"; +"logout_title" = "Deconectați-vă"; diff --git a/BookPlayerWatch/ru.lproj/Localizable.strings b/BookPlayerWatch/ru.lproj/Localizable.strings index 787405441..33fcc5a77 100644 --- a/BookPlayerWatch/ru.lproj/Localizable.strings +++ b/BookPlayerWatch/ru.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Главы"; "settings_controls_title" = "Управление плеером"; "settings_boostvolume_title" = "Увеличить громкость"; +"logout_title" = "Выйти"; diff --git a/BookPlayerWatch/sk-SK.lproj/Localizable.strings b/BookPlayerWatch/sk-SK.lproj/Localizable.strings index d04cd8f86..7ed11a7ef 100644 --- a/BookPlayerWatch/sk-SK.lproj/Localizable.strings +++ b/BookPlayerWatch/sk-SK.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Kapitoly"; "settings_controls_title" = "Ovládacie prvky Prehrávača"; "settings_boostvolume_title" = "Zvýšenie hlasitosti"; +"logout_title" = "Odhlásiť sa"; diff --git a/BookPlayerWatch/sv.lproj/Localizable.strings b/BookPlayerWatch/sv.lproj/Localizable.strings index bc91a581e..caf15a8ca 100644 --- a/BookPlayerWatch/sv.lproj/Localizable.strings +++ b/BookPlayerWatch/sv.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Kapitel"; "settings_controls_title" = "Spelarkontroller"; "settings_boostvolume_title" = "Öka volymen"; +"logout_title" = "Logga ut"; diff --git a/BookPlayerWatch/tr.lproj/Localizable.strings b/BookPlayerWatch/tr.lproj/Localizable.strings index eb24c806a..1c8be45cf 100644 --- a/BookPlayerWatch/tr.lproj/Localizable.strings +++ b/BookPlayerWatch/tr.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Bölümler"; "settings_controls_title" = "Oynatıcı Kontrolleri"; "settings_boostvolume_title" = "Sesi Güçlendir"; +"logout_title" = "Çıkış Yap"; diff --git a/BookPlayerWatch/uk.lproj/Localizable.strings b/BookPlayerWatch/uk.lproj/Localizable.strings index bb3ceb58b..8c2d53cf5 100644 --- a/BookPlayerWatch/uk.lproj/Localizable.strings +++ b/BookPlayerWatch/uk.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "Розділи"; "settings_controls_title" = "Керування програвачем"; "settings_boostvolume_title" = "Збільшення гучності"; +"logout_title" = "Вийти"; diff --git a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings index 46afb8b15..1bbc3c782 100644 --- a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings +++ b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings @@ -16,3 +16,4 @@ "chapters_title" = "章节"; "settings_controls_title" = "播放器控制"; "settings_boostvolume_title" = "提高音量"; +"logout_title" = "登出"; diff --git a/Shared/CoreData/Backed-Models/Account+CoreDataClass.swift b/Shared/CoreData/Backed-Models/Account+CoreDataClass.swift index 1476e705f..8b28159b6 100644 --- a/Shared/CoreData/Backed-Models/Account+CoreDataClass.swift +++ b/Shared/CoreData/Backed-Models/Account+CoreDataClass.swift @@ -10,4 +10,6 @@ import Foundation import CoreData @objc(Account) -public class Account: NSManagedObject {} +public class Account: NSManagedObject { + public var hasId: Bool { id.isEmpty == false } +} diff --git a/Shared/Services/Account/AccountService.swift b/Shared/Services/Account/AccountService.swift index c902ed744..b971ab630 100644 --- a/Shared/Services/Account/AccountService.swift +++ b/Shared/Services/Account/AccountService.swift @@ -273,6 +273,7 @@ public final class AccountService: AccountServiceProtocol { try self.keychain.set(token, key: .token) _ = try await Purchases.shared.logIn(userId) + UserDefaults.standard.set(userId, forKey: "rcUserId") } public func login( @@ -284,6 +285,7 @@ public final class AccountService: AccountServiceProtocol { try self.keychain.set(response.token, key: .token) let (customerInfo, _) = try await Purchases.shared.logIn(userId) + UserDefaults.standard.set(userId, forKey: "rcUserId") if let existingAccount = self.getAccount() { // Preserve donation made flag from stored account @@ -327,6 +329,7 @@ public final class AccountService: AccountServiceProtocol { ) Purchases.shared.logOut { _, _ in } + UserDefaults.standard.removeObject(forKey: "rcUserId") NotificationCenter.default.post(name: .logout, object: self) } From 0ee8ab73b3db86fff875d5ef668f140e5f112722 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Mon, 25 Nov 2024 06:32:14 -0500 Subject: [PATCH 04/31] Add support for player manager --- BookPlayer.xcodeproj/project.pbxproj | 20 + BookPlayer/Player/PlayerLoaderService.swift | 6 +- BookPlayer/Player/PlayerManagerProtocol.swift | 6 +- BookPlayer/Player/SleepTimer.swift | 19 +- BookPlayer/Player/SpeedService.swift | 6 +- BookPlayer/Player/WidgetReloadService.swift | 21 +- BookPlayer/Services/UserActivityManager.swift | 6 +- BookPlayerWatch/CoreServices.swift | 8 +- BookPlayerWatch/ExtensionDelegate.swift | 149 +- BookPlayerWatch/Info.plist | 26 +- BookPlayerWatch/PlayerManager.swift | 1220 +++++++++++++++++ .../RemoteItemList/RemoteItemListView.swift | 19 + 12 files changed, 1477 insertions(+), 29 deletions(-) create mode 100644 BookPlayerWatch/PlayerManager.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index 4853f1df9..11484d6fb 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -360,6 +360,15 @@ 634BA5A72C1777BB0015314D /* PricingBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5A62C1777BA0015314D /* PricingBoxView.swift */; }; 634BA5AD2C180F5E0015314D /* StoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 634BA5AC2C180F5E0015314D /* StoryViewModel.swift */; }; 6350E4642CF004160077CDC1 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6350E4632CF004160077CDC1 /* LoadingView.swift */; }; + 6350E4662CF423030077CDC1 /* PlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6350E4652CF423030077CDC1 /* PlayerManager.swift */; }; + 6350E4672CF423E80077CDC1 /* PlayerManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893972CAFAC7500946CD4 /* PlayerManagerProtocol.swift */; }; + 6350E4682CF4248A0077CDC1 /* SpeedService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4151A6A526E3A40600E49DBE /* SpeedService.swift */; }; + 6350E4692CF425500077CDC1 /* WidgetReloadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6308260C2AF6C312002ACE0D /* WidgetReloadService.swift */; }; + 6350E46A2CF429760077CDC1 /* SleepTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3EC372D206EE0650094B4E8 /* SleepTimer.swift */; }; + 6350E46B2CF42B500077CDC1 /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 419723FF21874D5F00AB1190 /* UserActivityManager.swift */; }; + 6350E46C2CF42FE10077CDC1 /* AVPlayer+BookPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 416A297C2568671F00605395 /* AVPlayer+BookPlayer.swift */; }; + 6350E46D2CF4315B0077CDC1 /* PlayerLoaderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893942CAFAB8F00946CD4 /* PlayerLoaderService.swift */; }; + 6350E46E2CF4316E0077CDC1 /* BPPlayerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893912CAFA89000946CD4 /* BPPlayerError.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 */; }; @@ -1139,6 +1148,7 @@ 634BA5A62C1777BA0015314D /* PricingBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PricingBoxView.swift; sourceTree = ""; }; 634BA5AC2C180F5E0015314D /* StoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryViewModel.swift; sourceTree = ""; }; 6350E4632CF004160077CDC1 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; + 6350E4652CF423030077CDC1 /* PlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerManager.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 = ""; }; @@ -1609,6 +1619,7 @@ 6350E4632CF004160077CDC1 /* LoadingView.swift */, 9FE07E1A27C522DE007591F7 /* RootView.swift */, 630EC82E2CEF9F0700C19411 /* ForcedEnvironment.swift */, + 6350E4652CF423030077CDC1 /* PlayerManager.swift */, 63CD851B2CE2963600EDBEA8 /* Settings */, 6399D06E2CEBA1F900A2E278 /* RemoteItemList */, 9FA334B427C156DB0064E8EA /* ItemList */, @@ -3381,22 +3392,28 @@ 9F82DF6927DE93A2001B0EA8 /* SkipIntervalView.swift in Sources */, 6399D0722CEBA37C00A2E278 /* RemoteItemListCellView.swift in Sources */, 9FA334B627C15DE30064E8EA /* VolumeView.swift in Sources */, + 6350E46D2CF4315B0077CDC1 /* PlayerLoaderService.swift in Sources */, + 6350E46B2CF42B500077CDC1 /* UserActivityManager.swift in Sources */, 9FA334B927C1B8450064E8EA /* NowPlayingTitleView.swift in Sources */, + 6350E4692CF425500077CDC1 /* WidgetReloadService.swift in Sources */, 9FA334B327C156CB0064E8EA /* NowPlayingView.swift in Sources */, 9FA334C527C285650064E8EA /* ChapterListView.swift in Sources */, 6399D0762CECFFA900A2E278 /* CoreServices.swift in Sources */, 4140EA47227288EF0009F794 /* ComplicationController.swift in Sources */, + 6350E4672CF423E80077CDC1 /* PlayerManagerProtocol.swift in Sources */, 9FA334B127C1499A0064E8EA /* ItemCellView.swift in Sources */, 4140EA43227288EF0009F794 /* ExtensionDelegate.swift in Sources */, 41A359C4276232E00020D5F5 /* MappingModel_v7_to_v8.xcmappingmodel in Sources */, 418CABB325EF28FC00D8C878 /* MappingModel_v3_to_v4.xcmappingmodel in Sources */, 9F82DF7127DF8203001B0EA8 /* WatchConnectivityService.swift in Sources */, 41A8BAFE227E6C88003C9895 /* Notification+BookPlayerWatchApp.swift in Sources */, + 6350E46A2CF429760077CDC1 /* SleepTimer.swift in Sources */, 630EC82F2CEF9F0700C19411 /* ForcedEnvironment.swift in Sources */, 6350E4642CF004160077CDC1 /* LoadingView.swift in Sources */, 41D20DB125D5F5A100AAEE30 /* MappingModel_v1_to_v2.xcmappingmodel in Sources */, 9F82DF6D27DE985A001B0EA8 /* NowPlayingPlaybackControlsView.swift in Sources */, 9FF383D22A40F97000BBAC11 /* MappingModel_v8_to_v9.xcmappingmodel in Sources */, + 6350E46E2CF4316E0077CDC1 /* BPPlayerError.swift in Sources */, 9FA334C327C2833B0064E8EA /* ResizeableImageView.swift in Sources */, 9F82DF6F27DEE83E001B0EA8 /* SkipDirection.swift in Sources */, 9FA334AB27C058210064E8EA /* ItemListView.swift in Sources */, @@ -3405,7 +3422,10 @@ 9FE07E1B27C522DE007591F7 /* RootView.swift in Sources */, 9F82DF6B27DE9792001B0EA8 /* NowPlayingMediaControlsView.swift in Sources */, 9FA334A927C0577E0064E8EA /* BookPlayerApp.swift in Sources */, + 6350E4662CF423030077CDC1 /* PlayerManager.swift in Sources */, 63CD85272CE3064600EDBEA8 /* BP+ErrorAlerts.swift in Sources */, + 6350E46C2CF42FE10077CDC1 /* AVPlayer+BookPlayer.swift in Sources */, + 6350E4682CF4248A0077CDC1 /* SpeedService.swift in Sources */, 63CD85232CE302D200EDBEA8 /* LoginView.swift in Sources */, 6399D0702CEBA35D00A2E278 /* RemoteItemListView.swift in Sources */, 41C3396225E04103003ED2B0 /* MappingModel_v2_to_v3.xcmappingmodel in Sources */, diff --git a/BookPlayer/Player/PlayerLoaderService.swift b/BookPlayer/Player/PlayerLoaderService.swift index 31ba3a0cc..45bf5d91d 100644 --- a/BookPlayer/Player/PlayerLoaderService.swift +++ b/BookPlayer/Player/PlayerLoaderService.swift @@ -6,7 +6,11 @@ // Copyright © 2024 Tortuga Power. All rights reserved. // -import BookPlayerKit +#if os(watchOS) + import BookPlayerWatchKit +#else + import BookPlayerKit +#endif import Foundation final class PlayerLoaderService: @unchecked Sendable { diff --git a/BookPlayer/Player/PlayerManagerProtocol.swift b/BookPlayer/Player/PlayerManagerProtocol.swift index f9e1c84d2..8bdeb4d41 100644 --- a/BookPlayer/Player/PlayerManagerProtocol.swift +++ b/BookPlayer/Player/PlayerManagerProtocol.swift @@ -6,7 +6,11 @@ // Copyright © 2024 Tortuga Power. All rights reserved. // -import BookPlayerKit +#if os(watchOS) + import BookPlayerWatchKit +#else + import BookPlayerKit +#endif import Combine import Foundation diff --git a/BookPlayer/Player/SleepTimer.swift b/BookPlayer/Player/SleepTimer.swift index 42d949339..259df7b1d 100644 --- a/BookPlayer/Player/SleepTimer.swift +++ b/BookPlayer/Player/SleepTimer.swift @@ -6,12 +6,17 @@ // Copyright © 2018 Florian Pichler. // -import BookPlayerKit import Combine import Foundation -import IntentsUI import UIKit +#if os(watchOS) + import BookPlayerWatchKit +#else + import BookPlayerKit + import IntentsUI +#endif + /// Available sleep timer states enum SleepTimerState: Equatable { case off @@ -53,7 +58,7 @@ final class SleepTimer { 600.0, 900.0, 1800.0, - 3600.0 + 3600.0, ] /// Publisher when the countdown timer reaches the defined threshold @@ -88,8 +93,10 @@ final class SleepTimer { let intent = SleepTimerIntent() intent.option = option - let interaction = INInteraction(intent: intent, response: nil) - interaction.donate(completion: nil) + #if os(iOS) + let interaction = INInteraction(intent: intent, response: nil) + interaction.donate(completion: nil) + #endif } /// Periodic function used for the `countdown` case of ``SleepTimerState`` @@ -116,7 +123,7 @@ final class SleepTimer { } // MARK: Public methods - + public func setTimer(_ newState: SleepTimerState) { /// Always cancel any ongoing timer reset() diff --git a/BookPlayer/Player/SpeedService.swift b/BookPlayer/Player/SpeedService.swift index 495ebc670..6834eb9c7 100755 --- a/BookPlayer/Player/SpeedService.swift +++ b/BookPlayer/Player/SpeedService.swift @@ -6,7 +6,11 @@ // Copyright © 2021 Tortuga Power. All rights reserved. // -import BookPlayerKit +#if os(watchOS) + import BookPlayerWatchKit +#else + import BookPlayerKit +#endif import Combine import Foundation diff --git a/BookPlayer/Player/WidgetReloadService.swift b/BookPlayer/Player/WidgetReloadService.swift index 58f32aa85..2cdaba500 100644 --- a/BookPlayer/Player/WidgetReloadService.swift +++ b/BookPlayer/Player/WidgetReloadService.swift @@ -7,9 +7,14 @@ // import Foundation -import BookPlayerKit import WidgetKit +#if os(watchOS) + import BookPlayerWatchKit +#else + import BookPlayerKit +#endif + protocol WidgetReloadServiceProtocol { /// Reload all the registered widgets func reloadAllWidgets() @@ -31,7 +36,9 @@ class WidgetReloadService: WidgetReloadServiceProtocol { $0.cancel() }) referenceWorkItems = [:] - WidgetCenter.shared.reloadAllTimelines() + if #available(watchOS 9.0, *) { + WidgetCenter.shared.reloadAllTimelines() + } } func reloadWidget(_ type: Constants.Widgets) { @@ -39,7 +46,9 @@ class WidgetReloadService: WidgetReloadServiceProtocol { referenceWorkItem?.cancel() - WidgetCenter.shared.reloadTimelines(ofKind: type.rawValue) + if #available(watchOS 9.0, *) { + WidgetCenter.shared.reloadTimelines(ofKind: type.rawValue) + } } func scheduleWidgetReload(of type: Constants.Widgets) { @@ -48,9 +57,11 @@ class WidgetReloadService: WidgetReloadServiceProtocol { referenceWorkItem?.cancel() let workItem = DispatchWorkItem { - WidgetCenter.shared.reloadTimelines(ofKind: type.rawValue) + if #available(watchOS 9.0, *) { + WidgetCenter.shared.reloadTimelines(ofKind: type.rawValue) + } } - + referenceWorkItems[type] = workItem DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: workItem) diff --git a/BookPlayer/Services/UserActivityManager.swift b/BookPlayer/Services/UserActivityManager.swift index 2acb62410..734720363 100644 --- a/BookPlayer/Services/UserActivityManager.swift +++ b/BookPlayer/Services/UserActivityManager.swift @@ -6,7 +6,11 @@ // Copyright © 2018 Tortuga Power. All rights reserved. // -import BookPlayerKit +#if os(watchOS) + import BookPlayerWatchKit +#else + import BookPlayerKit +#endif import Foundation import Intents diff --git a/BookPlayerWatch/CoreServices.swift b/BookPlayerWatch/CoreServices.swift index 15f28505b..8aa5b2836 100644 --- a/BookPlayerWatch/CoreServices.swift +++ b/BookPlayerWatch/CoreServices.swift @@ -15,6 +15,8 @@ class CoreServices: ObservableObject { let syncService: SyncServiceProtocol let libraryService: LibraryService let playbackService: PlaybackServiceProtocol + let playerManager: PlayerManagerProtocol + let playerLoaderService: PlayerLoaderService @Published var hasSyncEnabled = false @@ -23,7 +25,9 @@ class CoreServices: ObservableObject { accountService: AccountServiceProtocol, syncService: SyncServiceProtocol, libraryService: LibraryService, - playbackService: PlaybackServiceProtocol + playbackService: PlaybackServiceProtocol, + playerManager: PlayerManagerProtocol, + playerLoaderService: PlayerLoaderService ) { self.dataManager = dataManager self.accountService = accountService @@ -31,6 +35,8 @@ class CoreServices: ObservableObject { self.libraryService = libraryService self.playbackService = playbackService self.hasSyncEnabled = accountService.hasSyncEnabled() + self.playerManager = playerManager + self.playerLoaderService = playerLoaderService } func checkAndReloadIfSyncIsEnabled() { diff --git a/BookPlayerWatch/ExtensionDelegate.swift b/BookPlayerWatch/ExtensionDelegate.swift index d6355583c..1c3f4dded 100644 --- a/BookPlayerWatch/ExtensionDelegate.swift +++ b/BookPlayerWatch/ExtensionDelegate.swift @@ -10,6 +10,7 @@ import BookPlayerWatchKit import RevenueCat import SwiftUI import WatchKit +import MediaPlayer class ExtensionDelegate: NSObject, WKApplicationDelegate, ObservableObject { static var contextManager = ContextManager() @@ -23,6 +24,7 @@ class ExtensionDelegate: NSObject, WKApplicationDelegate, ObservableObject { func applicationDidFinishLaunching() { setupRevenueCat() setupCoreServices() + setupMPRemoteCommands() } func setupRevenueCat() { @@ -62,12 +64,27 @@ class ExtensionDelegate: NSObject, WKApplicationDelegate, ObservableObject { libraryService: libraryService ) let playbackService = PlaybackService(libraryService: libraryService) + let playerManager = PlayerManager( + libraryService: libraryService, + playbackService: playbackService, + syncService: syncService, + speedService: SpeedService(libraryService: libraryService), + widgetReloadService: WidgetReloadService() + ) + let playerLoaderService = PlayerLoaderService( + syncService: syncService, + libraryService: libraryService, + playbackService: playbackService, + playerManager: playerManager + ) let coreServices = CoreServices( dataManager: dataManager, accountService: accountService, syncService: syncService, libraryService: libraryService, - playbackService: playbackService + playbackService: playbackService, + playerManager: playerManager, + playerLoaderService: playerLoaderService ) self.coreServices = coreServices @@ -112,6 +129,136 @@ class ExtensionDelegate: NSObject, WKApplicationDelegate, ObservableObject { } } } + + func setupMPRemoteCommands() { + Task { + self.setupMPPlaybackRemoteCommands() + self.setupMPSkipRemoteCommands() + } + } + + func setupMPPlaybackRemoteCommands() { + let center = MPRemoteCommandCenter.shared() + // Play / Pause + center.togglePlayPauseCommand.isEnabled = true + center.togglePlayPauseCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + guard let playerManager = self?.coreServices?.playerManager else { + return .commandFailed + } + + playerManager.playPause() + + return .success + } + + center.playCommand.isEnabled = true + center.playCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + guard let playerManager = self?.coreServices?.playerManager else { + return .commandFailed + } + + playerManager.playPause() + + return .success + } + + center.pauseCommand.isEnabled = true + center.pauseCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + guard let playerManager = self?.coreServices?.playerManager else { + return .commandFailed + } + + playerManager.pause() + + return .success + } + + center.changePlaybackPositionCommand.isEnabled = true + center.changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in + guard + let playerManager = self?.coreServices?.playerManager, + let currentItem = playerManager.currentItem, + let event = remoteEvent as? MPChangePlaybackPositionCommandEvent + else { return .commandFailed } + + var newTime = event.positionTime + + if UserDefaults.sharedDefaults.bool(forKey: Constants.UserDefaults.chapterContextEnabled), + let currentChapter = currentItem.currentChapter + { + newTime += currentChapter.start + } + + playerManager.jumpTo(newTime, recordBookmark: true) + + return .success + } + } + + // For now, seek forward/backward and next/previous track perform the same function + func setupMPSkipRemoteCommands() { + let center = MPRemoteCommandCenter.shared() + // Forward + center.skipForwardCommand.preferredIntervals = [NSNumber(value: PlayerManager.forwardInterval)] + center.skipForwardCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + guard let playerManager = self?.coreServices?.playerManager else { return .commandFailed } + + playerManager.forward() + return .success + } + + center.nextTrackCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + guard let playerManager = self?.coreServices?.playerManager else { return .commandFailed } + + playerManager.forward() + return .success + } + + center.seekForwardCommand.addTarget { [weak self] (commandEvent) -> MPRemoteCommandHandlerStatus in + guard let cmd = commandEvent as? MPSeekCommandEvent, + cmd.type == .endSeeking + else { + return .success + } + + guard let playerManager = self?.coreServices?.playerManager else { return .success } + + // End seeking + playerManager.forward() + return .success + } + + // Rewind + center.skipBackwardCommand.preferredIntervals = [NSNumber(value: PlayerManager.rewindInterval)] + center.skipBackwardCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + guard let playerManager = self?.coreServices?.playerManager else { return .commandFailed } + + playerManager.rewind() + return .success + } + + center.previousTrackCommand.addTarget { [weak self] (_) -> MPRemoteCommandHandlerStatus in + guard let playerManager = self?.coreServices?.playerManager else { return .commandFailed } + + playerManager.rewind() + return .success + } + + center.seekBackwardCommand.addTarget { [weak self] (commandEvent) -> MPRemoteCommandHandlerStatus in + guard + let cmd = commandEvent as? MPSeekCommandEvent, + cmd.type == .endSeeking + else { + return .success + } + + guard let playerManager = self?.coreServices?.playerManager else { return .success } + + // End seeking + playerManager.rewind() + return .success + } + } } extension ExtensionDelegate: PurchasesDelegate { diff --git a/BookPlayerWatch/Info.plist b/BookPlayerWatch/Info.plist index 6c7bf7f4d..cf359b667 100644 --- a/BookPlayerWatch/Info.plist +++ b/BookPlayerWatch/Info.plist @@ -2,20 +2,18 @@ - BP_API_DOMAIN - $(BP_API_DOMAIN) - BP_API_PORT - $(BP_API_PORT) - BP_API_SCHEME - $(BP_API_SCHEME) - BP_BUNDLE_IDENTIFIER - $(BP_BUNDLE_IDENTIFIER) - BP_REVENUECAT_KEY - $(BP_REVENUECAT_KEY) + BP_API_DOMAIN + $(BP_API_DOMAIN) + BP_API_PORT + $(BP_API_PORT) + BP_API_SCHEME + $(BP_API_SCHEME) BP_BUNDLE_IDENTIFIER $(BP_BUNDLE_IDENTIFIER) - BP_MOCKED_BEARER_TOKEN - $(BP_MOCKED_BEARER_TOKEN) + BP_MOCKED_BEARER_TOKEN + $(BP_MOCKED_BEARER_TOKEN) + BP_REVENUECAT_KEY + $(BP_REVENUECAT_KEY) CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -36,6 +34,10 @@ $(CURRENT_PROJECT_VERSION) CLKComplicationPrincipalClass $(PRODUCT_MODULE_NAME).ComplicationController + UIBackgroundModes + + audio + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/BookPlayerWatch/PlayerManager.swift b/BookPlayerWatch/PlayerManager.swift new file mode 100644 index 000000000..21579b921 --- /dev/null +++ b/BookPlayerWatch/PlayerManager.swift @@ -0,0 +1,1220 @@ +// +// PlayerManager.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 24/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import AVFoundation +import BookPlayerWatchKit +import Combine +import Foundation +import MediaPlayer + +// swiftlint:disable:next file_length + +final class PlayerManager: NSObject, PlayerManagerProtocol { + private let libraryService: LibraryServiceProtocol + private let playbackService: PlaybackServiceProtocol + private let syncService: SyncServiceProtocol + private let speedService: SpeedServiceProtocol + private let userActivityManager: UserActivityManager + private let widgetReloadService: WidgetReloadServiceProtocol + + private var audioPlayer = AVPlayer() + + private var fadeTimer: Timer? + + private var timeControlPassthroughPublisher = CurrentValueSubject(.paused) + private var timeControlSubscription: AnyCancellable? + private var playableChapterSubscription: AnyCancellable? + private var isPlayingSubscription: AnyCancellable? + private var periodicTimeObserver: Any? + private var disposeBag = Set() + /// Flag determining if it should resume playback after finishing up loading an item + @Published private var playbackQueued: Bool? + /// Flag determining if it's in the process of fetching the URL for playback + @Published private var isFetchingRemoteURL: Bool? + /// Prevent loop from automatic URL refreshes + private var canFetchRemoteURL = true + private var hasObserverRegistered = false + private var observeStatus: Bool = false { + didSet { + guard oldValue != self.observeStatus else { return } + + if self.observeStatus { + self.playerItem?.addObserver(self, forKeyPath: "status", options: .new, context: nil) + self.hasObserverRegistered = true + } else if self.hasObserverRegistered { + self.playerItem?.removeObserver(self, forKeyPath: "status") + self.hasObserverRegistered = false + } + } + } + + weak var syncProgressDelegate: PlaybackSyncProgressDelegate? + /// Reference to the ongoing play task + private var playTask: Task<(), Error>? + private var playerItem: AVPlayerItem? + private var loadChapterTask: Task<(), Never>? + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() + @Published var currentItem: PlayableItem? + @Published var currentSpeed: Float = 1.0 + + var nowPlayingInfo = [String: Any]() + + private let queue = OperationQueue() + + init( + libraryService: LibraryServiceProtocol, + playbackService: PlaybackServiceProtocol, + syncService: SyncServiceProtocol, + speedService: SpeedServiceProtocol, + widgetReloadService: WidgetReloadServiceProtocol + ) { + self.libraryService = libraryService + self.playbackService = playbackService + self.syncService = syncService + self.speedService = speedService + self.userActivityManager = UserActivityManager(libraryService: libraryService) + self.widgetReloadService = widgetReloadService + super.init() + + setupPlayerInstance() + bindObservers() + } + + func bindObservers() { + NotificationCenter.default.addObserver( + self, + selector: #selector(playerDidFinishPlaying(_:)), + name: .AVPlayerItemDidPlayToEndTime, + object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(handleMediaServicesWereReset), + name: AVAudioSession.mediaServicesWereResetNotification, + object: nil + ) + + SleepTimer.shared.countDownThresholdPublisher.sink { [weak self] _ in + self?.handleSleepTimerThresholdEvent() + }.store(in: &disposeBag) + + SleepTimer.shared.timerEndedPublisher.sink { [weak self] state in + self?.handleSleepTimerEndEvent(state) + }.store(in: &disposeBag) + + isPlayingPublisher() + .removeDuplicates() + .sink { [weak self] isPlayingValue in + if isPlayingValue { + UserDefaults.sharedDefaults.set( + self?.currentItem?.relativePath, + forKey: Constants.UserDefaults.sharedWidgetNowPlayingPath + ) + } else { + UserDefaults.sharedDefaults.removeObject(forKey: Constants.UserDefaults.sharedWidgetNowPlayingPath) + } + }.store(in: &disposeBag) + } + + func bindInterruptObserver() { + NotificationCenter.default.removeObserver( + self, + name: AVAudioSession.interruptionNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(self.handleAudioInterruptions(_:)), + name: AVAudioSession.interruptionNotification, + object: nil + ) + } + + func setupPlayerInstance() { + if let observer = periodicTimeObserver { + audioPlayer.removeTimeObserver(observer) + } + + audioPlayer = AVPlayer() + + let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC)) + periodicTimeObserver = audioPlayer.addPeriodicTimeObserver( + forInterval: interval, + queue: DispatchQueue.main + ) { [weak self] _ in + guard let self = self else { return } + + self.updateTime() + } + + bindTimeControlPassthroughPublisher() + } + + func currentItemPublisher() -> AnyPublisher { + return self.$currentItem.eraseToAnyPublisher() + } + + func hasLoadedBook() -> Bool { + if playerItem == nil { + return currentItem != nil + } else { + return true + } + } + + func loadRemoteURLAsset(for chapter: PlayableChapter, forceRefresh: Bool) async throws -> AVURLAsset { + let fileURL: URL + + if !forceRefresh, + let chapterURL = chapter.remoteURL + { + fileURL = chapterURL + } else { + isFetchingRemoteURL = true + fileURL = + try await syncService + .getRemoteFileURLs(of: chapter.relativePath, type: .book)[0].url + isFetchingRemoteURL = false + } + + let asset = AVURLAsset(url: fileURL, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true]) + + // TODO: Check if there's a way to reduce the time this operation takes + // it's currently a bottleneck when streaming playback + await asset.loadValues(forKeys: [ + "duration", + "playable", + "preferredRate", + "preferredVolume", + "hasProtectedContent", + "providesPreciseDurationAndTiming", + "commonMetadata", + "metadata", + ]) + + guard !Task.isCancelled else { + throw BookPlayerError.cancelledTask + } + + /// Load artwork if it's not cached + if !ArtworkService.isCached(relativePath: chapter.relativePath), + let data = AVMetadataItem.metadataItems( + from: asset.commonMetadata, + filteredByIdentifier: .commonIdentifierArtwork + ).first?.dataValue + { + await ArtworkService.storeInCache(data, for: chapter.relativePath) + } + + if currentItem?.isBoundBook == false { + await libraryService.loadChaptersIfNeeded(relativePath: chapter.relativePath, asset: asset) + + if let libraryItem = libraryService.getSimpleItem(with: chapter.relativePath) { + currentItem = try playbackService.getPlayableItem(from: libraryItem) + } + } + + return asset + } + + func loadPlayerItem(for chapter: PlayableChapter, forceRefreshURL: Bool) async throws { + let fileURL = DataManager.getProcessedFolderURL().appendingPathComponent(chapter.relativePath) + + let asset: AVURLAsset + + if syncService.isActive, + !FileManager.default.fileExists(atPath: fileURL.path) + { + asset = try await loadRemoteURLAsset(for: chapter, forceRefresh: forceRefreshURL) + } else { + asset = AVURLAsset(url: fileURL, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true]) + } + + // Clean just in case + if self.hasObserverRegistered { + self.playerItem?.removeObserver(self, forKeyPath: "status") + self.hasObserverRegistered = false + } + + self.playerItem = AVPlayerItem(asset: asset) + self.playerItem?.audioTimePitchAlgorithm = .timeDomain + } + + func load(_ item: PlayableItem, autoplay: Bool) { + load(item, autoplay: autoplay, forceRefreshURL: false) + } + + private func load(_ item: PlayableItem, autoplay: Bool, forceRefreshURL: Bool) { + /// Cancel in case there's an ongoing load task + playTask?.cancel() + loadChapterTask?.cancel() + + // Recover in case of failure + if audioPlayer.status == .failed { + setupPlayerInstance() + } + + // Preload item + if self.currentItem != nil { + stopPlayback() + playerItem = nil + /// Clear out flag when `playerItem` is nulled out + hasObserverRegistered = false + currentItem = nil + } + + self.currentItem = item + + self.playableChapterSubscription?.cancel() + self.playableChapterSubscription = item.$currentChapter.sink { [weak self] chapter in + guard let chapter = chapter else { return } + + self?.setNowPlayingBookTitle(chapter: chapter) + NotificationCenter.default.post(name: .chapterChange, object: nil, userInfo: nil) + self?.widgetReloadService.scheduleWidgetReload(of: .sharedNowPlayingWidget) + } + + loadChapterMetadata(item.currentChapter, autoplay: autoplay, forceRefreshURL: forceRefreshURL) + storeWidgetItem(item) + } + + func storeWidgetItem(_ item: PlayableItem) { + var widgetItems: [WidgetLibraryItem] = [ + WidgetLibraryItem( + relativePath: item.relativePath, + title: item.title, + details: item.author + ) + ] + + if let itemsData = UserDefaults.sharedDefaults.data(forKey: Constants.UserDefaults.sharedWidgetLastPlayedItems), + let items = try? decoder.decode([WidgetLibraryItem].self, from: itemsData) + { + widgetItems.append(contentsOf: items.filter({ $0.relativePath != item.relativePath })) + widgetItems = Array(widgetItems.prefix(10)) + } + + guard let data = try? encoder.encode(widgetItems) else { + return + } + + UserDefaults.sharedDefaults.set(data, forKey: Constants.UserDefaults.sharedWidgetLastPlayedItems) + widgetReloadService.reloadWidget(.lastPlayedWidget) + } + + func loadChapterMetadata(_ chapter: PlayableChapter, autoplay: Bool? = nil, forceRefreshURL: Bool = false) { + if let autoplay { + playbackQueued = autoplay + } + + loadChapterTask = Task { [unowned self] in + do { + try await self.loadPlayerItem(for: chapter, forceRefreshURL: forceRefreshURL) + self.loadChapterOperation(chapter) + } catch BookPlayerError.cancelledTask { + /// Do nothing, as it was cancelled to load another item + } catch { + self.playbackQueued = nil + self.isFetchingRemoteURL = nil + self.observeStatus = false + self.showErrorAlert(title: "\("error_title".localized) Metadata", error.localizedDescription) + return + } + } + } + + func loadChapterOperation(_ chapter: PlayableChapter) { + self.queue.addOperation { + // try loading the player + guard + let playerItem = self.playerItem, + chapter.duration > 0 + else { + DispatchQueue.main.async { [weak self] in + self?.playbackQueued = nil + self?.isFetchingRemoteURL = nil + NotificationCenter.default.post(name: .bookReady, object: nil, userInfo: ["loaded": false]) + } + return + } + + self.audioPlayer.replaceCurrentItem(with: nil) + self.observeStatus = true + self.isFetchingRemoteURL = nil + self.audioPlayer.replaceCurrentItem(with: playerItem) + + // Update UI on main thread + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + + self.currentSpeed = self.speedService.getSpeed(relativePath: chapter.relativePath) + // Set book metadata for lockscreen and control center + self.nowPlayingInfo = [ + MPNowPlayingInfoPropertyDefaultPlaybackRate: self.currentSpeed + ] + + self.setNowPlayingBookTitle(chapter: chapter) + self.setNowPlayingBookTime() + self.setNowPlayingArtwork(chapter: chapter) + + MPNowPlayingInfoCenter.default().nowPlayingInfo = self.nowPlayingInfo + MPNowPlayingInfoCenter.default().playbackState = .playing + + if let currentItem = self.currentItem { + // if book is truly finished, start book again to avoid autoplaying next one + // add 1 second as a finished threshold + if currentItem.currentTime > 0.0 { + let time = (currentItem.currentTime + 1) >= currentItem.duration ? 0 : currentItem.currentTime + self.initializeChapterTime(time) + } + } + + NotificationCenter.default.post(name: .bookReady, object: nil, userInfo: ["loaded": true]) + self.widgetReloadService.reloadAllWidgets() + } + } + } + + func setNowPlayingArtwork(chapter: PlayableChapter) { + var pathForArtwork = chapter.relativePath + + if !ArtworkService.isCached(relativePath: chapter.relativePath), + let currentItem = currentItem + { + pathForArtwork = currentItem.relativePath + } + + ArtworkService.retrieveImageFromCache(for: pathForArtwork) { [weak self] result in + guard + let self, + case .success(let value) = result + else { return } + + let image: UIImage = value.image + + self.nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork( + boundsSize: image.size, + requestHandler: { (_) -> UIImage in + image + } + ) + + MPNowPlayingInfoCenter.default().nowPlayingInfo = self.nowPlayingInfo + } + } + + // Called every second by the timer + func updateTime() { + guard + let currentItem, + let playerItem, + playerItem.status == .readyToPlay + else { + return + } + + var currentTime = CMTimeGetSeconds(self.audioPlayer.currentTime()) + + // When using devices with AirPlay 1, + // `currentTime` can be negative when switching chapters + if currentTime < 0 { + currentTime = 0.05 + } + + if currentItem.isBoundBook { + currentTime += (currentItem.currentChapter.start - currentItem.currentChapter.chapterOffset) + } + + if currentTime >= currentItem.currentChapter.end || currentTime < currentItem.currentChapter.start, + let newChapter = currentItem.getChapter(at: currentTime), + newChapter != currentItem.currentChapter, + !currentItem.isBoundBook || newChapter.chapterOffset != 0 + { + /// Avoid setting the same chapter, as it would publish an update event + currentItem.currentChapter = newChapter + } + + updatePlaybackTime(item: currentItem, time: currentTime) + + self.userActivityManager.recordTime() + + self.setNowPlayingBookTime() + + MPNowPlayingInfoCenter.default().nowPlayingInfo = self.nowPlayingInfo + + // stop timer if the book is finished + if Int(currentTime) == Int(currentItem.duration) { + // Once book a book is finished, ask for a review + UserDefaults.standard.set(true, forKey: "ask_review") + } + + NotificationCenter.default.post(name: .bookPlaying, object: nil, userInfo: nil) + } + + // MARK: - Player states + + var isPlaying: Bool { + let controlStatusFlag = audioPlayer.timeControlStatus != .paused + let playbackQueuedFlag = playbackQueued == true + + return controlStatusFlag + || playbackQueuedFlag + || (isFetchingRemoteURL == true && playbackQueuedFlag) + } + + /// We need an intermediate publisher for the `timeControlStatus`, as the `AVPlayer` instance can be recreated, + /// thus invalidating the registered observers for `isPlaying` + func bindTimeControlPassthroughPublisher() { + timeControlSubscription?.cancel() + timeControlSubscription = audioPlayer.publisher(for: \.timeControlStatus) + .sink { [weak self] timeControlStatus in + self?.timeControlPassthroughPublisher.send(timeControlStatus) + } + } + + func bindPauseObserver() { + self.isPlayingSubscription?.cancel() + self.isPlayingSubscription = + timeControlPassthroughPublisher + .delay(for: .seconds(0.1), scheduler: RunLoop.main, options: .none) + .sink { timeControlStatus in + if timeControlStatus == .paused { + try? AVAudioSession.sharedInstance().setActive(false) + self.isPlayingSubscription?.cancel() + } + } + } + + func isPlayingPublisher() -> AnyPublisher { + return Publishers.CombineLatest3( + timeControlPassthroughPublisher, + $playbackQueued, + $isFetchingRemoteURL + ) + .map({ (timeControlStatus, playbackQueued, isFetchingRemoteURL) -> Bool in + let controlStatusFlag = timeControlStatus != .paused + let playbackQueuedFlag = playbackQueued == true + return controlStatusFlag + || playbackQueuedFlag + || (isFetchingRemoteURL == true && playbackQueuedFlag) + }) + .eraseToAnyPublisher() + } + + var boostVolume: Bool = false { + didSet { + self.audioPlayer.volume = + self.boostVolume + ? Constants.Volume.boosted + : Constants.Volume.normal + } + } + + static var rewindInterval: TimeInterval { + get { + if UserDefaults.standard.object(forKey: Constants.UserDefaults.rewindInterval) == nil { + return 30.0 + } + + return UserDefaults.standard.double(forKey: Constants.UserDefaults.rewindInterval) + } + + set { + UserDefaults.standard.set(newValue, forKey: Constants.UserDefaults.rewindInterval) + + MPRemoteCommandCenter.shared().skipBackwardCommand.preferredIntervals = [newValue] as [NSNumber] + } + } + + static var forwardInterval: TimeInterval { + get { + if UserDefaults.standard.object(forKey: Constants.UserDefaults.forwardInterval) == nil { + return 30.0 + } + + return UserDefaults.standard.double(forKey: Constants.UserDefaults.forwardInterval) + } + + set { + UserDefaults.standard.set(newValue, forKey: Constants.UserDefaults.forwardInterval) + + MPRemoteCommandCenter.shared().skipForwardCommand.preferredIntervals = [newValue] as [NSNumber] + } + } + + func setNowPlayingBookTitle(chapter: PlayableChapter) { + guard let currentItem = self.currentItem else { return } + + self.nowPlayingInfo[MPMediaItemPropertyTitle] = chapter.title + + /// If the chapter title is the same as the current item, show the author instead + if chapter.title == currentItem.title { + self.nowPlayingInfo[MPMediaItemPropertyArtist] = currentItem.author + } else { + self.nowPlayingInfo[MPMediaItemPropertyArtist] = currentItem.title + } + self.nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = currentItem.author + } + + func setNowPlayingBookTime() { + guard let currentItem = self.currentItem else { return } + + let prefersChapterContext = UserDefaults.sharedDefaults.bool( + forKey: Constants.UserDefaults.chapterContextEnabled + ) + let prefersRemainingTime = UserDefaults.sharedDefaults.bool( + forKey: Constants.UserDefaults.remainingTimeEnabled + ) + let currentTimeInContext = currentItem.currentTimeInContext(prefersChapterContext) + let maxTimeInContext = currentItem.maxTimeInContext( + prefersChapterContext: prefersChapterContext, + prefersRemainingTime: prefersRemainingTime, + at: self.currentSpeed + ) + + // 1x is needed because of how the control center behaves when decrementing time + self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 + self.nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTimeInContext + + let playbackDuration: TimeInterval + let itemProgress: TimeInterval + + if prefersRemainingTime { + playbackDuration = (abs(maxTimeInContext) + currentTimeInContext) + + let realMaxTime = currentItem.maxTimeInContext( + prefersChapterContext: prefersChapterContext, + prefersRemainingTime: false, + at: self.currentSpeed + ) + + itemProgress = currentTimeInContext / realMaxTime + } else { + playbackDuration = maxTimeInContext + itemProgress = currentTimeInContext / maxTimeInContext + } + + self.nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = playbackDuration + self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackProgress] = itemProgress + } +} + +// MARK: - Seek Controls + +extension PlayerManager { + func jumpToChapter(_ chapter: PlayableChapter) { + jumpTo(chapter.start + 0.1, recordBookmark: false) + } + + func initializeChapterTime(_ time: Double) { + guard let currentItem = self.currentItem else { return } + + let boundedTime = min(max(time, 0), currentItem.duration) + + let newTime = + currentItem.isBoundBook + ? currentItem.getChapterTime(in: currentItem.currentChapter, for: boundedTime) + : boundedTime + self.audioPlayer.seek(to: CMTime(seconds: newTime, preferredTimescale: CMTimeScale(NSEC_PER_SEC))) + } + + func jumpTo(_ time: Double, recordBookmark: Bool = true) { + guard let currentItem = self.currentItem else { return } + + if recordBookmark { + self.createOrUpdateAutomaticBookmark( + at: currentItem.currentTime, + relativePath: currentItem.relativePath, + type: .skip + ) + } + + let boundedTime = min(max(time, 0), currentItem.duration) + + let chapterBeforeSkip = currentItem.currentChapter + updatePlaybackTime(item: currentItem, time: boundedTime) + if let chapterAfterSkip = currentItem.getChapter(at: boundedTime), + chapterBeforeSkip != chapterAfterSkip + { + currentItem.currentChapter = chapterAfterSkip + // If chapters are different, and it's a bound book, + // load the new chapter + if currentItem.isBoundBook, + chapterBeforeSkip?.relativePath != chapterAfterSkip.relativePath + { + loadChapterMetadata(chapterAfterSkip) + return + } + } + + let newTime = + currentItem.isBoundBook + ? currentItem.getChapterTime(in: currentItem.currentChapter, for: boundedTime) + : boundedTime + self.audioPlayer.seek(to: CMTime(seconds: newTime, preferredTimescale: CMTimeScale(NSEC_PER_SEC))) + } + + func forward() { + skip(PlayerManager.forwardInterval) + } + + func rewind() { + skip(-PlayerManager.rewindInterval) + } + + func skip(_ interval: TimeInterval) { + guard let currentItem = self.currentItem else { return } + + let newTime = currentItem.getInterval(from: interval) + currentItem.currentTime + self.jumpTo(newTime) + } +} + +// MARK: - Playback + +extension PlayerManager { + func prepareForPlayback(_ currentItem: PlayableItem) async -> Bool { + /// Allow refetching remote URL if the action was initiating by the user + canFetchRemoteURL = true + + guard let playerItem else { + /// Check if the playbable item is in the process of being set + if observeStatus == false { + if isFetchingRemoteURL == true { + playbackQueued = true + } else { + load(currentItem, autoplay: true) + } + } + return false + } + + guard playerItem.status == .readyToPlay && playerItem.error == nil else { + /// Try to reload the item if it failed to load previously + if playerItem.status == .failed || playerItem.error != nil { + load(currentItem, autoplay: true) + } else { + // queue playback + self.playbackQueued = true + self.observeStatus = true + } + + return false + } + + /// Update nowPlaying state so the UI displays correctly + playbackQueued = true + await syncProgressDelegate?.waitForSyncInProgress() + + return true + } + + func play() { + play(autoPlayed: false) + } + + func play(autoPlayed: Bool) { + playTask?.cancel() + playTask = Task { @MainActor in + /// Ignore play commands if there's no item loaded, + /// and only continue if the item is loaded and ready + guard + let currentItem, + await prepareForPlayback(currentItem), + !Task.isCancelled + else { return } + + userActivityManager.resumePlaybackActivity() + + do { + let audioSession = AVAudioSession.sharedInstance() + try audioSession.setCategory( + AVAudioSession.Category.playback, + mode: .spokenAudio, + policy: .longFormAudio, + options: [] + ) + try await audioSession.activate() + } catch { + showErrorAlert(title: "error_title".localized, error.localizedDescription) + } + + createOrUpdateAutomaticBookmark( + at: currentItem.currentTime, + relativePath: currentItem.relativePath, + type: .play + ) + + // If book is completed, stop + let playerTime = CMTimeGetSeconds(audioPlayer.currentTime()) + if playerTime.isFinite && Int(currentItem.duration) == Int(playerTime) { return } + + handleSmartRewind(currentItem) + + if !autoPlayed { + handleAutoTimer() + } + + fadeTimer?.invalidate() + boostVolume = UserDefaults.standard.bool(forKey: Constants.UserDefaults.boostVolumeEnabled) + bindInterruptObserver() + // Set play state on player and control center + audioPlayer.playImmediately(atRate: currentSpeed) + /// Clean up flag after player starts playing + playbackQueued = nil + + setNowPlayingBookTitle(chapter: currentItem.currentChapter) + + NotificationCenter.default.post(name: .bookPlayed, object: nil, userInfo: ["book": currentItem]) + } + } + + func handleSmartRewind(_ item: PlayableItem) { + let smartRewindEnabled = UserDefaults.standard.bool(forKey: Constants.UserDefaults.smartRewindEnabled) + + if smartRewindEnabled, + let lastPlayTime = item.lastPlayDate + { + let timePassed = Date().timeIntervalSince(lastPlayTime) + let timePassedLimited = min(max(timePassed, 0), Constants.SmartRewind.threshold) + + let delta = timePassedLimited / Constants.SmartRewind.threshold + + // Using a cubic curve to soften the rewind effect for lower values and strengthen it for higher + let rewindTime = pow(delta, 3) * Constants.SmartRewind.maxTime + + let newPlayerTime = max(CMTimeGetSeconds(self.audioPlayer.currentTime()) - rewindTime, 0) + + self.audioPlayer.seek(to: CMTime(seconds: newPlayerTime, preferredTimescale: CMTimeScale(NSEC_PER_SEC))) + } + } + + func handleAutoTimer() { + guard UserDefaults.standard.bool(forKey: Constants.UserDefaults.autoTimerEnabled) else { return } + + SleepTimer.shared.restartTimer() + } + + func setSpeed(_ newValue: Float) { + self.speedService.setSpeed(newValue, relativePath: self.currentItem?.relativePath) + self.currentSpeed = newValue + if self.isPlaying { + self.audioPlayer.rate = newValue + } + } + + func setBoostVolume(_ newValue: Bool) { + self.boostVolume = newValue + } + + // swiftlint:disable block_based_kvo + // Using this instead of new form, because the new one wouldn't work properly on AVPlayerItem + override func observeValue( + forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey: Any]?, + context: UnsafeMutableRawPointer? + ) { + guard + let path = keyPath, + path == "status", + let item = object as? AVPlayerItem + else { + super.observeValue( + forKeyPath: keyPath, + of: object, + change: change, + context: context + ) + return + } + + switch item.status { + case .readyToPlay: + self.observeStatus = false + + if self.playbackQueued == true { + self.play(autoPlayed: true) + } + // Clean up flag + self.playbackQueued = nil + case .failed: + if canFetchRemoteURL, + let nsError = item.error as? NSError, + nsError.code == NSURLErrorResourceUnavailable + || nsError.code == NSURLErrorNoPermissionsToReadFile, + let currentItem + { + loadAndRefreshURL(item: currentItem) + canFetchRemoteURL = false + } else { + /// Avoid showing any alert if playback is not queued, this could be from the initial app launch + /// where we preload the player with the last played item + if playbackQueued == true { + if let nsError = item.error as? NSError { + let errorDescription = """ + \(nsError.localizedDescription) + + Error Domain + \(nsError.domain) + + Additional Info + \(nsError.userInfo) + """ + showErrorAlert(title: "\("error_title".localized) \(nsError.code)", errorDescription) + } else { + showErrorAlert(title: "error_title".localized, item.error?.localizedDescription) + } + } + + playbackQueued = nil + observeStatus = false + playerItem = nil + } + case .unknown: + /// Do not handle .unknown states, as we're only interested in the success and failure states + fallthrough + @unknown default: + break + } + } + // swiftlint:enable block_based_kvo + + func pause() { + pause(removeInterruptObserver: true) + } + + func pause(removeInterruptObserver: Bool) { + guard self.currentItem != nil else { return } + + self.observeStatus = false + + self.userActivityManager.stopPlaybackActivity() + + NotificationCenter.default.post(name: .bookPaused, object: nil) + + bindPauseObserver() + // Set pause state on player and control center + audioPlayer.pause() + playbackQueued = nil + playTask?.cancel() + loadChapterTask?.cancel() + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 + MPNowPlayingInfoCenter.default().playbackState = .paused + setNowPlayingBookTime() + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + if removeInterruptObserver { + NotificationCenter.default.removeObserver( + self, + name: AVAudioSession.interruptionNotification, + object: nil + ) + } + } + + // Toggle play/pause of book + func playPause() { + // Pause player if it's playing + if self.audioPlayer.timeControlStatus == .playing || playbackQueued == true { + self.pause() + } else { + self.play() + } + } + + func stop() { + stopPlayback() + + self.currentItem = nil + playerItem = nil + /// Clear out flag when `playerItem` is nulled out + hasObserverRegistered = false + MPNowPlayingInfoCenter.default().playbackState = .stopped + } + + private func stopPlayback() { + observeStatus = false + playbackQueued = nil + + audioPlayer.pause() + playTask?.cancel() + loadChapterTask?.cancel() + + userActivityManager.stopPlaybackActivity() + NotificationCenter.default.removeObserver( + self, + name: AVAudioSession.interruptionNotification, + object: nil + ) + } + + func markAsCompleted(_ flag: Bool) { + guard let currentItem = self.currentItem else { return } + + self.libraryService.markAsFinished(flag: flag, relativePath: currentItem.relativePath) + + if let parentFolderPath = currentItem.parentFolder { + /// Defer all the folder progress updates until the user opens up the app again + playbackService.markStaleProgress(folderPath: parentFolderPath) + } + + currentItem.isFinished = flag + + NotificationCenter.default.post(name: .bookEnd, object: nil, userInfo: nil) + } + + func currentSpeedPublisher() -> AnyPublisher { + return $currentSpeed.eraseToAnyPublisher() + } + + func playPreviousItem() { + guard + let currentItem = self.currentItem, + let previousBook = self.playbackService.getPlayableItem( + before: currentItem.relativePath, + parentFolder: currentItem.parentFolder + ) + else { return } + + load(previousBook, autoplay: true) + } + + func playNextItem(autoPlayed: Bool = false, shouldAutoplay: Bool = true) { + /// If it's autoplayed, check if setting is enabled + if autoPlayed, + !UserDefaults.standard.bool(forKey: Constants.UserDefaults.autoplayEnabled) + { + return + } + + let restartFinished = UserDefaults.standard.bool(forKey: Constants.UserDefaults.autoplayRestartEnabled) + + guard + let currentItem = self.currentItem, + let nextBook = getNextPlayableBook( + after: currentItem, + autoPlayed: autoPlayed, + restartFinished: restartFinished + ) + else { return } + + /// If autoplaying a finished book and restart is enabled, set currentTime to 0 + if autoPlayed, + nextBook.isFinished, + restartFinished + { + updatePlaybackTime(item: nextBook, time: 0) + } + + load(nextBook, autoplay: shouldAutoplay) + libraryService.setLibraryLastBook(with: nextBook.relativePath) + } + + /// Check `UTType` of the book before returning it + /// Note: if the type does not conform to `.audiovisualContent` it will skip the item + func getNextPlayableBook( + after item: PlayableItem, + autoPlayed: Bool, + restartFinished: Bool + ) -> PlayableItem? { + guard + let nextBook = self.playbackService.getPlayableItem( + after: item.relativePath, + parentFolder: item.parentFolder, + autoplayed: autoPlayed, + restartFinished: restartFinished + ) + else { return nil } + + let fileExtension = nextBook.fileURL.pathExtension + + /// Only check for audiovisual content if a file extension is present + if !fileExtension.isEmpty, + let fileType = UTType(filenameExtension: fileExtension), + !fileType.isSubtype(of: .audiovisualContent) + { + return getNextPlayableBook( + after: nextBook, + autoPlayed: autoPlayed, + restartFinished: restartFinished + ) + } + + return nextBook + } + + /// Check `UTType` of the chapter before returning it + /// Note: if the type does not conform to `.audiovisualContent` it will skip the item + func getNextPlayableChapter( + currentItem: PlayableItem, + after chapter: PlayableChapter + ) -> PlayableChapter? { + guard + let nextChapter = self.playbackService.getNextChapter( + from: currentItem, + after: chapter + ) + else { return nil } + + let fileExtension = nextChapter.fileURL.pathExtension + + /// Only check for audiovisual content if a file extension is present + if !fileExtension.isEmpty, + let fileType = UTType(filenameExtension: fileExtension), + !fileType.isSubtype(of: .audiovisualContent) + { + return getNextPlayableChapter( + currentItem: currentItem, + after: nextChapter + ) + } + + return nextChapter + } + + @objc + func playerDidFinishPlaying(_ notification: Notification) { + guard let currentItem = self.currentItem else { return } + + let endOfChapterActive = SleepTimer.shared.state == .endOfChapter + + if currentItem.chapters.last == currentItem.currentChapter { + if UserDefaults.standard.bool( + forKey: currentItem.filename + Constants.UserDefaults.repeatEnabledSuffix + ) { + updatePlaybackTime(item: currentItem, time: 0) + let firstChapter = currentItem.chapters.first! + currentItem.currentChapter = firstChapter + loadChapterMetadata(firstChapter, autoplay: !endOfChapterActive) + } else { + self.libraryService.setLibraryLastBook(with: nil) + + self.markAsCompleted(true) + + self.playNextItem(autoPlayed: true, shouldAutoplay: !endOfChapterActive) + + NotificationCenter.default.post(name: .bookEnd, object: nil) + } + } else if currentItem.isBoundBook { + updatePlaybackTime(item: currentItem, time: currentItem.currentTime) + /// Load next chapter + guard + let nextChapter = getNextPlayableChapter( + currentItem: currentItem, + after: currentItem.currentChapter + ) + else { return } + currentItem.currentChapter = nextChapter + loadChapterMetadata(nextChapter, autoplay: !endOfChapterActive) + } + } + + /// Update the current item playback time, and checks for difference in progress percentage + func updatePlaybackTime(item: PlayableItem, time: Float64) { + let previousPercentage = Int(item.percentCompleted) + self.playbackService.updatePlaybackTime(item: item, time: time) + let newPercentage = Int(item.percentCompleted) + + if previousPercentage != newPercentage { + if let parentFolder = item.parentFolder { + /// Defer all the folder progress updates until the user opens up the app again + playbackService.markStaleProgress(folderPath: parentFolder) + } + + widgetReloadService.scheduleWidgetReload(of: .sharedNowPlayingWidget) + } + } + + private func loadAndRefreshURL(item: PlayableItem) { + load(item, autoplay: playbackQueued == true, forceRefreshURL: true) + } + + @objc + private func handleMediaServicesWereReset() { + /// Playback should be stopped, and wait for the user to activate it again + if isPlaying { + stopPlayback() + } + + try? AVAudioSession.sharedInstance().setCategory( + AVAudioSession.Category.playback, + mode: .spokenAudio, + options: [] + ) + + setupPlayerInstance() + } + + /// Playback may be interrupted by calls. Handle resuming the audio if needed + @objc + func handleAudioInterruptions(_ notification: Notification) { + guard + let userInfo = notification.userInfo, + let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt, + let type = AVAudioSession.InterruptionType(rawValue: typeValue) + else { + return + } + + switch type { + case .began: + pause(removeInterruptObserver: false) + case .ended: + guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { + return + } + let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) + if options.contains(.shouldResume) { + play(autoPlayed: true) + } + @unknown default: + break + } + } +} + +// MARK: - BookMarks +extension PlayerManager { + public func createOrUpdateAutomaticBookmark(at time: Double, relativePath: String, type: BookmarkType) { + /// Clean up old bookmark + if let bookmark = libraryService.getBookmarks(of: type, relativePath: relativePath)?.first { + libraryService.deleteBookmark(bookmark) + } + + guard + let bookmark = libraryService.createBookmark(at: floor(time), relativePath: relativePath, type: type) + else { return } + + libraryService.addNote(type.getNote() ?? "", bookmark: bookmark) + } +} + +extension PlayerManager { + private func showErrorAlert(title: String, _ message: String?) { + print("=== error: \(message)") +// DispatchQueue.main.async { +// AppDelegate.shared?.activeSceneDelegate? +// .startingNavigationController +// .getTopVisibleViewController()? +// .showAlert(title, message: message) +// } + } +} + +// MARK: - Sleep timer +extension PlayerManager { + private func handleSleepTimerThresholdEvent() { + fadeTimer = audioPlayer.fadeVolume(from: 1, to: 0, duration: 5, completion: {}) + } + + private func handleSleepTimerEndEvent(_ state: SleepTimerState) { + pause() + } +} diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift index ec5a052d1..469986957 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -81,6 +81,16 @@ struct RemoteItemListView: View { Section { if let lastPlayedItem { RemoteItemListCellView(item: lastPlayedItem) + .onTapGesture { + Task { + do { + try await coreServices.playerLoaderService.loadPlayer(lastPlayedItem.relativePath, autoplay: true) + } catch { + print("=== loading player failed: \(error)") + } + } +// coreServices.playerManager.load(<#T##item: PlayableItem##PlayableItem#>, autoplay: <#T##Bool#>) + } } } header: { Text(verbatim: "watchapp_last_played_title".localized) @@ -103,6 +113,15 @@ struct RemoteItemListView: View { } else { RemoteItemListCellView(item: item) .foregroundColor(getForegroundColor(for: item)) + .onTapGesture { + Task { + do { + try await coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) + } catch { + print("=== loading player failed: \(error)") + } + } + } } } } header: { From 99cbbfa86b391714622786dd529233c104f69479 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Thu, 28 Nov 2024 16:01:13 -0500 Subject: [PATCH 05/31] Setup player for remote items --- BookPlayer.xcodeproj/project.pbxproj | 24 +++++ .../Base.lproj/Localizable.strings | 4 + BookPlayerWatch/ContextManager.swift | 93 +++++++++------- BookPlayerWatch/CoreServices.swift | 11 +- BookPlayerWatch/ExtensionDelegate.swift | 2 +- .../NowPlaying/NowPlayingView.swift | 3 +- .../NowPlayingPlaybackControlsView.swift | 2 +- .../Views/NowPlayingTitleView.swift | 15 +-- .../NowPlaying/Views/VolumeView.swift | 5 +- .../PlaybackFullControlsView.swift | 100 ++++++++++++++++++ BookPlayerWatch/PlayerControlsView.swift | 62 +++++++++++ BookPlayerWatch/PlayerManager.swift | 22 ++-- BookPlayerWatch/PlayerMoreListView.swift | 26 +++++ BookPlayerWatch/PlayerToolbarView.swift | 91 ++++++++++++++++ .../RemoteItemListCellView.swift | 37 +++++++ .../RemoteItemList/RemoteItemListView.swift | 71 +++++++++---- BookPlayerWatch/RemotePlayerView.swift | 31 ++++++ BookPlayerWatch/SkipDurationListView.swift | 78 ++++++++++++++ BookPlayerWatch/ar.lproj/Localizable.strings | 4 + BookPlayerWatch/cs.lproj/Localizable.strings | 4 + BookPlayerWatch/da.lproj/Localizable.strings | 4 + BookPlayerWatch/de.lproj/Localizable.strings | 4 + BookPlayerWatch/el.lproj/Localizable.strings | 4 + BookPlayerWatch/en.lproj/Localizable.strings | 4 + BookPlayerWatch/es.lproj/Localizable.strings | 4 + BookPlayerWatch/fi.lproj/Localizable.strings | 4 + BookPlayerWatch/fr.lproj/Localizable.strings | 4 + BookPlayerWatch/hu.lproj/Localizable.strings | 4 + BookPlayerWatch/it.lproj/Localizable.strings | 4 + BookPlayerWatch/nb.lproj/Localizable.strings | 4 + BookPlayerWatch/nl.lproj/Localizable.strings | 4 + BookPlayerWatch/pl.lproj/Localizable.strings | 4 + .../pt-BR.lproj/Localizable.strings | 4 + .../pt-PT.lproj/Localizable.strings | 4 + BookPlayerWatch/ro.lproj/Localizable.strings | 4 + BookPlayerWatch/ru.lproj/Localizable.strings | 4 + .../sk-SK.lproj/Localizable.strings | 4 + BookPlayerWatch/sv.lproj/Localizable.strings | 4 + BookPlayerWatch/tr.lproj/Localizable.strings | 4 + BookPlayerWatch/uk.lproj/Localizable.strings | 4 + .../zh-Hans.lproj/Localizable.strings | 4 + Shared/Network/NetworkClient.swift | 1 + 42 files changed, 683 insertions(+), 87 deletions(-) create mode 100644 BookPlayerWatch/PlaybackFullControlsView.swift create mode 100644 BookPlayerWatch/PlayerControlsView.swift create mode 100644 BookPlayerWatch/PlayerMoreListView.swift create mode 100644 BookPlayerWatch/PlayerToolbarView.swift create mode 100644 BookPlayerWatch/RemotePlayerView.swift create mode 100644 BookPlayerWatch/SkipDurationListView.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index 11484d6fb..8a4fdc5b5 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -340,6 +340,8 @@ 6327E0C62ADB9913004780DC /* DownloadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB20EA429A281140021663B /* DownloadState.swift */; }; 6327E0C72ADB9914004780DC /* DownloadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB20EA429A281140021663B /* DownloadState.swift */; }; 632941402AEEE739000AD2EE /* CircularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6329413F2AEEE739000AD2EE /* CircularView.swift */; }; + 6334CF1B2CF8D87500F1FA17 /* SkipDurationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF1A2CF8D87500F1FA17 /* SkipDurationListView.swift */; }; + 6334CF1D2CF90AF900F1FA17 /* PlayerMoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF1C2CF90AF900F1FA17 /* PlayerMoreListView.swift */; }; 633BE3E52AE6102D00F983AC /* BPShortcutsLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 633BE3E42AE6102D00F983AC /* BPShortcutsLink.swift */; }; 6340D5692AF12D48003D0B09 /* SharedWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6340D5682AF12D48003D0B09 /* SharedWidget.swift */; }; 6340D56E2AF13E4C003D0B09 /* RectangularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6340D56D2AF13E4C003D0B09 /* RectangularView.swift */; }; @@ -369,6 +371,10 @@ 6350E46C2CF42FE10077CDC1 /* AVPlayer+BookPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 416A297C2568671F00605395 /* AVPlayer+BookPlayer.swift */; }; 6350E46D2CF4315B0077CDC1 /* PlayerLoaderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893942CAFAB8F00946CD4 /* PlayerLoaderService.swift */; }; 6350E46E2CF4316E0077CDC1 /* BPPlayerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E893912CAFA89000946CD4 /* BPPlayerError.swift */; }; + 6350E4702CF498160077CDC1 /* RemotePlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6350E46F2CF498160077CDC1 /* RemotePlayerView.swift */; }; + 6350E4722CF4D0220077CDC1 /* PlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6350E4712CF4D0220077CDC1 /* PlayerControlsView.swift */; }; + 6350E4742CF4D2660077CDC1 /* PlayerToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6350E4732CF4D2660077CDC1 /* PlayerToolbarView.swift */; }; + 6350E4762CF4F6E90077CDC1 /* PlaybackFullControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6350E4752CF4F6E90077CDC1 /* PlaybackFullControlsView.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 */; }; @@ -1127,6 +1133,8 @@ 631C75C82AB92C540013E7E5 /* BPPushPresentationFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPPushPresentationFlow.swift; sourceTree = ""; }; 631C75CB2AB92FA60013E7E5 /* BPModalPresentationFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPModalPresentationFlow.swift; sourceTree = ""; }; 6329413F2AEEE739000AD2EE /* CircularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularView.swift; sourceTree = ""; }; + 6334CF1A2CF8D87500F1FA17 /* SkipDurationListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkipDurationListView.swift; sourceTree = ""; }; + 6334CF1C2CF90AF900F1FA17 /* PlayerMoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerMoreListView.swift; sourceTree = ""; }; 633BE3E12AE43D1300F983AC /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/AppShortcuts.strings"; sourceTree = ""; }; 633BE3E42AE6102D00F983AC /* BPShortcutsLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPShortcutsLink.swift; sourceTree = ""; }; 6340D5682AF12D48003D0B09 /* SharedWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedWidget.swift; sourceTree = ""; }; @@ -1149,6 +1157,10 @@ 634BA5AC2C180F5E0015314D /* StoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryViewModel.swift; sourceTree = ""; }; 6350E4632CF004160077CDC1 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 6350E4652CF423030077CDC1 /* PlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerManager.swift; sourceTree = ""; }; + 6350E46F2CF498160077CDC1 /* RemotePlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePlayerView.swift; sourceTree = ""; }; + 6350E4712CF4D0220077CDC1 /* PlayerControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = ""; }; + 6350E4732CF4D2660077CDC1 /* PlayerToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerToolbarView.swift; sourceTree = ""; }; + 6350E4752CF4F6E90077CDC1 /* PlaybackFullControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackFullControlsView.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 = ""; }; @@ -1620,6 +1632,12 @@ 9FE07E1A27C522DE007591F7 /* RootView.swift */, 630EC82E2CEF9F0700C19411 /* ForcedEnvironment.swift */, 6350E4652CF423030077CDC1 /* PlayerManager.swift */, + 6350E46F2CF498160077CDC1 /* RemotePlayerView.swift */, + 6350E4712CF4D0220077CDC1 /* PlayerControlsView.swift */, + 6350E4732CF4D2660077CDC1 /* PlayerToolbarView.swift */, + 6350E4752CF4F6E90077CDC1 /* PlaybackFullControlsView.swift */, + 6334CF1A2CF8D87500F1FA17 /* SkipDurationListView.swift */, + 6334CF1C2CF90AF900F1FA17 /* PlayerMoreListView.swift */, 63CD851B2CE2963600EDBEA8 /* Settings */, 6399D06E2CEBA1F900A2E278 /* RemoteItemList */, 9FA334B427C156DB0064E8EA /* ItemList */, @@ -3406,18 +3424,23 @@ 41A359C4276232E00020D5F5 /* MappingModel_v7_to_v8.xcmappingmodel in Sources */, 418CABB325EF28FC00D8C878 /* MappingModel_v3_to_v4.xcmappingmodel in Sources */, 9F82DF7127DF8203001B0EA8 /* WatchConnectivityService.swift in Sources */, + 6350E4742CF4D2660077CDC1 /* PlayerToolbarView.swift in Sources */, 41A8BAFE227E6C88003C9895 /* Notification+BookPlayerWatchApp.swift in Sources */, + 6350E4762CF4F6E90077CDC1 /* PlaybackFullControlsView.swift in Sources */, 6350E46A2CF429760077CDC1 /* SleepTimer.swift in Sources */, 630EC82F2CEF9F0700C19411 /* ForcedEnvironment.swift in Sources */, 6350E4642CF004160077CDC1 /* LoadingView.swift in Sources */, 41D20DB125D5F5A100AAEE30 /* MappingModel_v1_to_v2.xcmappingmodel in Sources */, 9F82DF6D27DE985A001B0EA8 /* NowPlayingPlaybackControlsView.swift in Sources */, + 6334CF1B2CF8D87500F1FA17 /* SkipDurationListView.swift in Sources */, 9FF383D22A40F97000BBAC11 /* MappingModel_v8_to_v9.xcmappingmodel in Sources */, + 6334CF1D2CF90AF900F1FA17 /* PlayerMoreListView.swift in Sources */, 6350E46E2CF4316E0077CDC1 /* BPPlayerError.swift in Sources */, 9FA334C327C2833B0064E8EA /* ResizeableImageView.swift in Sources */, 9F82DF6F27DEE83E001B0EA8 /* SkipDirection.swift in Sources */, 9FA334AB27C058210064E8EA /* ItemListView.swift in Sources */, 63CD85432CE3105300EDBEA8 /* ProfileView.swift in Sources */, + 6350E4702CF498160077CDC1 /* RemotePlayerView.swift in Sources */, 9FA334AF27C05EBB0064E8EA /* ContextManager.swift in Sources */, 9FE07E1B27C522DE007591F7 /* RootView.swift in Sources */, 9F82DF6B27DE9792001B0EA8 /* NowPlayingMediaControlsView.swift in Sources */, @@ -3429,6 +3452,7 @@ 63CD85232CE302D200EDBEA8 /* LoginView.swift in Sources */, 6399D0702CEBA35D00A2E278 /* RemoteItemListView.swift in Sources */, 41C3396225E04103003ED2B0 /* MappingModel_v2_to_v3.xcmappingmodel in Sources */, + 6350E4722CF4D0220077CDC1 /* PlayerControlsView.swift in Sources */, 9FA334C727C28D650064E8EA /* PlaybackControlsView.swift in Sources */, 4140EA8522728A160009F794 /* BookPlayer.xcdatamodeld in Sources */, 63CD851D2CE2964900EDBEA8 /* SettingsView.swift in Sources */, diff --git a/BookPlayerWatch/Base.lproj/Localizable.strings b/BookPlayerWatch/Base.lproj/Localizable.strings index e8bd22af1..dd92665ac 100644 --- a/BookPlayerWatch/Base.lproj/Localizable.strings +++ b/BookPlayerWatch/Base.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Chapters"; "settings_controls_title" = "Player Controls"; "settings_boostvolume_title" = "Boost Volume"; +"settings_skip_title" = "SKIP INTERVALS"; +"settings_skip_rewind_title" = "Rewind"; +"settings_skip_forward_title" = "Forward"; +"speed_title" = "speed"; "logout_title" = "Log out"; diff --git a/BookPlayerWatch/ContextManager.swift b/BookPlayerWatch/ContextManager.swift index 14510d3df..563d09771 100644 --- a/BookPlayerWatch/ContextManager.swift +++ b/BookPlayerWatch/ContextManager.swift @@ -134,47 +134,6 @@ class ContextManager: ObservableObject, BPLogger { ]) } - func handleNewSpeed(_ rate: Float) { - let roundedValue = round(rate * 100) / 100.0 - - guard roundedValue >= 0.5 && roundedValue <= 4.0 else { return } - - self.watchConnectivityService.sendMessage(message: [ - "command": Command.speed.rawValue as AnyObject, - "rate": "\(rate)" as AnyObject - ]) - - self.applicationContext.rate = roundedValue - } - - func handleNewSpeedJump() { - let rate: Float - - if self.applicationContext.rate == 4.0 { - rate = 0.5 - } else { - rate = min(self.applicationContext.rate + 0.5, 4.0) - } - - let roundedValue = round(rate * 100) / 100.0 - - self.watchConnectivityService.sendMessage(message: [ - "command": Command.speed.rawValue as AnyObject, - "rate": "\(rate)" as AnyObject - ]) - - self.applicationContext.rate = roundedValue - } - - func handleBoostVolumeToggle() { - self.applicationContext.boostVolume = !self.applicationContext.boostVolume - - self.watchConnectivityService.sendMessage(message: [ - "command": Command.boostVolume.rawValue as AnyObject, - "isOn": "\(self.applicationContext.boostVolume)" as AnyObject - ]) - } - func handleSkip(_ direction: SkipDirection) { let payload: [String: AnyObject] @@ -259,3 +218,55 @@ class ContextManager: ObservableObject, BPLogger { ) } } + +/// Playback controls (companion version) +extension ContextManager { + var rate: Float { + applicationContext.rate + } + + var boostVolume: Bool { + applicationContext.boostVolume + } + + func handleBoostVolumeToggle() { + self.applicationContext.boostVolume = !self.applicationContext.boostVolume + + self.watchConnectivityService.sendMessage(message: [ + "command": Command.boostVolume.rawValue as AnyObject, + "isOn": "\(self.applicationContext.boostVolume)" as AnyObject + ]) + } + + func handleNewSpeed(_ rate: Float) { + let roundedValue = round(rate * 100) / 100.0 + + guard roundedValue >= 0.5 && roundedValue <= 4.0 else { return } + + self.watchConnectivityService.sendMessage(message: [ + "command": Command.speed.rawValue as AnyObject, + "rate": "\(rate)" as AnyObject + ]) + + self.applicationContext.rate = roundedValue + } + + func handleNewSpeedJump() { + let rate: Float + + if self.applicationContext.rate == 4.0 { + rate = 0.5 + } else { + rate = min(self.applicationContext.rate + 0.5, 4.0) + } + + let roundedValue = round(rate * 100) / 100.0 + + self.watchConnectivityService.sendMessage(message: [ + "command": Command.speed.rawValue as AnyObject, + "rate": "\(rate)" as AnyObject + ]) + + self.applicationContext.rate = roundedValue + } +} diff --git a/BookPlayerWatch/CoreServices.swift b/BookPlayerWatch/CoreServices.swift index 8aa5b2836..b41d191dd 100644 --- a/BookPlayerWatch/CoreServices.swift +++ b/BookPlayerWatch/CoreServices.swift @@ -12,10 +12,10 @@ import Foundation class CoreServices: ObservableObject { let dataManager: DataManager let accountService: AccountServiceProtocol - let syncService: SyncServiceProtocol + var syncService: SyncServiceProtocol let libraryService: LibraryService let playbackService: PlaybackServiceProtocol - let playerManager: PlayerManagerProtocol + let playerManager: PlayerManager let playerLoaderService: PlayerLoaderService @Published var hasSyncEnabled = false @@ -26,7 +26,7 @@ class CoreServices: ObservableObject { syncService: SyncServiceProtocol, libraryService: LibraryService, playbackService: PlaybackServiceProtocol, - playerManager: PlayerManagerProtocol, + playerManager: PlayerManager, playerLoaderService: PlayerLoaderService ) { self.dataManager = dataManager @@ -42,4 +42,9 @@ class CoreServices: ObservableObject { func checkAndReloadIfSyncIsEnabled() { self.hasSyncEnabled = accountService.hasSyncEnabled() } + + func updateSyncEnabled(_ enabled: Bool) { + hasSyncEnabled = enabled + syncService.isActive = enabled + } } diff --git a/BookPlayerWatch/ExtensionDelegate.swift b/BookPlayerWatch/ExtensionDelegate.swift index 1c3f4dded..3a690414e 100644 --- a/BookPlayerWatch/ExtensionDelegate.swift +++ b/BookPlayerWatch/ExtensionDelegate.swift @@ -263,6 +263,6 @@ class ExtensionDelegate: NSObject, WKApplicationDelegate, ObservableObject { extension ExtensionDelegate: PurchasesDelegate { func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) { - coreServices?.hasSyncEnabled = customerInfo.entitlements.all["pro"]?.isActive == true + coreServices?.updateSyncEnabled(customerInfo.entitlements.all["pro"]?.isActive == true) } } diff --git a/BookPlayerWatch/NowPlaying/NowPlayingView.swift b/BookPlayerWatch/NowPlaying/NowPlayingView.swift index 8117a32cc..b04b304ff 100644 --- a/BookPlayerWatch/NowPlaying/NowPlayingView.swift +++ b/BookPlayerWatch/NowPlaying/NowPlayingView.swift @@ -15,8 +15,7 @@ struct NowPlayingView: View { var body: some View { VStack { NowPlayingTitleView( - author: contextManager.applicationContext.currentItem?.author ?? "", - title: contextManager.applicationContext.currentItem?.title ?? "" + item: .constant(contextManager.applicationContext.currentItem) ) Spacer() diff --git a/BookPlayerWatch/NowPlaying/Views/NowPlayingPlaybackControlsView.swift b/BookPlayerWatch/NowPlaying/Views/NowPlayingPlaybackControlsView.swift index 9285364b1..be02398b3 100644 --- a/BookPlayerWatch/NowPlaying/Views/NowPlayingPlaybackControlsView.swift +++ b/BookPlayerWatch/NowPlaying/Views/NowPlayingPlaybackControlsView.swift @@ -27,7 +27,7 @@ struct NowPlayingPlaybackControlsView: View { Spacer() - VolumeView() + VolumeView(type: .companion) Spacer() diff --git a/BookPlayerWatch/NowPlaying/Views/NowPlayingTitleView.swift b/BookPlayerWatch/NowPlaying/Views/NowPlayingTitleView.swift index b14fa3024..074332b82 100644 --- a/BookPlayerWatch/NowPlaying/Views/NowPlayingTitleView.swift +++ b/BookPlayerWatch/NowPlaying/Views/NowPlayingTitleView.swift @@ -6,20 +6,21 @@ // Copyright © 2022 Tortuga Power. All rights reserved. // +import BookPlayerWatchKit import SwiftUI struct NowPlayingTitleView: View { - let author: String - let title: String + @Binding var item: PlayableItem? + var body: some View { VStack(alignment: .leading) { Spacer() .frame(maxWidth: .infinity, maxHeight: 0) - Text(author) + Text(item?.author ?? "") .font(.subheadline.smallCaps()) .foregroundColor(Color.secondary) .lineLimit(1) - Text(title) + Text(item?.title ?? "") .font(.headline) .foregroundColor(Color.primary) .lineLimit(2) @@ -27,9 +28,3 @@ struct NowPlayingTitleView: View { } } } - -struct NowPlayingTitleView_Previews: PreviewProvider { - static var previews: some View { - NowPlayingTitleView(author: "author 1", title: "title 1 title 1 title 1 title 1 title 1") - } -} diff --git a/BookPlayerWatch/NowPlaying/Views/VolumeView.swift b/BookPlayerWatch/NowPlaying/Views/VolumeView.swift index 0d742a6a5..22b86df25 100644 --- a/BookPlayerWatch/NowPlaying/Views/VolumeView.swift +++ b/BookPlayerWatch/NowPlaying/Views/VolumeView.swift @@ -10,9 +10,10 @@ import SwiftUI struct VolumeView: WKInterfaceObjectRepresentable { typealias WKInterfaceObjectType = WKInterfaceVolumeControl + let type: WKInterfaceVolumeControl.Origin func makeWKInterfaceObject(context: Self.Context) -> WKInterfaceVolumeControl { - return WKInterfaceVolumeControl(origin: .companion) + return WKInterfaceVolumeControl(origin: type) } func updateWKInterfaceObject(_ wkInterfaceObject: WKInterfaceVolumeControl, context: WKInterfaceObjectRepresentableContext) { @@ -21,6 +22,6 @@ struct VolumeView: WKInterfaceObjectRepresentable { struct VolumeView_Previews: PreviewProvider { static var previews: some View { - VolumeView() + VolumeView(type: .companion) } } diff --git a/BookPlayerWatch/PlaybackFullControlsView.swift b/BookPlayerWatch/PlaybackFullControlsView.swift new file mode 100644 index 000000000..473066197 --- /dev/null +++ b/BookPlayerWatch/PlaybackFullControlsView.swift @@ -0,0 +1,100 @@ +// +// PlaybackFullControlsView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 25/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerWatchKit +import SwiftUI + +struct PlaybackFullControlsView: View { + @ObservedObject var model: PlaybackFullControlsViewModel + @AppStorage(Constants.UserDefaults.rewindInterval) var rewindInterval: TimeInterval = 30 + @AppStorage(Constants.UserDefaults.forwardInterval) var forwardInterval: TimeInterval = 30 + + var body: some View { + GeometryReader { metrics in + List { + Section { + Toggle( + "settings_boostvolume_title", + isOn: .init( + get: { model.boostVolume }, + set: { _ in + model.handleBoostVolumeToggle() + } + ) + ) + } + + Section("speed".localized.uppercased()) { + VStack { + HStack { + Spacer() + Button { + model.handleNewSpeed(model.rate - 0.1) + } label: { + ResizeableImageView(name: "minus.circle") + } + .buttonStyle(PlainButtonStyle()) + .frame(width: metrics.size.width * 0.15) + Spacer() + .padding([.leading], 5) + Button { + model.handleNewSpeedJump() + } label: { + Text("\(model.rate, specifier: "%.2f")x") + .padding() + .frame(maxWidth: .infinity) + .background(Color.black.brightness(0.2)) + .cornerRadius(5) + } + .buttonStyle(PlainButtonStyle()) + .frame(width: metrics.size.width * 0.4) + + Spacer() + .padding([.leading], 5) + Button { + model.handleNewSpeed(model.rate + 0.1) + } label: { + ResizeableImageView(name: "plus.circle") + } + .buttonStyle(PlainButtonStyle()) + .frame(width: metrics.size.width * 0.15) + Spacer() + } + } + } + .listRowBackground(Color.clear) + + Section("settings_skip_title") { + NavigationLink { + SkipDurationListView(skipDirection: .back) + } label: { + HStack { + Text("settings_skip_rewind_title") + Spacer() + Text(TimeParser.formatDuration(rewindInterval)) + Image(systemName: "chevron.forward") + } + } + + NavigationLink { + SkipDurationListView(skipDirection: .forward) + } label: { + HStack { + Text("settings_skip_forward_title") + Spacer() + Text(TimeParser.formatDuration(forwardInterval)) + Image(systemName: "chevron.forward") + } + } + } + } + .environment(\.defaultMinListRowHeight, 40) + } + .navigationTitle("settings_controls_title") + } +} diff --git a/BookPlayerWatch/PlayerControlsView.swift b/BookPlayerWatch/PlayerControlsView.swift new file mode 100644 index 000000000..396ca7c53 --- /dev/null +++ b/BookPlayerWatch/PlayerControlsView.swift @@ -0,0 +1,62 @@ +// +// PlayerControlsView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 25/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerWatchKit +import SwiftUI + +struct PlayerControlsView: View { + @ObservedObject var playerManager: PlayerManager + @AppStorage(Constants.UserDefaults.rewindInterval) var rewindInterval: TimeInterval = 30 + @AppStorage(Constants.UserDefaults.forwardInterval) var forwardInterval: TimeInterval = 30 + + var body: some View { + GeometryReader { geometry in + HStack { + Spacer() + Button { + playerManager.rewind() + } label: { + SkipIntervalView( + interval: Int(rewindInterval.rounded()), + skipDirection: .back + ) + .padding(10) + } + .buttonStyle(PlainButtonStyle()) + .frame(width: geometry.size.width * 0.28) + Spacer() + Button { + playerManager.playPause() + } label: { + ResizeableImageView( + name: playerManager.isPlaying + ? "pause.fill" + : "play.fill" + ) + .padding(8) + } + .buttonStyle(PlainButtonStyle()) + .frame(width: geometry.size.width * 0.28) + Spacer() + Button { + playerManager.forward() + } label: { + SkipIntervalView( + interval: Int(forwardInterval.rounded()), + skipDirection: .forward + ) + .padding(10) + } + .buttonStyle(PlainButtonStyle()) + .frame(width: geometry.size.width * 0.28) + Spacer() + } + .frame(maxHeight: .infinity) + } + } +} diff --git a/BookPlayerWatch/PlayerManager.swift b/BookPlayerWatch/PlayerManager.swift index 21579b921..96cfff6dd 100644 --- a/BookPlayerWatch/PlayerManager.swift +++ b/BookPlayerWatch/PlayerManager.swift @@ -14,7 +14,7 @@ import MediaPlayer // swiftlint:disable:next file_length -final class PlayerManager: NSObject, PlayerManagerProtocol { +final class PlayerManager: NSObject, PlayerManagerProtocol, ObservableObject { private let libraryService: LibraryServiceProtocol private let playbackService: PlaybackServiceProtocol private let syncService: SyncServiceProtocol @@ -169,6 +169,7 @@ final class PlayerManager: NSObject, PlayerManagerProtocol { } } + @MainActor func loadRemoteURLAsset(for chapter: PlayableChapter, forceRefresh: Bool) async throws -> AVURLAsset { let fileURL: URL @@ -217,7 +218,9 @@ final class PlayerManager: NSObject, PlayerManagerProtocol { await libraryService.loadChaptersIfNeeded(relativePath: chapter.relativePath, asset: asset) if let libraryItem = libraryService.getSimpleItem(with: chapter.relativePath) { - currentItem = try playbackService.getPlayableItem(from: libraryItem) + try await MainActor.run { + currentItem = try playbackService.getPlayableItem(from: libraryItem) + } } } @@ -347,13 +350,13 @@ final class PlayerManager: NSObject, PlayerManagerProtocol { self.audioPlayer.replaceCurrentItem(with: nil) self.observeStatus = true - self.isFetchingRemoteURL = nil - self.audioPlayer.replaceCurrentItem(with: playerItem) // Update UI on main thread DispatchQueue.main.async { [weak self] in guard let self = self else { return } + self.isFetchingRemoteURL = nil + self.audioPlayer.replaceCurrentItem(with: playerItem) self.currentSpeed = self.speedService.getSpeed(relativePath: chapter.relativePath) // Set book metadata for lockscreen and control center self.nowPlayingInfo = [ @@ -517,7 +520,7 @@ final class PlayerManager: NSObject, PlayerManagerProtocol { } } - static var rewindInterval: TimeInterval { + public static var rewindInterval: TimeInterval { get { if UserDefaults.standard.object(forKey: Constants.UserDefaults.rewindInterval) == nil { return 30.0 @@ -533,7 +536,7 @@ final class PlayerManager: NSObject, PlayerManagerProtocol { } } - static var forwardInterval: TimeInterval { + public static var forwardInterval: TimeInterval { get { if UserDefaults.standard.object(forKey: Constants.UserDefaults.forwardInterval) == nil { return 30.0 @@ -709,8 +712,11 @@ extension PlayerManager { return false } - /// Update nowPlaying state so the UI displays correctly - playbackQueued = true + await MainActor.run { + /// Update nowPlaying state so the UI displays correctly + playbackQueued = true + } + await syncProgressDelegate?.waitForSyncInProgress() return true diff --git a/BookPlayerWatch/PlayerMoreListView.swift b/BookPlayerWatch/PlayerMoreListView.swift new file mode 100644 index 000000000..0114a5c7b --- /dev/null +++ b/BookPlayerWatch/PlayerMoreListView.swift @@ -0,0 +1,26 @@ +// +// PlayerMoreListView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 28/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import SwiftUI + +struct PlayerMoreListView: View { + var body: some View { + List { + Button { + print("Downloading Book") + } label: { + Text("Download Book") + } + } + .environment(\.defaultMinListRowHeight, 40) + } +} + +#Preview { + PlayerMoreListView() +} diff --git a/BookPlayerWatch/PlayerToolbarView.swift b/BookPlayerWatch/PlayerToolbarView.swift new file mode 100644 index 000000000..e63544a68 --- /dev/null +++ b/BookPlayerWatch/PlayerToolbarView.swift @@ -0,0 +1,91 @@ +// +// PlayerToolbarView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 25/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerWatchKit +import SwiftUI + +final class PlaybackFullControlsViewModel: ObservableObject { + let playerManager: PlayerManager + + var rate: Float { + self.playerManager.currentSpeed + } + + var boostVolume: Bool { + UserDefaults.standard.bool(forKey: Constants.UserDefaults.boostVolumeEnabled) + } + + init(playerManager: PlayerManager) { + self.playerManager = playerManager + } + + func handleBoostVolumeToggle() { + let flag = !boostVolume + UserDefaults.standard.set(flag, forKey: Constants.UserDefaults.boostVolumeEnabled) + + self.playerManager.setBoostVolume(flag) + } + + func handleNewSpeed(_ rate: Float) { + let roundedValue = round(rate * 100) / 100.0 + + guard roundedValue >= 0.5 && roundedValue <= 4.0 else { return } + + self.playerManager.setSpeed(roundedValue) + } + + func handleNewSpeedJump() { + let rate: Float + + if self.rate == 4.0 { + rate = 0.5 + } else { + rate = min(self.rate + 0.5, 4.0) + } + + let roundedValue = round(rate * 100) / 100.0 + + self.playerManager.setSpeed(roundedValue) + } + +} + +struct PlayerToolbarView: View { + @ObservedObject var playerManager: PlayerManager + + var body: some View { + HStack { + Spacer() + + NavigationLink( + destination: PlaybackFullControlsView(model: PlaybackFullControlsViewModel(playerManager: playerManager)) + ) { + ResizeableImageView(name: "dial.max") + .padding(11) + } + .buttonStyle(PlainButtonStyle()) + + Spacer() + + VolumeView(type: .local) + + Spacer() + + NavigationLink( + destination: PlayerMoreListView() + ) { + ResizeableImageView(name: "ellipsis.circle") + .padding(14) + } + .buttonStyle(PlainButtonStyle()) + + Spacer() + } + .fixedSize(horizontal: false, vertical: true) + } +} diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift index 2699c8db1..8489f0da5 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift @@ -12,6 +12,16 @@ import SwiftUI struct RemoteItemListCellView: View { let item: SimpleLibraryItem + var percentCompleted: String { + guard item.progress > 0 else { return "" } + + if item.isFinished { + return "100% - " + } else { + return "\(Int(item.percentCompleted))% - " + } + } + var body: some View { HStack { VStack(alignment: .leading) { @@ -21,6 +31,10 @@ struct RemoteItemListCellView: View { .font(.footnote) .foregroundColor(Color.secondary) .lineLimit(1) + Text("\(percentCompleted)\(item.durationFormatted)") + .font(.footnote) + .foregroundColor(Color.secondary) + .lineLimit(1) } Spacer() if item.type == .folder { @@ -29,3 +43,26 @@ struct RemoteItemListCellView: View { } } } + +struct LinearProgressView: View { + var value: Double + var shape: Shape + + var body: some View { + shape.fill(.secondary) + .overlay(alignment: .leading) { + GeometryReader { proxy in + shape.fill(.white) + .frame(width: proxy.size.width * value) + } + } + .clipShape(shape) + } +} + +extension LinearProgressView where Shape == Capsule { + init(value: Double, shape: Shape = Capsule()) { + self.value = value + self.shape = shape + } +} diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift index 469986957..cbeff014d 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -14,7 +14,9 @@ struct RemoteItemListView: View { @State var items: [SimpleLibraryItem] @State var lastPlayedItem: SimpleLibraryItem? @State var playingItemParentPath: String? - @State var error: Error? + @State private var isLoading = false + @State private var error: Error? + @State var showPlayer = false let folderRelativePath: String? @@ -80,17 +82,25 @@ struct RemoteItemListView: View { if folderRelativePath == nil { Section { if let lastPlayedItem { - RemoteItemListCellView(item: lastPlayedItem) - .onTapGesture { - Task { - do { - try await coreServices.playerLoaderService.loadPlayer(lastPlayedItem.relativePath, autoplay: true) - } catch { - print("=== loading player failed: \(error)") + ZStack { + NavigationLink(destination: RemotePlayerView(playerManager: coreServices.playerManager), isActive: $showPlayer) { + EmptyView() + } + RemoteItemListCellView(item: lastPlayedItem) + .onTapGesture { + Task { + do { + isLoading = true + try await coreServices.playerLoaderService.loadPlayer(lastPlayedItem.relativePath, autoplay: true) + isLoading = false + showPlayer = true + } catch { + isLoading = false + self.error = error + } } } -// coreServices.playerManager.load(<#T##item: PlayableItem##PlayableItem#>, autoplay: <#T##Bool#>) - } + } } } header: { Text(verbatim: "watchapp_last_played_title".localized) @@ -111,17 +121,25 @@ struct RemoteItemListView: View { .foregroundColor(getForegroundColor(for: item)) } } else { - RemoteItemListCellView(item: item) - .foregroundColor(getForegroundColor(for: item)) - .onTapGesture { - Task { - do { - try await coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) - } catch { - print("=== loading player failed: \(error)") + ZStack { + NavigationLink(destination: RemotePlayerView(playerManager: coreServices.playerManager), isActive: $showPlayer) { + EmptyView() + } + RemoteItemListCellView(item: item) + .onTapGesture { + Task { + do { + isLoading = true + try await coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) + isLoading = false + showPlayer = true + } catch { + isLoading = false + self.error = error + } } } - } + } } } } header: { @@ -130,6 +148,21 @@ struct RemoteItemListView: View { } } .errorAlert(error: $error) + .overlay { + Group { + if isLoading { + ProgressView() + .tint(.white) + .padding() + .background( + Color.black + .opacity(0.9) + .clipShape(RoundedRectangle(cornerRadius: 10)) + ) + .ignoresSafeArea(.all) + } + } + } .onAppear { Task { guard diff --git a/BookPlayerWatch/RemotePlayerView.swift b/BookPlayerWatch/RemotePlayerView.swift new file mode 100644 index 000000000..c926830ba --- /dev/null +++ b/BookPlayerWatch/RemotePlayerView.swift @@ -0,0 +1,31 @@ +// +// RemotePlayerView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 25/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import SwiftUI + +struct RemotePlayerView: View { + @ObservedObject var playerManager: PlayerManager + + var body: some View { + VStack { + NowPlayingTitleView( + item: $playerManager.currentItem + ) + + Spacer() + + PlayerControlsView(playerManager: playerManager) + + Spacer() + + PlayerToolbarView(playerManager: playerManager) + } + .fixedSize(horizontal: false, vertical: false) + .ignoresSafeArea(edges: .bottom) + } +} diff --git a/BookPlayerWatch/SkipDurationListView.swift b/BookPlayerWatch/SkipDurationListView.swift new file mode 100644 index 000000000..d4cb58da1 --- /dev/null +++ b/BookPlayerWatch/SkipDurationListView.swift @@ -0,0 +1,78 @@ +// +// SkipDurationListView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 28/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerWatchKit +import SwiftUI + +struct SkipDurationListView: View { + @AppStorage(Constants.UserDefaults.rewindInterval) var selectedRewindInterval: TimeInterval = 30 + @AppStorage(Constants.UserDefaults.forwardInterval) var selectedForwardInterval: TimeInterval = 30 + @Environment(\.dismiss) var dismiss + + var skipDirection: SkipDirection + + var selectedInterval: TimeInterval { + switch skipDirection { + case .forward: + selectedForwardInterval + case .back: + selectedRewindInterval + } + } + + private let intervals: [TimeInterval] = [ + 2.0, + 5.0, + 10.0, + 15.0, + 20.0, + 30.0, + 45.0, + 60.0, + 90.0, + 120.0, + 180.0, + 240.0, + 300.0 + ] + + var body: some View { + ScrollViewReader { proxy in + List { + ForEach(intervals, id: \.self) { interval in + Button { + switch skipDirection { + case .forward: + selectedForwardInterval = interval + case .back: + selectedRewindInterval = interval + } + dismiss() + } label: { + HStack { + Text(TimeParser.formatDuration(interval)) + .font(.caption) + Spacer() + if interval == selectedInterval { + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + } + } + } + .onAppear { + proxy.scrollTo(selectedInterval, anchor: .center) + } + } + } +} + +#Preview { + SkipDurationListView(skipDirection: .forward) +} diff --git a/BookPlayerWatch/ar.lproj/Localizable.strings b/BookPlayerWatch/ar.lproj/Localizable.strings index 82fb281ed..e549bfde5 100644 --- a/BookPlayerWatch/ar.lproj/Localizable.strings +++ b/BookPlayerWatch/ar.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "الفصول"; "settings_controls_title" = "ضبط المشغل"; "settings_boostvolume_title" = "زيادة مستوى الصوت"; +"settings_skip_title" = "القفزات الزمنية"; +"settings_skip_rewind_title" = "التأخير"; +"settings_skip_forward_title" = "التقديم"; +"speed_title" = "السرعة"; "logout_title" = "تسجيل خروج"; diff --git a/BookPlayerWatch/cs.lproj/Localizable.strings b/BookPlayerWatch/cs.lproj/Localizable.strings index 661ae8468..594a2661d 100644 --- a/BookPlayerWatch/cs.lproj/Localizable.strings +++ b/BookPlayerWatch/cs.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Kapitoly"; "settings_controls_title" = "Ovládací prvky přehrávače"; "settings_boostvolume_title" = "Zvýšení hlasitosti"; +"settings_skip_title" = "Intervaly přetáčení"; +"settings_skip_rewind_title" = "Vrátit"; +"settings_skip_forward_title" = "Přetočit"; +"speed_title" = "Rychlost"; "logout_title" = "Odhlásit se"; diff --git a/BookPlayerWatch/da.lproj/Localizable.strings b/BookPlayerWatch/da.lproj/Localizable.strings index a6ed0af54..2cf885226 100644 --- a/BookPlayerWatch/da.lproj/Localizable.strings +++ b/BookPlayerWatch/da.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Kapitler"; "settings_controls_title" = "Afspiller kontrol"; "settings_boostvolume_title" = "Boost lydstyrken"; +"settings_skip_title" = "SPRING OVER-INTERVALLER"; +"settings_skip_rewind_title" = "Spol tilbage"; +"settings_skip_forward_title" = "Spol frem"; +"speed_title" = "Hastighed"; "logout_title" = "Log ud"; diff --git a/BookPlayerWatch/de.lproj/Localizable.strings b/BookPlayerWatch/de.lproj/Localizable.strings index 48c0f2ebf..1631e4b0f 100644 --- a/BookPlayerWatch/de.lproj/Localizable.strings +++ b/BookPlayerWatch/de.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Kapitel"; "settings_controls_title" = "Steuerung"; "settings_boostvolume_title" = "Lautstärke erhöhen"; +"settings_skip_title" = "Tasten „Überspringen“"; +"settings_skip_rewind_title" = "Rückwärts"; +"settings_skip_forward_title" = "Vorwärts"; +"speed_title" = "Wiedergabegeschwindigkeit"; "logout_title" = "Ausloggen"; diff --git a/BookPlayerWatch/el.lproj/Localizable.strings b/BookPlayerWatch/el.lproj/Localizable.strings index d9f10148b..a42627010 100644 --- a/BookPlayerWatch/el.lproj/Localizable.strings +++ b/BookPlayerWatch/el.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Κεφάλαια"; "settings_controls_title" = "Χειριστήρια αναπαραγωγού"; "settings_boostvolume_title" = "Ενίσχυση όγκου"; +"settings_skip_title" = "ΔΙΑΣΤΗΜΑΤΑ ΠΑΡΑΛΕΙΨΗΣ"; +"settings_skip_rewind_title" = "Πίσω"; +"settings_skip_forward_title" = "Προς τα εμπρός"; +"speed_title" = "Ταχύτητα"; "logout_title" = "Αποσύνδεση"; diff --git a/BookPlayerWatch/en.lproj/Localizable.strings b/BookPlayerWatch/en.lproj/Localizable.strings index e8bd22af1..dd92665ac 100644 --- a/BookPlayerWatch/en.lproj/Localizable.strings +++ b/BookPlayerWatch/en.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Chapters"; "settings_controls_title" = "Player Controls"; "settings_boostvolume_title" = "Boost Volume"; +"settings_skip_title" = "SKIP INTERVALS"; +"settings_skip_rewind_title" = "Rewind"; +"settings_skip_forward_title" = "Forward"; +"speed_title" = "speed"; "logout_title" = "Log out"; diff --git a/BookPlayerWatch/es.lproj/Localizable.strings b/BookPlayerWatch/es.lproj/Localizable.strings index 8e4ae3a57..e7397e295 100644 --- a/BookPlayerWatch/es.lproj/Localizable.strings +++ b/BookPlayerWatch/es.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Capítulos"; "settings_controls_title" = "Controles Del Reproductor"; "settings_boostvolume_title" = "Aumentar el volumen"; +"settings_skip_title" = "INTERVALOS DE SALTO"; +"settings_skip_rewind_title" = "Retroceder"; +"settings_skip_forward_title" = "Adelante"; +"speed_title" = "velocidad"; "logout_title" = "Cerrar sesión"; diff --git a/BookPlayerWatch/fi.lproj/Localizable.strings b/BookPlayerWatch/fi.lproj/Localizable.strings index f2e4f7029..18f795a24 100644 --- a/BookPlayerWatch/fi.lproj/Localizable.strings +++ b/BookPlayerWatch/fi.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Kappaleet"; "settings_controls_title" = "Soitinsäätimet"; "settings_boostvolume_title" = "Lisää äänenvoimakkuutta"; +"settings_skip_title" = "Skippaus intervallit"; +"settings_skip_rewind_title" = "Kelaa taaksepäin"; +"settings_skip_forward_title" = "Eteenpäin"; +"speed_title" = "nopeus"; "logout_title" = "Kirjautua ulos"; diff --git a/BookPlayerWatch/fr.lproj/Localizable.strings b/BookPlayerWatch/fr.lproj/Localizable.strings index a5294d72f..1022b33ac 100644 --- a/BookPlayerWatch/fr.lproj/Localizable.strings +++ b/BookPlayerWatch/fr.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Chapitres"; "settings_controls_title" = "Contrôles de lecture"; "settings_boostvolume_title" = "Augmenter le volume"; +"settings_skip_title" = "INTERVALLES DE SAUT"; +"settings_skip_rewind_title" = "Rembobiner"; +"settings_skip_forward_title" = "Avancer"; +"speed_title" = "vitesse"; "logout_title" = "Se déconnecter"; diff --git a/BookPlayerWatch/hu.lproj/Localizable.strings b/BookPlayerWatch/hu.lproj/Localizable.strings index bef049db7..17db14326 100644 --- a/BookPlayerWatch/hu.lproj/Localizable.strings +++ b/BookPlayerWatch/hu.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Fejezetek"; "settings_controls_title" = "Lejátszóvezérlők"; "settings_boostvolume_title" = "Hangerő növelése"; +"settings_skip_title" = "UGRÁSOK IDŐTARTAMA"; +"settings_skip_rewind_title" = "Visszatekerés"; +"settings_skip_forward_title" = "Előretekerés"; +"speed_title" = "sebesség"; "logout_title" = "Kijelentkezés"; diff --git a/BookPlayerWatch/it.lproj/Localizable.strings b/BookPlayerWatch/it.lproj/Localizable.strings index c2af83d0b..a17a0a9af 100644 --- a/BookPlayerWatch/it.lproj/Localizable.strings +++ b/BookPlayerWatch/it.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Capitoli"; "settings_controls_title" = "Controlli della Riproduzione"; "settings_boostvolume_title" = "Aumenta il volume massimo"; +"settings_skip_title" = "SALTA INTERVALLI"; +"settings_skip_rewind_title" = "Riavvolgi"; +"settings_skip_forward_title" = "Avanti"; +"speed_title" = "velocità"; "logout_title" = "Disconnettersi"; diff --git a/BookPlayerWatch/nb.lproj/Localizable.strings b/BookPlayerWatch/nb.lproj/Localizable.strings index 5f2c3ef66..a576f3cec 100644 --- a/BookPlayerWatch/nb.lproj/Localizable.strings +++ b/BookPlayerWatch/nb.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Kapitler"; "settings_controls_title" = "Avspillingskontroller"; "settings_boostvolume_title" = "Boost volumet"; +"settings_skip_title" = "HOPP OVER INTERVALLER"; +"settings_skip_rewind_title" = "Spol tilbake"; +"settings_skip_forward_title" = "Spol fremover"; +"speed_title" = "hastighet"; "logout_title" = "Logg ut"; diff --git a/BookPlayerWatch/nl.lproj/Localizable.strings b/BookPlayerWatch/nl.lproj/Localizable.strings index c7168548a..2dd3eaeac 100644 --- a/BookPlayerWatch/nl.lproj/Localizable.strings +++ b/BookPlayerWatch/nl.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Hoofdstukken"; "settings_controls_title" = "Spelerbediening"; "settings_boostvolume_title" = "Boost volume"; +"settings_skip_title" = "INTERVAL OVERSLAAN"; +"settings_skip_rewind_title" = "Terugspoelen"; +"settings_skip_forward_title" = "Naar voren"; +"speed_title" = "snelheid"; "logout_title" = "Uitloggen"; diff --git a/BookPlayerWatch/pl.lproj/Localizable.strings b/BookPlayerWatch/pl.lproj/Localizable.strings index 7969f0a61..d7cd80723 100644 --- a/BookPlayerWatch/pl.lproj/Localizable.strings +++ b/BookPlayerWatch/pl.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Rozdziały"; "settings_controls_title" = "Elementy sterujące odtwarzacza"; "settings_boostvolume_title" = "Zwiększ głośność"; +"settings_skip_title" = "POMIJANIE INTERWAŁÓW"; +"settings_skip_rewind_title" = "Cofnij"; +"settings_skip_forward_title" = "Do przodu"; +"speed_title" = "prędkość"; "logout_title" = "Wyloguj"; diff --git a/BookPlayerWatch/pt-BR.lproj/Localizable.strings b/BookPlayerWatch/pt-BR.lproj/Localizable.strings index 5de7a0416..9b9c6aa42 100644 --- a/BookPlayerWatch/pt-BR.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-BR.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Capítulos"; "settings_controls_title" = "Controles do Reprodutor"; "settings_boostvolume_title" = "Aumentar volume"; +"settings_skip_title" = "PULAR INTERVALOS"; +"settings_skip_rewind_title" = "Retroceder"; +"settings_skip_forward_title" = "Avançar"; +"speed_title" = "velocidade"; "logout_title" = "Sair"; diff --git a/BookPlayerWatch/pt-PT.lproj/Localizable.strings b/BookPlayerWatch/pt-PT.lproj/Localizable.strings index f4b1ce57c..6f3434aa7 100644 --- a/BookPlayerWatch/pt-PT.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-PT.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Capítulos"; "settings_controls_title" = "Controles do Reprodutor"; "settings_boostvolume_title" = "Aumentar volume"; +"settings_skip_title" = "SALTAR INTERVALOS"; +"settings_skip_rewind_title" = "Rebobinar"; +"settings_skip_forward_title" = "Avançar"; +"speed_title" = "velocidade"; "logout_title" = "Sair"; diff --git a/BookPlayerWatch/ro.lproj/Localizable.strings b/BookPlayerWatch/ro.lproj/Localizable.strings index 037be9c9d..4fecbb45e 100644 --- a/BookPlayerWatch/ro.lproj/Localizable.strings +++ b/BookPlayerWatch/ro.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Capitole"; "settings_controls_title" = "Controlul Playerului"; "settings_boostvolume_title" = "Creșterea volumului"; +"settings_skip_title" = "SĂRIȚI INTERVALURILE"; +"settings_skip_rewind_title" = "Derulare înapoi"; +"settings_skip_forward_title" = "Înainte"; +"speed_title" = "Viteza"; "logout_title" = "Deconectați-vă"; diff --git a/BookPlayerWatch/ru.lproj/Localizable.strings b/BookPlayerWatch/ru.lproj/Localizable.strings index 33fcc5a77..2d19ab9c0 100644 --- a/BookPlayerWatch/ru.lproj/Localizable.strings +++ b/BookPlayerWatch/ru.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Главы"; "settings_controls_title" = "Управление плеером"; "settings_boostvolume_title" = "Увеличить громкость"; +"settings_skip_title" = "ШАГ ПЕРЕМОТКИ"; +"settings_skip_rewind_title" = "Назад"; +"settings_skip_forward_title" = "Вперёд"; +"speed_title" = "скорость"; "logout_title" = "Выйти"; diff --git a/BookPlayerWatch/sk-SK.lproj/Localizable.strings b/BookPlayerWatch/sk-SK.lproj/Localizable.strings index 7ed11a7ef..bd5ead1a5 100644 --- a/BookPlayerWatch/sk-SK.lproj/Localizable.strings +++ b/BookPlayerWatch/sk-SK.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Kapitoly"; "settings_controls_title" = "Ovládacie prvky Prehrávača"; "settings_boostvolume_title" = "Zvýšenie hlasitosti"; +"settings_skip_title" = "PRETÁČANIE"; +"settings_skip_rewind_title" = "Dozadu"; +"settings_skip_forward_title" = "Dopredu"; +"speed_title" = "rýchlosť"; "logout_title" = "Odhlásiť sa"; diff --git a/BookPlayerWatch/sv.lproj/Localizable.strings b/BookPlayerWatch/sv.lproj/Localizable.strings index caf15a8ca..c3109b849 100644 --- a/BookPlayerWatch/sv.lproj/Localizable.strings +++ b/BookPlayerWatch/sv.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Kapitel"; "settings_controls_title" = "Spelarkontroller"; "settings_boostvolume_title" = "Öka volymen"; +"settings_skip_title" = "SNABBSPOLNING"; +"settings_skip_rewind_title" = "Tillbaka"; +"settings_skip_forward_title" = "Framåt"; +"speed_title" = "hastighet"; "logout_title" = "Logga ut"; diff --git a/BookPlayerWatch/tr.lproj/Localizable.strings b/BookPlayerWatch/tr.lproj/Localizable.strings index 1c8be45cf..b0f06a6c0 100644 --- a/BookPlayerWatch/tr.lproj/Localizable.strings +++ b/BookPlayerWatch/tr.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Bölümler"; "settings_controls_title" = "Oynatıcı Kontrolleri"; "settings_boostvolume_title" = "Sesi Güçlendir"; +"settings_skip_title" = "ATLAMA ARALIKLARI"; +"settings_skip_rewind_title" = "Geri Sarma"; +"settings_skip_forward_title" = "İleri Sarma"; +"speed_title" = "hız"; "logout_title" = "Çıkış Yap"; diff --git a/BookPlayerWatch/uk.lproj/Localizable.strings b/BookPlayerWatch/uk.lproj/Localizable.strings index 8c2d53cf5..c41b34ac2 100644 --- a/BookPlayerWatch/uk.lproj/Localizable.strings +++ b/BookPlayerWatch/uk.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "Розділи"; "settings_controls_title" = "Керування програвачем"; "settings_boostvolume_title" = "Збільшення гучності"; +"settings_skip_title" = "Інтервали перемотування"; +"settings_skip_rewind_title" = "Назад"; +"settings_skip_forward_title" = "Вперед"; +"speed_title" = "швидкість"; "logout_title" = "Вийти"; diff --git a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings index 1bbc3c782..622e601c7 100644 --- a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings +++ b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings @@ -16,4 +16,8 @@ "chapters_title" = "章节"; "settings_controls_title" = "播放器控制"; "settings_boostvolume_title" = "提高音量"; +"settings_skip_title" = "跳过间隔"; +"settings_skip_rewind_title" = "倒带"; +"settings_skip_forward_title" = "快进"; +"speed_title" = "速度"; "logout_title" = "登出"; diff --git a/Shared/Network/NetworkClient.swift b/Shared/Network/NetworkClient.swift index 9ff437f26..b1b5418ac 100644 --- a/Shared/Network/NetworkClient.swift +++ b/Shared/Network/NetworkClient.swift @@ -219,6 +219,7 @@ public class NetworkClient: NetworkClientProtocol, BPLogger { request.setValue("application/json", forHTTPHeaderField: "Content-Type") #if os(watchOS) request.setValue("watch.bookplayer.app", forHTTPHeaderField: "origin") + request.setValue("2022-12-12", forHTTPHeaderField: "accept-version") #endif if useKeychain, From c39d2013e6289ad48d5bc4c3717e8b3e8bd0578b Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Fri, 29 Nov 2024 10:35:10 -0500 Subject: [PATCH 06/31] Add chapters --- .../ChapterList/ChapterListView.swift | 12 +++++----- .../NowPlayingPlaybackControlsView.swift | 23 ++++++++++++++----- BookPlayerWatch/PlayerMoreListView.swift | 20 ++++++++++++---- BookPlayerWatch/PlayerToolbarView.swift | 19 +++++++++++---- 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/BookPlayerWatch/ChapterList/ChapterListView.swift b/BookPlayerWatch/ChapterList/ChapterListView.swift index 2ec127db4..c143c5df1 100644 --- a/BookPlayerWatch/ChapterList/ChapterListView.swift +++ b/BookPlayerWatch/ChapterList/ChapterListView.swift @@ -10,17 +10,16 @@ import BookPlayerWatchKit import SwiftUI struct ChapterListView: View { - @Environment(\.presentationMode) var presentationMode - @EnvironmentObject var contextManager: ContextManager + @Binding var currentItem: PlayableItem? + var didSelectChapter: (PlayableChapter) -> Void var body: some View { ScrollViewReader { proxy in List { - if let currentItem = contextManager.applicationContext.currentItem { + if let currentItem { ForEach(currentItem.chapters) { chapter in Button { - contextManager.handleChapterSelected(chapter) - presentationMode.wrappedValue.dismiss() + didSelectChapter(chapter) } label: { HStack { Text(chapter.title) @@ -34,8 +33,9 @@ struct ChapterListView: View { } } } + .environment(\.defaultMinListRowHeight, 40) .onAppear { - if let currentChapter = contextManager.applicationContext.currentItem?.currentChapter { + if let currentChapter = currentItem?.currentChapter { proxy.scrollTo(currentChapter.index) } } diff --git a/BookPlayerWatch/NowPlaying/Views/NowPlayingPlaybackControlsView.swift b/BookPlayerWatch/NowPlaying/Views/NowPlayingPlaybackControlsView.swift index be02398b3..bb4bd344e 100644 --- a/BookPlayerWatch/NowPlaying/Views/NowPlayingPlaybackControlsView.swift +++ b/BookPlayerWatch/NowPlaying/Views/NowPlayingPlaybackControlsView.swift @@ -11,6 +11,7 @@ import SwiftUI struct NowPlayingPlaybackControlsView: View { @EnvironmentObject var contextManager: ContextManager + @State var isShowingChapterList: Bool = false var body: some View { HStack { @@ -31,15 +32,25 @@ struct NowPlayingPlaybackControlsView: View { Spacer() - NavigationLink( - destination: ChapterListView() - .environmentObject(contextManager) - ) { + ZStack { + NavigationLink( + destination: ChapterListView( + currentItem: $contextManager.applicationContext.currentItem + ) { chapter in + contextManager.handleChapterSelected(chapter) + isShowingChapterList = false + }, + isActive: $isShowingChapterList + ) { + EmptyView() + } + .buttonStyle(PlainButtonStyle()) ResizeableImageView(name: "list.bullet") .padding(14) + .onTapGesture { + isShowingChapterList = true + } } - .buttonStyle(PlainButtonStyle()) - Spacer() } .fixedSize(horizontal: false, vertical: true) diff --git a/BookPlayerWatch/PlayerMoreListView.swift b/BookPlayerWatch/PlayerMoreListView.swift index 0114a5c7b..190f10848 100644 --- a/BookPlayerWatch/PlayerMoreListView.swift +++ b/BookPlayerWatch/PlayerMoreListView.swift @@ -6,9 +6,13 @@ // Copyright © 2024 Tortuga Power. All rights reserved. // +import BookPlayerWatchKit import SwiftUI struct PlayerMoreListView: View { + @ObservedObject var playerManager: PlayerManager + @Binding var isShowingView: Bool + var body: some View { List { Button { @@ -16,11 +20,19 @@ struct PlayerMoreListView: View { } label: { Text("Download Book") } + + NavigationLink( + destination: ChapterListView( + currentItem: $playerManager.currentItem, + didSelectChapter: { chapter in + playerManager.jumpToChapter(chapter) + isShowingView = false + } + ) + ) { + Text("chapters_title") + } } .environment(\.defaultMinListRowHeight, 40) } } - -#Preview { - PlayerMoreListView() -} diff --git a/BookPlayerWatch/PlayerToolbarView.swift b/BookPlayerWatch/PlayerToolbarView.swift index e63544a68..a3e630657 100644 --- a/BookPlayerWatch/PlayerToolbarView.swift +++ b/BookPlayerWatch/PlayerToolbarView.swift @@ -57,6 +57,7 @@ final class PlaybackFullControlsViewModel: ObservableObject { struct PlayerToolbarView: View { @ObservedObject var playerManager: PlayerManager + @State var isShowingMoreList: Bool = false var body: some View { HStack { @@ -76,13 +77,23 @@ struct PlayerToolbarView: View { Spacer() - NavigationLink( - destination: PlayerMoreListView() - ) { + ZStack { + NavigationLink( + destination: PlayerMoreListView( + playerManager: playerManager, + isShowingView: $isShowingMoreList + ), + isActive: $isShowingMoreList + ) { + EmptyView() + } + .buttonStyle(PlainButtonStyle()) ResizeableImageView(name: "ellipsis.circle") .padding(14) + .onTapGesture { + isShowingMoreList = true + } } - .buttonStyle(PlainButtonStyle()) Spacer() } From efc472dd9cdbf09d4e345453dc6682ca7cb7000f Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 30 Nov 2024 00:09:32 -0500 Subject: [PATCH 07/31] Add download logic --- BookPlayer.xcodeproj/project.pbxproj | 4 + .../ChapterList/ChapterListView.swift | 2 +- BookPlayerWatch/PlayerMoreListView.swift | 12 +-- BookPlayerWatch/PlayerToolbarView.swift | 36 ++++---- .../RemoteItemCellViewModel.swift | 82 +++++++++++++++++++ .../RemoteItemListCellView.swift | 80 +++++++++++++++--- .../RemoteItemList/RemoteItemListView.swift | 66 +++++++-------- 7 files changed, 213 insertions(+), 69 deletions(-) create mode 100644 BookPlayerWatch/RemoteItemList/RemoteItemCellViewModel.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index 8a4fdc5b5..18933f0ac 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -342,6 +342,7 @@ 632941402AEEE739000AD2EE /* CircularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6329413F2AEEE739000AD2EE /* CircularView.swift */; }; 6334CF1B2CF8D87500F1FA17 /* SkipDurationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF1A2CF8D87500F1FA17 /* SkipDurationListView.swift */; }; 6334CF1D2CF90AF900F1FA17 /* PlayerMoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF1C2CF90AF900F1FA17 /* PlayerMoreListView.swift */; }; + 6334CF1F2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF1E2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift */; }; 633BE3E52AE6102D00F983AC /* BPShortcutsLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 633BE3E42AE6102D00F983AC /* BPShortcutsLink.swift */; }; 6340D5692AF12D48003D0B09 /* SharedWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6340D5682AF12D48003D0B09 /* SharedWidget.swift */; }; 6340D56E2AF13E4C003D0B09 /* RectangularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6340D56D2AF13E4C003D0B09 /* RectangularView.swift */; }; @@ -1135,6 +1136,7 @@ 6329413F2AEEE739000AD2EE /* CircularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularView.swift; sourceTree = ""; }; 6334CF1A2CF8D87500F1FA17 /* SkipDurationListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkipDurationListView.swift; sourceTree = ""; }; 6334CF1C2CF90AF900F1FA17 /* PlayerMoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerMoreListView.swift; sourceTree = ""; }; + 6334CF1E2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteItemCellViewModel.swift; sourceTree = ""; }; 633BE3E12AE43D1300F983AC /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/AppShortcuts.strings"; sourceTree = ""; }; 633BE3E42AE6102D00F983AC /* BPShortcutsLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPShortcutsLink.swift; sourceTree = ""; }; 6340D5682AF12D48003D0B09 /* SharedWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedWidget.swift; sourceTree = ""; }; @@ -2320,6 +2322,7 @@ children = ( 6399D06F2CEBA35D00A2E278 /* RemoteItemListView.swift */, 6399D0712CEBA37C00A2E278 /* RemoteItemListCellView.swift */, + 6334CF1E2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift */, ); path = RemoteItemList; sourceTree = ""; @@ -3412,6 +3415,7 @@ 9FA334B627C15DE30064E8EA /* VolumeView.swift in Sources */, 6350E46D2CF4315B0077CDC1 /* PlayerLoaderService.swift in Sources */, 6350E46B2CF42B500077CDC1 /* UserActivityManager.swift in Sources */, + 6334CF1F2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift in Sources */, 9FA334B927C1B8450064E8EA /* NowPlayingTitleView.swift in Sources */, 6350E4692CF425500077CDC1 /* WidgetReloadService.swift in Sources */, 9FA334B327C156CB0064E8EA /* NowPlayingView.swift in Sources */, diff --git a/BookPlayerWatch/ChapterList/ChapterListView.swift b/BookPlayerWatch/ChapterList/ChapterListView.swift index c143c5df1..98258e698 100644 --- a/BookPlayerWatch/ChapterList/ChapterListView.swift +++ b/BookPlayerWatch/ChapterList/ChapterListView.swift @@ -36,7 +36,7 @@ struct ChapterListView: View { .environment(\.defaultMinListRowHeight, 40) .onAppear { if let currentChapter = currentItem?.currentChapter { - proxy.scrollTo(currentChapter.index) + proxy.scrollTo(currentChapter.index, anchor: .center) } } } diff --git a/BookPlayerWatch/PlayerMoreListView.swift b/BookPlayerWatch/PlayerMoreListView.swift index 190f10848..b7611bd3d 100644 --- a/BookPlayerWatch/PlayerMoreListView.swift +++ b/BookPlayerWatch/PlayerMoreListView.swift @@ -9,18 +9,18 @@ import BookPlayerWatchKit import SwiftUI +/// To be used later in the player view struct PlayerMoreListView: View { @ObservedObject var playerManager: PlayerManager @Binding var isShowingView: Bool + init(playerManager: PlayerManager, isShowingView: Binding) { + self.playerManager = playerManager + self._isShowingView = isShowingView + } + var body: some View { List { - Button { - print("Downloading Book") - } label: { - Text("Download Book") - } - NavigationLink( destination: ChapterListView( currentItem: $playerManager.currentItem, diff --git a/BookPlayerWatch/PlayerToolbarView.swift b/BookPlayerWatch/PlayerToolbarView.swift index a3e630657..81c759515 100644 --- a/BookPlayerWatch/PlayerToolbarView.swift +++ b/BookPlayerWatch/PlayerToolbarView.swift @@ -77,26 +77,30 @@ struct PlayerToolbarView: View { Spacer() - ZStack { - NavigationLink( - destination: PlayerMoreListView( - playerManager: playerManager, - isShowingView: $isShowingMoreList - ), - isActive: $isShowingMoreList - ) { - EmptyView() + ResizeableImageView(name: "ellipsis.circle") + .padding(14) + .onTapGesture { + isShowingMoreList = true } - .buttonStyle(PlainButtonStyle()) - ResizeableImageView(name: "ellipsis.circle") - .padding(14) - .onTapGesture { - isShowingMoreList = true - } - } Spacer() } + .background { + NavigationLink( + destination: ChapterListView( + currentItem: $playerManager.currentItem, + didSelectChapter: { chapter in + playerManager.jumpToChapter(chapter) + isShowingMoreList = false + } + ), + isActive: $isShowingMoreList + ) { + EmptyView() + } + .buttonStyle(PlainButtonStyle()) + .opacity(0) + } .fixedSize(horizontal: false, vertical: true) } } diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemCellViewModel.swift b/BookPlayerWatch/RemoteItemList/RemoteItemCellViewModel.swift new file mode 100644 index 000000000..b405c92ed --- /dev/null +++ b/BookPlayerWatch/RemoteItemList/RemoteItemCellViewModel.swift @@ -0,0 +1,82 @@ +// +// RemoteItemCellViewModel.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 29/11/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import BookPlayerWatchKit +import Combine +import Foundation + +class RemoteItemCellViewModel: ObservableObject { + let item: SimpleLibraryItem + let coreServices: CoreServices + @Published var downloadState: DownloadState + + private var disposeBag = Set() + + init(item: SimpleLibraryItem, coreServices: CoreServices) { + self.item = item + self.coreServices = coreServices + self._downloadState = .init(initialValue: coreServices.syncService.getDownloadState(for: item)) + bindObservers() + } + + func bindObservers() { + coreServices.syncService.downloadCompletedPublisher + .filter({ [weak self] in + $0.1 == self?.item.parentFolder || $0.2 == self?.item.parentFolder + }) + .receive(on: DispatchQueue.main) + .sink { [weak self] (relativePath, initiatingItemPath, _) in + guard + relativePath == self?.item.relativePath || initiatingItemPath == self?.item.relativePath + else { return } + + self?.downloadState = .downloaded + }.store(in: &disposeBag) + + coreServices.syncService.downloadProgressPublisher + .filter({ [weak self] in + $0.1 == self?.item.parentFolder || $0.2 == self?.item.parentFolder + }) + .receive(on: DispatchQueue.main) + .sink { [weak self] (relativePath, initiatingItemPath, _, progress) in + guard + relativePath == self?.item.relativePath || initiatingItemPath == self?.item.relativePath + else { return } + + self?.downloadState = .downloading(progress: progress) + }.store(in: &disposeBag) + } + + func startDownload() async throws { + let fileURL = item.fileURL + /// Create backing folder if it does not exist + if item.type == .folder || item.type == .bound { + try DataManager.createBackingFolderIfNeeded(fileURL) + } + + try await coreServices.syncService.downloadRemoteFiles(for: item) + } + + func cancelDownload() throws { + try coreServices.syncService.cancelDownload(of: item) + downloadState = .notDownloaded + } + + func offloadItem() throws { + let fileURL = item.fileURL + try FileManager.default.removeItem(at: fileURL) + if item.type == .bound || item.type == .folder { + try FileManager.default.createDirectory( + at: fileURL, + withIntermediateDirectories: false, + attributes: nil + ) + } + downloadState = .notDownloaded + } +} diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift index 8489f0da5..a2a00621d 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift @@ -10,37 +10,95 @@ import BookPlayerWatchKit import SwiftUI struct RemoteItemListCellView: View { - let item: SimpleLibraryItem + @StateObject var model: RemoteItemCellViewModel + @State private var error: Error? + + init(item: SimpleLibraryItem, coreServices: CoreServices) { + self._model = .init(wrappedValue: .init(item: item, coreServices: coreServices)) + } var percentCompleted: String { - guard item.progress > 0 else { return "" } + guard model.item.progress > 0 else { return "" } - if item.isFinished { + if model.item.isFinished { return "100% - " } else { - return "\(Int(item.percentCompleted))% - " + return "\(Int(model.item.percentCompleted))% - " } } var body: some View { HStack { VStack(alignment: .leading) { - Text(item.title) + Text(model.item.title) .lineLimit(2) - Text(item.details) - .font(.footnote) - .foregroundColor(Color.secondary) - .lineLimit(1) - Text("\(percentCompleted)\(item.durationFormatted)") + Text(model.item.details) .font(.footnote) .foregroundColor(Color.secondary) .lineLimit(1) + switch model.downloadState { + case .downloading(let progress): + LinearProgressView(value: progress) + .frame(maxWidth: 100, maxHeight: 10) + case .downloaded: + Text(Image(systemName: "applewatch")) + .font(.caption2) + + Text(" - \(percentCompleted)\(model.item.durationFormatted)") + .font(.footnote) + .foregroundColor(Color.secondary) + case .notDownloaded: + Text(Image(systemName: "icloud.fill")) + .font(.caption2) + + Text(" - \(percentCompleted)\(model.item.durationFormatted)") + .font(.footnote) + .foregroundColor(Color.secondary) + } } Spacer() - if item.type == .folder { + if model.item.type == .folder { Image(systemName: "chevron.forward") } } + .errorAlert(error: $error) + .swipeActions { + switch model.downloadState { + case .downloading: + Button { + do { + try model.cancelDownload() + } catch { + self.error = error + } + } label: { + Image(systemName: "xmark.circle") + .imageScale(.large) + } + case .downloaded: + Button { + do { + try model.offloadItem() + } catch { + self.error = error + } + } label: { + Image(systemName: "applewatch.slash") + .imageScale(.large) + } + case .notDownloaded: + Button { + Task { + do { + try await model.startDownload() + } catch { + self.error = error + } + } + } label: { + Image(systemName: "icloud.and.arrow.down.fill") + .imageScale(.large) + } + } + } } } diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift index cbeff014d..400dda574 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -82,25 +82,20 @@ struct RemoteItemListView: View { if folderRelativePath == nil { Section { if let lastPlayedItem { - ZStack { - NavigationLink(destination: RemotePlayerView(playerManager: coreServices.playerManager), isActive: $showPlayer) { - EmptyView() - } - RemoteItemListCellView(item: lastPlayedItem) - .onTapGesture { - Task { - do { - isLoading = true - try await coreServices.playerLoaderService.loadPlayer(lastPlayedItem.relativePath, autoplay: true) - isLoading = false - showPlayer = true - } catch { - isLoading = false - self.error = error - } + RemoteItemListCellView(item: lastPlayedItem, coreServices: coreServices) + .onTapGesture { + Task { + do { + isLoading = true + try await coreServices.playerLoaderService.loadPlayer(lastPlayedItem.relativePath, autoplay: true) + isLoading = false + showPlayer = true + } catch { + isLoading = false + self.error = error } } - } + } } } header: { Text(verbatim: "watchapp_last_played_title".localized) @@ -117,29 +112,24 @@ struct RemoteItemListView: View { folderRelativePath: item.relativePath ) } label: { - RemoteItemListCellView(item: item) + RemoteItemListCellView(item: item, coreServices: coreServices) .foregroundColor(getForegroundColor(for: item)) } } else { - ZStack { - NavigationLink(destination: RemotePlayerView(playerManager: coreServices.playerManager), isActive: $showPlayer) { - EmptyView() - } - RemoteItemListCellView(item: item) - .onTapGesture { - Task { - do { - isLoading = true - try await coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) - isLoading = false - showPlayer = true - } catch { - isLoading = false - self.error = error - } + RemoteItemListCellView(item: item, coreServices: coreServices) + .onTapGesture { + Task { + do { + isLoading = true + try await coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) + isLoading = false + showPlayer = true + } catch { + isLoading = false + self.error = error } } - } + } } } } header: { @@ -147,6 +137,12 @@ struct RemoteItemListView: View { .foregroundStyle(Color.accentColor) } } + .background( + NavigationLink(destination: RemotePlayerView(playerManager: coreServices.playerManager), isActive: $showPlayer) { + EmptyView() + } + .opacity(0) + ) .errorAlert(error: $error) .overlay { Group { From 07b262203c3458a7bfd7418a0b441190d0919221 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 30 Nov 2024 00:22:29 -0500 Subject: [PATCH 08/31] Update widgets --- .../SharedNowPlayingWidget/SharedWidget.swift | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/BookPlayerWidgets/Shared/SharedNowPlayingWidget/SharedWidget.swift b/BookPlayerWidgets/Shared/SharedNowPlayingWidget/SharedWidget.swift index 59e326ee7..86dfb5ebe 100644 --- a/BookPlayerWidgets/Shared/SharedNowPlayingWidget/SharedWidget.swift +++ b/BookPlayerWidgets/Shared/SharedNowPlayingWidget/SharedWidget.swift @@ -78,7 +78,7 @@ struct SharedWidgetTimelineProvider: TimelineProvider { func getEntryForTimeline(context: Context) async throws -> SharedWidgetEntry { let currentItem: PlayableItem #if os(watchOS) - currentItem = try getWatchLastPlayedItem() + currentItem = try await getWatchLastPlayedItem() #else currentItem = try await getPhoneLastPlayedItem() #endif @@ -105,17 +105,24 @@ struct SharedWidgetTimelineProvider: TimelineProvider { ) } - func getWatchLastPlayedItem() throws -> PlayableItem { - guard - let watchContextFileURL = FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: Constants.ApplicationGroupIdentifier - )?.appendingPathComponent("WatchContextLastPlayed.data") - else { - throw BookPlayerError.emptyResponse - } + func getWatchLastPlayedItem() async throws -> PlayableItem { + let keychainService = KeychainService() + + if try keychainService.get(.token) != nil { + /// User is signed in, pull data from DB + return try await getPhoneLastPlayedItem() + } else { + guard + let watchContextFileURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: Constants.ApplicationGroupIdentifier + )?.appendingPathComponent("WatchContextLastPlayed.data") + else { + throw BookPlayerError.emptyResponse + } - let data = try Data(contentsOf: watchContextFileURL) - return try decoder.decode(PlayableItem.self, from: data) + let data = try Data(contentsOf: watchContextFileURL) + return try decoder.decode(PlayableItem.self, from: data) + } } func getPhoneLastPlayedItem() async throws -> PlayableItem { From eb3c0eb567c7ef755c2a824bea37757ed2ee2041 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 30 Nov 2024 13:02:51 -0500 Subject: [PATCH 09/31] remove public modifiers --- Shared/Network/NetworkProvider.swift | 2 +- Shared/Services/Account/AccountAPI.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Shared/Network/NetworkProvider.swift b/Shared/Network/NetworkProvider.swift index dea15fcbe..7feca359f 100644 --- a/Shared/Network/NetworkProvider.swift +++ b/Shared/Network/NetworkProvider.swift @@ -8,7 +8,7 @@ import Foundation -public class NetworkProvider { +class NetworkProvider { public let client: NetworkClientProtocol public init(client: NetworkClientProtocol = NetworkClient()) { diff --git a/Shared/Services/Account/AccountAPI.swift b/Shared/Services/Account/AccountAPI.swift index 2a48abe64..be589afe0 100644 --- a/Shared/Services/Account/AccountAPI.swift +++ b/Shared/Services/Account/AccountAPI.swift @@ -47,15 +47,15 @@ extension AccountAPI: Endpoint { return [ "rc_id": anonymousId, "first_seen": firstSeen, - "region": region + "region": region, ] } } } -public struct LoginResponse: Decodable { - public let email: String - public let token: String +struct LoginResponse: Decodable { + let email: String + let token: String } struct DeleteResponse: Decodable { From a523ea8c233472a967e7db6652d5402e8c545dee Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Mon, 2 Dec 2024 11:41:31 -0500 Subject: [PATCH 10/31] fix watch widgets --- BookPlayerWatch/ExtensionDelegate.swift | 2 +- BookPlayerWatch/PlayerManager.swift | 10 ++-------- .../SharedNowPlayingWidget/RectangularView.swift | 11 +++++++---- .../SharedNowPlayingWidget/SharedWidget.swift | 12 ++++++++---- .../SharedWidgetContainerView.swift | 16 +++++++++++++--- Shared/Services/Account/AccountService.swift | 6 +++--- 6 files changed, 34 insertions(+), 23 deletions(-) diff --git a/BookPlayerWatch/ExtensionDelegate.swift b/BookPlayerWatch/ExtensionDelegate.swift index 3a690414e..84bcb1818 100644 --- a/BookPlayerWatch/ExtensionDelegate.swift +++ b/BookPlayerWatch/ExtensionDelegate.swift @@ -32,7 +32,7 @@ class ExtensionDelegate: NSObject, WKApplicationDelegate, ObservableObject { for: .revenueCat ) Purchases.logLevel = .error - let rcUserId = UserDefaults.standard.string(forKey: "rcUserId") + let rcUserId = UserDefaults.sharedDefaults.string(forKey: "rcUserId") Purchases.configure(withAPIKey: revenueCatApiKey, appUserID: rcUserId) Purchases.shared.delegate = self } diff --git a/BookPlayerWatch/PlayerManager.swift b/BookPlayerWatch/PlayerManager.swift index 96cfff6dd..3f84d7e0d 100644 --- a/BookPlayerWatch/PlayerManager.swift +++ b/BookPlayerWatch/PlayerManager.swift @@ -289,16 +289,10 @@ final class PlayerManager: NSObject, PlayerManagerProtocol, ObservableObject { } func storeWidgetItem(_ item: PlayableItem) { - var widgetItems: [WidgetLibraryItem] = [ - WidgetLibraryItem( - relativePath: item.relativePath, - title: item.title, - details: item.author - ) - ] + var widgetItems: [PlayableItem] = [item] if let itemsData = UserDefaults.sharedDefaults.data(forKey: Constants.UserDefaults.sharedWidgetLastPlayedItems), - let items = try? decoder.decode([WidgetLibraryItem].self, from: itemsData) + let items = try? decoder.decode([PlayableItem].self, from: itemsData) { widgetItems.append(contentsOf: items.filter({ $0.relativePath != item.relativePath })) widgetItems = Array(widgetItems.prefix(10)) diff --git a/BookPlayerWidgets/Shared/SharedNowPlayingWidget/RectangularView.swift b/BookPlayerWidgets/Shared/SharedNowPlayingWidget/RectangularView.swift index ec1239a07..2fb35f4cb 100644 --- a/BookPlayerWidgets/Shared/SharedNowPlayingWidget/RectangularView.swift +++ b/BookPlayerWidgets/Shared/SharedNowPlayingWidget/RectangularView.swift @@ -14,14 +14,17 @@ struct RectangularView: View { let chapterTitle: String let bookTitle: String let details: String + let includeLogo: Bool var body: some View { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 4) { - Image("Graphic Circular") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 24) + if includeLogo { + Image("Graphic Circular") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24) + } Text(chapterTitle) .font(.headline) Spacer() diff --git a/BookPlayerWidgets/Shared/SharedNowPlayingWidget/SharedWidget.swift b/BookPlayerWidgets/Shared/SharedNowPlayingWidget/SharedWidget.swift index 86dfb5ebe..40f684af5 100644 --- a/BookPlayerWidgets/Shared/SharedNowPlayingWidget/SharedWidget.swift +++ b/BookPlayerWidgets/Shared/SharedNowPlayingWidget/SharedWidget.swift @@ -106,11 +106,15 @@ struct SharedWidgetTimelineProvider: TimelineProvider { } func getWatchLastPlayedItem() async throws -> PlayableItem { - let keychainService = KeychainService() + if UserDefaults.sharedDefaults.object(forKey: "rcUserId") != nil { + guard + let itemsData = UserDefaults.sharedDefaults.data(forKey: Constants.UserDefaults.sharedWidgetLastPlayedItems), + let item = (try decoder.decode([PlayableItem].self, from: itemsData)).first + else { + throw BookPlayerError.emptyResponse + } - if try keychainService.get(.token) != nil { - /// User is signed in, pull data from DB - return try await getPhoneLastPlayedItem() + return item } else { guard let watchContextFileURL = FileManager.default.containerURL( diff --git a/BookPlayerWidgets/Shared/SharedNowPlayingWidget/SharedWidgetContainerView.swift b/BookPlayerWidgets/Shared/SharedNowPlayingWidget/SharedWidgetContainerView.swift index 0f5098ac4..feac58634 100644 --- a/BookPlayerWidgets/Shared/SharedNowPlayingWidget/SharedWidgetContainerView.swift +++ b/BookPlayerWidgets/Shared/SharedNowPlayingWidget/SharedWidgetContainerView.swift @@ -28,18 +28,28 @@ struct SharedWidgetContainerView: View { fillFraction: entry.percentCompleted ) .widgetBackground(backgroundView: Color.clear) - case .accessoryRectangular, .accessoryInline: + case .accessoryRectangular: RectangularView( chapterTitle: entry.chapterTitle, bookTitle: entry.bookTitle, - details: entry.details + details: entry.details, + includeLogo: true + ) + .widgetBackground(backgroundView: Color.clear) + case .accessoryInline: + RectangularView( + chapterTitle: entry.chapterTitle, + bookTitle: entry.bookTitle, + details: entry.details, + includeLogo: false ) .widgetBackground(backgroundView: Color.clear) default: RectangularView( chapterTitle: entry.chapterTitle, bookTitle: entry.bookTitle, - details: entry.details + details: entry.details, + includeLogo: true ) .widgetBackground(backgroundView: Color.clear) } diff --git a/Shared/Services/Account/AccountService.swift b/Shared/Services/Account/AccountService.swift index b971ab630..43bd9fbb3 100644 --- a/Shared/Services/Account/AccountService.swift +++ b/Shared/Services/Account/AccountService.swift @@ -273,7 +273,7 @@ public final class AccountService: AccountServiceProtocol { try self.keychain.set(token, key: .token) _ = try await Purchases.shared.logIn(userId) - UserDefaults.standard.set(userId, forKey: "rcUserId") + UserDefaults.sharedDefaults.set(userId, forKey: "rcUserId") } public func login( @@ -285,7 +285,7 @@ public final class AccountService: AccountServiceProtocol { try self.keychain.set(response.token, key: .token) let (customerInfo, _) = try await Purchases.shared.logIn(userId) - UserDefaults.standard.set(userId, forKey: "rcUserId") + UserDefaults.sharedDefaults.set(userId, forKey: "rcUserId") if let existingAccount = self.getAccount() { // Preserve donation made flag from stored account @@ -329,7 +329,7 @@ public final class AccountService: AccountServiceProtocol { ) Purchases.shared.logOut { _, _ in } - UserDefaults.standard.removeObject(forKey: "rcUserId") + UserDefaults.sharedDefaults.removeObject(forKey: "rcUserId") NotificationCenter.default.post(name: .logout, object: self) } From e108a4760f82c37561bde37b07546aac18ebf941 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Mon, 2 Dec 2024 12:04:03 -0500 Subject: [PATCH 11/31] Fix updating last played item --- .../RemoteItemList/RemoteItemCellViewModel.swift | 4 ++-- .../RemoteItemList/RemoteItemListCellView.swift | 6 +----- .../RemoteItemList/RemoteItemListView.swift | 11 ++++++----- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemCellViewModel.swift b/BookPlayerWatch/RemoteItemList/RemoteItemCellViewModel.swift index b405c92ed..ee169118e 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemCellViewModel.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemCellViewModel.swift @@ -11,9 +11,9 @@ import Combine import Foundation class RemoteItemCellViewModel: ObservableObject { - let item: SimpleLibraryItem - let coreServices: CoreServices + @Published var item: SimpleLibraryItem @Published var downloadState: DownloadState + let coreServices: CoreServices private var disposeBag = Set() diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift index a2a00621d..3ef93a490 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift @@ -10,13 +10,9 @@ import BookPlayerWatchKit import SwiftUI struct RemoteItemListCellView: View { - @StateObject var model: RemoteItemCellViewModel + @ObservedObject var model: RemoteItemCellViewModel @State private var error: Error? - init(item: SimpleLibraryItem, coreServices: CoreServices) { - self._model = .init(wrappedValue: .init(item: item, coreServices: coreServices)) - } - var percentCompleted: String { guard model.item.progress > 0 else { return "" } diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift index 400dda574..0dc9d25a6 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -82,14 +82,14 @@ struct RemoteItemListView: View { if folderRelativePath == nil { Section { if let lastPlayedItem { - RemoteItemListCellView(item: lastPlayedItem, coreServices: coreServices) + RemoteItemListCellView(model: .init(item: lastPlayedItem, coreServices: coreServices)) .onTapGesture { Task { do { isLoading = true try await coreServices.playerLoaderService.loadPlayer(lastPlayedItem.relativePath, autoplay: true) - isLoading = false showPlayer = true + isLoading = false } catch { isLoading = false self.error = error @@ -112,18 +112,19 @@ struct RemoteItemListView: View { folderRelativePath: item.relativePath ) } label: { - RemoteItemListCellView(item: item, coreServices: coreServices) + RemoteItemListCellView(model: .init(item: item, coreServices: coreServices)) .foregroundColor(getForegroundColor(for: item)) } } else { - RemoteItemListCellView(item: item, coreServices: coreServices) + RemoteItemListCellView(model: .init(item: item, coreServices: coreServices)) .onTapGesture { Task { do { isLoading = true try await coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) - isLoading = false showPlayer = true + isLoading = false + lastPlayedItem = item } catch { isLoading = false self.error = error From 3f76d3c86680815f7b5b5e014586c5cf5cee9fe0 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Tue, 3 Dec 2024 14:07:43 -0500 Subject: [PATCH 12/31] Add custom pull to refresh --- BookPlayer.xcodeproj/project.pbxproj | 4 + BookPlayerWatch/RefreshableScrollView.swift | 108 +++++++++++ .../RemoteItemList/RemoteItemListView.swift | 182 ++++++++++-------- 3 files changed, 216 insertions(+), 78 deletions(-) create mode 100644 BookPlayerWatch/RefreshableScrollView.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index 18933f0ac..efcffa7a4 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -343,6 +343,7 @@ 6334CF1B2CF8D87500F1FA17 /* SkipDurationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF1A2CF8D87500F1FA17 /* SkipDurationListView.swift */; }; 6334CF1D2CF90AF900F1FA17 /* PlayerMoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF1C2CF90AF900F1FA17 /* PlayerMoreListView.swift */; }; 6334CF1F2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF1E2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift */; }; + 6334CF212CFE330300F1FA17 /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF202CFE330300F1FA17 /* RefreshableScrollView.swift */; }; 633BE3E52AE6102D00F983AC /* BPShortcutsLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 633BE3E42AE6102D00F983AC /* BPShortcutsLink.swift */; }; 6340D5692AF12D48003D0B09 /* SharedWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6340D5682AF12D48003D0B09 /* SharedWidget.swift */; }; 6340D56E2AF13E4C003D0B09 /* RectangularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6340D56D2AF13E4C003D0B09 /* RectangularView.swift */; }; @@ -1137,6 +1138,7 @@ 6334CF1A2CF8D87500F1FA17 /* SkipDurationListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkipDurationListView.swift; sourceTree = ""; }; 6334CF1C2CF90AF900F1FA17 /* PlayerMoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerMoreListView.swift; sourceTree = ""; }; 6334CF1E2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteItemCellViewModel.swift; sourceTree = ""; }; + 6334CF202CFE330300F1FA17 /* RefreshableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = ""; }; 633BE3E12AE43D1300F983AC /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/AppShortcuts.strings"; sourceTree = ""; }; 633BE3E42AE6102D00F983AC /* BPShortcutsLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPShortcutsLink.swift; sourceTree = ""; }; 6340D5682AF12D48003D0B09 /* SharedWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedWidget.swift; sourceTree = ""; }; @@ -1640,6 +1642,7 @@ 6350E4752CF4F6E90077CDC1 /* PlaybackFullControlsView.swift */, 6334CF1A2CF8D87500F1FA17 /* SkipDurationListView.swift */, 6334CF1C2CF90AF900F1FA17 /* PlayerMoreListView.swift */, + 6334CF202CFE330300F1FA17 /* RefreshableScrollView.swift */, 63CD851B2CE2963600EDBEA8 /* Settings */, 6399D06E2CEBA1F900A2E278 /* RemoteItemList */, 9FA334B427C156DB0064E8EA /* ItemList */, @@ -3412,6 +3415,7 @@ files = ( 9F82DF6927DE93A2001B0EA8 /* SkipIntervalView.swift in Sources */, 6399D0722CEBA37C00A2E278 /* RemoteItemListCellView.swift in Sources */, + 6334CF212CFE330300F1FA17 /* RefreshableScrollView.swift in Sources */, 9FA334B627C15DE30064E8EA /* VolumeView.swift in Sources */, 6350E46D2CF4315B0077CDC1 /* PlayerLoaderService.swift in Sources */, 6350E46B2CF42B500077CDC1 /* UserActivityManager.swift in Sources */, diff --git a/BookPlayerWatch/RefreshableScrollView.swift b/BookPlayerWatch/RefreshableScrollView.swift new file mode 100644 index 000000000..742b9e1fb --- /dev/null +++ b/BookPlayerWatch/RefreshableScrollView.swift @@ -0,0 +1,108 @@ +// +// RefreshableScrollView.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 2/12/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import SwiftUI + +/// Pull to refresh does not work natively on WatchOS +/// This implementation is inspired from this code: +/// https://gist.github.com/swiftui-lab/3de557a513fbdb2d8fced41e40347e01 +struct RefreshableScrollView: View { + @State private var previousScrollOffset: CGFloat = 0 + @State private var scrollOffset: CGFloat = 0 + @Binding var refreshing: Bool + + let threshold: CGFloat = 40 + let content: Content + + init(refreshing: Binding, @ViewBuilder content: () -> Content) { + self._refreshing = refreshing + self.content = content() + + } + + var body: some View { + ScrollView { + ZStack(alignment: .top) { + MovingView() + + content + .safeAreaInset(edge: .top) { + if refreshing { + ProgressView() + .tint(.white) + .background(Color.black.opacity(0.8)) + .frame(height: 10) + } + } + } + } + .background(FixedView()) + .onPreferenceChange(RefreshableKeyTypes.PrefKey.self) { values in + refreshLogic(values: values) + } + } + + @MainActor + func refreshLogic(values: [RefreshableKeyTypes.PrefData]) { + // Calculate scroll offset + let movingBounds = values.first { $0.vType == .movingView }?.bounds ?? .zero + let fixedBounds = values.first { $0.vType == .fixedView }?.bounds ?? .zero + + self.scrollOffset = movingBounds.minY - fixedBounds.minY + + // Crossing the threshold on the way down, we start the refresh process + if !self.refreshing && (self.scrollOffset > self.threshold && self.previousScrollOffset <= self.threshold) { + self.refreshing = true + } + + // Update last scroll offset + self.previousScrollOffset = self.scrollOffset + } + + struct MovingView: View { + var body: some View { + GeometryReader { proxy in + Color.clear.preference( + key: RefreshableKeyTypes.PrefKey.self, + value: [RefreshableKeyTypes.PrefData(vType: .movingView, bounds: proxy.frame(in: .global))] + ) + }.frame(height: 0) + } + } + + struct FixedView: View { + var body: some View { + GeometryReader { proxy in + Color.clear.preference( + key: RefreshableKeyTypes.PrefKey.self, + value: [RefreshableKeyTypes.PrefData(vType: .fixedView, bounds: proxy.frame(in: .global))] + ) + } + } + } +} + +struct RefreshableKeyTypes { + enum ViewType: Int { + case movingView + case fixedView + } + + struct PrefData: Equatable { + let vType: ViewType + let bounds: CGRect + } + + struct PrefKey: PreferenceKey { + static var defaultValue: [PrefData] = [] + + static func reduce(value: inout [PrefData], nextValue: () -> [PrefData]) { + value.append(contentsOf: nextValue()) + } + } +} diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift index 0dc9d25a6..db9ae325a 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -17,6 +17,8 @@ struct RemoteItemListView: View { @State private var isLoading = false @State private var error: Error? @State var showPlayer = false + @State var isRefreshing: Bool = false + @State var isFirstLoad = true let folderRelativePath: String? @@ -45,6 +47,37 @@ struct RemoteItemListView: View { } } + func syncListContents(ignoreLastTimestamp: Bool) async { + guard + await coreServices.syncService.canSyncListContents( + at: folderRelativePath, + ignoreLastTimestamp: ignoreLastTimestamp + ) + else { return } + + do { + try await coreServices.syncService.syncListContents(at: folderRelativePath) + } catch BPSyncError.differentLastBook(let relativePath), BPSyncError.reloadLastBook(let relativePath) { + await coreServices.syncService.setLibraryLastBook(with: relativePath) + } catch { + self.error = error + } + + items = + coreServices.libraryService.fetchContents( + at: folderRelativePath, + limit: nil, + offset: nil + ) ?? [] + + lastPlayedItem = coreServices.libraryService.getLastPlayedItems(limit: 1)?.first + if let lastPlayedItem { + playingItemParentPath = getPathForParentOfItem(currentPlayingPath: lastPlayedItem.relativePath) + } else { + playingItemParentPath = nil + } + } + func getForegroundColor(for item: SimpleLibraryItem) -> Color { guard let lastPlayedItem else { return .primary } @@ -78,66 +111,75 @@ struct RemoteItemListView: View { } var body: some View { - List { - if folderRelativePath == nil { - Section { - if let lastPlayedItem { - RemoteItemListCellView(model: .init(item: lastPlayedItem, coreServices: coreServices)) - .onTapGesture { - Task { - do { - isLoading = true - try await coreServices.playerLoaderService.loadPlayer(lastPlayedItem.relativePath, autoplay: true) - showPlayer = true - isLoading = false - } catch { - isLoading = false - self.error = error + GeometryReader { geometry in + RefreshableScrollView(refreshing: $isRefreshing) { + List { + if folderRelativePath == nil { + Section { + if let lastPlayedItem { + RemoteItemListCellView(model: .init(item: lastPlayedItem, coreServices: coreServices)) + .onTapGesture { + Task { + do { + isLoading = true + try await coreServices.playerLoaderService.loadPlayer( + lastPlayedItem.relativePath, + autoplay: true + ) + showPlayer = true + isLoading = false + } catch { + isLoading = false + self.error = error + } + } } - } } + } header: { + Text(verbatim: "watchapp_last_played_title".localized) + .foregroundStyle(Color.accentColor) + } } - } header: { - Text(verbatim: "watchapp_last_played_title".localized) - .foregroundStyle(Color.accentColor) - } - } - Section { - ForEach(items) { item in - if item.type == .folder { - NavigationLink { - RemoteItemListView( - coreServices: coreServices, - folderRelativePath: item.relativePath - ) - } label: { - RemoteItemListCellView(model: .init(item: item, coreServices: coreServices)) - .foregroundColor(getForegroundColor(for: item)) - } - } else { - RemoteItemListCellView(model: .init(item: item, coreServices: coreServices)) - .onTapGesture { - Task { - do { - isLoading = true - try await coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) - showPlayer = true - isLoading = false - lastPlayedItem = item - } catch { - isLoading = false - self.error = error - } + Section { + ForEach(items) { item in + if item.type == .folder { + NavigationLink { + RemoteItemListView( + coreServices: coreServices, + folderRelativePath: item.relativePath + ) + } label: { + RemoteItemListCellView(model: .init(item: item, coreServices: coreServices)) + .foregroundColor(getForegroundColor(for: item)) } + } else { + RemoteItemListCellView(model: .init(item: item, coreServices: coreServices)) + .onTapGesture { + Task { + do { + isLoading = true + try await coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) + showPlayer = true + isLoading = false + lastPlayedItem = item + } catch { + isLoading = false + self.error = error + } + } + } } + } + } header: { + Text(verbatim: folderRelativePath?.components(separatedBy: "/").last ?? "library_title".localized) + .foregroundStyle(Color.accentColor) } } - } header: { - Text(verbatim: folderRelativePath?.components(separatedBy: "/").last ?? "library_title".localized) - .foregroundStyle(Color.accentColor) + .frame(minWidth: geometry.size.width, minHeight: geometry.size.height) } } + .ignoresSafeArea(edges: [.bottom]) .background( NavigationLink(destination: RemotePlayerView(playerManager: coreServices.playerManager), isActive: $showPlayer) { EmptyView() @@ -160,36 +202,20 @@ struct RemoteItemListView: View { } } } - .onAppear { + .onChange(of: isRefreshing) { newValue in + guard newValue else { return } + Task { - guard - await coreServices.syncService.canSyncListContents( - at: folderRelativePath, - ignoreLastTimestamp: false - ) - else { return } - - do { - try await coreServices.syncService.syncListContents(at: folderRelativePath) - } catch BPSyncError.differentLastBook(let relativePath), BPSyncError.reloadLastBook(let relativePath) { - await coreServices.syncService.setLibraryLastBook(with: relativePath) - } catch { - self.error = error - } + await syncListContents(ignoreLastTimestamp: true) + isRefreshing = false + } + } + .onAppear { + guard isFirstLoad else { return } + isFirstLoad = false - items = - coreServices.libraryService.fetchContents( - at: folderRelativePath, - limit: nil, - offset: nil - ) ?? [] - - lastPlayedItem = coreServices.libraryService.getLastPlayedItems(limit: 1)?.first - if let lastPlayedItem { - playingItemParentPath = getPathForParentOfItem(currentPlayingPath: lastPlayedItem.relativePath) - } else { - playingItemParentPath = nil - } + Task { + await syncListContents(ignoreLastTimestamp: false) } } } From 90cb3eadf58fb4f427dabcd5f402fcbe3c5f566a Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Wed, 4 Dec 2024 23:25:48 -0500 Subject: [PATCH 13/31] Add progress bar to player --- BookPlayerWatch/PlayerManager.swift | 1 + BookPlayerWatch/PlayerToolbarView.swift | 12 +++++++----- .../RemoteItemList/RemoteItemListCellView.swift | 8 +++++--- .../RemoteItemList/RemoteItemListView.swift | 9 +++++++++ BookPlayerWatch/RemotePlayerView.swift | 11 +++++++++++ 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/BookPlayerWatch/PlayerManager.swift b/BookPlayerWatch/PlayerManager.swift index 3f84d7e0d..7617868fc 100644 --- a/BookPlayerWatch/PlayerManager.swift +++ b/BookPlayerWatch/PlayerManager.swift @@ -453,6 +453,7 @@ final class PlayerManager: NSObject, PlayerManagerProtocol, ObservableObject { } NotificationCenter.default.post(name: .bookPlaying, object: nil, userInfo: nil) + objectWillChange.send() } // MARK: - Player states diff --git a/BookPlayerWatch/PlayerToolbarView.swift b/BookPlayerWatch/PlayerToolbarView.swift index 81c759515..2b20b494b 100644 --- a/BookPlayerWatch/PlayerToolbarView.swift +++ b/BookPlayerWatch/PlayerToolbarView.swift @@ -77,11 +77,13 @@ struct PlayerToolbarView: View { Spacer() - ResizeableImageView(name: "ellipsis.circle") - .padding(14) - .onTapGesture { - isShowingMoreList = true - } + Button { + isShowingMoreList = true + } label: { + ResizeableImageView(name: "list.bullet") + .padding(14) + } + .buttonStyle(PlainButtonStyle()) Spacer() } diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift index 3ef93a490..31838f2ce 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift @@ -34,7 +34,7 @@ struct RemoteItemListCellView: View { .lineLimit(1) switch model.downloadState { case .downloading(let progress): - LinearProgressView(value: progress) + LinearProgressView(value: progress, fillColor: .white) .frame(maxWidth: 100, maxHeight: 10) case .downloaded: Text(Image(systemName: "applewatch")) @@ -101,12 +101,13 @@ struct RemoteItemListCellView: View { struct LinearProgressView: View { var value: Double var shape: Shape + var fillColor: Color var body: some View { shape.fill(.secondary) .overlay(alignment: .leading) { GeometryReader { proxy in - shape.fill(.white) + shape.fill(fillColor) .frame(width: proxy.size.width * value) } } @@ -115,8 +116,9 @@ struct LinearProgressView: View { } extension LinearProgressView where Shape == Capsule { - init(value: Double, shape: Shape = Capsule()) { + init(value: Double, fillColor: Color, shape: Shape = Capsule()) { self.value = value self.shape = shape + self.fillColor = fillColor } } diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift index db9ae325a..7990e1814 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -10,6 +10,7 @@ import BookPlayerWatchKit import SwiftUI struct RemoteItemListView: View { + @Environment(\.scenePhase) var scenePhase @ObservedObject var coreServices: CoreServices @State var items: [SimpleLibraryItem] @State var lastPlayedItem: SimpleLibraryItem? @@ -210,6 +211,14 @@ struct RemoteItemListView: View { isRefreshing = false } } + .onChange(of: scenePhase) { newPhase in + guard + newPhase == .active, + coreServices.playerManager.isPlaying + else { return } + + showPlayer = true + } .onAppear { guard isFirstLoad else { return } isFirstLoad = false diff --git a/BookPlayerWatch/RemotePlayerView.swift b/BookPlayerWatch/RemotePlayerView.swift index c926830ba..e4481caaa 100644 --- a/BookPlayerWatch/RemotePlayerView.swift +++ b/BookPlayerWatch/RemotePlayerView.swift @@ -6,6 +6,7 @@ // Copyright © 2024 Tortuga Power. All rights reserved. // +import BookPlayerWatchKit import SwiftUI struct RemotePlayerView: View { @@ -13,6 +14,15 @@ struct RemotePlayerView: View { var body: some View { VStack { + + if let currentItem = playerManager.currentItem { + LinearProgressView( + value: currentItem.currentTime / currentItem.duration, + fillColor: .accentColor + ) + .frame(maxHeight: 3) + } + NowPlayingTitleView( item: $playerManager.currentItem ) @@ -27,5 +37,6 @@ struct RemotePlayerView: View { } .fixedSize(horizontal: false, vertical: false) .ignoresSafeArea(edges: .bottom) + .navigationTitle(TimeParser.formatTotalDuration(playerManager.currentItem?.maxTimeInContext(prefersChapterContext: false, prefersRemainingTime: true, at: 1.0) ?? 0)) } } From 7d50997f52aabdb3ad821d03b8c0fe39b1bbf6dc Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Wed, 4 Dec 2024 23:50:59 -0500 Subject: [PATCH 14/31] Fix autoplay and playing again finished items --- BookPlayerWatch/PlayerManager.swift | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/BookPlayerWatch/PlayerManager.swift b/BookPlayerWatch/PlayerManager.swift index 7617868fc..acf17f48e 100644 --- a/BookPlayerWatch/PlayerManager.swift +++ b/BookPlayerWatch/PlayerManager.swift @@ -755,7 +755,17 @@ extension PlayerManager { // If book is completed, stop let playerTime = CMTimeGetSeconds(audioPlayer.currentTime()) - if playerTime.isFinite && Int(currentItem.duration) == Int(playerTime) { return } + if playerTime.isFinite && Int(currentItem.duration) == Int(playerTime) { + /// if it was manually selected, restart book + if !autoPlayed { + updatePlaybackTime(item: currentItem, time: 0) + let firstChapter = currentItem.chapters.first! + currentItem.currentChapter = firstChapter + loadChapterMetadata(firstChapter, autoplay: true) + } else { + return + } + } handleSmartRewind(currentItem) @@ -988,12 +998,12 @@ extension PlayerManager { } func playNextItem(autoPlayed: Bool = false, shouldAutoplay: Bool = true) { - /// If it's autoplayed, check if setting is enabled - if autoPlayed, - !UserDefaults.standard.bool(forKey: Constants.UserDefaults.autoplayEnabled) - { - return - } + /// If it's autoplayed, check if setting is enabled (disabled on watch app for the time being) + // if autoPlayed, + // !UserDefaults.standard.bool(forKey: Constants.UserDefaults.autoplayEnabled) + // { + // return + // } let restartFinished = UserDefaults.standard.bool(forKey: Constants.UserDefaults.autoplayRestartEnabled) From 37ec9e7be64fbd3d3138e6d2b39205589a849ab3 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Thu, 5 Dec 2024 11:28:58 -0500 Subject: [PATCH 15/31] Add error feedback from player manager --- BookPlayer/Player/PlayerLoaderService.swift | 4 +-- BookPlayerWatch/PlayerManager.swift | 32 +++++++++---------- .../RemoteItemList/RemoteItemListView.swift | 10 +++--- BookPlayerWatch/RemotePlayerView.swift | 1 + 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/BookPlayer/Player/PlayerLoaderService.swift b/BookPlayer/Player/PlayerLoaderService.swift index 45bf5d91d..0fda6c5d7 100644 --- a/BookPlayer/Player/PlayerLoaderService.swift +++ b/BookPlayer/Player/PlayerLoaderService.swift @@ -72,9 +72,7 @@ final class PlayerLoaderService: @unchecked Sendable { playerManager.load(item, autoplay: autoplay) if recordAsLastBook { - await MainActor.run { - libraryService.setLibraryLastBook(with: item.relativePath) - } + libraryService.setLibraryLastBook(with: item.relativePath) } } } diff --git a/BookPlayerWatch/PlayerManager.swift b/BookPlayerWatch/PlayerManager.swift index acf17f48e..d04dc94e2 100644 --- a/BookPlayerWatch/PlayerManager.swift +++ b/BookPlayerWatch/PlayerManager.swift @@ -62,6 +62,7 @@ final class PlayerManager: NSObject, PlayerManagerProtocol, ObservableObject { private let decoder = JSONDecoder() @Published var currentItem: PlayableItem? @Published var currentSpeed: Float = 1.0 + @Published var error: Error? var nowPlayingInfo = [String: Any]() @@ -251,9 +252,12 @@ final class PlayerManager: NSObject, PlayerManagerProtocol, ObservableObject { } func load(_ item: PlayableItem, autoplay: Bool) { - load(item, autoplay: autoplay, forceRefreshURL: false) + Task { @MainActor in + load(item, autoplay: autoplay, forceRefreshURL: false) + } } + @MainActor private func load(_ item: PlayableItem, autoplay: Bool, forceRefreshURL: Bool) { /// Cancel in case there's an ongoing load task playTask?.cancel() @@ -311,7 +315,7 @@ final class PlayerManager: NSObject, PlayerManagerProtocol, ObservableObject { playbackQueued = autoplay } - loadChapterTask = Task { [unowned self] in + loadChapterTask = Task { @MainActor [unowned self] in do { try await self.loadPlayerItem(for: chapter, forceRefreshURL: forceRefreshURL) self.loadChapterOperation(chapter) @@ -321,7 +325,7 @@ final class PlayerManager: NSObject, PlayerManagerProtocol, ObservableObject { self.playbackQueued = nil self.isFetchingRemoteURL = nil self.observeStatus = false - self.showErrorAlert(title: "\("error_title".localized) Metadata", error.localizedDescription) + self.showError(error) return } } @@ -744,7 +748,7 @@ extension PlayerManager { ) try await audioSession.activate() } catch { - showErrorAlert(title: "error_title".localized, error.localizedDescription) + showError(error) } createOrUpdateAutomaticBookmark( @@ -879,9 +883,9 @@ extension PlayerManager { Additional Info \(nsError.userInfo) """ - showErrorAlert(title: "\("error_title".localized) \(nsError.code)", errorDescription) - } else { - showErrorAlert(title: "error_title".localized, item.error?.localizedDescription) + showError(nsError) + } else if let itemError = item.error { + showError(itemError) } } @@ -1144,7 +1148,9 @@ extension PlayerManager { } private func loadAndRefreshURL(item: PlayableItem) { - load(item, autoplay: playbackQueued == true, forceRefreshURL: true) + Task { @MainActor in + load(item, autoplay: playbackQueued == true, forceRefreshURL: true) + } } @objc @@ -1208,14 +1214,8 @@ extension PlayerManager { } extension PlayerManager { - private func showErrorAlert(title: String, _ message: String?) { - print("=== error: \(message)") -// DispatchQueue.main.async { -// AppDelegate.shared?.activeSceneDelegate? -// .startingNavigationController -// .getTopVisibleViewController()? -// .showAlert(title, message: message) -// } + func showError(_ error: Error) { + self.error = error } } diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift index 7990e1814..15a414ffb 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -29,11 +29,11 @@ struct RemoteItemListView: View { ) { self.coreServices = coreServices let fetchedItems = - coreServices.libraryService.fetchContents( - at: folderRelativePath, - limit: nil, - offset: nil - ) ?? [] + coreServices.libraryService.fetchContents( + at: folderRelativePath, + limit: nil, + offset: nil + ) ?? [] self._items = .init(initialValue: fetchedItems) let lastItem = coreServices.libraryService.getLastPlayedItems(limit: 1)?.first self._lastPlayedItem = .init(initialValue: lastItem) diff --git a/BookPlayerWatch/RemotePlayerView.swift b/BookPlayerWatch/RemotePlayerView.swift index e4481caaa..2a3b68322 100644 --- a/BookPlayerWatch/RemotePlayerView.swift +++ b/BookPlayerWatch/RemotePlayerView.swift @@ -38,5 +38,6 @@ struct RemotePlayerView: View { .fixedSize(horizontal: false, vertical: false) .ignoresSafeArea(edges: .bottom) .navigationTitle(TimeParser.formatTotalDuration(playerManager.currentItem?.maxTimeInContext(prefersChapterContext: false, prefersRemainingTime: true, at: 1.0) ?? 0)) + .errorAlert(error: $playerManager.error) } } From 5b687c4cce6113e3672528e336d55ccaa8b52214 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Thu, 5 Dec 2024 11:49:44 -0500 Subject: [PATCH 16/31] Add autoplay setting --- .../Base.lproj/Localizable.strings | 1 + .../PlaybackFullControlsView.swift | 29 +++++++++++-------- BookPlayerWatch/PlayerManager.swift | 18 +++++++----- BookPlayerWatch/ar.lproj/Localizable.strings | 1 + BookPlayerWatch/cs.lproj/Localizable.strings | 1 + BookPlayerWatch/da.lproj/Localizable.strings | 1 + BookPlayerWatch/de.lproj/Localizable.strings | 1 + BookPlayerWatch/el.lproj/Localizable.strings | 1 + BookPlayerWatch/en.lproj/Localizable.strings | 1 + BookPlayerWatch/es.lproj/Localizable.strings | 1 + BookPlayerWatch/fi.lproj/Localizable.strings | 1 + BookPlayerWatch/fr.lproj/Localizable.strings | 1 + BookPlayerWatch/hu.lproj/Localizable.strings | 1 + BookPlayerWatch/it.lproj/Localizable.strings | 1 + BookPlayerWatch/nb.lproj/Localizable.strings | 1 + BookPlayerWatch/nl.lproj/Localizable.strings | 1 + BookPlayerWatch/pl.lproj/Localizable.strings | 1 + .../pt-BR.lproj/Localizable.strings | 1 + .../pt-PT.lproj/Localizable.strings | 1 + BookPlayerWatch/ro.lproj/Localizable.strings | 1 + BookPlayerWatch/ru.lproj/Localizable.strings | 1 + .../sk-SK.lproj/Localizable.strings | 1 + BookPlayerWatch/sv.lproj/Localizable.strings | 1 + BookPlayerWatch/tr.lproj/Localizable.strings | 1 + BookPlayerWatch/uk.lproj/Localizable.strings | 1 + .../zh-Hans.lproj/Localizable.strings | 1 + 26 files changed, 51 insertions(+), 20 deletions(-) diff --git a/BookPlayerWatch/Base.lproj/Localizable.strings b/BookPlayerWatch/Base.lproj/Localizable.strings index dd92665ac..0e1b79c43 100644 --- a/BookPlayerWatch/Base.lproj/Localizable.strings +++ b/BookPlayerWatch/Base.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "SKIP INTERVALS"; "settings_skip_rewind_title" = "Rewind"; "settings_skip_forward_title" = "Forward"; +"settings_autoplay_section_title" = "AUTOPLAY"; "speed_title" = "speed"; "logout_title" = "Log out"; diff --git a/BookPlayerWatch/PlaybackFullControlsView.swift b/BookPlayerWatch/PlaybackFullControlsView.swift index 473066197..e5fd52a25 100644 --- a/BookPlayerWatch/PlaybackFullControlsView.swift +++ b/BookPlayerWatch/PlaybackFullControlsView.swift @@ -11,24 +11,13 @@ import SwiftUI struct PlaybackFullControlsView: View { @ObservedObject var model: PlaybackFullControlsViewModel + @AppStorage(Constants.UserDefaults.autoplayEnabled) var autoplayEnabled: Bool = true @AppStorage(Constants.UserDefaults.rewindInterval) var rewindInterval: TimeInterval = 30 @AppStorage(Constants.UserDefaults.forwardInterval) var forwardInterval: TimeInterval = 30 var body: some View { GeometryReader { metrics in List { - Section { - Toggle( - "settings_boostvolume_title", - isOn: .init( - get: { model.boostVolume }, - set: { _ in - model.handleBoostVolumeToggle() - } - ) - ) - } - Section("speed".localized.uppercased()) { VStack { HStack { @@ -69,6 +58,22 @@ struct PlaybackFullControlsView: View { } .listRowBackground(Color.clear) + Section { + Toggle( + "settings_boostvolume_title", + isOn: .init( + get: { model.boostVolume }, + set: { _ in + model.handleBoostVolumeToggle() + } + ) + ) + Toggle( + "settings_autoplay_section_title".localized.capitalized, + isOn: $autoplayEnabled + ) + } + Section("settings_skip_title") { NavigationLink { SkipDurationListView(skipDirection: .back) diff --git a/BookPlayerWatch/PlayerManager.swift b/BookPlayerWatch/PlayerManager.swift index d04dc94e2..e0a254665 100644 --- a/BookPlayerWatch/PlayerManager.swift +++ b/BookPlayerWatch/PlayerManager.swift @@ -1002,14 +1002,16 @@ extension PlayerManager { } func playNextItem(autoPlayed: Bool = false, shouldAutoplay: Bool = true) { - /// If it's autoplayed, check if setting is enabled (disabled on watch app for the time being) - // if autoPlayed, - // !UserDefaults.standard.bool(forKey: Constants.UserDefaults.autoplayEnabled) - // { - // return - // } - - let restartFinished = UserDefaults.standard.bool(forKey: Constants.UserDefaults.autoplayRestartEnabled) + /// If it's autoplayed, check if setting is enabled + if autoPlayed, + UserDefaults.standard.object(forKey: Constants.UserDefaults.autoplayEnabled) != nil, + !UserDefaults.standard.bool(forKey: Constants.UserDefaults.autoplayEnabled) + { + return + } + + /// Always true for watch app for the moment + let restartFinished = true // UserDefaults.standard.bool(forKey: Constants.UserDefaults.autoplayRestartEnabled) guard let currentItem = self.currentItem, diff --git a/BookPlayerWatch/ar.lproj/Localizable.strings b/BookPlayerWatch/ar.lproj/Localizable.strings index e549bfde5..71d14b603 100644 --- a/BookPlayerWatch/ar.lproj/Localizable.strings +++ b/BookPlayerWatch/ar.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "القفزات الزمنية"; "settings_skip_rewind_title" = "التأخير"; "settings_skip_forward_title" = "التقديم"; +"settings_autoplay_section_title" = "التشغيل التلقائي"; "speed_title" = "السرعة"; "logout_title" = "تسجيل خروج"; diff --git a/BookPlayerWatch/cs.lproj/Localizable.strings b/BookPlayerWatch/cs.lproj/Localizable.strings index 594a2661d..c7878731d 100644 --- a/BookPlayerWatch/cs.lproj/Localizable.strings +++ b/BookPlayerWatch/cs.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "Intervaly přetáčení"; "settings_skip_rewind_title" = "Vrátit"; "settings_skip_forward_title" = "Přetočit"; +"settings_autoplay_section_title" = "AUTOMATICKÉ PŘEHRÁVÁNÍ"; "speed_title" = "Rychlost"; "logout_title" = "Odhlásit se"; diff --git a/BookPlayerWatch/da.lproj/Localizable.strings b/BookPlayerWatch/da.lproj/Localizable.strings index 2cf885226..d0754e841 100644 --- a/BookPlayerWatch/da.lproj/Localizable.strings +++ b/BookPlayerWatch/da.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "SPRING OVER-INTERVALLER"; "settings_skip_rewind_title" = "Spol tilbage"; "settings_skip_forward_title" = "Spol frem"; +"settings_autoplay_section_title" = "AUTOMATISK AFSPILNING"; "speed_title" = "Hastighed"; "logout_title" = "Log ud"; diff --git a/BookPlayerWatch/de.lproj/Localizable.strings b/BookPlayerWatch/de.lproj/Localizable.strings index 1631e4b0f..e26851cd2 100644 --- a/BookPlayerWatch/de.lproj/Localizable.strings +++ b/BookPlayerWatch/de.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "Tasten „Überspringen“"; "settings_skip_rewind_title" = "Rückwärts"; "settings_skip_forward_title" = "Vorwärts"; +"settings_autoplay_section_title" = "AUTOMATISCHES ABSPIELEN"; "speed_title" = "Wiedergabegeschwindigkeit"; "logout_title" = "Ausloggen"; diff --git a/BookPlayerWatch/el.lproj/Localizable.strings b/BookPlayerWatch/el.lproj/Localizable.strings index a42627010..901ee8f4a 100644 --- a/BookPlayerWatch/el.lproj/Localizable.strings +++ b/BookPlayerWatch/el.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "ΔΙΑΣΤΗΜΑΤΑ ΠΑΡΑΛΕΙΨΗΣ"; "settings_skip_rewind_title" = "Πίσω"; "settings_skip_forward_title" = "Προς τα εμπρός"; +"settings_autoplay_section_title" = "ΑΥΤΟΜΑΤΗ ΑΝΑΠΑΡΑΓΩΓΗ"; "speed_title" = "Ταχύτητα"; "logout_title" = "Αποσύνδεση"; diff --git a/BookPlayerWatch/en.lproj/Localizable.strings b/BookPlayerWatch/en.lproj/Localizable.strings index dd92665ac..0e1b79c43 100644 --- a/BookPlayerWatch/en.lproj/Localizable.strings +++ b/BookPlayerWatch/en.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "SKIP INTERVALS"; "settings_skip_rewind_title" = "Rewind"; "settings_skip_forward_title" = "Forward"; +"settings_autoplay_section_title" = "AUTOPLAY"; "speed_title" = "speed"; "logout_title" = "Log out"; diff --git a/BookPlayerWatch/es.lproj/Localizable.strings b/BookPlayerWatch/es.lproj/Localizable.strings index e7397e295..79e6dbf17 100644 --- a/BookPlayerWatch/es.lproj/Localizable.strings +++ b/BookPlayerWatch/es.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "INTERVALOS DE SALTO"; "settings_skip_rewind_title" = "Retroceder"; "settings_skip_forward_title" = "Adelante"; +"settings_autoplay_section_title" = "AUTO-REPRODUCCIÓN"; "speed_title" = "velocidad"; "logout_title" = "Cerrar sesión"; diff --git a/BookPlayerWatch/fi.lproj/Localizable.strings b/BookPlayerWatch/fi.lproj/Localizable.strings index 18f795a24..0ccc06e08 100644 --- a/BookPlayerWatch/fi.lproj/Localizable.strings +++ b/BookPlayerWatch/fi.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "Skippaus intervallit"; "settings_skip_rewind_title" = "Kelaa taaksepäin"; "settings_skip_forward_title" = "Eteenpäin"; +"settings_autoplay_section_title" = "AUTOMAATTINEN TOISTO"; "speed_title" = "nopeus"; "logout_title" = "Kirjautua ulos"; diff --git a/BookPlayerWatch/fr.lproj/Localizable.strings b/BookPlayerWatch/fr.lproj/Localizable.strings index 1022b33ac..4ff37720f 100644 --- a/BookPlayerWatch/fr.lproj/Localizable.strings +++ b/BookPlayerWatch/fr.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "INTERVALLES DE SAUT"; "settings_skip_rewind_title" = "Rembobiner"; "settings_skip_forward_title" = "Avancer"; +"settings_autoplay_section_title" = "LECTURE AUTOMATIQUE"; "speed_title" = "vitesse"; "logout_title" = "Se déconnecter"; diff --git a/BookPlayerWatch/hu.lproj/Localizable.strings b/BookPlayerWatch/hu.lproj/Localizable.strings index 17db14326..ac1944af8 100644 --- a/BookPlayerWatch/hu.lproj/Localizable.strings +++ b/BookPlayerWatch/hu.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "UGRÁSOK IDŐTARTAMA"; "settings_skip_rewind_title" = "Visszatekerés"; "settings_skip_forward_title" = "Előretekerés"; +"settings_autoplay_section_title" = "AUTOMATIKUS LEJÁTSZÁS"; "speed_title" = "sebesség"; "logout_title" = "Kijelentkezés"; diff --git a/BookPlayerWatch/it.lproj/Localizable.strings b/BookPlayerWatch/it.lproj/Localizable.strings index a17a0a9af..ac765836c 100644 --- a/BookPlayerWatch/it.lproj/Localizable.strings +++ b/BookPlayerWatch/it.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "SALTA INTERVALLI"; "settings_skip_rewind_title" = "Riavvolgi"; "settings_skip_forward_title" = "Avanti"; +"settings_autoplay_section_title" = "RIPRODUZIONE AUTOMATICA"; "speed_title" = "velocità"; "logout_title" = "Disconnettersi"; diff --git a/BookPlayerWatch/nb.lproj/Localizable.strings b/BookPlayerWatch/nb.lproj/Localizable.strings index a576f3cec..3b6c22804 100644 --- a/BookPlayerWatch/nb.lproj/Localizable.strings +++ b/BookPlayerWatch/nb.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "HOPP OVER INTERVALLER"; "settings_skip_rewind_title" = "Spol tilbake"; "settings_skip_forward_title" = "Spol fremover"; +"settings_autoplay_section_title" = "AUTOPLAY"; "speed_title" = "hastighet"; "logout_title" = "Logg ut"; diff --git a/BookPlayerWatch/nl.lproj/Localizable.strings b/BookPlayerWatch/nl.lproj/Localizable.strings index 2dd3eaeac..07dce253a 100644 --- a/BookPlayerWatch/nl.lproj/Localizable.strings +++ b/BookPlayerWatch/nl.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "INTERVAL OVERSLAAN"; "settings_skip_rewind_title" = "Terugspoelen"; "settings_skip_forward_title" = "Naar voren"; +"settings_autoplay_section_title" = "AUTOMATISCH AFSPELEN"; "speed_title" = "snelheid"; "logout_title" = "Uitloggen"; diff --git a/BookPlayerWatch/pl.lproj/Localizable.strings b/BookPlayerWatch/pl.lproj/Localizable.strings index d7cd80723..21a8dcc67 100644 --- a/BookPlayerWatch/pl.lproj/Localizable.strings +++ b/BookPlayerWatch/pl.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "POMIJANIE INTERWAŁÓW"; "settings_skip_rewind_title" = "Cofnij"; "settings_skip_forward_title" = "Do przodu"; +"settings_autoplay_section_title" = "AUTOMATYCZNE ODTWARZANIE"; "speed_title" = "prędkość"; "logout_title" = "Wyloguj"; diff --git a/BookPlayerWatch/pt-BR.lproj/Localizable.strings b/BookPlayerWatch/pt-BR.lproj/Localizable.strings index 9b9c6aa42..73a82d2d8 100644 --- a/BookPlayerWatch/pt-BR.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-BR.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "PULAR INTERVALOS"; "settings_skip_rewind_title" = "Retroceder"; "settings_skip_forward_title" = "Avançar"; +"settings_autoplay_section_title" = "REPRODUÇÃO AUTOMÁTICA"; "speed_title" = "velocidade"; "logout_title" = "Sair"; diff --git a/BookPlayerWatch/pt-PT.lproj/Localizable.strings b/BookPlayerWatch/pt-PT.lproj/Localizable.strings index 6f3434aa7..139f67bee 100644 --- a/BookPlayerWatch/pt-PT.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-PT.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "SALTAR INTERVALOS"; "settings_skip_rewind_title" = "Rebobinar"; "settings_skip_forward_title" = "Avançar"; +"settings_autoplay_section_title" = "REPRODUÇÃO AUTOMÁTICA"; "speed_title" = "velocidade"; "logout_title" = "Sair"; diff --git a/BookPlayerWatch/ro.lproj/Localizable.strings b/BookPlayerWatch/ro.lproj/Localizable.strings index 4fecbb45e..5be2afa0d 100644 --- a/BookPlayerWatch/ro.lproj/Localizable.strings +++ b/BookPlayerWatch/ro.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "SĂRIȚI INTERVALURILE"; "settings_skip_rewind_title" = "Derulare înapoi"; "settings_skip_forward_title" = "Înainte"; +"settings_autoplay_section_title" = "REDARE AUTOMATA"; "speed_title" = "Viteza"; "logout_title" = "Deconectați-vă"; diff --git a/BookPlayerWatch/ru.lproj/Localizable.strings b/BookPlayerWatch/ru.lproj/Localizable.strings index 2d19ab9c0..e5a81f4f6 100644 --- a/BookPlayerWatch/ru.lproj/Localizable.strings +++ b/BookPlayerWatch/ru.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "ШАГ ПЕРЕМОТКИ"; "settings_skip_rewind_title" = "Назад"; "settings_skip_forward_title" = "Вперёд"; +"settings_autoplay_section_title" = "АВТОВОСПРОИЗВЕДЕНИЕ"; "speed_title" = "скорость"; "logout_title" = "Выйти"; diff --git a/BookPlayerWatch/sk-SK.lproj/Localizable.strings b/BookPlayerWatch/sk-SK.lproj/Localizable.strings index bd5ead1a5..b0318fb31 100644 --- a/BookPlayerWatch/sk-SK.lproj/Localizable.strings +++ b/BookPlayerWatch/sk-SK.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "PRETÁČANIE"; "settings_skip_rewind_title" = "Dozadu"; "settings_skip_forward_title" = "Dopredu"; +"settings_autoplay_section_title" = "AUTOMATICKÉ PREHRÁVANIE"; "speed_title" = "rýchlosť"; "logout_title" = "Odhlásiť sa"; diff --git a/BookPlayerWatch/sv.lproj/Localizable.strings b/BookPlayerWatch/sv.lproj/Localizable.strings index c3109b849..f888e9547 100644 --- a/BookPlayerWatch/sv.lproj/Localizable.strings +++ b/BookPlayerWatch/sv.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "SNABBSPOLNING"; "settings_skip_rewind_title" = "Tillbaka"; "settings_skip_forward_title" = "Framåt"; +"settings_autoplay_section_title" = "AUTOSPELA"; "speed_title" = "hastighet"; "logout_title" = "Logga ut"; diff --git a/BookPlayerWatch/tr.lproj/Localizable.strings b/BookPlayerWatch/tr.lproj/Localizable.strings index b0f06a6c0..adaaf4b75 100644 --- a/BookPlayerWatch/tr.lproj/Localizable.strings +++ b/BookPlayerWatch/tr.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "ATLAMA ARALIKLARI"; "settings_skip_rewind_title" = "Geri Sarma"; "settings_skip_forward_title" = "İleri Sarma"; +"settings_autoplay_section_title" = "OTOMATİK OYNATMA"; "speed_title" = "hız"; "logout_title" = "Çıkış Yap"; diff --git a/BookPlayerWatch/uk.lproj/Localizable.strings b/BookPlayerWatch/uk.lproj/Localizable.strings index c41b34ac2..c41fe9b3d 100644 --- a/BookPlayerWatch/uk.lproj/Localizable.strings +++ b/BookPlayerWatch/uk.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "Інтервали перемотування"; "settings_skip_rewind_title" = "Назад"; "settings_skip_forward_title" = "Вперед"; +"settings_autoplay_section_title" = "АВТОМАТИЧНЕ ВІДТВОРЕННЯ"; "speed_title" = "швидкість"; "logout_title" = "Вийти"; diff --git a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings index 622e601c7..5923c8d25 100644 --- a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings +++ b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings @@ -19,5 +19,6 @@ "settings_skip_title" = "跳过间隔"; "settings_skip_rewind_title" = "倒带"; "settings_skip_forward_title" = "快进"; +"settings_autoplay_section_title" = "自动播放"; "speed_title" = "速度"; "logout_title" = "登出"; From 4e9d2c2ea1bc79ee7a52fc9edfe5d1cec53ba1bd Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Thu, 5 Dec 2024 12:09:42 -0500 Subject: [PATCH 17/31] Fix last played not updating after autoplay --- BookPlayerWatch/PlayerManager.swift | 9 --------- .../RemoteItemList/RemoteItemListView.swift | 17 +++++++++++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/BookPlayerWatch/PlayerManager.swift b/BookPlayerWatch/PlayerManager.swift index e0a254665..534cc0de1 100644 --- a/BookPlayerWatch/PlayerManager.swift +++ b/BookPlayerWatch/PlayerManager.swift @@ -874,15 +874,6 @@ extension PlayerManager { /// where we preload the player with the last played item if playbackQueued == true { if let nsError = item.error as? NSError { - let errorDescription = """ - \(nsError.localizedDescription) - - Error Domain - \(nsError.domain) - - Additional Info - \(nsError.userInfo) - """ showError(nsError) } else if let itemError = item.error { showError(itemError) diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift index 15a414ffb..353258e15 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -12,8 +12,8 @@ import SwiftUI struct RemoteItemListView: View { @Environment(\.scenePhase) var scenePhase @ObservedObject var coreServices: CoreServices + @ObservedObject var playerManager: PlayerManager @State var items: [SimpleLibraryItem] - @State var lastPlayedItem: SimpleLibraryItem? @State var playingItemParentPath: String? @State private var isLoading = false @State private var error: Error? @@ -28,6 +28,7 @@ struct RemoteItemListView: View { folderRelativePath: String? = nil ) { self.coreServices = coreServices + self.playerManager = coreServices.playerManager let fetchedItems = coreServices.libraryService.fetchContents( at: folderRelativePath, @@ -36,7 +37,6 @@ struct RemoteItemListView: View { ) ?? [] self._items = .init(initialValue: fetchedItems) let lastItem = coreServices.libraryService.getLastPlayedItems(limit: 1)?.first - self._lastPlayedItem = .init(initialValue: lastItem) self.folderRelativePath = folderRelativePath if let lastItem { @@ -71,7 +71,6 @@ struct RemoteItemListView: View { offset: nil ) ?? [] - lastPlayedItem = coreServices.libraryService.getLastPlayedItems(limit: 1)?.first if let lastPlayedItem { playingItemParentPath = getPathForParentOfItem(currentPlayingPath: lastPlayedItem.relativePath) } else { @@ -111,6 +110,17 @@ struct RemoteItemListView: View { return parentFolders[elementIndex] } + var lastPlayedItem: SimpleLibraryItem? { + guard + let currentItem = playerManager.currentItem, + let lastPlayedItem = coreServices.libraryService.getSimpleItem(with: currentItem.relativePath) + else { + return coreServices.libraryService.getLastPlayedItems(limit: 1)?.first + } + + return lastPlayedItem + } + var body: some View { GeometryReader { geometry in RefreshableScrollView(refreshing: $isRefreshing) { @@ -163,7 +173,6 @@ struct RemoteItemListView: View { try await coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) showPlayer = true isLoading = false - lastPlayedItem = item } catch { isLoading = false self.error = error From 7ea5a25d6bdf451d7a7af50558cda1aa8a8446e6 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Thu, 5 Dec 2024 12:22:25 -0500 Subject: [PATCH 18/31] add translation for login --- BookPlayerWatch/Base.lproj/Localizable.strings | 1 + BookPlayerWatch/Settings/Login/LoginView.swift | 2 +- BookPlayerWatch/ar.lproj/Localizable.strings | 1 + BookPlayerWatch/cs.lproj/Localizable.strings | 1 + BookPlayerWatch/da.lproj/Localizable.strings | 1 + BookPlayerWatch/de.lproj/Localizable.strings | 1 + BookPlayerWatch/el.lproj/Localizable.strings | 1 + BookPlayerWatch/en.lproj/Localizable.strings | 1 + BookPlayerWatch/es.lproj/Localizable.strings | 1 + BookPlayerWatch/fi.lproj/Localizable.strings | 1 + BookPlayerWatch/fr.lproj/Localizable.strings | 1 + BookPlayerWatch/hu.lproj/Localizable.strings | 1 + BookPlayerWatch/it.lproj/Localizable.strings | 1 + BookPlayerWatch/nb.lproj/Localizable.strings | 1 + BookPlayerWatch/nl.lproj/Localizable.strings | 1 + BookPlayerWatch/pl.lproj/Localizable.strings | 1 + BookPlayerWatch/pt-BR.lproj/Localizable.strings | 1 + BookPlayerWatch/pt-PT.lproj/Localizable.strings | 1 + BookPlayerWatch/ro.lproj/Localizable.strings | 1 + BookPlayerWatch/ru.lproj/Localizable.strings | 1 + BookPlayerWatch/sk-SK.lproj/Localizable.strings | 1 + BookPlayerWatch/sv.lproj/Localizable.strings | 1 + BookPlayerWatch/tr.lproj/Localizable.strings | 1 + BookPlayerWatch/uk.lproj/Localizable.strings | 1 + BookPlayerWatch/zh-Hans.lproj/Localizable.strings | 1 + 25 files changed, 25 insertions(+), 1 deletion(-) diff --git a/BookPlayerWatch/Base.lproj/Localizable.strings b/BookPlayerWatch/Base.lproj/Localizable.strings index 0e1b79c43..f0df97482 100644 --- a/BookPlayerWatch/Base.lproj/Localizable.strings +++ b/BookPlayerWatch/Base.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTOPLAY"; "speed_title" = "speed"; "logout_title" = "Log out"; +"watchapp_login_description" = "Stream your recent books to your Apple Watch, or download them to listen offline on the go."; diff --git a/BookPlayerWatch/Settings/Login/LoginView.swift b/BookPlayerWatch/Settings/Login/LoginView.swift index 035a9aa30..d30b233d8 100644 --- a/BookPlayerWatch/Settings/Login/LoginView.swift +++ b/BookPlayerWatch/Settings/Login/LoginView.swift @@ -20,7 +20,7 @@ struct LoginView: View { VStack(spacing: Spacing.S1) { Text("BookPlayer Pro") .font(Font(Fonts.titleLarge)) - Text("Stream your recent books to your Apple Watch, or download them to listen offline on the go.") + Text("watchapp_login_description".localized) .font(Font(Fonts.body)) .multilineTextAlignment(.center) #if targetEnvironment(simulator) diff --git a/BookPlayerWatch/ar.lproj/Localizable.strings b/BookPlayerWatch/ar.lproj/Localizable.strings index 71d14b603..34f5c455f 100644 --- a/BookPlayerWatch/ar.lproj/Localizable.strings +++ b/BookPlayerWatch/ar.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "التشغيل التلقائي"; "speed_title" = "السرعة"; "logout_title" = "تسجيل خروج"; +"watchapp_login_description" = "قم ببث كتبك الأخيرة إلى Apple Watch، أو قم بتنزيلها للاستماع إليها أثناء التنقل دون الاتصال بالإنترنت."; diff --git a/BookPlayerWatch/cs.lproj/Localizable.strings b/BookPlayerWatch/cs.lproj/Localizable.strings index c7878731d..d5cf6b1a0 100644 --- a/BookPlayerWatch/cs.lproj/Localizable.strings +++ b/BookPlayerWatch/cs.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATICKÉ PŘEHRÁVÁNÍ"; "speed_title" = "Rychlost"; "logout_title" = "Odhlásit se"; +"watchapp_login_description" = "Streamujte své nedávné knihy do hodinek Apple Watch nebo si je stáhněte a poslouchejte offline na cestách."; diff --git a/BookPlayerWatch/da.lproj/Localizable.strings b/BookPlayerWatch/da.lproj/Localizable.strings index d0754e841..76e73b106 100644 --- a/BookPlayerWatch/da.lproj/Localizable.strings +++ b/BookPlayerWatch/da.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATISK AFSPILNING"; "speed_title" = "Hastighed"; "logout_title" = "Log ud"; +"watchapp_login_description" = "Stream dine seneste bøger til dit Apple Watch, eller download dem for at lytte offline på farten."; diff --git a/BookPlayerWatch/de.lproj/Localizable.strings b/BookPlayerWatch/de.lproj/Localizable.strings index e26851cd2..a339c85df 100644 --- a/BookPlayerWatch/de.lproj/Localizable.strings +++ b/BookPlayerWatch/de.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATISCHES ABSPIELEN"; "speed_title" = "Wiedergabegeschwindigkeit"; "logout_title" = "Ausloggen"; +"watchapp_login_description" = "Streamen Sie Ihre letzten Bücher auf Ihre Apple Watch oder laden Sie sie herunter, um sie unterwegs offline anzuhören."; diff --git a/BookPlayerWatch/el.lproj/Localizable.strings b/BookPlayerWatch/el.lproj/Localizable.strings index 901ee8f4a..a44343b80 100644 --- a/BookPlayerWatch/el.lproj/Localizable.strings +++ b/BookPlayerWatch/el.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "ΑΥΤΟΜΑΤΗ ΑΝΑΠΑΡΑΓΩΓΗ"; "speed_title" = "Ταχύτητα"; "logout_title" = "Αποσύνδεση"; +"watchapp_login_description" = "Μεταδώστε τα πρόσφατα βιβλία σας σε ροή στο Apple Watch ή κατεβάστε τα για να τα ακούσετε εκτός σύνδεσης εν κινήσει."; diff --git a/BookPlayerWatch/en.lproj/Localizable.strings b/BookPlayerWatch/en.lproj/Localizable.strings index 0e1b79c43..f0df97482 100644 --- a/BookPlayerWatch/en.lproj/Localizable.strings +++ b/BookPlayerWatch/en.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTOPLAY"; "speed_title" = "speed"; "logout_title" = "Log out"; +"watchapp_login_description" = "Stream your recent books to your Apple Watch, or download them to listen offline on the go."; diff --git a/BookPlayerWatch/es.lproj/Localizable.strings b/BookPlayerWatch/es.lproj/Localizable.strings index 79e6dbf17..e5ff88c52 100644 --- a/BookPlayerWatch/es.lproj/Localizable.strings +++ b/BookPlayerWatch/es.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTO-REPRODUCCIÓN"; "speed_title" = "velocidad"; "logout_title" = "Cerrar sesión"; +"watchapp_login_description" = "Transmite tus libros recientes a tu Apple Watch o descárgalos para escucharlos sin conexión mientras viajas."; diff --git a/BookPlayerWatch/fi.lproj/Localizable.strings b/BookPlayerWatch/fi.lproj/Localizable.strings index 0ccc06e08..728944015 100644 --- a/BookPlayerWatch/fi.lproj/Localizable.strings +++ b/BookPlayerWatch/fi.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTOMAATTINEN TOISTO"; "speed_title" = "nopeus"; "logout_title" = "Kirjautua ulos"; +"watchapp_login_description" = "Suoratoista viimeisimmät kirjasi Apple Watchiin tai lataa ne kuunnellaksesi offline-tilassa liikkeellä ollessasi."; diff --git a/BookPlayerWatch/fr.lproj/Localizable.strings b/BookPlayerWatch/fr.lproj/Localizable.strings index 4ff37720f..c1cf301d1 100644 --- a/BookPlayerWatch/fr.lproj/Localizable.strings +++ b/BookPlayerWatch/fr.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "LECTURE AUTOMATIQUE"; "speed_title" = "vitesse"; "logout_title" = "Se déconnecter"; +"watchapp_login_description" = "Diffusez vos livres récents sur votre Apple Watch ou téléchargez-les pour les écouter hors ligne lors de vos déplacements."; diff --git a/BookPlayerWatch/hu.lproj/Localizable.strings b/BookPlayerWatch/hu.lproj/Localizable.strings index ac1944af8..cd76f1b9f 100644 --- a/BookPlayerWatch/hu.lproj/Localizable.strings +++ b/BookPlayerWatch/hu.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATIKUS LEJÁTSZÁS"; "speed_title" = "sebesség"; "logout_title" = "Kijelentkezés"; +"watchapp_login_description" = "Streamelje legutóbbi könyveit Apple Watch-ra, vagy töltse le őket, hogy offline hallgathassa útközben."; diff --git a/BookPlayerWatch/it.lproj/Localizable.strings b/BookPlayerWatch/it.lproj/Localizable.strings index ac765836c..27f78c013 100644 --- a/BookPlayerWatch/it.lproj/Localizable.strings +++ b/BookPlayerWatch/it.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "RIPRODUZIONE AUTOMATICA"; "speed_title" = "velocità"; "logout_title" = "Disconnettersi"; +"watchapp_login_description" = "Trasmetti in streaming i tuoi libri più recenti sul tuo Apple Watch oppure scaricali per ascoltarli offline ovunque ti trovi."; diff --git a/BookPlayerWatch/nb.lproj/Localizable.strings b/BookPlayerWatch/nb.lproj/Localizable.strings index 3b6c22804..a4ab3de72 100644 --- a/BookPlayerWatch/nb.lproj/Localizable.strings +++ b/BookPlayerWatch/nb.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTOPLAY"; "speed_title" = "hastighet"; "logout_title" = "Logg ut"; +"watchapp_login_description" = "Strøm de siste bøkene dine til Apple Watch, eller last dem ned for å lytte offline mens du er på farten."; diff --git a/BookPlayerWatch/nl.lproj/Localizable.strings b/BookPlayerWatch/nl.lproj/Localizable.strings index 07dce253a..37f9abc2c 100644 --- a/BookPlayerWatch/nl.lproj/Localizable.strings +++ b/BookPlayerWatch/nl.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATISCH AFSPELEN"; "speed_title" = "snelheid"; "logout_title" = "Uitloggen"; +"watchapp_login_description" = "Stream uw recente boeken naar uw Apple Watch of download ze om ze offline te beluisteren, waar u ook bent."; diff --git a/BookPlayerWatch/pl.lproj/Localizable.strings b/BookPlayerWatch/pl.lproj/Localizable.strings index 21a8dcc67..0bd21026e 100644 --- a/BookPlayerWatch/pl.lproj/Localizable.strings +++ b/BookPlayerWatch/pl.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATYCZNE ODTWARZANIE"; "speed_title" = "prędkość"; "logout_title" = "Wyloguj"; +"watchapp_login_description" = "Przesyłaj strumieniowo ostatnio przeczytane książki na swój zegarek Apple Watch lub pobieraj je, aby słuchać ich w trybie offline w podróży."; diff --git a/BookPlayerWatch/pt-BR.lproj/Localizable.strings b/BookPlayerWatch/pt-BR.lproj/Localizable.strings index 73a82d2d8..4a6f3b226 100644 --- a/BookPlayerWatch/pt-BR.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-BR.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "REPRODUÇÃO AUTOMÁTICA"; "speed_title" = "velocidade"; "logout_title" = "Sair"; +"watchapp_login_description" = "Transmita seus livros recentes para o seu Apple Watch ou baixe-os para ouvir offline em qualquer lugar."; diff --git a/BookPlayerWatch/pt-PT.lproj/Localizable.strings b/BookPlayerWatch/pt-PT.lproj/Localizable.strings index 139f67bee..97424d782 100644 --- a/BookPlayerWatch/pt-PT.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-PT.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "REPRODUÇÃO AUTOMÁTICA"; "speed_title" = "velocidade"; "logout_title" = "Sair"; +"watchapp_login_description" = "Transmita seus livros recentes para o seu Apple Watch ou baixe-os para ouvir offline em qualquer lugar."; diff --git a/BookPlayerWatch/ro.lproj/Localizable.strings b/BookPlayerWatch/ro.lproj/Localizable.strings index 5be2afa0d..f2ea5f549 100644 --- a/BookPlayerWatch/ro.lproj/Localizable.strings +++ b/BookPlayerWatch/ro.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "REDARE AUTOMATA"; "speed_title" = "Viteza"; "logout_title" = "Deconectați-vă"; +"watchapp_login_description" = "Transmiteți-vă cărțile recente pe Apple Watch sau descărcați-le pentru a le asculta offline din mers."; diff --git a/BookPlayerWatch/ru.lproj/Localizable.strings b/BookPlayerWatch/ru.lproj/Localizable.strings index e5a81f4f6..031bfe501 100644 --- a/BookPlayerWatch/ru.lproj/Localizable.strings +++ b/BookPlayerWatch/ru.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "АВТОВОСПРОИЗВЕДЕНИЕ"; "speed_title" = "скорость"; "logout_title" = "Выйти"; +"watchapp_login_description" = "Транслируйте недавно прочитанные книги на Apple Watch или загружайте их, чтобы слушать их офлайн в дороге."; diff --git a/BookPlayerWatch/sk-SK.lproj/Localizable.strings b/BookPlayerWatch/sk-SK.lproj/Localizable.strings index b0318fb31..6caa6ad05 100644 --- a/BookPlayerWatch/sk-SK.lproj/Localizable.strings +++ b/BookPlayerWatch/sk-SK.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATICKÉ PREHRÁVANIE"; "speed_title" = "rýchlosť"; "logout_title" = "Odhlásiť sa"; +"watchapp_login_description" = "Streamujte svoje nedávne knihy do hodiniek Apple Watch alebo si ich stiahnite a počúvajte offline na cestách."; diff --git a/BookPlayerWatch/sv.lproj/Localizable.strings b/BookPlayerWatch/sv.lproj/Localizable.strings index f888e9547..79ce2e669 100644 --- a/BookPlayerWatch/sv.lproj/Localizable.strings +++ b/BookPlayerWatch/sv.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "AUTOSPELA"; "speed_title" = "hastighet"; "logout_title" = "Logga ut"; +"watchapp_login_description" = "Strömma dina senaste böcker till din Apple Watch eller ladda ner dem för att lyssna offline när du är på språng."; diff --git a/BookPlayerWatch/tr.lproj/Localizable.strings b/BookPlayerWatch/tr.lproj/Localizable.strings index adaaf4b75..37af38e94 100644 --- a/BookPlayerWatch/tr.lproj/Localizable.strings +++ b/BookPlayerWatch/tr.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "OTOMATİK OYNATMA"; "speed_title" = "hız"; "logout_title" = "Çıkış Yap"; +"watchapp_login_description" = "Son okuduğunuz kitapları Apple Watch'unuza aktarın veya çevrimdışıyken dinlemek için indirin."; diff --git a/BookPlayerWatch/uk.lproj/Localizable.strings b/BookPlayerWatch/uk.lproj/Localizable.strings index c41fe9b3d..b521d3693 100644 --- a/BookPlayerWatch/uk.lproj/Localizable.strings +++ b/BookPlayerWatch/uk.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "АВТОМАТИЧНЕ ВІДТВОРЕННЯ"; "speed_title" = "швидкість"; "logout_title" = "Вийти"; +"watchapp_login_description" = "Транслюйте свої останні книги на Apple Watch або завантажуйте їх, щоб слухати в режимі офлайн у дорозі."; diff --git a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings index 5923c8d25..0a9c63232 100644 --- a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings +++ b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings @@ -22,3 +22,4 @@ "settings_autoplay_section_title" = "自动播放"; "speed_title" = "速度"; "logout_title" = "登出"; +"watchapp_login_description" = "将您最近的书籍流式传输到您的 Apple Watch,或下载它们以便在旅途中离线收听。"; From 7deb4e5e6dc79bb475c23e421fe32563a2226622 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Thu, 5 Dec 2024 12:42:21 -0500 Subject: [PATCH 19/31] Update benefits on login screen on iPhone --- BookPlayer/Base.lproj/Localizable.strings | 1 + .../Login Screen/LoginViewController.swift | 34 +++++++++++++------ BookPlayer/ar.lproj/Localizable.strings | 1 + BookPlayer/cs.lproj/Localizable.strings | 1 + BookPlayer/da.lproj/Localizable.strings | 1 + BookPlayer/de.lproj/Localizable.strings | 1 + BookPlayer/el.lproj/Localizable.strings | 1 + BookPlayer/en.lproj/Localizable.strings | 1 + BookPlayer/es.lproj/Localizable.strings | 1 + BookPlayer/fi.lproj/Localizable.strings | 1 + BookPlayer/fr.lproj/Localizable.strings | 1 + BookPlayer/hu.lproj/Localizable.strings | 1 + BookPlayer/it.lproj/Localizable.strings | 1 + BookPlayer/nb.lproj/Localizable.strings | 1 + BookPlayer/nl.lproj/Localizable.strings | 1 + BookPlayer/pl.lproj/Localizable.strings | 1 + BookPlayer/pt-BR.lproj/Localizable.strings | 1 + BookPlayer/pt-PT.lproj/Localizable.strings | 1 + BookPlayer/ro.lproj/Localizable.strings | 1 + BookPlayer/ru.lproj/Localizable.strings | 1 + BookPlayer/sk-SK.lproj/Localizable.strings | 1 + BookPlayer/sv.lproj/Localizable.strings | 1 + BookPlayer/tr.lproj/Localizable.strings | 1 + BookPlayer/uk.lproj/Localizable.strings | 1 + BookPlayer/zh-Hans.lproj/Localizable.strings | 1 + .../Base.lproj/Localizable.strings | 2 +- .../Settings/Login/LoginView.swift | 2 +- BookPlayerWatch/ar.lproj/Localizable.strings | 2 +- BookPlayerWatch/cs.lproj/Localizable.strings | 2 +- BookPlayerWatch/da.lproj/Localizable.strings | 2 +- BookPlayerWatch/de.lproj/Localizable.strings | 2 +- BookPlayerWatch/el.lproj/Localizable.strings | 2 +- BookPlayerWatch/en.lproj/Localizable.strings | 2 +- BookPlayerWatch/es.lproj/Localizable.strings | 2 +- BookPlayerWatch/fi.lproj/Localizable.strings | 2 +- BookPlayerWatch/fr.lproj/Localizable.strings | 2 +- BookPlayerWatch/hu.lproj/Localizable.strings | 2 +- BookPlayerWatch/it.lproj/Localizable.strings | 2 +- BookPlayerWatch/nb.lproj/Localizable.strings | 2 +- BookPlayerWatch/nl.lproj/Localizable.strings | 2 +- BookPlayerWatch/pl.lproj/Localizable.strings | 2 +- .../pt-BR.lproj/Localizable.strings | 2 +- .../pt-PT.lproj/Localizable.strings | 2 +- BookPlayerWatch/ro.lproj/Localizable.strings | 2 +- BookPlayerWatch/ru.lproj/Localizable.strings | 2 +- .../sk-SK.lproj/Localizable.strings | 2 +- BookPlayerWatch/sv.lproj/Localizable.strings | 2 +- BookPlayerWatch/tr.lproj/Localizable.strings | 2 +- BookPlayerWatch/uk.lproj/Localizable.strings | 2 +- .../zh-Hans.lproj/Localizable.strings | 2 +- 50 files changed, 72 insertions(+), 36 deletions(-) diff --git a/BookPlayer/Base.lproj/Localizable.strings b/BookPlayer/Base.lproj/Localizable.strings index ac5dc5fc6..101b7faf0 100644 --- a/BookPlayer/Base.lproj/Localizable.strings +++ b/BookPlayer/Base.lproj/Localizable.strings @@ -321,3 +321,4 @@ We're working hard on providing a seamless experience, if possible, please conta "more_title" = "More"; "repeat_turn_on_title" = "Turn on Repeat for this book"; "repeat_turn_off_title" = "Turn off Repeat for this book"; +"benefits_watchapp_description" = "Stream your recent books to your Apple Watch, or download them to listen offline on the go."; diff --git a/BookPlayer/Profile/Login Screen/LoginViewController.swift b/BookPlayer/Profile/Login Screen/LoginViewController.swift index dee5a826b..ff170f13e 100644 --- a/BookPlayer/Profile/Login Screen/LoginViewController.swift +++ b/BookPlayer/Profile/Login Screen/LoginViewController.swift @@ -38,6 +38,17 @@ class LoginViewController: UIViewController { return stackView }() + private lazy var watchAppBenefitStackView: UIStackView = { + let stackView = LoginBenefitView( + title: "Apple Watch (Beta)", + description: "benefits_watchapp_description".localized, + systemName: "applewatch.radiowaves.left.and.right", + imageAlpha: 0.5 + ) + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + private lazy var cosmeticBenefitStackView: UIStackView = { let stackView = LoginBenefitView( title: "benefits_themesicons_title".localized, @@ -66,7 +77,6 @@ class LoginViewController: UIViewController { disclaimers: [ "benefits_disclaimer_account_description".localized, "benefits_disclaimer_subscription_description".localized, - "benefits_disclaimer_watch_description".localized ] ) stackView.translatesAutoresizingMaskIntoConstraints = false @@ -115,8 +125,8 @@ class LoginViewController: UIViewController { view.addSubview(scrollView) scrollView.addSubview(contentView) contentView.addSubview(cloudBenefitStackView) + contentView.addSubview(watchAppBenefitStackView) contentView.addSubview(cosmeticBenefitStackView) - contentView.addSubview(supportBenefitStackView) contentView.addSubview(disclaimerStackView) } @@ -145,19 +155,21 @@ class LoginViewController: UIViewController { cloudBenefitStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Spacing.M), cloudBenefitStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), cloudBenefitStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Spacing.M), - cosmeticBenefitStackView.topAnchor.constraint(equalTo: cloudBenefitStackView.bottomAnchor, constant: 30), - cosmeticBenefitStackView.leadingAnchor.constraint(equalTo: cloudBenefitStackView.leadingAnchor), - cosmeticBenefitStackView.trailingAnchor.constraint(equalTo: cloudBenefitStackView.trailingAnchor), - supportBenefitStackView.topAnchor.constraint(equalTo: cosmeticBenefitStackView.bottomAnchor, constant: 30), - supportBenefitStackView.leadingAnchor.constraint(equalTo: cosmeticBenefitStackView.leadingAnchor), - supportBenefitStackView.trailingAnchor.constraint(equalTo: cosmeticBenefitStackView.trailingAnchor), + watchAppBenefitStackView.topAnchor.constraint(equalTo: cloudBenefitStackView.bottomAnchor, constant: 30), + watchAppBenefitStackView.leadingAnchor.constraint(equalTo: cloudBenefitStackView.leadingAnchor), + watchAppBenefitStackView.trailingAnchor.constraint(equalTo: cloudBenefitStackView.trailingAnchor), + + cosmeticBenefitStackView.topAnchor.constraint(equalTo: watchAppBenefitStackView.bottomAnchor, constant: 30), + cosmeticBenefitStackView.leadingAnchor.constraint(equalTo: watchAppBenefitStackView.leadingAnchor), + cosmeticBenefitStackView.trailingAnchor.constraint(equalTo: watchAppBenefitStackView.trailingAnchor), + // setup disclaimer disclaimerStackView.topAnchor.constraint( - greaterThanOrEqualTo: supportBenefitStackView.bottomAnchor, + greaterThanOrEqualTo: cosmeticBenefitStackView.bottomAnchor, constant: 45 ), - disclaimerStackView.leadingAnchor.constraint(equalTo: supportBenefitStackView.leadingAnchor, constant: Spacing.M), - disclaimerStackView.trailingAnchor.constraint(equalTo: supportBenefitStackView.trailingAnchor), + disclaimerStackView.leadingAnchor.constraint(equalTo: cosmeticBenefitStackView.leadingAnchor, constant: Spacing.M), + disclaimerStackView.trailingAnchor.constraint(equalTo: cosmeticBenefitStackView.trailingAnchor), disclaimerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Spacing.L), ]) } diff --git a/BookPlayer/ar.lproj/Localizable.strings b/BookPlayer/ar.lproj/Localizable.strings index da40e5157..b42a55787 100644 --- a/BookPlayer/ar.lproj/Localizable.strings +++ b/BookPlayer/ar.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "أكثر"; "repeat_turn_on_title" = "قم بتشغيل التكرار لهذا الكتاب"; "repeat_turn_off_title" = "إيقاف تكرار هذا الكتاب"; +"benefits_watchapp_description" = "قم ببث كتبك الأخيرة إلى Apple Watch، أو قم بتنزيلها للاستماع إليها أثناء التنقل دون الاتصال بالإنترنت."; diff --git a/BookPlayer/cs.lproj/Localizable.strings b/BookPlayer/cs.lproj/Localizable.strings index 423819443..1179fd192 100644 --- a/BookPlayer/cs.lproj/Localizable.strings +++ b/BookPlayer/cs.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Více"; "repeat_turn_on_title" = "Zapněte opakování pro tuto knihu"; "repeat_turn_off_title" = "Vypnout opakování pro tuto knihu"; +"benefits_watchapp_description" = "Streamujte své nedávné knihy do hodinek Apple Watch nebo si je stáhněte a poslouchejte offline na cestách."; diff --git a/BookPlayer/da.lproj/Localizable.strings b/BookPlayer/da.lproj/Localizable.strings index 44c0023cd..2bfd42a32 100644 --- a/BookPlayer/da.lproj/Localizable.strings +++ b/BookPlayer/da.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Mere"; "repeat_turn_on_title" = "Slå Gentag til for denne bog"; "repeat_turn_off_title" = "Slå Gentag fra for denne bog"; +"benefits_watchapp_description" = "Stream dine seneste bøger til dit Apple Watch, eller download dem for at lytte offline på farten."; diff --git a/BookPlayer/de.lproj/Localizable.strings b/BookPlayer/de.lproj/Localizable.strings index 22a0f3c2b..c73ae69aa 100644 --- a/BookPlayer/de.lproj/Localizable.strings +++ b/BookPlayer/de.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Mehr"; "repeat_turn_on_title" = "Aktivieren Sie die Option „Wiederholen“ für dieses Buch"; "repeat_turn_off_title" = "Deaktivieren Sie „Wiederholen“ für dieses Buch"; +"benefits_watchapp_description" = "Streamen Sie Ihre letzten Bücher auf Ihre Apple Watch oder laden Sie sie herunter, um sie unterwegs offline anzuhören."; diff --git a/BookPlayer/el.lproj/Localizable.strings b/BookPlayer/el.lproj/Localizable.strings index 8a78c9302..d63cc8164 100644 --- a/BookPlayer/el.lproj/Localizable.strings +++ b/BookPlayer/el.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Περισσότερα"; "repeat_turn_on_title" = "Ενεργοποιήστε το Repeat για αυτό το βιβλίο"; "repeat_turn_off_title" = "Απενεργοποιήστε το Repeat για αυτό το βιβλίο"; +"benefits_watchapp_description" = "Μεταδώστε τα πρόσφατα βιβλία σας σε ροή στο Apple Watch ή κατεβάστε τα για να τα ακούσετε εκτός σύνδεσης εν κινήσει."; diff --git a/BookPlayer/en.lproj/Localizable.strings b/BookPlayer/en.lproj/Localizable.strings index 33fc34faa..918d9dda8 100644 --- a/BookPlayer/en.lproj/Localizable.strings +++ b/BookPlayer/en.lproj/Localizable.strings @@ -321,3 +321,4 @@ We're working hard on providing a seamless experience, if possible, please conta "more_title" = "More"; "repeat_turn_on_title" = "Turn on Repeat for this book"; "repeat_turn_off_title" = "Turn off Repeat for this book"; +"benefits_watchapp_description" = "Stream your recent books to your Apple Watch, or download them to listen offline on the go."; diff --git a/BookPlayer/es.lproj/Localizable.strings b/BookPlayer/es.lproj/Localizable.strings index ccd7d0106..462f4d107 100644 --- a/BookPlayer/es.lproj/Localizable.strings +++ b/BookPlayer/es.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Más"; "repeat_turn_on_title" = "Activar repetición para este libro"; "repeat_turn_off_title" = "Desactivar la repetición para este libro"; +"benefits_watchapp_description" = "Transmite tus libros recientes a tu Apple Watch o descárgalos para escucharlos sin conexión mientras viajas."; diff --git a/BookPlayer/fi.lproj/Localizable.strings b/BookPlayer/fi.lproj/Localizable.strings index def93f8ad..5c466d1fa 100644 --- a/BookPlayer/fi.lproj/Localizable.strings +++ b/BookPlayer/fi.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Lisää"; "repeat_turn_on_title" = "Ota Toista käyttöön tälle kirjalle"; "repeat_turn_off_title" = "Poista Toista käytöstä tästä kirjasta"; +"benefits_watchapp_description" = "Suoratoista viimeisimmät kirjasi Apple Watchiin tai lataa ne kuunnellaksesi offline-tilassa liikkeellä ollessasi."; diff --git a/BookPlayer/fr.lproj/Localizable.strings b/BookPlayer/fr.lproj/Localizable.strings index 52705a403..de8d68e3f 100644 --- a/BookPlayer/fr.lproj/Localizable.strings +++ b/BookPlayer/fr.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Plus"; "repeat_turn_on_title" = "Activer la répétition pour ce livre"; "repeat_turn_off_title" = "Désactiver la répétition pour ce livre"; +"benefits_watchapp_description" = "Diffusez vos livres récents sur votre Apple Watch ou téléchargez-les pour les écouter hors ligne lors de vos déplacements."; diff --git a/BookPlayer/hu.lproj/Localizable.strings b/BookPlayer/hu.lproj/Localizable.strings index e893e54a1..b9ffafead 100644 --- a/BookPlayer/hu.lproj/Localizable.strings +++ b/BookPlayer/hu.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Több"; "repeat_turn_on_title" = "Kapcsolja be az Ismétlés funkciót ennél a könyvnél"; "repeat_turn_off_title" = "Kapcsolja ki az Ismétlés funkciót ennél a könyvnél"; +"benefits_watchapp_description" = "Streamelje legutóbbi könyveit Apple Watch-ra, vagy töltse le őket, hogy offline hallgathassa útközben."; diff --git a/BookPlayer/it.lproj/Localizable.strings b/BookPlayer/it.lproj/Localizable.strings index c7ee6ecf9..95c8a79f7 100644 --- a/BookPlayer/it.lproj/Localizable.strings +++ b/BookPlayer/it.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Di più"; "repeat_turn_on_title" = "Attiva Ripeti per questo libro"; "repeat_turn_off_title" = "Disattiva Ripeti per questo libro"; +"benefits_watchapp_description" = "Trasmetti in streaming i tuoi libri più recenti sul tuo Apple Watch oppure scaricali per ascoltarli offline ovunque ti trovi."; diff --git a/BookPlayer/nb.lproj/Localizable.strings b/BookPlayer/nb.lproj/Localizable.strings index c7fde3e3a..04fe227c2 100644 --- a/BookPlayer/nb.lproj/Localizable.strings +++ b/BookPlayer/nb.lproj/Localizable.strings @@ -321,3 +321,4 @@ Vi jobber hardt for å gi deg en sømløs opplevelse. Hvis mulig, kontakt oss p "more_title" = "Flere"; "repeat_turn_on_title" = "Slå på Gjenta for denne boken"; "repeat_turn_off_title" = "Slå av Gjenta for denne boken"; +"benefits_watchapp_description" = "Strøm de siste bøkene dine til Apple Watch, eller last dem ned for å lytte offline mens du er på farten."; diff --git a/BookPlayer/nl.lproj/Localizable.strings b/BookPlayer/nl.lproj/Localizable.strings index 6c4ed0ced..28224c953 100644 --- a/BookPlayer/nl.lproj/Localizable.strings +++ b/BookPlayer/nl.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Meer"; "repeat_turn_on_title" = "Schakel Herhalen in voor dit boek"; "repeat_turn_off_title" = "Herhalen voor dit boek uitschakelen"; +"benefits_watchapp_description" = "Stream uw recente boeken naar uw Apple Watch of download ze om ze offline te beluisteren, waar u ook bent."; diff --git a/BookPlayer/pl.lproj/Localizable.strings b/BookPlayer/pl.lproj/Localizable.strings index de5b281db..31d02391b 100644 --- a/BookPlayer/pl.lproj/Localizable.strings +++ b/BookPlayer/pl.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Więcej"; "repeat_turn_on_title" = "Włącz opcję Powtórz dla tej książki"; "repeat_turn_off_title" = "Wyłącz opcję Powtórz dla tej książki"; +"benefits_watchapp_description" = "Przesyłaj strumieniowo ostatnio przeczytane książki na swój zegarek Apple Watch lub pobieraj je, aby słuchać ich w trybie offline w podróży."; diff --git a/BookPlayer/pt-BR.lproj/Localizable.strings b/BookPlayer/pt-BR.lproj/Localizable.strings index c42271b75..a78c2b431 100644 --- a/BookPlayer/pt-BR.lproj/Localizable.strings +++ b/BookPlayer/pt-BR.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Mais"; "repeat_turn_on_title" = "Ativar repetição para este livro"; "repeat_turn_off_title" = "Desativar Repetir para este livro"; +"benefits_watchapp_description" = "Transmita seus livros recentes para o seu Apple Watch ou baixe-os para ouvir offline em qualquer lugar."; diff --git a/BookPlayer/pt-PT.lproj/Localizable.strings b/BookPlayer/pt-PT.lproj/Localizable.strings index 9e7409071..513a73065 100644 --- a/BookPlayer/pt-PT.lproj/Localizable.strings +++ b/BookPlayer/pt-PT.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Mais"; "repeat_turn_on_title" = "Ativar repetição para este livro"; "repeat_turn_off_title" = "Desativar Repetir para este livro"; +"benefits_watchapp_description" = "Transmita seus livros recentes para o seu Apple Watch ou baixe-os para ouvir offline em qualquer lugar."; diff --git a/BookPlayer/ro.lproj/Localizable.strings b/BookPlayer/ro.lproj/Localizable.strings index 8aade4fbb..33d8c2ab7 100644 --- a/BookPlayer/ro.lproj/Localizable.strings +++ b/BookPlayer/ro.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Mai mult"; "repeat_turn_on_title" = "Activați Repetare pentru această carte"; "repeat_turn_off_title" = "Dezactivează Repetarea pentru această carte"; +"benefits_watchapp_description" = "Transmiteți-vă cărțile recente pe Apple Watch sau descărcați-le pentru a le asculta offline din mers."; diff --git a/BookPlayer/ru.lproj/Localizable.strings b/BookPlayer/ru.lproj/Localizable.strings index 2f903cba7..382bfec0c 100644 --- a/BookPlayer/ru.lproj/Localizable.strings +++ b/BookPlayer/ru.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Более"; "repeat_turn_on_title" = "Включить повтор для этой книги"; "repeat_turn_off_title" = "Отключить повтор для этой книги"; +"benefits_watchapp_description" = "Транслируйте недавно прочитанные книги на Apple Watch или загружайте их, чтобы слушать их офлайн в дороге."; diff --git a/BookPlayer/sk-SK.lproj/Localizable.strings b/BookPlayer/sk-SK.lproj/Localizable.strings index 7e14fa22a..f7912b994 100644 --- a/BookPlayer/sk-SK.lproj/Localizable.strings +++ b/BookPlayer/sk-SK.lproj/Localizable.strings @@ -321,3 +321,4 @@ Usilovne pracujeme na poskytovaní bezproblémového zážitku, ak je to možné "more_title" = "Viac"; "repeat_turn_on_title" = "Zapnúť Opakovať pre túto knihu"; "repeat_turn_off_title" = "Vypnúť Opakovať pre túto knihu"; +"benefits_watchapp_description" = "Streamujte svoje nedávne knihy do hodiniek Apple Watch alebo si ich stiahnite a počúvajte offline na cestách."; diff --git a/BookPlayer/sv.lproj/Localizable.strings b/BookPlayer/sv.lproj/Localizable.strings index 4716b30be..82be6310f 100644 --- a/BookPlayer/sv.lproj/Localizable.strings +++ b/BookPlayer/sv.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Mer"; "repeat_turn_on_title" = "Aktivera Upprepa för den här boken"; "repeat_turn_off_title" = "Stäng av Upprepa för den här boken"; +"benefits_watchapp_description" = "Strömma dina senaste böcker till din Apple Watch eller ladda ner dem för att lyssna offline när du är på språng."; diff --git a/BookPlayer/tr.lproj/Localizable.strings b/BookPlayer/tr.lproj/Localizable.strings index ddf39d37c..fcee18fba 100644 --- a/BookPlayer/tr.lproj/Localizable.strings +++ b/BookPlayer/tr.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "Daha"; "repeat_turn_on_title" = "Bu kitap için Tekrarı açın"; "repeat_turn_off_title" = "Bu kitap için Tekrarı kapatın"; +"benefits_watchapp_description" = "Son okuduğunuz kitapları Apple Watch'unuza aktarın veya çevrimdışıyken dinlemek için indirin."; diff --git a/BookPlayer/uk.lproj/Localizable.strings b/BookPlayer/uk.lproj/Localizable.strings index 36d486686..22d1299e9 100644 --- a/BookPlayer/uk.lproj/Localizable.strings +++ b/BookPlayer/uk.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "більше"; "repeat_turn_on_title" = "Увімкніть повтор для цієї книги"; "repeat_turn_off_title" = "Вимкніть повтор для цієї книги"; +"benefits_watchapp_description" = "Транслюйте свої останні книги на Apple Watch або завантажуйте їх, щоб слухати в режимі офлайн у дорозі."; diff --git a/BookPlayer/zh-Hans.lproj/Localizable.strings b/BookPlayer/zh-Hans.lproj/Localizable.strings index 5fb6c31dc..01c869538 100644 --- a/BookPlayer/zh-Hans.lproj/Localizable.strings +++ b/BookPlayer/zh-Hans.lproj/Localizable.strings @@ -321,3 +321,4 @@ "more_title" = "更多的"; "repeat_turn_on_title" = "为这本书开启重复"; "repeat_turn_off_title" = "关闭此书的重复功能"; +"benefits_watchapp_description" = "将您最近的书籍流式传输到您的 Apple Watch,或下载它们以便在旅途中离线收听。"; diff --git a/BookPlayerWatch/Base.lproj/Localizable.strings b/BookPlayerWatch/Base.lproj/Localizable.strings index f0df97482..1d3d3e129 100644 --- a/BookPlayerWatch/Base.lproj/Localizable.strings +++ b/BookPlayerWatch/Base.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTOPLAY"; "speed_title" = "speed"; "logout_title" = "Log out"; -"watchapp_login_description" = "Stream your recent books to your Apple Watch, or download them to listen offline on the go."; +"benefits_watchapp_description" = "Stream your recent books to your Apple Watch, or download them to listen offline on the go."; diff --git a/BookPlayerWatch/Settings/Login/LoginView.swift b/BookPlayerWatch/Settings/Login/LoginView.swift index d30b233d8..0858dabf4 100644 --- a/BookPlayerWatch/Settings/Login/LoginView.swift +++ b/BookPlayerWatch/Settings/Login/LoginView.swift @@ -20,7 +20,7 @@ struct LoginView: View { VStack(spacing: Spacing.S1) { Text("BookPlayer Pro") .font(Font(Fonts.titleLarge)) - Text("watchapp_login_description".localized) + Text("benefits_watchapp_description".localized) .font(Font(Fonts.body)) .multilineTextAlignment(.center) #if targetEnvironment(simulator) diff --git a/BookPlayerWatch/ar.lproj/Localizable.strings b/BookPlayerWatch/ar.lproj/Localizable.strings index 34f5c455f..fc2e6d8c9 100644 --- a/BookPlayerWatch/ar.lproj/Localizable.strings +++ b/BookPlayerWatch/ar.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "التشغيل التلقائي"; "speed_title" = "السرعة"; "logout_title" = "تسجيل خروج"; -"watchapp_login_description" = "قم ببث كتبك الأخيرة إلى Apple Watch، أو قم بتنزيلها للاستماع إليها أثناء التنقل دون الاتصال بالإنترنت."; +"benefits_watchapp_description" = "قم ببث كتبك الأخيرة إلى Apple Watch، أو قم بتنزيلها للاستماع إليها أثناء التنقل دون الاتصال بالإنترنت."; diff --git a/BookPlayerWatch/cs.lproj/Localizable.strings b/BookPlayerWatch/cs.lproj/Localizable.strings index d5cf6b1a0..273893597 100644 --- a/BookPlayerWatch/cs.lproj/Localizable.strings +++ b/BookPlayerWatch/cs.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATICKÉ PŘEHRÁVÁNÍ"; "speed_title" = "Rychlost"; "logout_title" = "Odhlásit se"; -"watchapp_login_description" = "Streamujte své nedávné knihy do hodinek Apple Watch nebo si je stáhněte a poslouchejte offline na cestách."; +"benefits_watchapp_description" = "Streamujte své nedávné knihy do hodinek Apple Watch nebo si je stáhněte a poslouchejte offline na cestách."; diff --git a/BookPlayerWatch/da.lproj/Localizable.strings b/BookPlayerWatch/da.lproj/Localizable.strings index 76e73b106..35c11bff3 100644 --- a/BookPlayerWatch/da.lproj/Localizable.strings +++ b/BookPlayerWatch/da.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATISK AFSPILNING"; "speed_title" = "Hastighed"; "logout_title" = "Log ud"; -"watchapp_login_description" = "Stream dine seneste bøger til dit Apple Watch, eller download dem for at lytte offline på farten."; +"benefits_watchapp_description" = "Stream dine seneste bøger til dit Apple Watch, eller download dem for at lytte offline på farten."; diff --git a/BookPlayerWatch/de.lproj/Localizable.strings b/BookPlayerWatch/de.lproj/Localizable.strings index a339c85df..019d013ef 100644 --- a/BookPlayerWatch/de.lproj/Localizable.strings +++ b/BookPlayerWatch/de.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATISCHES ABSPIELEN"; "speed_title" = "Wiedergabegeschwindigkeit"; "logout_title" = "Ausloggen"; -"watchapp_login_description" = "Streamen Sie Ihre letzten Bücher auf Ihre Apple Watch oder laden Sie sie herunter, um sie unterwegs offline anzuhören."; +"benefits_watchapp_description" = "Streamen Sie Ihre letzten Bücher auf Ihre Apple Watch oder laden Sie sie herunter, um sie unterwegs offline anzuhören."; diff --git a/BookPlayerWatch/el.lproj/Localizable.strings b/BookPlayerWatch/el.lproj/Localizable.strings index a44343b80..0ef9f5d6e 100644 --- a/BookPlayerWatch/el.lproj/Localizable.strings +++ b/BookPlayerWatch/el.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "ΑΥΤΟΜΑΤΗ ΑΝΑΠΑΡΑΓΩΓΗ"; "speed_title" = "Ταχύτητα"; "logout_title" = "Αποσύνδεση"; -"watchapp_login_description" = "Μεταδώστε τα πρόσφατα βιβλία σας σε ροή στο Apple Watch ή κατεβάστε τα για να τα ακούσετε εκτός σύνδεσης εν κινήσει."; +"benefits_watchapp_description" = "Μεταδώστε τα πρόσφατα βιβλία σας σε ροή στο Apple Watch ή κατεβάστε τα για να τα ακούσετε εκτός σύνδεσης εν κινήσει."; diff --git a/BookPlayerWatch/en.lproj/Localizable.strings b/BookPlayerWatch/en.lproj/Localizable.strings index f0df97482..1d3d3e129 100644 --- a/BookPlayerWatch/en.lproj/Localizable.strings +++ b/BookPlayerWatch/en.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTOPLAY"; "speed_title" = "speed"; "logout_title" = "Log out"; -"watchapp_login_description" = "Stream your recent books to your Apple Watch, or download them to listen offline on the go."; +"benefits_watchapp_description" = "Stream your recent books to your Apple Watch, or download them to listen offline on the go."; diff --git a/BookPlayerWatch/es.lproj/Localizable.strings b/BookPlayerWatch/es.lproj/Localizable.strings index e5ff88c52..16db7894e 100644 --- a/BookPlayerWatch/es.lproj/Localizable.strings +++ b/BookPlayerWatch/es.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTO-REPRODUCCIÓN"; "speed_title" = "velocidad"; "logout_title" = "Cerrar sesión"; -"watchapp_login_description" = "Transmite tus libros recientes a tu Apple Watch o descárgalos para escucharlos sin conexión mientras viajas."; +"benefits_watchapp_description" = "Transmite tus libros recientes a tu Apple Watch o descárgalos para escucharlos sin conexión mientras viajas."; diff --git a/BookPlayerWatch/fi.lproj/Localizable.strings b/BookPlayerWatch/fi.lproj/Localizable.strings index 728944015..1bb4c75eb 100644 --- a/BookPlayerWatch/fi.lproj/Localizable.strings +++ b/BookPlayerWatch/fi.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTOMAATTINEN TOISTO"; "speed_title" = "nopeus"; "logout_title" = "Kirjautua ulos"; -"watchapp_login_description" = "Suoratoista viimeisimmät kirjasi Apple Watchiin tai lataa ne kuunnellaksesi offline-tilassa liikkeellä ollessasi."; +"benefits_watchapp_description" = "Suoratoista viimeisimmät kirjasi Apple Watchiin tai lataa ne kuunnellaksesi offline-tilassa liikkeellä ollessasi."; diff --git a/BookPlayerWatch/fr.lproj/Localizable.strings b/BookPlayerWatch/fr.lproj/Localizable.strings index c1cf301d1..c531db79e 100644 --- a/BookPlayerWatch/fr.lproj/Localizable.strings +++ b/BookPlayerWatch/fr.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "LECTURE AUTOMATIQUE"; "speed_title" = "vitesse"; "logout_title" = "Se déconnecter"; -"watchapp_login_description" = "Diffusez vos livres récents sur votre Apple Watch ou téléchargez-les pour les écouter hors ligne lors de vos déplacements."; +"benefits_watchapp_description" = "Diffusez vos livres récents sur votre Apple Watch ou téléchargez-les pour les écouter hors ligne lors de vos déplacements."; diff --git a/BookPlayerWatch/hu.lproj/Localizable.strings b/BookPlayerWatch/hu.lproj/Localizable.strings index cd76f1b9f..90bce5bc7 100644 --- a/BookPlayerWatch/hu.lproj/Localizable.strings +++ b/BookPlayerWatch/hu.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATIKUS LEJÁTSZÁS"; "speed_title" = "sebesség"; "logout_title" = "Kijelentkezés"; -"watchapp_login_description" = "Streamelje legutóbbi könyveit Apple Watch-ra, vagy töltse le őket, hogy offline hallgathassa útközben."; +"benefits_watchapp_description" = "Streamelje legutóbbi könyveit Apple Watch-ra, vagy töltse le őket, hogy offline hallgathassa útközben."; diff --git a/BookPlayerWatch/it.lproj/Localizable.strings b/BookPlayerWatch/it.lproj/Localizable.strings index 27f78c013..f0b5a080a 100644 --- a/BookPlayerWatch/it.lproj/Localizable.strings +++ b/BookPlayerWatch/it.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "RIPRODUZIONE AUTOMATICA"; "speed_title" = "velocità"; "logout_title" = "Disconnettersi"; -"watchapp_login_description" = "Trasmetti in streaming i tuoi libri più recenti sul tuo Apple Watch oppure scaricali per ascoltarli offline ovunque ti trovi."; +"benefits_watchapp_description" = "Trasmetti in streaming i tuoi libri più recenti sul tuo Apple Watch oppure scaricali per ascoltarli offline ovunque ti trovi."; diff --git a/BookPlayerWatch/nb.lproj/Localizable.strings b/BookPlayerWatch/nb.lproj/Localizable.strings index a4ab3de72..591c86fcf 100644 --- a/BookPlayerWatch/nb.lproj/Localizable.strings +++ b/BookPlayerWatch/nb.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTOPLAY"; "speed_title" = "hastighet"; "logout_title" = "Logg ut"; -"watchapp_login_description" = "Strøm de siste bøkene dine til Apple Watch, eller last dem ned for å lytte offline mens du er på farten."; +"benefits_watchapp_description" = "Strøm de siste bøkene dine til Apple Watch, eller last dem ned for å lytte offline mens du er på farten."; diff --git a/BookPlayerWatch/nl.lproj/Localizable.strings b/BookPlayerWatch/nl.lproj/Localizable.strings index 37f9abc2c..dd1cb7b5a 100644 --- a/BookPlayerWatch/nl.lproj/Localizable.strings +++ b/BookPlayerWatch/nl.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATISCH AFSPELEN"; "speed_title" = "snelheid"; "logout_title" = "Uitloggen"; -"watchapp_login_description" = "Stream uw recente boeken naar uw Apple Watch of download ze om ze offline te beluisteren, waar u ook bent."; +"benefits_watchapp_description" = "Stream uw recente boeken naar uw Apple Watch of download ze om ze offline te beluisteren, waar u ook bent."; diff --git a/BookPlayerWatch/pl.lproj/Localizable.strings b/BookPlayerWatch/pl.lproj/Localizable.strings index 0bd21026e..4ba354d46 100644 --- a/BookPlayerWatch/pl.lproj/Localizable.strings +++ b/BookPlayerWatch/pl.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATYCZNE ODTWARZANIE"; "speed_title" = "prędkość"; "logout_title" = "Wyloguj"; -"watchapp_login_description" = "Przesyłaj strumieniowo ostatnio przeczytane książki na swój zegarek Apple Watch lub pobieraj je, aby słuchać ich w trybie offline w podróży."; +"benefits_watchapp_description" = "Przesyłaj strumieniowo ostatnio przeczytane książki na swój zegarek Apple Watch lub pobieraj je, aby słuchać ich w trybie offline w podróży."; diff --git a/BookPlayerWatch/pt-BR.lproj/Localizable.strings b/BookPlayerWatch/pt-BR.lproj/Localizable.strings index 4a6f3b226..56adba256 100644 --- a/BookPlayerWatch/pt-BR.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-BR.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "REPRODUÇÃO AUTOMÁTICA"; "speed_title" = "velocidade"; "logout_title" = "Sair"; -"watchapp_login_description" = "Transmita seus livros recentes para o seu Apple Watch ou baixe-os para ouvir offline em qualquer lugar."; +"benefits_watchapp_description" = "Transmita seus livros recentes para o seu Apple Watch ou baixe-os para ouvir offline em qualquer lugar."; diff --git a/BookPlayerWatch/pt-PT.lproj/Localizable.strings b/BookPlayerWatch/pt-PT.lproj/Localizable.strings index 97424d782..995631e40 100644 --- a/BookPlayerWatch/pt-PT.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-PT.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "REPRODUÇÃO AUTOMÁTICA"; "speed_title" = "velocidade"; "logout_title" = "Sair"; -"watchapp_login_description" = "Transmita seus livros recentes para o seu Apple Watch ou baixe-os para ouvir offline em qualquer lugar."; +"benefits_watchapp_description" = "Transmita seus livros recentes para o seu Apple Watch ou baixe-os para ouvir offline em qualquer lugar."; diff --git a/BookPlayerWatch/ro.lproj/Localizable.strings b/BookPlayerWatch/ro.lproj/Localizable.strings index f2ea5f549..5aa01d269 100644 --- a/BookPlayerWatch/ro.lproj/Localizable.strings +++ b/BookPlayerWatch/ro.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "REDARE AUTOMATA"; "speed_title" = "Viteza"; "logout_title" = "Deconectați-vă"; -"watchapp_login_description" = "Transmiteți-vă cărțile recente pe Apple Watch sau descărcați-le pentru a le asculta offline din mers."; +"benefits_watchapp_description" = "Transmiteți-vă cărțile recente pe Apple Watch sau descărcați-le pentru a le asculta offline din mers."; diff --git a/BookPlayerWatch/ru.lproj/Localizable.strings b/BookPlayerWatch/ru.lproj/Localizable.strings index 031bfe501..277665426 100644 --- a/BookPlayerWatch/ru.lproj/Localizable.strings +++ b/BookPlayerWatch/ru.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "АВТОВОСПРОИЗВЕДЕНИЕ"; "speed_title" = "скорость"; "logout_title" = "Выйти"; -"watchapp_login_description" = "Транслируйте недавно прочитанные книги на Apple Watch или загружайте их, чтобы слушать их офлайн в дороге."; +"benefits_watchapp_description" = "Транслируйте недавно прочитанные книги на Apple Watch или загружайте их, чтобы слушать их офлайн в дороге."; diff --git a/BookPlayerWatch/sk-SK.lproj/Localizable.strings b/BookPlayerWatch/sk-SK.lproj/Localizable.strings index 6caa6ad05..28b8ecbe1 100644 --- a/BookPlayerWatch/sk-SK.lproj/Localizable.strings +++ b/BookPlayerWatch/sk-SK.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTOMATICKÉ PREHRÁVANIE"; "speed_title" = "rýchlosť"; "logout_title" = "Odhlásiť sa"; -"watchapp_login_description" = "Streamujte svoje nedávne knihy do hodiniek Apple Watch alebo si ich stiahnite a počúvajte offline na cestách."; +"benefits_watchapp_description" = "Streamujte svoje nedávne knihy do hodiniek Apple Watch alebo si ich stiahnite a počúvajte offline na cestách."; diff --git a/BookPlayerWatch/sv.lproj/Localizable.strings b/BookPlayerWatch/sv.lproj/Localizable.strings index 79ce2e669..9c826b9a9 100644 --- a/BookPlayerWatch/sv.lproj/Localizable.strings +++ b/BookPlayerWatch/sv.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "AUTOSPELA"; "speed_title" = "hastighet"; "logout_title" = "Logga ut"; -"watchapp_login_description" = "Strömma dina senaste böcker till din Apple Watch eller ladda ner dem för att lyssna offline när du är på språng."; +"benefits_watchapp_description" = "Strömma dina senaste böcker till din Apple Watch eller ladda ner dem för att lyssna offline när du är på språng."; diff --git a/BookPlayerWatch/tr.lproj/Localizable.strings b/BookPlayerWatch/tr.lproj/Localizable.strings index 37af38e94..358aec92c 100644 --- a/BookPlayerWatch/tr.lproj/Localizable.strings +++ b/BookPlayerWatch/tr.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "OTOMATİK OYNATMA"; "speed_title" = "hız"; "logout_title" = "Çıkış Yap"; -"watchapp_login_description" = "Son okuduğunuz kitapları Apple Watch'unuza aktarın veya çevrimdışıyken dinlemek için indirin."; +"benefits_watchapp_description" = "Son okuduğunuz kitapları Apple Watch'unuza aktarın veya çevrimdışıyken dinlemek için indirin."; diff --git a/BookPlayerWatch/uk.lproj/Localizable.strings b/BookPlayerWatch/uk.lproj/Localizable.strings index b521d3693..8a94e7454 100644 --- a/BookPlayerWatch/uk.lproj/Localizable.strings +++ b/BookPlayerWatch/uk.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "АВТОМАТИЧНЕ ВІДТВОРЕННЯ"; "speed_title" = "швидкість"; "logout_title" = "Вийти"; -"watchapp_login_description" = "Транслюйте свої останні книги на Apple Watch або завантажуйте їх, щоб слухати в режимі офлайн у дорозі."; +"benefits_watchapp_description" = "Транслюйте свої останні книги на Apple Watch або завантажуйте їх, щоб слухати в режимі офлайн у дорозі."; diff --git a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings index 0a9c63232..cb01cc7a9 100644 --- a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings +++ b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings @@ -22,4 +22,4 @@ "settings_autoplay_section_title" = "自动播放"; "speed_title" = "速度"; "logout_title" = "登出"; -"watchapp_login_description" = "将您最近的书籍流式传输到您的 Apple Watch,或下载它们以便在旅途中离线收听。"; +"benefits_watchapp_description" = "将您最近的书籍流式传输到您的 Apple Watch,或下载它们以便在旅途中离线收听。"; From d321e1293dc4f90e558cc18c63cd624e051fe104 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Thu, 5 Dec 2024 16:36:39 -0500 Subject: [PATCH 20/31] Add storage removal option --- .../Base.lproj/Localizable.strings | 2 + .../Settings/Profile/ProfileView.swift | 96 ++++++++++++++++--- BookPlayerWatch/ar.lproj/Localizable.strings | 2 + BookPlayerWatch/cs.lproj/Localizable.strings | 2 + BookPlayerWatch/da.lproj/Localizable.strings | 2 + BookPlayerWatch/de.lproj/Localizable.strings | 2 + BookPlayerWatch/el.lproj/Localizable.strings | 2 + BookPlayerWatch/en.lproj/Localizable.strings | 2 + BookPlayerWatch/es.lproj/Localizable.strings | 2 + BookPlayerWatch/fi.lproj/Localizable.strings | 2 + BookPlayerWatch/fr.lproj/Localizable.strings | 2 + BookPlayerWatch/hu.lproj/Localizable.strings | 2 + BookPlayerWatch/it.lproj/Localizable.strings | 2 + BookPlayerWatch/nb.lproj/Localizable.strings | 2 + BookPlayerWatch/nl.lproj/Localizable.strings | 2 + BookPlayerWatch/pl.lproj/Localizable.strings | 2 + .../pt-BR.lproj/Localizable.strings | 2 + .../pt-PT.lproj/Localizable.strings | 2 + BookPlayerWatch/ro.lproj/Localizable.strings | 2 + BookPlayerWatch/ru.lproj/Localizable.strings | 2 + .../sk-SK.lproj/Localizable.strings | 2 + BookPlayerWatch/sv.lproj/Localizable.strings | 2 + BookPlayerWatch/tr.lproj/Localizable.strings | 2 + BookPlayerWatch/uk.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + 25 files changed, 131 insertions(+), 13 deletions(-) diff --git a/BookPlayerWatch/Base.lproj/Localizable.strings b/BookPlayerWatch/Base.lproj/Localizable.strings index 1d3d3e129..3ba05cc11 100644 --- a/BookPlayerWatch/Base.lproj/Localizable.strings +++ b/BookPlayerWatch/Base.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Library"; +"delete_button" = "Delete"; "ok_button" = "OK"; "recent_title" = "Recent"; +"storage_total_title" = "Total space used"; "watchapp_last_played_title" = "Last Played"; "watchapp_refresh_data_title" = "Refresh data"; "watchapp_connect_error_title" = "Connectivity Error"; diff --git a/BookPlayerWatch/Settings/Profile/ProfileView.swift b/BookPlayerWatch/Settings/Profile/ProfileView.swift index bd7448125..cb9f3e693 100644 --- a/BookPlayerWatch/Settings/Profile/ProfileView.swift +++ b/BookPlayerWatch/Settings/Profile/ProfileView.swift @@ -12,23 +12,58 @@ import SwiftUI struct ProfileView: View { @ForcedEnvironment(\.coreServices) var coreServices @Binding var account: Account? + @State private var totalSpaceUsed: String = "" @State private var isLoading = false @State private var error: Error? - var body: some View { - VStack { - Image(systemName: "person.crop.circle") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 45, height: 45) - if let email = coreServices.accountService.getAccount()?.email { - Text(verbatim: email) + init(account: Binding) { + self._account = account + self._totalSpaceUsed = .init(initialValue: getFolderSize()) + } + + func getFolderSize() -> String { + var folderSize: Int64 = 0 + let folderURL = DataManager.getProcessedFolderURL() + + let enumerator = FileManager.default.enumerator( + at: folderURL, + includingPropertiesForKeys: [], + options: [.skipsHiddenFiles], + errorHandler: { (url, error) -> Bool in + print("directoryEnumerator error at \(url): ", error) + return true } - Spacer() - Button("logout_title".localized) { - Task { + )! + + for case let fileURL as URL in enumerator { + guard let fileAttributes = try? FileManager.default.attributesOfItem(atPath: fileURL.path) else { continue } + folderSize += fileAttributes[FileAttributeKey.size] as? Int64 ?? 0 + } + + return ByteCountFormatter.string( + fromByteCount: folderSize, + countStyle: ByteCountFormatter.CountStyle.file + ) + } + + func deleteFolder() throws { + // Delete file item if it exists + let folderURL = DataManager.getProcessedFolderURL() + if FileManager.default.fileExists(atPath: folderURL.path) { + try FileManager.default.removeItem(at: folderURL) + } + /// Recreate folder + _ = DataManager.getProcessedFolderURL() + totalSpaceUsed = getFolderSize() + } + + var body: some View { + List { + Section { + Button { do { isLoading = true + try deleteFolder() try coreServices.accountService.logout() isLoading = false account = nil @@ -37,11 +72,46 @@ struct ProfileView: View { isLoading = false self.error = error } + } label: { + Text("logout_title".localized) + .frame(maxWidth: .infinity, alignment: .center) + } + .buttonStyle(PlainButtonStyle()) + .foregroundStyle(.red) + .listRowBackground(Color.clear) + } header: { + if let email = coreServices.accountService.getAccount()?.email { + Text(verbatim: email) + .foregroundColor(.secondary) + } + } + + Section { + Text(totalSpaceUsed) + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color.clear) + Button { + do { + isLoading = true + try deleteFolder() + isLoading = false + } catch { + isLoading = false + self.error = error + } + } label: { + Text("delete_button".localized) + .frame(maxWidth: .infinity, alignment: .center) } + .buttonStyle(PlainButtonStyle()) + .foregroundStyle(.red) + .listRowBackground(Color.clear) + } header: { + Text("storage_total_title".localized) + .foregroundColor(.secondary) } - .buttonStyle(PlainButtonStyle()) - .foregroundStyle(.red) } + .environment(\.defaultMinListRowHeight, 30) .errorAlert(error: $error) .overlay { Group { diff --git a/BookPlayerWatch/ar.lproj/Localizable.strings b/BookPlayerWatch/ar.lproj/Localizable.strings index fc2e6d8c9..32c4ed925 100644 --- a/BookPlayerWatch/ar.lproj/Localizable.strings +++ b/BookPlayerWatch/ar.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "المكتبة"; +"delete_button" = "حذف"; "ok_button" = "موافق"; "recent_title" = "مؤخراً"; +"storage_total_title" = "المساحة الإجمالية المُستَخدَمة"; "watchapp_last_played_title" = "التشغيل الاخير"; "watchapp_refresh_data_title" = "تحديث البيانات"; "watchapp_connect_error_title" = "خطأ في الاتصال"; diff --git a/BookPlayerWatch/cs.lproj/Localizable.strings b/BookPlayerWatch/cs.lproj/Localizable.strings index 273893597..487d3739c 100644 --- a/BookPlayerWatch/cs.lproj/Localizable.strings +++ b/BookPlayerWatch/cs.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Knihovna"; +"delete_button" = "Odstranit"; "ok_button" = "OK"; "recent_title" = "Nedávné"; +"storage_total_title" = "Celkový využitý prostor"; "watchapp_last_played_title" = "Naposledy přehráno"; "watchapp_refresh_data_title" = "Obnovit data"; "watchapp_connect_error_title" = "Chyba připojení"; diff --git a/BookPlayerWatch/da.lproj/Localizable.strings b/BookPlayerWatch/da.lproj/Localizable.strings index 35c11bff3..653efba86 100644 --- a/BookPlayerWatch/da.lproj/Localizable.strings +++ b/BookPlayerWatch/da.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Bibliotek"; +"delete_button" = "Slet"; "ok_button" = "OK"; "recent_title" = "Seneste"; +"storage_total_title" = "Samlet plads brugt"; "watchapp_last_played_title" = "Senest afspillet"; "watchapp_refresh_data_title" = "Genindlæs"; "watchapp_connect_error_title" = "Forbindelsesfejl"; diff --git a/BookPlayerWatch/de.lproj/Localizable.strings b/BookPlayerWatch/de.lproj/Localizable.strings index 019d013ef..4b0963934 100644 --- a/BookPlayerWatch/de.lproj/Localizable.strings +++ b/BookPlayerWatch/de.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Bibliothek"; +"delete_button" = "Löschen"; "ok_button" = "OK"; "recent_title" = "Kürzlich"; +"storage_total_title" = "Insgesamt belegter Speicher"; "watchapp_last_played_title" = "Zuletzt gespielt"; "watchapp_refresh_data_title" = "Daten aktualisieren"; "watchapp_connect_error_title" = "Verbindungsfehler"; diff --git a/BookPlayerWatch/el.lproj/Localizable.strings b/BookPlayerWatch/el.lproj/Localizable.strings index 0ef9f5d6e..91ef4175b 100644 --- a/BookPlayerWatch/el.lproj/Localizable.strings +++ b/BookPlayerWatch/el.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Βιβλιοθήκη"; +"delete_button" = "Διαγράφω"; "ok_button" = "Εντάξει"; "recent_title" = "Πρόσφατος"; +"storage_total_title" = "Συνολικός χώρος που χρησιμοποιείται"; "watchapp_last_played_title" = "Τελευταία αναπαραγωγή"; "watchapp_refresh_data_title" = "Ανανέωση δεδομένων"; "watchapp_connect_error_title" = "Σφάλμα συνδεσιμότητας"; diff --git a/BookPlayerWatch/en.lproj/Localizable.strings b/BookPlayerWatch/en.lproj/Localizable.strings index 1d3d3e129..3ba05cc11 100644 --- a/BookPlayerWatch/en.lproj/Localizable.strings +++ b/BookPlayerWatch/en.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Library"; +"delete_button" = "Delete"; "ok_button" = "OK"; "recent_title" = "Recent"; +"storage_total_title" = "Total space used"; "watchapp_last_played_title" = "Last Played"; "watchapp_refresh_data_title" = "Refresh data"; "watchapp_connect_error_title" = "Connectivity Error"; diff --git a/BookPlayerWatch/es.lproj/Localizable.strings b/BookPlayerWatch/es.lproj/Localizable.strings index 16db7894e..8fd70694c 100644 --- a/BookPlayerWatch/es.lproj/Localizable.strings +++ b/BookPlayerWatch/es.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Biblioteca"; +"delete_button" = "Eliminar"; "ok_button" = "OK"; "recent_title" = "Recientes"; +"storage_total_title" = "Espacio total utilizado"; "watchapp_last_played_title" = "Reciente"; "watchapp_refresh_data_title" = "Actualizar data"; "watchapp_connect_error_title" = "Error de conectividad"; diff --git a/BookPlayerWatch/fi.lproj/Localizable.strings b/BookPlayerWatch/fi.lproj/Localizable.strings index 1bb4c75eb..81e959a93 100644 --- a/BookPlayerWatch/fi.lproj/Localizable.strings +++ b/BookPlayerWatch/fi.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Kirjasto"; +"delete_button" = "Poista"; "ok_button" = "OK"; "recent_title" = "Viimeisin"; +"storage_total_title" = "Käytetty tila yhteensä"; "watchapp_last_played_title" = "Viimeksi soitettu"; "watchapp_refresh_data_title" = "Päivitä tiedot"; "watchapp_connect_error_title" = "Yhteysvirhe"; diff --git a/BookPlayerWatch/fr.lproj/Localizable.strings b/BookPlayerWatch/fr.lproj/Localizable.strings index c531db79e..04f7c3c9a 100644 --- a/BookPlayerWatch/fr.lproj/Localizable.strings +++ b/BookPlayerWatch/fr.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Bibliothèque"; +"delete_button" = "Supprimer"; "ok_button" = "OK"; "recent_title" = "Récent"; +"storage_total_title" = "Espace total utilisé"; "watchapp_last_played_title" = "Dernier lu"; "watchapp_refresh_data_title" = "Actualiser"; "watchapp_connect_error_title" = "Erreur de connexion"; diff --git a/BookPlayerWatch/hu.lproj/Localizable.strings b/BookPlayerWatch/hu.lproj/Localizable.strings index 90bce5bc7..fdc5dfe55 100644 --- a/BookPlayerWatch/hu.lproj/Localizable.strings +++ b/BookPlayerWatch/hu.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Könyvtár"; +"delete_button" = "Törlés"; "ok_button" = "OK"; "recent_title" = "Friss"; +"storage_total_title" = "Teljes felhasznált terület"; "watchapp_last_played_title" = "Utoljára lejátszott"; "watchapp_refresh_data_title" = "Adatok frissítése"; "watchapp_connect_error_title" = "Kapcsolódási hiba"; diff --git a/BookPlayerWatch/it.lproj/Localizable.strings b/BookPlayerWatch/it.lproj/Localizable.strings index f0b5a080a..44561fbc9 100644 --- a/BookPlayerWatch/it.lproj/Localizable.strings +++ b/BookPlayerWatch/it.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Libreria"; +"delete_button" = "Elimina"; "ok_button" = "OK"; "recent_title" = "Recente"; +"storage_total_title" = "Spazio totale utilizzato"; "watchapp_last_played_title" = "Ultimo Riprodotto"; "watchapp_refresh_data_title" = "Aggiorna i dati"; "watchapp_connect_error_title" = "Errore di connettività"; diff --git a/BookPlayerWatch/nb.lproj/Localizable.strings b/BookPlayerWatch/nb.lproj/Localizable.strings index 591c86fcf..900a642b2 100644 --- a/BookPlayerWatch/nb.lproj/Localizable.strings +++ b/BookPlayerWatch/nb.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Bibliotek"; +"delete_button" = "Slett"; "ok_button" = "OK"; "recent_title" = "Nylig"; +"storage_total_title" = "Total plass brukt"; "watchapp_last_played_title" = "Sist spilt"; "watchapp_refresh_data_title" = "Oppdater data"; "watchapp_connect_error_title" = "Tilkoblingsfeil"; diff --git a/BookPlayerWatch/nl.lproj/Localizable.strings b/BookPlayerWatch/nl.lproj/Localizable.strings index dd1cb7b5a..5624d53c6 100644 --- a/BookPlayerWatch/nl.lproj/Localizable.strings +++ b/BookPlayerWatch/nl.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Bibliotheek"; +"delete_button" = "Verwijderen"; "ok_button" = "Oké"; "recent_title" = "Recente"; +"storage_total_title" = "Totale gebruikte ruimte"; "watchapp_last_played_title" = "Laatst Afgespeeld"; "watchapp_refresh_data_title" = "Ververs data"; "watchapp_connect_error_title" = "Verbindingsfout"; diff --git a/BookPlayerWatch/pl.lproj/Localizable.strings b/BookPlayerWatch/pl.lproj/Localizable.strings index 4ba354d46..79b2e2c4b 100644 --- a/BookPlayerWatch/pl.lproj/Localizable.strings +++ b/BookPlayerWatch/pl.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Biblioteka"; +"delete_button" = "Usuń"; "ok_button" = "OK"; "recent_title" = "Ostatnie"; +"storage_total_title" = "Całkowita wykorzystana przestrzeń"; "watchapp_last_played_title" = "Ostatnio odtwarzane"; "watchapp_refresh_data_title" = "Odśwież dane"; "watchapp_connect_error_title" = "Błąd połączenia"; diff --git a/BookPlayerWatch/pt-BR.lproj/Localizable.strings b/BookPlayerWatch/pt-BR.lproj/Localizable.strings index 56adba256..b0e263bfa 100644 --- a/BookPlayerWatch/pt-BR.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-BR.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Biblioteca"; +"delete_button" = "Excluir"; "ok_button" = "OK"; "recent_title" = "Mais recente"; +"storage_total_title" = "Espaço total utilizado"; "watchapp_last_played_title" = "Último Reproduzido"; "watchapp_refresh_data_title" = "Atualizar dados"; "watchapp_connect_error_title" = "Erro de conexão"; diff --git a/BookPlayerWatch/pt-PT.lproj/Localizable.strings b/BookPlayerWatch/pt-PT.lproj/Localizable.strings index 995631e40..ce4b2bdfb 100644 --- a/BookPlayerWatch/pt-PT.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-PT.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Biblioteca"; +"delete_button" = "Apagar"; "ok_button" = "OK"; "recent_title" = "Recente"; +"storage_total_title" = "Total de armazenamento utilizado"; "watchapp_last_played_title" = "Último Reproduzido"; "watchapp_refresh_data_title" = "Actualizar dados"; "watchapp_connect_error_title" = "Erro de ligação"; diff --git a/BookPlayerWatch/ro.lproj/Localizable.strings b/BookPlayerWatch/ro.lproj/Localizable.strings index 5aa01d269..e53efc6d0 100644 --- a/BookPlayerWatch/ro.lproj/Localizable.strings +++ b/BookPlayerWatch/ro.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Bibliotecă"; +"delete_button" = "Ștergere"; "ok_button" = "OK"; "recent_title" = "Recent"; +"storage_total_title" = "Spațiul total folosit"; "watchapp_last_played_title" = "Ultimul redat"; "watchapp_refresh_data_title" = "Actualizați datele"; "watchapp_connect_error_title" = "Eroare de conectivitate"; diff --git a/BookPlayerWatch/ru.lproj/Localizable.strings b/BookPlayerWatch/ru.lproj/Localizable.strings index 277665426..b4b835b01 100644 --- a/BookPlayerWatch/ru.lproj/Localizable.strings +++ b/BookPlayerWatch/ru.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Библиотека"; +"delete_button" = "Удалить"; "ok_button" = "OK"; "recent_title" = "Последние"; +"storage_total_title" = "Использовано памяти"; "watchapp_last_played_title" = "Последняя воспроизведенная книга"; "watchapp_refresh_data_title" = "Обновить данные"; "watchapp_connect_error_title" = "Ошибка подключения"; diff --git a/BookPlayerWatch/sk-SK.lproj/Localizable.strings b/BookPlayerWatch/sk-SK.lproj/Localizable.strings index 28b8ecbe1..afe13e833 100644 --- a/BookPlayerWatch/sk-SK.lproj/Localizable.strings +++ b/BookPlayerWatch/sk-SK.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Knižnica"; +"delete_button" = "Odstrániť"; "ok_button" = "OK"; "recent_title" = "Nedávne"; +"storage_total_title" = "Celkový využitý priestor"; "watchapp_last_played_title" = "Naposledy prehraté"; "watchapp_refresh_data_title" = "Obnoviť dáta"; "watchapp_connect_error_title" = "Chyba pripojenia"; diff --git a/BookPlayerWatch/sv.lproj/Localizable.strings b/BookPlayerWatch/sv.lproj/Localizable.strings index 9c826b9a9..ee4ad09cd 100644 --- a/BookPlayerWatch/sv.lproj/Localizable.strings +++ b/BookPlayerWatch/sv.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Bibliotek"; +"delete_button" = "Radera"; "ok_button" = "OK"; "recent_title" = "Senaste"; +"storage_total_title" = "Totalt använt utrymme"; "watchapp_last_played_title" = "Senast spelad"; "watchapp_refresh_data_title" = "Uppdatera data"; "watchapp_connect_error_title" = "Anslutningsfel"; diff --git a/BookPlayerWatch/tr.lproj/Localizable.strings b/BookPlayerWatch/tr.lproj/Localizable.strings index 358aec92c..a0ae670ff 100644 --- a/BookPlayerWatch/tr.lproj/Localizable.strings +++ b/BookPlayerWatch/tr.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Kitaplık"; +"delete_button" = "Sil"; "ok_button" = "Tamam"; "recent_title" = "Son"; +"storage_total_title" = "Kullanılan toplam alan"; "watchapp_last_played_title" = "Son Oynatılan"; "watchapp_refresh_data_title" = "Verileri yenile"; "watchapp_connect_error_title" = "Bağlantı Hatası"; diff --git a/BookPlayerWatch/uk.lproj/Localizable.strings b/BookPlayerWatch/uk.lproj/Localizable.strings index 8a94e7454..dbb97fcf5 100644 --- a/BookPlayerWatch/uk.lproj/Localizable.strings +++ b/BookPlayerWatch/uk.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "Бібліотека"; +"delete_button" = "Видалити"; "ok_button" = "Гаразд"; "recent_title" = "Останні"; +"storage_total_title" = "Загальний використаний простір"; "watchapp_last_played_title" = "Остання відтворена"; "watchapp_refresh_data_title" = "Оновити дані"; "watchapp_connect_error_title" = "Помилка з'єднання"; diff --git a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings index cb01cc7a9..957094ea5 100644 --- a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings +++ b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings @@ -7,8 +7,10 @@ */ "library_title" = "有声书库"; +"delete_button" = "删除"; "ok_button" = "确定"; "recent_title" = "最近"; +"storage_total_title" = "总使用空间"; "watchapp_last_played_title" = "上次播放"; "watchapp_refresh_data_title" = "刷新数据"; "watchapp_connect_error_title" = "连接错误"; From 8f0eb74f39b71bdc1ab488d27dd3ceb0333dea8d Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Thu, 5 Dec 2024 23:49:54 -0500 Subject: [PATCH 21/31] Fix entitlements check --- Shared/Services/Account/AccountService.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Shared/Services/Account/AccountService.swift b/Shared/Services/Account/AccountService.swift index 43bd9fbb3..bb0f13b37 100644 --- a/Shared/Services/Account/AccountService.swift +++ b/Shared/Services/Account/AccountService.swift @@ -345,15 +345,15 @@ public final class AccountService: AccountServiceProtocol { public func getSecondOnboarding() async throws -> T { guard let customerInfo = Purchases.shared.cachedCustomerInfo, - customerInfo.activeSubscriptions.isEmpty, - let countryCode = await Storefront.currentStorefront?.countryCode + let countryCode = await Storefront.currentStorefront?.countryCode, + customerInfo.entitlements.all.isEmpty || customerInfo.entitlements.all["pro"]?.isActive == false else { throw SecondOnboardingError.notApplicable } return try await provider.request(.secondOnboarding( anonymousId: customerInfo.id, - firstSeen: customerInfo.firstSeen.timeIntervalSince1970, + firstSeen: Date.distantPast.timeIntervalSince1970, region: countryCode )) } From 28f657ae3e63b3a3f6a5b923c4079b7994928cbb Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Thu, 5 Dec 2024 23:53:24 -0500 Subject: [PATCH 22/31] revert test code --- Shared/Services/Account/AccountService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/Services/Account/AccountService.swift b/Shared/Services/Account/AccountService.swift index bb0f13b37..866fce982 100644 --- a/Shared/Services/Account/AccountService.swift +++ b/Shared/Services/Account/AccountService.swift @@ -353,7 +353,7 @@ public final class AccountService: AccountServiceProtocol { return try await provider.request(.secondOnboarding( anonymousId: customerInfo.id, - firstSeen: Date.distantPast.timeIntervalSince1970, + firstSeen: customerInfo.firstSeen.timeIntervalSince1970, region: countryCode )) } From 3b7c3a15b24e1a13cfb1fdfe9f5f0ad21a20f77c Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Fri, 6 Dec 2024 11:50:55 -0500 Subject: [PATCH 23/31] Update translations --- BookPlayer/Base.lproj/Localizable.strings | 4 ++-- .../Views/Base.lproj/PlusBannerView.xib | 21 ++++++++----------- BookPlayer/ar.lproj/Localizable.strings | 4 ++-- BookPlayer/cs.lproj/Localizable.strings | 4 ++-- BookPlayer/da.lproj/Localizable.strings | 4 ++-- BookPlayer/de.lproj/Localizable.strings | 4 ++-- BookPlayer/el.lproj/Localizable.strings | 4 ++-- BookPlayer/en.lproj/Localizable.strings | 4 ++-- BookPlayer/es.lproj/Localizable.strings | 4 ++-- BookPlayer/fi.lproj/Localizable.strings | 4 ++-- BookPlayer/fr.lproj/Localizable.strings | 4 ++-- BookPlayer/hu.lproj/Localizable.strings | 4 ++-- BookPlayer/it.lproj/Localizable.strings | 4 ++-- BookPlayer/nb.lproj/Localizable.strings | 4 ++-- BookPlayer/nl.lproj/Localizable.strings | 4 ++-- BookPlayer/pl.lproj/Localizable.strings | 4 ++-- BookPlayer/pt-BR.lproj/Localizable.strings | 4 ++-- BookPlayer/pt-PT.lproj/Localizable.strings | 4 ++-- BookPlayer/ro.lproj/Localizable.strings | 4 ++-- BookPlayer/ru.lproj/Localizable.strings | 4 ++-- BookPlayer/sk-SK.lproj/Localizable.strings | 4 ++-- BookPlayer/sv.lproj/Localizable.strings | 4 ++-- BookPlayer/tr.lproj/Localizable.strings | 4 ++-- BookPlayer/uk.lproj/Localizable.strings | 4 ++-- BookPlayer/zh-Hans.lproj/Localizable.strings | 4 ++-- .../Base.lproj/Localizable.strings | 2 +- BookPlayerWatch/ar.lproj/Localizable.strings | 2 +- BookPlayerWatch/cs.lproj/Localizable.strings | 2 +- BookPlayerWatch/da.lproj/Localizable.strings | 2 +- BookPlayerWatch/de.lproj/Localizable.strings | 2 +- BookPlayerWatch/el.lproj/Localizable.strings | 2 +- BookPlayerWatch/en.lproj/Localizable.strings | 2 +- BookPlayerWatch/es.lproj/Localizable.strings | 2 +- BookPlayerWatch/fi.lproj/Localizable.strings | 2 +- BookPlayerWatch/fr.lproj/Localizable.strings | 2 +- BookPlayerWatch/hu.lproj/Localizable.strings | 2 +- BookPlayerWatch/it.lproj/Localizable.strings | 2 +- BookPlayerWatch/nb.lproj/Localizable.strings | 2 +- BookPlayerWatch/nl.lproj/Localizable.strings | 2 +- BookPlayerWatch/pl.lproj/Localizable.strings | 2 +- .../pt-BR.lproj/Localizable.strings | 2 +- .../pt-PT.lproj/Localizable.strings | 2 +- BookPlayerWatch/ro.lproj/Localizable.strings | 2 +- BookPlayerWatch/ru.lproj/Localizable.strings | 2 +- .../sk-SK.lproj/Localizable.strings | 2 +- BookPlayerWatch/sv.lproj/Localizable.strings | 2 +- BookPlayerWatch/tr.lproj/Localizable.strings | 2 +- BookPlayerWatch/uk.lproj/Localizable.strings | 2 +- .../zh-Hans.lproj/Localizable.strings | 2 +- 49 files changed, 81 insertions(+), 84 deletions(-) diff --git a/BookPlayer/Base.lproj/Localizable.strings b/BookPlayer/Base.lproj/Localizable.strings index 101b7faf0..8f8bb175b 100644 --- a/BookPlayer/Base.lproj/Localizable.strings +++ b/BookPlayer/Base.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Siri Shortcuts are available on iOS 12 and above"; "support_bookplayer_title" = "Support BookPlayer"; -"support_bookplayer_description" = "Get BookPlayer Pro to support future development and get extra themes, app icons and cloud sync"; +"support_bookplayer_description" = "Support future development and get extra themes, app icons, cloud sync and stand-alone playback on the Apple Watch"; "learn_more_title" = "LEARN MORE"; "settings_appearance_title" = "Appearance"; "settings_theme_title" = "Theme"; @@ -321,4 +321,4 @@ We're working hard on providing a seamless experience, if possible, please conta "more_title" = "More"; "repeat_turn_on_title" = "Turn on Repeat for this book"; "repeat_turn_off_title" = "Turn off Repeat for this book"; -"benefits_watchapp_description" = "Stream your recent books to your Apple Watch, or download them to listen offline on the go."; +"benefits_watchapp_description" = "Stream or download your books and listen on the go without your phone."; diff --git a/BookPlayer/Settings/Plus Screen/Views/Base.lproj/PlusBannerView.xib b/BookPlayer/Settings/Plus Screen/Views/Base.lproj/PlusBannerView.xib index 9e24291b4..e63409879 100644 --- a/BookPlayer/Settings/Plus Screen/Views/Base.lproj/PlusBannerView.xib +++ b/BookPlayer/Settings/Plus Screen/Views/Base.lproj/PlusBannerView.xib @@ -1,9 +1,9 @@ - + - + @@ -21,27 +21,24 @@ - - + + - diff --git a/BookPlayer/ar.lproj/Localizable.strings b/BookPlayer/ar.lproj/Localizable.strings index b42a55787..36a949bcd 100644 --- a/BookPlayer/ar.lproj/Localizable.strings +++ b/BookPlayer/ar.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "موافق"; "siri_alert_description" = "تتوفر اختصارات Siri في iOS 12 والإصدارات الأحدث"; "support_bookplayer_title" = "دعم bookplayer"; -"support_bookplayer_description" = "احصل على BookPlayer Pro، لدعم التطوير المستقبلي، والحصول على أشكال وأيقونات إضافية للتطبيق، وخاصية المزامنة السحابية"; +"support_bookplayer_description" = "ادعم التطوير المستقبلي واحصل على سمات إضافية وأيقونات تطبيقات ومزامنة سحابية وتشغيل مستقل على Apple Watch"; "learn_more_title" = "التعرف على المزيد"; "settings_appearance_title" = "المظهر"; "settings_theme_title" = "المظهر"; @@ -321,4 +321,4 @@ "more_title" = "أكثر"; "repeat_turn_on_title" = "قم بتشغيل التكرار لهذا الكتاب"; "repeat_turn_off_title" = "إيقاف تكرار هذا الكتاب"; -"benefits_watchapp_description" = "قم ببث كتبك الأخيرة إلى Apple Watch، أو قم بتنزيلها للاستماع إليها أثناء التنقل دون الاتصال بالإنترنت."; +"benefits_watchapp_description" = "قم ببث كتبك أو تنزيلها واستمع إليها أثناء التنقل دون الحاجة إلى هاتفك."; diff --git a/BookPlayer/cs.lproj/Localizable.strings b/BookPlayer/cs.lproj/Localizable.strings index 1179fd192..2f5bdb612 100644 --- a/BookPlayer/cs.lproj/Localizable.strings +++ b/BookPlayer/cs.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Zkratky pro Siri jsou k dispozici v iOS 12 a vyšším"; "support_bookplayer_title" = "Podpořte BookPlayer"; -"support_bookplayer_description" = "Získejte BookPlayer Pro pro podporu budoucího vývoje a získejte další motivy, ikony aplikací a synchronizaci s cloudem"; +"support_bookplayer_description" = "Podpořte budoucí vývoj a získejte další motivy, ikony aplikací, cloudovou synchronizaci a samostatné přehrávání na Apple Watch"; "learn_more_title" = "Více"; "settings_appearance_title" = "Vzhled"; "settings_theme_title" = "Téma"; @@ -321,4 +321,4 @@ "more_title" = "Více"; "repeat_turn_on_title" = "Zapněte opakování pro tuto knihu"; "repeat_turn_off_title" = "Vypnout opakování pro tuto knihu"; -"benefits_watchapp_description" = "Streamujte své nedávné knihy do hodinek Apple Watch nebo si je stáhněte a poslouchejte offline na cestách."; +"benefits_watchapp_description" = "Streamujte nebo stahujte své knihy a poslouchejte je na cestách bez telefonu."; diff --git a/BookPlayer/da.lproj/Localizable.strings b/BookPlayer/da.lproj/Localizable.strings index 2bfd42a32..6874cd867 100644 --- a/BookPlayer/da.lproj/Localizable.strings +++ b/BookPlayer/da.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Siri-genveje er tilgængelige på iOS 12 og opefter"; "support_bookplayer_title" = "Støt BookPlayer"; -"support_bookplayer_description" = "Få BookPlayer Pro til at understøtte fremtidig udvikling og få ekstra temaer, appikoner og skysynkronisering"; +"support_bookplayer_description" = "Støt fremtidig udvikling og få ekstra temaer, appikoner, skysynkronisering og selvstændig afspilning på Apple Watch"; "learn_more_title" = "LÆR MERE"; "settings_appearance_title" = "Udseende"; "settings_theme_title" = "Tema"; @@ -321,4 +321,4 @@ "more_title" = "Mere"; "repeat_turn_on_title" = "Slå Gentag til for denne bog"; "repeat_turn_off_title" = "Slå Gentag fra for denne bog"; -"benefits_watchapp_description" = "Stream dine seneste bøger til dit Apple Watch, eller download dem for at lytte offline på farten."; +"benefits_watchapp_description" = "Stream eller download dine bøger, og lyt på farten uden din telefon."; diff --git a/BookPlayer/de.lproj/Localizable.strings b/BookPlayer/de.lproj/Localizable.strings index c73ae69aa..f3951f1dd 100644 --- a/BookPlayer/de.lproj/Localizable.strings +++ b/BookPlayer/de.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Siri Kurzbefehle sind erst ab iOS 12 verfügbar."; "support_bookplayer_title" = "BookPlayer unterstützen"; -"support_bookplayer_description" = "Holen Sie sich BookPlayer Pro, um zukünftige Entwicklungen zu unterstützen und zusätzliche Themen, App-Symbole und Cloud-Synchronisierung zu erhalten"; +"support_bookplayer_description" = "Unterstützen Sie zukünftige Entwicklungen und erhalten Sie zusätzliche Designs, App-Symbole, Cloud-Synchronisierung und eigenständige Wiedergabe auf der Apple Watch"; "learn_more_title" = "Mehr erfahren"; "settings_appearance_title" = "Erscheinungsbild"; "settings_theme_title" = "Design"; @@ -321,4 +321,4 @@ "more_title" = "Mehr"; "repeat_turn_on_title" = "Aktivieren Sie die Option „Wiederholen“ für dieses Buch"; "repeat_turn_off_title" = "Deaktivieren Sie „Wiederholen“ für dieses Buch"; -"benefits_watchapp_description" = "Streamen Sie Ihre letzten Bücher auf Ihre Apple Watch oder laden Sie sie herunter, um sie unterwegs offline anzuhören."; +"benefits_watchapp_description" = "Streamen oder laden Sie Ihre Bücher herunter und hören Sie sie unterwegs ohne Ihr Telefon."; diff --git a/BookPlayer/el.lproj/Localizable.strings b/BookPlayer/el.lproj/Localizable.strings index d63cc8164..5826e2dfc 100644 --- a/BookPlayer/el.lproj/Localizable.strings +++ b/BookPlayer/el.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "Εντάξει"; "siri_alert_description" = "Οι συντομεύσεις Siri είναι διαθέσιμες σε iOS 12 και νεότερη έκδοση"; "support_bookplayer_title" = "Υποστήριξη BookPlayer"; -"support_bookplayer_description" = "Αποκτήστε το BookPlayer Pro για υποστήριξη μελλοντικής ανάπτυξης και λήψη επιπλέον θεμάτων, εικονιδίων εφαρμογών και συγχρονισμού στο cloud"; +"support_bookplayer_description" = "Υποστήριξη μελλοντικής ανάπτυξης και λήψη επιπλέον θεμάτων, εικονιδίων εφαρμογών, συγχρονισμού cloud και αυτόνομης αναπαραγωγής στο Apple Watch"; "learn_more_title" = "ΜΑΘΕ ΠΕΡΙΣΣΟΤΕΡΑ"; "settings_appearance_title" = "Εμφάνιση"; "settings_theme_title" = "Θέμα"; @@ -321,4 +321,4 @@ "more_title" = "Περισσότερα"; "repeat_turn_on_title" = "Ενεργοποιήστε το Repeat για αυτό το βιβλίο"; "repeat_turn_off_title" = "Απενεργοποιήστε το Repeat για αυτό το βιβλίο"; -"benefits_watchapp_description" = "Μεταδώστε τα πρόσφατα βιβλία σας σε ροή στο Apple Watch ή κατεβάστε τα για να τα ακούσετε εκτός σύνδεσης εν κινήσει."; +"benefits_watchapp_description" = "Μεταδώστε ροή ή κατεβάστε τα βιβλία σας και ακούστε τα εν κινήσει χωρίς το τηλέφωνό σας."; diff --git a/BookPlayer/en.lproj/Localizable.strings b/BookPlayer/en.lproj/Localizable.strings index 918d9dda8..1a3e41386 100644 --- a/BookPlayer/en.lproj/Localizable.strings +++ b/BookPlayer/en.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Siri Shortcuts are available on iOS 12 and above"; "support_bookplayer_title" = "Support BookPlayer"; -"support_bookplayer_description" = "Get BookPlayer Pro to support future development and get extra themes, app icons and cloud sync"; +"support_bookplayer_description" = "Support future development and get extra themes, app icons, cloud sync and stand-alone playback on the Apple Watch"; "learn_more_title" = "LEARN MORE"; "settings_appearance_title" = "Appearance"; "settings_theme_title" = "Theme"; @@ -321,4 +321,4 @@ We're working hard on providing a seamless experience, if possible, please conta "more_title" = "More"; "repeat_turn_on_title" = "Turn on Repeat for this book"; "repeat_turn_off_title" = "Turn off Repeat for this book"; -"benefits_watchapp_description" = "Stream your recent books to your Apple Watch, or download them to listen offline on the go."; +"benefits_watchapp_description" = "Stream or download your books and listen on the go without your phone."; diff --git a/BookPlayer/es.lproj/Localizable.strings b/BookPlayer/es.lproj/Localizable.strings index 462f4d107..d3c529336 100644 --- a/BookPlayer/es.lproj/Localizable.strings +++ b/BookPlayer/es.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Los atajos de Siri están disponibles desde iOS 12 en adelante"; "support_bookplayer_title" = "Patrocina a BookPlayer"; -"support_bookplayer_description" = "Obtén BookPlayer Pro para patrocinar el desarrollo futuro y obtener temas de colores adicionales, iconos extras para la aplicación, y sincronización en la nube"; +"support_bookplayer_description" = "Patrocina el desarrollo futuro y obtén temas adicionales, íconos extras para la aplicación, sincronización en la nube y reproducción independiente en el Apple Watch"; "learn_more_title" = "VER MÁS"; "settings_appearance_title" = "Apariencia"; "settings_theme_title" = "Tema"; @@ -321,4 +321,4 @@ "more_title" = "Más"; "repeat_turn_on_title" = "Activar repetición para este libro"; "repeat_turn_off_title" = "Desactivar la repetición para este libro"; -"benefits_watchapp_description" = "Transmite tus libros recientes a tu Apple Watch o descárgalos para escucharlos sin conexión mientras viajas."; +"benefits_watchapp_description" = "Transmite o descarga tus libros y escúchalos mientras viajas sin tu teléfono."; diff --git a/BookPlayer/fi.lproj/Localizable.strings b/BookPlayer/fi.lproj/Localizable.strings index 5c466d1fa..f2524daef 100644 --- a/BookPlayer/fi.lproj/Localizable.strings +++ b/BookPlayer/fi.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Siri-pikakuvakkeet ovat saatavilla iOS 12:ssa ja uudemmissa"; "support_bookplayer_title" = "Tue BookPlayeriä"; -"support_bookplayer_description" = "Hanki BookPlayer Pro tukemaan tulevaa kehitystä ja hankkimaan lisäteemoja, sovelluskuvakkeita ja pilvisynkronointia"; +"support_bookplayer_description" = "Tue tulevaa kehitystä ja hanki lisäteemoja, sovelluskuvakkeita, pilvisynkronointia ja erillistä toistoa Apple Watchissa"; "learn_more_title" = "LUE LISÄÄ"; "settings_appearance_title" = "Ulkonäkö"; "settings_theme_title" = "Teema"; @@ -321,4 +321,4 @@ "more_title" = "Lisää"; "repeat_turn_on_title" = "Ota Toista käyttöön tälle kirjalle"; "repeat_turn_off_title" = "Poista Toista käytöstä tästä kirjasta"; -"benefits_watchapp_description" = "Suoratoista viimeisimmät kirjasi Apple Watchiin tai lataa ne kuunnellaksesi offline-tilassa liikkeellä ollessasi."; +"benefits_watchapp_description" = "Suoratoista tai lataa kirjojasi ja kuuntele tien päällä ilman puhelinta."; diff --git a/BookPlayer/fr.lproj/Localizable.strings b/BookPlayer/fr.lproj/Localizable.strings index de8d68e3f..c269d430c 100644 --- a/BookPlayer/fr.lproj/Localizable.strings +++ b/BookPlayer/fr.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Les raccourcis Siri sont disponibles sur iOS 12 et supérieur"; "support_bookplayer_title" = "Soutenir BookPlayer"; -"support_bookplayer_description" = "Obtenez BookPlayer Pro pour soutenir le développement futur et obtenir des thèmes, des icônes d'application supplémentaires et une synchronisation dans le cloud"; +"support_bookplayer_description" = "Soutenez le développement futur et obtenez des thèmes supplémentaires, des icônes d'application, la synchronisation dans le cloud et la lecture autonome sur l'Apple Watch"; "learn_more_title" = "EN SAVOIR PLUS"; "settings_appearance_title" = "Apparence"; "settings_theme_title" = "Thème"; @@ -321,4 +321,4 @@ "more_title" = "Plus"; "repeat_turn_on_title" = "Activer la répétition pour ce livre"; "repeat_turn_off_title" = "Désactiver la répétition pour ce livre"; -"benefits_watchapp_description" = "Diffusez vos livres récents sur votre Apple Watch ou téléchargez-les pour les écouter hors ligne lors de vos déplacements."; +"benefits_watchapp_description" = "Diffusez ou téléchargez vos livres et écoutez-les en déplacement sans votre téléphone."; diff --git a/BookPlayer/hu.lproj/Localizable.strings b/BookPlayer/hu.lproj/Localizable.strings index b9ffafead..3cd64d8e0 100644 --- a/BookPlayer/hu.lproj/Localizable.strings +++ b/BookPlayer/hu.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "A Siri-parancsok iOS 12-es vagy újabb verziókon érhetőek el"; "support_bookplayer_title" = "A BookPlayer támogatása"; -"support_bookplayer_description" = "Szerezd be a BookPlayer Pro-t, hogy támogassa a jövőbeni fejlesztést, és további témákat, alkalmazásikonokat és felhőszinkronizálást kapjon"; +"support_bookplayer_description" = "Támogassa a jövőbeli fejlesztéseket, és szerezzen be további témákat, alkalmazásikonokat, felhőszinkronizálást és önálló lejátszást az Apple Watchon"; "learn_more_title" = "RÉSZLETEK"; "settings_appearance_title" = "Megjelenés"; "settings_theme_title" = "Téma"; @@ -321,4 +321,4 @@ "more_title" = "Több"; "repeat_turn_on_title" = "Kapcsolja be az Ismétlés funkciót ennél a könyvnél"; "repeat_turn_off_title" = "Kapcsolja ki az Ismétlés funkciót ennél a könyvnél"; -"benefits_watchapp_description" = "Streamelje legutóbbi könyveit Apple Watch-ra, vagy töltse le őket, hogy offline hallgathassa útközben."; +"benefits_watchapp_description" = "Streamelje vagy töltse le könyveit, és telefonja nélkül hallgassa útközben."; diff --git a/BookPlayer/it.lproj/Localizable.strings b/BookPlayer/it.lproj/Localizable.strings index 95c8a79f7..173d85731 100644 --- a/BookPlayer/it.lproj/Localizable.strings +++ b/BookPlayer/it.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Le Shortcuts di Siri sono disponibili da iOS 12 in su"; "support_bookplayer_title" = "Supporta BookPlayer"; -"support_bookplayer_description" = "Ottieni BookPlayer Pro per supportare lo sviluppo futuro e ottenere temi extra, icone delle app e sincronizzazione cloud"; +"support_bookplayer_description" = "Supporta lo sviluppo futuro e ottieni temi extra, icone delle app, sincronizzazione cloud e riproduzione autonoma su Apple Watch"; "learn_more_title" = "PER SAPERNE DI PIÙ"; "settings_appearance_title" = "Aspetto"; "settings_theme_title" = "Tema"; @@ -321,4 +321,4 @@ "more_title" = "Di più"; "repeat_turn_on_title" = "Attiva Ripeti per questo libro"; "repeat_turn_off_title" = "Disattiva Ripeti per questo libro"; -"benefits_watchapp_description" = "Trasmetti in streaming i tuoi libri più recenti sul tuo Apple Watch oppure scaricali per ascoltarli offline ovunque ti trovi."; +"benefits_watchapp_description" = "Ascolta in streaming o scarica i tuoi libri e ascoltali ovunque tu sia, senza usare il telefono."; diff --git a/BookPlayer/nb.lproj/Localizable.strings b/BookPlayer/nb.lproj/Localizable.strings index 04fe227c2..d35ad3618 100644 --- a/BookPlayer/nb.lproj/Localizable.strings +++ b/BookPlayer/nb.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Siri-snarveier er tilgjengelige på iOS 12 og nyere"; "support_bookplayer_title" = "Støtt BookPlayer"; -"support_bookplayer_description" = "Skaff deg BookPlayer Pro for å støtte fremtidig utvikling og få ekstra temaer, appikoner og skysynkronisering"; +"support_bookplayer_description" = "Støtt fremtidig utvikling og få ekstra temaer, appikoner, skysynkronisering og frittstående avspilling på Apple Watch"; "learn_more_title" = "MER INFORMASJON"; "settings_appearance_title" = "Utseende"; "settings_theme_title" = "Tema"; @@ -321,4 +321,4 @@ Vi jobber hardt for å gi deg en sømløs opplevelse. Hvis mulig, kontakt oss p "more_title" = "Flere"; "repeat_turn_on_title" = "Slå på Gjenta for denne boken"; "repeat_turn_off_title" = "Slå av Gjenta for denne boken"; -"benefits_watchapp_description" = "Strøm de siste bøkene dine til Apple Watch, eller last dem ned for å lytte offline mens du er på farten."; +"benefits_watchapp_description" = "Stream eller last ned bøkene dine og lytt mens du er på farten uten telefonen."; diff --git a/BookPlayer/nl.lproj/Localizable.strings b/BookPlayer/nl.lproj/Localizable.strings index 28224c953..6867a5fe1 100644 --- a/BookPlayer/nl.lproj/Localizable.strings +++ b/BookPlayer/nl.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "Oké"; "siri_alert_description" = "Siri-snelkoppelingen zijn beschikbaar op iOS 12 en hoger"; "support_bookplayer_title" = "Ondersteuning BookPlayer"; -"support_bookplayer_description" = "Koop BookPlayer Pro om toekomstige ontwikkeling te ondersteunen en ontvang extra thema's, app-pictogrammen en cloudsynchronisatie"; +"support_bookplayer_description" = "Ondersteun toekomstige ontwikkelingen en ontvang extra thema's, app-pictogrammen, cloudsynchronisatie en zelfstandige weergave op de Apple Watch"; "learn_more_title" = "LEER MEER"; "settings_appearance_title" = "Uiterlijk"; "settings_theme_title" = "Thema"; @@ -321,4 +321,4 @@ "more_title" = "Meer"; "repeat_turn_on_title" = "Schakel Herhalen in voor dit boek"; "repeat_turn_off_title" = "Herhalen voor dit boek uitschakelen"; -"benefits_watchapp_description" = "Stream uw recente boeken naar uw Apple Watch of download ze om ze offline te beluisteren, waar u ook bent."; +"benefits_watchapp_description" = "Stream of download uw boeken en luister er onderweg naar, zonder uw telefoon."; diff --git a/BookPlayer/pl.lproj/Localizable.strings b/BookPlayer/pl.lproj/Localizable.strings index 31d02391b..f1c61790f 100644 --- a/BookPlayer/pl.lproj/Localizable.strings +++ b/BookPlayer/pl.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Skróty Siri są dostępne w systemie iOS 12 i nowszym"; "support_bookplayer_title" = "Wsparcie BookPlayer'a"; -"support_bookplayer_description" = "Pobierz BookPlayer Pro, aby wspierać przyszły rozwój i uzyskać dodatkowe motywy, ikony aplikacji i synchronizację z chmurą"; +"support_bookplayer_description" = "Wesprzyj przyszły rozwój i uzyskaj dodatkowe motywy, ikony aplikacji, synchronizację z chmurą i samodzielne odtwarzanie na Apple Watch"; "learn_more_title" = "Dowiedz się więcej"; "settings_appearance_title" = "Wygląd"; "settings_theme_title" = "Motyw"; @@ -321,4 +321,4 @@ "more_title" = "Więcej"; "repeat_turn_on_title" = "Włącz opcję Powtórz dla tej książki"; "repeat_turn_off_title" = "Wyłącz opcję Powtórz dla tej książki"; -"benefits_watchapp_description" = "Przesyłaj strumieniowo ostatnio przeczytane książki na swój zegarek Apple Watch lub pobieraj je, aby słuchać ich w trybie offline w podróży."; +"benefits_watchapp_description" = "Przesyłaj strumieniowo lub pobieraj książki i słuchaj ich w podróży, bez użycia telefonu."; diff --git a/BookPlayer/pt-BR.lproj/Localizable.strings b/BookPlayer/pt-BR.lproj/Localizable.strings index a78c2b431..cbcb343be 100644 --- a/BookPlayer/pt-BR.lproj/Localizable.strings +++ b/BookPlayer/pt-BR.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Os atalhos da Siri estão disponíveis no iOS 12 e superior"; "support_bookplayer_title" = "Suporte BookPlayer"; -"support_bookplayer_description" = "Obtenha o BookPlayer Pro para oferecer suporte ao desenvolvimento futuro e obter temas extras, ícones de aplicativos e sincronização na nuvem"; +"support_bookplayer_description" = "Dê suporte ao desenvolvimento futuro e obtenha temas extras, ícones de aplicativos, sincronização em nuvem e reprodução autônoma no Apple Watch"; "learn_more_title" = "SAIBA MAIS"; "settings_appearance_title" = "Aparência"; "settings_theme_title" = "Tema"; @@ -321,4 +321,4 @@ "more_title" = "Mais"; "repeat_turn_on_title" = "Ativar repetição para este livro"; "repeat_turn_off_title" = "Desativar Repetir para este livro"; -"benefits_watchapp_description" = "Transmita seus livros recentes para o seu Apple Watch ou baixe-os para ouvir offline em qualquer lugar."; +"benefits_watchapp_description" = "Transmita ou baixe seus livros e ouça em qualquer lugar, sem precisar do seu celular."; diff --git a/BookPlayer/pt-PT.lproj/Localizable.strings b/BookPlayer/pt-PT.lproj/Localizable.strings index 513a73065..37f4c75d3 100644 --- a/BookPlayer/pt-PT.lproj/Localizable.strings +++ b/BookPlayer/pt-PT.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Os atalhos da Siri só estão disponíveis no iOS 12 e posterior"; "support_bookplayer_title" = "Apoiar o BookPlayer"; -"support_bookplayer_description" = "Obter o BookPlayer Pro para apoiar o desenvolvimento futuro e obter temas extras, ícones de aplicações e sincronização na nuvem"; +"support_bookplayer_description" = "Dê suporte ao desenvolvimento futuro e obtenha temas extras, ícones de aplicativos, sincronização em nuvem e reprodução autônoma no Apple Watch"; "learn_more_title" = "SAIBA MAIS"; "settings_appearance_title" = "Aspecto"; "settings_theme_title" = "Tema"; @@ -321,4 +321,4 @@ "more_title" = "Mais"; "repeat_turn_on_title" = "Ativar repetição para este livro"; "repeat_turn_off_title" = "Desativar Repetir para este livro"; -"benefits_watchapp_description" = "Transmita seus livros recentes para o seu Apple Watch ou baixe-os para ouvir offline em qualquer lugar."; +"benefits_watchapp_description" = "Transmita ou baixe seus livros e ouça em qualquer lugar, sem precisar do seu celular."; diff --git a/BookPlayer/ro.lproj/Localizable.strings b/BookPlayer/ro.lproj/Localizable.strings index 33d8c2ab7..1727e1f55 100644 --- a/BookPlayer/ro.lproj/Localizable.strings +++ b/BookPlayer/ro.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Comenzile rapide de la Siri sunt disponibile pe iOS 12 și versiuni ulterioare"; "support_bookplayer_title" = "Sprijiniți BookPlayer"; -"support_bookplayer_description" = "Obțineți BookPlayer Pro pentru a sprijini dezvoltarea viitoare și pentru a obține teme suplimentare, pictograme pentru aplicații și sincronizare în cloud"; +"support_bookplayer_description" = "Sprijină dezvoltarea viitoare și obține teme suplimentare, pictograme pentru aplicații, sincronizare în cloud și redare autonomă pe Apple Watch"; "learn_more_title" = "AFLAȚI MAI MULTE"; "settings_appearance_title" = "Aspect"; "settings_theme_title" = "Tema"; @@ -321,4 +321,4 @@ "more_title" = "Mai mult"; "repeat_turn_on_title" = "Activați Repetare pentru această carte"; "repeat_turn_off_title" = "Dezactivează Repetarea pentru această carte"; -"benefits_watchapp_description" = "Transmiteți-vă cărțile recente pe Apple Watch sau descărcați-le pentru a le asculta offline din mers."; +"benefits_watchapp_description" = "Transmiteți în flux sau descărcați cărțile și ascultați din mers fără telefon."; diff --git a/BookPlayer/ru.lproj/Localizable.strings b/BookPlayer/ru.lproj/Localizable.strings index 382bfec0c..437603b89 100644 --- a/BookPlayer/ru.lproj/Localizable.strings +++ b/BookPlayer/ru.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Быстрые команды Siri доступны на iOS 12 и выше"; "support_bookplayer_title" = "Поддержите BookPlayer"; -"support_bookplayer_description" = "Получите BookPlayer Pro для поддержки будущих разработок и получения дополнительных тем, значков приложений и облачной синхронизации."; +"support_bookplayer_description" = "Поддержите будущую разработку и получите дополнительные темы, значки приложений, облачную синхронизацию и автономное воспроизведение на Apple Watch"; "learn_more_title" = "ПОДРОБНЕЕ"; "settings_appearance_title" = "Внешний вид"; "settings_theme_title" = "Тема"; @@ -321,4 +321,4 @@ "more_title" = "Более"; "repeat_turn_on_title" = "Включить повтор для этой книги"; "repeat_turn_off_title" = "Отключить повтор для этой книги"; -"benefits_watchapp_description" = "Транслируйте недавно прочитанные книги на Apple Watch или загружайте их, чтобы слушать их офлайн в дороге."; +"benefits_watchapp_description" = "Слушайте книги онлайн или скачивайте их, где бы вы ни находились, без телефона."; diff --git a/BookPlayer/sk-SK.lproj/Localizable.strings b/BookPlayer/sk-SK.lproj/Localizable.strings index f7912b994..bf30bcc6b 100644 --- a/BookPlayer/sk-SK.lproj/Localizable.strings +++ b/BookPlayer/sk-SK.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Skratky pre Siri sú k dispozícii v iOS 12 a vyššom"; "support_bookplayer_title" = "Podporte BookPlayer"; -"support_bookplayer_description" = "Získajte BookPlayer Pro na podporu budúceho vývoja a získajte ďalšie témy, ikony aplikácií a synchronizáciu s cloudom"; +"support_bookplayer_description" = "Podporte budúci vývoj a získajte ďalšie motívy, ikony aplikácií, synchronizáciu s cloudom a samostatné prehrávanie na hodinkách Apple Watch"; "learn_more_title" = "ZISTIŤ VIAC"; "settings_appearance_title" = "Vzhľad"; "settings_theme_title" = "Téma"; @@ -321,4 +321,4 @@ Usilovne pracujeme na poskytovaní bezproblémového zážitku, ak je to možné "more_title" = "Viac"; "repeat_turn_on_title" = "Zapnúť Opakovať pre túto knihu"; "repeat_turn_off_title" = "Vypnúť Opakovať pre túto knihu"; -"benefits_watchapp_description" = "Streamujte svoje nedávne knihy do hodiniek Apple Watch alebo si ich stiahnite a počúvajte offline na cestách."; +"benefits_watchapp_description" = "Streamujte alebo sťahujte svoje knihy a počúvajte na cestách bez telefónu."; diff --git a/BookPlayer/sv.lproj/Localizable.strings b/BookPlayer/sv.lproj/Localizable.strings index 82be6310f..6fa477e58 100644 --- a/BookPlayer/sv.lproj/Localizable.strings +++ b/BookPlayer/sv.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "OK"; "siri_alert_description" = "Siri-genvägar är tillgängliga på iOS 12 och högre"; "support_bookplayer_title" = "Stöd BookPlayer"; -"support_bookplayer_description" = "Skaffa BookPlayer Pro för att stödja framtida utveckling och få extra teman, appikoner och molnsynkronisering"; +"support_bookplayer_description" = "Stöd framtida utveckling och få extra teman, appikoner, molnsynkronisering och fristående uppspelning på Apple Watch"; "learn_more_title" = "LÄS MER"; "settings_appearance_title" = "Utseende"; "settings_theme_title" = "Tema"; @@ -321,4 +321,4 @@ "more_title" = "Mer"; "repeat_turn_on_title" = "Aktivera Upprepa för den här boken"; "repeat_turn_off_title" = "Stäng av Upprepa för den här boken"; -"benefits_watchapp_description" = "Strömma dina senaste böcker till din Apple Watch eller ladda ner dem för att lyssna offline när du är på språng."; +"benefits_watchapp_description" = "Streama eller ladda ner dina böcker och lyssna på språng utan din telefon."; diff --git a/BookPlayer/tr.lproj/Localizable.strings b/BookPlayer/tr.lproj/Localizable.strings index fcee18fba..f2c3fcfd2 100644 --- a/BookPlayer/tr.lproj/Localizable.strings +++ b/BookPlayer/tr.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "Tamam"; "siri_alert_description" = "Siri Kısayolları iOS 12 ve sonraki İOS sürümlerinde kullanılabilir"; "support_bookplayer_title" = "BookPlayer'ı Destekle"; -"support_bookplayer_description" = "Gelecekteki geliştirmeleri desteklemek ve ekstra temalar, uygulama simgeleri ve bulut senkronizasyonu elde etmek için BookPlayer Pro'yu edinin"; +"support_bookplayer_description" = "Gelecekteki geliştirmeleri destekleyin ve Apple Watch'ta ekstra temalar, uygulama simgeleri, bulut senkronizasyonu ve bağımsız oynatma özelliği edinin"; "learn_more_title" = "DAHA FAZLA BİLGİ EDİN"; "settings_appearance_title" = "Görünüm"; "settings_theme_title" = "Tema"; @@ -321,4 +321,4 @@ "more_title" = "Daha"; "repeat_turn_on_title" = "Bu kitap için Tekrarı açın"; "repeat_turn_off_title" = "Bu kitap için Tekrarı kapatın"; -"benefits_watchapp_description" = "Son okuduğunuz kitapları Apple Watch'unuza aktarın veya çevrimdışıyken dinlemek için indirin."; +"benefits_watchapp_description" = "Kitaplarınızı yayınlayın veya indirin ve telefonunuz olmadan hareket halindeyken dinleyin."; diff --git a/BookPlayer/uk.lproj/Localizable.strings b/BookPlayer/uk.lproj/Localizable.strings index 22d1299e9..75777e64f 100644 --- a/BookPlayer/uk.lproj/Localizable.strings +++ b/BookPlayer/uk.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "Гаразд"; "siri_alert_description" = "Команди Siri доступні для iOS 12 і вище"; "support_bookplayer_title" = "Підтримайте Bookplayer"; -"support_bookplayer_description" = "Отримайте BookPlayer Pro для підтримки майбутніх розробок і отримайте додаткові теми, піктограми додатків і хмарну синхронізацію"; +"support_bookplayer_description" = "Підтримуйте майбутні розробки та отримуйте додаткові теми, піктограми програм, хмарну синхронізацію та автономне відтворення на Apple Watch"; "learn_more_title" = "Дізнатись більше"; "settings_appearance_title" = "Зовнішній вигляд"; "settings_theme_title" = "Тема"; @@ -321,4 +321,4 @@ "more_title" = "більше"; "repeat_turn_on_title" = "Увімкніть повтор для цієї книги"; "repeat_turn_off_title" = "Вимкніть повтор для цієї книги"; -"benefits_watchapp_description" = "Транслюйте свої останні книги на Apple Watch або завантажуйте їх, щоб слухати в режимі офлайн у дорозі."; +"benefits_watchapp_description" = "Транслюйте або завантажуйте свої книги та слухайте їх у дорозі без телефону."; diff --git a/BookPlayer/zh-Hans.lproj/Localizable.strings b/BookPlayer/zh-Hans.lproj/Localizable.strings index 01c869538..83e4d9ec7 100644 --- a/BookPlayer/zh-Hans.lproj/Localizable.strings +++ b/BookPlayer/zh-Hans.lproj/Localizable.strings @@ -55,7 +55,7 @@ "ok_button" = "确定"; "siri_alert_description" = "Siri快捷方式在iOS 12及更高版本上可用"; "support_bookplayer_title" = "支持我们"; -"support_bookplayer_description" = "获取 BookPlayer Pro 以支持未来的开发并获得额外的主题、应用程序图标和云同步"; +"support_bookplayer_description" = "支持未来开发并在 Apple Watch 上获得额外的主题、应用程序图标、云同步和独立播放"; "learn_more_title" = "了解更多"; "settings_appearance_title" = "外观"; "settings_theme_title" = "主题"; @@ -321,4 +321,4 @@ "more_title" = "更多的"; "repeat_turn_on_title" = "为这本书开启重复"; "repeat_turn_off_title" = "关闭此书的重复功能"; -"benefits_watchapp_description" = "将您最近的书籍流式传输到您的 Apple Watch,或下载它们以便在旅途中离线收听。"; +"benefits_watchapp_description" = "流式传输或下载您的书籍并随时随地收听,无需使用手机。"; diff --git a/BookPlayerWatch/Base.lproj/Localizable.strings b/BookPlayerWatch/Base.lproj/Localizable.strings index 3ba05cc11..446409d67 100644 --- a/BookPlayerWatch/Base.lproj/Localizable.strings +++ b/BookPlayerWatch/Base.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTOPLAY"; "speed_title" = "speed"; "logout_title" = "Log out"; -"benefits_watchapp_description" = "Stream your recent books to your Apple Watch, or download them to listen offline on the go."; +"benefits_watchapp_description" = "Stream or download your books and listen on the go without your phone."; diff --git a/BookPlayerWatch/ar.lproj/Localizable.strings b/BookPlayerWatch/ar.lproj/Localizable.strings index 32c4ed925..3dd8fcc32 100644 --- a/BookPlayerWatch/ar.lproj/Localizable.strings +++ b/BookPlayerWatch/ar.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "التشغيل التلقائي"; "speed_title" = "السرعة"; "logout_title" = "تسجيل خروج"; -"benefits_watchapp_description" = "قم ببث كتبك الأخيرة إلى Apple Watch، أو قم بتنزيلها للاستماع إليها أثناء التنقل دون الاتصال بالإنترنت."; +"benefits_watchapp_description" = "قم ببث كتبك أو تنزيلها واستمع إليها أثناء التنقل دون الحاجة إلى هاتفك."; diff --git a/BookPlayerWatch/cs.lproj/Localizable.strings b/BookPlayerWatch/cs.lproj/Localizable.strings index 487d3739c..54abba682 100644 --- a/BookPlayerWatch/cs.lproj/Localizable.strings +++ b/BookPlayerWatch/cs.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTOMATICKÉ PŘEHRÁVÁNÍ"; "speed_title" = "Rychlost"; "logout_title" = "Odhlásit se"; -"benefits_watchapp_description" = "Streamujte své nedávné knihy do hodinek Apple Watch nebo si je stáhněte a poslouchejte offline na cestách."; +"benefits_watchapp_description" = "Streamujte nebo stahujte své knihy a poslouchejte je na cestách bez telefonu."; diff --git a/BookPlayerWatch/da.lproj/Localizable.strings b/BookPlayerWatch/da.lproj/Localizable.strings index 653efba86..1389046d8 100644 --- a/BookPlayerWatch/da.lproj/Localizable.strings +++ b/BookPlayerWatch/da.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTOMATISK AFSPILNING"; "speed_title" = "Hastighed"; "logout_title" = "Log ud"; -"benefits_watchapp_description" = "Stream dine seneste bøger til dit Apple Watch, eller download dem for at lytte offline på farten."; +"benefits_watchapp_description" = "Stream eller download dine bøger, og lyt på farten uden din telefon."; diff --git a/BookPlayerWatch/de.lproj/Localizable.strings b/BookPlayerWatch/de.lproj/Localizable.strings index 4b0963934..0886a1d7a 100644 --- a/BookPlayerWatch/de.lproj/Localizable.strings +++ b/BookPlayerWatch/de.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTOMATISCHES ABSPIELEN"; "speed_title" = "Wiedergabegeschwindigkeit"; "logout_title" = "Ausloggen"; -"benefits_watchapp_description" = "Streamen Sie Ihre letzten Bücher auf Ihre Apple Watch oder laden Sie sie herunter, um sie unterwegs offline anzuhören."; +"benefits_watchapp_description" = "Streamen oder laden Sie Ihre Bücher herunter und hören Sie sie unterwegs ohne Ihr Telefon."; diff --git a/BookPlayerWatch/el.lproj/Localizable.strings b/BookPlayerWatch/el.lproj/Localizable.strings index 91ef4175b..b04d85cb1 100644 --- a/BookPlayerWatch/el.lproj/Localizable.strings +++ b/BookPlayerWatch/el.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "ΑΥΤΟΜΑΤΗ ΑΝΑΠΑΡΑΓΩΓΗ"; "speed_title" = "Ταχύτητα"; "logout_title" = "Αποσύνδεση"; -"benefits_watchapp_description" = "Μεταδώστε τα πρόσφατα βιβλία σας σε ροή στο Apple Watch ή κατεβάστε τα για να τα ακούσετε εκτός σύνδεσης εν κινήσει."; +"benefits_watchapp_description" = "Μεταδώστε ροή ή κατεβάστε τα βιβλία σας και ακούστε τα εν κινήσει χωρίς το τηλέφωνό σας."; diff --git a/BookPlayerWatch/en.lproj/Localizable.strings b/BookPlayerWatch/en.lproj/Localizable.strings index 3ba05cc11..446409d67 100644 --- a/BookPlayerWatch/en.lproj/Localizable.strings +++ b/BookPlayerWatch/en.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTOPLAY"; "speed_title" = "speed"; "logout_title" = "Log out"; -"benefits_watchapp_description" = "Stream your recent books to your Apple Watch, or download them to listen offline on the go."; +"benefits_watchapp_description" = "Stream or download your books and listen on the go without your phone."; diff --git a/BookPlayerWatch/es.lproj/Localizable.strings b/BookPlayerWatch/es.lproj/Localizable.strings index 8fd70694c..e9cbea2c9 100644 --- a/BookPlayerWatch/es.lproj/Localizable.strings +++ b/BookPlayerWatch/es.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTO-REPRODUCCIÓN"; "speed_title" = "velocidad"; "logout_title" = "Cerrar sesión"; -"benefits_watchapp_description" = "Transmite tus libros recientes a tu Apple Watch o descárgalos para escucharlos sin conexión mientras viajas."; +"benefits_watchapp_description" = "Transmite o descarga tus libros y escúchalos mientras viajas sin tu teléfono."; diff --git a/BookPlayerWatch/fi.lproj/Localizable.strings b/BookPlayerWatch/fi.lproj/Localizable.strings index 81e959a93..3aa757bcd 100644 --- a/BookPlayerWatch/fi.lproj/Localizable.strings +++ b/BookPlayerWatch/fi.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTOMAATTINEN TOISTO"; "speed_title" = "nopeus"; "logout_title" = "Kirjautua ulos"; -"benefits_watchapp_description" = "Suoratoista viimeisimmät kirjasi Apple Watchiin tai lataa ne kuunnellaksesi offline-tilassa liikkeellä ollessasi."; +"benefits_watchapp_description" = "Suoratoista tai lataa kirjojasi ja kuuntele tien päällä ilman puhelinta."; diff --git a/BookPlayerWatch/fr.lproj/Localizable.strings b/BookPlayerWatch/fr.lproj/Localizable.strings index 04f7c3c9a..2eb455db5 100644 --- a/BookPlayerWatch/fr.lproj/Localizable.strings +++ b/BookPlayerWatch/fr.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "LECTURE AUTOMATIQUE"; "speed_title" = "vitesse"; "logout_title" = "Se déconnecter"; -"benefits_watchapp_description" = "Diffusez vos livres récents sur votre Apple Watch ou téléchargez-les pour les écouter hors ligne lors de vos déplacements."; +"benefits_watchapp_description" = "Diffusez ou téléchargez vos livres et écoutez-les en déplacement sans votre téléphone."; diff --git a/BookPlayerWatch/hu.lproj/Localizable.strings b/BookPlayerWatch/hu.lproj/Localizable.strings index fdc5dfe55..1168eb64b 100644 --- a/BookPlayerWatch/hu.lproj/Localizable.strings +++ b/BookPlayerWatch/hu.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTOMATIKUS LEJÁTSZÁS"; "speed_title" = "sebesség"; "logout_title" = "Kijelentkezés"; -"benefits_watchapp_description" = "Streamelje legutóbbi könyveit Apple Watch-ra, vagy töltse le őket, hogy offline hallgathassa útközben."; +"benefits_watchapp_description" = "Streamelje vagy töltse le könyveit, és telefonja nélkül hallgassa útközben."; diff --git a/BookPlayerWatch/it.lproj/Localizable.strings b/BookPlayerWatch/it.lproj/Localizable.strings index 44561fbc9..703d22695 100644 --- a/BookPlayerWatch/it.lproj/Localizable.strings +++ b/BookPlayerWatch/it.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "RIPRODUZIONE AUTOMATICA"; "speed_title" = "velocità"; "logout_title" = "Disconnettersi"; -"benefits_watchapp_description" = "Trasmetti in streaming i tuoi libri più recenti sul tuo Apple Watch oppure scaricali per ascoltarli offline ovunque ti trovi."; +"benefits_watchapp_description" = "Ascolta in streaming o scarica i tuoi libri e ascoltali ovunque tu sia, senza usare il telefono."; diff --git a/BookPlayerWatch/nb.lproj/Localizable.strings b/BookPlayerWatch/nb.lproj/Localizable.strings index 900a642b2..84c6c2251 100644 --- a/BookPlayerWatch/nb.lproj/Localizable.strings +++ b/BookPlayerWatch/nb.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTOPLAY"; "speed_title" = "hastighet"; "logout_title" = "Logg ut"; -"benefits_watchapp_description" = "Strøm de siste bøkene dine til Apple Watch, eller last dem ned for å lytte offline mens du er på farten."; +"benefits_watchapp_description" = "Stream eller last ned bøkene dine og lytt mens du er på farten uten telefonen."; diff --git a/BookPlayerWatch/nl.lproj/Localizable.strings b/BookPlayerWatch/nl.lproj/Localizable.strings index 5624d53c6..5d13d3446 100644 --- a/BookPlayerWatch/nl.lproj/Localizable.strings +++ b/BookPlayerWatch/nl.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTOMATISCH AFSPELEN"; "speed_title" = "snelheid"; "logout_title" = "Uitloggen"; -"benefits_watchapp_description" = "Stream uw recente boeken naar uw Apple Watch of download ze om ze offline te beluisteren, waar u ook bent."; +"benefits_watchapp_description" = "Stream of download uw boeken en luister er onderweg naar, zonder uw telefoon."; diff --git a/BookPlayerWatch/pl.lproj/Localizable.strings b/BookPlayerWatch/pl.lproj/Localizable.strings index 79b2e2c4b..872d9d476 100644 --- a/BookPlayerWatch/pl.lproj/Localizable.strings +++ b/BookPlayerWatch/pl.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTOMATYCZNE ODTWARZANIE"; "speed_title" = "prędkość"; "logout_title" = "Wyloguj"; -"benefits_watchapp_description" = "Przesyłaj strumieniowo ostatnio przeczytane książki na swój zegarek Apple Watch lub pobieraj je, aby słuchać ich w trybie offline w podróży."; +"benefits_watchapp_description" = "Przesyłaj strumieniowo lub pobieraj książki i słuchaj ich w podróży, bez użycia telefonu."; diff --git a/BookPlayerWatch/pt-BR.lproj/Localizable.strings b/BookPlayerWatch/pt-BR.lproj/Localizable.strings index b0e263bfa..3668b718d 100644 --- a/BookPlayerWatch/pt-BR.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-BR.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "REPRODUÇÃO AUTOMÁTICA"; "speed_title" = "velocidade"; "logout_title" = "Sair"; -"benefits_watchapp_description" = "Transmita seus livros recentes para o seu Apple Watch ou baixe-os para ouvir offline em qualquer lugar."; +"benefits_watchapp_description" = "Transmita ou baixe seus livros e ouça em qualquer lugar, sem precisar do seu celular."; diff --git a/BookPlayerWatch/pt-PT.lproj/Localizable.strings b/BookPlayerWatch/pt-PT.lproj/Localizable.strings index ce4b2bdfb..d9bd119f8 100644 --- a/BookPlayerWatch/pt-PT.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-PT.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "REPRODUÇÃO AUTOMÁTICA"; "speed_title" = "velocidade"; "logout_title" = "Sair"; -"benefits_watchapp_description" = "Transmita seus livros recentes para o seu Apple Watch ou baixe-os para ouvir offline em qualquer lugar."; +"benefits_watchapp_description" = "Transmita ou baixe seus livros e ouça em qualquer lugar, sem precisar do seu celular."; diff --git a/BookPlayerWatch/ro.lproj/Localizable.strings b/BookPlayerWatch/ro.lproj/Localizable.strings index e53efc6d0..6f2001d97 100644 --- a/BookPlayerWatch/ro.lproj/Localizable.strings +++ b/BookPlayerWatch/ro.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "REDARE AUTOMATA"; "speed_title" = "Viteza"; "logout_title" = "Deconectați-vă"; -"benefits_watchapp_description" = "Transmiteți-vă cărțile recente pe Apple Watch sau descărcați-le pentru a le asculta offline din mers."; +"benefits_watchapp_description" = "Transmiteți în flux sau descărcați cărțile și ascultați din mers fără telefon."; diff --git a/BookPlayerWatch/ru.lproj/Localizable.strings b/BookPlayerWatch/ru.lproj/Localizable.strings index b4b835b01..b8c69c7d4 100644 --- a/BookPlayerWatch/ru.lproj/Localizable.strings +++ b/BookPlayerWatch/ru.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "АВТОВОСПРОИЗВЕДЕНИЕ"; "speed_title" = "скорость"; "logout_title" = "Выйти"; -"benefits_watchapp_description" = "Транслируйте недавно прочитанные книги на Apple Watch или загружайте их, чтобы слушать их офлайн в дороге."; +"benefits_watchapp_description" = "Слушайте книги онлайн или скачивайте их, где бы вы ни находились, без телефона."; diff --git a/BookPlayerWatch/sk-SK.lproj/Localizable.strings b/BookPlayerWatch/sk-SK.lproj/Localizable.strings index afe13e833..9b34875df 100644 --- a/BookPlayerWatch/sk-SK.lproj/Localizable.strings +++ b/BookPlayerWatch/sk-SK.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTOMATICKÉ PREHRÁVANIE"; "speed_title" = "rýchlosť"; "logout_title" = "Odhlásiť sa"; -"benefits_watchapp_description" = "Streamujte svoje nedávne knihy do hodiniek Apple Watch alebo si ich stiahnite a počúvajte offline na cestách."; +"benefits_watchapp_description" = "Streamujte alebo sťahujte svoje knihy a počúvajte na cestách bez telefónu."; diff --git a/BookPlayerWatch/sv.lproj/Localizable.strings b/BookPlayerWatch/sv.lproj/Localizable.strings index ee4ad09cd..a2a931fe6 100644 --- a/BookPlayerWatch/sv.lproj/Localizable.strings +++ b/BookPlayerWatch/sv.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "AUTOSPELA"; "speed_title" = "hastighet"; "logout_title" = "Logga ut"; -"benefits_watchapp_description" = "Strömma dina senaste böcker till din Apple Watch eller ladda ner dem för att lyssna offline när du är på språng."; +"benefits_watchapp_description" = "Streama eller ladda ner dina böcker och lyssna på språng utan din telefon."; diff --git a/BookPlayerWatch/tr.lproj/Localizable.strings b/BookPlayerWatch/tr.lproj/Localizable.strings index a0ae670ff..095daabc7 100644 --- a/BookPlayerWatch/tr.lproj/Localizable.strings +++ b/BookPlayerWatch/tr.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "OTOMATİK OYNATMA"; "speed_title" = "hız"; "logout_title" = "Çıkış Yap"; -"benefits_watchapp_description" = "Son okuduğunuz kitapları Apple Watch'unuza aktarın veya çevrimdışıyken dinlemek için indirin."; +"benefits_watchapp_description" = "Kitaplarınızı yayınlayın veya indirin ve telefonunuz olmadan hareket halindeyken dinleyin."; diff --git a/BookPlayerWatch/uk.lproj/Localizable.strings b/BookPlayerWatch/uk.lproj/Localizable.strings index dbb97fcf5..0ea47a723 100644 --- a/BookPlayerWatch/uk.lproj/Localizable.strings +++ b/BookPlayerWatch/uk.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "АВТОМАТИЧНЕ ВІДТВОРЕННЯ"; "speed_title" = "швидкість"; "logout_title" = "Вийти"; -"benefits_watchapp_description" = "Транслюйте свої останні книги на Apple Watch або завантажуйте їх, щоб слухати в режимі офлайн у дорозі."; +"benefits_watchapp_description" = "Транслюйте або завантажуйте свої книги та слухайте їх у дорозі без телефону."; diff --git a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings index 957094ea5..3efbd951d 100644 --- a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings +++ b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings @@ -24,4 +24,4 @@ "settings_autoplay_section_title" = "自动播放"; "speed_title" = "速度"; "logout_title" = "登出"; -"benefits_watchapp_description" = "将您最近的书籍流式传输到您的 Apple Watch,或下载它们以便在旅途中离线收听。"; +"benefits_watchapp_description" = "流式传输或下载您的书籍并随时随地收听,无需使用手机。"; From 0a0cbb988b238e55c2268ddf33d01717cf33124f Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Fri, 6 Dec 2024 12:16:14 -0500 Subject: [PATCH 24/31] Add clarifying label to profile view --- BookPlayerWatch/Base.lproj/Localizable.strings | 1 + BookPlayerWatch/Settings/Profile/ProfileView.swift | 6 ++++++ BookPlayerWatch/ar.lproj/Localizable.strings | 1 + BookPlayerWatch/cs.lproj/Localizable.strings | 1 + BookPlayerWatch/da.lproj/Localizable.strings | 1 + BookPlayerWatch/de.lproj/Localizable.strings | 1 + BookPlayerWatch/el.lproj/Localizable.strings | 1 + BookPlayerWatch/en.lproj/Localizable.strings | 1 + BookPlayerWatch/es.lproj/Localizable.strings | 1 + BookPlayerWatch/fi.lproj/Localizable.strings | 1 + BookPlayerWatch/fr.lproj/Localizable.strings | 1 + BookPlayerWatch/hu.lproj/Localizable.strings | 1 + BookPlayerWatch/it.lproj/Localizable.strings | 1 + BookPlayerWatch/nb.lproj/Localizable.strings | 1 + BookPlayerWatch/nl.lproj/Localizable.strings | 1 + BookPlayerWatch/pl.lproj/Localizable.strings | 1 + BookPlayerWatch/pt-BR.lproj/Localizable.strings | 1 + BookPlayerWatch/pt-PT.lproj/Localizable.strings | 1 + BookPlayerWatch/ro.lproj/Localizable.strings | 1 + BookPlayerWatch/ru.lproj/Localizable.strings | 1 + BookPlayerWatch/sk-SK.lproj/Localizable.strings | 1 + BookPlayerWatch/sv.lproj/Localizable.strings | 1 + BookPlayerWatch/tr.lproj/Localizable.strings | 1 + BookPlayerWatch/uk.lproj/Localizable.strings | 1 + BookPlayerWatch/zh-Hans.lproj/Localizable.strings | 1 + 25 files changed, 30 insertions(+) diff --git a/BookPlayerWatch/Base.lproj/Localizable.strings b/BookPlayerWatch/Base.lproj/Localizable.strings index 446409d67..6b69ee479 100644 --- a/BookPlayerWatch/Base.lproj/Localizable.strings +++ b/BookPlayerWatch/Base.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "speed"; "logout_title" = "Log out"; "benefits_watchapp_description" = "Stream or download your books and listen on the go without your phone."; +"subscription_required_title" = "Subscription required"; diff --git a/BookPlayerWatch/Settings/Profile/ProfileView.swift b/BookPlayerWatch/Settings/Profile/ProfileView.swift index cb9f3e693..9df4c272d 100644 --- a/BookPlayerWatch/Settings/Profile/ProfileView.swift +++ b/BookPlayerWatch/Settings/Profile/ProfileView.swift @@ -60,6 +60,12 @@ struct ProfileView: View { var body: some View { List { Section { + if !coreServices.hasSyncEnabled { + Text("subscription_required_title".localized) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + } + Button { do { isLoading = true diff --git a/BookPlayerWatch/ar.lproj/Localizable.strings b/BookPlayerWatch/ar.lproj/Localizable.strings index 3dd8fcc32..9fc1f13cc 100644 --- a/BookPlayerWatch/ar.lproj/Localizable.strings +++ b/BookPlayerWatch/ar.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "السرعة"; "logout_title" = "تسجيل خروج"; "benefits_watchapp_description" = "قم ببث كتبك أو تنزيلها واستمع إليها أثناء التنقل دون الحاجة إلى هاتفك."; +"subscription_required_title" = "الاشتراك مطلوب"; diff --git a/BookPlayerWatch/cs.lproj/Localizable.strings b/BookPlayerWatch/cs.lproj/Localizable.strings index 54abba682..c535e8823 100644 --- a/BookPlayerWatch/cs.lproj/Localizable.strings +++ b/BookPlayerWatch/cs.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "Rychlost"; "logout_title" = "Odhlásit se"; "benefits_watchapp_description" = "Streamujte nebo stahujte své knihy a poslouchejte je na cestách bez telefonu."; +"subscription_required_title" = "Je vyžadováno předplatné"; diff --git a/BookPlayerWatch/da.lproj/Localizable.strings b/BookPlayerWatch/da.lproj/Localizable.strings index 1389046d8..71c987e02 100644 --- a/BookPlayerWatch/da.lproj/Localizable.strings +++ b/BookPlayerWatch/da.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "Hastighed"; "logout_title" = "Log ud"; "benefits_watchapp_description" = "Stream eller download dine bøger, og lyt på farten uden din telefon."; +"subscription_required_title" = "Kræver abonnement"; diff --git a/BookPlayerWatch/de.lproj/Localizable.strings b/BookPlayerWatch/de.lproj/Localizable.strings index 0886a1d7a..34dc8ff3a 100644 --- a/BookPlayerWatch/de.lproj/Localizable.strings +++ b/BookPlayerWatch/de.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "Wiedergabegeschwindigkeit"; "logout_title" = "Ausloggen"; "benefits_watchapp_description" = "Streamen oder laden Sie Ihre Bücher herunter und hören Sie sie unterwegs ohne Ihr Telefon."; +"subscription_required_title" = "Abonnement erforderlich"; diff --git a/BookPlayerWatch/el.lproj/Localizable.strings b/BookPlayerWatch/el.lproj/Localizable.strings index b04d85cb1..71f11fef4 100644 --- a/BookPlayerWatch/el.lproj/Localizable.strings +++ b/BookPlayerWatch/el.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "Ταχύτητα"; "logout_title" = "Αποσύνδεση"; "benefits_watchapp_description" = "Μεταδώστε ροή ή κατεβάστε τα βιβλία σας και ακούστε τα εν κινήσει χωρίς το τηλέφωνό σας."; +"subscription_required_title" = "Απαιτείται συνδρομή"; diff --git a/BookPlayerWatch/en.lproj/Localizable.strings b/BookPlayerWatch/en.lproj/Localizable.strings index 446409d67..6b69ee479 100644 --- a/BookPlayerWatch/en.lproj/Localizable.strings +++ b/BookPlayerWatch/en.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "speed"; "logout_title" = "Log out"; "benefits_watchapp_description" = "Stream or download your books and listen on the go without your phone."; +"subscription_required_title" = "Subscription required"; diff --git a/BookPlayerWatch/es.lproj/Localizable.strings b/BookPlayerWatch/es.lproj/Localizable.strings index e9cbea2c9..4f0c263fe 100644 --- a/BookPlayerWatch/es.lproj/Localizable.strings +++ b/BookPlayerWatch/es.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "velocidad"; "logout_title" = "Cerrar sesión"; "benefits_watchapp_description" = "Transmite o descarga tus libros y escúchalos mientras viajas sin tu teléfono."; +"subscription_required_title" = "Se requiere suscripción"; diff --git a/BookPlayerWatch/fi.lproj/Localizable.strings b/BookPlayerWatch/fi.lproj/Localizable.strings index 3aa757bcd..20cb3d31d 100644 --- a/BookPlayerWatch/fi.lproj/Localizable.strings +++ b/BookPlayerWatch/fi.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "nopeus"; "logout_title" = "Kirjautua ulos"; "benefits_watchapp_description" = "Suoratoista tai lataa kirjojasi ja kuuntele tien päällä ilman puhelinta."; +"subscription_required_title" = "Tilaus vaaditaan"; diff --git a/BookPlayerWatch/fr.lproj/Localizable.strings b/BookPlayerWatch/fr.lproj/Localizable.strings index 2eb455db5..067010659 100644 --- a/BookPlayerWatch/fr.lproj/Localizable.strings +++ b/BookPlayerWatch/fr.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "vitesse"; "logout_title" = "Se déconnecter"; "benefits_watchapp_description" = "Diffusez ou téléchargez vos livres et écoutez-les en déplacement sans votre téléphone."; +"subscription_required_title" = "Abonnement requis"; diff --git a/BookPlayerWatch/hu.lproj/Localizable.strings b/BookPlayerWatch/hu.lproj/Localizable.strings index 1168eb64b..46b23b9eb 100644 --- a/BookPlayerWatch/hu.lproj/Localizable.strings +++ b/BookPlayerWatch/hu.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "sebesség"; "logout_title" = "Kijelentkezés"; "benefits_watchapp_description" = "Streamelje vagy töltse le könyveit, és telefonja nélkül hallgassa útközben."; +"subscription_required_title" = "Előfizetés szükséges"; diff --git a/BookPlayerWatch/it.lproj/Localizable.strings b/BookPlayerWatch/it.lproj/Localizable.strings index 703d22695..75edd51e3 100644 --- a/BookPlayerWatch/it.lproj/Localizable.strings +++ b/BookPlayerWatch/it.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "velocità"; "logout_title" = "Disconnettersi"; "benefits_watchapp_description" = "Ascolta in streaming o scarica i tuoi libri e ascoltali ovunque tu sia, senza usare il telefono."; +"subscription_required_title" = "Abbonamento richiesto"; diff --git a/BookPlayerWatch/nb.lproj/Localizable.strings b/BookPlayerWatch/nb.lproj/Localizable.strings index 84c6c2251..782c82889 100644 --- a/BookPlayerWatch/nb.lproj/Localizable.strings +++ b/BookPlayerWatch/nb.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "hastighet"; "logout_title" = "Logg ut"; "benefits_watchapp_description" = "Stream eller last ned bøkene dine og lytt mens du er på farten uten telefonen."; +"subscription_required_title" = "Abonnement kreves"; diff --git a/BookPlayerWatch/nl.lproj/Localizable.strings b/BookPlayerWatch/nl.lproj/Localizable.strings index 5d13d3446..c1e932a78 100644 --- a/BookPlayerWatch/nl.lproj/Localizable.strings +++ b/BookPlayerWatch/nl.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "snelheid"; "logout_title" = "Uitloggen"; "benefits_watchapp_description" = "Stream of download uw boeken en luister er onderweg naar, zonder uw telefoon."; +"subscription_required_title" = "Abonnement vereist"; diff --git a/BookPlayerWatch/pl.lproj/Localizable.strings b/BookPlayerWatch/pl.lproj/Localizable.strings index 872d9d476..d660931a4 100644 --- a/BookPlayerWatch/pl.lproj/Localizable.strings +++ b/BookPlayerWatch/pl.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "prędkość"; "logout_title" = "Wyloguj"; "benefits_watchapp_description" = "Przesyłaj strumieniowo lub pobieraj książki i słuchaj ich w podróży, bez użycia telefonu."; +"subscription_required_title" = "Wymagana subskrypcja"; diff --git a/BookPlayerWatch/pt-BR.lproj/Localizable.strings b/BookPlayerWatch/pt-BR.lproj/Localizable.strings index 3668b718d..ac7cba5f1 100644 --- a/BookPlayerWatch/pt-BR.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-BR.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "velocidade"; "logout_title" = "Sair"; "benefits_watchapp_description" = "Transmita ou baixe seus livros e ouça em qualquer lugar, sem precisar do seu celular."; +"subscription_required_title" = "Assinatura necessária"; diff --git a/BookPlayerWatch/pt-PT.lproj/Localizable.strings b/BookPlayerWatch/pt-PT.lproj/Localizable.strings index d9bd119f8..2384bb2d3 100644 --- a/BookPlayerWatch/pt-PT.lproj/Localizable.strings +++ b/BookPlayerWatch/pt-PT.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "velocidade"; "logout_title" = "Sair"; "benefits_watchapp_description" = "Transmita ou baixe seus livros e ouça em qualquer lugar, sem precisar do seu celular."; +"subscription_required_title" = "Assinatura necessária"; diff --git a/BookPlayerWatch/ro.lproj/Localizable.strings b/BookPlayerWatch/ro.lproj/Localizable.strings index 6f2001d97..cbc5c121f 100644 --- a/BookPlayerWatch/ro.lproj/Localizable.strings +++ b/BookPlayerWatch/ro.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "Viteza"; "logout_title" = "Deconectați-vă"; "benefits_watchapp_description" = "Transmiteți în flux sau descărcați cărțile și ascultați din mers fără telefon."; +"subscription_required_title" = "Este necesar un abonament"; diff --git a/BookPlayerWatch/ru.lproj/Localizable.strings b/BookPlayerWatch/ru.lproj/Localizable.strings index b8c69c7d4..f458b62fd 100644 --- a/BookPlayerWatch/ru.lproj/Localizable.strings +++ b/BookPlayerWatch/ru.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "скорость"; "logout_title" = "Выйти"; "benefits_watchapp_description" = "Слушайте книги онлайн или скачивайте их, где бы вы ни находились, без телефона."; +"subscription_required_title" = "Требуется подписка"; diff --git a/BookPlayerWatch/sk-SK.lproj/Localizable.strings b/BookPlayerWatch/sk-SK.lproj/Localizable.strings index 9b34875df..3c183ba45 100644 --- a/BookPlayerWatch/sk-SK.lproj/Localizable.strings +++ b/BookPlayerWatch/sk-SK.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "rýchlosť"; "logout_title" = "Odhlásiť sa"; "benefits_watchapp_description" = "Streamujte alebo sťahujte svoje knihy a počúvajte na cestách bez telefónu."; +"subscription_required_title" = "Vyžaduje sa predplatné"; diff --git a/BookPlayerWatch/sv.lproj/Localizable.strings b/BookPlayerWatch/sv.lproj/Localizable.strings index a2a931fe6..3bc744d0a 100644 --- a/BookPlayerWatch/sv.lproj/Localizable.strings +++ b/BookPlayerWatch/sv.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "hastighet"; "logout_title" = "Logga ut"; "benefits_watchapp_description" = "Streama eller ladda ner dina böcker och lyssna på språng utan din telefon."; +"subscription_required_title" = "Prenumeration krävs"; diff --git a/BookPlayerWatch/tr.lproj/Localizable.strings b/BookPlayerWatch/tr.lproj/Localizable.strings index 095daabc7..63aa78f89 100644 --- a/BookPlayerWatch/tr.lproj/Localizable.strings +++ b/BookPlayerWatch/tr.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "hız"; "logout_title" = "Çıkış Yap"; "benefits_watchapp_description" = "Kitaplarınızı yayınlayın veya indirin ve telefonunuz olmadan hareket halindeyken dinleyin."; +"subscription_required_title" = "Abonelik gerekli"; diff --git a/BookPlayerWatch/uk.lproj/Localizable.strings b/BookPlayerWatch/uk.lproj/Localizable.strings index 0ea47a723..b7649b77d 100644 --- a/BookPlayerWatch/uk.lproj/Localizable.strings +++ b/BookPlayerWatch/uk.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "швидкість"; "logout_title" = "Вийти"; "benefits_watchapp_description" = "Транслюйте або завантажуйте свої книги та слухайте їх у дорозі без телефону."; +"subscription_required_title" = "Потрібна підписка"; diff --git a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings index 3efbd951d..76341072b 100644 --- a/BookPlayerWatch/zh-Hans.lproj/Localizable.strings +++ b/BookPlayerWatch/zh-Hans.lproj/Localizable.strings @@ -25,3 +25,4 @@ "speed_title" = "速度"; "logout_title" = "登出"; "benefits_watchapp_description" = "流式传输或下载您的书籍并随时随地收听,无需使用手机。"; +"subscription_required_title" = "需要订阅"; From 96bd2f915682c93c3e097e766ca968bb743620f0 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Fri, 6 Dec 2024 13:14:35 -0500 Subject: [PATCH 25/31] Add watch icon support --- .../Views/StoryViewer/StoryActionView.swift | 131 ++++++++++-------- .../Utils/Views/StoryViewer/StoryView.swift | 11 ++ 2 files changed, 84 insertions(+), 58 deletions(-) diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryActionView.swift b/BookPlayer/Utils/Views/StoryViewer/StoryActionView.swift index 3dd67cebb..4df006cf1 100644 --- a/BookPlayer/Utils/Views/StoryViewer/StoryActionView.swift +++ b/BookPlayer/Utils/Views/StoryViewer/StoryActionView.swift @@ -45,7 +45,8 @@ struct StoryActionView: View { var body: some View { VStack { if showSlider, - let sliderOptions = action.sliderOptions { + let sliderOptions = action.sliderOptions + { VStack { Text(String(format: "$%.0f/mo", sliderValue)) .font(Font(Fonts.pricingTitle)) @@ -79,59 +80,71 @@ struct StoryActionView: View { .padding([.bottom], Spacing.L1) } else { - HStack(spacing: Spacing.S1) { - Spacer() - ForEach(action.options) { option in - PricingBoxView( - title: .constant(option.title), - isSelected: .constant(selected == option) - ) - .onTapGesture { - selected = option + VStack { + HStack(spacing: Spacing.S1) { + Spacer() + ForEach(action.options) { option in + PricingBoxView( + title: .constant(option.title), + isSelected: .constant(selected == option) + ) + .onTapGesture { + selected = option + } } + Spacer() + } + if action.sliderOptions != nil { + Button( + action: { + showSlider.toggle() + }, + label: { + Text("Choose custom amount") + .font(Font(Fonts.title)) + .foregroundColor(.white) + .underline() + .padding([.top], Spacing.S4) + } + ) } - Spacer() - } - if action.sliderOptions != nil { - Button(action: { - showSlider.toggle() - }, label: { - Text("Choose custom amount") - .font(Font(Fonts.title)) - .foregroundColor(.white) - .underline() - .padding([.top], Spacing.S4) - }) - } } + .padding([.bottom], Spacing.L1) + } - Button(action: { - if showSlider, - let option = sliderSelectedOption { - onSubscription(option) - } else { - onSubscription(selected) + Button( + action: { + if showSlider, + let option = sliderSelectedOption + { + onSubscription(option) + } else { + onSubscription(selected) + } + }, + label: { + Text(action.button) + .contentShape(Rectangle()) + .font(Font(Fonts.headline)) + .frame(height: 45) + .frame(maxWidth: .infinity) + .foregroundColor(Color(UIColor(hex: "334046"))) + .background(Color.white) + .cornerRadius(6) } - }, label: { - Text(action.button) - .contentShape(Rectangle()) - .font(Font(Fonts.headline)) - .frame(height: 45) - .frame(maxWidth: .infinity) - .foregroundColor(Color(UIColor(hex: "334046"))) - .background(Color.white) - .cornerRadius(6) - }) - .padding([.top], Spacing.L1) + ) if let dismiss = action.dismiss { - Button(action: { - onDismiss() - }, label: { - Text(dismiss) - .underline() - .font(Font(Fonts.body)) - .foregroundColor(.white) - }) + Button( + action: { + onDismiss() + }, + label: { + Text(dismiss) + .underline() + .font(Font(Fonts.body)) + .foregroundColor(.white) + } + ) .padding([.top], Spacing.S5) } } @@ -142,16 +155,18 @@ struct StoryActionView: View { ZStack { StoryBackgroundView() StoryActionView( - action: .constant(.init( - options: [ - .init(id: "supportTier4", title: "$3.99", price: 3.99), - .init(id: "proMonthly", title: "$4.99", price: 4.99), - .init(id: "supportTier10", title: "$9.99", price: 9.99) - ], - defaultOption: .init(id: "proMonthly", title: "$4.99", price: 4.99), - sliderOptions: .init(min: 3.99, max: 9.99), - button: "Continue" - )), + action: .constant( + .init( + options: [ + .init(id: "supportTier4", title: "$3.99", price: 3.99), + .init(id: "proMonthly", title: "$4.99", price: 4.99), + .init(id: "supportTier10", title: "$9.99", price: 9.99), + ], + defaultOption: .init(id: "proMonthly", title: "$4.99", price: 4.99), + sliderOptions: .init(min: 3.99, max: 9.99), + button: "Continue" + ) + ), onSubscription: { option in print(option.title) }, onDismiss: {} ) diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryView.swift b/BookPlayer/Utils/Views/StoryViewer/StoryView.swift index 89823bc71..027f9a0b6 100644 --- a/BookPlayer/Utils/Views/StoryViewer/StoryView.swift +++ b/BookPlayer/Utils/Views/StoryViewer/StoryView.swift @@ -28,6 +28,17 @@ struct StoryView: View { .accessibilityHidden(true) VStack { VStack { + if model.image == "apple-watch" { + Image(systemName: "applewatch.radiowaves.left.and.right") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 150) + .cornerRadius(9) + .allowsHitTesting(false) + .padding(.top, Spacing.L1 * 2) + .padding(.bottom, Spacing.L1) + .accessibilityHidden(true) + } Text(model.title) .shadow(radius: 2, y: 3) .font(Font(Fonts.titleStory)) From fe43a3457f3ee9a44c9cd4775c8bbb812c59d158 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 7 Dec 2024 08:51:54 -0500 Subject: [PATCH 26/31] Add support for double tap gesture --- BookPlayerWatch/PlayerControlsView.swift | 38 +++++++++++++------ .../RemoteItemList/RemoteItemListView.swift | 30 ++++++++++++++- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/BookPlayerWatch/PlayerControlsView.swift b/BookPlayerWatch/PlayerControlsView.swift index 396ca7c53..fafcb4e10 100644 --- a/BookPlayerWatch/PlayerControlsView.swift +++ b/BookPlayerWatch/PlayerControlsView.swift @@ -30,18 +30,34 @@ struct PlayerControlsView: View { .buttonStyle(PlainButtonStyle()) .frame(width: geometry.size.width * 0.28) Spacer() - Button { - playerManager.playPause() - } label: { - ResizeableImageView( - name: playerManager.isPlaying - ? "pause.fill" - : "play.fill" - ) - .padding(8) + if #available(watchOS 11.0, *) { + Button { + playerManager.playPause() + } label: { + ResizeableImageView( + name: playerManager.isPlaying + ? "pause.fill" + : "play.fill" + ) + .padding(8) + } + .buttonStyle(PlainButtonStyle()) + .frame(width: geometry.size.width * 0.28) + .handGestureShortcut(.primaryAction) + } else { + Button { + playerManager.playPause() + } label: { + ResizeableImageView( + name: playerManager.isPlaying + ? "pause.fill" + : "play.fill" + ) + .padding(8) + } + .buttonStyle(PlainButtonStyle()) + .frame(width: geometry.size.width * 0.28) } - .buttonStyle(PlainButtonStyle()) - .frame(width: geometry.size.width * 0.28) Spacer() Button { playerManager.forward() diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift index 353258e15..85c9d3079 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -128,8 +128,28 @@ struct RemoteItemListView: View { if folderRelativePath == nil { Section { if let lastPlayedItem { - RemoteItemListCellView(model: .init(item: lastPlayedItem, coreServices: coreServices)) - .onTapGesture { + if #available(watchOS 11.0, *) { + Button { + Task { + do { + isLoading = true + try await coreServices.playerLoaderService.loadPlayer( + lastPlayedItem.relativePath, + autoplay: true + ) + showPlayer = true + isLoading = false + } catch { + isLoading = false + self.error = error + } + } + } label: { + RemoteItemListCellView(model: .init(item: lastPlayedItem, coreServices: coreServices)) + } + .handGestureShortcut(.primaryAction) + } else { + Button { Task { do { isLoading = true @@ -144,7 +164,10 @@ struct RemoteItemListView: View { self.error = error } } + } label: { + RemoteItemListCellView(model: .init(item: lastPlayedItem, coreServices: coreServices)) } + } } } header: { Text(verbatim: "watchapp_last_played_title".localized) @@ -185,6 +208,9 @@ struct RemoteItemListView: View { Text(verbatim: folderRelativePath?.components(separatedBy: "/").last ?? "library_title".localized) .foregroundStyle(Color.accentColor) } + + Spacer().frame(height: 0) + .listRowBackground(Color.clear) } .frame(minWidth: geometry.size.width, minHeight: geometry.size.height) } From 60f7f98961870a15da6f1903a22315cda9df8279 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 7 Dec 2024 22:15:33 -0500 Subject: [PATCH 27/31] Fix continuation leak --- Shared/Services/LibraryService.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Shared/Services/LibraryService.swift b/Shared/Services/LibraryService.swift index 38b50ddc4..0ba02667a 100644 --- a/Shared/Services/LibraryService.swift +++ b/Shared/Services/LibraryService.swift @@ -1089,7 +1089,10 @@ extension LibraryService { context.perform { [unowned self, context] in guard let book = getItem(with: relativePath, context: context) as? Book - else { return } + else { + continuation.resume() + return + } let hadEmptyChapters = book.loadChaptersIfNeeded(from: asset, context: context) From 2c5fd0447383a99946623f7e500dd3061d71a9d2 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Mon, 9 Dec 2024 11:33:36 -0500 Subject: [PATCH 28/31] Fix resuming download on launch --- .../RemoteItemListCellView.swift | 30 ++++++- .../SimpleLibraryItem.swift | 1 + Shared/Services/Sync/SyncService.swift | 83 +++++++++++++------ 3 files changed, 86 insertions(+), 28 deletions(-) diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift index 31838f2ce..15758aa3f 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift @@ -13,6 +13,16 @@ struct RemoteItemListCellView: View { @ObservedObject var model: RemoteItemCellViewModel @State private var error: Error? + let numberFormatter: NumberFormatter + + init(model: RemoteItemCellViewModel) { + self.model = model + let formatter = NumberFormatter() + formatter.numberStyle = .percent + formatter.maximumFractionDigits = 2 + self.numberFormatter = formatter + } + var percentCompleted: String { guard model.item.progress > 0 else { return "" } @@ -23,6 +33,10 @@ struct RemoteItemListCellView: View { } } + func formattedProgress(_ progress: Double) -> String { + numberFormatter.string(from: NSNumber(value: progress)) ?? "" + } + var body: some View { HStack { VStack(alignment: .leading) { @@ -34,8 +48,20 @@ struct RemoteItemListCellView: View { .lineLimit(1) switch model.downloadState { case .downloading(let progress): - LinearProgressView(value: progress, fillColor: .white) - .frame(maxWidth: 100, maxHeight: 10) + HStack { + if #available(watchOS 10.0, *) { + Image(systemName: "icloud.and.arrow.down.fill") + .font(.caption2) + .symbolEffect(.pulse) + } else { + Image(systemName: "icloud.and.arrow.down.fill") + .font(.caption2) + } + LinearProgressView(value: progress, fillColor: .white) + .frame(maxWidth: 70, maxHeight: 10) + Text(formattedProgress(progress)) + .font(.footnote) + } case .downloaded: Text(Image(systemName: "applewatch")) .font(.caption2) diff --git a/Shared/CoreData/Lightweight-Models/SimpleLibraryItem.swift b/Shared/CoreData/Lightweight-Models/SimpleLibraryItem.swift index f5de9937e..7f5254552 100644 --- a/Shared/CoreData/Lightweight-Models/SimpleLibraryItem.swift +++ b/Shared/CoreData/Lightweight-Models/SimpleLibraryItem.swift @@ -69,6 +69,7 @@ public struct SimpleLibraryItem: Hashable, Identifiable { hasher.combine(title) hasher.combine(details) hasher.combine(percentCompleted) + hasher.combine(type.rawValue) } public init( diff --git a/Shared/Services/Sync/SyncService.swift b/Shared/Services/Sync/SyncService.swift index 9835f3050..1b42b8723 100644 --- a/Shared/Services/Sync/SyncService.swift +++ b/Shared/Services/Sync/SyncService.swift @@ -110,20 +110,7 @@ public final class SyncService: SyncServiceProtocol, BPLogger { /// Error publisher for ongoing-download tasks public var downloadErrorPublisher = PassthroughSubject<(String, Error), Never>() /// Background URL session to handle downloading synced items - private lazy var downloadURLSession: BPDownloadURLSession = { - BPDownloadURLSession { task, progress in - self.handleDownloadProgressUpdated( - task: task, - individualProgress: progress - ) - } didFinishDownloadingTask: { task, location, error in - self.handleFinishedDownload( - task: task, - location: location, - error: error - ) - } - }() + private var downloadURLSession: BPDownloadURLSession! private let provider: NetworkProvider @@ -144,6 +131,41 @@ public final class SyncService: SyncServiceProtocol, BPLogger { self.provider = NetworkProvider(client: client) bindObservers() + setupBackgroundDownloadSession() + } + + func setupBackgroundDownloadSession() { + self.downloadURLSession = BPDownloadURLSession { task, progress in + self.handleDownloadProgressUpdated( + task: task, + individualProgress: progress + ) + } didFinishDownloadingTask: { task, location, error in + self.handleFinishedDownload( + task: task, + location: location, + error: error + ) + } + + self.downloadURLSession.backgroundSession.getTasksWithCompletionHandler { _, _, downloadTasks in + for task in downloadTasks { + guard let relativePath = task.taskDescription else { continue } + + let paths: [String] = relativePath.allRanges(of: "/") + .map { String(relativePath.prefix(upTo: $0.lowerBound)) } + .reversed() + let parentFolder = paths.last + + let initiatingPath = parentFolder ?? relativePath + + var tasksArray = self.downloadTasksDictionary[initiatingPath] ?? [] + tasksArray.append(task) + self.downloadTasksDictionary[initiatingPath] = tasksArray + self.ongoingTasksParentReference[relativePath] = initiatingPath + self.initiatingFolderReference[relativePath] = paths.count > 1 ? parentFolder : nil + } + } } func bindObservers() { @@ -230,7 +252,8 @@ public final class SyncService: SyncServiceProtocol, BPLogger { let fetchedIdentifiers = try await fetchSyncedIdentifiers() if let itemsToUpload = await libraryService.getItemsToSync(remoteIdentifiers: fetchedIdentifiers), - !itemsToUpload.isEmpty { + !itemsToUpload.isEmpty + { Self.logger.trace("Scheduling upload tasks") await handleItemsToUpload(itemsToUpload) } @@ -270,7 +293,8 @@ public final class SyncService: SyncServiceProtocol, BPLogger { /// Only handle if the last item played is stored in the local library /// Note: we cannot just store the item, because we lack the info of the possible parent folders if let lastItemPlayed = response.lastItemPlayed, - await libraryService.itemExists(for: lastItemPlayed.relativePath) { + await libraryService.itemExists(for: lastItemPlayed.relativePath) + { try await handleSyncedLastPlayed(item: lastItemPlayed) } } @@ -292,7 +316,8 @@ public final class SyncService: SyncServiceProtocol, BPLogger { /// Only update the time if the remote last played timestamp is greater than the local timestamp if let remoteLastPlayDateTimestamp = item.lastPlayDateTimestamp, - remoteLastPlayDateTimestamp > localLastPlayDateTimestamp { + remoteLastPlayDateTimestamp > localLastPlayDateTimestamp + { await libraryService.updateInfo(for: item) throw BPSyncError.reloadLastBook(item.relativePath) } @@ -396,7 +421,8 @@ public final class SyncService: SyncServiceProtocol, BPLogger { downloadTasksDictionary[item.relativePath] = tasks ongoingTasksParentReference = tasks.reduce( - into: ongoingTasksParentReference, { + into: ongoingTasksParentReference, + { $0[$1.taskDescription!] = item.relativePath } ) @@ -485,7 +511,8 @@ extension SyncService { for folder in folders { if let contents = self.libraryService.getAllNestedItems(inside: folder.relativePath), - !contents.isEmpty { + !contents.isEmpty + { itemsToUpload.append(contentsOf: contents) } } @@ -559,7 +586,8 @@ extension SyncService { do { if error == nil, - let location { + let location + { let fileURL = DataManager.getProcessedFolderURL().appendingPathComponent(relativePath) /// If there's already something there, replace with new finished download @@ -593,7 +621,8 @@ extension SyncService { /// cleanup individual reference if downloadTasksDictionary[startingItemPath]? .filter({ $0 != task }) - .allSatisfy({ $0.state == .completed }) == true { + .allSatisfy({ $0.state == .completed }) == true + { downloadTasksDictionary[startingItemPath] = nil } ongoingTasksParentReference[relativePath] = nil @@ -625,7 +654,8 @@ extension SyncService { /// Clean up bound downloads if at least one was finished if item.type == .bound, - hasCompletedTasks { + hasCompletedTasks + { let fileURL = item.fileURL try FileManager.default.removeItem(at: fileURL) try FileManager.default.createDirectory( @@ -682,13 +712,14 @@ extension SyncService { let fileURL = item.fileURL - if (item.type == .bound || item.type == .folder), - let enumerator = FileManager.default.enumerator( + if item.type == .bound || item.type == .folder, + let enumerator = FileManager.default.enumerator( at: fileURL, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants] - ), - enumerator.nextObject() == nil { + ), + enumerator.nextObject() == nil + { return .notDownloaded } From e037f95424366acf0590d590581f246d7b7ffa71 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Mon, 9 Dec 2024 11:43:18 -0500 Subject: [PATCH 29/31] Update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3b34693c5..4ab5b246e 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Please visit our [Wiki](https://github.com/TortugaPower/BookPlayer/wiki) for our ### BookPlayer Pro - Cloud sync +- Stand-alone playback on your Apple Watch - Support Open Source development - Additional color themes - Select from alternative App Icons From f49221a0d87f718484a17cba1122991e7c9f71ea Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Mon, 9 Dec 2024 12:43:07 -0500 Subject: [PATCH 30/31] Rework pull to refresh --- BookPlayer.xcodeproj/project.pbxproj | 12 +- BookPlayerWatch/PlayerControlsView.swift | 35 ++-- ...llView.swift => RefreshableListView.swift} | 52 ++++-- .../RemoteItemList/RemoteItemListView.swift | 151 ++++++++---------- BookPlayerWatch/View+BookPlayer.swift | 29 ++++ 5 files changed, 149 insertions(+), 130 deletions(-) rename BookPlayerWatch/{RefreshableScrollView.swift => RefreshableListView.swift} (67%) create mode 100644 BookPlayerWatch/View+BookPlayer.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index efcffa7a4..189322045 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -343,7 +343,7 @@ 6334CF1B2CF8D87500F1FA17 /* SkipDurationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF1A2CF8D87500F1FA17 /* SkipDurationListView.swift */; }; 6334CF1D2CF90AF900F1FA17 /* PlayerMoreListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF1C2CF90AF900F1FA17 /* PlayerMoreListView.swift */; }; 6334CF1F2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF1E2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift */; }; - 6334CF212CFE330300F1FA17 /* RefreshableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF202CFE330300F1FA17 /* RefreshableScrollView.swift */; }; + 6334CF212CFE330300F1FA17 /* RefreshableListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334CF202CFE330300F1FA17 /* RefreshableListView.swift */; }; 633BE3E52AE6102D00F983AC /* BPShortcutsLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 633BE3E42AE6102D00F983AC /* BPShortcutsLink.swift */; }; 6340D5692AF12D48003D0B09 /* SharedWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6340D5682AF12D48003D0B09 /* SharedWidget.swift */; }; 6340D56E2AF13E4C003D0B09 /* RectangularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6340D56D2AF13E4C003D0B09 /* RectangularView.swift */; }; @@ -451,6 +451,7 @@ 63CD85232CE302D200EDBEA8 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85222CE302D200EDBEA8 /* LoginView.swift */; }; 63CD85272CE3064600EDBEA8 /* BP+ErrorAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85262CE3064600EDBEA8 /* BP+ErrorAlerts.swift */; }; 63CD85432CE3105300EDBEA8 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85422CE3105300EDBEA8 /* ProfileView.swift */; }; + 63E7DCC02D076185005B5E1F /* View+BookPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E7DCBF2D076185005B5E1F /* View+BookPlayer.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 */; }; @@ -1138,7 +1139,7 @@ 6334CF1A2CF8D87500F1FA17 /* SkipDurationListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkipDurationListView.swift; sourceTree = ""; }; 6334CF1C2CF90AF900F1FA17 /* PlayerMoreListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerMoreListView.swift; sourceTree = ""; }; 6334CF1E2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteItemCellViewModel.swift; sourceTree = ""; }; - 6334CF202CFE330300F1FA17 /* RefreshableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableScrollView.swift; sourceTree = ""; }; + 6334CF202CFE330300F1FA17 /* RefreshableListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableListView.swift; sourceTree = ""; }; 633BE3E12AE43D1300F983AC /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/AppShortcuts.strings"; sourceTree = ""; }; 633BE3E42AE6102D00F983AC /* BPShortcutsLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BPShortcutsLink.swift; sourceTree = ""; }; 6340D5682AF12D48003D0B09 /* SharedWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedWidget.swift; sourceTree = ""; }; @@ -1240,6 +1241,7 @@ 63CD85222CE302D200EDBEA8 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 63CD85262CE3064600EDBEA8 /* BP+ErrorAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BP+ErrorAlerts.swift"; sourceTree = ""; }; 63CD85422CE3105300EDBEA8 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; + 63E7DCBF2D076185005B5E1F /* View+BookPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+BookPlayer.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 = ""; }; @@ -1642,7 +1644,8 @@ 6350E4752CF4F6E90077CDC1 /* PlaybackFullControlsView.swift */, 6334CF1A2CF8D87500F1FA17 /* SkipDurationListView.swift */, 6334CF1C2CF90AF900F1FA17 /* PlayerMoreListView.swift */, - 6334CF202CFE330300F1FA17 /* RefreshableScrollView.swift */, + 6334CF202CFE330300F1FA17 /* RefreshableListView.swift */, + 63E7DCBF2D076185005B5E1F /* View+BookPlayer.swift */, 63CD851B2CE2963600EDBEA8 /* Settings */, 6399D06E2CEBA1F900A2E278 /* RemoteItemList */, 9FA334B427C156DB0064E8EA /* ItemList */, @@ -3415,10 +3418,11 @@ files = ( 9F82DF6927DE93A2001B0EA8 /* SkipIntervalView.swift in Sources */, 6399D0722CEBA37C00A2E278 /* RemoteItemListCellView.swift in Sources */, - 6334CF212CFE330300F1FA17 /* RefreshableScrollView.swift in Sources */, + 6334CF212CFE330300F1FA17 /* RefreshableListView.swift in Sources */, 9FA334B627C15DE30064E8EA /* VolumeView.swift in Sources */, 6350E46D2CF4315B0077CDC1 /* PlayerLoaderService.swift in Sources */, 6350E46B2CF42B500077CDC1 /* UserActivityManager.swift in Sources */, + 63E7DCC02D076185005B5E1F /* View+BookPlayer.swift in Sources */, 6334CF1F2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift in Sources */, 9FA334B927C1B8450064E8EA /* NowPlayingTitleView.swift in Sources */, 6350E4692CF425500077CDC1 /* WidgetReloadService.swift in Sources */, diff --git a/BookPlayerWatch/PlayerControlsView.swift b/BookPlayerWatch/PlayerControlsView.swift index fafcb4e10..c73d22419 100644 --- a/BookPlayerWatch/PlayerControlsView.swift +++ b/BookPlayerWatch/PlayerControlsView.swift @@ -30,34 +30,19 @@ struct PlayerControlsView: View { .buttonStyle(PlainButtonStyle()) .frame(width: geometry.size.width * 0.28) Spacer() - if #available(watchOS 11.0, *) { - Button { - playerManager.playPause() - } label: { - ResizeableImageView( - name: playerManager.isPlaying - ? "pause.fill" - : "play.fill" - ) - .padding(8) - } - .buttonStyle(PlainButtonStyle()) - .frame(width: geometry.size.width * 0.28) - .handGestureShortcut(.primaryAction) - } else { - Button { - playerManager.playPause() - } label: { - ResizeableImageView( - name: playerManager.isPlaying + Button { + playerManager.playPause() + } label: { + ResizeableImageView( + name: playerManager.isPlaying ? "pause.fill" : "play.fill" - ) - .padding(8) - } - .buttonStyle(PlainButtonStyle()) - .frame(width: geometry.size.width * 0.28) + ) + .padding(8) } + .buttonStyle(PlainButtonStyle()) + .frame(width: geometry.size.width * 0.28) + .applyPrimaryHandGesture() Spacer() Button { playerManager.forward() diff --git a/BookPlayerWatch/RefreshableScrollView.swift b/BookPlayerWatch/RefreshableListView.swift similarity index 67% rename from BookPlayerWatch/RefreshableScrollView.swift rename to BookPlayerWatch/RefreshableListView.swift index 742b9e1fb..f8f963759 100644 --- a/BookPlayerWatch/RefreshableScrollView.swift +++ b/BookPlayerWatch/RefreshableListView.swift @@ -1,5 +1,5 @@ // -// RefreshableScrollView.swift +// RefreshableListView.swift // BookPlayerWatch // // Created by Gianni Carlo on 2/12/24. @@ -11,9 +11,10 @@ import SwiftUI /// Pull to refresh does not work natively on WatchOS /// This implementation is inspired from this code: /// https://gist.github.com/swiftui-lab/3de557a513fbdb2d8fced41e40347e01 -struct RefreshableScrollView: View { +struct RefreshableListView: View { @State private var previousScrollOffset: CGFloat = 0 @State private var scrollOffset: CGFloat = 0 + @State private var rotation: Angle = .degrees(0) @Binding var refreshing: Bool let threshold: CGFloat = 40 @@ -22,25 +23,33 @@ struct RefreshableScrollView: View { init(refreshing: Binding, @ViewBuilder content: () -> Content) { self._refreshing = refreshing self.content = content() - } var body: some View { - ScrollView { - ZStack(alignment: .top) { - MovingView() - - content - .safeAreaInset(edge: .top) { - if refreshing { - ProgressView() - .tint(.white) - .background(Color.black.opacity(0.8)) - .frame(height: 10) + List { + Section { + if refreshing { + ProgressView() + .tint(.white) + .frame(height: 10) + .listRowBackground(Color.clear) + } else { + ZStack { + MovingView() + HStack { + Spacer() + Image(systemName: "arrow.down") + .rotationEffect(rotation) + Spacer() } } + .listRowBackground(Color.clear) + } } + content } + .customListSectionSpacing(0) + .environment(\.defaultMinListRowHeight, 1) .background(FixedView()) .onPreferenceChange(RefreshableKeyTypes.PrefKey.self) { values in refreshLogic(values: values) @@ -54,6 +63,7 @@ struct RefreshableScrollView: View { let fixedBounds = values.first { $0.vType == .fixedView }?.bounds ?? .zero self.scrollOffset = movingBounds.minY - fixedBounds.minY + self.rotation = self.symbolRotation(self.scrollOffset) // Crossing the threshold on the way down, we start the refresh process if !self.refreshing && (self.scrollOffset > self.threshold && self.previousScrollOffset <= self.threshold) { @@ -64,6 +74,20 @@ struct RefreshableScrollView: View { self.previousScrollOffset = self.scrollOffset } + func symbolRotation(_ scrollOffset: CGFloat) -> Angle { + // We will begin rotation, only after we have passed + // 60% of the way of reaching the threshold. + if scrollOffset < self.threshold * 0.60 { + return .degrees(0) + } else { + // Calculate rotation, based on the amount of scroll offset + let h = Double(self.threshold) + let d = Double(scrollOffset) + let v = max(min(d - (h * 0.6), h * 0.4), 0) + return .degrees(180 * v / (h * 0.4)) + } + } + struct MovingView: View { var body: some View { GeometryReader { proxy in diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift index 85c9d3079..4ca818831 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -30,11 +30,11 @@ struct RemoteItemListView: View { self.coreServices = coreServices self.playerManager = coreServices.playerManager let fetchedItems = - coreServices.libraryService.fetchContents( - at: folderRelativePath, - limit: nil, - offset: nil - ) ?? [] + coreServices.libraryService.fetchContents( + at: folderRelativePath, + limit: nil, + offset: nil + ) ?? [] self._items = .init(initialValue: fetchedItems) let lastItem = coreServices.libraryService.getLastPlayedItems(limit: 1)?.first self.folderRelativePath = folderRelativePath @@ -122,98 +122,73 @@ struct RemoteItemListView: View { } var body: some View { - GeometryReader { geometry in - RefreshableScrollView(refreshing: $isRefreshing) { - List { - if folderRelativePath == nil { - Section { - if let lastPlayedItem { - if #available(watchOS 11.0, *) { - Button { - Task { - do { - isLoading = true - try await coreServices.playerLoaderService.loadPlayer( - lastPlayedItem.relativePath, - autoplay: true - ) - showPlayer = true - isLoading = false - } catch { - isLoading = false - self.error = error - } - } - } label: { - RemoteItemListCellView(model: .init(item: lastPlayedItem, coreServices: coreServices)) - } - .handGestureShortcut(.primaryAction) - } else { - Button { - Task { - do { - isLoading = true - try await coreServices.playerLoaderService.loadPlayer( - lastPlayedItem.relativePath, - autoplay: true - ) - showPlayer = true - isLoading = false - } catch { - isLoading = false - self.error = error - } - } - } label: { - RemoteItemListCellView(model: .init(item: lastPlayedItem, coreServices: coreServices)) - } + RefreshableListView(refreshing: $isRefreshing) { + if folderRelativePath == nil { + Section { + if let lastPlayedItem { + Button { + Task { + do { + isLoading = true + try await coreServices.playerLoaderService.loadPlayer( + lastPlayedItem.relativePath, + autoplay: true + ) + showPlayer = true + isLoading = false + } catch { + isLoading = false + self.error = error } } - } header: { - Text(verbatim: "watchapp_last_played_title".localized) - .foregroundStyle(Color.accentColor) + } label: { + RemoteItemListCellView(model: .init(item: lastPlayedItem, coreServices: coreServices)) } + .applyPrimaryHandGesture() } + } header: { + Text(verbatim: "watchapp_last_played_title".localized) + .foregroundStyle(Color.accentColor) + } + } - Section { - ForEach(items) { item in - if item.type == .folder { - NavigationLink { - RemoteItemListView( - coreServices: coreServices, - folderRelativePath: item.relativePath - ) - } label: { - RemoteItemListCellView(model: .init(item: item, coreServices: coreServices)) - .foregroundColor(getForegroundColor(for: item)) - } - } else { - RemoteItemListCellView(model: .init(item: item, coreServices: coreServices)) - .onTapGesture { - Task { - do { - isLoading = true - try await coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) - showPlayer = true - isLoading = false - } catch { - isLoading = false - self.error = error - } - } + Section { + ForEach(items) { item in + if item.type == .folder { + NavigationLink { + RemoteItemListView( + coreServices: coreServices, + folderRelativePath: item.relativePath + ) + } label: { + RemoteItemListCellView(model: .init(item: item, coreServices: coreServices)) + .foregroundColor(getForegroundColor(for: item)) + } + } else { + RemoteItemListCellView(model: .init(item: item, coreServices: coreServices)) + .onTapGesture { + Task { + do { + isLoading = true + try await coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) + showPlayer = true + isLoading = false + } catch { + isLoading = false + self.error = error } + } } - } - } header: { - Text(verbatim: folderRelativePath?.components(separatedBy: "/").last ?? "library_title".localized) - .foregroundStyle(Color.accentColor) } - - Spacer().frame(height: 0) - .listRowBackground(Color.clear) } - .frame(minWidth: geometry.size.width, minHeight: geometry.size.height) + } header: { + Text(verbatim: folderRelativePath?.components(separatedBy: "/").last ?? "library_title".localized) + .foregroundStyle(Color.accentColor) + .padding(.top, folderRelativePath == nil ? 10 : 0) } + + Spacer().frame(height: 0) + .listRowBackground(Color.clear) } .ignoresSafeArea(edges: [.bottom]) .background( @@ -242,6 +217,8 @@ struct RemoteItemListView: View { guard newValue else { return } Task { + // Delay the task by 1 second to avoid jumping animations + try await Task.sleep(nanoseconds: 1_000_000_000) await syncListContents(ignoreLastTimestamp: true) isRefreshing = false } diff --git a/BookPlayerWatch/View+BookPlayer.swift b/BookPlayerWatch/View+BookPlayer.swift new file mode 100644 index 000000000..036e17e6c --- /dev/null +++ b/BookPlayerWatch/View+BookPlayer.swift @@ -0,0 +1,29 @@ +// +// View+BookPlayer.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 9/12/24. +// Copyright © 2024 Tortuga Power. All rights reserved. +// + +import SwiftUI + +extension View { + @ViewBuilder + func customListSectionSpacing(_ spacing: CGFloat) -> some View { + if #available(watchOS 10.0, *) { + listSectionSpacing(spacing) + } else { + self + } + } + + @ViewBuilder + func applyPrimaryHandGesture() -> some View { + if #available(watchOS 11.0, *) { + handGestureShortcut(.primaryAction) + } else { + self + } + } +} From 2412d5a93c61d32b7857340864af3ac866632470 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Tue, 10 Dec 2024 14:07:30 -0500 Subject: [PATCH 31/31] Setup spacing at the bottom --- BookPlayerWatch/RemoteItemList/RemoteItemListView.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift index 4ca818831..02f75de46 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -187,8 +187,13 @@ struct RemoteItemListView: View { .padding(.top, folderRelativePath == nil ? 10 : 0) } - Spacer().frame(height: 0) - .listRowBackground(Color.clear) + /// Create padding at the bottom + Section { + Spacer().frame(height: 10) + .listRowBackground(Color.clear) + } header: { + Text("") + } } .ignoresSafeArea(edges: [.bottom]) .background(