Merge pull request #3901 from omnivore-app/feat/ios-skip-chapters

iOS Digest Chapters + audio issues
This commit is contained in:
Jackson Harper
2024-05-06 18:01:28 +08:00
committed by GitHub
11 changed files with 357 additions and 144 deletions

View File

@ -4,31 +4,74 @@ import Services
import Views
import MarkdownUI
import Utils
import Transmission
func getChapterData(digest: DigestResult) -> [(DigestChapter, DigestChapterData)] {
let speed = 1.0
var chapterData: [(DigestChapter, DigestChapterData)] = []
var currentAudioIndex = 0
var currentWordCount = 0.0
for (index, speechFile) in digest.speechFiles.enumerated() {
let chapter = digest.chapters[index]
let duration = currentWordCount / SpeechDocument.averageWPM / speed * 60.0
chapterData.append((chapter, DigestChapterData(
time: formatTimeInterval(duration) ?? "00:00",
start: Int(currentAudioIndex),
end: currentAudioIndex + Int(speechFile.utterances.count)
)))
currentAudioIndex += Int(speechFile.utterances.count)
currentWordCount += chapter.wordCount
}
return chapterData
}
func formatTimeInterval(_ time: TimeInterval) -> String? {
let componentFormatter = DateComponentsFormatter()
componentFormatter.unitsStyle = .positional
componentFormatter.allowedUnits = time >= 3600 ? [.second, .minute, .hour] : [.second, .minute]
componentFormatter.zeroFormattingBehavior = .pad
return componentFormatter.string(from: time)
}
@MainActor
public class FullScreenDigestViewModel: ObservableObject {
@Published var isLoading = false
@Published var digest: DigestResult?
@Published var chapterInfo: [(DigestChapter, DigestChapterData)]?
@Published var presentedLibraryItem: String?
@Published var presentWebContainer = false
@AppStorage(UserDefaultKey.lastVisitedDigestId.rawValue) var lastVisitedDigestId = ""
func load(dataService: DataService, audioController: AudioController) async {
if let digest = dataService.loadStoredDigest() {
self.digest = digest
} else {
isLoading = true
}
do {
if let digest = try await dataService.getLatestDigest(timeoutInterval: 10) {
if !digestNeedsRefresh() {
if let digest = dataService.loadStoredDigest() {
self.digest = digest
lastVisitedDigestId = digest.id
if let playingDigest = audioController.itemAudioProperties as? DigestAudioItem, playingDigest.digest.id == digest.id {
// Don't think we need to do anything here
} else {
audioController.play(itemAudioProperties: DigestAudioItem(digest: digest))
}
}
} catch {
print("ERROR WITH DIGEST: ", error)
} else {
do {
if let digest = try await dataService.getLatestDigest(timeoutInterval: 10) {
self.digest = digest
}
} catch {
print("ERROR WITH DIGEST: ", error)
self.digest = nil
}
}
if let digest = self.digest {
self.digest = digest
self.chapterInfo = getChapterData(digest: digest)
self.lastVisitedDigestId = digest.id
if let playingDigest = audioController.itemAudioProperties as? DigestAudioItem, playingDigest.digest.id == digest.id {
// Don't think we need to do anything here
} else {
let chapterData = self.chapterInfo?.map { $0.1 }
audioController.play(itemAudioProperties: DigestAudioItem(digest: digest, chapters: chapterData ?? []))
}
}
isLoading = false
@ -41,6 +84,22 @@ public class FullScreenDigestViewModel: ObservableObject {
print("ERROR WITH DIGEST: ", error)
}
}
func digestNeedsRefresh() -> Bool {
let fileManager = FileManager.default
let localURL = URL.om_cachesDirectory.appendingPathComponent("digest.json")
do {
let attributes = try fileManager.attributesOfItem(atPath: localURL.path)
if let modificationDate = attributes[.modificationDate] as? Date {
// Two hours ago
let twoHoursAgo = Date().addingTimeInterval(-2 * 60 * 60)
return modificationDate < twoHoursAgo
}
} catch {
print("Error: \(error)")
}
return true
}
}
@available(iOS 17.0, *)
@ -82,10 +141,36 @@ struct FullScreenDigestView: View {
return ""
}
var slideTransition: PresentationLinkTransition {
PresentationLinkTransition.slide(
options: PresentationLinkTransition.SlideTransitionOptions(
edge: .trailing,
options: PresentationLinkTransition.Options(
modalPresentationCapturesStatusBarAppearance: true
)
))
}
var body: some View {
VStack {
titleBlock
if let presentedLibraryItem = self.viewModel.presentedLibraryItem {
PresentationLink(
transition: slideTransition,
isPresented: $viewModel.presentWebContainer,
destination: {
WebReaderLoadingContainer(requestID: presentedLibraryItem)
.background(ThemeManager.currentBgColor)
.onDisappear {
self.viewModel.presentedLibraryItem = nil
}
}, label: {
EmptyView()
}
)
}
Group {
if viewModel.isLoading {
VStack {
@ -114,35 +199,6 @@ struct FullScreenDigestView: View {
.buttonStyle(.plain)
}
func getChapterData(digest: DigestResult) -> [String:(time: String, start: Int, end: Int)] {
let speed = 1.0
var chapterData: [String:(time: String, start: Int, end: Int)] = [:]
var currentAudioIndex = 0
var currentWordCount = 0.0
for (index, speechFile) in digest.speechFiles.enumerated() {
let chapter = digest.chapters[index]
let duration = currentWordCount / SpeechDocument.averageWPM / speed * 60.0
chapterData[chapter.id] = (
time: formatTimeInterval(duration) ?? "00:00",
start: Int(currentAudioIndex),
end: currentAudioIndex + Int(speechFile.utterances.count)
)
currentAudioIndex += Int(speechFile.utterances.count)
currentWordCount += chapter.wordCount
}
return chapterData
}
func formatTimeInterval(_ time: TimeInterval) -> String? {
let componentFormatter = DateComponentsFormatter()
componentFormatter.unitsStyle = .positional
componentFormatter.allowedUnits = time >= 3600 ? [.second, .minute, .hour] : [.second, .minute]
componentFormatter.zeroFormattingBehavior = .pad
return componentFormatter.string(from: time)
}
@available(iOS 17.0, *)
var itemBody: some View {
VStack {
@ -166,11 +222,6 @@ struct FullScreenDigestView: View {
.font(Font.system(size: 12))
.foregroundColor(Color(hex: "#898989"))
.lineLimit(1)
Text(digest.description)
.font(Font.system(size: 14))
.lineSpacing(/*@START_MENU_TOKEN@*/10.0/*@END_MENU_TOKEN@*/)
.foregroundColor(Color.themeLibraryItemSubtle)
.lineLimit(6)
} else {
Text("We're building you a new digest")
.font(Font.system(size: 17, weight: .semibold))
@ -182,30 +233,35 @@ struct FullScreenDigestView: View {
.background(Color.themeLabelBackground.opacity(0.6))
.cornerRadius(5)
if let digest = viewModel.digest {
if let chapters = viewModel.chapterInfo {
VStack(alignment: .leading, spacing: 10) {
Text("Chapters")
.font(Font.system(size: 17, weight: .semibold))
.frame(maxWidth: .infinity, alignment: .leading)
.padding(0)
let chapterData = getChapterData(digest: digest)
ForEach(digest.chapters, id: \.id) { chapter in
if let startTime = chapterData[chapter.id]?.time, let skipIndex = chapterData[chapter.id]?.start {
let currentChapter = audioController.currentAudioIndex >= (chapterData[chapter.id]?.start ?? 0) &&
audioController.currentAudioIndex < (chapterData[chapter.id]?.end ?? 0)
ChapterView(
startTime: startTime,
skipIndex: skipIndex,
chapter: chapter
)
.onTapGesture {
audioController.seek(toIdx: skipIndex)
ForEach(chapters, id: \.0.id) { chaps in
let (chapter, chapterData) = chaps
let currentChapter = (audioController.currentAudioIndex >= chapterData.start && audioController.currentAudioIndex < chapterData.end)
ChapterView(
startTime: chapterData.time,
skipIndex: chapterData.start,
chapter: chapter
)
.onTapGesture {
audioController.seek(toIdx: chapterData.start)
if audioController.state != .loading && !audioController.isPlaying {
audioController.unpause()
}
.background(
currentChapter ? Color.themeLabelBackground.opacity(0.6) : Color.clear
)
.cornerRadius(5)
}
.onLongPressGesture {
viewModel.presentedLibraryItem = chapter.id
viewModel.presentWebContainer = true
}
.background(
currentChapter ? Color.themeLabelBackground.opacity(0.6) : Color.clear
)
.cornerRadius(5)
}
}
.padding(.top, 20)
@ -238,27 +294,27 @@ struct FullScreenDigestView: View {
RatingWidget()
Spacer(minLength: 60)
}
VStack(alignment: .leading, spacing: 20) {
Text("If you didn't like today's digest or would like another one you can create another one. The process takes a few minutes")
Button(action: {
Task {
await viewModel.refreshDigest(dataService: dataService)
}
}, label: {
Text("Create new digest")
.font(Font.system(size: 13, weight: .medium))
.padding(.horizontal, 8)
.padding(.vertical, 5)
.tint(Color.blue)
.background(Color.themeLabelBackground)
.cornerRadius(5)
})
}
.padding(15)
.background(Color.themeLabelBackground.opacity(0.6))
.cornerRadius(5)
//
// VStack(alignment: .leading, spacing: 20) {
// Text("If you didn't like today's digest or would like another one you can create another one. The process takes a few minutes")
// Button(action: {
// Task {
// await viewModel.refreshDigest(dataService: dataService)
// }
// }, label: {
// Text("Create new digest")
// .font(Font.system(size: 13, weight: .medium))
// .padding(.horizontal, 8)
// .padding(.vertical, 5)
// .tint(Color.blue)
// .background(Color.themeLabelBackground)
// .cornerRadius(5)
// })
// }
// .padding(15)
// .background(Color.themeLabelBackground.opacity(0.6))
// .cornerRadius(5)
//
}.contentMargins(10, for: .scrollContent)
Spacer()

View File

@ -114,6 +114,7 @@ import Views
if let appliedFilter = filterState.appliedFilter {
let shouldRemoteSearch = forceRemote || items.count < 1 || isRefresh && appliedFilter.shouldRemoteSearch
if shouldRemoteSearch {
updateFetchController(dataService: dataService, filterState: filterState)
await loadSearchQuery(dataService: dataService, filterState: filterState, isRefresh: isRefresh)
} else {
updateFetchController(dataService: dataService, filterState: filterState)

View File

@ -38,7 +38,7 @@ enum LoadingBarStyle {
@Published var selectedLabels = [LinkedItemLabel]()
@Published var negatedLabels = [LinkedItemLabel]()
@Published var appliedSort = LinkedItemSort.newest.rawValue
@Published var digestIsUnread = false
@State var lastMoreFetched: Date?

View File

@ -323,7 +323,7 @@ struct WebReaderContainerView: View {
.padding(.trailing, 5)
.popover(isPresented: $showPreferencesPopover) {
webPreferencesPopoverView
.frame(maxWidth: 400, maxHeight: 475)
.frame(width: 400, height: 475)
}
.formSheet(isPresented: $showPreferencesFormsheet, modalSize: CGSize(width: 400, height: 475)) {
webPreferencesPopoverView

View File

@ -33,16 +33,20 @@ public struct DigestAudioItem: AudioItemProperties {
public let digest: DigestResult
public let itemID: String
public let title: String
public let chapters: [DigestChapterData]
public var byline: String?
public var imageURL: URL?
public var language: String?
public var startIndex: Int = 0
public var startOffset: Double = 0.0
public init(digest: DigestResult) {
public init(digest: DigestResult, chapters: [DigestChapterData]) {
self.digest = digest
self.itemID = digest.id
self.title = digest.title
self.chapters = chapters
self.startIndex = 0
self.startOffset = 0
@ -697,7 +701,6 @@ public struct DigestAudioItem: AudioItemProperties {
player?.insert(playerItem, after: nil)
if player?.items().count == 1, atOffset > 0.0 {
playerItem.seek(to: CMTimeMakeWithSeconds(atOffset, preferredTimescale: 600)) { success in
print("success seeking to time: ", success)
self.fireTimer()
}
}
@ -810,15 +813,16 @@ public struct DigestAudioItem: AudioItemProperties {
let percentProgress = timeElapsed / duration
let speechIndex = (player?.currentItem as? SpeechPlayerItem)?.speechItem.audioIdx ?? 0
let anchorIndex = Int((player?.currentItem as? SpeechPlayerItem)?.speechItem.htmlIdx ?? "") ?? 0
if let itemID = itemAudioProperties?.itemID {
dataService.updateLinkReadingProgress(itemID: itemID, readingProgress: percentProgress, anchorIndex: anchorIndex, force: true)
}
if let itemID = itemAudioProperties?.itemID, let player = player, let currentItem = player.currentItem {
if let itemID = itemAudioProperties?.itemID,
let player = player,
let currentItem = player.currentItem,
itemAudioProperties?.audioItemType == .libraryItem {
let currentOffset = CMTimeGetSeconds(currentItem.currentTime())
print("updating listening info: ", speechIndex, currentOffset, timeElapsed)
dataService.updateLinkListeningProgress(itemID: itemID,
listenIndex: speechIndex,
listenOffset: currentOffset,
@ -907,12 +911,61 @@ public struct DigestAudioItem: AudioItemProperties {
}
return .commandFailed
}
if let digest = self.itemAudioProperties as? DigestAudioItem {
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.addTarget { event -> MPRemoteCommandHandlerStatus in
if let next = self.nextChapterIndex(chapters: digest.chapters, idx: self.currentAudioIndex) {
self.seek(toIdx: next)
return .success
}
return .commandFailed
}
commandCenter.previousTrackCommand.isEnabled = true
commandCenter.previousTrackCommand.addTarget { event -> MPRemoteCommandHandlerStatus in
if let next = self.prevChapterIndex(chapters: digest.chapters, idx: self.currentAudioIndex) {
self.seek(toIdx: next)
return .success
}
return .commandFailed
}
}
Task {
await downloadAndSetArtwork()
}
}
func nextChapterIndex(chapters: [DigestChapterData], idx: Int) -> Int? {
if let chapterIdx = currentChapterIndex(chapters: chapters, idx: idx) {
if chapterIdx + 1 < chapters.count {
return chapters[chapterIdx + 1].start
}
}
return nil
}
func prevChapterIndex(chapters: [DigestChapterData], idx: Int) -> Int? {
if let chapterIdx = currentChapterIndex(chapters: chapters, idx: idx) {
if chapterIdx - 1 > 0 {
return chapterIdx - 1
}
}
return nil
}
func currentChapterIndex(chapters: [DigestChapterData], idx: Int) -> Int? {
for (chapterIdx, chapter) in chapters.enumerated() {
if idx >= chapter.start && idx < chapter.end {
if chapterIdx + 1 < chapters.count {
return chapterIdx
}
}
}
return nil
}
func isoLangForCurrentVoice() -> String {
// currentVoicePair should not ever be nil but if it is we return an empty string
if let isoLang = currentVoicePair?.language {
@ -965,9 +1018,7 @@ public struct DigestAudioItem: AudioItemProperties {
throw BasicError.message(messageText: "audioFetch failed. no data received.")
}
let str = String(decoding: data, as: UTF8.self)
print("result speech file: ", str)
let str = String(decoding: data, as: UTF8.self)
if let document = try? JSONDecoder().decode(SpeechDocument.self, from: data) {
do {
try? FileManager.default.createDirectory(at: document.audioDirectory, withIntermediateDirectories: true)

View File

@ -70,11 +70,9 @@ class SpeechPlayerItem: AVPlayerItem {
DispatchQueue.main.async {
if self.speechItem.audioIdx > self.session.currentAudioIndex + 5 {
// prefetch has gotten too far ahead of the audio. Pause the prefetch queue
print("PAUSING PREFETCH QUEUE", self.speechItem.audioIdx, self.session.currentAudioIndex + 10, self.speechItem.text)
prefetchQueue.isSuspended = true
}
if self.speechItem.audioIdx < self.session.currentAudioIndex + 5 {
print("RESUMING PREFETCH QUEUE", self.speechItem.audioIdx, self.session.currentAudioIndex + 5)
prefetchQueue.isSuspended = false
}
}

View File

@ -36,6 +36,18 @@ public struct DigestChapter: Codable {
}
}
public struct DigestChapterData {
public let time: String
public let start: Int
public let end: Int
public init(time: String, start: Int, end: Int) {
self.time = time
self.start = start
self.end = end
}
}
public struct RefreshDigestResult: Codable {
public let jobId: String
}

View File

@ -175,6 +175,15 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable {
position: 10,
defaultFilter: true
),
InternalFilter(
id: "following_unread",
name: "Unread",
folder: "following",
filter: "in:following is:unread",
visible: true,
position: 11,
defaultFilter: true
),
InternalFilter(
id: "rss",
name: "Feeds",

View File

@ -55,7 +55,7 @@ func savedDateString(_ savedAt: Date?) -> String {
dateFormatter.dateFormat = "MMM dd"
}
dateFormatter.locale = locale
return dateFormatter.string(from: savedAt) + ""
return dateFormatter.string(from: savedAt)
}
return ""
}
@ -160,12 +160,19 @@ public struct LibraryItemCard: View {
var estimatedReadingTime: String {
if item.wordsCount > 0 {
let readLen = max(1, item.wordsCount / readingSpeed)
return "\(readLen) MIN READ"
return "\(readLen) MIN READ"
}
return ""
}
var readingProgress: String {
if item.readingProgress < 2 {
return ""
}
var readingProgress = item.readingProgress
if readingProgress > 95 {
readingProgress = 100
}
// If there is no wordsCount don't show progress because it will make no sense
if item.wordsCount > 0 {
return "\(String(format: "%d", Int(item.readingProgress)))%"
@ -181,18 +188,15 @@ public struct LibraryItemCard: View {
item.wordsCount > 0 || item.highlights?.first { ($0 as? Highlight)?.annotation != nil } != nil
}
var highlightsText: String {
var highlightsStr: String {
if let highlights = item.highlights, highlights.count > 0 {
let fmted = LocalText.pluralizedText(key: "number_of_highlights", count: highlights.count)
if item.wordsCount > 0 || item.isPDF {
return "\(fmted)"
}
return fmted
}
return ""
}
var notesText: String {
var notesStr: String {
let notes = item.highlights?.filter { item in
if let highlight = item as? Highlight {
return !(highlight.annotation ?? "").isEmpty
@ -202,9 +206,6 @@ public struct LibraryItemCard: View {
if let notes = notes, notes.count > 0 {
let fmted = LocalText.pluralizedText(key: "number_of_notes", count: notes.count)
if hasMultipleInfoItems {
return "\(fmted)"
}
return fmted
}
return ""
@ -228,35 +229,66 @@ public struct LibraryItemCard: View {
}
}
var savedAtText: Text? {
if !savedAtStr.isEmpty {
return Text(savedAtStr)
.font(.footnote)
.foregroundColor(Color.themeLibraryItemSubtle)
}
return nil
}
var estimatedReadingTimeText: Text? {
if !estimatedReadingTime.isEmpty {
return Text("\(estimatedReadingTime)")
.font(.footnote)
.foregroundColor(Color.themeLibraryItemSubtle)
}
return nil
}
var readingProgressText: Text? {
if !readingProgress.isEmpty {
return Text("\(readingProgress)")
.font(.footnote)
.foregroundColor(isPartiallyRead ? Color.appGreenSuccess : Color.themeLibraryItemSubtle)
}
return nil
}
var highlightsText: Text? {
if !highlightsStr.isEmpty {
return Text("\(highlightsStr)")
.font(.footnote)
.foregroundColor(Color.themeLibraryItemSubtle)
}
return nil
}
var notesText: Text? {
if !notesStr.isEmpty {
return Text("\(notesStr)")
.font(.footnote)
.foregroundColor(Color.themeLibraryItemSubtle)
}
return nil
}
var readInfo: some View {
HStack(alignment: .center, spacing: 5.0) {
ForEach(flairLabels, id: \.self) {
$0.icon
}
Text(savedAtStr)
.font(.footnote)
.foregroundColor(Color.themeLibraryItemSubtle)
let texts = [savedAtText, estimatedReadingTimeText, readingProgressText, highlightsText, notesText]
.compactMap { $0 }
+
Text("\(estimatedReadingTime)")
.font(.footnote)
.foregroundColor(Color.themeLibraryItemSubtle)
+
Text("\(readingProgress)")
.font(.footnote)
.foregroundColor(isPartiallyRead ? Color.appGreenSuccess : Color.themeLibraryItemSubtle)
+
Text("\(highlightsText)")
.font(.footnote)
.foregroundColor(Color.themeLibraryItemSubtle)
+
Text("\(notesText)")
.font(.footnote)
.foregroundColor(Color.themeLibraryItemSubtle)
if texts.count > 0 {
texts.dropLast().reduce(Text("")) { result, text in
result + text + Text("").font(.footnote).foregroundColor(Color.themeLibraryItemSubtle)
} + texts.last!
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}

View File

@ -216,7 +216,9 @@ public enum WebFont: String, CaseIterable {
updateReaderPreferences()
}, label: { Image(systemName: "textformat.size.smaller") })
.buttonStyle(.plain)
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(.appGrayTextContrast.opacity(0.4))
CustomSlider(value: $storedFontSize, minValue: 10, maxValue: 48) { _ in
if storedFontSize % 1 == 0 {
updateReaderPreferences()
@ -230,7 +232,9 @@ public enum WebFont: String, CaseIterable {
updateReaderPreferences()
}, label: { Image(systemName: "textformat.size.larger") })
.buttonStyle(.plain)
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(.appGrayTextContrast.opacity(0.4))
}
}
@ -244,7 +248,9 @@ public enum WebFont: String, CaseIterable {
updateReaderPreferences()
}, label: { Image("margin-smaller", bundle: .module) })
.buttonStyle(.plain)
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(.appGrayTextContrast.opacity(0.4))
CustomSlider(value: $storedMaxWidthPercentage, minValue: minValue, maxValue: 100) { _ in
updateReaderPreferences()
}
@ -256,7 +262,9 @@ public enum WebFont: String, CaseIterable {
updateReaderPreferences()
}, label: { Image("margin-larger", bundle: .module) })
.buttonStyle(.plain)
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(.appGrayTextContrast.opacity(0.4))
}
}
@ -267,7 +275,9 @@ public enum WebFont: String, CaseIterable {
updateReaderPreferences()
}, label: { Image("lineheight-smaller", bundle: .module) })
.buttonStyle(.plain)
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(.appGrayTextContrast.opacity(0.4))
CustomSlider(value: $storedLineSpacing, minValue: 100, maxValue: 300) { _ in
updateReaderPreferences()
}
@ -279,7 +289,9 @@ public enum WebFont: String, CaseIterable {
updateReaderPreferences()
}, label: { Image("lineheight-larger", bundle: .module) })
.buttonStyle(.plain)
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(.appGrayTextContrast.opacity(0.4))
}
}
@ -288,7 +300,9 @@ public enum WebFont: String, CaseIterable {
Button(action: {
storedFontSize = min(storedFontSize + 2, 28)
}, label: { Image("brightness-lower", bundle: .module) })
.buttonStyle(.plain)
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(.appGrayTextContrast.opacity(0.4))
CustomSlider(value: $storedFontSize, minValue: 10, maxValue: 28) { _ in
updateReaderPreferences()
}
@ -297,7 +311,9 @@ public enum WebFont: String, CaseIterable {
Button(action: {
storedFontSize = max(storedFontSize - 2, 10)
}, label: { Image("brightness-higher", bundle: .module) })
.buttonStyle(.plain)
.frame(width: 25, height: 25, alignment: .center)
.foregroundColor(.appGrayTextContrast.opacity(0.4))
}
}
@ -371,6 +387,7 @@ public enum WebFont: String, CaseIterable {
Text("Margin")
.font(Font.system(size: 14))
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.appGrayTextContrast)
marginSlider
.padding(.top, 5)
@ -379,6 +396,7 @@ public enum WebFont: String, CaseIterable {
Text("Line Height")
.font(Font.system(size: 14))
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.appGrayTextContrast)
lineHeightSlider
// .padding(.bottom, 20)
@ -390,7 +408,7 @@ public enum WebFont: String, CaseIterable {
//
// brightnessSlider
}.tint(Color(hex: "#6A6968"))
}.tint(.appGrayTextContrast)
.padding(.horizontal, 30)
Divider()
@ -401,6 +419,7 @@ public enum WebFont: String, CaseIterable {
Text("Theme")
.font(Font.system(size: 14))
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.appGrayTextContrast)
Spacer()
systemThemeCheckbox

View File

@ -27,6 +27,20 @@
}
}
@available(iOS 16.0, *)
enum OmnivoreIntentError: Swift.Error, CustomLocalizedStringResourceConvertible {
case general
case message(_ message: String)
var localizedStringResource: LocalizedStringResource {
switch self {
case let .message(message): return "Error: \(message)"
case .general: return "My general error"
}
}
}
@available(iOS 16.0, *)
struct LibraryItemEntity: AppEntity {
static var defaultQuery = LibraryItemQuery()
@ -108,15 +122,36 @@
var link: URL
@MainActor
func perform() async throws -> some IntentResult & ProvidesDialog {
do {
let requestId = UUID().uuidString.lowercased()
_ = try? await Services().dataService.saveURL(id: requestId, url: link.absoluteString)
return .result(dialog: "Link saved to Omnivore")
} catch {
print("error saving URL: ", error)
func perform() async throws -> some IntentResult & ProvidesDialog & ReturnsValue<URL> {
let requestId = UUID().uuidString.lowercased()
let result = try? await Services().dataService.saveURL(id: requestId, url: link.absoluteString)
if let result = result, let deepLink = URL(string: "omnivore://read/\(result)") {
return .result(value: deepLink, dialog: "Link saved")
}
return .result(dialog: "Error saving link")
throw OmnivoreIntentError.message("Unable to save link")
}
}
@available(iOS 16.0, *)
struct SaveToOmnivoreAndReturnDeeplinkIntent: AppIntent {
static var title: LocalizedStringResource = "Save to Omnivore"
static var description: LocalizedStringResource = "Save a URL to your Omnivore library"
static var parameterSummary: some ParameterSummary {
Summary("Save \(\.$link) to your Omnivore library.")
}
@Parameter(title: "link")
var link: URL
@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<URL> {
let requestId = UUID().uuidString.lowercased()
let result = try? await Services().dataService.saveURL(id: requestId, url: link.absoluteString)
if let result = result, let deepLink = URL(string: "omnivore://read/\(result)") {
return .result(value: deepLink)
}
throw OmnivoreIntentError.message("Unable to save link")
}
}