From 4d8ac4309d6ea32a301acb1c190d84d7d4784c06 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sun, 30 Nov 2025 22:59:38 -0500 Subject: [PATCH 1/9] Add sort by recent for Jellyfin --- .../Library Screen/AudiobookShelfLibraryView.swift | 2 +- .../Jellyfin/Library Screen/JellyfinLibraryView.swift | 1 + .../Library Screen/JellyfinLibraryViewModel.swift | 2 +- .../Jellyfin/Network/JellyfinConnectionService.swift | 8 +++++++- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/BookPlayer/AudiobookShelf/Library Screen/AudiobookShelfLibraryView.swift b/BookPlayer/AudiobookShelf/Library Screen/AudiobookShelfLibraryView.swift index 1cda95870..f548e28e2 100644 --- a/BookPlayer/AudiobookShelf/Library Screen/AudiobookShelfLibraryView.swift +++ b/BookPlayer/AudiobookShelf/Library Screen/AudiobookShelfLibraryView.swift @@ -88,7 +88,7 @@ struct AudiobookShelfLibraryView: } Section { Picker(selection: $viewModel.sortBy, label: Text("Sort by".localized)) { - Text("Recently Added".localized).tag(AudiobookShelfLayout.SortBy.recent) + Label("sort_most_recent_button", systemImage: "clock").tag(AudiobookShelfLayout.SortBy.recent) Label("Title".localized, systemImage: "textformat.abc").tag(AudiobookShelfLayout.SortBy.title) } } diff --git a/BookPlayer/Jellyfin/Library Screen/JellyfinLibraryView.swift b/BookPlayer/Jellyfin/Library Screen/JellyfinLibraryView.swift index 851549b97..33d5a4122 100644 --- a/BookPlayer/Jellyfin/Library Screen/JellyfinLibraryView.swift +++ b/BookPlayer/Jellyfin/Library Screen/JellyfinLibraryView.swift @@ -103,6 +103,7 @@ struct JellyfinLibraryView: View { Section { Picker(selection: $viewModel.sortBy, label: Text("Sort by".localized)) { Text("Default".localized).tag(JellyfinLayout.SortBy.smart) + Label("sort_most_recent_button", systemImage: "clock").tag(JellyfinLayout.SortBy.recent) Label("Name".localized, systemImage: "textformat.abc").tag(JellyfinLayout.SortBy.name) } } diff --git a/BookPlayer/Jellyfin/Library Screen/JellyfinLibraryViewModel.swift b/BookPlayer/Jellyfin/Library Screen/JellyfinLibraryViewModel.swift index ed10470cb..32703e493 100644 --- a/BookPlayer/Jellyfin/Library Screen/JellyfinLibraryViewModel.swift +++ b/BookPlayer/Jellyfin/Library Screen/JellyfinLibraryViewModel.swift @@ -62,7 +62,7 @@ enum JellyfinLayout { } enum SortBy: String { - case name, smart + case recent, name, smart } } diff --git a/BookPlayer/Jellyfin/Network/JellyfinConnectionService.swift b/BookPlayer/Jellyfin/Network/JellyfinConnectionService.swift index 04b9129bf..1e12902b7 100644 --- a/BookPlayer/Jellyfin/Network/JellyfinConnectionService.swift +++ b/BookPlayer/Jellyfin/Network/JellyfinConnectionService.swift @@ -118,18 +118,24 @@ class JellyfinConnectionService: BPLogger { sortBy: JellyfinLayout.SortBy ) async throws -> (items: [JellyfinLibraryItem], nextStartIndex: Int, maxCountItems: Int) { let orderBy: [JellyfinAPI.ItemSortBy] + let sortOrder: [JellyfinAPI.SortOrder] switch sortBy { + case .recent: + orderBy = [.dateCreated] + sortOrder = [.descending] case .name: orderBy = [.name] + sortOrder = [.ascending] case .smart: orderBy = [.isFolder, .sortName] + sortOrder = [.ascending] } let parameters = Paths.GetItemsParameters( startIndex: startIndex, limit: limit, isRecursive: false, - sortOrder: [.ascending], + sortOrder: sortOrder, parentID: folderID, fields: [.sortName], includeItemTypes: [.audioBook, .folder], From 8625f17e038889a4e2474cbbe877667d8857d999 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Wed, 3 Dec 2025 06:36:29 -0500 Subject: [PATCH 2/9] add support for editing volumes details field --- BookPlayer/Library/ItemDetails/ItemDetailsViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BookPlayer/Library/ItemDetails/ItemDetailsViewModel.swift b/BookPlayer/Library/ItemDetails/ItemDetailsViewModel.swift index ee65ba058..94bb7efb3 100644 --- a/BookPlayer/Library/ItemDetails/ItemDetailsViewModel.swift +++ b/BookPlayer/Library/ItemDetails/ItemDetailsViewModel.swift @@ -38,7 +38,7 @@ final class ItemDetailsViewModel: ObservableObject { @Published var originalFileName: String /// Title of the item @Published var title: String - /// Author of the item (only applies for books) + /// Author of the item (applies for books and volumes) @Published var author: String /// Artwork image @Published var selectedImage: UIImage? @@ -53,7 +53,7 @@ final class ItemDetailsViewModel: ObservableObject { /// Determines if there's an update for the artwork var artworkIsUpdated: Bool = false /// Flag to show the author field - var showAuthor: Bool { item.type == .book } + var showAuthor: Bool { item.type != .folder } @Published var hardcoverSectionViewModel: ItemDetailsHardcoverSectionView.Model? From 505c1cc1cfc1caeb063f35f079b745b0c708b85d Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 6 Dec 2025 09:32:46 -0500 Subject: [PATCH 3/9] Remove extra button from queued tasks alert --- BookPlayer/Library/ItemList/ItemListView+Alerts.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/BookPlayer/Library/ItemList/ItemListView+Alerts.swift b/BookPlayer/Library/ItemList/ItemListView+Alerts.swift index 432e2972d..ca0e59bde 100644 --- a/BookPlayer/Library/ItemList/ItemListView+Alerts.swift +++ b/BookPlayer/Library/ItemList/ItemListView+Alerts.swift @@ -37,7 +37,6 @@ extension ItemListView { @ViewBuilder func queuedTasksAlert() -> some View { - Text("sync_tasks_inprogress_alert_title") Button("sync_tasks_view_title") { activeSheet = .queuedTasks } From a0195a0aace8248d38b8ea3f927d97c0379e7f06 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 6 Dec 2025 09:33:05 -0500 Subject: [PATCH 4/9] Optimize fix all button for storage management --- BookPlayer/Settings/Storage/StorageView.swift | 51 +++++--- .../Settings/Storage/StorageViewModel.swift | 112 +++++++++++++++--- 2 files changed, 130 insertions(+), 33 deletions(-) diff --git a/BookPlayer/Settings/Storage/StorageView.swift b/BookPlayer/Settings/Storage/StorageView.swift index 06e8124f8..3c1caff28 100644 --- a/BookPlayer/Settings/Storage/StorageView.swift +++ b/BookPlayer/Settings/Storage/StorageView.swift @@ -16,7 +16,27 @@ struct StorageView: View { var body: some View { if viewModel.showProgressIndicator { - ProgressView() + VStack(spacing: 16) { + if let progress = viewModel.fixProgress { + VStack(spacing: 8) { + ProgressView(value: progress.percentage) + .progressViewStyle(.linear) + + Text("\(progress.processed) of \(progress.total)") + .font(.subheadline) + .foregroundStyle(theme.secondaryColor) + + Button("cancel_button".localized) { + viewModel.cancelFixAll() + } + .foregroundStyle(theme.linkColor) + .padding(.top, 8) + } + .padding() + } else { + ProgressView() + } + } } else { Form { Section { @@ -40,22 +60,19 @@ struct StorageView: View { } Section { - LazyVStack(spacing: 0) { + List { ForEach(viewModel.publishedFiles) { file in - VStack(spacing: 0) { - StorageRowView( - item: file, - onDeleteTap: { - viewModel.storageAlert = .delete(item: file) - viewModel.showAlert = true - }, - onWarningTap: { - viewModel.storageAlert = .fix(item: file) - viewModel.showAlert = true - } - ) - } - + StorageRowView( + item: file, + onDeleteTap: { + viewModel.storageAlert = .delete(item: file) + viewModel.showAlert = true + }, + onWarningTap: { + viewModel.storageAlert = .fix(item: file) + viewModel.showAlert = true + } + ) } } } header: { @@ -123,12 +140,14 @@ struct StorageView_Previews: PreviewProvider { var showFixAllButton: Bool = true var showAlert: Bool = false var showProgressIndicator: Bool = false + var fixProgress: FixProgress? = nil var alert: Alert { Alert(title: Text("")) } let fixButtonTitle = "Fix all" func getTotalFoldersSize() -> String { return "0 Kb" } func getArtworkFolderSize() -> String { return "0 Kb" } func dismiss() {} + func cancelFixAll() {} } static var previews: some View { StorageView(viewModel: MockStorageViewModel()) diff --git a/BookPlayer/Settings/Storage/StorageViewModel.swift b/BookPlayer/Settings/Storage/StorageViewModel.swift index df5f56e33..bbad01b11 100644 --- a/BookPlayer/Settings/Storage/StorageViewModel.swift +++ b/BookPlayer/Settings/Storage/StorageViewModel.swift @@ -20,11 +20,13 @@ protocol StorageViewModelProtocol: ObservableObject { var storageAlert: BPStorageAlert { get set } var showAlert: Bool { get set } var showProgressIndicator: Bool { get set } + var fixProgress: FixProgress? { get set } var alert: Alert { get } var fixButtonTitle: String { get } func getTotalFoldersSize() -> String func getArtworkFolderSize() -> String + func cancelFixAll() } enum BPStorageSortBy: Int { @@ -58,6 +60,7 @@ final class StorageViewModel: StorageViewModelProtocol { @Published var showFixAllButton = false @Published var showAlert = false @Published var showProgressIndicator = false + @Published var fixProgress: FixProgress? @Published var sortBy: BPStorageSortBy { didSet { @@ -94,6 +97,8 @@ final class StorageViewModel: StorageViewModelProtocol { libraryService.getLibrary() }() + private var fixTask: Task? + init( libraryService: LibraryServiceProtocol, syncService: SyncServiceProtocol, @@ -232,18 +237,32 @@ final class StorageViewModel: StorageViewModelProtocol { guard !brokenItems.isEmpty else { return } + // Cancel any existing fix operation + fixTask?.cancel() + showProgressIndicator = true - do { - try handleFix(for: brokenItems) { [weak self] in - self?.showProgressIndicator = false + fixProgress = FixProgress(total: brokenItems.count, processed: 0, succeeded: 0, failed: 0) + + fixTask = Task { [weak self] in + guard let self = self else { return } + + await self.processItemsInBackground(brokenItems) + + await MainActor.run { + self.showProgressIndicator = false + self.fixProgress = nil + self.loadItems() } - } catch { - showProgressIndicator = false - storageAlert = .error(errorMessage: error.localizedDescription) - showAlert = true } } + func cancelFixAll() { + fixTask?.cancel() + fixTask = nil + showProgressIndicator = false + fixProgress = nil + } + // MARK: - Private functions private func loadItems() { @@ -385,20 +404,59 @@ final class StorageViewModel: StorageViewModelProtocol { return await syncService.hasUploadTask(for: item.path) } - private func handleFix(for items: [StorageItem], completion: @escaping () -> Void) throws { + private func processItemsInBackground(_ items: [StorageItem]) async { + await processItemsRecursively(items) + } + + private func processItemsRecursively(_ items: [StorageItem]) async { + // Base case: no more items to process guard !items.isEmpty else { - loadItems() - completion() return } + + // Check for cancellation + if Task.isCancelled { + await MainActor.run { + self.storageAlert = .error(errorMessage: "Operation cancelled") + self.showAlert = true + } + return + } + + // Process the first item + var remainingItems = items + let currentItem = remainingItems.removeFirst() + + let result = await fixItemSafely(currentItem) + + // Update progress on main thread + await MainActor.run { + guard var progress = self.fixProgress else { return } + + progress.processed += 1 + if result.success { + progress.succeeded += 1 + } else { + progress.failed += 1 + } + + self.fixProgress = progress + } + + // Recursively process remaining items + await processItemsRecursively(remainingItems) + } - var mutableItems = items - - let currentItem = mutableItems.removeFirst() - - try handleFix(for: currentItem, shouldReloadItems: false) - - try handleFix(for: mutableItems, completion: completion) + private func fixItemSafely(_ item: StorageItem) async -> FixResult { + do { + try await Task.detached(priority: .userInitiated) { + try self.handleFix(for: item, shouldReloadItems: false) + }.value + + return FixResult(item: item, success: true, error: nil) + } catch { + return FixResult(item: item, success: false, error: error) + } } private var fixAllAlert: Alert { @@ -471,3 +529,23 @@ final class StorageViewModel: StorageViewModelProtocol { ) } } + +// MARK: - Supporting Types + +struct FixProgress { + let total: Int + var processed: Int + var succeeded: Int + var failed: Int + + var percentage: Double { + guard total > 0 else { return 0 } + return Double(processed) / Double(total) + } +} + +struct FixResult { + let item: StorageItem + let success: Bool + let error: Error? +} From 9229901e12b48a9b3243f4d1eb0da4659ae4c79a Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 6 Dec 2025 10:33:37 -0500 Subject: [PATCH 5/9] Disable purchases on TestFlight builds --- BookPlayer.xcodeproj/project.pbxproj | 14 ++++++ .../Support/Tips/TipJarViewModel.swift | 22 ++++++++++ .../Settings/Sections/SettingsTipView.swift | 4 ++ BookPlayer/Utils/PurchasesManager.swift | 21 +++++++++ Shared/Services/Account/AccountService.swift | 12 ++++++ Shared/Utils/AppEnvironment.swift | 43 +++++++++++++++++++ 6 files changed, 116 insertions(+) create mode 100644 Shared/Utils/AppEnvironment.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index 4cf9244fd..95e0c771b 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -550,6 +550,8 @@ 63B760F72C32734000AA98C7 /* SecondOnboardingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B760F62C32734000AA98C7 /* SecondOnboardingResponse.swift */; }; 63B760F92C32738E00AA98C7 /* StoryAccountSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B760F82C32738E00AA98C7 /* StoryAccountSubscriptionService.swift */; }; 63B760FC2C33B77F00AA98C7 /* SupportProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B760FB2C33B77F00AA98C7 /* SupportProfileView.swift */; }; + 63B9149E2EE47951000918D8 /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B9149C2EE47951000918D8 /* AppEnvironment.swift */; }; + 63B9149F2EE47951000918D8 /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B9149C2EE47951000918D8 /* AppEnvironment.swift */; }; 63B9EFA82E9A4230002361A0 /* PlayerControlsBoostVolumeSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B9EFA72E9A4230002361A0 /* PlayerControlsBoostVolumeSectionView.swift */; }; 63B9EFAA2E9A4644002361A0 /* PlayerControlsSpeedSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B9EFA92E9A4644002361A0 /* PlayerControlsSpeedSectionView.swift */; }; 63C1A8B02B0915EE00C4B418 /* WidgetUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 418445C2258AE11E0072DD13 /* WidgetUtils.swift */; }; @@ -1456,6 +1458,7 @@ 63B760F62C32734000AA98C7 /* SecondOnboardingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondOnboardingResponse.swift; sourceTree = ""; }; 63B760F82C32738E00AA98C7 /* StoryAccountSubscriptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryAccountSubscriptionService.swift; sourceTree = ""; }; 63B760FB2C33B77F00AA98C7 /* SupportProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportProfileView.swift; sourceTree = ""; }; + 63B9149C2EE47951000918D8 /* AppEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = ""; }; 63B9EFA72E9A4230002361A0 /* PlayerControlsBoostVolumeSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsBoostVolumeSectionView.swift; sourceTree = ""; }; 63B9EFA92E9A4644002361A0 /* PlayerControlsSpeedSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsSpeedSectionView.swift; sourceTree = ""; }; 63C48C792E3DAC9D005FBB96 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; @@ -2135,6 +2138,7 @@ 638352C92E67F327009EE79E /* SwiftData */, 6279361B272CDA8A0097837D /* Artwork */, 41A1B106226E9DCA00EA0400 /* Extensions */, + 63B9149D2EE47951000918D8 /* Utils */, 41A8941F2652A5DE0032E972 /* Configuration.swift */, D367F7671FA2A6F000FEDB37 /* Constants.swift */, 4140EA1E22723CFC0009F794 /* CommandParser.swift */, @@ -2812,6 +2816,14 @@ path = RemoteItemList; sourceTree = ""; }; + 63B9149D2EE47951000918D8 /* Utils */ = { + isa = PBXGroup; + children = ( + 63B9149C2EE47951000918D8 /* AppEnvironment.swift */, + ); + path = Utils; + sourceTree = ""; + }; 63C6C2E42B50299D00FFE0D8 /* Autolock */ = { isa = PBXGroup; children = ( @@ -4113,6 +4125,7 @@ 41EB07162752F17B00EFEE13 /* PlayableItem.swift in Sources */, 4140EA84227289E60009F794 /* BookPlayer.xcdatamodeld in Sources */, 412AB70A2701421600969618 /* ManualOrderMigrationUtils.swift in Sources */, + 63B9149E2EE47951000918D8 /* AppEnvironment.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4529,6 +4542,7 @@ 9F2DC9E52A014BB5006CDF1F /* PricingModel.swift in Sources */, 41A1B126226F88C500EA0400 /* LibraryItem+CoreDataProperties.swift in Sources */, 6327E0C62ADB9913004780DC /* DownloadState.swift in Sources */, + 63B9149F2EE47951000918D8 /* AppEnvironment.swift in Sources */, 63C6C3192B5E102200FFE0D8 /* SyncTask.swift in Sources */, 3FF5BA662E10EA6F00B82BCC /* HardcoverBook+CoreDataProperties.swift in Sources */, 62CADBAB2725C23A00A4A98F /* ArtworkService.swift in Sources */, diff --git a/BookPlayer/SecondOnboarding/Support/Tips/TipJarViewModel.swift b/BookPlayer/SecondOnboarding/Support/Tips/TipJarViewModel.swift index c6de4a38b..8dc9a02b1 100644 --- a/BookPlayer/SecondOnboarding/Support/Tips/TipJarViewModel.swift +++ b/BookPlayer/SecondOnboarding/Support/Tips/TipJarViewModel.swift @@ -40,6 +40,17 @@ public final class TipJarViewModel: ObservableObject { @MainActor func donate(_ tip: TipOption) async { + guard AppEnvironment.isPurchaseEnabled else { + onTransition?( + .showAlert( + BPAlertContent.errorAlert( + message: PurchaseError.testFlightPurchasesDisabled.errorDescription! + ) + ) + ) + return + } + onTransition?(.showLoader(true)) do { let product = await Purchases.shared.products([tip.rawValue]).first! @@ -66,6 +77,17 @@ public final class TipJarViewModel: ObservableObject { @MainActor func restorePurchases() async { + guard AppEnvironment.isPurchaseEnabled else { + onTransition?( + .showAlert( + BPAlertContent.errorAlert( + message: PurchaseError.testFlightPurchasesDisabled.errorDescription! + ) + ) + ) + return + } + onTransition?(.showLoader(true)) do { let customerInfo = try await Purchases.shared.restorePurchases() diff --git a/BookPlayer/Settings/Sections/SettingsTipView.swift b/BookPlayer/Settings/Sections/SettingsTipView.swift index 2f5e15f01..48ee17d13 100644 --- a/BookPlayer/Settings/Sections/SettingsTipView.swift +++ b/BookPlayer/Settings/Sections/SettingsTipView.swift @@ -101,6 +101,10 @@ struct SettingsTipView: View { } func purchaseProduct() async throws { + guard AppEnvironment.isPurchaseEnabled else { + throw PurchaseError.testFlightPurchasesDisabled + } + _ = await loadProductTask?.result guard let product else { diff --git a/BookPlayer/Utils/PurchasesManager.swift b/BookPlayer/Utils/PurchasesManager.swift index a7926ec6f..ed4922e29 100644 --- a/BookPlayer/Utils/PurchasesManager.swift +++ b/BookPlayer/Utils/PurchasesManager.swift @@ -10,11 +10,27 @@ import BookPlayerKit import Foundation import RevenueCat +enum PurchaseError: LocalizedError { + case testFlightPurchasesDisabled + + var errorDescription: String? { + switch self { + case .testFlightPurchasesDisabled: + return "In-app purchases are disabled in TestFlight builds. Please download the app from the App Store for donations or new subscriptions." + } + } +} + struct PurchasesManager { static func restoreTips( loadingState: LoadingOverlayState, onSuccess: @escaping () -> Void ) { + guard AppEnvironment.isPurchaseEnabled else { + loadingState.error = PurchaseError.testFlightPurchasesDisabled + return + } + loadingState.show = true Task { @MainActor in @@ -39,6 +55,11 @@ struct PurchasesManager { loadingState: LoadingOverlayState, onSuccess: @escaping () -> Void ) { + guard AppEnvironment.isPurchaseEnabled else { + loadingState.error = PurchaseError.testFlightPurchasesDisabled + return + } + loadingState.show = true Task { @MainActor in diff --git a/Shared/Services/Account/AccountService.swift b/Shared/Services/Account/AccountService.swift index ff574bcb2..e26f3cbb9 100644 --- a/Shared/Services/Account/AccountService.swift +++ b/Shared/Services/Account/AccountService.swift @@ -19,6 +19,8 @@ public enum AccountError: Error { case managementUnavailable /// Sign in with Apple didn't return identityToken case missingToken + /// In-app purchases are disabled in TestFlight builds + case testFlightPurchasesDisabled } public enum SecondOnboardingError: Error { @@ -44,6 +46,8 @@ extension AccountError: LocalizedError { case .inactiveSubscription: return "We couldn't find an active subscription for your account. If you believe this is an error, please contact us at support@bookplayer.app" + case .testFlightPurchasesDisabled: + return "In-app purchases are disabled in TestFlight builds. Please download the app from the App Store for donations or new subscriptions." } } } @@ -313,6 +317,10 @@ public final class AccountService: AccountServiceProtocol { } private func subscribe(productId: String) async throws -> Bool { + guard AppEnvironment.isPurchaseEnabled else { + throw AccountError.testFlightPurchasesDisabled + } + let products = await Purchases.shared.products([productId]) guard let product = products.first else { @@ -329,6 +337,10 @@ public final class AccountService: AccountServiceProtocol { } public func restorePurchases() async throws -> CustomerInfo { + guard AppEnvironment.isPurchaseEnabled else { + throw AccountError.testFlightPurchasesDisabled + } + return try await Purchases.shared.restorePurchases() } diff --git a/Shared/Utils/AppEnvironment.swift b/Shared/Utils/AppEnvironment.swift new file mode 100644 index 000000000..5a83335e0 --- /dev/null +++ b/Shared/Utils/AppEnvironment.swift @@ -0,0 +1,43 @@ +// +// AppEnvironment.swift +// BookPlayer +// +// Created by BookPlayer on 12/6/25. +// Copyright © 2025 BookPlayer LLC. All rights reserved. +// + +import Foundation + +public enum AppEnvironment { + /// Checks if the app is running in a TestFlight environment + public static var isTestFlight: Bool { + #if DEBUG + return false + #else + // Check if the app is installed via TestFlight + guard let receiptURL = Bundle.main.appStoreReceiptURL else { + return false + } + + return receiptURL.lastPathComponent == "sandboxReceipt" + #endif + } + + /// Checks if in-app purchases should be enabled + public static var isPurchaseEnabled: Bool { + return !isTestFlight + } + + /// Returns the current environment description for debugging + public static var environmentDescription: String { + if isTestFlight { + return "TestFlight" + } + #if DEBUG + return "Debug" + #else + return "Production" + #endif + } +} + From eae2b6bd429befed2774d26d1eb68629a703aed5 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 6 Dec 2025 11:17:46 -0500 Subject: [PATCH 6/9] Add discord link --- BookPlayer/Base.lproj/Localizable.strings | 1 + .../Settings/Sections/SettingsSupportSectionView.swift | 5 +++++ BookPlayer/ar.lproj/Localizable.strings | 1 + BookPlayer/ca.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/ja.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 + 27 files changed, 31 insertions(+) diff --git a/BookPlayer/Base.lproj/Localizable.strings b/BookPlayer/Base.lproj/Localizable.strings index 03617611d..fcc669c8f 100644 --- a/BookPlayer/Base.lproj/Localizable.strings +++ b/BookPlayer/Base.lproj/Localizable.strings @@ -394,3 +394,4 @@ We're working hard on providing a seamless experience, if possible, please conta "database_restore_failed_message" = "The database is corrupted and the backup restoration failed. The library will need to be reset and rebuilt from your audio files."; "database_no_backup_message" = "The database is corrupted and no backup is available. The library will need to be reset and rebuilt from your audio files."; "settings_smartrewind_max_interval_title" = "Smart Rewind Limit"; +"settings_support_discord_title" = "Join our Discord server"; diff --git a/BookPlayer/Settings/Sections/SettingsSupportSectionView.swift b/BookPlayer/Settings/Sections/SettingsSupportSectionView.swift index 0aaee5a33..7855b488b 100644 --- a/BookPlayer/Settings/Sections/SettingsSupportSectionView.swift +++ b/BookPlayer/Settings/Sections/SettingsSupportSectionView.swift @@ -58,6 +58,11 @@ struct SettingsSupportSectionView: View { openURL(url) } .foregroundStyle(theme.primaryColor) + Button("settings_support_discord_title") { + let url = URL(string: "https://discord.gg/RPPyhyMPXW")! + openURL(url) + } + .foregroundStyle(theme.primaryColor) } header: { Text("settings_support_title") .foregroundStyle(theme.secondaryColor) diff --git a/BookPlayer/ar.lproj/Localizable.strings b/BookPlayer/ar.lproj/Localizable.strings index 8ef6d9d7e..f86111c62 100644 --- a/BookPlayer/ar.lproj/Localizable.strings +++ b/BookPlayer/ar.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "جاري تحميل غلاف الكتاب"; "integrations_title" = "التكاملات"; "settings_smartrewind_max_interval_title" = "حد التراجع الذكي"; +"settings_support_discord_title" = "انضم إلى خادم Discord الخاص بنا"; diff --git a/BookPlayer/ca.lproj/Localizable.strings b/BookPlayer/ca.lproj/Localizable.strings index 382ae2391..d76b6bf45 100644 --- a/BookPlayer/ca.lproj/Localizable.strings +++ b/BookPlayer/ca.lproj/Localizable.strings @@ -379,3 +379,4 @@ Estem treballant dur per oferir una experiència perfecta; si és possible, pose "voiceover_loading_book_cover" = "S'està carregant la coberta del llibre"; "integrations_title" = "Integracions"; "settings_smartrewind_max_interval_title" = "Límit de rebobinat intel·ligent"; +"settings_support_discord_title" = "Uneix-te al nostre servidor de Discord"; diff --git a/BookPlayer/cs.lproj/Localizable.strings b/BookPlayer/cs.lproj/Localizable.strings index e43112312..3262516e5 100644 --- a/BookPlayer/cs.lproj/Localizable.strings +++ b/BookPlayer/cs.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Načítá se obálka knihy"; "integrations_title" = "Integrace"; "settings_smartrewind_max_interval_title" = "Chytrý limit převíjení"; +"settings_support_discord_title" = "Připojte se k našemu Discord serveru"; diff --git a/BookPlayer/da.lproj/Localizable.strings b/BookPlayer/da.lproj/Localizable.strings index 907781667..09e1a7152 100644 --- a/BookPlayer/da.lproj/Localizable.strings +++ b/BookPlayer/da.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Indlæser bogomlaget"; "integrations_title" = "Integrationer"; "settings_smartrewind_max_interval_title" = "Smart tilbagespolingsgrænse"; +"settings_support_discord_title" = "Deltag i vores Discord-server"; diff --git a/BookPlayer/de.lproj/Localizable.strings b/BookPlayer/de.lproj/Localizable.strings index cf79a644d..3ef86279e 100644 --- a/BookPlayer/de.lproj/Localizable.strings +++ b/BookPlayer/de.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Buchcover wird geladen"; "integrations_title" = "Integrationen"; "settings_smartrewind_max_interval_title" = "Intelligentes Rückspullimit"; +"settings_support_discord_title" = "Tritt unserem Discord-Server bei"; diff --git a/BookPlayer/el.lproj/Localizable.strings b/BookPlayer/el.lproj/Localizable.strings index a5d2d9f69..1c8ca2f6b 100644 --- a/BookPlayer/el.lproj/Localizable.strings +++ b/BookPlayer/el.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Φόρτωση εξώφυλλου βιβλίου"; "integrations_title" = "Ενσωματώσεις"; "settings_smartrewind_max_interval_title" = "Έξυπνο όριο επαναφοράς"; +"settings_support_discord_title" = "Συμμετάσχετε στον διακομιστή Discord μας"; diff --git a/BookPlayer/en.lproj/Localizable.strings b/BookPlayer/en.lproj/Localizable.strings index 1ac2ad46a..6596b0bca 100644 --- a/BookPlayer/en.lproj/Localizable.strings +++ b/BookPlayer/en.lproj/Localizable.strings @@ -392,3 +392,4 @@ We're working hard on providing a seamless experience, if possible, please conta "database_restore_failed_message" = "The database is corrupted and the backup restoration failed. The library will need to be reset and rebuilt from your audio files."; "database_no_backup_message" = "The database is corrupted and no backup is available. The library will need to be reset and rebuilt from your audio files."; "settings_smartrewind_max_interval_title" = "Smart Rewind Limit"; +"settings_support_discord_title" = "Join our Discord server"; diff --git a/BookPlayer/es.lproj/Localizable.strings b/BookPlayer/es.lproj/Localizable.strings index 17929df13..0ef59794c 100644 --- a/BookPlayer/es.lproj/Localizable.strings +++ b/BookPlayer/es.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Cargando portada del libro"; "integrations_title" = "Integraciones"; "settings_smartrewind_max_interval_title" = "Límite de rebobinado inteligente"; +"settings_support_discord_title" = "Únete a nuestro servidor de Discord"; diff --git a/BookPlayer/fi.lproj/Localizable.strings b/BookPlayer/fi.lproj/Localizable.strings index c6e46e7a5..721d4869d 100644 --- a/BookPlayer/fi.lproj/Localizable.strings +++ b/BookPlayer/fi.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Ladataan kirjan kantta"; "integrations_title" = "Integraatiot"; "settings_smartrewind_max_interval_title" = "Älykäs kelausrajoitus"; +"settings_support_discord_title" = "Liity Discord-palvelimellemme"; diff --git a/BookPlayer/fr.lproj/Localizable.strings b/BookPlayer/fr.lproj/Localizable.strings index b8d445cba..30eef85b8 100644 --- a/BookPlayer/fr.lproj/Localizable.strings +++ b/BookPlayer/fr.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Chargement de la couverture"; "integrations_title" = "Intégrations"; "settings_smartrewind_max_interval_title" = "Limite de rembobinage intelligente"; +"settings_support_discord_title" = "Rejoignez notre serveur Discord"; diff --git a/BookPlayer/hu.lproj/Localizable.strings b/BookPlayer/hu.lproj/Localizable.strings index 1a9135db0..84ca45c91 100644 --- a/BookPlayer/hu.lproj/Localizable.strings +++ b/BookPlayer/hu.lproj/Localizable.strings @@ -383,3 +383,4 @@ "Overview" = "Áttekintés"; "Tags" = "Cimkék"; "settings_smartrewind_max_interval_title" = "Intelligens visszatekerés korlátozása"; +"settings_support_discord_title" = "Csatlakozz a Discord szerverünkhöz"; diff --git a/BookPlayer/it.lproj/Localizable.strings b/BookPlayer/it.lproj/Localizable.strings index ada515f24..cb5d95361 100644 --- a/BookPlayer/it.lproj/Localizable.strings +++ b/BookPlayer/it.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Caricamento copertina del libro"; "integrations_title" = "Integrazioni"; "settings_smartrewind_max_interval_title" = "Limite di riavvolgimento intelligente"; +"settings_support_discord_title" = "Unisciti al nostro server Discord"; diff --git a/BookPlayer/ja.lproj/Localizable.strings b/BookPlayer/ja.lproj/Localizable.strings index 8d1d7f54c..7019f2b64 100644 --- a/BookPlayer/ja.lproj/Localizable.strings +++ b/BookPlayer/ja.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "本の表紙を読み込み中"; "integrations_title" = "統合"; "settings_smartrewind_max_interval_title" = "スマート巻き戻し制限"; +"settings_support_discord_title" = "Discordサーバーに参加する"; diff --git a/BookPlayer/nb.lproj/Localizable.strings b/BookPlayer/nb.lproj/Localizable.strings index c6bf04c5d..a7effac8a 100644 --- a/BookPlayer/nb.lproj/Localizable.strings +++ b/BookPlayer/nb.lproj/Localizable.strings @@ -379,3 +379,4 @@ Vi jobber hardt for å gi deg en sømløs opplevelse. Hvis mulig, kontakt oss p "voiceover_loading_book_cover" = "Laster bokomslag"; "integrations_title" = "Integrasjoner"; "settings_smartrewind_max_interval_title" = "Smart tilbakespolingsgrense"; +"settings_support_discord_title" = "Bli med på vår Discord-server"; diff --git a/BookPlayer/nl.lproj/Localizable.strings b/BookPlayer/nl.lproj/Localizable.strings index ab367a22d..01d294d63 100644 --- a/BookPlayer/nl.lproj/Localizable.strings +++ b/BookPlayer/nl.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Boekomslag laden"; "integrations_title" = "Integraties"; "settings_smartrewind_max_interval_title" = "Slimme terugspoellimiet"; +"settings_support_discord_title" = "Word lid van onze Discord-server"; diff --git a/BookPlayer/pl.lproj/Localizable.strings b/BookPlayer/pl.lproj/Localizable.strings index 9336fe784..2139001e6 100644 --- a/BookPlayer/pl.lproj/Localizable.strings +++ b/BookPlayer/pl.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Ładowanie okładki książki"; "integrations_title" = "Integracje"; "settings_smartrewind_max_interval_title" = "Limit inteligentnego przewijania"; +"settings_support_discord_title" = "Dołącz do naszego serwera Discord"; diff --git a/BookPlayer/pt-BR.lproj/Localizable.strings b/BookPlayer/pt-BR.lproj/Localizable.strings index 361c38f3a..f9b8aca15 100644 --- a/BookPlayer/pt-BR.lproj/Localizable.strings +++ b/BookPlayer/pt-BR.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Carregando capa do livro"; "integrations_title" = "Integrações"; "settings_smartrewind_max_interval_title" = "Limite de retrocesso inteligente"; +"settings_support_discord_title" = "Junte-se ao nosso servidor Discord"; diff --git a/BookPlayer/pt-PT.lproj/Localizable.strings b/BookPlayer/pt-PT.lproj/Localizable.strings index 9a0eb35e7..5c2f8a179 100644 --- a/BookPlayer/pt-PT.lproj/Localizable.strings +++ b/BookPlayer/pt-PT.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "A carregar capa do livro"; "integrations_title" = "Integrações"; "settings_smartrewind_max_interval_title" = "Limite de retrocesso inteligente"; +"settings_support_discord_title" = "Junte-se ao nosso servidor Discord"; diff --git a/BookPlayer/ro.lproj/Localizable.strings b/BookPlayer/ro.lproj/Localizable.strings index c62b9abdd..acc579426 100644 --- a/BookPlayer/ro.lproj/Localizable.strings +++ b/BookPlayer/ro.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Se încarcă coperta cărții"; "integrations_title" = "Integrări"; "settings_smartrewind_max_interval_title" = "Limită de derulare înapoi inteligentă"; +"settings_support_discord_title" = "Alătură-te serverului nostru Discord"; diff --git a/BookPlayer/ru.lproj/Localizable.strings b/BookPlayer/ru.lproj/Localizable.strings index e4dc4f755..a9f9013e8 100644 --- a/BookPlayer/ru.lproj/Localizable.strings +++ b/BookPlayer/ru.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Загрузка обложки книги"; "integrations_title" = "Интеграции"; "settings_smartrewind_max_interval_title" = "Ограничение смарт-перемотки"; +"settings_support_discord_title" = "Присоединяйтесь к нашему Discord-серверу"; diff --git a/BookPlayer/sk-SK.lproj/Localizable.strings b/BookPlayer/sk-SK.lproj/Localizable.strings index 1c53d12d3..2efac748b 100644 --- a/BookPlayer/sk-SK.lproj/Localizable.strings +++ b/BookPlayer/sk-SK.lproj/Localizable.strings @@ -379,3 +379,4 @@ Usilovne pracujeme na poskytovaní bezproblémového zážitku, ak je to možné "voiceover_loading_book_cover" = "Načítavam obálku knihy"; "integrations_title" = "Integrácie"; "settings_smartrewind_max_interval_title" = "Inteligentný limit pretáčania"; +"settings_support_discord_title" = "Pripojte sa k nášmu Discord serveru"; diff --git a/BookPlayer/sv.lproj/Localizable.strings b/BookPlayer/sv.lproj/Localizable.strings index 85d8e50ff..b8caf764b 100644 --- a/BookPlayer/sv.lproj/Localizable.strings +++ b/BookPlayer/sv.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Laddar bokomslag"; "integrations_title" = "Integrationer"; "settings_smartrewind_max_interval_title" = "Smart återspolningsgräns"; +"settings_support_discord_title" = "Gå med i vår Discord-server"; diff --git a/BookPlayer/tr.lproj/Localizable.strings b/BookPlayer/tr.lproj/Localizable.strings index 5efbe1bfd..4a717a95e 100644 --- a/BookPlayer/tr.lproj/Localizable.strings +++ b/BookPlayer/tr.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Kitap kapağı yükleniyor"; "integrations_title" = "Entegrasyonlar"; "settings_smartrewind_max_interval_title" = "Akıllı Geri Sarma Sınırı"; +"settings_support_discord_title" = "Discord sunucumuza katılın"; diff --git a/BookPlayer/uk.lproj/Localizable.strings b/BookPlayer/uk.lproj/Localizable.strings index 29e94df55..7bcc9046b 100644 --- a/BookPlayer/uk.lproj/Localizable.strings +++ b/BookPlayer/uk.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "Завантаження обкладинки книги"; "integrations_title" = "Інтеграції"; "settings_smartrewind_max_interval_title" = "Розумне обмеження перемотування"; +"settings_support_discord_title" = "Приєднуйтесь до нашого Discord-сервера"; diff --git a/BookPlayer/zh-Hans.lproj/Localizable.strings b/BookPlayer/zh-Hans.lproj/Localizable.strings index fde90b810..bf6efca21 100644 --- a/BookPlayer/zh-Hans.lproj/Localizable.strings +++ b/BookPlayer/zh-Hans.lproj/Localizable.strings @@ -379,3 +379,4 @@ "voiceover_loading_book_cover" = "正在加载图书封面"; "integrations_title" = "集成"; "settings_smartrewind_max_interval_title" = "智能倒带限制"; +"settings_support_discord_title" = "加入我们的 Discord 服务器"; From aecec14a08aac0bcd481b50aa18e900a69196da8 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 6 Dec 2025 11:37:52 -0500 Subject: [PATCH 7/9] Improve feedback for Wifi required message --- BookPlayer.xcodeproj/project.pbxproj | 4 ++ .../Profile/Profile/QueuedSyncTasksView.swift | 3 +- BookPlayer/Utils/NetworkMonitor.swift | 59 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 BookPlayer/Utils/NetworkMonitor.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index 95e0c771b..b38908de4 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -552,6 +552,7 @@ 63B760FC2C33B77F00AA98C7 /* SupportProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B760FB2C33B77F00AA98C7 /* SupportProfileView.swift */; }; 63B9149E2EE47951000918D8 /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B9149C2EE47951000918D8 /* AppEnvironment.swift */; }; 63B9149F2EE47951000918D8 /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B9149C2EE47951000918D8 /* AppEnvironment.swift */; }; + 63B914A12EE49079000918D8 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B914A02EE49079000918D8 /* NetworkMonitor.swift */; }; 63B9EFA82E9A4230002361A0 /* PlayerControlsBoostVolumeSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B9EFA72E9A4230002361A0 /* PlayerControlsBoostVolumeSectionView.swift */; }; 63B9EFAA2E9A4644002361A0 /* PlayerControlsSpeedSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B9EFA92E9A4644002361A0 /* PlayerControlsSpeedSectionView.swift */; }; 63C1A8B02B0915EE00C4B418 /* WidgetUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 418445C2258AE11E0072DD13 /* WidgetUtils.swift */; }; @@ -1459,6 +1460,7 @@ 63B760F82C32738E00AA98C7 /* StoryAccountSubscriptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryAccountSubscriptionService.swift; sourceTree = ""; }; 63B760FB2C33B77F00AA98C7 /* SupportProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportProfileView.swift; sourceTree = ""; }; 63B9149C2EE47951000918D8 /* AppEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppEnvironment.swift; sourceTree = ""; }; + 63B914A02EE49079000918D8 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; 63B9EFA72E9A4230002361A0 /* PlayerControlsBoostVolumeSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsBoostVolumeSectionView.swift; sourceTree = ""; }; 63B9EFA92E9A4644002361A0 /* PlayerControlsSpeedSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsSpeedSectionView.swift; sourceTree = ""; }; 63C48C792E3DAC9D005FBB96 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; @@ -3290,6 +3292,7 @@ 8ADD46212CF29A02002E9C50 /* TextFieldFocused.swift */, 63C48C832E3E66D1005FBB96 /* PurchasesManager.swift */, 63344BFF2EA708F800B90DF7 /* BackupDatabaseOperation.swift */, + 63B914A02EE49079000918D8 /* NetworkMonitor.swift */, ); path = Utils; sourceTree = ""; @@ -4212,6 +4215,7 @@ 63C48C842E3E66D1005FBB96 /* PurchasesManager.swift in Sources */, 8ADD46222CF29A02002E9C50 /* TextFieldFocused.swift in Sources */, 63C48C7E2E3DB24A005FBB96 /* LoginDisclaimerSectionView.swift in Sources */, + 63B914A12EE49079000918D8 /* NetworkMonitor.swift in Sources */, 9F22DE39288CBE6A00056FCD /* FormButton.swift in Sources */, 63B4A3BE2E9187B800784A22 /* ItemListView+ConfirmationDialogs.swift in Sources */, 4151A6A826E48C3400E49DBE /* Storyboarded.swift in Sources */, diff --git a/BookPlayer/Profile/Profile/QueuedSyncTasksView.swift b/BookPlayer/Profile/Profile/QueuedSyncTasksView.swift index b4b857ee9..6c7992b9d 100644 --- a/BookPlayer/Profile/Profile/QueuedSyncTasksView.swift +++ b/BookPlayer/Profile/Profile/QueuedSyncTasksView.swift @@ -16,6 +16,7 @@ struct QueuedSyncTasksView: View { @State private var queuedJobs = [SyncTaskReference]() @State private var jobsCount = 0 @State private var showInfoAlert = false + @State private var networkMonitor = NetworkMonitor() @Environment(\.syncService) private var syncService @EnvironmentObject private var theme: ThemeViewModel @@ -30,7 +31,7 @@ struct QueuedSyncTasksView: View { ) } } header: { - if !allowsCellularData { + if !allowsCellularData && !networkMonitor.isConnectedViaWiFi { HStack { Spacer() Image(systemName: "wifi") diff --git a/BookPlayer/Utils/NetworkMonitor.swift b/BookPlayer/Utils/NetworkMonitor.swift new file mode 100644 index 000000000..3c64b2feb --- /dev/null +++ b/BookPlayer/Utils/NetworkMonitor.swift @@ -0,0 +1,59 @@ +// +// NetworkMonitor.swift +// BookPlayer +// +// Created by BookPlayer on 6/12/24. +// Copyright © 2024 BookPlayer LLC. All rights reserved. +// + +import Foundation +import Network +import SwiftUI + +/// Monitors network connectivity and provides information about the current connection type +@Observable +class NetworkMonitor { + private let monitor = NWPathMonitor() + private let queue = DispatchQueue(label: "NetworkMonitor") + + var isConnected: Bool = false + var isConnectedViaWiFi: Bool = false + var isConnectedViaCellular: Bool = false + + init() { + startMonitoring() + } + + deinit { + stopMonitoring() + } + + func startMonitoring() { + monitor.pathUpdateHandler = { [weak self] path in + DispatchQueue.main.async { + self?.isConnected = path.status == .satisfied + self?.isConnectedViaWiFi = path.usesInterfaceType(.wifi) + self?.isConnectedViaCellular = path.usesInterfaceType(.cellular) + } + } + monitor.start(queue: queue) + } + + func stopMonitoring() { + monitor.cancel() + } +} + +// MARK: - Environment Key + +private struct NetworkMonitorKey: EnvironmentKey { + static let defaultValue = NetworkMonitor() +} + +extension EnvironmentValues { + var networkMonitor: NetworkMonitor { + get { self[NetworkMonitorKey.self] } + set { self[NetworkMonitorKey.self] = newValue } + } +} + From f76918117d799687d55a1bfea1da61aed5bb6df2 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sat, 6 Dec 2025 11:45:38 -0500 Subject: [PATCH 8/9] Fix debug information not generating --- .../Settings/Sections/DebugFileTransferable.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/BookPlayer/Settings/Sections/DebugFileTransferable.swift b/BookPlayer/Settings/Sections/DebugFileTransferable.swift index 0fa67a915..2bd8c8c35 100644 --- a/BookPlayer/Settings/Sections/DebugFileTransferable.swift +++ b/BookPlayer/Settings/Sections/DebugFileTransferable.swift @@ -17,9 +17,14 @@ struct DebugFileTransferable: Transferable { var remoteIdentifiers: [String]? var syncJobsInformation: String? + var syncError: String? if syncService.isActive { - remoteIdentifiers = try await syncService.fetchSyncedIdentifiers() + do { + remoteIdentifiers = try await syncService.fetchSyncedIdentifiers() + } catch { + syncError = "Error fetching remote identifiers: \(error.localizedDescription)" + } syncJobsInformation = await file.getSyncOperationsInformation() } @@ -43,6 +48,10 @@ struct DebugFileTransferable: Transferable { libraryRepresentation += syncJobsInformation } + if let syncError { + libraryRepresentation += "\n\n⚠️ Sync Error:\n\(syncError)\n" + } + return libraryRepresentation.data(using: .utf8)! } .suggestedFileName { _ in From 7cfa7b888224dfdb8fa00b07055d1b24ce739bff Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Wed, 3 Dec 2025 06:25:50 -0500 Subject: [PATCH 9/9] set app version --- BookPlayer.xcodeproj/project.pbxproj | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index b38908de4..9bdee6e4e 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -4894,7 +4894,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerIntents"; @@ -4928,7 +4928,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerIntents"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4960,7 +4960,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerIntents"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4997,7 +4997,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).watchkitapp"; @@ -5039,7 +5039,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).watchkitapp"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5078,7 +5078,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).watchkitapp"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5247,7 +5247,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerWidgetUI"; @@ -5285,7 +5285,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerWidgetUI"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5321,7 +5321,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerWidgetUI"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5475,7 +5475,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = BookPlayer; PROVISIONING_PROFILE_SPECIFIER = "$(BP_PROVISIONING_MAIN)"; @@ -5514,7 +5514,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = BookPlayer; PROVISIONING_PROFILE_SPECIFIER = "$(BP_PROVISIONING_MAIN)"; @@ -5736,7 +5736,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).watchkitapp.widgets"; @@ -5774,7 +5774,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).watchkitapp.widgets"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5810,7 +5810,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).watchkitapp.widgets"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5849,7 +5849,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerShareExtension"; @@ -5889,7 +5889,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5927,7 +5927,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6020,7 +6020,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.15.1; + MARKETING_VERSION = 5.15.2; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = BookPlayer; PROVISIONING_PROFILE_SPECIFIER = "$(BP_PROVISIONING_MAIN)";