Merge pull request #3352 from omnivore-app/feat/ios-1.43

Use sheet instead of fullCover, add labels on PDF, add Discord button
This commit is contained in:
Jackson Harper
2024-01-11 14:36:35 +08:00
committed by GitHub
11 changed files with 261 additions and 126 deletions

View File

@ -1388,7 +1388,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.42.0;
MARKETING_VERSION = 1.43.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
@ -1423,7 +1423,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.42.0;
MARKETING_VERSION = 1.43.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1478,7 +1478,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.42.0;
MARKETING_VERSION = 1.43.0;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = Omnivore;
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1819,7 +1819,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.42.0;
MARKETING_VERSION = 1.43.0;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = Omnivore;
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -1,49 +0,0 @@
#if os(iOS)
import AppIntents
import Services
import SwiftUI
@available(iOS 16.0, *)
public struct OmnivoreAppShorcuts: AppShortcutsProvider {
@AppShortcutsBuilder public static var appShortcuts: [AppShortcut] {
AppShortcut(intent: SaveToOmnivoreIntent(), phrases: ["Save URL to \(.applicationName)"])
}
}
//
// @available(iOS 16.0, *)
// struct ExportAllTransactionsIntent: AppIntent {
// static var title: LocalizedStringResource = "Export all transactions"
//
// static var description =
// IntentDescription("Exports your transaction history as CSV data.")
// }
@available(iOS 16.0, *)
struct SaveToOmnivoreIntent: 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 {
do {
let services = Services()
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)
}
return .result(dialog: "Error saving link")
}
}
#endif

View File

@ -44,6 +44,7 @@ import Utils
@State private var errorMessage: String?
@State private var showNotebookView = false
@State private var showLabelsModal = false
@State private var hasPerformedHighlightMutations = false
@State private var errorAlertMessage: String?
@State private var showErrorAlertMessage = false
@ -131,6 +132,12 @@ import Utils
style: .plain,
target: coordinator,
action: #selector(PDFViewCoordinator.toggleNotebookView)
),
UIBarButtonItem(
image: UIImage(named: "label", in: Bundle(url: ViewsPackage.bundleURL), with: nil),
style: .plain,
target: coordinator,
action: #selector(PDFViewCoordinator.toggleLabelsView)
)
]
@ -228,14 +235,14 @@ import Utils
}
.navigationViewStyle(StackNavigationViewStyle())
}
.fullScreenCover(isPresented: $readerView, content: {
.sheet(isPresented: $readerView, content: {
PDFReaderViewController(document: document)
})
.accentColor(Color(red: 255 / 255.0, green: 234 / 255.0, blue: 159 / 255.0))
.sheet(item: $shareLink) {
ShareSheet(activityItems: [$0.url])
}
.fullScreenCover(isPresented: $showNotebookView, onDismiss: onNotebookViewDismissal) {
.sheet(isPresented: $showNotebookView, onDismiss: onNotebookViewDismissal) {
NotebookView(
viewModel: NotebookViewModel(item: viewModel.pdfItem.item),
hasHighlightMutations: $hasPerformedHighlightMutations,
@ -244,6 +251,17 @@ import Utils
}
)
}
.sheet(isPresented: $showLabelsModal) {
ApplyLabelsView(mode: .item(viewModel.pdfItem.item), onSave: { _ in
showLabelsModal = false
})
}.task {
viewModel.updateItemReadProgress(
dataService: dataService,
percent: viewModel.pdfItem.item.readingProgress,
anchorIndex: Int(viewModel.pdfItem.item.readingProgressAnchor)
)
}
} else if let errorMessage = errorMessage {
Text(errorMessage)
} else {
@ -483,6 +501,12 @@ import Utils
}
}
@objc public func toggleLabelsView() {
if let viewer = self.viewer {
viewer.showLabelsModal = !viewer.showLabelsModal
}
}
func shortHighlightIds(_ annotations: [HighlightAnnotation]) -> [String] {
annotations.compactMap { ($0.customData?["omnivoreHighlight"] as? [String: String])?["shortId"] }
}

View File

