From 91adebb094ec24a3370b1e5da56830d906869908 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Fri, 31 Jan 2025 12:03:17 -0500 Subject: [PATCH 1/4] Add bookmarks feature --- BookPlayer.xcodeproj/project.pbxproj | 20 ++ .../xcschemes/BookPlayerWidgetsPhone.xcscheme | 1 + BookPlayer/pl.lproj/Localizable.strings | 6 +- BookPlayerWatch/Bookmarks/BookmarksView.swift | 109 +++++++++++ .../Bookmarks/BookmarksViewModel.swift | 128 ++++++++++++ BookPlayerWatch/PlayerToolbarView.swift | 18 +- .../RemoteItemListCellView.swift | 5 +- .../RemoteItemList/RemoteItemListView.swift | 183 +++--------------- .../RemoteItemListViewModel.swift | 147 ++++++++++++++ BookPlayerWatch/RemotePlayerView.swift | 13 +- BookPlayerWatch/RootView.swift | 2 +- .../Lightweight-Models/SimpleBookmark.swift | 5 +- 12 files changed, 476 insertions(+), 161 deletions(-) create mode 100644 BookPlayerWatch/Bookmarks/BookmarksView.swift create mode 100644 BookPlayerWatch/Bookmarks/BookmarksViewModel.swift create mode 100644 BookPlayerWatch/RemoteItemList/RemoteItemListViewModel.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index d07941d8e..c8aec272b 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -279,6 +279,8 @@ 41F1A20D254B0A0C0043FCF3 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 41F1A20C254B0A0C0043FCF3 /* Sentry */; }; 41F1A228254B0C6C0043FCF3 /* ZipArchive in Frameworks */ = {isa = PBXBuildFile; productRef = 41F1A227254B0C6C0043FCF3 /* ZipArchive */; }; 4645F9FD2D1E46AC00A04257 /* SwipeInlineTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4645F9FC2D1E46AC00A04257 /* SwipeInlineTip.swift */; }; + 465D87522D3195D600A4AA47 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 465D87512D3195D600A4AA47 /* BookmarksView.swift */; }; + 465D87542D31965100A4AA47 /* BookmarksViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 465D87532D31965100A4AA47 /* BookmarksViewModel.swift */; }; 4689C06D2D270A7100D6C169 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 419B375423B8D5A500128A8F /* Localizable.strings */; }; 46EEDDC92D23154C0063811F /* VoiceOverService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69343D322133844D000C425E /* VoiceOverService.swift */; }; 5126F121258E9F18009965DC /* URL+BookPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126F120258E9F18009965DC /* URL+BookPlayer.swift */; }; @@ -453,6 +455,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 */; }; + 63E54C322D494E110040355D /* RemoteItemListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E54C312D494E110040355D /* RemoteItemListViewModel.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 */; }; @@ -1112,6 +1115,8 @@ 41F898AE2402080C00F58B8A /* ZipArchive.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ZipArchive.framework; path = Carthage/Build/iOS/ZipArchive.framework; sourceTree = ""; }; 41FCA32625E87EC600BFB9E6 /* Audiobook Player 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Audiobook Player 4.xcdatamodel"; sourceTree = ""; }; 4645F9FC2D1E46AC00A04257 /* SwipeInlineTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeInlineTip.swift; sourceTree = ""; }; + 465D87512D3195D600A4AA47 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = ""; }; + 465D87532D31965100A4AA47 /* BookmarksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksViewModel.swift; sourceTree = ""; }; 5126F120258E9F18009965DC /* URL+BookPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+BookPlayer.swift"; sourceTree = ""; }; 5CBB29522163A17F00E3A9FF /* ZIPFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ZIPFoundation.framework; path = Carthage/Build/iOS/ZIPFoundation.framework; sourceTree = ""; }; 620C73C7275DA00300D495AA /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1251,6 +1256,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 = ""; }; + 63E54C312D494E110040355D /* RemoteItemListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteItemListViewModel.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 = ""; }; @@ -1679,6 +1685,7 @@ 6334CF202CFE330300F1FA17 /* RefreshableListView.swift */, 63E7DCBF2D076185005B5E1F /* View+BookPlayer.swift */, 4645F9FC2D1E46AC00A04257 /* SwipeInlineTip.swift */, + 465D87502D3195B600A4AA47 /* Bookmarks */, 63CD851B2CE2963600EDBEA8 /* Settings */, 6399D06E2CEBA1F900A2E278 /* RemoteItemList */, 9FA334B427C156DB0064E8EA /* ItemList */, @@ -2067,6 +2074,15 @@ path = iPad; sourceTree = ""; }; + 465D87502D3195B600A4AA47 /* Bookmarks */ = { + isa = PBXGroup; + children = ( + 465D87512D3195D600A4AA47 /* BookmarksView.swift */, + 465D87532D31965100A4AA47 /* BookmarksViewModel.swift */, + ); + path = Bookmarks; + sourceTree = ""; + }; 62793612272CC19E0097837D /* Models */ = { isa = PBXGroup; children = ( @@ -2362,6 +2378,7 @@ isa = PBXGroup; children = ( 6399D06F2CEBA35D00A2E278 /* RemoteItemListView.swift */, + 63E54C312D494E110040355D /* RemoteItemListViewModel.swift */, 6399D0712CEBA37C00A2E278 /* RemoteItemListCellView.swift */, 6334CF1E2CFAD1B700F1FA17 /* RemoteItemCellViewModel.swift */, ); @@ -3507,9 +3524,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 465D87542D31965100A4AA47 /* BookmarksViewModel.swift in Sources */, 9F82DF6927DE93A2001B0EA8 /* SkipIntervalView.swift in Sources */, 6399D0722CEBA37C00A2E278 /* RemoteItemListCellView.swift in Sources */, 6334CF212CFE330300F1FA17 /* RefreshableListView.swift in Sources */, + 63E54C322D494E110040355D /* RemoteItemListViewModel.swift in Sources */, 9FA334B627C15DE30064E8EA /* VolumeView.swift in Sources */, 46EEDDC92D23154C0063811F /* VoiceOverService.swift in Sources */, 6350E46D2CF4315B0077CDC1 /* PlayerLoaderService.swift in Sources */, @@ -3530,6 +3549,7 @@ 418CABB325EF28FC00D8C878 /* MappingModel_v3_to_v4.xcmappingmodel in Sources */, 9F82DF7127DF8203001B0EA8 /* WatchConnectivityService.swift in Sources */, 6350E4742CF4D2660077CDC1 /* PlayerToolbarView.swift in Sources */, + 465D87522D3195D600A4AA47 /* BookmarksView.swift in Sources */, 41A8BAFE227E6C88003C9895 /* Notification+BookPlayerWatchApp.swift in Sources */, 6350E4762CF4F6E90077CDC1 /* PlaybackFullControlsView.swift in Sources */, 6350E46A2CF429760077CDC1 /* SleepTimer.swift in Sources */, diff --git a/BookPlayer.xcodeproj/xcshareddata/xcschemes/BookPlayerWidgetsPhone.xcscheme b/BookPlayer.xcodeproj/xcshareddata/xcschemes/BookPlayerWidgetsPhone.xcscheme index 7741fa6e4..85dc0587b 100644 --- a/BookPlayer.xcodeproj/xcshareddata/xcschemes/BookPlayerWidgetsPhone.xcscheme +++ b/BookPlayer.xcodeproj/xcshareddata/xcschemes/BookPlayerWidgetsPhone.xcscheme @@ -90,6 +90,7 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" + askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2"> diff --git a/BookPlayer/pl.lproj/Localizable.strings b/BookPlayer/pl.lproj/Localizable.strings index e0e42c81a..ae25a0d8e 100644 --- a/BookPlayer/pl.lproj/Localizable.strings +++ b/BookPlayer/pl.lproj/Localizable.strings @@ -135,10 +135,10 @@ "voiceover_unknown_author" = "Nieznany autor"; "voiceover_book_info" = "%@ - %@"; "voiceover_book_chapter" = "%@ - %@, rozdział %@"; -"voiceover_rewind_time" = "Przewiń wstecz %@"; +"voiceover_rewind_time" = "Przewiń do tyłu %@"; "voiceover_forward_time" = "Przewiń do przodu %@"; "watchapp_last_played_title" = "Ostatnio odtwarzane"; -"watchapp_refresh_data_title" = "Odśwież dane"; +"watchapp_refresh_data_title" = "Odśwież"; "recent_title" = "Ostatnie"; "carplay_library_error" = "Nie można załadować książek"; "siri_invocation_phrase" = "Kontynuuj moją książkę"; @@ -158,7 +158,7 @@ "voiceover_currently_playing_title" = "Aktualnie odtwarzam %@ - %@"; "voiceover_miniplayer_hint" = "Miniodtwarzacz. Dotknij, aby wyświetlić odtwarzacz"; "voiceover_chapter_time_title" = "Bieżący czas rozdziału: %@"; -"voiceover_dismiss_player_title" = "Wyłącz Odtwarzacz"; +"voiceover_dismiss_player_title" = "Zamknij odtwarzacz"; "sort_most_recent_button" = "Najczęściej Odtwarzane"; "sort_reversed_button" = "Odwróć kolejność"; "voiceover_continue_playback_title" = "Kontynuuj odtwarzanie"; diff --git a/BookPlayerWatch/Bookmarks/BookmarksView.swift b/BookPlayerWatch/Bookmarks/BookmarksView.swift new file mode 100644 index 000000000..9d34a48a2 --- /dev/null +++ b/BookPlayerWatch/Bookmarks/BookmarksView.swift @@ -0,0 +1,109 @@ +// +// BookmarksView.swift +// BookPlayerWatch +// +// Created by GC on 1/10/25. +// Copyright © 2025 BookPlayer LLC. All rights reserved. +// + +import BookPlayerWatchKit +import SwiftUI + +struct BookmarksView: View { + @StateObject var model: BookmarksViewModel + + @State var error: Error? + + @Environment(\.dismiss) var dismiss + + var body: some View { + List { + HStack { + Spacer() + Button { + do { + try model.createBookmark() + } catch { + self.error = error + } + } label: { + Image(systemName: "plus.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + } + .buttonStyle(PlainButtonStyle()) + Spacer() + } + .frame(height: 24) + .listRowBackground(Color.clear) + + Section { + ForEach(model.userBookmarks) { bookmark in + Button { + model.playerManager.jumpTo(bookmark.time + 0.01, recordBookmark: false) + dismiss() + } label: { + VStack(alignment: .leading) { + Text(TimeParser.formatTime(bookmark.time)) + .foregroundColor(Color.secondary) + .font(.footnote) + if let note = bookmark.note { + Text(note) + } + } + .frame(minHeight: 24) + .padding(.vertical, Spacing.S4) + } + .swipeActions { + Button( + role: .destructive, + action: { model.deleteBookmark(bookmark) }, + label: { + Image(systemName: "trash") + .imageScale(.large) + } + ) + .accessibilityLabel("delete_button".localized) + } + } + } header: { + Text("bookmark_type_user_title".localized) + .foregroundStyle(Color.accentColor) + } + + Section { + ForEach(model.automaticBookmarks) { bookmark in + Button { + model.playerManager.jumpTo(bookmark.time + 0.01, recordBookmark: false) + dismiss() + } label: { + VStack(alignment: .leading) { + HStack { + Text(TimeParser.formatTime(bookmark.time)) + .foregroundColor(Color.secondary) + .font(.footnote) + Spacer() + if let imageName = bookmark.getImageNameForType() { + Image(systemName: imageName) + .foregroundColor(Color.secondary) + } + } + if let note = bookmark.note { + Text(note) + } + } + } + } + } header: { + Text("bookmark_type_automatic_title".localized) + .foregroundStyle(Color.accentColor) + .padding(.top, 10) + } + } + .environment(\.defaultMinListRowHeight, 1) + .customListSectionSpacing(0) + .errorAlert(error: $error) + .navigationTitle("bookmarks_title") + } +} diff --git a/BookPlayerWatch/Bookmarks/BookmarksViewModel.swift b/BookPlayerWatch/Bookmarks/BookmarksViewModel.swift new file mode 100644 index 000000000..a2d91d7fa --- /dev/null +++ b/BookPlayerWatch/Bookmarks/BookmarksViewModel.swift @@ -0,0 +1,128 @@ +// +// BookmarksViewModel.swift +// BookPlayerWatch +// +// Created by GC on 1/10/25. +// Copyright © 2025 BookPlayer LLC. All rights reserved. +// + +import BookPlayerWatchKit +import Combine +import Foundation + +@MainActor +class BookmarksViewModel: ObservableObject { + @Published var automaticBookmarks = [SimpleBookmark]() + @Published var userBookmarks = [SimpleBookmark]() + @Published var selectedBookmarkToDelete: SimpleBookmark? + private var disposeBag = Set() + + let playerManager: PlayerManager + let libraryService: LibraryServiceProtocol + let syncService: SyncServiceProtocol + + init(coreServices: CoreServices) { + self.playerManager = coreServices.playerManager + self.libraryService = coreServices.libraryService + self.syncService = coreServices.syncService + + self.bindCurrentItemObserver() + } + + func bindCurrentItemObserver() { + playerManager.currentItemPublisher() + .receive(on: DispatchQueue.main) + .sink { [weak self] currentItem in + guard let self else { return } + + if let currentItem { + self.automaticBookmarks = self.getAutomaticBookmarks(for: currentItem.relativePath) + self.userBookmarks = self.getUserBookmarks(for: currentItem.relativePath) + self.syncBookmarks(for: currentItem.relativePath) + } else { + self.automaticBookmarks = [] + self.userBookmarks = [] + } + } + .store(in: &disposeBag) + } + + func syncBookmarks(for relativePath: String) { + Task { @MainActor [weak self] in + guard + let self = self, + let bookmarks = try await self.syncService.syncBookmarksList(relativePath: relativePath) + else { return } + + self.userBookmarks = bookmarks + } + } + + func getAutomaticBookmarks(for relativePath: String) -> [SimpleBookmark] { + let playBookmarks = self.libraryService.getBookmarks(of: .play, relativePath: relativePath) ?? [] + let skipBookmarks = self.libraryService.getBookmarks(of: .skip, relativePath: relativePath) ?? [] + + let bookmarks = playBookmarks + skipBookmarks + + return bookmarks.sorted(by: { $0.time < $1.time }) + } + + func getUserBookmarks(for relativePath: String) -> [SimpleBookmark] { + return self.libraryService.getBookmarks(of: .user, relativePath: relativePath) ?? [] + } + + func createBookmark() throws { + guard let currentItem = playerManager.currentItem else { return } + + let currentTime = currentItem.currentTime + + if let bookmark = libraryService.getBookmark( + at: currentTime, + relativePath: currentItem.relativePath, + type: .user + ) { + throw BookmarksAlerts.bookmarkExists(bookmark: bookmark) + } + + if let bookmark = libraryService.createBookmark( + at: floor(currentTime), + relativePath: currentItem.relativePath, + type: .user + ) { + syncService.scheduleSetBookmark( + relativePath: currentItem.relativePath, + time: floor(currentTime), + note: nil + ) + userBookmarks = getUserBookmarks(for: currentItem.relativePath) + throw BookmarksAlerts.bookmarkCreated(bookmark: bookmark) + } else { + throw BookmarksAlerts.fileMissing + } + } + + func deleteBookmark(_ bookmark: SimpleBookmark) { + libraryService.deleteBookmark(bookmark) + userBookmarks = getUserBookmarks(for: bookmark.relativePath) + syncService.scheduleDeleteBookmark(bookmark) + } +} + +enum BookmarksAlerts: LocalizedError { + case bookmarkExists(bookmark: SimpleBookmark) + case bookmarkCreated(bookmark: SimpleBookmark) + case fileMissing + + public var errorDescription: String? { + switch self { + case .bookmarkExists(let bookmark): + let formattedTime = TimeParser.formatTime(bookmark.time) + return String.localizedStringWithFormat("bookmark_exists_title".localized, formattedTime) + case .bookmarkCreated(let bookmark): + let formattedTime = TimeParser.formatTime(bookmark.time) + return String.localizedStringWithFormat("bookmark_created_title".localized, formattedTime) + case .fileMissing: + return "file_missing_title".localized + } + } +} diff --git a/BookPlayerWatch/PlayerToolbarView.swift b/BookPlayerWatch/PlayerToolbarView.swift index a058cc918..bb1da1ae8 100644 --- a/BookPlayerWatch/PlayerToolbarView.swift +++ b/BookPlayerWatch/PlayerToolbarView.swift @@ -53,9 +53,17 @@ final class PlaybackFullControlsViewModel: ObservableObject { } struct PlayerToolbarView: View { + /// PlayerManager needs to be an ObservedObject, otherwise the Published property do not reload properly @ObservedObject var playerManager: PlayerManager @State var isShowingMoreList: Bool = false + let coreServices: CoreServices + + init(coreServices: CoreServices) { + self.coreServices = coreServices + self.playerManager = coreServices.playerManager + } + var body: some View { HStack { Spacer() @@ -71,8 +79,14 @@ struct PlayerToolbarView: View { Spacer() - VolumeView(type: .local) - .accessibilityHidden(true) + NavigationLink( + destination: BookmarksView(model: .init(coreServices: coreServices)) + ) { + ResizeableImageView(name: "bookmark.fill") + .accessibilityLabel("bookmarks_title".localized) + .padding(20) + } + .buttonStyle(PlainButtonStyle()) Spacer() diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift index c21cfec5c..5aa8bba98 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListCellView.swift @@ -40,7 +40,7 @@ struct RemoteItemListCellView: View { case .notDownloaded: return ". ☁️" case .downloading(let progress): - return "" + return "\(Int(progress * 100))%" case .downloaded: return ". ⌚️" } @@ -110,6 +110,7 @@ struct RemoteItemListCellView: View { Image(systemName: "xmark.circle") .imageScale(.large) } + .accessibilityLabel("cancel_download_title".localized) case .downloaded: Button { do { @@ -121,6 +122,7 @@ struct RemoteItemListCellView: View { Image(systemName: "applewatch.slash") .imageScale(.large) } + .accessibilityLabel("remove_downloaded_file_title".localized) case .notDownloaded: Button { Task { @@ -134,6 +136,7 @@ struct RemoteItemListCellView: View { Image(systemName: "icloud.and.arrow.down.fill") .imageScale(.large) } + .accessibilityLabel("download_title".localized) } } .accessibilityLabel(VoiceOverService.getAccessibilityLabel(for: model.item) + accessibilityDownloadStateLabel) diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift index b974fc901..1e4abf1af 100644 --- a/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListView.swift @@ -12,162 +12,33 @@ import TipKit struct RemoteItemListView: View { @Environment(\.scenePhase) var scenePhase - @ObservedObject var coreServices: CoreServices - @ObservedObject var playerManager: PlayerManager - @State var items: [SimpleLibraryItem] - @State var playingItemParentPath: String? + @StateObject var model: RemoteItemListViewModel @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? - - init( - coreServices: CoreServices, - folderRelativePath: String? = nil - ) { - self.coreServices = coreServices - self.playerManager = coreServices.playerManager - 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.folderRelativePath = folderRelativePath - - if let lastItem { - self._playingItemParentPath = .init( - initialValue: getPathForParentOfItem(currentPlayingPath: lastItem.relativePath) - ) - } else { - self._playingItemParentPath = .init(initialValue: nil) - } - } - - private 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.reloadLastBook(let relativePath) { - reloadLastBook(relativePath: relativePath) - } catch BPSyncError.differentLastBook(let relativePath) { - await setSyncedLastPlayedItem(relativePath: relativePath) - } catch { - self.error = error - } - - items = - coreServices.libraryService.fetchContents( - at: folderRelativePath, - limit: nil, - offset: nil - ) ?? [] - - if let lastPlayedItem { - playingItemParentPath = getPathForParentOfItem(currentPlayingPath: lastPlayedItem.relativePath) - } else { - playingItemParentPath = nil - } - } - - @MainActor - private func reloadLastBook(relativePath: String) { - let wasPlaying = playerManager.isPlaying - playerManager.stop() - - Task { @MainActor in - do { - try await coreServices.playerLoaderService.loadPlayer( - relativePath, - autoplay: wasPlaying - ) - } catch { - self.error = error - } - } - } - - @MainActor - private func setSyncedLastPlayedItem(relativePath: String) async { - /// Only continue overriding local book if it's not currently playing - guard playerManager.isPlaying == false else { return } - - await coreServices.syncService.setLibraryLastBook(with: relativePath) - - do { - try await coreServices.playerLoaderService.loadPlayer( - relativePath, - autoplay: false - ) - } catch { - self.error = error - } - } - func getForegroundColor(for item: SimpleLibraryItem) -> Color { - guard let lastPlayedItem else { return .primary } + guard let lastPlayedItem = model.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 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 + return item.relativePath == model.playingItemParentPath ? .accentColor : .primary } var body: some View { RefreshableListView(refreshing: $isRefreshing) { - if folderRelativePath == nil { + if model.folderRelativePath == nil { Section { - if let lastPlayedItem { - RemoteItemListCellView(model: .init(item: lastPlayedItem, coreServices: coreServices)) { + if let lastPlayedItem = model.lastPlayedItem { + RemoteItemListCellView(model: .init(item: lastPlayedItem, coreServices: model.coreServices)) { Task { do { isLoading = true - try await coreServices.playerLoaderService.loadPlayer(lastPlayedItem.relativePath, autoplay: true) + try await model.coreServices.playerLoaderService.loadPlayer(lastPlayedItem.relativePath, autoplay: true) showPlayer = true isLoading = false } catch { @@ -186,31 +57,31 @@ struct RemoteItemListView: View { Section { if #available(watchOS 10.0, *), - folderRelativePath == nil, - !items.isEmpty + model.folderRelativePath == nil, + !model.items.isEmpty { TipView(SwipeInlineTip()) .listRowBackground(Color.clear) } - ForEach(items) { item in + ForEach(model.items) { item in if item.type == .folder { NavigationLink { - RemoteItemListView( - coreServices: coreServices, + RemoteItemListView(model: .init( + coreServices: model.coreServices, folderRelativePath: item.relativePath - ) + )) } label: { - RemoteItemListCellView(model: .init(item: item, coreServices: coreServices)) {} + RemoteItemListCellView(model: .init(item: item, coreServices: model.coreServices)) {} .allowsHitTesting(false) .foregroundColor(getForegroundColor(for: item)) } } else { - RemoteItemListCellView(model: .init(item: item, coreServices: coreServices)) { + RemoteItemListCellView(model: .init(item: item, coreServices: model.coreServices)) { Task { do { isLoading = true - try await coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) + try await model.coreServices.playerLoaderService.loadPlayer(item.relativePath, autoplay: true) showPlayer = true isLoading = false } catch { @@ -222,9 +93,9 @@ struct RemoteItemListView: View { } } } header: { - Text(verbatim: folderRelativePath?.components(separatedBy: "/").last ?? "library_title".localized) + Text(verbatim: model.folderRelativePath?.components(separatedBy: "/").last ?? "library_title".localized) .foregroundStyle(Color.accentColor) - .padding(.top, folderRelativePath == nil ? 10 : 0) + .padding(.top, model.folderRelativePath == nil ? 10 : 0) } /// Create padding at the bottom @@ -238,7 +109,7 @@ struct RemoteItemListView: View { } .ignoresSafeArea(edges: [.bottom]) .background( - NavigationLink(destination: RemotePlayerView(playerManager: coreServices.playerManager), isActive: $showPlayer) { + NavigationLink(destination: RemotePlayerView(coreServices: model.coreServices), isActive: $showPlayer) { EmptyView() } .opacity(0) @@ -265,14 +136,18 @@ struct RemoteItemListView: View { Task { // Delay the task by 1 second to avoid jumping animations try await Task.sleep(nanoseconds: 1_000_000_000) - await syncListContents(ignoreLastTimestamp: true) + do { + try await model.syncListContents(ignoreLastTimestamp: true) + } catch { + self.error = error + } isRefreshing = false } } .onChange(of: scenePhase) { newPhase in guard newPhase == .active, - coreServices.playerManager.isPlaying + model.playerManager.isPlaying else { return } showPlayer = true @@ -281,8 +156,12 @@ struct RemoteItemListView: View { guard isFirstLoad else { return } isFirstLoad = false - Task { - await syncListContents(ignoreLastTimestamp: false) + Task { @MainActor in + do { + try await model.syncListContents(ignoreLastTimestamp: false) + } catch { + self.error = error + } } } } diff --git a/BookPlayerWatch/RemoteItemList/RemoteItemListViewModel.swift b/BookPlayerWatch/RemoteItemList/RemoteItemListViewModel.swift new file mode 100644 index 000000000..f4fd47ee8 --- /dev/null +++ b/BookPlayerWatch/RemoteItemList/RemoteItemListViewModel.swift @@ -0,0 +1,147 @@ +// +// RemoteItemListViewModel.swift +// BookPlayerWatch +// +// Created by Gianni Carlo on 28/1/25. +// Copyright © 2025 BookPlayer LLC. All rights reserved. +// + +import BookPlayerWatchKit +import Combine +import Foundation + +@MainActor +final class RemoteItemListViewModel: ObservableObject { + @Published var items: [SimpleLibraryItem] + @Published var lastPlayedItem: SimpleLibraryItem? + @Published var playingItemParentPath: String? + @Published var playerManager: PlayerManager + + let coreServices: CoreServices + let folderRelativePath: String? + private var disposeBag = Set() + + init( + coreServices: CoreServices, + folderRelativePath: String? = nil + ) { + self.coreServices = coreServices + self.playerManager = coreServices.playerManager + self.folderRelativePath = folderRelativePath + /// initial load of data + let fetchedItems = + coreServices.libraryService.fetchContents( + at: folderRelativePath, + limit: nil, + offset: nil + ) ?? [] + self._items = .init(initialValue: fetchedItems) + let lastItem = coreServices.libraryService.getLastPlayedItems(limit: 1)?.first + + if let lastItem { + self._lastPlayedItem = .init(initialValue: lastItem) + self._playingItemParentPath = .init( + initialValue: getPathForParentOfItem(currentPlayingPath: lastItem.relativePath) + ) + } else { + self._lastPlayedItem = .init(initialValue: nil) + self._playingItemParentPath = .init(initialValue: nil) + } + + self.bindCurrentItemObserver() + } + + func bindCurrentItemObserver() { + playerManager.currentItemPublisher() + .receive(on: DispatchQueue.main) + .sink { [weak self] currentItem in + guard let self else { return } + + if let currentItem, + let lastPlayedItem = self.coreServices.libraryService.getSimpleItem(with: currentItem.relativePath) { + self.lastPlayedItem = lastPlayedItem + } else { + self.lastPlayedItem = coreServices.libraryService.getLastPlayedItems(limit: 1)?.first + } + } + .store(in: &disposeBag) + } + + 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] + } + + func syncListContents(ignoreLastTimestamp: Bool) async throws { + guard + await coreServices.syncService.canSyncListContents( + at: folderRelativePath, + ignoreLastTimestamp: ignoreLastTimestamp + ) + else { return } + + do { + try await coreServices.syncService.syncListContents(at: folderRelativePath) + } catch BPSyncError.reloadLastBook(let relativePath) { + try await reloadLastBook(relativePath: relativePath) + } catch BPSyncError.differentLastBook(let relativePath) { + try await setSyncedLastPlayedItem(relativePath: relativePath) + } catch { + throw error + } + + items = + coreServices.libraryService.fetchContents( + at: folderRelativePath, + limit: nil, + offset: nil + ) ?? [] + + if let lastPlayedItem { + playingItemParentPath = getPathForParentOfItem(currentPlayingPath: lastPlayedItem.relativePath) + } else { + playingItemParentPath = nil + } + } + + @MainActor + private func reloadLastBook(relativePath: String) async throws { + let wasPlaying = playerManager.isPlaying + playerManager.stop() + + try await coreServices.playerLoaderService.loadPlayer( + relativePath, + autoplay: wasPlaying + ) + } + + @MainActor + private func setSyncedLastPlayedItem(relativePath: String) async throws { + /// Only continue overriding local book if it's not currently playing + guard playerManager.isPlaying == false else { return } + + await coreServices.syncService.setLibraryLastBook(with: relativePath) + + try await coreServices.playerLoaderService.loadPlayer( + relativePath, + autoplay: false + ) + } +} diff --git a/BookPlayerWatch/RemotePlayerView.swift b/BookPlayerWatch/RemotePlayerView.swift index 36f1696d8..eae4bd1a7 100644 --- a/BookPlayerWatch/RemotePlayerView.swift +++ b/BookPlayerWatch/RemotePlayerView.swift @@ -11,6 +11,12 @@ import SwiftUI struct RemotePlayerView: View { @ObservedObject var playerManager: PlayerManager + let coreServices: CoreServices + + init(coreServices: CoreServices) { + self.coreServices = coreServices + self.playerManager = coreServices.playerManager + } var body: some View { VStack { @@ -34,7 +40,12 @@ struct RemotePlayerView: View { Spacer() - PlayerToolbarView(playerManager: playerManager) + PlayerToolbarView(coreServices: coreServices) + } + .background { + VolumeView(type: .local) + .accessibilityHidden(true) + .opacity(0) } .fixedSize(horizontal: false, vertical: false) .ignoresSafeArea(edges: .bottom) diff --git a/BookPlayerWatch/RootView.swift b/BookPlayerWatch/RootView.swift index a8c051d58..85e073c08 100644 --- a/BookPlayerWatch/RootView.swift +++ b/BookPlayerWatch/RootView.swift @@ -17,7 +17,7 @@ struct RootView: View { var body: some View { VStack { if coreServices.hasSyncEnabled { - RemoteItemListView(coreServices: coreServices) + RemoteItemListView(model: .init(coreServices: coreServices)) } else if contextManager.items.isEmpty && contextManager.isConnecting { ProgressView() } else { diff --git a/Shared/CoreData/Lightweight-Models/SimpleBookmark.swift b/Shared/CoreData/Lightweight-Models/SimpleBookmark.swift index 79eb43cfb..1cf6e9cb6 100644 --- a/Shared/CoreData/Lightweight-Models/SimpleBookmark.swift +++ b/Shared/CoreData/Lightweight-Models/SimpleBookmark.swift @@ -8,7 +8,10 @@ import Foundation -public struct SimpleBookmark: Decodable { +public struct SimpleBookmark: Decodable, Identifiable { + public var id: String { + return UUID().uuidString + } public let time: Double public let note: String? let type: BookmarkType From a0783c97b6b0a4bd297ece25774786ec3f201a24 Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Tue, 4 Feb 2025 10:10:39 -0500 Subject: [PATCH 2/4] Address feedback for popup --- BookPlayer.xcodeproj/project.pbxproj | 24 +++++ .../Coordinators/SettingsCoordinator.swift | 1 - .../SecondOnboardingCoordinator.swift | 4 +- .../SecondOnboardingResponse.swift | 4 + .../Support/SupportFlowCoordinator.swift | 48 ++++++++++ .../Support/Tips/TipJarView.swift | 82 ++++++++++++++++ .../Support/Tips/TipJarViewModel.swift | 93 +++++++++++++++++++ .../Support/Tips/TipOption.swift | 38 ++++++++ .../Support/Tips/TipOptionView.swift | 64 +++++++++++++ .../Settings/Plus Screen/PlusViewModel.swift | 1 - .../Views/StoryViewer/StoryActionView.swift | 22 ++++- .../Utils/Views/StoryViewer/StoryView.swift | 6 +- .../Views/StoryViewer/StoryViewModel.swift | 6 +- .../Utils/Views/StoryViewer/StoryViewer.swift | 17 ++-- .../StoryViewer/StoryViewerViewModel.swift | 5 + 15 files changed, 401 insertions(+), 14 deletions(-) create mode 100644 BookPlayer/SecondOnboarding/Support/Tips/TipJarView.swift create mode 100644 BookPlayer/SecondOnboarding/Support/Tips/TipJarViewModel.swift create mode 100644 BookPlayer/SecondOnboarding/Support/Tips/TipOption.swift create mode 100644 BookPlayer/SecondOnboarding/Support/Tips/TipOptionView.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index c8aec272b..fae8be82c 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -395,6 +395,8 @@ 6357F11A2A8BA084007947FC /* BPURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6357F1182A8BA084007947FC /* BPURLSession.swift */; }; 635907A02B161B14002FA524 /* DebugInformationFileActivityItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6359079F2B161B14002FA524 /* DebugInformationFileActivityItemProvider.swift */; }; 636086012C5B3EB400341D78 /* CustomRewindIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 636086002C5B3EB400341D78 /* CustomRewindIntent.swift */; }; + 636ED1E52D51254E00BFF3FD /* TipOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 636ED1E42D51254E00BFF3FD /* TipOption.swift */; }; + 636ED1E82D51331E00BFF3FD /* TipJarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 636ED1E72D51331E00BFF3FD /* TipJarViewModel.swift */; }; 63717EAA2B792E350006291E /* RefreshTaskOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63717EA92B792E350006291E /* RefreshTaskOperation.swift */; }; 637609192ADCD81400A01D5D /* AppShortcuts.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6376091B2ADCD81400A01D5D /* AppShortcuts.strings */; }; 637DAB0B2AEB3F6D006DC2D1 /* WidgetEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 637DAB092AEB3E0D006DC2D1 /* WidgetEntries.swift */; }; @@ -456,6 +458,8 @@ 63CD85272CE3064600EDBEA8 /* BP+ErrorAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85262CE3064600EDBEA8 /* BP+ErrorAlerts.swift */; }; 63CD85432CE3105300EDBEA8 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CD85422CE3105300EDBEA8 /* ProfileView.swift */; }; 63E54C322D494E110040355D /* RemoteItemListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E54C312D494E110040355D /* RemoteItemListViewModel.swift */; }; + 63E54C342D50576C0040355D /* TipJarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E54C332D50576C0040355D /* TipJarView.swift */; }; + 63E54C362D50626A0040355D /* TipOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E54C352D50626A0040355D /* TipOptionView.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 */; }; @@ -1197,6 +1201,8 @@ 636086022C5B589900341D78 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = ""; }; 636086032C5B589900341D78 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; 636086042C5B589900341D78 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/AppShortcuts.strings; sourceTree = ""; }; + 636ED1E42D51254E00BFF3FD /* TipOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipOption.swift; sourceTree = ""; }; + 636ED1E72D51331E00BFF3FD /* TipJarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipJarViewModel.swift; sourceTree = ""; }; 63717EA92B792E350006291E /* RefreshTaskOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshTaskOperation.swift; sourceTree = ""; }; 6376091A2ADCD81400A01D5D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/AppShortcuts.strings; sourceTree = ""; }; 6376091C2ADCD82100A01D5D /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/AppShortcuts.strings; sourceTree = ""; }; @@ -1257,6 +1263,8 @@ 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 = ""; }; 63E54C312D494E110040355D /* RemoteItemListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteItemListViewModel.swift; sourceTree = ""; }; + 63E54C332D50576C0040355D /* TipJarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipJarView.swift; sourceTree = ""; }; + 63E54C352D50626A0040355D /* TipOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipOptionView.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 = ""; }; @@ -2309,6 +2317,7 @@ 634BA58E2C14FB010015314D /* Support */ = { isa = PBXGroup; children = ( + 636ED1E32D51254400BFF3FD /* Tips */, 63B760E72C31FFC600AA98C7 /* SupportFlowCoordinator.swift */, 63B760FB2C33B77F00AA98C7 /* SupportProfileView.swift */, ); @@ -2347,6 +2356,17 @@ path = AppIntents; sourceTree = ""; }; + 636ED1E32D51254400BFF3FD /* Tips */ = { + isa = PBXGroup; + children = ( + 63E54C332D50576C0040355D /* TipJarView.swift */, + 636ED1E72D51331E00BFF3FD /* TipJarViewModel.swift */, + 63E54C352D50626A0040355D /* TipOptionView.swift */, + 636ED1E42D51254E00BFF3FD /* TipOption.swift */, + ); + path = Tips; + sourceTree = ""; + }; 63833DF82AAC134600496246 /* PerformanceTests */ = { isa = PBXGroup; children = ( @@ -3755,6 +3775,7 @@ 631C75CC2AB92FA60013E7E5 /* BPModalPresentationFlow.swift in Sources */, 9FF710AC2A212221006490E0 /* QueuedSyncTasksViewController.swift in Sources */, 9FEC87B227FA9F1C006C71D5 /* LoginViewModel.swift in Sources */, + 636ED1E52D51254E00BFF3FD /* TipOption.swift in Sources */, 63C1A8B22B09166F00C4B418 /* WidgetEntries.swift in Sources */, 9F17D5CD29159297004F4929 /* SearchListViewModel.swift in Sources */, 41AD3DA3221C7DCE00DC41E1 /* IconsViewController.swift in Sources */, @@ -3897,6 +3918,7 @@ 9F2681B628898A7300359BD3 /* LoginDisclaimerView.swift in Sources */, 9FD8D95829DC53750074C2D8 /* CoreServices.swift in Sources */, 9F4691F22800D58A00A8F0E8 /* CompleteAccountCoordinator.swift in Sources */, + 63E54C342D50576C0040355D /* TipJarView.swift in Sources */, 9F00A5F6294F793F005EA316 /* ItemDetailsView.swift in Sources */, 4158386E26EAF76700F4A12B /* FolderListCoordinator.swift in Sources */, 9F64C6262793D5B100B2493C /* PlayerControlsViewController.swift in Sources */, @@ -3914,6 +3936,7 @@ 41A1B12D226FC7E500EA0400 /* ImportManager.swift in Sources */, 9F22DE46288E517600056FCD /* AccountProBenefitsView.swift in Sources */, 8A9D0D242CCED53C007A924D /* JellyfinLibraryViewModel.swift in Sources */, + 636ED1E82D51331E00BFF3FD /* TipJarViewModel.swift in Sources */, 9F2E00DE2A5C2B810001FE20 /* StorageCloudDeletedViewModel.swift in Sources */, 4151A6A626E3A40600E49DBE /* SpeedService.swift in Sources */, 6356F9B92AC7CDDE00B7A027 /* EndChapterSleepTimerIntent.swift in Sources */, @@ -3942,6 +3965,7 @@ 41188D2A26ED4D8E0017124E /* ItemListViewModel.swift in Sources */, C398559C20C492FF00BE9EC0 /* AddButton.swift in Sources */, 8A9D0D222CCD00D2007A924D /* JellyfinCoordinator.swift in Sources */, + 63E54C362D50626A0040355D /* TipOptionView.swift in Sources */, 8ADD46262CF67592002E9C50 /* JellyfinAudiobookDetailsViewModel.swift in Sources */, 9F89D89C27EDFCA400F73947 /* SceneDelegate.swift in Sources */, 9F3D0CE928C2BFC600E9E8A3 /* ButtonFreeCoordinator.swift in Sources */, diff --git a/BookPlayer/Coordinators/SettingsCoordinator.swift b/BookPlayer/Coordinators/SettingsCoordinator.swift index d6f9803af..7d2c706d4 100644 --- a/BookPlayer/Coordinators/SettingsCoordinator.swift +++ b/BookPlayer/Coordinators/SettingsCoordinator.swift @@ -192,7 +192,6 @@ class SettingsCoordinator: Coordinator, AlertPresenter { func showTipJar() { let viewModel = PlusViewModel(accountService: self.accountService) - viewModel.coordinator = self let vc = PlusViewController.instantiate(from: .Settings) vc.viewModel = viewModel vc.navigationItem.largeTitleDisplayMode = .never diff --git a/BookPlayer/SecondOnboarding/SecondOnboardingCoordinator.swift b/BookPlayer/SecondOnboarding/SecondOnboardingCoordinator.swift index cefad1984..c6b6f9772 100644 --- a/BookPlayer/SecondOnboarding/SecondOnboardingCoordinator.swift +++ b/BookPlayer/SecondOnboarding/SecondOnboardingCoordinator.swift @@ -80,7 +80,9 @@ class SecondOnboardingCoordinator: Coordinator { ), sliderOptions: action.sliderOptions, button: action.button, - dismiss: action.dismiss + dismiss: action.dismiss, + tipJar: action.tipJar, + tipJarDisclaimer: action.tipJarDisclaimer ) } diff --git a/BookPlayer/SecondOnboarding/SecondOnboardingResponse.swift b/BookPlayer/SecondOnboarding/SecondOnboardingResponse.swift index 918beee26..a909ef7dd 100644 --- a/BookPlayer/SecondOnboarding/SecondOnboardingResponse.swift +++ b/BookPlayer/SecondOnboarding/SecondOnboardingResponse.swift @@ -33,9 +33,13 @@ struct StoryActionResponseModel: Codable { var sliderOptions: SliderOptions? var button: String var dismiss: String? + var tipJar: String? + var tipJarDisclaimer: String? enum CodingKeys: String, CodingKey { case options, button, dismiss + case tipJar = "tip_jar" + case tipJarDisclaimer = "tip_jar_disclaimer" case defaultOption = "default_option" case sliderOptions = "slider_options" } diff --git a/BookPlayer/SecondOnboarding/Support/SupportFlowCoordinator.swift b/BookPlayer/SecondOnboarding/Support/SupportFlowCoordinator.swift index 3fc60b4d4..235034aa8 100644 --- a/BookPlayer/SecondOnboarding/Support/SupportFlowCoordinator.swift +++ b/BookPlayer/SecondOnboarding/Support/SupportFlowCoordinator.swift @@ -45,6 +45,10 @@ class SupportFlowCoordinator: Coordinator, AlertPresenter { viewModel.onTransition = { route in switch route { + case .tipJar(let disclaimer): + Task { @MainActor in + self.showTipJar(disclaimer: disclaimer) + } case .dismiss: self.dismiss() case .showAlert(let model): @@ -99,6 +103,50 @@ class SupportFlowCoordinator: Coordinator, AlertPresenter { } } + @MainActor + func showTipJar(disclaimer: String?) { + let viewModel = TipJarViewModel( + disclaimer: disclaimer, + accountService: accountService + ) + + viewModel.onTransition = { route in + switch route { + case .showLoader(let flag): + if flag { + self.showLoader() + } else { + self.stopLoader() + } + case .showAlert(let model): + self.presentedController?.getTopVisibleViewController()?.showAlert(model) + case .success(let message): + self.showCongratsTip(message) + case .dismiss: + self.flow.finishPresentation(animated: true) + } + } + + let vc = UIHostingController(rootView: TipJarView(viewModel: viewModel)) + vc.modalPresentationStyle = .overFullScreen + + presentedController?.present(vc, animated: true) + } + + func showCongratsTip(_ message: String) { + eventsService.sendEvent( + "second_onboarding_tip", + payload: [ + "rc_id": anonymousId, + "onboarding_id": onboardingId, + ] + ) + presentedController?.getTopVisibleViewController()?.view.startConfetti() + presentedController?.getTopVisibleViewController()?.showAlert(message, message: nil) { [weak self] in + self?.flow.finishPresentation(animated: true) + } + } + func showCongrats() { eventsService.sendEvent( "second_onboarding_subscription", diff --git a/BookPlayer/SecondOnboarding/Support/Tips/TipJarView.swift b/BookPlayer/SecondOnboarding/Support/Tips/TipJarView.swift new file mode 100644 index 000000000..6d030d98a --- /dev/null +++ b/BookPlayer/SecondOnboarding/Support/Tips/TipJarView.swift @@ -0,0 +1,82 @@ +// +// TipJarView.swift +// BookPlayer +// +// Created by Gianni Carlo on 2/2/25. +// Copyright © 2025 BookPlayer LLC. All rights reserved. +// + +import BookPlayerKit +import SwiftUI + +struct TipJarView: View { + @ObservedObject var viewModel: TipJarViewModel + @StateObject var themeViewModel = ThemeViewModel() + @State var selected: TipOption = TipOption.excellent + @State var error: Error? + + var body: some View { + NavigationView { + VStack { + if let disclaimer = viewModel.disclaimer { + Text(disclaimer) + .foregroundColor(themeViewModel.primaryColor) + .font(Font(Fonts.titleRegular)) + .padding() + } + + HStack(spacing: Spacing.S1) { + Spacer() + ForEach(TipOption.allCases) { option in + TipOptionView( + title: .constant(option.title), + price: .constant(option.price), + isSelected: .constant(selected == option) + ) + .onTapGesture { + selected = option + } + } + Spacer() + } + Button(action: { + Task { @MainActor in + await viewModel.donate(selected) + } + }) { + Text("Donate") + .contentShape(Rectangle()) + .font(Font(Fonts.headline)) + .frame(height: 45) + .frame(maxWidth: .infinity) + .foregroundColor(.white) + .background(Color(UIColor(hex: "687AB7"))) + .cornerRadius(6) + .padding(.top, Spacing.S1) + } + Spacer() + } + .padding(.horizontal, Spacing.M) + .background(themeViewModel.systemGroupedBackgroundColor) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + viewModel.dismiss() + } label: { + Image(systemName: "xmark") + } + } + + ToolbarItem(placement: .confirmationAction) { + Button("Restore") { + Task { @MainActor in + await viewModel.restorePurchases() + } + } + } + } + .navigationTitle("Tip Jar") + .navigationBarTitleDisplayMode(.inline) + } + } +} diff --git a/BookPlayer/SecondOnboarding/Support/Tips/TipJarViewModel.swift b/BookPlayer/SecondOnboarding/Support/Tips/TipJarViewModel.swift new file mode 100644 index 000000000..769d427ea --- /dev/null +++ b/BookPlayer/SecondOnboarding/Support/Tips/TipJarViewModel.swift @@ -0,0 +1,93 @@ +// +// TipJarViewModel.swift +// BookPlayer +// +// Created by Gianni Carlo on 3/2/25. +// Copyright © 2025 BookPlayer LLC. All rights reserved. +// + +import BookPlayerKit +import Foundation +import RevenueCat + +@MainActor +public final class TipJarViewModel: ObservableObject { + enum Routes { + case showLoader(Bool) + case showAlert(BPAlertContent) + case success(message: String) + case dismiss + } + + let disclaimer: String? + let accountService: AccountServiceProtocol + /// Callback to handle actions on this screen + var onTransition: BPTransition? + + init( + disclaimer: String?, + accountService: AccountServiceProtocol + ) { + self.disclaimer = disclaimer + self.accountService = accountService + } + + func donate(_ tip: TipOption) async { + onTransition?(.showLoader(true)) + do { + let product = await Purchases.shared.products([tip.rawValue]).first! + let result = try await Purchases.shared.purchase(product: product) + onTransition?(.showLoader(false)) + if !result.userCancelled { + accountService.updateAccount( + id: nil, + email: nil, + donationMade: true, + hasSubscription: nil + ) + onTransition?(.success(message: "thanks_amazing_title".localized)) + } + } catch { + onTransition?(.showLoader(false)) + onTransition?( + .showAlert( + BPAlertContent.errorAlert(message: error.localizedDescription) + ) + ) + } + } + + func restorePurchases() async { + onTransition?(.showLoader(true)) + do { + let customerInfo = try await Purchases.shared.restorePurchases() + onTransition?(.showLoader(false)) + if customerInfo.nonSubscriptions.isEmpty { + onTransition?( + .showAlert( + BPAlertContent.errorAlert(message: "tip_missing_title".localized) + ) + ) + } else { + accountService.updateAccount( + id: nil, + email: nil, + donationMade: true, + hasSubscription: nil + ) + onTransition?(.success(message: "purchases_restored_title".localized)) + } + } catch { + onTransition?(.showLoader(false)) + onTransition?( + .showAlert( + BPAlertContent.errorAlert(message: error.localizedDescription) + ) + ) + } + } + + func dismiss() { + onTransition?(.dismiss) + } +} diff --git a/BookPlayer/SecondOnboarding/Support/Tips/TipOption.swift b/BookPlayer/SecondOnboarding/Support/Tips/TipOption.swift new file mode 100644 index 000000000..9968aca60 --- /dev/null +++ b/BookPlayer/SecondOnboarding/Support/Tips/TipOption.swift @@ -0,0 +1,38 @@ +// +// TipOption.swift +// BookPlayer +// +// Created by Gianni Carlo on 3/2/25. +// Copyright © 2025 BookPlayer LLC. All rights reserved. +// + +import Foundation + +enum TipOption: String, Identifiable, CaseIterable { + public var id: Self { self } + case kind = "com.tortugapower.audiobookplayer.tip.kind" + case excellent = "com.tortugapower.audiobookplayer.tip.excellent" + case incredible = "com.tortugapower.audiobookplayer.tip.incredible" + + var title: String { + switch self { + case .kind: + return "Kind\ntip of" + case .excellent: + return "Excellent\ntip of" + case .incredible: + return "Incredible\ntip of" + } + } + + var price: String { + switch self { + case .kind: + return "$2.99" + case .excellent: + return "$4.99" + case .incredible: + return "$9.99" + } + } +} diff --git a/BookPlayer/SecondOnboarding/Support/Tips/TipOptionView.swift b/BookPlayer/SecondOnboarding/Support/Tips/TipOptionView.swift new file mode 100644 index 000000000..66c21df05 --- /dev/null +++ b/BookPlayer/SecondOnboarding/Support/Tips/TipOptionView.swift @@ -0,0 +1,64 @@ +// +// TipOptionView.swift +// BookPlayer +// +// Created by Gianni Carlo on 2/2/25. +// Copyright © 2025 BookPlayer LLC. All rights reserved. +// + +import BookPlayerKit +import SwiftUI + +struct TipOptionView: View { + @Binding var title: String + @Binding var price: String + @Binding var isSelected: Bool + + var imageLength: CGFloat = 16 + var imageName: String { + isSelected ? "checkmark.circle" : "circle" + } + var foregroundColor: Color { + isSelected + ? Color(UIColor(hex: "3488D1")) + : Color(UIColor(hex: "334046")) + } + + var body: some View { + VStack(spacing: 0) { + HStack { + Spacer() + Image(systemName: imageName) + .resizable() + .frame(width: imageLength, height: imageLength) + .foregroundColor(foregroundColor) + .padding([.trailing, .top], Spacing.S3) + } + Text(title) + .font(Font(Fonts.titleRegular)) + .foregroundColor(foregroundColor.opacity(0.7)) + .multilineTextAlignment(.center) + Text(price) + .font(Font(Fonts.titleLarge)) + .foregroundColor(foregroundColor) + } + .padding([.bottom]) + .background(Color(UIColor(hex: "F8F8F8"))) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .contentShape(Rectangle()) + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isButton) + .frame(maxWidth: 88) + + } +} + +#Preview { + ZStack { + TipOptionView( + title: .constant("Kind tip\nof"), + price: .constant("1.99"), + isSelected: .constant(true) + ) + } +} diff --git a/BookPlayer/Settings/Plus Screen/PlusViewModel.swift b/BookPlayer/Settings/Plus Screen/PlusViewModel.swift index 895f90e4a..86296e7ed 100644 --- a/BookPlayer/Settings/Plus Screen/PlusViewModel.swift +++ b/BookPlayer/Settings/Plus Screen/PlusViewModel.swift @@ -10,7 +10,6 @@ import BookPlayerKit import Combine final class PlusViewModel { - weak var coordinator: SettingsCoordinator! let accountService: AccountServiceProtocol @Published var account: Account? diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryActionView.swift b/BookPlayer/Utils/Views/StoryViewer/StoryActionView.swift index acfc402e9..899bc20a4 100644 --- a/BookPlayer/Utils/Views/StoryViewer/StoryActionView.swift +++ b/BookPlayer/Utils/Views/StoryViewer/StoryActionView.swift @@ -29,17 +29,20 @@ struct StoryActionView: View { } var onSubscription: (PricingModel) -> Void var onDismiss: () -> Void + var onTipJar: (String?) -> Void init( action: Binding, onSubscription: @escaping (PricingModel) -> Void, - onDismiss: @escaping () -> Void + onDismiss: @escaping () -> Void, + onTipJar: @escaping (String?) -> Void ) { self._action = action self.selected = action.wrappedValue.defaultOption self.sliderValue = action.wrappedValue.defaultOption.price self.onSubscription = onSubscription self.onDismiss = onDismiss + self.onTipJar = onTipJar } var body: some View { @@ -147,6 +150,20 @@ struct StoryActionView: View { ) .padding([.top], Spacing.S5) } + if let tipJar = action.tipJar { + Button( + action: { + onTipJar(action.tipJarDisclaimer) + }, + label: { + Text(tipJar) + .underline() + .font(Font(Fonts.body)) + .foregroundColor(.white) + } + ) + .padding([.top], Spacing.S5) + } } } } @@ -168,7 +185,8 @@ struct StoryActionView: View { ) ), onSubscription: { option in print(option.title) }, - onDismiss: {} + onDismiss: {}, + onTipJar: { _ in } ) } } diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryView.swift b/BookPlayer/Utils/Views/StoryViewer/StoryView.swift index 527415938..b345736b7 100644 --- a/BookPlayer/Utils/Views/StoryViewer/StoryView.swift +++ b/BookPlayer/Utils/Views/StoryViewer/StoryView.swift @@ -17,6 +17,7 @@ struct StoryView: View { var onResume: () -> Void var onSubscription: (PricingModel) -> Void var onDismiss: () -> Void + var onTipJar: (String?) -> Void var body: some View { ZStack { @@ -108,7 +109,8 @@ struct StoryView: View { StoryActionView( action: action, onSubscription: onSubscription, - onDismiss: onDismiss + onDismiss: onDismiss, + onTipJar: onTipJar ) .padding([.leading, .trailing]) .padding([.top], Spacing.L1) @@ -160,6 +162,8 @@ struct StoryView: View { print(option.title) }, onDismiss: { print("Dismiss") + }, onTipJar: { _ in + print("Tip Jar") }) .foregroundColor(.white) } diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryViewModel.swift b/BookPlayer/Utils/Views/StoryViewer/StoryViewModel.swift index 886adc038..c205341e7 100644 --- a/BookPlayer/Utils/Views/StoryViewer/StoryViewModel.swift +++ b/BookPlayer/Utils/Views/StoryViewer/StoryViewModel.swift @@ -15,9 +15,13 @@ struct StoryActionType: Codable { var sliderOptions: SliderOptions? var button: String var dismiss: String? - + var tipJar: String? + var tipJarDisclaimer: String? + enum CodingKeys: String, CodingKey { case options, button, dismiss + case tipJar = "tip_jar" + case tipJarDisclaimer = "tip_jar_disclaimer" case defaultOption = "default_option" case sliderOptions = "slider_options" } diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryViewer.swift b/BookPlayer/Utils/Views/StoryViewer/StoryViewer.swift index f09aa4743..5cb521af3 100644 --- a/BookPlayer/Utils/Views/StoryViewer/StoryViewer.swift +++ b/BookPlayer/Utils/Views/StoryViewer/StoryViewer.swift @@ -17,12 +17,14 @@ struct StoryViewer: View { ZStack(alignment: .top) { StoryBackgroundView() .accessibilityHidden(true) - StoryProgress( - storiesCount: .constant(viewModel.storiesCount), - progress: $viewModel.progress - ) - .padding([.trailing, .leading, .bottom]) - .accessibilityHidden(true) + if viewModel.storiesCount > 1 { + StoryProgress( + storiesCount: .constant(viewModel.storiesCount), + progress: $viewModel.progress + ) + .padding([.trailing, .leading, .bottom]) + .accessibilityHidden(true) + } StoryView( model: $viewModel.currentModel, onPrevious: viewModel.previous, @@ -30,7 +32,8 @@ struct StoryViewer: View { onPause: viewModel.pause, onResume: viewModel.start, onSubscription: viewModel.handleSubscription(option:), - onDismiss: viewModel.handleDismiss + onDismiss: viewModel.handleDismiss, + onTipJar: viewModel.handleTipJar ) .foregroundColor(Color.white) .padding() diff --git a/BookPlayer/Utils/Views/StoryViewer/StoryViewerViewModel.swift b/BookPlayer/Utils/Views/StoryViewer/StoryViewerViewModel.swift index 379e3fb4a..7c23fe883 100644 --- a/BookPlayer/Utils/Views/StoryViewer/StoryViewerViewModel.swift +++ b/BookPlayer/Utils/Views/StoryViewer/StoryViewerViewModel.swift @@ -14,6 +14,7 @@ class StoryViewerViewModel: ObservableObject { enum Routes { case showLoader(Bool) case showAlert(BPAlertContent) + case tipJar(String?) case success case dismiss } @@ -99,4 +100,8 @@ class StoryViewerViewModel: ObservableObject { func handleDismiss() { onTransition?(.dismiss) } + + func handleTipJar(disclaimer: String?) { + onTransition?(.tipJar(disclaimer)) + } } From f6f78dc1a90f039ae54ab83c3e6157d5c43e213f Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Tue, 4 Feb 2025 10:43:28 -0500 Subject: [PATCH 3/4] remove coredata fallback from widgets --- .../Phone/LastPlayed/LastPlayedProvider.swift | 42 ++----------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/BookPlayerWidgets/Phone/LastPlayed/LastPlayedProvider.swift b/BookPlayerWidgets/Phone/LastPlayed/LastPlayedProvider.swift index af724cdf2..7dcb945f2 100644 --- a/BookPlayerWidgets/Phone/LastPlayed/LastPlayedProvider.swift +++ b/BookPlayerWidgets/Phone/LastPlayed/LastPlayedProvider.swift @@ -57,18 +57,7 @@ struct LastPlayedProvider: TimelineProvider { func getEntryForTimeline( context: Context ) async throws -> RecentlyPlayedEntry { - let items: [WidgetLibraryItem] - let theme: SimpleTheme - - /// Attempt to fetch from shared defaults, otherwise default to database - if let (widgetItems, widgetTheme) = getItemsFromDefaults() { - items = widgetItems - theme = widgetTheme - } else { - let (widgetItems, widgetTheme) = try await getDataFromDatabase() - items = widgetItems - theme = widgetTheme - } + let (items, theme) = try getItemsFromDefaults() let currentlyPlaying = UserDefaults.sharedDefaults.string( forKey: Constants.UserDefaults.sharedWidgetNowPlayingPath @@ -82,37 +71,12 @@ struct LastPlayedProvider: TimelineProvider { ) } - func getDataFromDatabase() async throws -> ([WidgetLibraryItem], SimpleTheme) { - let stack = try await DatabaseInitializer().loadCoreDataStack() - let dataManager = DataManager(coreDataStack: stack) - let libraryService = LibraryService(dataManager: dataManager) - - guard - let lastPlayedItems = libraryService.getLastPlayedItems(limit: 4) - else { - throw BookPlayerError.emptyResponse - } - - let items = lastPlayedItems.map({ - WidgetLibraryItem( - relativePath: $0.relativePath, - title: $0.title, - details: $0.details - ) - }) - - return ( - items, - libraryService.getLibraryCurrentTheme() ?? SimpleTheme.getDefaultTheme() - ) - } - - func getItemsFromDefaults() -> ([WidgetLibraryItem], SimpleTheme)? { + func getItemsFromDefaults() throws -> ([WidgetLibraryItem], SimpleTheme) { guard let itemsData = UserDefaults.sharedDefaults.data(forKey: Constants.UserDefaults.sharedWidgetLastPlayedItems), let items = try? decoder.decode([WidgetLibraryItem].self, from: itemsData) else { - return nil + throw BookPlayerError.emptyResponse } let theme: SimpleTheme From bb12f0fe6ee534fd2de40f1233afe35568f8217a Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Fri, 31 Jan 2025 23:10:44 -0500 Subject: [PATCH 4/4] 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 fae8be82c..87dced818 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -4415,7 +4415,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerIntents"; @@ -4449,7 +4449,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerIntents"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4481,7 +4481,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerIntents"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4517,7 +4517,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).watchkitapp"; @@ -4558,7 +4558,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).watchkitapp"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4596,7 +4596,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).watchkitapp"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4765,7 +4765,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerWidgetUI"; @@ -4803,7 +4803,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerWidgetUI"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4839,7 +4839,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerWidgetUI"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4992,7 +4992,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = BookPlayer; PROVISIONING_PROFILE_SPECIFIER = "$(BP_PROVISIONING_MAIN)"; @@ -5030,7 +5030,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = BookPlayer; PROVISIONING_PROFILE_SPECIFIER = "$(BP_PROVISIONING_MAIN)"; @@ -5252,7 +5252,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).watchkitapp.widgets"; @@ -5290,7 +5290,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).watchkitapp.widgets"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5326,7 +5326,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).watchkitapp.widgets"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5365,7 +5365,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerShareExtension"; @@ -5405,7 +5405,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5443,7 +5443,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER).BookPlayerShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5535,7 +5535,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.5.2; + MARKETING_VERSION = 5.5.3; PRODUCT_BUNDLE_IDENTIFIER = "$(BP_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = BookPlayer; PROVISIONING_PROFILE_SPECIFIER = "$(BP_PROVISIONING_MAIN)";