diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 35eb891ab..b18c1c177 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -137,6 +137,35 @@ import Views } } } + .sheet(isPresented: $viewModel.showAudioInfoAlert) { + VStack { + Text("Welcome to the Omnivore text to speech beta.") + .font(.appTitle) + + Spacer() + + Text( + """ + This build introduces offline text to speech files. Normally these files will\ + be downloaded in the background and made available offline. + + During the beta these files can be manually downloaded by long pressing on an item and\ + choosing Download Audio, or by tapping the play button. When you first tap the\ + play button, the audio will be generated and downloaded. This can take some time. + + Future versions will do this in the background. + """) + Text("") + + Spacer() + + Button( + action: { viewModel.dismissAudioInfoAlert() }, + label: { Text("Dismiss").frame(maxWidth: .infinity) } + ) + .buttonStyle(RoundedRectButtonStyle()) + }.padding(24) + } .task { if viewModel.items.isEmpty { loadItems(isRefresh: true) @@ -223,6 +252,8 @@ import Views struct HomeFeedListView: View { @EnvironmentObject var dataService: DataService + @EnvironmentObject var audioSession: AudioSession + @Binding var prefersListLayout: Bool @State private var itemToRemove: LinkedItem? @@ -276,6 +307,10 @@ import Views Label { Text("Snooze") } icon: { Image.moon } } } + Button( + action: { viewModel.downloadAudio(audioSession: audioSession, item: item) }, + label: { Label("Download Audio", systemImage: "icloud.and.arrow.down") } + ) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { if !item.isArchived { @@ -360,6 +395,8 @@ import Views viewModel.itemUnderLabelEdit = item case .editTitle: viewModel.itemUnderTitleEdit = item + case .downloadAudio: + viewModel.downloadAudio(audioSession: audioSession, item: item) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 755e3e979..12e199f99 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -26,6 +26,7 @@ import Views @Published var linkRequest: LinkRequest? @Published var showLoadingBar = false @Published var appliedSort = LinkedItemSort.newest.rawValue + @AppStorage(UserDefaultKey.audioInfoAlertShown.rawValue) var showAudioInfoAlert = false @AppStorage(UserDefaultKey.lastSelectedLinkedItemFilter.rawValue) var appliedFilter = LinkedItemFilter.inbox.rawValue @@ -55,7 +56,7 @@ import Views items.insert(item, at: 0) } - func loadItems(dataService: DataService, audioSession _: AudioSession, isRefresh: Bool) async { + func loadItems(dataService: DataService, audioSession: AudioSession, isRefresh: Bool) async { let syncStartTime = Date() let thisSearchIdx = searchIdx searchIdx += 1 @@ -123,7 +124,13 @@ import Views cursor = queryResult.cursor if let username = dataService.currentViewer?.username { await dataService.prefetchPages(itemIDs: newItems.map(\.unwrappedID), username: username) - // await audioSession.preload(itemIDs: newItems.map(\.unwrappedID)) + // Only preload the first item in the list. We are doing this during the beta + // because it will kick off the user's future items being automatically transcribed. + // This happens because when an article is saved, we check if the user has a recent + // listen. If they do, we will automatically transcribe their message. + if let first = newItems.first?.id { + _ = await audioSession.preload(itemIDs: [first]) + } } } else { updateFetchController(dataService: dataService) @@ -133,6 +140,19 @@ import Views showLoadingBar = false } + func dismissAudioInfoAlert() { + UserDefaults.standard.set(true, forKey: UserDefaultKey.audioInfoAlertShown.rawValue) + showAudioInfoAlert = false + } + + func downloadAudio(audioSession: AudioSession, item: LinkedItem) { + Snackbar.show(message: "Downloading Offline Audio") + Task { + let downloaded = await audioSession.preload(itemIDs: [item.unwrappedID]) + Snackbar.show(message: downloaded ? "Audio file downloaded" : "Error downloading audio") + } + } + private var fetchRequest: NSFetchRequest { let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() diff --git a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioSession.swift b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioSession.swift index abf47ab3d..9c87efa6c 100644 --- a/apple/OmnivoreKit/Sources/Services/AudioSession/AudioSession.swift +++ b/apple/OmnivoreKit/Sources/Services/AudioSession/AudioSession.swift @@ -76,7 +76,7 @@ public class AudioSession: NSObject, ObservableObject, AVAudioPlayerDelegate { downloadTask?.cancel() } - public func preload(itemIDs: [String], retryCount: Int = 0) async { + public func preload(itemIDs: [String], retryCount: Int = 0) async -> Bool { var pendingList = [String]() for pageId in itemIDs { @@ -96,23 +96,25 @@ public class AudioSession: NSObject, ObservableObject, AVAudioPlayerDelegate { if let result = result, result.pending { print("audio file is pending download: ", pageId) pendingList.append(pageId) + } else { + print("audio file is downloaded: ", pageId) } } print("audio files pending download: ", pendingList) if pendingList.isEmpty { - return + return true } if retryCount > 5 { print("reached max preload depth, stopping preloading") - return + return false } let retryDelayInNanoSeconds = UInt64(retryCount * 2 * 1_000_000_000) try? await Task.sleep(nanoseconds: retryDelayInNanoSeconds) - await preload(itemIDs: pendingList, retryCount: retryCount + 1) + return await preload(itemIDs: pendingList, retryCount: retryCount + 1) } public var localAudioUrl: URL? { diff --git a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift index deab520db..85707344d 100644 --- a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift +++ b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift @@ -13,4 +13,5 @@ public enum UserDefaultKey: String { case lastUsedAppVersion case lastUsedAppBuildNumber case lastItemSyncTime + case audioInfoAlertShown } diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift index 2cb04ca89..d3f1707e8 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift @@ -7,6 +7,7 @@ public enum GridCardAction { case delete case editLabels case editTitle + case downloadAudio } public struct GridCard: View { @@ -65,6 +66,10 @@ public struct GridCard: View { action: { menuActionHandler(.delete) }, label: { Label("Delete", systemImage: "trash") } ) + Button( + action: { menuActionHandler(.downloadAudio) }, + label: { Label("Download Audio", systemImage: "icloud.and.arrow.down") } + ) } }