@ -295,7 +295,7 @@ struct AnimatingCellHeight: AnimatableModifier {
LibraryAddLinkView()
}
}
.fullScreenCover(isPresented: $showExpandedAudioPlayer) {
.sheet(isPresented: $showExpandedAudioPlayer) {
ExpandedAudioPlayer(
delete: {
showExpandedAudioPlayer = false
@ -330,7 +330,7 @@ struct AnimatingCellHeight: AnimatableModifier {
viewModel.selectedItem = linkedItem
viewModel.linkIsActive = true
}
.fullScreenCover(isPresented: $searchPresented) {
.sheet(isPresented: $searchPresented) {
LibrarySearchView(homeFeedViewModel: self.viewModel)
}
.task {
@ -884,7 +884,9 @@ struct AnimatingCellHeight: AnimatableModifier {
case .delete:
return AnyView(Button(
action: {
viewModel.removeLibraryItem(dataService: dataService, objectID: item.objectID)
withAnimation(.linear(duration: 0.4)) {
viewModel.removeLibraryItem(dataService: dataService, objectID: item.objectID)
}
},
label: {
Label("Remove", systemImage: "trash")
@ -893,7 +895,9 @@ struct AnimatingCellHeight: AnimatableModifier {
case .moveToInbox:
return AnyView(Button(
action: {
viewModel.moveToFolder(dataService: dataService, item: item, folder: "inbox")
withAnimation(.linear(duration: 0.4)) {
viewModel.moveToFolder(dataService: dataService, item: item, folder: "inbox")
}
},
label: {
Label(title: { Text("Move to Library") },

View File

@ -48,27 +48,32 @@ public struct LibrarySplitView: View {
$0.preferredPrimaryColumnWidth = 230
$0.displayModeButtonVisibility = .always
}
// .onOpenURL { url in
// inboxViewModel.linkRequest = nil
// if let deepLink = DeepLink.make(from: url) {
// switch deepLink {
// case let .search(query):
// inboxViewModel.searchTerm = query
// case let .savedSearch(named):
// if let filter = inboxViewModel.findFilter(dataService, named: named) {
// inboxViewModel.appliedFilter = filter
// }
// case let .webAppLinkRequest(requestID):
// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
// withoutAnimation {
// inboxViewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID)
// inboxViewModel.presentWebContainer = true
// }
// }
// }
// }
// // selectedTab = "inbox"
// }
.onOpenURL { url in
viewModel.linkRequest = nil
withoutAnimation {
NotificationCenter.default.post(Notification(name: Notification.Name("PopToRoot")))
}
if let deepLink = DeepLink.make(from: url) {
switch deepLink {
case let .search(query):
viewModel.searchTerm = query
case let .savedSearch(named):
if let filter = viewModel.findFilter(dataService, named: named) {
viewModel.appliedFilter = filter
}
case let .webAppLinkRequest(requestID):
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
withoutAnimation {
viewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID)
viewModel.presentWebContainer = true
}
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
Task {
await syncManager.syncUpdates(dataService: dataService)

View File

@ -108,7 +108,7 @@ struct LibraryTabView: View {
.padding(0)
}
}
.fullScreenCover(isPresented: $showExpandedAudioPlayer) {
.sheet(isPresented: $showExpandedAudioPlayer) {
ExpandedAudioPlayer(
delete: {
showExpandedAudioPlayer = false
@ -135,6 +135,11 @@ struct LibraryTabView: View {
}
.onOpenURL { url in
inboxViewModel.linkRequest = nil
withoutAnimation {
NotificationCenter.default.post(Notification(name: Notification.Name("PopToRoot")))
}
if let deepLink = DeepLink.make(from: url) {
switch deepLink {
case let .search(query):
@ -144,6 +149,7 @@ struct LibraryTabView: View {
inboxViewModel.appliedFilter = filter
}
case let .webAppLinkRequest(requestID):
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
withoutAnimation {
inboxViewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID)

View File

@ -162,6 +162,15 @@ struct ProfileView: View {
)
#endif
Button(
action: {
if let url = URL(string: "https://discord.gg/h2z5rppzz9") {
openURL(url)
}
},
label: { Text("Join community on Discord") }
)
Button(
action: {
if let url = URL(string: "https://omnivore.app/privacy") {

View File

@ -210,7 +210,6 @@ struct WebReaderContainerView: View {
},
label: { Label("Reset Read Location", systemImage: "arrow.counterclockwise.circle") }
)
audioMenuItem()
if viewModel.hasOriginalUrl(item) {
Button(
@ -340,28 +339,6 @@ struct WebReaderContainerView: View {
.frame(maxWidth: .infinity)
.foregroundColor(ThemeManager.currentTheme.toolbarColor)
.background(ThemeManager.currentBgColor)
.sheet(isPresented: $showLabelsModal) {
ApplyLabelsView(mode: .item(item), onSave: { labels in
showLabelsModal = false
item.labels = NSSet(array: labels)
readerSettingsChangedTransactionID = UUID()
})
}
.sheet(isPresented: $showTitleEdit) {
LinkedItemMetadataEditView(item: item, onSave: { title, _ in
item.title = title
// We dont need to update description because its never rendered in this view
readerSettingsChangedTransactionID = UUID()
})
}
#if os(iOS)
.sheet(isPresented: $showNotebookView, onDismiss: onNotebookViewDismissal) {
NotebookView(
viewModel: NotebookViewModel(item: item),
hasHighlightMutations: $hasPerformedHighlightMutations
)
}
#endif
#if os(macOS)
.buttonStyle(PlainButtonStyle())
#endif
@ -421,9 +398,12 @@ struct WebReaderContainerView: View {
.statusBar(hidden: prefersHideStatusBarInReader)
#endif
.onAppear {
if item.isUnread {
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0, force: false)
}
dataService.updateLinkReadingProgress(
itemID: item.unwrappedID,
readingProgress: max(item.readingProgress, 0.1),
anchorIndex: Int(item.readingProgressAnchor),
force: false
)
Task {
await audioController.preload(itemIDs: [item.unwrappedID])
}
@ -450,11 +430,11 @@ struct WebReaderContainerView: View {
}, label: { Text(LocalText.readerSave) })
}
#if os(iOS)
.fullScreenCover(item: $safariWebLink) {
.sheet(item: $safariWebLink) {
SafariView(url: $0.url)
.ignoresSafeArea(.all, edges: .bottom)
}
.fullScreenCover(isPresented: $showExpandedAudioPlayer) {
.sheet(isPresented: $showExpandedAudioPlayer) {
ExpandedAudioPlayer(delete: { _ in
showExpandedAudioPlayer = false
audioController.stop()
@ -519,6 +499,28 @@ struct WebReaderContainerView: View {
}
}
}
.sheet(isPresented: $showLabelsModal) {
ApplyLabelsView(mode: .item(item), onSave: { labels in
showLabelsModal = false
item.labels = NSSet(array: labels)
readerSettingsChangedTransactionID = UUID()
})
}
.sheet(isPresented: $showTitleEdit) {
LinkedItemMetadataEditView(item: item, onSave: { title, _ in
item.title = title
// We dont need to update description because its never rendered in this view
readerSettingsChangedTransactionID = UUID()
})
}
#if os(iOS)
.sheet(isPresented: $showNotebookView, onDismiss: onNotebookViewDismissal) {
NotebookView(
viewModel: NotebookViewModel(item: item),
hasHighlightMutations: $hasPerformedHighlightMutations
)
}
#endif
} else if let errorMessage = viewModel.errorMessage {
VStack {
if viewModel.allowRetry, viewModel.hasOriginalUrl(item) {
@ -620,6 +622,9 @@ struct WebReaderContainerView: View {
// WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
WebViewManager.shared().loadHTMLString(WebReaderContent.emptyContent(isDark: Color.isDarkMode), baseURL: nil)
}
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("PopToRoot"))) { _ in
pop()
}
.popup(isPresented: $viewModel.showSnackbar) {
if let operation = viewModel.snackbarOperation {
Snackbar(isShowing: $viewModel.showSnackbar, operation: operation)

View File

@ -286,7 +286,8 @@ public extension LibraryItem {
newAuthor: String? = nil,
listenPositionIndex: Int? = nil,
listenPositionOffset: Double? = nil,
listenPositionTime: Double? = nil
listenPositionTime: Double? = nil,
readAt: Date? = nil
) {
context.perform {
if let newReadingProgress = newReadingProgress {
@ -325,6 +326,10 @@ public extension LibraryItem {
self.listenPositionTime = listenPositionTime
}
if let readAt = readAt {
self.readAt = readAt
}
guard context.hasChanges else { return }
self.updatedAt = Date()

View File

@ -9,17 +9,12 @@ extension DataService {
guard let self = self else { return }
guard let linkedItem = LibraryItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return }
if let force = force, !force {
if readingProgress != 0, readingProgress < linkedItem.readingProgress {
return
}
}
print("updating reading progress: ", readingProgress, anchorIndex)
linkedItem.update(
inContext: self.backgroundContext,
newReadingProgress: readingProgress,
newAnchorIndex: anchorIndex
newAnchorIndex: anchorIndex,
readAt: Date()
)
// Send update to server

View File

@ -1,6 +1,7 @@
#if os(iOS)
import App
import AppIntents
import CoreData
import Firebase
import FirebaseMessaging
import Foundation
@ -9,6 +10,72 @@
import UIKit
import Utils
@available(iOS 16.0, *)
func filterQuery(predicte: NSPredicate, sort: NSSortDescriptor, limit: Int = 10) async throws -> [LibraryItemEntity] {
let context = await Services().dataService.viewContext
let fetchRequest: NSFetchRequest<Models.LibraryItem> = LibraryItem.fetchRequest()
fetchRequest.fetchLimit = limit
fetchRequest.predicate = predicte
fetchRequest.sortDescriptors = [sort]
return try context.performAndWait {
do {
return try context.fetch(fetchRequest).map { LibraryItemEntity(item: $0) }
} catch {
throw error
}
}
}
@available(iOS 16.0, *)
struct LibraryItemEntity: AppEntity {
static var defaultQuery = LibraryItemQuery()
let id: UUID
@Property(title: "Title")
var title: String
@Property(title: "Orignal URL")
var originalURL: String?
@Property(title: "Omnivore web URL")
var omnivoreWebURL: String
@Property(title: "Omnivore deeplink URL")
var omnivoreShortcutURL: String
init(item: Models.LibraryItem) {
self.id = UUID(uuidString: item.unwrappedID)!
self.title = item.unwrappedTitle
self.originalURL = item.pageURLString
self.omnivoreWebURL = "https://omnivore.app/me/\(item.slug!)"
self.omnivoreShortcutURL = "omnivore://read/\(item.unwrappedID)"
}
static var typeDisplayRepresentation = TypeDisplayRepresentation(
stringLiteral: "Library Item"
)
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(title)")
}
}
@available(iOS 16.0, *)
struct LibraryItemQuery: EntityQuery {
func entities(for itemIds: [UUID]) async throws -> [LibraryItemEntity] {
let predicate = NSPredicate(format: "id IN %@", itemIds)
let sort = FeaturedItemFilter.continueReading.sortDescriptor // sort by read recency
return try await filterQuery(predicte: predicate, sort: sort)
}
func suggestedEntities() async throws -> [LibraryItemEntity] {
try await filterQuery(
predicte: FeaturedItemFilter.continueReading.predicate,
sort: FeaturedItemFilter.continueReading.sortDescriptor,
limit: 10
)
}
}
@available(iOS 16.0, *)
public struct OmnivoreAppShorcuts: AppShortcutsProvider {
@AppShortcutsBuilder public static var appShortcuts: [AppShortcut] {
@ -16,15 +83,6 @@
}
}
//
// @available(iOS 16.0, *)
// struct ExportAllTransactionsIntent: AppIntent {
// static var title: LocalizedStringResource = "Export all transactions"
//
// static var description =
// IntentDescription("Exports your transaction history as CSV data.")
// }
@available(iOS 16.0, *)
struct SaveToOmnivoreIntent: AppIntent {
static var title: LocalizedStringResource = "Save to Omnivore"
@ -71,4 +129,77 @@
}
}
@available(iOS 16.4, *)
struct GetMostRecentLibraryItem: AppIntent {
static let title: LocalizedStringResource = "Get most recently read library item"
func perform() async throws -> some IntentResult & ReturnsValue<LibraryItemEntity?> {
let result = try await filterQuery(
predicte: LinkedItemFilter.all.predicate,
sort: FeaturedItemFilter.continueReading.sortDescriptor,
limit: 10
)
if let result = result.first {
return .result(value: result)
}
return .result(value: nil)
}
}
@available(iOS 16.4, *)
struct GetContinueReadingLibraryItems: AppIntent {
static let title: LocalizedStringResource = "Get your continue reading library items"
func perform() async throws -> some IntentResult & ReturnsValue<[LibraryItemEntity]> {
let result = try await filterQuery(
predicte: FeaturedItemFilter.continueReading.predicate,
sort: FeaturedItemFilter.continueReading.sortDescriptor,
limit: 10
)
return .result(value: result)
}
}
@available(iOS 16.4, *)
struct GetFollowingLibraryItems: AppIntent {
static let title: LocalizedStringResource = "Get your following library items"
func perform() async throws -> some IntentResult & ReturnsValue<[LibraryItemEntity]> {
let savedAtSort = NSSortDescriptor(key: #keyPath(Models.LibraryItem.savedAt), ascending: false)
let folderPredicate = NSPredicate(
format: "%K == %@", #keyPath(Models.LibraryItem.folder), "following"
)
let result = try await filterQuery(
predicte: folderPredicate,
sort: savedAtSort,
limit: 10
)
return .result(value: result)
}
}
@available(iOS 16.4, *)
struct GetSavedLibraryItems: AppIntent {
static let title: LocalizedStringResource = "Get your saved library items"
func perform() async throws -> some IntentResult & ReturnsValue<[LibraryItemEntity]> {
let savedAtSort = NSSortDescriptor(key: #keyPath(Models.LibraryItem.savedAt), ascending: false)
let folderPredicate = NSPredicate(
format: "%K == %@", #keyPath(Models.LibraryItem.folder), "inbox"
)
let result = try await filterQuery(
predicte: folderPredicate,
sort: savedAtSort,
limit: 10
)
return .result(value: result)
}
}
#endif