diff --git a/apple/Omnivore.xcodeproj/project.pbxproj b/apple/Omnivore.xcodeproj/project.pbxproj index 1bd97059d..e44edd0fb 100644 --- a/apple/Omnivore.xcodeproj/project.pbxproj +++ b/apple/Omnivore.xcodeproj/project.pbxproj @@ -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 = ""; diff --git a/apple/OmnivoreKit/Sources/App/AppIntents.swift b/apple/OmnivoreKit/Sources/App/AppIntents.swift deleted file mode 100644 index 4ce8ebb0c..000000000 --- a/apple/OmnivoreKit/Sources/App/AppIntents.swift +++ /dev/null @@ -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 diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift index 248665c95..7ff6eb686 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift @@ -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"] } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 939bf0502..f3995ef9c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -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") }, diff --git a/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift b/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift index 9287b8b6c..7b31771eb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift @@ -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) diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index 078a326f0..d298e5ed4 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -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) diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index db8b0f8c5..b394dc0f7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -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") { diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 52af57555..8244b4400 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -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("", 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) diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift index 70e016241..6d887ff74 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift @@ -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() diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleReadingProgress.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleReadingProgress.swift index c3d850348..e503f0e9e 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleReadingProgress.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleReadingProgress.swift @@ -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 diff --git a/apple/Sources/AppIntents.swift b/apple/Sources/AppIntents.swift index 2152529bb..ee9d12fb3 100644 --- a/apple/Sources/AppIntents.swift +++ b/apple/Sources/AppIntents.swift @@ -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 = 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 { + 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