Make offline audio download a user action during beta
This gets us past our cold start problem with audio, giving the user the control to start downloading items.
This commit is contained in:
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<Models.LinkedItem> {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
|
||||
|
||||
@ -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? {
|
||||
|
||||
@ -13,4 +13,5 @@ public enum UserDefaultKey: String {
|
||||
case lastUsedAppVersion
|
||||
case lastUsedAppBuildNumber
|
||||
case lastItemSyncTime
|
||||
case audioInfoAlertShown
|
||||
}
|
||||
|
||||
@ -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") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user