From e63b4f9b2c8dfb4e62d07c89862cca4a16527883 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 16 Nov 2023 16:07:50 +0800 Subject: [PATCH 01/35] Abstract out fetching from view model so we can better handle multiple fetch folders Rename LinkedItem to LibraryItem More on following Add new fetcher Tab bar --- .../xcshareddata/swiftpm/Package.resolved | 36 + apple/OmnivoreKit/Package.swift | 7 +- .../Share/ShareExtensionViewModel.swift | 4 +- .../Sources/App/Views/BriefingView.swift | 237 ++-- .../Views/Highlights/NotebookViewModel.swift | 6 +- .../Components/FeedCardNavigationLink.swift | 78 +- .../Home/Components/FollowingFetcher.swift | 257 ++++ .../Views/Home/Components/InboxFetcher.swift | 276 +++++ .../LibraryFeatureCardNavigationLink.swift | 2 +- .../App/Views/Home/FilterSelectorView.swift | 121 ++ .../App/Views/Home/HomeFeedViewIOS.swift | 169 ++- .../App/Views/Home/HomeFeedViewMac.swift | 4 +- .../App/Views/Home/HomeFeedViewModel.swift | 290 +---- .../App/Views/Home/LibraryItemFetcher.swift | 43 + .../App/Views/Home/LibraryItemMenu.swift | 56 +- .../App/Views/Home/LibraryListView.swift | 19 +- .../App/Views/Home/LibrarySearchView.swift | 10 +- .../App/Views/Labels/ApplyLabelsView.swift | 2 +- .../App/Views/Labels/LabelsViewModel.swift | 2 +- .../Sources/App/Views/LibraryTabView.swift | 40 +- .../App/Views/LinkItemDetailView.swift | 12 +- .../Views/LinkedItemMetadataEditView.swift | 8 +- .../App/Views/PrimaryContentView.swift | 4 +- .../App/Views/Profile/ProfileView.swift | 7 +- .../App/Views/RemoveLibraryItemAction.swift | 4 +- .../App/Views/TabBar/CustomTabBar.swift | 41 + .../App/Views/WebReader/WebReader.swift | 2 +- .../Views/WebReader/WebReaderContainer.swift | 4 +- .../Views/WebReader/WebReaderContent.swift | 4 +- .../WebReader/WebReaderLoadingContainer.swift | 7 +- .../Views/WebReader/WebReaderViewModel.swift | 4 +- .../CoreDataModel.xcdatamodel/contents | 9 +- .../Sources/Models/DataModels/FeedItem.swift | 9 +- .../Sources/Models/DataModels/PDFItem.swift | 2 +- .../Sources/Models/InboxFilters.swift | 178 +-- .../Sources/Models/LinkedItemFilter.swift | 227 ++++ .../Sources/Models/LinkedItemSort.swift | 10 +- .../Services/DataService/ContentLoading.swift | 10 +- .../Services/DataService/DataService.swift | 6 +- .../FetchLinkedItemsBackgroundTask.swift | 2 +- .../Services/DataService/GQLSchema.swift | 1088 +++++++++++++++++ .../DataService/Mutations/ArchiveLink.swift | 4 +- .../Mutations/BulkActionMutation.swift | 2 +- .../DataService/Mutations/RemoveLink.swift | 6 +- .../DataService/Mutations/UndeleteItem.swift | 2 +- .../UpdateArticleLabelsPublisher.swift | 4 +- ...UpdateArticleListenProgressPublisher.swift | 2 +- .../UpdateArticleReadingProgress.swift | 4 +- .../Mutations/UpdateLinkedItemTitle.swift | 4 +- .../Services/DataService/OfflineSync.swift | 10 +- .../Public/LinkedItemLoading.swift | 2 +- .../DataService/Public/PDFLoading.swift | 4 +- .../Queries/ArticleContentQuery.swift | 5 +- .../Queries/LinkedItemNetworkQuery.swift | 20 +- .../InternalModels/InternalFilter.swift | 14 +- .../InternalModels/InternalHighlight.swift | 2 +- ...edItem.swift => InternalLibraryItem.swift} | 17 +- .../Sources/Views/Colors/Colors.swift | 2 + .../_themeTabBarColor.colorset/Contents.json | 38 + .../Contents.json | 38 + .../Sources/Views/FeedItem/GridCard.swift | 4 +- .../Views/FeedItem/LibraryFeatureCard.swift | 4 +- .../Views/FeedItem/LibraryItemCard.swift | 6 +- .../Sources/Views/Images/Images.swift | 8 +- .../_profileTab.imageset/Contents.json | 12 - .../_profileTab.imageset/profile-tab.svg | 5 - .../Contents.json | 12 - .../profile-tab-selected.svg | 6 - .../Group 1000002644.png | Bin 1104 -> 0 bytes .../Contents.json | 2 +- .../_tab_following.imageset/Frame.svg | 12 + .../_tab_library.imageset/BookOpen.png | Bin 482 -> 0 bytes .../_tab_library.imageset/Contents.json | 2 +- .../_tab_library.imageset/Frame.svg | 11 + .../Contents.json | 2 +- .../_tab_search.imageset/MagnifyingGlass.png | Bin 0 -> 552 bytes .../BookmarksSimple.png | Bin 707 -> 0 bytes apple/OmnivoreKit/Sources/Views/Theme.swift | 2 +- apple/swiftgraphql.yml | 1 + 79 files changed, 2758 insertions(+), 808 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/Home/Components/FollowingFetcher.swift create mode 100644 apple/OmnivoreKit/Sources/App/Views/Home/Components/InboxFetcher.swift create mode 100644 apple/OmnivoreKit/Sources/App/Views/Home/FilterSelectorView.swift create mode 100644 apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemFetcher.swift create mode 100644 apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift create mode 100644 apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift rename apple/OmnivoreKit/Sources/Services/InternalModels/{InternalLinkedItem.swift => InternalLibraryItem.swift} (91%) create mode 100644 apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_themeTabBarColor.colorset/Contents.json create mode 100644 apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_themeTabButtonColor.colorset/Contents.json delete mode 100644 apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTab.imageset/Contents.json delete mode 100644 apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTab.imageset/profile-tab.svg delete mode 100644 apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTabSelected.imageset/Contents.json delete mode 100644 apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTabSelected.imageset/profile-tab-selected.svg delete mode 100644 apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_briefing.imageset/Group 1000002644.png rename apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/{_tab_subscriptions.imageset => _tab_following.imageset}/Contents.json (89%) create mode 100644 apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_following.imageset/Frame.svg delete mode 100644 apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_library.imageset/BookOpen.png create mode 100644 apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_library.imageset/Frame.svg rename apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/{_tab_briefing.imageset => _tab_search.imageset}/Contents.json (88%) create mode 100644 apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_search.imageset/MagnifyingGlass.png delete mode 100644 apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_subscriptions.imageset/BookmarksSimple.png diff --git a/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved index d60e90e69..3fd58797a 100644 --- a/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "version" : "0.9.0" } }, + { + "identity" : "engine", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nathantannar4/Engine", + "state" : { + "revision" : "31949c114698e4fd43fd76290913bca415fa87bc", + "version" : "1.1.0" + } + }, { "identity" : "files", "kind" : "remoteSourceControl", @@ -207,6 +216,15 @@ "version" : "1.19.0" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", + "version" : "509.0.2" + } + }, { "identity" : "swiftformat", "kind" : "remoteSourceControl", @@ -225,6 +243,24 @@ "version" : "0.1.4" } }, + { + "identity" : "transmission", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nathantannar4/Transmission", + "state" : { + "revision" : "9517912f8f528c777f86f7896b5c35d7e43fa916", + "version" : "1.0.1" + } + }, + { + "identity" : "turbocharger", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nathantannar4/Turbocharger", + "state" : { + "revision" : "b4201ba0bc094facf6cabe3b36fd3763b51ccfc8", + "version" : "1.0.1" + } + }, { "identity" : "valet", "kind" : "remoteSourceControl", diff --git a/apple/OmnivoreKit/Package.swift b/apple/OmnivoreKit/Package.swift index 46084404c..4698e8436 100644 --- a/apple/OmnivoreKit/Package.swift +++ b/apple/OmnivoreKit/Package.swift @@ -15,6 +15,7 @@ let package = Package( .library(name: "Services", targets: ["Services"]), .library(name: "Models", targets: ["Models"]), .library(name: "Utils", targets: ["Utils"]) + ], dependencies: dependencies, targets: [ @@ -26,7 +27,8 @@ let package = Package( "Models", .product(name: "Introspect", package: "SwiftUI-Introspect"), .product(name: "MarkdownUI", package: "swift-markdown-ui"), - .productItem(name: "PopupView", package: "PopupView") + .productItem(name: "PopupView", package: "PopupView"), + .product(name: "Transmission", package: "Transmission") ], resources: [.process("Resources")] ), @@ -70,7 +72,8 @@ var dependencies: [Package.Dependency] { .package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2"), .package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.0.0"), .package(url: "https://github.com/exyte/PopupView.git", from: "2.6.0"), - .package(url: "https://github.com/PostHog/posthog-ios.git", from: "2.0.0") + .package(url: "https://github.com/PostHog/posthog-ios.git", from: "2.0.0"), + .package(url: "https://github.com/nathantannar4/Transmission", from: "1.0.1") ] // Comment out following line for macOS build deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", from: "13.1.0")) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift index 61814a5ad..bc99263ed 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -11,7 +11,7 @@ public class ShareExtensionViewModel: ObservableObject { @Published public var url: String? @Published public var iconURL: URL? @Published public var highlightData: HighlightData? - @Published public var linkedItem: LinkedItem? + @Published public var linkedItem: Models.LibraryItem? @Published public var requestId = UUID().uuidString.lowercased() @Published var debugText: String? @Published var noteText: String = "" @@ -204,7 +204,7 @@ public class ShareExtensionViewModel: ObservableObject { } if let objectID = objectID { - self.linkedItem = self.services.dataService.viewContext.object(with: objectID) as? LinkedItem + self.linkedItem = self.services.dataService.viewContext.object(with: objectID) as? Models.LibraryItem if let title = self.linkedItem?.title { self.title = title } diff --git a/apple/OmnivoreKit/Sources/App/Views/BriefingView.swift b/apple/OmnivoreKit/Sources/App/Views/BriefingView.swift index 6c781c48d..417b0c1ec 100644 --- a/apple/OmnivoreKit/Sources/App/Views/BriefingView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/BriefingView.swift @@ -1,3 +1,5 @@ +// swiftlint:disable line_length + import CoreData import Models import Services @@ -5,155 +7,122 @@ import SwiftUI import Utils import Views -@MainActor final class BriefingViewModel: ObservableObject { - @Published var item: LinkedItem? +let BRIEFING = """ +## Inbox - func loadItem(articleId: String, dataService: DataService) async { - let item = await dataService.viewContext.perform { - LinkedItem.lookup(byID: articleId, inContext: dataService.viewContext) - // dataService.viewContext.object(with: linkedItemObjectID) as? LinkedItem - } +- [ ] Who owns Real Madrid? How 'socios' remain in control of Los Blancos with president Florentino Perez at the helm | Sporting News Singapore | Kyle Bonn - if let item = item { - self.item = item - } - } +## Subscriptions -// func handleArchiveAction(dataService: DataService) { -// guard let objectID = item?.objectID ?? pdfItem?.objectID else { return } -// dataService.archiveLink(objectID: objectID, archived: !isItemArchived) -// showInSnackbar(!isItemArchived ? "Link archived" : "Link moved to Inbox") -// } -// -// func handleDeleteAction(dataService: DataService) { -// guard let objectID = item?.objectID ?? pdfItem?.objectID else { return } -// showInSnackbar("Link removed") -// dataService.removeLink(objectID: objectID) -// } -// -// func updateItemReadStatus(dataService: DataService) { -// guard let itemID = item?.unwrappedID ?? pdfItem?.itemID else { return } -// -// dataService.updateLinkReadingProgress( -// itemID: itemID, -// readingProgress: isItemRead ? 0 : 100, -// anchorIndex: 0 -// ) -// } +- [ ] Astral Codex Ten + - [ ] In Continued Defense Of Effective Altruism + - [ ] God Help Us, Let's Try To Understand AI Monosemanticity + - [ ] Open Thread 304 +- [ ] The Pragmatic Engineer: Holiday Season Gift Ideas for Techies +- [ ] Golang Weekly: 🥶 Like me, Go 1.22 is now frozen +- [ ] Linus Ekenstam at Inside My Head: Enhance/upscale anything - Magnific AI +- [ ] Not Boring: Narrative Tug-of-War +- [ ] Colin Wright: One Sentence News / November 28, 2023 +- [ ] Lenny's Newsletter + - [ ] Lessons from going freemium: a decision that broke our business + - [ ] Billion dollar failures, and billion dollar success | Tom Conrad (Quibi, Pandora, Pets.com, Snap, Zero) +- [ ] Aeon+Psyche Daily + - [ ] When bereavement turns to activism + - [ ] Why training won’t solve implicit bias +- [ ] Etgar Keret from Alphabet Soup: Alternative Fun Facts: Friendship Baby +- [ ] Huddle Up: How Populous Became The Top Sports Architecture Firm In The World +- [ ] Write With AI: How To (Productively) Edit Your Writing With ChatGPT -// private func trackReadEvent() { -// guard let itemID = item?.unwrappedID ?? pdfItem?.itemID else { return } -// guard let slug = item?.unwrappedSlug ?? pdfItem?.slug else { return } -// guard let originalArticleURL = item?.unwrappedPageURLString ?? pdfItem?.originalArticleURL else { return } -// -// EventTracker.track( -// .linkRead( -// linkID: itemID, -// slug: slug, -// originalArticleURL: originalArticleURL -// ) -// ) -// } +## Read + +- [ ] Big brands keep dropping X over antisemitism; $75M loss, report estimates | Ars Technica +- [ ] My Hero [Comic]Geeks are Sexy Technology News +- [ ] 'Project DNA': How Japan's J1 League became a 'flair factory' for Europe's top clubs +- [ ] One Sentence News / November 27, 2023 +- [ ] Only 1 day left to claim your 30% discount on annual membership! + +## Highlights + +- [ ] Big brands keep dropping X over antisemitism; $75M loss, report estimates | Ars Technica + - [ ] Musk [responded](https://twitter.com/elonmusk/status/1728164110137725260) to an X user who [said](https://x.com/JohnnaCrider1/status/1728155588993970484?s=20) that some users were "mad" over lower ad revenue-sharing, saying there was "not much we can do if advertisers boycott or reduce spend on our platform." +- [ ] 'Project DNA': How Japan's J1 League became a 'flair factory' for Europe's top clubs + - [ ] communities are being bound to clubs to create a nascent cultural heritage. + - [ ] The FA and the J1 League brought prefectures, associations, clubs, coaches, universities and schools together to work for a common good. There is a Japanese concept – “ikigai” – which describes the sourcing of meaning or fulfilment from a purpose. The national team was the ikigai. + - [ ] That aim is governed by regulation. It is now mandatory for every club to run their own academy with at least Under-15 and Under-18 teams. There are limits on the number of foreigners on each squad. The first team starting XI must contain at least two homegrown players and one Under-21 player. + - [ ] Clubs are also rewarded for developing and playing academy graduates. The earlier the player moves, the more development they get in a different footballing culture and the more space it allows for another young player to replace them. + - [ ] But in 2016, the Japanese FA created “Project DNA”, an initiative that aimed to adjust and amend existing training methods to produce more rounded footballers. They sent coaches to European clubs, including those in the Premier League. They studied and they cherry-picked and they vowed that insularity should never rule because Japan would never have enough alone. + - [ ] Japan’s most successful recent exports (Mitoma at Brighton, Daichi Kamada at Eintracht Frankfurt, Takefusa Kubo at Real Sociedad) are fun, unpredictable, exciting attacking players. + +## Archived + +- [ ] Why Hunter Biden Asked to Testify Publicly in Impeachment Bid | TIME +""" + +struct BriefingSection { + let title: String + var items = [String]() } -struct BriefingView: View { - @EnvironmentObject var authenticator: Authenticator - @EnvironmentObject var dataService: DataService - @Environment(\.presentationMode) var presentationMode: Binding +@MainActor final class BriefingViewModel: ObservableObject { + @Published var sections = [BriefingSection]() - static let navBarHeight = 50.0 - let articleId: String + func load() async { + var currentSection: BriefingSection? + var sections = [BriefingSection]() + let lines = BRIEFING.components(separatedBy: .newlines) - @StateObject private var viewModel = BriefingViewModel() - @State private var showFontSizePopover = false - @State private var showTitleEdit = false - @State private var navBarVisibilityRatio = 1.0 - @State private var showDeleteConfirmation = false - - var removeLinkToolbarItem: some View { - Button( - action: { print("delete item action") }, - label: { - Image(systemName: "trash") + lines.forEach { line in + if line.starts(with: "## ") { + if let currentSection = currentSection { + sections.append(currentSection) + } + let title = line.replacingOccurrences(of: "## ", with: "") + currentSection = BriefingSection(title: title) + } else if line.isEmpty { + return + } else { + currentSection?.items.append(line) } - ) + } + if let currentSection = currentSection { + sections.append(currentSection) + } + self.sections = sections } +} + +@MainActor +struct BriefingView: View { + @StateObject private var viewModel = BriefingViewModel() var body: some View { - ZStack { // Using ZStack so .task can be used on if/else body - if let item = viewModel.item { - WebReaderContainerView(item: item, pop: {}) - } - } - .task { - await viewModel.loadItem(articleId: articleId, dataService: dataService) - NotificationCenter.default.post(Notification(name: Notification.Name("ReaderSettingsChanged"))) -// static var readerSettingsChangedPublisher: NotificationCenter.Publisher { -// NotificationCenter.default.publisher(for: ReaderSettingsChanged) -// } - } - #if os(iOS) - .navigationBarHidden(true) - #endif - } - - var navBar: some View { - HStack(alignment: .center) { - Spacer() - Button( - action: { showFontSizePopover.toggle() }, - label: { - Image(systemName: "textformat.size") - .font(.appTitleTwo) - } - ) - .padding(.horizontal) - .scaleEffect(navBarVisibilityRatio) - Menu( - content: { - Group { - Button( - action: { showTitleEdit = true }, - label: { Label("Edit Info", systemImage: "info.circle") } - ) -// Button( -// action: { viewModel.handleArchiveAction(dataService: dataService) }, -// label: { -// Label( -// viewModel.isItemArchived ? "Unarchive" : "Archive", -// systemImage: viewModel.isItemArchived ? "tray.and.arrow.down.fill" : "archivebox" -// ) -// } -// ) - Button( - action: { showDeleteConfirmation = true }, - label: { Label("Delete", systemImage: "trash") } - ) + List { + ForEach(viewModel.sections, id: \.title) { section in + Section(section.title) { + ForEach(section.items, id: \.self) { item in + if item.starts(with: "- [ ] ") { + let idx = item.index(item.startIndex, offsetBy: 6) + HStack { + Image(systemName: "square") + Text(item.suffix(from: idx)) + .lineLimit(2) + } + } else if item.starts(with: " - [ ] ") { + let idx = item.index(item.startIndex, offsetBy: 8) + HStack { + Text(" ") + Image(systemName: "square") + Text(item.suffix(from: idx)) + .lineLimit(2) + } + } } - }, - label: { - Image(systemName: "ellipsis") - .padding(.horizontal) - .scaleEffect(navBarVisibilityRatio) } - ) - } - .frame(height: readerViewNavBarHeight * navBarVisibilityRatio) - .opacity(navBarVisibilityRatio) - .background(Color.systemBackground) - .onTapGesture { - showFontSizePopover = false - } - .alert("Are you sure?", isPresented: $showDeleteConfirmation) { -// Button("Remove Link", role: .destructive) { -// viewModel.handleDeleteAction(dataService: dataService) -// } - Button(LocalText.cancelGeneric, role: .cancel, action: {}) - } - .sheet(isPresented: $showTitleEdit) { - if let item = viewModel.item { - LinkedItemMetadataEditView(item: item) } } + .listStyle(.plain) + .task { + await viewModel.load() + } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Highlights/NotebookViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Highlights/NotebookViewModel.swift index 759ee5d83..33db22c4f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Highlights/NotebookViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Highlights/NotebookViewModel.swift @@ -25,7 +25,7 @@ struct NoteItemParams: Identifiable { @Published var highlightItems = [HighlightListItemParams]() func load(itemObjectID: NSManagedObjectID, dataService: DataService) { - if let linkedItem = dataService.viewContext.object(with: itemObjectID) as? LinkedItem { + if let linkedItem = dataService.viewContext.object(with: itemObjectID) as? Models.LibraryItem { loadHighlights(item: linkedItem) } } @@ -53,7 +53,7 @@ struct NoteItemParams: Identifiable { let highlightId = UUID().uuidString.lowercased() let shortId = NanoID.generate(alphabet: NanoID.Alphabet.urlSafe.rawValue, size: 8) - if let linkedItem = dataService.viewContext.object(with: itemObjectID) as? LinkedItem { + if let linkedItem = dataService.viewContext.object(with: itemObjectID) as? Models.LibraryItem { noteItem = NoteItemParams(highlightID: highlightId, annotation: annotation) let highlight = dataService.createNote(shortId: shortId, highlightID: highlightId, @@ -105,7 +105,7 @@ struct NoteItemParams: Identifiable { highlightItems.map { highlightAsMarkdown(item: $0) }.lazy.joined(separator: "\n\n") } - private func loadHighlights(item: LinkedItem) { + private func loadHighlights(item: Models.LibraryItem) { let unsortedHighlights = item.highlights.asArray(of: Highlight.self) .filter { $0.type == "HIGHLIGHT" && $0.serverSyncStatus != ServerSyncStatus.needsDeletion.rawValue } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index ad02df3d8..8714d9b52 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -1,13 +1,14 @@ import Models import Services import SwiftUI +import Transmission import Views struct MacFeedCardNavigationLink: View { @EnvironmentObject var dataService: DataService @EnvironmentObject var audioController: AudioController - let item: LinkedItem + let item: Models.LibraryItem @ObservedObject var viewModel: HomeFeedViewModel @@ -31,19 +32,50 @@ struct FeedCardNavigationLink: View { @EnvironmentObject var dataService: DataService @EnvironmentObject var audioController: AudioController - let item: LinkedItem + let item: Models.LibraryItem let isInMultiSelectMode: Bool @ObservedObject var viewModel: HomeFeedViewModel var body: some View { ZStack { LibraryItemCard(item: item, viewer: dataService.currentViewer) - NavigationLink(destination: LinkItemDetailView( - linkedItemObjectID: item.objectID, - isPDF: item.isPDF - ), label: { - EmptyView() - }).opacity(0) +// PresentationLink({ +// <#code#> +// } label: { +// EmptyView() +// }).opacity(0) + +// public init( +// edge: Edge = .bottom, +// prefersScaleEffect: Bool = true, +// preferredCornerRadius: CGFloat? = nil, +// isInteractive: Bool = true, +// options: Options = .init(modalPresentationCapturesStatusBarAppearance: true) +// ) { +// self.edge = edge +// self.prefersScaleEffect = prefersScaleEffect +// self.preferredCornerRadius = preferredCornerRadius +// self.isInteractive = isInteractive +// self.options = options +// } +// + PresentationLink( + transition: PresentationLinkTransition.slide( + options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing, + options: + PresentationLinkTransition.Options( + modalPresentationCapturesStatusBarAppearance: true + ))), + destination: { + LinkItemDetailView( + linkedItemObjectID: item.objectID, + isPDF: item.isPDF + ) + .background(ThemeManager.currentBgColor) + }, label: { + EmptyView() + } + ) } .onAppear { Task { await viewModel.itemAppeared(item: item, dataService: dataService) } @@ -57,7 +89,7 @@ struct GridCardNavigationLink: View { @State private var scale = 1.0 - let item: LinkedItem + let item: Models.LibraryItem let actionHandler: (GridCardAction) -> Void @Binding var isContextMenuOpen: Bool @@ -65,12 +97,28 @@ struct GridCardNavigationLink: View { @ObservedObject var viewModel: HomeFeedViewModel var body: some View { - NavigationLink(destination: LinkItemDetailView( - linkedItemObjectID: item.objectID, - isPDF: item.isPDF - )) { - GridCard(item: item, isContextMenuOpen: $isContextMenuOpen, actionHandler: actionHandler) - } + PresentationLink( + transition: PresentationLinkTransition.slide( + options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing, + options: + PresentationLinkTransition.Options( + modalPresentationCapturesStatusBarAppearance: true + ))), + destination: { + LinkItemDetailView( + linkedItemObjectID: item.objectID, + isPDF: item.isPDF + ) + }, label: { + GridCard(item: item, isContextMenuOpen: $isContextMenuOpen, actionHandler: actionHandler) + } + ) +// NavigationLink(destination: LinkItemDetailView( +// linkedItemObjectID: item.objectID, +// isPDF: item.isPDF +// )) { +// +// } .onAppear { Task { await viewModel.itemAppeared(item: item, dataService: dataService) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FollowingFetcher.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FollowingFetcher.swift new file mode 100644 index 000000000..d7ad556da --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FollowingFetcher.swift @@ -0,0 +1,257 @@ +//// +//// FollowingFetcher.swift +//// +//// +//// Created by Jackson Harper on 11/16/23. +//// +// +// import Foundation +// +// import CoreData +// import Models +// import Services +// import SwiftUI +// import Utils +// import Views +// +// @MainActor final class FollowingFetcher: NSObject, ObservableObject, LibraryItemFetcher { +// var folder = "following" +// +// @Published var items = [Models.LibraryItem]() +// var itemsPublisher: Published<[Models.LibraryItem]>.Publisher { $items } +// +// private var fetchedResultsController: NSFetchedResultsController? +// +// var cursor: String? +// +// // These are used to make sure we handle search result +// // responses in the right order +// var searchIdx = 0 +// var receivedIdx = 0 +// +// var syncCursor: String? +// +// func setItems(_: NSManagedObjectContext, _ items: [Models.LibraryItem]) { +// self.items = items +// } +// +// func loadCurrentViewer(dataService: DataService) async { +// // Cache the viewer +// if dataService.currentViewer == nil { +// _ = try? await dataService.fetchViewer() +// } +// } +// +// func loadLabels(dataService: DataService) async { +// let fetchRequest: NSFetchRequest = LinkedItemLabel.fetchRequest() +// fetchRequest.fetchLimit = 1 +// +// if (try? dataService.viewContext.count(for: fetchRequest)) == 0 { +// _ = try? await dataService.labels() +// } +// } +// +// func syncItems(dataService: DataService) async { +// let syncStart = Date.now +// let lastSyncDate = dataService.lastItemSyncTime +// +// try? await dataService.syncOfflineItemsWithServerIfNeeded() +// +// let syncResult = try? await dataService.syncLinkedItems(since: lastSyncDate, +// cursor: nil) +// +// syncCursor = syncResult?.cursor +// if let syncResult = syncResult, syncResult.hasMore { +// dataService.syncLinkedItemsInBackground(since: lastSyncDate) { +// // do nothing +// } +// } else { +// dataService.lastItemSyncTime = syncStart +// } +// +// // If possible start prefetching new pages in the background +// if +// let itemIDs = syncResult?.updatedItemIDs, +// let username = dataService.currentViewer?.username, +// !itemIDs.isEmpty +// { +// Task.detached(priority: .background) { +// await dataService.prefetchPages(itemIDs: itemIDs, username: username) +// } +// } +// } +// +// func loadSearchQuery(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { +// let thisSearchIdx = searchIdx +// searchIdx += 1 +// +// if thisSearchIdx > 0, thisSearchIdx <= receivedIdx { +// return +// } +// +// let queryResult = try? await dataService.loadLinkedItems( +// limit: 10, +// searchQuery: searchQuery(filterState), +// cursor: isRefresh ? nil : cursor +// ) +// +// let filter = LinkedItemFilter(rawValue: filterState.appliedFilter) +// +// if let queryResult = queryResult { +// let newItems: [Models.LibraryItem] = { +// var itemObjects = [Models.LibraryItem]() +// dataService.viewContext.performAndWait { +// itemObjects = queryResult.itemIDs.compactMap { dataService.viewContext.object(with: $0) as? Models.LibraryItem } +// } +// return itemObjects +// }() +// +// print("RESULTS OF SEARCH: ", newItems) +// +// if filterState.searchTerm.replacingOccurrences(of: " ", with: "").isEmpty, filter?.allowLocalFetch ?? false { +// updateFetchController(dataService: dataService, filterState: filterState) +// } else { +// // Don't use FRC for searching. Use server results directly. +// if fetchedResultsController != nil { +// fetchedResultsController = nil +// setItems(dataService.viewContext, []) +// } +// setItems(dataService.viewContext, isRefresh ? newItems : items + newItems) +// } +// +// receivedIdx = thisSearchIdx +// cursor = queryResult.cursor +//// if let username = dataService.currentViewer?.username { +//// await dataService.prefetchPages(itemIDs: newItems.map(\.unwrappedID), username: username) +//// } +// } else { +// updateFetchController(dataService: dataService, filterState: filterState) +// } +// } +// +// func loadItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { +// await withTaskGroup(of: Void.self) { group in +// group.addTask { await self.loadCurrentViewer(dataService: dataService) } +// group.addTask { await self.loadLabels(dataService: dataService) } +// group.addTask { await self.syncItems(dataService: dataService) } +// group.addTask { await self.updateFetchController(dataService: dataService, filterState: filterState) } +// await group.waitForAll() +// } +// +// let filter = LinkedItemFilter(rawValue: filterState.appliedFilter) +// let shouldSearch = items.count < 1 || isRefresh && filter != LinkedItemFilter.downloaded +// if shouldSearch { +// await loadSearchQuery(dataService: dataService, filterState: filterState, isRefresh: isRefresh) +// } else { +// updateFetchController(dataService: dataService, filterState: filterState) +// } +// } +// +// func loadMoreItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { +// let filter = LinkedItemFilter(rawValue: filterState.appliedFilter) +// if filter != LinkedItemFilter.downloaded { +// await loadSearchQuery(dataService: dataService, filterState: filterState, isRefresh: isRefresh) +// } +// } +// +// private func fetchRequest(_ filterState: FetcherFilterState) -> NSFetchRequest { +// let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() +// +// var subPredicates = [NSPredicate]() +// +// let folderPredicate = NSPredicate( +// format: "%K == %@", #keyPath(Models.LibraryItem.folder), folder +// ) +// subPredicates.append(folderPredicate) +// +// if !filterState.selectedLabels.isEmpty { +// var labelSubPredicates = [NSPredicate]() +// +// for label in filterState.selectedLabels { +// labelSubPredicates.append( +// NSPredicate(format: "SUBQUERY(labels, $label, $label.id == \"\(label.unwrappedID)\").@count > 0") +// ) +// } +// +// subPredicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: labelSubPredicates)) +// } +// +// if !filterState.negatedLabels.isEmpty { +// var labelSubPredicates = [NSPredicate]() +// +// for label in filterState.negatedLabels { +// labelSubPredicates.append( +// NSPredicate(format: "SUBQUERY(labels, $label, $label.id == \"\(label.unwrappedID)\").@count == 0") +// ) +// } +// +// subPredicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: labelSubPredicates)) +// } +// +// fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: subPredicates) +// fetchRequest.sortDescriptors = (LinkedItemSort(rawValue: filterState.appliedSort) ?? .newest).sortDescriptors +// +// return fetchRequest +// } +// +// private func updateFetchController(dataService: DataService, filterState: FetcherFilterState) { +// fetchedResultsController = NSFetchedResultsController( +// fetchRequest: fetchRequest(filterState), +// managedObjectContext: dataService.viewContext, +// sectionNameKeyPath: nil, +// cacheName: nil +// ) +// +// guard let fetchedResultsController = fetchedResultsController else { +// return +// } +// +// fetchedResultsController.delegate = self +// try? fetchedResultsController.performFetch() +// setItems(dataService.viewContext, fetchedResultsController.fetchedObjects ?? []) +// } +// +// private func searchQuery(_ filterState: FetcherFilterState) -> String { +// let sort = LinkedItemSort(rawValue: filterState.appliedSort) ?? .newest +//// var query = sort.queryString +// +// var query = "in:following \(sort.queryString)" +//// if !queryContainsFilter(filterState), let filter = LinkedItemFilter(rawValue: filterState.appliedFilter) { +//// query = "\(filter.queryString) \(sort.queryString)" +//// } +// +// if !filterState.searchTerm.isEmpty { +// query.append(" \(filterState.searchTerm)") +// } +// +// if !filterState.selectedLabels.isEmpty { +// query.append(" label:") +// query.append(filterState.selectedLabels.compactMap { label in +// if let name = label.name { +// return "\"\(name)\"" +// } +// return nil +// }.joined(separator: ",")) +// } +// +// if !filterState.negatedLabels.isEmpty { +// query.append(" !label:") +// query.append(filterState.negatedLabels.compactMap { label in +// if let name = label.name { +// return "\"\(name)\"" +// } +// return nil +// }.joined(separator: ",")) +// } +// +// print("QUERY: `\(query)`") +// +// return query +// } +// } +// +// extension FollowingFetcher: NSFetchedResultsControllerDelegate { +// func controllerDidChangeContent(_ controller: NSFetchedResultsController) { +// setItems(controller.managedObjectContext, controller.fetchedObjects as? [Models.LibraryItem] ?? []) +// } +// } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/InboxFetcher.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/InboxFetcher.swift new file mode 100644 index 000000000..5394b2a8d --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/InboxFetcher.swift @@ -0,0 +1,276 @@ +// +// InboxFetcher.swift +// +// +// Created by Jackson Harper on 11/16/23. +// + +import Foundation + +import CoreData +import Models +import Services +import SwiftUI +import Utils +import Views + +@MainActor final class InboxFetcher: NSObject, ObservableObject, LibraryItemFetcher { + var folder = "inbox" + + @Published var items = [Models.LibraryItem]() + var itemsPublisher: Published<[Models.LibraryItem]>.Publisher { $items } + + private var fetchedResultsController: NSFetchedResultsController? + + var cursor: String? + + // These are used to make sure we handle search result + // responses in the right order + var searchIdx = 0 + var receivedIdx = 0 + + var syncCursor: String? + + func setItems(_: NSManagedObjectContext, _ items: [Models.LibraryItem]) { + self.items = items + } + + func loadCurrentViewer(dataService: DataService) async { + // Cache the viewer + if dataService.currentViewer == nil { + _ = try? await dataService.fetchViewer() + } + } + + func loadLabels(dataService: DataService) async { + let fetchRequest: NSFetchRequest = LinkedItemLabel.fetchRequest() + fetchRequest.fetchLimit = 1 + + if (try? dataService.viewContext.count(for: fetchRequest)) == 0 { + _ = try? await dataService.labels() + } + } + + func syncItems(dataService: DataService) async { + let syncStart = Date.now + let lastSyncDate = dataService.lastItemSyncTime + + try? await dataService.syncOfflineItemsWithServerIfNeeded() + + let syncResult = try? await dataService.syncLinkedItems(since: lastSyncDate, + cursor: nil) + + syncCursor = syncResult?.cursor + if let syncResult = syncResult, syncResult.hasMore { + dataService.syncLinkedItemsInBackground(since: lastSyncDate) { + // do nothing + } + } else { + dataService.lastItemSyncTime = syncStart + } + + // If possible start prefetching new pages in the background + if + let itemIDs = syncResult?.updatedItemIDs, + let username = dataService.currentViewer?.username, + !itemIDs.isEmpty + { + Task.detached(priority: .background) { + await dataService.prefetchPages(itemIDs: itemIDs, username: username) + } + } + } + + func loadSearchQuery(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { + let thisSearchIdx = searchIdx + searchIdx += 1 + + if thisSearchIdx > 0, thisSearchIdx <= receivedIdx { + return + } + + let queryResult = try? await dataService.loadLinkedItems( + limit: 10, + searchQuery: searchQuery(filterState), + cursor: isRefresh ? nil : cursor + ) + + if let appliedFilter = filterState.appliedFilter, let queryResult = queryResult { + let newItems: [Models.LibraryItem] = { + var itemObjects = [Models.LibraryItem]() + dataService.viewContext.performAndWait { + itemObjects = queryResult.itemIDs.compactMap { dataService.viewContext.object(with: $0) as? Models.LibraryItem } + } + return itemObjects + }() + + if filterState.searchTerm.replacingOccurrences(of: " ", with: "").isEmpty, appliedFilter.allowLocalFetch { + updateFetchController(dataService: dataService, filterState: filterState) + } else { + // Don't use FRC for searching. Use server results directly. + if fetchedResultsController != nil { + fetchedResultsController = nil + setItems(dataService.viewContext, []) + } + setItems(dataService.viewContext, isRefresh ? newItems : items + newItems) + } + + receivedIdx = thisSearchIdx + cursor = queryResult.cursor + if let username = dataService.currentViewer?.username { + await dataService.prefetchPages(itemIDs: newItems.map(\.unwrappedID), username: username) + } + } else { + updateFetchController(dataService: dataService, filterState: filterState) + } + } + + func loadItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { + await withTaskGroup(of: Void.self) { group in + group.addTask { await self.loadCurrentViewer(dataService: dataService) } + group.addTask { await self.loadLabels(dataService: dataService) } + group.addTask { await self.syncItems(dataService: dataService) } + group.addTask { await self.updateFetchController(dataService: dataService, filterState: filterState) } + await group.waitForAll() + } + + if let appliedFilter = filterState.appliedFilter { + let shouldRemoteSearch = items.count < 1 || isRefresh && appliedFilter.shouldRemoteSearch + if shouldRemoteSearch { + await loadSearchQuery(dataService: dataService, filterState: filterState, isRefresh: isRefresh) + } else { + updateFetchController(dataService: dataService, filterState: filterState) + } + } + } + + func loadMoreItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { + if let appliedFilter = filterState.appliedFilter, appliedFilter.shouldRemoteSearch { + await loadSearchQuery(dataService: dataService, filterState: filterState, isRefresh: isRefresh) + } + } + + func loadFeatureItems(context: NSManagedObjectContext, predicate: NSPredicate, sort: NSSortDescriptor) async -> [Models.LibraryItem] { + let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() + fetchRequest.fetchLimit = 25 + fetchRequest.predicate = predicate + fetchRequest.sortDescriptors = [sort] + + return (try? context.fetch(fetchRequest)) ?? [] + } + + private func fetchRequest(_ filterState: FetcherFilterState) -> NSFetchRequest { + let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() + + var subPredicates = [NSPredicate]() + + let folderPredicate = NSPredicate( + format: "%K == %@", #keyPath(Models.LibraryItem.folder), folder + ) + subPredicates.append(folderPredicate) + + if let predicate = filterState.appliedFilter?.predicate { + subPredicates.append(predicate) + } + + if !filterState.selectedLabels.isEmpty { + var labelSubPredicates = [NSPredicate]() + + for label in filterState.selectedLabels { + labelSubPredicates.append( + NSPredicate(format: "SUBQUERY(labels, $label, $label.id == \"\(label.unwrappedID)\").@count > 0") + ) + } + + subPredicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: labelSubPredicates)) + } + + if !filterState.negatedLabels.isEmpty { + var labelSubPredicates = [NSPredicate]() + + for label in filterState.negatedLabels { + labelSubPredicates.append( + NSPredicate(format: "SUBQUERY(labels, $label, $label.id == \"\(label.unwrappedID)\").@count == 0") + ) + } + + subPredicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: labelSubPredicates)) + } + + fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: subPredicates) + fetchRequest.sortDescriptors = (LinkedItemSort(rawValue: filterState.appliedSort) ?? .newest).sortDescriptors + + return fetchRequest + } + + private func updateFetchController(dataService: DataService, filterState: FetcherFilterState) { + fetchedResultsController = NSFetchedResultsController( + fetchRequest: fetchRequest(filterState), + managedObjectContext: dataService.viewContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + + guard let fetchedResultsController = fetchedResultsController else { + return + } + + fetchedResultsController.delegate = self + try? fetchedResultsController.performFetch() + setItems(dataService.viewContext, fetchedResultsController.fetchedObjects ?? []) + } + + private func queryContainsFilter(_ filterState: FetcherFilterState) -> Bool { + if filterState.searchTerm.contains("in:inbox") || + filterState.searchTerm.contains("in:all") || + filterState.searchTerm.contains("in:archive") + { + return true + } + + return false + } + + private func searchQuery(_ filterState: FetcherFilterState) -> String { + let sort = LinkedItemSort(rawValue: filterState.appliedSort) ?? .newest + var query = sort.queryString + + if !queryContainsFilter(filterState), let queryString = filterState.appliedFilter?.filter { + query = "\(queryString) \(sort.queryString)" + } + + if !filterState.searchTerm.isEmpty { + query.append(" \(filterState.searchTerm)") + } + + if !filterState.selectedLabels.isEmpty { + query.append(" label:") + query.append(filterState.selectedLabels.compactMap { label in + if let name = label.name { + return "\"\(name)\"" + } + return nil + }.joined(separator: ",")) + } + + if !filterState.negatedLabels.isEmpty { + query.append(" !label:") + query.append(filterState.negatedLabels.compactMap { label in + if let name = label.name { + return "\"\(name)\"" + } + return nil + }.joined(separator: ",")) + } + + print("QUERY: `\(query)`") + + return query + } +} + +extension InboxFetcher: NSFetchedResultsControllerDelegate { + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + setItems(controller.managedObjectContext, controller.fetchedObjects as? [Models.LibraryItem] ?? []) + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift index 78a8c7409..c107f4d10 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift @@ -14,7 +14,7 @@ struct LibraryFeatureCardNavigationLink: View { @EnvironmentObject var dataService: DataService @EnvironmentObject var audioController: AudioController - let item: LinkedItem + let item: Models.LibraryItem @ObservedObject var viewModel: HomeFeedViewModel @State var showFeatureActions = false diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/FilterSelectorView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/FilterSelectorView.swift new file mode 100644 index 000000000..7c5f4b75a --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Home/FilterSelectorView.swift @@ -0,0 +1,121 @@ + +// import Introspect +// import Models +// import Services +// import SwiftUI +// import Views +// +// @MainActor final class FilterSelectorViewModel: NSObject, ObservableObject { +// @Published var isLoading = false +// @Published var errorMessage: String = "" +// @Published var showErrorMessage: Bool = false +// +// func error(_ msg: String) { +// errorMessage = msg +// showErrorMessage = true +// isLoading = false +// } +// } +// +// struct FilterSelectorView: View { +// @ObservedObject var viewModel: HomeFeedViewModel +// @ObservedObject var filterViewModel = FilterByLabelsViewModel() +// @EnvironmentObject var dataService: DataService +// @Environment(\.dismiss) private var dismiss +// +// @State var showLabelsSheet = false +// +// init(viewModel: HomeFeedViewModel) { +// self.viewModel = viewModel +// } +// +// var body: some View { +// Group { +// #if os(iOS) +// List { +// innerBody +// } +// .listStyle(.grouped) +// #elseif os(macOS) +// List { +// innerBody +// } +// .listStyle(.plain) +// #endif +// } +// #if os(iOS) +// .navigationBarTitle("Library") +// .navigationBarTitleDisplayMode(.inline) +// .navigationBarItems(trailing: doneButton) +// #endif +// } +// +// private var innerBody: some View { +// Group { +// Section { +// ForEach(LinkedItemFilter.allCases, id: \.self) { filter in +// HStack { +// Text(filter.displayName) +// .foregroundColor(filterState.appliedFilter == filter.rawValue ? Color.blue : Color.appTextDefault) +// Spacer() +// if filterState.appliedFilter == filter.rawValue { +// Image(systemName: "checkmark") +// .foregroundColor(Color.blue) +// } +// } +// .contentShape(Rectangle()) +// .onTapGesture { +// filterState.appliedFilter = filter.rawValue +// } +// } +// } +// +// Section("Labels") { +// Button( +// action: { +// showLabelsSheet = true +// }, +// label: { +// HStack { +// Text("Select Labels (\(filterState.selectedLabels.count))") +// Spacer() +// Image(systemName: "chevron.right") +// } +// } +// ) +// } +// } +// .sheet(isPresented: $showLabelsSheet) { +// FilterByLabelsView( +// initiallySelected: filterState.selectedLabels, +// initiallyNegated: filterState.negatedLabels +// ) { +// self.filterState.selectedLabels = $0 +// self.filterState.negatedLabels = $1 +// } +// } +// .task { +// await filterViewModel.loadLabels( +// dataService: dataService, +// initiallySelectedLabels: filterState.selectedLabels, +// initiallyNegatedLabels: filterState.negatedLabels +// ) +// } +// } +// +// func isNegated(_ label: LinkedItemLabel) -> Bool { +// filterViewModel.negatedLabels.contains(where: { $0.id == label.id }) +// } +// +// func isSelected(_ label: LinkedItemLabel) -> Bool { +// filterViewModel.selectedLabels.contains(where: { $0.id == label.id }) +// } +// +// var doneButton: some View { +// Button( +// action: { dismiss() }, +// label: { Text("Done") } +// ) +// .disabled(viewModel.isLoading) +// } +// } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 0bfae8717..86f294e6f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -43,19 +43,25 @@ struct AnimatingCellHeight: AnimatableModifier { @ObservedObject var viewModel: HomeFeedViewModel @State private var selection = Set() + @ObservedObject var filterState = FetcherFilterState( + appliedFilterName: UserDefaults.standard.string(forKey: UserDefaultKey.lastSelectedLinkedItemFilter.rawValue) ?? + LinkedItemFilter.inbox.rawValue + ) func loadItems(isRefresh: Bool) { - Task { await viewModel.loadItems(dataService: dataService, isRefresh: isRefresh) } + Task { await viewModel.loadItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh) } } var showFeatureCards: Bool { viewModel.listConfig.hasFeatureCards && !viewModel.hideFeatureSection && - viewModel.items.count > 0 && - viewModel.searchTerm.isEmpty && - viewModel.selectedLabels.isEmpty && - viewModel.negatedLabels.isEmpty && - viewModel.appliedFilterName == "inbox" + viewModel.fetcher.items.count > 0 && + filterState.searchTerm.isEmpty && + filterState.selectedLabels.isEmpty && + filterState.negatedLabels.isEmpty + // MERGE TODO + // && +// viewModel.appliedFilterName == "inbox" } var body: some View { @@ -66,30 +72,34 @@ struct AnimatingCellHeight: AnimatableModifier { isEditMode: $isEditMode, selection: $selection, viewModel: viewModel, + filterState: filterState, showFeatureCards: showFeatureCards ) .refreshable { loadItems(isRefresh: true) } - .onChange(of: viewModel.searchTerm) { _ in + .onChange(of: filterState.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without loadItems(isRefresh: true) } - .onChange(of: viewModel.selectedLabels) { _ in + .onChange(of: filterState.selectedLabels) { _ in loadItems(isRefresh: true) } - .onChange(of: viewModel.negatedLabels) { _ in + .onChange(of: filterState.negatedLabels) { _ in loadItems(isRefresh: true) } - .onChange(of: viewModel.appliedFilter) { _ in + .onChange(of: filterState.appliedFilter) { _ in loadItems(isRefresh: true) } - .onChange(of: viewModel.appliedSort) { _ in + .onChange(of: filterState.appliedSort) { _ in loadItems(isRefresh: true) } - .sheet(item: $viewModel.itemUnderLabelEdit) { item in - ApplyLabelsView(mode: .item(item), onSave: nil) + .sheet(item: $viewModel.itemUnderLabelEdit) { _ in + NavigationView { + BriefingView() + } + // ApplyLabelsView(mode: .item(item), onSave: nil) } .sheet(item: $viewModel.itemUnderTitleEdit) { item in LinkedItemMetadataEditView(item: item) @@ -115,7 +125,7 @@ struct AnimatingCellHeight: AnimatableModifier { .onReceive(NotificationCenter.default.publisher(for: Notification.Name("PushJSONArticle"))) { notification in guard let jsonArticle = notification.userInfo?["article"] as? JSONArticle else { return } guard let objectID = dataService.persist(jsonArticle: jsonArticle) else { return } - guard let linkedItem = dataService.viewContext.object(with: objectID) as? LinkedItem else { return } + guard let linkedItem = dataService.viewContext.object(with: objectID) as? Models.LibraryItem else { return } viewModel.pushFeedItem(item: linkedItem) viewModel.selectedItem = linkedItem viewModel.linkIsActive = true @@ -125,10 +135,10 @@ struct AnimatingCellHeight: AnimatableModifier { if let deepLink = DeepLink.make(from: url) { switch deepLink { case let .search(query): - viewModel.searchTerm = query + filterState.searchTerm = query case let .savedSearch(named): if let filter = viewModel.findFilter(dataService, named: named) { - viewModel.appliedFilter = filter + filterState.appliedFilter = filter } case let .webAppLinkRequest(requestID): DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { @@ -153,7 +163,7 @@ struct AnimatingCellHeight: AnimatableModifier { } } .task { - if viewModel.items.isEmpty { + if viewModel.fetcher.items.isEmpty { loadItems(isRefresh: false) } } @@ -165,10 +175,9 @@ struct AnimatingCellHeight: AnimatableModifier { ToolbarItem(placement: .barLeading) { VStack(alignment: .leading) { let showDate = isListScrolled && !listTitle.isEmpty - if let title = viewModel.appliedFilter?.name { + if let title = filterState.appliedFilter?.name { Text(title) .font(Font.system(size: showDate ? 10 : 18, weight: .semibold)) - if showDate, prefersListLayout, isListScrolled || !showFeatureCards { Text(listTitle) .font(Font.system(size: 15, weight: .regular)) @@ -263,6 +272,7 @@ struct AnimatingCellHeight: AnimatableModifier { @Binding var isEditMode: EditMode @Binding var selection: Set @ObservedObject var viewModel: HomeFeedViewModel + @ObservedObject var filterState: FetcherFilterState let showFeatureCards: Bool @@ -292,18 +302,23 @@ struct AnimatingCellHeight: AnimatableModifier { isEditMode: $isEditMode, selection: $selection, viewModel: viewModel, + filterState: filterState, showFeatureCards: showFeatureCards ) } else { - HomeFeedGridView(viewModel: viewModel, isListScrolled: $isListScrolled) + HomeFeedGridView( + viewModel: viewModel, + filterState: filterState, + isListScrolled: $isListScrolled + ) } }.sheet(isPresented: $viewModel.showLabelsSheet) { FilterByLabelsView( - initiallySelected: viewModel.selectedLabels, - initiallyNegated: viewModel.negatedLabels + initiallySelected: filterState.selectedLabels, + initiallyNegated: filterState.negatedLabels ) { - self.viewModel.selectedLabels = $0 - self.viewModel.negatedLabels = $1 + self.filterState.selectedLabels = $0 + self.filterState.negatedLabels = $1 } } .popup(isPresented: $viewModel.showSnackbar) { @@ -344,6 +359,7 @@ struct AnimatingCellHeight: AnimatableModifier { @Binding var selection: Set @ObservedObject var viewModel: HomeFeedViewModel + @ObservedObject var filterState: FetcherFilterState let showFeatureCards: Bool @@ -351,20 +367,20 @@ struct AnimatingCellHeight: AnimatableModifier { GeometryReader { reader in ScrollView(.horizontal, showsIndicators: false) { HStack { - if viewModel.searchTerm.count > 0 { - TextChipButton.makeSearchFilterButton(title: viewModel.searchTerm) { - viewModel.searchTerm = "" + if filterState.searchTerm.count > 0 { + TextChipButton.makeSearchFilterButton(title: filterState.searchTerm) { + filterState.searchTerm = "" }.frame(maxWidth: reader.size.width * 0.66) } else { Menu( content: { ForEach(viewModel.filters) { filter in - Button(filter.name, action: { viewModel.appliedFilter = filter }) + Button(filter.name, action: { filterState.appliedFilter = filter }) } }, label: { TextChipButton.makeMenuButton( - title: viewModel.appliedFilter?.name ?? "-", + title: filterState.appliedFilter?.name ?? "-", color: .systemGray6 ) } @@ -373,25 +389,25 @@ struct AnimatingCellHeight: AnimatableModifier { Menu( content: { ForEach(LinkedItemSort.allCases, id: \.self) { sort in - Button(sort.displayName, action: { viewModel.appliedSort = sort.rawValue }) + Button(sort.displayName, action: { filterState.appliedSort = sort.rawValue }) } }, label: { TextChipButton.makeMenuButton( - title: LinkedItemSort(rawValue: viewModel.appliedSort)?.displayName ?? "Sort", + title: LinkedItemSort(rawValue: filterState.appliedSort)?.displayName ?? "Sort", color: .systemGray6 ) } ) TextChipButton.makeAddLabelButton(color: .systemGray6, onTap: { viewModel.showLabelsSheet = true }) - ForEach(viewModel.selectedLabels, id: \.self) { label in + ForEach(filterState.selectedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: false) { - viewModel.selectedLabels.removeAll { $0.id == label.id } + filterState.selectedLabels.removeAll { $0.id == label.id } } } - ForEach(viewModel.negatedLabels, id: \.self) { label in + ForEach(filterState.negatedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: true) { - viewModel.negatedLabels.removeAll { $0.id == label.id } + filterState.negatedLabels.removeAll { $0.id == label.id } } } Spacer() @@ -403,7 +419,7 @@ struct AnimatingCellHeight: AnimatableModifier { .dynamicTypeSize(.small ... .accessibility1) } - func menuItems(for item: LinkedItem) -> some View { + func menuItems(for item: Models.LibraryItem) -> some View { libraryItemMenu(dataService: dataService, viewModel: viewModel, item: item) } @@ -509,9 +525,9 @@ struct AnimatingCellHeight: AnimatableModifier { static func reduce(value _: inout CGPoint, nextValue _: () -> CGPoint) {} } - @State var topItem: LinkedItem? + @State var topItem: Models.LibraryItem? - func setTopItem(_ item: LinkedItem) { + func setTopItem(_ item: Models.LibraryItem) { if let date = item.savedAt, let daysAgo = Calendar.current.dateComponents([.day], from: date, to: Date()).day { if daysAgo < 1 { let formatter = DateFormatter() @@ -561,7 +577,7 @@ struct AnimatingCellHeight: AnimatableModifier { .listRowSeparator(.hidden, edges: .all) .listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 0, trailing: horizontalInset)) - if let appliedFilter = viewModel.appliedFilter, + if let appliedFilter = filterState.appliedFilter, networkMonitor.status == .disconnected, !appliedFilter.allowLocalFetch { @@ -593,7 +609,7 @@ struct AnimatingCellHeight: AnimatableModifier { } } - ForEach(Array(viewModel.items.enumerated()), id: \.1.unwrappedID) { _, item in + ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { _, item in FeedCardNavigationLink( item: item, isInMultiSelectMode: viewModel.isInMultiSelectMode, @@ -642,41 +658,7 @@ struct AnimatingCellHeight: AnimatableModifier { } } - func dateSummaryCard(_: Date) -> some View { - VStack(alignment: .center, spacing: 15) { - Text("3 articles saved today") - .frame(maxWidth: .infinity, alignment: .center) - .font(.body) - HStack { - Spacer() - HStack(spacing: 0) { - Button(action: {}, label: { - Text("Archive all") - .font(Font.system(size: 14)) - .padding(.horizontal, 10) - }) - .frame(height: 30) - .background(Color.blue) - Button(action: {}, label: { - Image(systemName: "chevron.down") - .resizable() - .scaledToFit() - .frame(width: 10, height: 10) - .padding(.leading, 7.5) - .padding(.trailing, 7.5) - .foregroundColor(Color.white) - }) - .frame(height: 30) - .background(Color(hex: "345BB8")) - } - .cornerRadius(2.5) - Spacer() - } - } - .padding(15) - } - - func swipeActionButton(action: SwipeAction, item: LinkedItem) -> AnyView { + func swipeActionButton(action: SwipeAction, item: Models.LibraryItem) -> AnyView { switch action { case .pin: let isPinned = item.labels?.allObjects.first { ($0 as? LinkedItemLabel)?.name == "Pinned" } != nil @@ -717,7 +699,8 @@ struct AnimatingCellHeight: AnimatableModifier { // viewModel.addLabel(dataService: dataService, item: item, label: "Inbox", color) }, label: { - Label("Move to Inbox", systemImage: "tray.fill") + Label(title: { Text("Move to Library") }, + icon: { Image.tabLibrary }) } ).tint(Color(hex: "#0A84FF"))) } @@ -731,9 +714,11 @@ struct AnimatingCellHeight: AnimatableModifier { @State var isContextMenuOpen = false @ObservedObject var viewModel: HomeFeedViewModel + @ObservedObject var filterState: FetcherFilterState + @Binding var isListScrolled: Bool - func contextMenuActionHandler(item: LinkedItem, action: GridCardAction) { + func contextMenuActionHandler(item: Models.LibraryItem, action: GridCardAction) { switch action { case .viewHighlights: viewModel.itemForHighlightsView = item @@ -749,27 +734,27 @@ struct AnimatingCellHeight: AnimatableModifier { } func loadItems(isRefresh: Bool) { - Task { await viewModel.loadItems(dataService: dataService, isRefresh: isRefresh) } + Task { await viewModel.loadItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh) } } var filtersHeader: some View { GeometryReader { reader in ScrollView(.horizontal, showsIndicators: false) { HStack { - if viewModel.searchTerm.count > 0 { - TextChipButton.makeSearchFilterButton(title: viewModel.searchTerm) { - viewModel.searchTerm = "" + if filterState.searchTerm.count > 0 { + TextChipButton.makeSearchFilterButton(title: filterState.searchTerm) { + filterState.searchTerm = "" }.frame(maxWidth: reader.size.width * 0.66) } else { Menu( content: { ForEach(viewModel.filters, id: \.self) { filter in - Button(filter.name, action: { viewModel.appliedFilter = filter }) + Button(filter.name, action: { filterState.appliedFilter = filter }) } }, label: { TextChipButton.makeMenuButton( - title: viewModel.appliedFilter?.name ?? "-", + title: filterState.appliedFilter?.name ?? "-", color: .systemGray6 ) } @@ -778,25 +763,25 @@ struct AnimatingCellHeight: AnimatableModifier { Menu( content: { ForEach(LinkedItemSort.allCases, id: \.self) { sort in - Button(sort.displayName, action: { viewModel.appliedSort = sort.rawValue }) + Button(sort.displayName, action: { filterState.appliedSort = sort.rawValue }) } }, label: { TextChipButton.makeMenuButton( - title: LinkedItemSort(rawValue: viewModel.appliedSort)?.displayName ?? "Sort", + title: LinkedItemSort(rawValue: filterState.appliedSort)?.displayName ?? "Sort", color: .systemGray6 ) } ) TextChipButton.makeAddLabelButton(color: .systemGray6, onTap: { viewModel.showLabelsSheet = true }) - ForEach(viewModel.selectedLabels, id: \.self) { label in + ForEach(filterState.selectedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: false) { - viewModel.selectedLabels.removeAll { $0.id == label.id } + filterState.selectedLabels.removeAll { $0.id == label.id } } } - ForEach(viewModel.negatedLabels, id: \.self) { label in + ForEach(filterState.negatedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: true) { - viewModel.negatedLabels.removeAll { $0.id == label.id } + filterState.negatedLabels.removeAll { $0.id == label.id } } } Spacer() @@ -832,7 +817,7 @@ struct AnimatingCellHeight: AnimatableModifier { ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: 325, maximum: 400), spacing: 16)], alignment: .center, spacing: 30) { - ForEach(viewModel.items) { item in + ForEach(viewModel.fetcher.items) { item in GridCardNavigationLink( item: item, actionHandler: { contextMenuActionHandler(item: item, action: $0) }, @@ -860,7 +845,7 @@ struct AnimatingCellHeight: AnimatableModifier { } } - if viewModel.items.isEmpty, viewModel.isLoading { + if viewModel.fetcher.items.isEmpty, viewModel.isLoading { LoadingSection() } } @@ -896,7 +881,7 @@ struct ScrollViewOffsetPreferenceKey: PreferenceKey { #endif struct LinkDestination: View { - let selectedItem: LinkedItem? + let selectedItem: Models.LibraryItem? var body: some View { Group { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index e96127e08..23614150f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -11,7 +11,7 @@ import Views @EnvironmentObject var audioController: AudioController @EnvironmentObject var authenticator: Authenticator - @State private var itemToRemove: LinkedItem? + @State private var itemToRemove: Models.LibraryItem? @State private var confirmationShown = false @State private var presentProfileSheet = false @State private var addLinkPresented = false @@ -29,7 +29,7 @@ import Views } } - func menuItems(_ item: LinkedItem) -> some View { + func menuItems(_ item: Models.LibraryItem) -> some View { Group { Button( action: { viewModel.itemUnderTitleEdit = item }, diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 66b7a52ae..a0dd184b2 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -5,35 +5,30 @@ import SwiftUI import Utils import Views -@MainActor final class HomeFeedViewModel: NSObject, ObservableObject { +@MainActor final class HomeFeedViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate { var currentDetailViewModel: LinkItemDetailViewModel? - private var fetchedResultsController: NSFetchedResultsController? + private var fetchedResultsController: NSFetchedResultsController? - @Published var items = [LinkedItem]() @Published var isLoading = false @Published var showPushNotificationPrimer = false - @Published var itemUnderLabelEdit: LinkedItem? - @Published var itemUnderTitleEdit: LinkedItem? - @Published var itemForHighlightsView: LinkedItem? - @Published var searchTerm = "" - @Published var scopeSelection = 0 - @Published var selectedLabels = [LinkedItemLabel]() - @Published var negatedLabels = [LinkedItemLabel]() + @Published var itemUnderLabelEdit: Models.LibraryItem? + @Published var itemUnderTitleEdit: Models.LibraryItem? + @Published var itemForHighlightsView: Models.LibraryItem? @Published var snoozePresented = false @Published var itemToSnoozeID: String? @Published var linkRequest: LinkRequest? @Published var showLoadingBar = false @Published var isInMultiSelectMode = false - @Published var appliedSort = LinkedItemSort.newest.rawValue @Published var selectedLinkItem: NSManagedObjectID? // used by mac app only - @Published var selectedItem: LinkedItem? + @Published var selectedItem: Models.LibraryItem? @Published var linkIsActive = false @Published var showLabelsSheet = false @Published var showFiltersModal = false - @Published var featureItems = [LinkedItem]() + @Published var showCommunityModal = false + @Published var featureItems = [Models.LibraryItem]() @Published var listConfig: LibraryListConfig @@ -41,11 +36,6 @@ import Views @Published var snackbarOperation: SnackbarOperation? @Published var filters = [InternalFilter]() - @Published var appliedFilter: InternalFilter? { - didSet { - appliedFilterName = appliedFilter?.name.lowercased() ?? "inbox" - } - } var cursor: String? @@ -57,19 +47,16 @@ import Views var syncCursor: String? @AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false - @AppStorage(UserDefaultKey.lastSelectedLinkedItemFilter.rawValue) var appliedFilterName = "inbox" @AppStorage(UserDefaultKey.lastSelectedFeaturedItemFilter.rawValue) var featureFilter = FeaturedItemFilter.continueReading.rawValue - init(listConfig: LibraryListConfig) { + let fetcher: LibraryItemFetcher + + init(fetcher: LibraryItemFetcher, listConfig: LibraryListConfig) { + self.fetcher = fetcher self.listConfig = listConfig super.init() } - func setItems(_ context: NSManagedObjectContext, _ items: [LinkedItem]) { - self.items = items - updateFeatureFilter(context: context, filter: FeaturedItemFilter(rawValue: featureFilter)) - } - func updateFeatureFilter(context: NSManagedObjectContext, filter: FeaturedItemFilter?) { if let filter = filter { Task { @@ -86,21 +73,22 @@ import Views } } - func itemAppeared(item: LinkedItem, dataService: DataService) async { + func itemAppeared(item: Models.LibraryItem, dataService _: DataService) async { if isLoading { return } - let itemIndex = items.firstIndex(where: { $0.id == item.id }) - let thresholdIndex = items.index(items.endIndex, offsetBy: -5) + let itemIndex = fetcher.items.firstIndex(where: { $0.id == item.id }) + let thresholdIndex = fetcher.items.index(fetcher.items.endIndex, offsetBy: -5) // Check if user has scrolled to the last five items in the list // Make sure we aren't currently loading though, as this would get triggered when the first set // of items are presented to the user. - if let itemIndex = itemIndex, itemIndex > thresholdIndex { - await loadMoreItems(dataService: dataService, isRefresh: false) - } +// if let itemIndex = itemIndex, itemIndex > thresholdIndex { +// await loadMoreItems(dataService: dataService, isRefresh: false) +// } } - func pushFeedItem(item: LinkedItem) { - items.insert(item, at: 0) + func pushFeedItem(item _: Models.LibraryItem) { + /// TODO: jackson + // fetcher.items.insert(item, at: 0) } func loadCurrentViewer(dataService: DataService) async { @@ -139,110 +127,19 @@ import Views } } - func updateFilters(newFilters: [InternalFilter]) { - filters = newFilters.sorted(by: { $0.position < $1.position }) + [InternalFilter.DeletedFilter, InternalFilter.DownloadedFilter] - if let newFilter = filters.first(where: { $0.name.lowercased() == appliedFilterName }), newFilter.id != appliedFilter?.id { - appliedFilter = newFilter - } + func updateFilters(newFilters _: [InternalFilter]) { +// filters = newFilters.sorted(by: { $0.position < $1.position }) + [InternalFilter.DeletedFilter, InternalFilter.DownloadedFilter] +// if let newFilter = filters.first(where: { $0.name.lowercased() == appliedFilterName }), newFilter.id != appliedFilter?.id { +// appliedFilter = newFilter +// } } - func syncItems(dataService: DataService) async { - let syncStart = Date.now - let lastSyncDate = dataService.lastItemSyncTime - - try? await dataService.syncOfflineItemsWithServerIfNeeded() - - let syncResult = try? await dataService.syncLinkedItems(since: lastSyncDate, - cursor: nil) - - syncCursor = syncResult?.cursor - if let syncResult = syncResult, syncResult.hasMore { - dataService.syncLinkedItemsInBackground(since: lastSyncDate) { - // Set isLoading to false here - self.isLoading = false - } - } else { - dataService.lastItemSyncTime = syncStart - } - - // If possible start prefetching new pages in the background - if - let itemIDs = syncResult?.updatedItemIDs, - let username = dataService.currentViewer?.username, - !itemIDs.isEmpty - { - Task.detached(priority: .background) { - await dataService.prefetchPages(itemIDs: itemIDs, username: username) - } - } - } - - func loadSearchQuery(dataService: DataService, isRefresh: Bool) async { - let thisSearchIdx = searchIdx - searchIdx += 1 - - if thisSearchIdx > 0, thisSearchIdx <= receivedIdx { - return - } - - let queryResult = try? await dataService.loadLinkedItems( - limit: 10, - searchQuery: searchQuery, - cursor: isRefresh ? nil : cursor - ) - - if let appliedFilter = appliedFilter, let queryResult = queryResult { - let newItems: [LinkedItem] = { - var itemObjects = [LinkedItem]() - dataService.viewContext.performAndWait { - itemObjects = queryResult.itemIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItem } - } - return itemObjects - }() - - if searchTerm.replacingOccurrences(of: " ", with: "").isEmpty, appliedFilter.predicate != nil { - updateFetchController(dataService: dataService) - } else { - // Don't use FRC for searching. Use server results directly. - if fetchedResultsController != nil { - fetchedResultsController = nil - setItems(dataService.viewContext, []) - } - setItems(dataService.viewContext, isRefresh ? newItems : items + newItems) - } - - isLoading = false - receivedIdx = thisSearchIdx - cursor = queryResult.cursor - if let username = dataService.currentViewer?.username { - await dataService.prefetchPages(itemIDs: newItems.map(\.unwrappedID), username: username) - } - } else { - updateFetchController(dataService: dataService) - } - } - - func loadItems(dataService: DataService, isRefresh: Bool) async { + func loadItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { isLoading = true showLoadingBar = true - await withTaskGroup(of: Void.self) { group in - group.addTask { await self.loadCurrentViewer(dataService: dataService) } - group.addTask { await self.loadLabels(dataService: dataService) } - group.addTask { await self.loadFilters(dataService: dataService) } - group.addTask { await self.syncItems(dataService: dataService) } - group.addTask { await self.updateFetchController(dataService: dataService) } - await group.waitForAll() - } - - if let appliedFilter = appliedFilter { - let shouldRemoteSearch = items.count < 1 || isRefresh && appliedFilter.shouldRemoteSearch - if shouldRemoteSearch { - await loadSearchQuery(dataService: dataService, isRefresh: isRefresh) - } else { - updateFetchController(dataService: dataService) - } - } + // group.addTask { await self.loadFilters(dataService: dataService) } + await fetcher.loadItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh) updateFeatureFilter(context: dataService.viewContext, filter: FeaturedItemFilter(rawValue: featureFilter)) @@ -250,20 +147,18 @@ import Views showLoadingBar = false } - func loadMoreItems(dataService: DataService, isRefresh: Bool) async { + func loadMoreItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { isLoading = true showLoadingBar = true - if let appliedFilter, appliedFilter.shouldRemoteSearch { - await loadSearchQuery(dataService: dataService, isRefresh: isRefresh) - } + await fetcher.loadMoreItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh) isLoading = false showLoadingBar = false } - func loadFeatureItems(context: NSManagedObjectContext, predicate: NSPredicate, sort: NSSortDescriptor) async -> [LinkedItem] { - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + func loadFeatureItems(context: NSManagedObjectContext, predicate: NSPredicate, sort: NSSortDescriptor) async -> [Models.LibraryItem] { + let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() fetchRequest.fetchLimit = 25 fetchRequest.predicate = predicate fetchRequest.sortDescriptors = [sort] @@ -271,62 +166,6 @@ import Views return (try? context.fetch(fetchRequest)) ?? [] } - private var fetchRequest: NSFetchRequest { - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - - var subPredicates = [NSPredicate]() - - if let predicate = appliedFilter?.predicate { - subPredicates.append(predicate) - } - - if !selectedLabels.isEmpty { - var labelSubPredicates = [NSPredicate]() - - for label in selectedLabels { - labelSubPredicates.append( - NSPredicate(format: "SUBQUERY(labels, $label, $label.id == \"\(label.unwrappedID)\").@count > 0") - ) - } - - subPredicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: labelSubPredicates)) - } - - if !negatedLabels.isEmpty { - var labelSubPredicates = [NSPredicate]() - - for label in negatedLabels { - labelSubPredicates.append( - NSPredicate(format: "SUBQUERY(labels, $label, $label.id == \"\(label.unwrappedID)\").@count == 0") - ) - } - - subPredicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: labelSubPredicates)) - } - - fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: subPredicates) - fetchRequest.sortDescriptors = (LinkedItemSort(rawValue: appliedSort) ?? .newest).sortDescriptors - - return fetchRequest - } - - private func updateFetchController(dataService: DataService) { - fetchedResultsController = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: dataService.viewContext, - sectionNameKeyPath: nil, - cacheName: nil - ) - - guard let fetchedResultsController = fetchedResultsController else { - return - } - - fetchedResultsController.delegate = self - try? fetchedResultsController.performFetch() - setItems(dataService.viewContext, fetchedResultsController.fetchedObjects ?? []) - } - func snackbar(_ message: String, undoAction: SnackbarUndoAction? = nil) { snackbarOperation = SnackbarOperation(message: message, undoAction: undoAction) showSnackbar = true @@ -362,7 +201,7 @@ import Views return nil } - func addLabel(dataService: DataService, item: LinkedItem, label: String, color: String) { + func addLabel(dataService: DataService, item: Models.LibraryItem, label: String, color: String) { if let label = getOrCreateLabel(dataService: dataService, named: "Pinned", color: color) { let existingLabels = item.labels?.allObjects.compactMap { $0 as? LinkedItemLabel } ?? [] dataService.setItemLabels(itemID: item.unwrappedID, labels: InternalLinkedItemLabel.make(Set(existingLabels + [label]) as NSSet)) @@ -372,7 +211,7 @@ import Views } } - func removeLabel(dataService: DataService, item: LinkedItem, named: String) { + func removeLabel(dataService: DataService, item: Models.LibraryItem, named: String) { let labels = item.labels? .filter { ($0 as? LinkedItemLabel)?.name != named } .compactMap { $0 as? LinkedItemLabel } ?? [] @@ -380,25 +219,25 @@ import Views item.update(inContext: dataService.viewContext) } - func pinItem(dataService: DataService, item: LinkedItem) { + func pinItem(dataService: DataService, item: Models.LibraryItem) { addLabel(dataService: dataService, item: item, label: "Pinned", color: "#0A84FF") if featureFilter == FeaturedItemFilter.pinned.rawValue { updateFeatureFilter(context: dataService.viewContext, filter: .pinned) } } - func unpinItem(dataService: DataService, item: LinkedItem) { + func unpinItem(dataService: DataService, item: Models.LibraryItem) { removeLabel(dataService: dataService, item: item, named: "Pinned") if featureFilter == FeaturedItemFilter.pinned.rawValue { updateFeatureFilter(context: dataService.viewContext, filter: .pinned) } } - func markRead(dataService: DataService, item: LinkedItem) { + func markRead(dataService: DataService, item: Models.LibraryItem) { dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 100, anchorIndex: 0, force: true) } - func markUnread(dataService: DataService, item: LinkedItem) { + func markUnread(dataService: DataService, item: Models.LibraryItem) { dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0, force: true) } @@ -420,55 +259,4 @@ import Views func findFilter(_: DataService, named: String) -> InternalFilter? { filters.first(where: { $0.name == named }) } - - private var queryContainsFilter: Bool { - if searchTerm.contains("in:inbox") || searchTerm.contains("in:all") || searchTerm.contains("in:archive") { - return true - } - - return false - } - - private var searchQuery: String { - let sort = LinkedItemSort(rawValue: appliedSort) ?? .newest - var query = sort.queryString - - if !queryContainsFilter, let filter = appliedFilter?.filter { - query = "\(filter) \(sort.queryString)" - } - - if !searchTerm.isEmpty { - query.append(" \(searchTerm)") - } - - if !selectedLabels.isEmpty { - query.append(" label:") - query.append(selectedLabels.compactMap { label in - if let name = label.name { - return "\"\(name)\"" - } - return nil - }.joined(separator: ",")) - } - - if !negatedLabels.isEmpty { - query.append(" !label:") - query.append(negatedLabels.compactMap { label in - if let name = label.name { - return "\"\(name)\"" - } - return nil - }.joined(separator: ",")) - } - - print("QUERY: `\(query)`") - - return query - } -} - -extension HomeFeedViewModel: NSFetchedResultsControllerDelegate { - func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - setItems(controller.managedObjectContext, controller.fetchedObjects as? [LinkedItem] ?? []) - } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemFetcher.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemFetcher.swift new file mode 100644 index 000000000..44ea54c1e --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemFetcher.swift @@ -0,0 +1,43 @@ +// +// File.swift +// +// +// Created by Jackson Harper on 11/16/23. +// + +import Foundation +import Models +import Services +import SwiftUI +import Utils + +@MainActor +class FetcherFilterState: ObservableObject { + @Published var searchTerm = "" + @Published var selectedLabels = [LinkedItemLabel]() + @Published var negatedLabels = [LinkedItemLabel]() + + @Published var appliedSort = LinkedItemSort.newest.rawValue + + @AppStorage(UserDefaultKey.lastSelectedLinkedItemFilter.rawValue) var appliedFilterName = "inbox" + @Published var appliedFilter: InternalFilter? { + didSet { + appliedFilterName = appliedFilter?.name.lowercased() ?? "inbox" + } + } + + init(appliedFilterName: String) { + self.appliedFilterName = appliedFilterName + } +} + +@MainActor +protocol LibraryItemFetcher { + var folder: String { get } + + var items: [Models.LibraryItem] { get } + var itemsPublisher: Published<[Models.LibraryItem]>.Publisher { get } + + func loadItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async + func loadMoreItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemMenu.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemMenu.swift index 2c06ce60b..b91ffdd46 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemMenu.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemMenu.swift @@ -6,7 +6,7 @@ import UserNotifications import Utils import Views -@MainActor func libraryItemMenu(dataService: DataService, viewModel: HomeFeedViewModel, item: LinkedItem) -> some View { +@MainActor func libraryItemMenu(dataService: DataService, viewModel: HomeFeedViewModel, item: Models.LibraryItem) -> some View { Group { if item.state != "DELETED" { Button( @@ -17,33 +17,33 @@ import Views action: { viewModel.itemUnderLabelEdit = item }, label: { Label(item.labels?.count == 0 ? "Add Labels" : "Edit Labels", systemImage: "tag") } ) - Button(action: { - withAnimation(.linear(duration: 0.4)) { - viewModel.setLinkArchived( - dataService: dataService, - objectID: item.objectID, - archived: !item.isArchived - ) - } - }, label: { - Label( - item.isArchived ? "Unarchive" : "Archive", - systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox" - ) - }) - Button("Remove Item", role: .destructive) { - viewModel.removeLink(dataService: dataService, objectID: item.objectID) - } - if let author = item.author { - Button( - action: { - viewModel.searchTerm = "author:\"\(author)\"" - }, - label: { - Label(String("More by \(author)"), systemImage: "person") - } - ) - } +// Button(action: { +// withAnimation(.linear(duration: 0.4)) { +// viewModel.setLinkArchived( +// dataService: dataService, +// objectID: item.objectID, +// archived: !item.isArchived +// ) +// } +// }, label: { +// Label( +// item.isArchived ? "Unarchive" : "Archive", +// systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox" +// ) +// }) +// Button("Remove Item", role: .destructive) { +// viewModel.removeLink(dataService: dataService, objectID: item.objectID) +// } +// if let author = item.author { +// Button( +// action: { +// viewModel.filterState.searchTerm = "author:\"\(author)\"" +// }, +// label: { +// Label(String("More by \(author)"), systemImage: "person") +// } +// ) +// } } else { Button( action: { viewModel.recoverItem(dataService: dataService, itemID: item.unwrappedID) }, diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift index bbf94692b..43c9da2f8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift @@ -10,16 +10,8 @@ import Models import SwiftUI struct LibraryListView: View { - @StateObject private var subViewModel = HomeFeedViewModel( - listConfig: LibraryListConfig( - hasFeatureCards: false, - leadingSwipeActions: [.moveToInbox], - trailingSwipeActions: [.archive, .delete], - cardStyle: .library - ) - ) - @StateObject private var libraryViewModel = HomeFeedViewModel( + fetcher: InboxFetcher(), listConfig: LibraryListConfig( hasFeatureCards: true, leadingSwipeActions: [.pin], @@ -28,15 +20,6 @@ struct LibraryListView: View { ) ) - @StateObject private var highlightsViewModel = HomeFeedViewModel( - listConfig: LibraryListConfig( - hasFeatureCards: true, - leadingSwipeActions: [.pin], - trailingSwipeActions: [.archive, .delete], - cardStyle: .highlights - ) - ) - var body: some View { // ZStack { // NavigationLink( diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibrarySearchView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibrarySearchView.swift index 523867779..cd49f94fa 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/LibrarySearchView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibrarySearchView.swift @@ -33,11 +33,11 @@ performTypeahead(searchTerm) } - func performSearch(_ searchTerm: String) { - let term = searchTerm.trimmingCharacters(in: Foundation.CharacterSet.whitespacesAndNewlines) - viewModel.saveRecentSearch(dataService: dataService, searchTerm: term) - recents = viewModel.recentSearches(dataService: dataService) - homeFeedViewModel.searchTerm = term + func performSearch(_: String) { +// let term = searchTerm.trimmingCharacters(in: Foundation.CharacterSet.whitespacesAndNewlines) +// viewModel.saveRecentSearch(dataService: dataService, searchTerm: term) +// recents = viewModel.recentSearches(dataService: dataService) +// homeFeedViewModel.searchTerm = term dismiss() } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index 409955170..8496a693e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -7,7 +7,7 @@ import Views @MainActor struct ApplyLabelsView: View { enum Mode { - case item(LinkedItem) + case item(Models.LibraryItem) case highlight(Highlight) case list([LinkedItemLabel]) diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift index 569014ab3..23ad0059c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsViewModel.swift @@ -25,7 +25,7 @@ import SwiftUI func loadLabels( dataService: DataService, - item: LinkedItem? = nil, + item: Models.LibraryItem? = nil, highlight: Highlight? = nil, initiallySelectedLabels: [LinkedItemLabel]? = nil ) async { diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index 763bef4f6..6af409c87 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -16,7 +16,14 @@ import Views struct LibraryTabView: View { @EnvironmentObject var dataService: DataService - @StateObject private var subViewModel = HomeFeedViewModel( + @MainActor + public init() { + UITabBar.appearance().isHidden = true + UITabBar.appearance().backgroundColor = UIColor(Color.themeTabBarColor) + } + + @StateObject private var followingViewModel = HomeFeedViewModel( + fetcher: InboxFetcher(), listConfig: LibraryListConfig( hasFeatureCards: false, leadingSwipeActions: [.moveToInbox], @@ -26,6 +33,7 @@ struct LibraryTabView: View { ) @StateObject private var libraryViewModel = HomeFeedViewModel( + fetcher: InboxFetcher(), listConfig: LibraryListConfig( hasFeatureCards: true, leadingSwipeActions: [.pin], @@ -35,6 +43,7 @@ struct LibraryTabView: View { ) @StateObject private var highlightsViewModel = HomeFeedViewModel( + fetcher: InboxFetcher(), listConfig: LibraryListConfig( hasFeatureCards: true, leadingSwipeActions: [.pin], @@ -43,12 +52,31 @@ struct LibraryTabView: View { ) ) + @State var selectedTab = "following" + var body: some View { - NavigationView { - HomeView(viewModel: libraryViewModel) - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif + VStack(spacing: 0) { + TabView(selection: $selectedTab) { + NavigationView { + HomeView(viewModel: followingViewModel) + .navigationViewStyle(.stack) + } + .tag("following") + + NavigationView { + HomeView(viewModel: libraryViewModel) + .navigationViewStyle(.stack) + } + .tag("inbox") + + NavigationView { + ProfileView() + .navigationViewStyle(.stack) + } + .tag("profile") + } + CustomTabBar(selectedTab: $selectedTab) } + .ignoresSafeArea() } } diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index 393f87f46..8b0a1f010 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -7,11 +7,11 @@ import Views @MainActor final class LinkItemDetailViewModel: ObservableObject { @Published var pdfItem: PDFItem? - @Published var item: LinkedItem? + @Published var item: Models.LibraryItem? func loadItem(linkedItemObjectID: NSManagedObjectID, dataService: DataService) async { let item = await dataService.viewContext.perform { - dataService.viewContext.object(with: linkedItemObjectID) as? LinkedItem + dataService.viewContext.object(with: linkedItemObjectID) as? Models.LibraryItem } if let item = item { @@ -85,17 +85,11 @@ struct LinkItemDetailView: View { } var body: some View { - ZStack { + Group { if isPDF { pdfContainerView } else if let item = viewModel.item { WebReaderContainerView(item: item, pop: { dismiss() }) - #if os(iOS) - .navigationBarHidden(true) - .lazyPop(pop: { - dismiss() - }, isEnabled: $isEnabled) - #endif } } .ignoresSafeArea(.all, edges: .bottom) diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkedItemMetadataEditView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkedItemMetadataEditView.swift index 39065ef6d..26560de47 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkedItemMetadataEditView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkedItemMetadataEditView.swift @@ -8,13 +8,13 @@ import Views @Published var description = "" @Published var author = "" - func load(item: LinkedItem) { + func load(item: Models.LibraryItem) { title = item.unwrappedTitle author = item.author ?? "" description = item.descriptionText ?? "" } - func submit(dataService: DataService, item: LinkedItem) { + func submit(dataService: DataService, item: Models.LibraryItem) { dataService.updateLinkedItemTitleAndDescription( itemID: item.unwrappedID, title: title, @@ -30,10 +30,10 @@ struct LinkedItemMetadataEditView: View { @Environment(\.presentationMode) private var presentationMode @StateObject var viewModel = LinkedItemMetadataEditViewModel() - let item: LinkedItem + let item: Models.LibraryItem let onSave: ((String, String) -> Void)? - init(item: LinkedItem, onSave: ((String, String) -> Void)? = nil) { + init(item: Models.LibraryItem, onSave: ((String, String) -> Void)? = nil) { self.item = item self.onSave = onSave } diff --git a/apple/OmnivoreKit/Sources/App/Views/PrimaryContentView.swift b/apple/OmnivoreKit/Sources/App/Views/PrimaryContentView.swift index a65b1def5..6ce2f22d4 100644 --- a/apple/OmnivoreKit/Sources/App/Views/PrimaryContentView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/PrimaryContentView.swift @@ -18,11 +18,11 @@ import Views public var innerBody: some View { #if os(iOS) if UIDevice.isIPad { - return AnyView(splitView) + return AnyView(LibraryTabView()) + // return AnyView(splitView) } else { return AnyView( LibraryTabView() - .navigationViewStyle(.stack) ) } #else diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index 7f8a331b2..4b32de72f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -70,12 +70,7 @@ struct ProfileView: View { innerBody } .navigationTitle(LocalText.genericProfile) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - dismissButton - } - } + .navigationBarTitleDisplayMode(.large) #elseif os(macOS) List { innerBody diff --git a/apple/OmnivoreKit/Sources/App/Views/RemoveLibraryItemAction.swift b/apple/OmnivoreKit/Sources/App/Views/RemoveLibraryItemAction.swift index 60269a8e1..763b4b191 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RemoveLibraryItemAction.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RemoveLibraryItemAction.swift @@ -8,7 +8,7 @@ import Views func removeLibraryItemAction(dataService: DataService, objectID: NSManagedObjectID) { dataService.viewContext.performAndWait { - if let item = dataService.viewContext.object(with: objectID) as? LinkedItem { + if let item = dataService.viewContext.object(with: objectID) as? Models.LibraryItem { item.state = "DELETED" try? dataService.viewContext.save() @@ -37,7 +37,7 @@ func removeLibraryItemAction(dataService: DataService, objectID: NSManagedObject print("canceling task", syncTask) syncTask.cancel() dataService.viewContext.performAndWait { - if let item = dataService.viewContext.object(with: objectID) as? LinkedItem { + if let item = dataService.viewContext.object(with: objectID) as? Models.LibraryItem { item.state = "SUCCEEDED" try? dataService.viewContext.save() } diff --git a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift new file mode 100644 index 000000000..28c39586e --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift @@ -0,0 +1,41 @@ +import Foundation +import SwiftUI + +struct CustomTabBar: View { + @Binding var selectedTab: String + @Namespace var animation + var body: some View { + HStack(spacing: 0) { + TabBarButton(key: "following", image: Image.tabFollowing, selectedTab: $selectedTab, animation: animation) + TabBarButton(key: "inbox", image: Image.tabLibrary, selectedTab: $selectedTab, animation: animation) + TabBarButton(key: "profile", image: Image.tabHighlights, selectedTab: $selectedTab, animation: animation) + } + .padding(.top, 10) + .padding(.bottom, 40) + .background(Color.themeTabBarColor) + } +} + +struct TabBarButton: View { + let key: String + let image: Image + @Binding var selectedTab: String + var animation: Namespace.ID + + var body: some View { + Button(action: { + withAnimation(.spring()) { + selectedTab = key + } + }, label: { + image + .resizable() + .renderingMode(.template) + .aspectRatio(contentMode: .fit) + .frame(width: 28, height: 28) + .foregroundColor(selectedTab == key ? Color.blue : Color.themeTabButtonColor) + + .frame(maxWidth: .infinity) + }) + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift index 029a443e7..b45b610da 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift @@ -6,7 +6,7 @@ import WebKit @MainActor struct WebReader: PlatformViewRepresentable { - let item: LinkedItem + let item: Models.LibraryItem let viewModel: WebReaderViewModel let articleContent: ArticleContent let openLinkAction: (URL) -> Void diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 885b7bfd6..3e96ab26d 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -9,7 +9,7 @@ import WebKit // swiftlint:disable file_length type_body_length struct WebReaderContainerView: View { - let item: LinkedItem + let item: Models.LibraryItem let pop: () -> Void @State private var showPreferencesPopover = false @@ -209,7 +209,7 @@ struct WebReaderContainerView: View { ) } - func menuItems(for item: LinkedItem) -> some View { + func menuItems(for item: Models.LibraryItem) -> some View { let hasLabels = item.labels?.count != 0 return Group { Button( diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContent.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContent.swift index 6cb9956f0..84490173c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContent.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContent.swift @@ -7,7 +7,7 @@ struct WebReaderContent { let textFontSize: Int let lineHeight: Int let maxWidthPercentage: Int - let item: LinkedItem + let item: LibraryItem let isDark: Bool let themeKey: String let fontFamily: WebFont @@ -17,7 +17,7 @@ struct WebReaderContent { let justifyText: Bool init( - item: LinkedItem, + item: Models.LibraryItem, articleContent: ArticleContent, isDark: Bool, fontSize: Int, diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift index 545968187..ae05dd6f0 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -6,7 +6,7 @@ import Utils import Views @MainActor final class WebReaderLoadingContainerViewModel: ObservableObject { - @Published var item: LinkedItem? + @Published var item: Models.LibraryItem? @Published var errorMessage: String? func loadItem(dataService: DataService, username: String, requestID: String) async { @@ -15,7 +15,7 @@ import Views else { return } - item = dataService.viewContext.object(with: objectID) as? LinkedItem + item = dataService.viewContext.object(with: objectID) as? Models.LibraryItem } func trackReadEvent() { @@ -38,7 +38,6 @@ public struct WebReaderLoadingContainer: View { @EnvironmentObject var dataService: DataService @EnvironmentObject var audioController: AudioController - @State var lazyPopIsEnabled = true @StateObject var viewModel = WebReaderLoadingContainerViewModel() public var body: some View { @@ -59,8 +58,6 @@ public struct WebReaderLoadingContainer: View { WebReaderContainerView(item: item, pop: { dismiss() }) #if os(iOS) .navigationViewStyle(.stack) - .navigationBarHidden(true) - .lazyPop(pop: { dismiss() }, isEnabled: $lazyPopIsEnabled) #endif .accentColor(.appGrayTextContrast) .task { viewModel.trackReadEvent() } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift index 2f29e5bc7..8301b90c0 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderViewModel.swift @@ -24,7 +24,7 @@ struct SafariWebLink: Identifiable { showSnackbar = true } - func hasOriginalUrl(_ item: LinkedItem) -> Bool { + func hasOriginalUrl(_ item: Models.LibraryItem) -> Bool { if let pageURLString = item.pageURLString, let host = URL(string: pageURLString)?.host { if host == "omnivore.app" { return false @@ -34,7 +34,7 @@ struct SafariWebLink: Identifiable { return false } - func downloadAudio(audioController: AudioController, item: LinkedItem) { + func downloadAudio(audioController: AudioController, item: Models.LibraryItem) { snackbar(message: "Downloading Offline Audio") isDownloadingAudio = true diff --git a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents index 5ab4d7d1d..d8adbc203 100644 --- a/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents +++ b/apple/OmnivoreKit/Sources/Models/CoreData/CoreDataModel.xcdatamodeld/CoreDataModel.xcdatamodel/contents @@ -28,20 +28,21 @@ - + - + + @@ -86,7 +87,7 @@ - + @@ -113,7 +114,7 @@ - + diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift index ab370b541..7861f8575 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/FeedItem.swift @@ -47,6 +47,7 @@ public struct JSONArticle: Decodable { public let updatedAt: Date public let savedAt: Date public let readAt: Date? + public let folder: String public let image: String public let readingProgressPercent: Double public let readingProgressAnchorIndex: Int @@ -59,7 +60,7 @@ public struct JSONArticle: Decodable { public let downloadURL: String } -public extension LinkedItem { +public extension LibraryItem { var unwrappedID: String { id ?? "" } var unwrappedSlug: String { slug ?? "" } var unwrappedTitle: String { title ?? "" } @@ -247,13 +248,13 @@ public extension LinkedItem { ) } - static func lookup(byID itemID: String, inContext context: NSManagedObjectContext) -> LinkedItem? { - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + static func lookup(byID itemID: String, inContext context: NSManagedObjectContext) -> LibraryItem? { + let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() fetchRequest.predicate = NSPredicate( format: "id == %@", itemID ) - var item: LinkedItem? + var item: LibraryItem? context.performAndWait { item = (try? context.fetch(fetchRequest))?.first diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift index 224e60901..3dcc5c39e 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift @@ -17,7 +17,7 @@ public struct PDFItem { public let downloadURL: String public let highlights: [Highlight] - public static func make(item: LinkedItem) -> PDFItem? { + public static func make(item: LibraryItem) -> PDFItem? { guard item.isPDF else { return nil } return PDFItem( diff --git a/apple/OmnivoreKit/Sources/Models/InboxFilters.swift b/apple/OmnivoreKit/Sources/Models/InboxFilters.swift index 123fdba6a..f35c1a3ed 100644 --- a/apple/OmnivoreKit/Sources/Models/InboxFilters.swift +++ b/apple/OmnivoreKit/Sources/Models/InboxFilters.swift @@ -94,92 +94,92 @@ import Foundation // } // } -public enum FeaturedItemFilter: String, CaseIterable { - case continueReading - case recommended - case newsletters - case pinned -} - -public extension FeaturedItemFilter { - var title: String { - switch self { - case .continueReading: - return "Continue Reading" - case .recommended: - return "Recommended" - case .newsletters: - return "Newsletters" - case .pinned: - return "Pinned" - } - } - - var emptyMessage: String { - switch self { - case .continueReading: - return "Your recently read items will appear here." - case .pinned: - return "Create a label named Pinned and add it to items you would like to appear here." - case .recommended: - return "Reads recommended in your Clubs will appear here." - case .newsletters: - return "All your Newsletters will appear here." - } - } - - var predicate: NSPredicate { - let undeletedPredicate = NSPredicate( - format: "%K != %i", #keyPath(LinkedItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue) - ) - let notInArchivePredicate = NSPredicate( - format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: false) as NSNumber - ) - - switch self { - case .continueReading: - // Use > 1 instead of 0 so its only reads they have made slight progress on. - let continueReadingPredicate = NSPredicate( - format: "readingProgress > 1 AND readingProgress < 100 AND readAt != nil" - ) - return NSCompoundPredicate(andPredicateWithSubpredicates: [ - continueReadingPredicate, undeletedPredicate, notInArchivePredicate - ]) - case .pinned: - let pinnedPredicate = NSPredicate( - format: "SUBQUERY(labels, $label, $label.name == \"Pinned\").@count > 0" - ) - return NSCompoundPredicate(andPredicateWithSubpredicates: [ - notInArchivePredicate, undeletedPredicate, pinnedPredicate - ]) - case .newsletters: - // non-archived or deleted items with the Newsletter label - let newsletterLabelPredicate = NSPredicate( - format: "SUBQUERY(labels, $label, $label.name == \"Newsletter\").@count > 0" - ) - return NSCompoundPredicate(andPredicateWithSubpredicates: [ - notInArchivePredicate, undeletedPredicate, newsletterLabelPredicate - ]) - case .recommended: - // non-archived or deleted items with the Newsletter label - let recommendedPredicate = NSPredicate( - format: "recommendations.@count > 0" - ) - return NSCompoundPredicate(andPredicateWithSubpredicates: [ - notInArchivePredicate, undeletedPredicate, recommendedPredicate - ]) - } - } - - var sortDescriptor: NSSortDescriptor { - let savedAtSort = NSSortDescriptor(key: #keyPath(LinkedItem.savedAt), ascending: false) - switch self { - case .continueReading: - return NSSortDescriptor(key: #keyPath(LinkedItem.readAt), ascending: false) - case .pinned: - return NSSortDescriptor(key: #keyPath(LinkedItem.updatedAt), ascending: false) - default: - return savedAtSort - } - } -} +// public enum FeaturedItemFilter: String, CaseIterable { +// case continueReading +// case recommended +// case newsletters +// case pinned +// } +// +// public extension FeaturedItemFilter { +// var title: String { +// switch self { +// case .continueReading: +// return "Continue Reading" +// case .recommended: +// return "Recommended" +// case .newsletters: +// return "Newsletters" +// case .pinned: +// return "Pinned" +// } +// } +// +// var emptyMessage: String { +// switch self { +// case .continueReading: +// return "Your recently read items will appear here." +// case .pinned: +// return "Create a label named Pinned and add it to items you would like to appear here." +// case .recommended: +// return "Reads recommended in your Clubs will appear here." +// case .newsletters: +// return "All your Newsletters will appear here." +// } +// } +// +// var predicate: NSPredicate { +// let undeletedPredicate = NSPredicate( +// format: "%K != %i", #keyPath(LinkedItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue) +// ) +// let notInArchivePredicate = NSPredicate( +// format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: false) as NSNumber +// ) +// +// switch self { +// case .continueReading: +// // Use > 1 instead of 0 so its only reads they have made slight progress on. +// let continueReadingPredicate = NSPredicate( +// format: "readingProgress > 1 AND readingProgress < 100 AND readAt != nil" +// ) +// return NSCompoundPredicate(andPredicateWithSubpredicates: [ +// continueReadingPredicate, undeletedPredicate, notInArchivePredicate +// ]) +// case .pinned: +// let pinnedPredicate = NSPredicate( +// format: "SUBQUERY(labels, $label, $label.name == \"Pinned\").@count > 0" +// ) +// return NSCompoundPredicate(andPredicateWithSubpredicates: [ +// notInArchivePredicate, undeletedPredicate, pinnedPredicate +// ]) +// case .newsletters: +// // non-archived or deleted items with the Newsletter label +// let newsletterLabelPredicate = NSPredicate( +// format: "SUBQUERY(labels, $label, $label.name == \"Newsletter\").@count > 0" +// ) +// return NSCompoundPredicate(andPredicateWithSubpredicates: [ +// notInArchivePredicate, undeletedPredicate, newsletterLabelPredicate +// ]) +// case .recommended: +// // non-archived or deleted items with the Newsletter label +// let recommendedPredicate = NSPredicate( +// format: "recommendations.@count > 0" +// ) +// return NSCompoundPredicate(andPredicateWithSubpredicates: [ +// notInArchivePredicate, undeletedPredicate, recommendedPredicate +// ]) +// } +// } +// +// var sortDescriptor: NSSortDescriptor { +// let savedAtSort = NSSortDescriptor(key: #keyPath(LinkedItem.savedAt), ascending: false) +// switch self { +// case .continueReading: +// return NSSortDescriptor(key: #keyPath(LinkedItem.readAt), ascending: false) +// case .pinned: +// return NSSortDescriptor(key: #keyPath(LinkedItem.updatedAt), ascending: false) +// default: +// return savedAtSort +// } +// } +// } diff --git a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift new file mode 100644 index 000000000..0ba7c82dc --- /dev/null +++ b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift @@ -0,0 +1,227 @@ +import Foundation + +public enum LinkedItemFilter: String, CaseIterable { + case inbox + case feeds + case readlater + case newsletters + case downloaded + case recommended + case all + case archived + case deleted + case hasHighlights + case files +} + +public extension LinkedItemFilter { + var queryString: String { + switch self { + case .inbox: + return "in:inbox" + case .feeds: + return "label:RSS" + case .readlater: + return "in:library" + case .downloaded: + return "" + case .newsletters: + return "in:inbox label:Newsletter" + case .recommended: + return "recommendedBy:*" + case .all: + return "in:all" + case .archived: + return "in:archive" + case .deleted: + return "in:trash" + case .hasHighlights: + return "has:highlights" + case .files: + return "type:file" + } + } + + var allowLocalFetch: Bool { + switch self { + case .inbox: + return true + default: + return false + } + } + + var predicate: NSPredicate { + let undeletedPredicate = NSPredicate( + format: "%K != %i AND %K != \"DELETED\"", + #keyPath(LibraryItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue), + #keyPath(LibraryItem.state) + ) + let notInArchivePredicate = NSPredicate( + format: "%K == %@", #keyPath(LibraryItem.isArchived), Int(truncating: false) as NSNumber + ) + + switch self { + case .inbox: + // non-archived items + return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate]) + case .readlater: + // non-archived or deleted items without the Newsletter label + let nonNewsletterLabelPredicate = NSPredicate( + format: "NOT SUBQUERY(labels, $label, $label.name == \"Newsletter\") .@count > 0" + ) + let nonRSSPredicate = NSPredicate( + format: "NOT SUBQUERY(labels, $label, $label.name == \"RSS\") .@count > 0" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + undeletedPredicate, notInArchivePredicate, nonNewsletterLabelPredicate, nonRSSPredicate + ]) + case .downloaded: + // include pdf only + let hasHTMLContent = NSPredicate( + format: "htmlContent.length > 0" + ) + let isPDFPredicate = NSPredicate( + format: "%K == %@", #keyPath(LibraryItem.contentReader), "PDF" + ) + let localPDFURL = NSPredicate( + format: "localPDF.length > 0" + ) + let downloadedPDF = NSCompoundPredicate(andPredicateWithSubpredicates: [isPDFPredicate, localPDFURL]) + return NSCompoundPredicate(orPredicateWithSubpredicates: [hasHTMLContent, downloadedPDF]) + case .newsletters: + // non-archived or deleted items with the Newsletter label + let newsletterLabelPredicate = NSPredicate( + format: "SUBQUERY(labels, $label, $label.name == \"Newsletter\").@count > 0" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, newsletterLabelPredicate]) + case .feeds: + let feedLabelPredicate = NSPredicate( + format: "SUBQUERY(labels, $label, $label.name == \"RSS\").@count > 0" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, feedLabelPredicate]) + case .recommended: + // non-archived or deleted items with the Newsletter label + let recommendedPredicate = NSPredicate( + format: "recommendations.@count > 0" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, recommendedPredicate]) + case .all: + // include everything undeleted + return undeletedPredicate + case .archived: + let inArchivePredicate = NSPredicate( + format: "%K == %@", #keyPath(LibraryItem.isArchived), Int(truncating: true) as NSNumber + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, inArchivePredicate]) + case .deleted: + let deletedPredicate = NSPredicate( + format: "%K == %i", #keyPath(LibraryItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue) + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [deletedPredicate]) + case .files: + // include pdf only + let isPDFPredicate = NSPredicate( + format: "%K == %@", #keyPath(LibraryItem.contentReader), "PDF" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, isPDFPredicate]) + case .hasHighlights: + let hasHighlightsPredicate = NSPredicate( + format: "highlights.@count > 0" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + hasHighlightsPredicate + ]) + } + } +} + +public enum FeaturedItemFilter: String, CaseIterable { + case continueReading + case recommended + case newsletters + case pinned +} + +public extension FeaturedItemFilter { + var title: String { + switch self { + case .continueReading: + return "Continue Reading" + case .recommended: + return "Recommended" + case .newsletters: + return "Newsletters" + case .pinned: + return "Pinned" + } + } + + var emptyMessage: String { + switch self { + case .continueReading: + return "Your recently read items will appear here." + case .pinned: + return "Create a label named Pinned and add it to items you would like to appear here." + case .recommended: + return "Reads recommended in your Clubs will appear here." + case .newsletters: + return "All your Newsletters will appear here." + } + } + + var predicate: NSPredicate { + let undeletedPredicate = NSPredicate( + format: "%K != %i", #keyPath(LibraryItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue) + ) + let notInArchivePredicate = NSPredicate( + format: "%K == %@", #keyPath(LibraryItem.isArchived), Int(truncating: false) as NSNumber + ) + + switch self { + case .continueReading: + // Use > 1 instead of 0 so its only reads they have made slight progress on. + let continueReadingPredicate = NSPredicate( + format: "readingProgress > 1 AND readingProgress < 100 AND readAt != nil" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + continueReadingPredicate, undeletedPredicate, notInArchivePredicate + ]) + case .pinned: + let pinnedPredicate = NSPredicate( + format: "SUBQUERY(labels, $label, $label.name == \"Pinned\").@count > 0" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + notInArchivePredicate, undeletedPredicate, pinnedPredicate + ]) + case .newsletters: + // non-archived or deleted items with the Newsletter label + let newsletterLabelPredicate = NSPredicate( + format: "SUBQUERY(labels, $label, $label.name == \"Newsletter\").@count > 0" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + notInArchivePredicate, undeletedPredicate, newsletterLabelPredicate + ]) + case .recommended: + // non-archived or deleted items with the Newsletter label + let recommendedPredicate = NSPredicate( + format: "recommendations.@count > 0" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + notInArchivePredicate, undeletedPredicate, recommendedPredicate + ]) + } + } + + var sortDescriptor: NSSortDescriptor { + let savedAtSort = NSSortDescriptor(key: #keyPath(LibraryItem.savedAt), ascending: false) + switch self { + case .continueReading: + return NSSortDescriptor(key: #keyPath(LibraryItem.readAt), ascending: false) + case .pinned: + return NSSortDescriptor(key: #keyPath(LibraryItem.updatedAt), ascending: false) + default: + return savedAtSort + } + } +} diff --git a/apple/OmnivoreKit/Sources/Models/LinkedItemSort.swift b/apple/OmnivoreKit/Sources/Models/LinkedItemSort.swift index 706e6833e..4759b3a2d 100644 --- a/apple/OmnivoreKit/Sources/Models/LinkedItemSort.swift +++ b/apple/OmnivoreKit/Sources/Models/LinkedItemSort.swift @@ -24,16 +24,16 @@ public extension LinkedItemSort { var sortDescriptors: [NSSortDescriptor] { switch self { case .newest: - return [NSSortDescriptor(keyPath: \LinkedItem.savedAt, ascending: false)] + return [NSSortDescriptor(keyPath: \LibraryItem.savedAt, ascending: false)] case .oldest: - return [NSSortDescriptor(keyPath: \LinkedItem.savedAt, ascending: true)] + return [NSSortDescriptor(keyPath: \LibraryItem.savedAt, ascending: true)] case .recentlyRead: return [ - NSSortDescriptor(keyPath: \LinkedItem.readAt, ascending: false), - NSSortDescriptor(keyPath: \LinkedItem.savedAt, ascending: false) + NSSortDescriptor(keyPath: \LibraryItem.readAt, ascending: false), + NSSortDescriptor(keyPath: \LibraryItem.savedAt, ascending: false) ] case .recentlyPublished: - return [NSSortDescriptor(keyPath: \LinkedItem.publishDate, ascending: false)] + return [NSSortDescriptor(keyPath: \LibraryItem.publishDate, ascending: false)] } } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift index a5427549b..2affe061d 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift @@ -69,7 +69,7 @@ extension DataService { } func cachedArticleContent(itemID: String) async -> ArticleContent? { - let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + let linkedItemFetchRequest: NSFetchRequest = LibraryItem.fetchRequest() linkedItemFetchRequest.predicate = NSPredicate( format: "id == %@", itemID ) @@ -105,11 +105,11 @@ extension DataService { await backgroundContext.perform { [weak self] in guard let self = self else { return } - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() fetchRequest.predicate = NSPredicate(format: "id == %@", articleProps.item.id) let existingItem = try? self.backgroundContext.fetch(fetchRequest).first - let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext) + let linkedItem = existingItem ?? LibraryItem(entity: LibraryItem.entity(), insertInto: self.backgroundContext) objectID = linkedItem.objectID let highlightObjects = articleProps.highlights.map { @@ -190,14 +190,14 @@ extension DataService { /// - Returns: The id of the CoreData object if found. func linkedItemID(from requestID: String) async -> String? { await backgroundContext.perform(schedule: .immediate) { - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() fetchRequest.predicate = NSPredicate(format: "createdId == %@ OR id == %@", requestID, requestID) return try? self.backgroundContext.fetch(fetchRequest).first?.unwrappedID } } func syncUnsyncedArticleContent(itemID: String) async { - let linkedItemFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + let linkedItemFetchRequest: NSFetchRequest = LibraryItem.fetchRequest() linkedItemFetchRequest.predicate = NSPredicate( format: "id == %@", itemID ) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift index 880a88a44..dc8e30de7 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/DataService.swift @@ -69,7 +69,7 @@ public final class DataService: ObservableObject { } public func cleanupDeletedItems(in context: NSManagedObjectContext) { - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() let calendar = Calendar.current let oneDayAgo = calendar.date(byAdding: .day, value: -1, to: Date())! @@ -219,12 +219,12 @@ public final class DataService: ObservableObject { try await backgroundContext.perform { [weak self] in guard let self = self else { return } - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() fetchRequest.predicate = NSPredicate(format: "pageURLString = %@", normalizedURL) let currentTime = Date() let existingItem = try? self.backgroundContext.fetch(fetchRequest).first - let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: self.backgroundContext) + let linkedItem = existingItem ?? LibraryItem(entity: LibraryItem.entity(), insertInto: self.backgroundContext) linkedItem.createdId = requestId linkedItem.id = existingItem?.unwrappedID ?? requestId diff --git a/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift b/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift index af354f3e4..bf14485de 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/FetchLinkedItemsBackgroundTask.swift @@ -48,7 +48,7 @@ extension DataService { } func itemsNotInStore(from itemIDs: [String]) async -> [String] { - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() fetchRequest.predicate = NSPredicate(format: "id IN %@", itemIDs) return await backgroundContext.perform(schedule: .immediate) { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index e5df26be6..f3cf55737 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -667,6 +667,7 @@ extension Objects { let contentReader: [String: Enums.ContentReader] let createdAt: [String: DateTime] let description: [String: String] + let folder: [String: String] let hasContent: [String: Bool] let hash: [String: String] let highlights: [String: [Objects.Highlight]] @@ -741,6 +742,10 @@ extension Objects.Article: Decodable { if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "folder": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "hasContent": if let value = try container.decode(Bool?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -896,6 +901,7 @@ extension Objects.Article: Decodable { contentReader = map["contentReader"] createdAt = map["createdAt"] description = map["description"] + folder = map["folder"] hasContent = map["hasContent"] hash = map["hash"] highlights = map["highlights"] @@ -1019,6 +1025,24 @@ extension Fields where TypeLock == Objects.Article { } } + func folder() throws -> String { + let field = GraphQLField.leaf( + name: "folder", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.folder[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } + func hasContent() throws -> Bool? { let field = GraphQLField.leaf( name: "hasContent", @@ -5609,6 +5633,251 @@ extension Selection where TypeLock == Never, Type == Never { typealias Feature = Selection } +extension Objects { + struct Feed { + let __typename: TypeName = .feed + let author: [String: String] + let createdAt: [String: DateTime] + let description: [String: String] + let id: [String: String] + let image: [String: String] + let publishedAt: [String: DateTime] + let title: [String: String] + let updatedAt: [String: DateTime] + let url: [String: String] + + enum TypeName: String, Codable { + case feed = "Feed" + } + } +} + +extension Objects.Feed: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "author": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "createdAt": + if let value = try container.decode(DateTime?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "description": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "id": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "image": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "publishedAt": + if let value = try container.decode(DateTime?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "title": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "updatedAt": + if let value = try container.decode(DateTime?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "url": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + author = map["author"] + createdAt = map["createdAt"] + description = map["description"] + id = map["id"] + image = map["image"] + publishedAt = map["publishedAt"] + title = map["title"] + updatedAt = map["updatedAt"] + url = map["url"] + } +} + +extension Fields where TypeLock == Objects.Feed { + func author() throws -> String? { + let field = GraphQLField.leaf( + name: "author", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.author[field.alias!] + case .mocking: + return nil + } + } + + func createdAt() throws -> DateTime { + let field = GraphQLField.leaf( + name: "createdAt", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.createdAt[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return DateTime.mockValue + } + } + + func description() throws -> String? { + let field = GraphQLField.leaf( + name: "description", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.description[field.alias!] + case .mocking: + return nil + } + } + + func id() throws -> String { + let field = GraphQLField.leaf( + name: "id", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.id[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } + + func image() throws -> String? { + let field = GraphQLField.leaf( + name: "image", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.image[field.alias!] + case .mocking: + return nil + } + } + + func publishedAt() throws -> DateTime? { + let field = GraphQLField.leaf( + name: "publishedAt", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.publishedAt[field.alias!] + case .mocking: + return nil + } + } + + func title() throws -> String { + let field = GraphQLField.leaf( + name: "title", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.title[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } + + func updatedAt() throws -> DateTime { + let field = GraphQLField.leaf( + name: "updatedAt", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.updatedAt[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return DateTime.mockValue + } + } + + func url() throws -> String { + let field = GraphQLField.leaf( + name: "url", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.url[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias Feed = Selection +} + extension Objects { struct FeedArticle { let __typename: TypeName = .feedArticle @@ -6125,6 +6394,252 @@ extension Selection where TypeLock == Never, Type == Never { typealias FeedArticlesSuccess = Selection } +extension Objects { + struct FeedEdge { + let __typename: TypeName = .feedEdge + let cursor: [String: String] + let node: [String: Objects.Feed] + + enum TypeName: String, Codable { + case feedEdge = "FeedEdge" + } + } +} + +extension Objects.FeedEdge: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "cursor": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "node": + if let value = try container.decode(Objects.Feed?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + cursor = map["cursor"] + node = map["node"] + } +} + +extension Fields where TypeLock == Objects.FeedEdge { + func cursor() throws -> String { + let field = GraphQLField.leaf( + name: "cursor", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.cursor[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } + + func node(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "node", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.node[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias FeedEdge = Selection +} + +extension Objects { + struct FeedsError { + let __typename: TypeName = .feedsError + let errorCodes: [String: [Enums.FeedsErrorCode]] + + enum TypeName: String, Codable { + case feedsError = "FeedsError" + } + } +} + +extension Objects.FeedsError: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.FeedsErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + errorCodes = map["errorCodes"] + } +} + +extension Fields where TypeLock == Objects.FeedsError { + func errorCodes() throws -> [Enums.FeedsErrorCode] { + let field = GraphQLField.leaf( + name: "errorCodes", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.errorCodes[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return [] + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias FeedsError = Selection +} + +extension Objects { + struct FeedsSuccess { + let __typename: TypeName = .feedsSuccess + let edges: [String: [Objects.FeedEdge]] + let pageInfo: [String: Objects.PageInfo] + + enum TypeName: String, Codable { + case feedsSuccess = "FeedsSuccess" + } + } +} + +extension Objects.FeedsSuccess: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "edges": + if let value = try container.decode([Objects.FeedEdge]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "pageInfo": + if let value = try container.decode(Objects.PageInfo?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + edges = map["edges"] + pageInfo = map["pageInfo"] + } +} + +extension Fields where TypeLock == Objects.FeedsSuccess { + func edges(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "edges", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.edges[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + + func pageInfo(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "pageInfo", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.pageInfo[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias FeedsSuccess = Selection +} + extension Objects { struct Filter { let __typename: TypeName = .filter @@ -10397,6 +10912,137 @@ extension Selection where TypeLock == Never, Type == Never { typealias MoveLabelSuccess = Selection } +extension Objects { + struct MoveToFolderError { + let __typename: TypeName = .moveToFolderError + let errorCodes: [String: [Enums.MoveToFolderErrorCode]] + + enum TypeName: String, Codable { + case moveToFolderError = "MoveToFolderError" + } + } +} + +extension Objects.MoveToFolderError: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.MoveToFolderErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + errorCodes = map["errorCodes"] + } +} + +extension Fields where TypeLock == Objects.MoveToFolderError { + func errorCodes() throws -> [Enums.MoveToFolderErrorCode] { + let field = GraphQLField.leaf( + name: "errorCodes", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.errorCodes[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return [] + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias MoveToFolderError = Selection +} + +extension Objects { + struct MoveToFolderSuccess { + let __typename: TypeName = .moveToFolderSuccess + let articleSavingRequest: [String: Objects.ArticleSavingRequest] + + enum TypeName: String, Codable { + case moveToFolderSuccess = "MoveToFolderSuccess" + } + } +} + +extension Objects.MoveToFolderSuccess: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "articleSavingRequest": + if let value = try container.decode(Objects.ArticleSavingRequest?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + articleSavingRequest = map["articleSavingRequest"] + } +} + +extension Fields where TypeLock == Objects.MoveToFolderSuccess { + func articleSavingRequest(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "articleSavingRequest", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.articleSavingRequest[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias MoveToFolderSuccess = Selection +} + extension Objects { struct Mutation { let __typename: TypeName = .mutation @@ -10427,6 +11073,7 @@ extension Objects { let mergeHighlight: [String: Unions.MergeHighlightResult] let moveFilter: [String: Unions.MoveFilterResult] let moveLabel: [String: Unions.MoveLabelResult] + let moveToFolder: [String: Unions.MoveToFolderResult] let optInFeature: [String: Unions.OptInFeatureResult] let recommend: [String: Unions.RecommendResult] let recommendHighlights: [String: Unions.RecommendHighlightsResult] @@ -10586,6 +11233,10 @@ extension Objects.Mutation: Decodable { if let value = try container.decode(Unions.MoveLabelResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "moveToFolder": + if let value = try container.decode(Unions.MoveToFolderResult?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "optInFeature": if let value = try container.decode(Unions.OptInFeatureResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -10751,6 +11402,7 @@ extension Objects.Mutation: Decodable { mergeHighlight = map["mergeHighlight"] moveFilter = map["moveFilter"] moveLabel = map["moveLabel"] + moveToFolder = map["moveToFolder"] optInFeature = map["optInFeature"] recommend = map["recommend"] recommendHighlights = map["recommendHighlights"] @@ -11300,6 +11952,25 @@ extension Fields where TypeLock == Objects.Mutation { } } + func moveToFolder(folder: String, id: String, selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "moveToFolder", + arguments: [Argument(name: "folder", type: "String!", value: folder), Argument(name: "id", type: "ID!", value: id)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.moveToFolder[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + func optInFeature(input: InputObjects.OptInFeatureInput, selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "optInFeature", @@ -13012,6 +13683,7 @@ extension Objects { let article: [String: Unions.ArticleResult] let articleSavingRequest: [String: Unions.ArticleSavingRequestResult] let deviceTokens: [String: Unions.DeviceTokensResult] + let feeds: [String: Unions.FeedsResult] let filters: [String: Unions.FiltersResult] let getUserPersonalization: [String: Unions.GetUserPersonalizationResult] let groups: [String: Unions.GroupsResult] @@ -13068,6 +13740,10 @@ extension Objects.Query: Decodable { if let value = try container.decode(Unions.DeviceTokensResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "feeds": + if let value = try container.decode(Unions.FeedsResult?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "filters": if let value = try container.decode(Unions.FiltersResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -13166,6 +13842,7 @@ extension Objects.Query: Decodable { article = map["article"] articleSavingRequest = map["articleSavingRequest"] deviceTokens = map["deviceTokens"] + feeds = map["feeds"] filters = map["filters"] getUserPersonalization = map["getUserPersonalization"] groups = map["groups"] @@ -13267,6 +13944,25 @@ extension Fields where TypeLock == Objects.Query { } } + func feeds(input: InputObjects.FeedsInput, selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "feeds", + arguments: [Argument(name: "input", type: "FeedsInput!", value: input)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.feeds[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + func filters(selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "filters", @@ -16834,16 +17530,20 @@ extension Objects { let contentReader: [String: Enums.ContentReader] let createdAt: [String: DateTime] let description: [String: String] + let folder: [String: String] let highlights: [String: [Objects.Highlight]] let id: [String: String] let image: [String: String] let isArchived: [String: Bool] let labels: [String: [Objects.Label]] let language: [String: String] + let links: [String: String] let originalArticleUrl: [String: String] let ownedByViewer: [String: Bool] let pageId: [String: String] let pageType: [String: Enums.PageType] + let previewContent: [String: String] + let previewContentType: [String: String] let publishedAt: [String: DateTime] let quote: [String: String] let readAt: [String: DateTime] @@ -16916,6 +17616,10 @@ extension Objects.SearchItem: Decodable { if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "folder": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "highlights": if let value = try container.decode([Objects.Highlight]?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -16940,6 +17644,10 @@ extension Objects.SearchItem: Decodable { if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "links": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "originalArticleUrl": if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -16956,6 +17664,14 @@ extension Objects.SearchItem: Decodable { if let value = try container.decode(Enums.PageType?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "previewContent": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "previewContentType": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "publishedAt": if let value = try container.decode(DateTime?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -17058,16 +17774,20 @@ extension Objects.SearchItem: Decodable { contentReader = map["contentReader"] createdAt = map["createdAt"] description = map["description"] + folder = map["folder"] highlights = map["highlights"] id = map["id"] image = map["image"] isArchived = map["isArchived"] labels = map["labels"] language = map["language"] + links = map["links"] originalArticleUrl = map["originalArticleUrl"] ownedByViewer = map["ownedByViewer"] pageId = map["pageId"] pageType = map["pageType"] + previewContent = map["previewContent"] + previewContentType = map["previewContentType"] publishedAt = map["publishedAt"] quote = map["quote"] readAt = map["readAt"] @@ -17219,6 +17939,24 @@ extension Fields where TypeLock == Objects.SearchItem { } } + func folder() throws -> String { + let field = GraphQLField.leaf( + name: "folder", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.folder[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } + func highlights(selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "highlights", @@ -17317,6 +18055,21 @@ extension Fields where TypeLock == Objects.SearchItem { } } + func links() throws -> String? { + let field = GraphQLField.leaf( + name: "links", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.links[field.alias!] + case .mocking: + return nil + } + } + func originalArticleUrl() throws -> String? { let field = GraphQLField.leaf( name: "originalArticleUrl", @@ -17380,6 +18133,36 @@ extension Fields where TypeLock == Objects.SearchItem { } } + func previewContent() throws -> String? { + let field = GraphQLField.leaf( + name: "previewContent", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.previewContent[field.alias!] + case .mocking: + return nil + } + } + + func previewContentType() throws -> String? { + let field = GraphQLField.leaf( + name: "previewContentType", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.previewContentType[field.alias!] + case .mocking: + return nil + } + } + func publishedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "publishedAt", @@ -19891,11 +20674,13 @@ extension Selection where TypeLock == Never, Type == Never { extension Objects { struct Subscription { let __typename: TypeName = .subscription + let autoAddToLibrary: [String: Bool] let count: [String: Int] let createdAt: [String: DateTime] let description: [String: String] let icon: [String: String] let id: [String: String] + let isPrivate: [String: Bool] let lastFetchedAt: [String: DateTime] let name: [String: String] let newsletterEmail: [String: String] @@ -19924,6 +20709,10 @@ extension Objects.Subscription: Decodable { let field = GraphQLField.getFieldNameFromAlias(alias) switch field { + case "autoAddToLibrary": + if let value = try container.decode(Bool?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "count": if let value = try container.decode(Int?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -19944,6 +20733,10 @@ extension Objects.Subscription: Decodable { if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "isPrivate": + if let value = try container.decode(Bool?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "lastFetchedAt": if let value = try container.decode(DateTime?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -19990,11 +20783,13 @@ extension Objects.Subscription: Decodable { } } + autoAddToLibrary = map["autoAddToLibrary"] count = map["count"] createdAt = map["createdAt"] description = map["description"] icon = map["icon"] id = map["id"] + isPrivate = map["isPrivate"] lastFetchedAt = map["lastFetchedAt"] name = map["name"] newsletterEmail = map["newsletterEmail"] @@ -20008,6 +20803,21 @@ extension Objects.Subscription: Decodable { } extension Fields where TypeLock == Objects.Subscription { + func autoAddToLibrary() throws -> Bool? { + let field = GraphQLField.leaf( + name: "autoAddToLibrary", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.autoAddToLibrary[field.alias!] + case .mocking: + return nil + } + } + func count() throws -> Int { let field = GraphQLField.leaf( name: "count", @@ -20092,6 +20902,21 @@ extension Fields where TypeLock == Objects.Subscription { } } + func isPrivate() throws -> Bool? { + let field = GraphQLField.leaf( + name: "isPrivate", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.isPrivate[field.alias!] + case .mocking: + return nil + } + } + func lastFetchedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "lastFetchedAt", @@ -23477,6 +24302,7 @@ extension Selection where TypeLock == Never, Type == Never { extension Objects { struct UserPersonalization { let __typename: TypeName = .userPersonalization + let fields: [String: String] let fontFamily: [String: String] let fontSize: [String: Int] let id: [String: String] @@ -23507,6 +24333,10 @@ extension Objects.UserPersonalization: Decodable { let field = GraphQLField.getFieldNameFromAlias(alias) switch field { + case "fields": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "fontFamily": if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -23561,6 +24391,7 @@ extension Objects.UserPersonalization: Decodable { } } + fields = map["fields"] fontFamily = map["fontFamily"] fontSize = map["fontSize"] id = map["id"] @@ -23576,6 +24407,21 @@ extension Objects.UserPersonalization: Decodable { } extension Fields where TypeLock == Objects.UserPersonalization { + func fields() throws -> String? { + let field = GraphQLField.leaf( + name: "fields", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.fields[field.alias!] + case .mocking: + return nil + } + } + func fontFamily() throws -> String? { let field = GraphQLField.leaf( name: "fontFamily", @@ -26618,6 +27464,86 @@ extension Selection where TypeLock == Never, Type == Never { typealias FeedArticlesResult = Selection } +extension Unions { + struct FeedsResult { + let __typename: TypeName + let edges: [String: [Objects.FeedEdge]] + let errorCodes: [String: [Enums.FeedsErrorCode]] + let pageInfo: [String: Objects.PageInfo] + + enum TypeName: String, Codable { + case feedsError = "FeedsError" + case feedsSuccess = "FeedsSuccess" + } + } +} + +extension Unions.FeedsResult: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "edges": + if let value = try container.decode([Objects.FeedEdge]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "errorCodes": + if let value = try container.decode([Enums.FeedsErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "pageInfo": + if let value = try container.decode(Objects.PageInfo?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!) + + edges = map["edges"] + errorCodes = map["errorCodes"] + pageInfo = map["pageInfo"] + } +} + +extension Fields where TypeLock == Unions.FeedsResult { + func on(feedsError: Selection, feedsSuccess: Selection) throws -> Type { + select([GraphQLField.fragment(type: "FeedsError", selection: feedsError.selection), GraphQLField.fragment(type: "FeedsSuccess", selection: feedsSuccess.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .feedsError: + let data = Objects.FeedsError(errorCodes: data.errorCodes) + return try feedsError.decode(data: data) + case .feedsSuccess: + let data = Objects.FeedsSuccess(edges: data.edges, pageInfo: data.pageInfo) + return try feedsSuccess.decode(data: data) + } + case .mocking: + return feedsError.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias FeedsResult = Selection +} + extension Unions { struct FiltersResult { let __typename: TypeName @@ -27956,6 +28882,80 @@ extension Selection where TypeLock == Never, Type == Never { typealias MoveLabelResult = Selection } +extension Unions { + struct MoveToFolderResult { + let __typename: TypeName + let articleSavingRequest: [String: Objects.ArticleSavingRequest] + let errorCodes: [String: [Enums.MoveToFolderErrorCode]] + + enum TypeName: String, Codable { + case moveToFolderError = "MoveToFolderError" + case moveToFolderSuccess = "MoveToFolderSuccess" + } + } +} + +extension Unions.MoveToFolderResult: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "articleSavingRequest": + if let value = try container.decode(Objects.ArticleSavingRequest?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "errorCodes": + if let value = try container.decode([Enums.MoveToFolderErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!) + + articleSavingRequest = map["articleSavingRequest"] + errorCodes = map["errorCodes"] + } +} + +extension Fields where TypeLock == Unions.MoveToFolderResult { + func on(moveToFolderError: Selection, moveToFolderSuccess: Selection) throws -> Type { + select([GraphQLField.fragment(type: "MoveToFolderError", selection: moveToFolderError.selection), GraphQLField.fragment(type: "MoveToFolderSuccess", selection: moveToFolderSuccess.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .moveToFolderError: + let data = Objects.MoveToFolderError(errorCodes: data.errorCodes) + return try moveToFolderError.decode(data: data) + case .moveToFolderSuccess: + let data = Objects.MoveToFolderSuccess(articleSavingRequest: data.articleSavingRequest) + return try moveToFolderSuccess.decode(data: data) + } + case .mocking: + return moveToFolderError.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias MoveToFolderResult = Selection +} + extension Unions { struct NewsletterEmailsResult { let __typename: TypeName @@ -32009,6 +33009,15 @@ extension Enums { } } +extension Enums { + /// FeedsErrorCode + enum FeedsErrorCode: String, CaseIterable, Codable { + case badRequest = "BAD_REQUEST" + + case unauthorized = "UNAUTHORIZED" + } +} + extension Enums { /// FiltersErrorCode enum FiltersErrorCode: String, CaseIterable, Codable { @@ -32079,6 +33088,19 @@ extension Enums { } } +extension Enums { + /// ImportItemState + enum ImportItemState: String, CaseIterable, Codable { + case all = "ALL" + + case archived = "ARCHIVED" + + case unarchived = "UNARCHIVED" + + case unread = "UNREAD" + } +} + extension Enums { /// IntegrationType enum IntegrationType: String, CaseIterable, Codable { @@ -32202,6 +33224,17 @@ extension Enums { } } +extension Enums { + /// MoveToFolderErrorCode + enum MoveToFolderErrorCode: String, CaseIterable, Codable { + case alreadyExists = "ALREADY_EXISTS" + + case badRequest = "BAD_REQUEST" + + case unauthorized = "UNAUTHORIZED" + } +} + extension Enums { /// NewsletterEmailsErrorCode enum NewsletterEmailsErrorCode: String, CaseIterable, Codable { @@ -33220,6 +34253,33 @@ extension InputObjects { } } +extension InputObjects { + struct FeedsInput: Encodable, Hashable { + var after: OptionalArgument = .absent() + + var first: OptionalArgument = .absent() + + var query: OptionalArgument = .absent() + + var sort: OptionalArgument = .absent() + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + if after.hasValue { try container.encode(after, forKey: .after) } + if first.hasValue { try container.encode(first, forKey: .first) } + if query.hasValue { try container.encode(query, forKey: .query) } + if sort.hasValue { try container.encode(sort, forKey: .sort) } + } + + enum CodingKeys: String, CodingKey { + case after + case first + case query + case sort + } + } +} + extension InputObjects { struct GenerateApiKeyInput: Encodable, Hashable { var expiresAt: DateTime @@ -33900,8 +34960,12 @@ extension InputObjects { var id: OptionalArgument = .absent() + var importItemState: OptionalArgument = .absent() + var name: String + var syncedAt: OptionalArgument = .absent() + var token: String var type: OptionalArgument = .absent() @@ -33910,7 +34974,9 @@ extension InputObjects { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(enabled, forKey: .enabled) if id.hasValue { try container.encode(id, forKey: .id) } + if importItemState.hasValue { try container.encode(importItemState, forKey: .importItemState) } try container.encode(name, forKey: .name) + if syncedAt.hasValue { try container.encode(syncedAt, forKey: .syncedAt) } try container.encode(token, forKey: .token) if type.hasValue { try container.encode(type, forKey: .type) } } @@ -33918,7 +34984,9 @@ extension InputObjects { enum CodingKeys: String, CodingKey { case enabled case id + case importItemState case name + case syncedAt case token case type } @@ -34058,6 +35126,8 @@ extension InputObjects { extension InputObjects { struct SetUserPersonalizationInput: Encodable, Hashable { + var fields: OptionalArgument = .absent() + var fontFamily: OptionalArgument = .absent() var fontSize: OptionalArgument = .absent() @@ -34080,6 +35150,7 @@ extension InputObjects { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + if fields.hasValue { try container.encode(fields, forKey: .fields) } if fontFamily.hasValue { try container.encode(fontFamily, forKey: .fontFamily) } if fontSize.hasValue { try container.encode(fontSize, forKey: .fontSize) } if libraryLayoutType.hasValue { try container.encode(libraryLayoutType, forKey: .libraryLayoutType) } @@ -34093,6 +35164,7 @@ extension InputObjects { } enum CodingKeys: String, CodingKey { + case fields case fontFamily case fontSize case libraryLayoutType @@ -34163,17 +35235,25 @@ extension InputObjects { extension InputObjects { struct SubscribeInput: Encodable, Hashable { + var autoAddToLibrary: OptionalArgument = .absent() + + var isPrivate: OptionalArgument = .absent() + var subscriptionType: OptionalArgument = .absent() var url: String func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + if autoAddToLibrary.hasValue { try container.encode(autoAddToLibrary, forKey: .autoAddToLibrary) } + if isPrivate.hasValue { try container.encode(isPrivate, forKey: .isPrivate) } if subscriptionType.hasValue { try container.encode(subscriptionType, forKey: .subscriptionType) } try container.encode(url, forKey: .url) } enum CodingKeys: String, CodingKey { + case autoAddToLibrary + case isPrivate case subscriptionType case url } @@ -34429,10 +35509,14 @@ extension InputObjects { extension InputObjects { struct UpdateSubscriptionInput: Encodable, Hashable { + var autoAddToLibrary: OptionalArgument = .absent() + var description: OptionalArgument = .absent() var id: String + var isPrivate: OptionalArgument = .absent() + var lastFetchedAt: OptionalArgument = .absent() var lastFetchedChecksum: OptionalArgument = .absent() @@ -34445,8 +35529,10 @@ extension InputObjects { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + if autoAddToLibrary.hasValue { try container.encode(autoAddToLibrary, forKey: .autoAddToLibrary) } if description.hasValue { try container.encode(description, forKey: .description) } try container.encode(id, forKey: .id) + if isPrivate.hasValue { try container.encode(isPrivate, forKey: .isPrivate) } if lastFetchedAt.hasValue { try container.encode(lastFetchedAt, forKey: .lastFetchedAt) } if lastFetchedChecksum.hasValue { try container.encode(lastFetchedChecksum, forKey: .lastFetchedChecksum) } if name.hasValue { try container.encode(name, forKey: .name) } @@ -34455,8 +35541,10 @@ extension InputObjects { } enum CodingKeys: String, CodingKey { + case autoAddToLibrary case description case id + case isPrivate case lastFetchedAt case lastFetchedChecksum case name diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/ArchiveLink.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/ArchiveLink.swift index 881edfd26..a2355cff8 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/ArchiveLink.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/ArchiveLink.swift @@ -9,7 +9,7 @@ extension DataService { // Update CoreData backgroundContext.perform { [weak self] in guard let self = self else { return } - guard let linkedItem = self.backgroundContext.object(with: objectID) as? LinkedItem else { return } + guard let linkedItem = self.backgroundContext.object(with: objectID) as? LibraryItem else { return } linkedItem.update(inContext: self.backgroundContext, newIsArchivedValue: archived) // Send update to server @@ -54,7 +54,7 @@ extension DataService { let syncStatus: ServerSyncStatus = data == nil ? .needsUpdate : .isNSync context.perform { - guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: context) else { return } + guard let linkedItem = LibraryItem.lookup(byID: itemID, inContext: context) else { return } linkedItem.serverSyncStatus = Int64(syncStatus.rawValue) do { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/BulkActionMutation.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/BulkActionMutation.swift index fcc12fcd4..b74fc4d68 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/BulkActionMutation.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/BulkActionMutation.swift @@ -29,7 +29,7 @@ public extension DataService { // If the item is still available locally, update its state backgroundContext.performAndWait { items.forEach { itemID in - if let linkedItem = LinkedItem.lookup(byID: itemID, inContext: backgroundContext) { + if let linkedItem = Models.LibraryItem.lookup(byID: itemID, inContext: backgroundContext) { if action == .delete { linkedItem.state = "DELETED" linkedItem.serverSyncStatus = Int64(ServerSyncStatus.needsDeletion.rawValue) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLink.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLink.swift index 36e7aa173..eeccc08da 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLink.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLink.swift @@ -10,13 +10,13 @@ public extension DataService { var linkedItemID: String? viewContext.performAndWait { - guard let linkedItem = self.viewContext.object(with: objectID) as? LinkedItem else { return } + guard let linkedItem = self.viewContext.object(with: objectID) as? LibraryItem else { return } linkedItem.serverSyncStatus = Int64(ServerSyncStatus.needsDeletion.rawValue) linkedItemID = linkedItem.id } viewContext.perform { - guard let linkedItem = self.viewContext.object(with: objectID) as? LinkedItem else { return } + guard let linkedItem = self.viewContext.object(with: objectID) as? LibraryItem else { return } linkedItem.serverSyncStatus = Int64(ServerSyncStatus.needsDeletion.rawValue) linkedItemID = linkedItem.id @@ -78,7 +78,7 @@ public extension DataService { let isSyncSuccess = data != nil context.perform { - guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: context) else { return } + guard let linkedItem = LibraryItem.lookup(byID: itemID, inContext: context) else { return } if isSyncSuccess { linkedItem.remove(inContext: context) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UndeleteItem.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UndeleteItem.swift index b75c73933..750d7a046 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UndeleteItem.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UndeleteItem.swift @@ -8,7 +8,7 @@ public extension DataService { var itemUpdatedLocal = false // If the item is still available locally, update its state backgroundContext.performAndWait { - if let linkedItem = LinkedItem.lookup(byID: itemID, inContext: backgroundContext) { + if let linkedItem = LibraryItem.lookup(byID: itemID, inContext: backgroundContext) { linkedItem.serverSyncStatus = Int64(ServerSyncStatus.needsUpdate.rawValue) do { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift index 8536e9d97..7a16b19de 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift @@ -7,7 +7,7 @@ public extension DataService { func setItemLabels(itemID: String, labels: [InternalLinkedItemLabel]) { backgroundContext.perform { [weak self] in guard let self = self else { return } - guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return } + guard let linkedItem = LibraryItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return } if let existingLabels = linkedItem.labels { linkedItem.removeFromLabels(existingLabels) @@ -65,7 +65,7 @@ public extension DataService { let syncStatus: ServerSyncStatus = data == nil ? .needsUpdate : .isNSync context.perform { - guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: context) else { return } + guard let linkedItem = LibraryItem.lookup(byID: itemID, inContext: context) else { return } linkedItem.serverSyncStatus = Int64(syncStatus.rawValue) do { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleListenProgressPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleListenProgressPublisher.swift index 51a62cd13..8958aad98 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleListenProgressPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleListenProgressPublisher.swift @@ -7,7 +7,7 @@ public extension DataService { func updateLinkListeningProgress(itemID: String, listenIndex: Int, listenOffset: Double, listenTime: Double) { backgroundContext.perform { [weak self] in guard let self = self else { return } - guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return } + guard let linkedItem = LibraryItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return } linkedItem.update( inContext: self.backgroundContext, diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleReadingProgress.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleReadingProgress.swift index 100109009..c3d850348 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleReadingProgress.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleReadingProgress.swift @@ -7,7 +7,7 @@ extension DataService { public func updateLinkReadingProgress(itemID: String, readingProgress: Double, anchorIndex: Int, force: Bool?) { backgroundContext.perform { [weak self] in guard let self = self else { return } - guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: self.backgroundContext) 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 { @@ -70,7 +70,7 @@ extension DataService { let syncStatus: ServerSyncStatus = data == nil ? .needsUpdate : .isNSync context.perform { - guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: context) else { return } + guard let linkedItem = LibraryItem.lookup(byID: itemID, inContext: context) else { return } linkedItem.serverSyncStatus = Int64(syncStatus.rawValue) if let mutationResult = data?.data, case let MutationResult.saved(readAt) = mutationResult { linkedItem.readAt = readAt diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateLinkedItemTitle.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateLinkedItemTitle.swift index e4acbf144..ada7516cb 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateLinkedItemTitle.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateLinkedItemTitle.swift @@ -7,7 +7,7 @@ extension DataService { public func updateLinkedItemTitleAndDescription(itemID: String, title: String, description: String, author: String?) { backgroundContext.perform { [weak self] in guard let self = self else { return } - guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return } + guard let linkedItem = LibraryItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return } linkedItem.update( inContext: self.backgroundContext, @@ -65,7 +65,7 @@ extension DataService { let syncStatus: ServerSyncStatus = data == nil ? .needsUpdate : .isNSync context.perform { - guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: context) else { return } + guard let linkedItem = LibraryItem.lookup(byID: itemID, inContext: context) else { return } linkedItem.serverSyncStatus = Int64(syncStatus.rawValue) do { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift index 6f50cbd8d..f7417f262 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift @@ -5,11 +5,11 @@ import Utils public extension DataService { func syncOfflineItemsWithServerIfNeeded() async throws { - var unsyncedLinkedItems = [LinkedItem]() + var unsyncedLinkedItems = [LibraryItem]() var unsyncedHighlights = [Highlight]() // LinkedItems - let itemsFetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + let itemsFetchRequest: NSFetchRequest = LibraryItem.fetchRequest() itemsFetchRequest.predicate = NSPredicate( format: "serverSyncStatus != %i", Int64(ServerSyncStatus.isNSync.rawValue) ) @@ -37,7 +37,7 @@ public extension DataService { private func updateLinkedItemStatus(id: String, newId: String?, status: ServerSyncStatus) async throws { backgroundContext.performAndWait { - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() + let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() fetchRequest.predicate = NSPredicate(format: "id == %@", id) guard let linkedItem = (try? backgroundContext.fetch(fetchRequest))?.first else { return } @@ -107,7 +107,7 @@ public extension DataService { } } - func syncLocalCreatedLinkedItem(item: LinkedItem) { + func syncLocalCreatedLinkedItem(item: LibraryItem) { switch item.contentReader { case "PDF": let id = item.unwrappedID @@ -135,7 +135,7 @@ public extension DataService { } } - private func syncLinkedItems(unsyncedLinkedItems: [LinkedItem]) { + private func syncLinkedItems(unsyncedLinkedItems: [LibraryItem]) { for item in unsyncedLinkedItems { guard let syncStatus = ServerSyncStatus(rawValue: Int(item.serverSyncStatus)) else { continue } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift index a4d68bdce..d0e315318 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/LinkedItemLoading.swift @@ -13,7 +13,7 @@ public extension DataService { ) async throws -> LinkedItemSyncResult { let fetchResult = try await linkedItemUpdates(since: since, limit: 20, cursor: cursor, descending: descending) - LinkedItem.deleteItems(ids: fetchResult.deletedItemIDs, context: backgroundContext) + LibraryItem.deleteItems(ids: fetchResult.deletedItemIDs, context: backgroundContext) if fetchResult.items.persist(context: backgroundContext) == nil { throw BasicError.message(messageText: "CoreData error") diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift index 8b6520640..e892c3299 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift @@ -25,8 +25,8 @@ public extension DataService { .appendingPathComponent(UUID().uuidString + ".pdf") try await backgroundContext.perform { [weak self] in - let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(LinkedItem.slug), slug) + let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(LibraryItem.slug), slug) let linkedItem = try? self?.backgroundContext.fetch(fetchRequest).first guard let linkedItem = linkedItem else { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift index f654003f3..9a7d38ed6 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/ArticleContentQuery.swift @@ -5,7 +5,7 @@ import SwiftGraphQL import Utils struct ArticleProps { - let item: InternalLinkedItem + let item: InternalLibraryItem let htmlContent: String let highlights: [InternalHighlight] } @@ -20,13 +20,14 @@ extension DataService { let articleContentSelection = Selection.Article { ArticleProps( - item: InternalLinkedItem( + item: InternalLibraryItem( id: try $0.id(), title: try $0.title(), createdAt: try $0.createdAt().value ?? Date(), savedAt: try $0.savedAt().value ?? Date(), readAt: try $0.readAt()?.value, updatedAt: try $0.updatedAt()?.value ?? Date(), + folder: try $0.folder(), state: try $0.state()?.rawValue.asArticleContentStatus ?? .succeeded, readingProgress: try $0.readingProgressPercent(), readingProgressAnchor: try $0.readingProgressAnchorIndex(), diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift index 9c1dd7a98..649c353dd 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Queries/LinkedItemNetworkQuery.swift @@ -4,12 +4,12 @@ import Models import SwiftGraphQL struct InternalLinkedItemQueryResult { - let items: [InternalLinkedItem] + let items: [InternalLibraryItem] let cursor: String? } struct InternalLinkedItemUpdatesQueryResult { - let items: [InternalLinkedItem] + let items: [InternalLibraryItem] let deletedItemIDs: [String] let cursor: String? let hasMoreItems: Bool @@ -19,7 +19,7 @@ struct InternalLinkedItemUpdatesQueryResult { private struct SyncItemEdge { let itemID: String let isDeletedItem: Bool - let item: InternalLinkedItem? + let item: InternalLibraryItem? } extension DataService { @@ -90,7 +90,7 @@ extension DataService { switch payload.data { case let .success(result: result): - var items = [InternalLinkedItem]() + var items = [InternalLibraryItem]() var deletedItemIDs = [String]() for edge in result.edges { @@ -186,13 +186,13 @@ extension DataService { /// - itemID: id of the item being requested /// - Returns: Returns an `InternalLinkedItem` or throws a `ContentFetchError` if /// request could not be completed - func fetchLinkedItem(username: String, itemID: String) async throws -> InternalLinkedItem { + func fetchLinkedItem(username: String, itemID: String) async throws -> InternalLibraryItem { struct ArticleProps { - let item: InternalLinkedItem + let item: InternalLibraryItem } enum QueryResult { - case success(result: InternalLinkedItem) + case success(result: InternalLibraryItem) case error(error: String) } @@ -252,13 +252,14 @@ let recommendationSelection = Selection.Recommendation { } private let libraryArticleSelection = Selection.Article { - InternalLinkedItem( + InternalLibraryItem( id: try $0.id(), title: try $0.title(), createdAt: try $0.createdAt().value ?? Date(), savedAt: try $0.savedAt().value ?? Date(), readAt: try $0.readAt()?.value, updatedAt: try $0.updatedAt()?.value ?? Date(), + folder: try $0.folder(), state: try $0.state()?.rawValue.asArticleContentStatus ?? .succeeded, readingProgress: try $0.readingProgressPercent(), readingProgressAnchor: try $0.readingProgressAnchorIndex(), @@ -292,13 +293,14 @@ private let syncItemEdgeSelection = Selection.SyncUpdatedItemEdge { } private let searchItemSelection = Selection.SearchItem { - InternalLinkedItem( + InternalLibraryItem( id: try $0.id(), title: try $0.title(), createdAt: try $0.createdAt().value ?? Date(), savedAt: try $0.savedAt().value ?? Date(), readAt: try $0.readAt()?.value, updatedAt: try $0.updatedAt()?.value ?? Date(), + folder: try $0.folder(), state: try $0.state()?.rawValue.asArticleContentStatus ?? .succeeded, readingProgress: try $0.readingProgressPercent(), readingProgressAnchor: try $0.readingProgressAnchorIndex(), diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift index f4de0afa4..ee51fd0fb 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift @@ -120,11 +120,11 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { let undeletedPredicate = NSPredicate( format: "%K != %i AND %K != \"DELETED\"", - #keyPath(LinkedItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue), - #keyPath(LinkedItem.state) + #keyPath(Models.LibraryItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue), + #keyPath(Models.LibraryItem.state) ) let notInArchivePredicate = NSPredicate( - format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: false) as NSNumber + format: "%K == %@", #keyPath(Models.LibraryItem.isArchived), Int(truncating: false) as NSNumber ) switch name { @@ -148,7 +148,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { format: "htmlContent.length > 0" ) let isPDFPredicate = NSPredicate( - format: "%K == %@", #keyPath(LinkedItem.contentReader), "PDF" + format: "%K == %@", #keyPath(Models.LibraryItem.contentReader), "PDF" ) let localPDFURL = NSPredicate( format: "localPDF.length > 0" @@ -177,18 +177,18 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { return undeletedPredicate case "Archived": let inArchivePredicate = NSPredicate( - format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: true) as NSNumber + format: "%K == %@", #keyPath(Models.LibraryItem.isArchived), Int(truncating: true) as NSNumber ) return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, inArchivePredicate]) case "Deleted": let deletedPredicate = NSPredicate( - format: "%K == %i", #keyPath(LinkedItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue) + format: "%K == %i", #keyPath(Models.LibraryItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue) ) return NSCompoundPredicate(andPredicateWithSubpredicates: [deletedPredicate]) case "Files": // include pdf only let isPDFPredicate = NSPredicate( - format: "%K == %@", #keyPath(LinkedItem.contentReader), "PDF" + format: "%K == %@", #keyPath(Models.LibraryItem.contentReader), "PDF" ) return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, isPDFPredicate]) case "Highlights": diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalHighlight.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalHighlight.swift index 03b829f46..ef0f0eb86 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalHighlight.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalHighlight.swift @@ -91,7 +91,7 @@ struct InternalHighlight: Encodable { let highlight = asManagedObject(context: context) if let associatedItemID = associatedItemID { - let linkedItem = LinkedItem.lookup(byID: associatedItemID, inContext: context) + let linkedItem = LibraryItem.lookup(byID: associatedItemID, inContext: context) linkedItem?.addToHighlights(highlight) } diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLibraryItem.swift similarity index 91% rename from apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift rename to apple/OmnivoreKit/Sources/Services/InternalModels/InternalLibraryItem.swift index 3031cd1ef..140ff1c53 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLinkedItem.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalLibraryItem.swift @@ -2,13 +2,14 @@ import CoreData import Foundation import Models -struct InternalLinkedItem { +struct InternalLibraryItem { let id: String let title: String let createdAt: Date let savedAt: Date let readAt: Date? let updatedAt: Date + let folder: String let state: ArticleContentStatus var readingProgress: Double var readingProgressAnchor: Int @@ -38,9 +39,9 @@ struct InternalLinkedItem { return pageURLString.hasSuffix("pdf") } - func asManagedObject(inContext context: NSManagedObjectContext) -> LinkedItem { - let existingItem = LinkedItem.lookup(byID: id, inContext: context) - let linkedItem = existingItem ?? LinkedItem(entity: LinkedItem.entity(), insertInto: context) + func asManagedObject(inContext context: NSManagedObjectContext) -> LibraryItem { + let existingItem = LibraryItem.lookup(byID: id, inContext: context) + let linkedItem = existingItem ?? LibraryItem(entity: LibraryItem.entity(), insertInto: context) linkedItem.id = id linkedItem.title = title @@ -48,6 +49,7 @@ struct InternalLinkedItem { linkedItem.savedAt = savedAt linkedItem.updatedAt = updatedAt linkedItem.readAt = readAt + linkedItem.folder = folder linkedItem.state = state.rawValue linkedItem.readingProgress = readingProgress linkedItem.readingProgressAnchor = Int64(readingProgressAnchor) @@ -89,9 +91,9 @@ struct InternalLinkedItem { } } -extension Sequence where Element == InternalLinkedItem { +extension Sequence where Element == InternalLibraryItem { func persist(context: NSManagedObjectContext) -> [NSManagedObjectID]? { - var linkedItems: [LinkedItem]? + var linkedItems: [LibraryItem]? context.performAndWait { linkedItems = map { $0.asManagedObject(inContext: context) } @@ -123,13 +125,14 @@ extension JSONArticle { func persistAsLinkedItem(context: NSManagedObjectContext) -> NSManagedObjectID? { var objectID: NSManagedObjectID? - let internalLinkedItem = InternalLinkedItem( + let internalLinkedItem = InternalLibraryItem( id: id, title: title, createdAt: createdAt, savedAt: savedAt, readAt: readAt, updatedAt: updatedAt, + folder: folder, state: .succeeded, readingProgress: readingProgressPercent, readingProgressAnchor: readingProgressAnchorIndex, diff --git a/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift b/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift index 643b1d3cd..214444c79 100644 --- a/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift +++ b/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift @@ -54,6 +54,8 @@ public extension Color { static var noteContainer: Color { Color("_noteContainer", bundle: .module) } static var textFieldBackground: Color { Color("_textFieldBackground", bundle: .module) } + static var themeTabBarColor: Color { Color("_themeTabBarColor", bundle: .module) } + static var themeTabButtonColor: Color { Color("_themeTabButtonColor", bundle: .module) } // Apple system UIColor equivalents #if os(iOS) diff --git a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_themeTabBarColor.colorset/Contents.json b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_themeTabBarColor.colorset/Contents.json new file mode 100644 index 000000000..982111ca3 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_themeTabBarColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2A", + "green" : "0x2A", + "red" : "0x2A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_themeTabButtonColor.colorset/Contents.json b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_themeTabButtonColor.colorset/Contents.json new file mode 100644 index 000000000..2312e4873 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Colors/ThemeColors.xcassets/_themeTabButtonColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x93", + "green" : "0x8E", + "red" : "0x8E" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x93", + "green" : "0x8E", + "red" : "0x8E" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift index ebbdae6ef..5ee53b457 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift @@ -12,12 +12,12 @@ public enum GridCardAction { public struct GridCard: View { @Binding var isContextMenuOpen: Bool - let item: LinkedItem + let item: Models.LibraryItem let actionHandler: (GridCardAction) -> Void // let tapAction: () -> Void public init( - item: LinkedItem, + item: Models.LibraryItem, isContextMenuOpen: Binding, actionHandler: @escaping (GridCardAction) -> Void ) { diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryFeatureCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryFeatureCard.swift index 97ee23227..35913ceff 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryFeatureCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryFeatureCard.swift @@ -5,9 +5,9 @@ import Utils public struct LibraryFeatureCard: View { let viewer: Viewer? let tapHandler: () -> Void - @ObservedObject var item: LinkedItem + @ObservedObject var item: Models.LibraryItem - public init(item: LinkedItem, viewer: Viewer?, tapHandler: @escaping () -> Void = {}) { + public init(item: Models.LibraryItem, viewer: Viewer?, tapHandler: @escaping () -> Void = {}) { self.item = item self.viewer = viewer self.tapHandler = tapHandler diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift index 0b1e837cb..1797ac986 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift @@ -32,7 +32,7 @@ enum FlairLabels: String { } public extension View { - func draggableItem(item: LinkedItem) -> some View { + func draggableItem(item: Models.LibraryItem) -> some View { #if os(iOS) if #available(iOS 16.0, *), let url = item.deepLink { return AnyView(self.draggable(url) { @@ -46,10 +46,10 @@ public extension View { public struct LibraryItemCard: View { let viewer: Viewer? - @ObservedObject var item: LinkedItem + @ObservedObject var item: Models.LibraryItem @State var noteLineLimit: Int? = 3 - public init(item: LinkedItem, viewer: Viewer?) { + public init(item: Models.LibraryItem, viewer: Viewer?) { self.item = item self.viewer = viewer } diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.swift b/apple/OmnivoreKit/Sources/Views/Images/Images.swift index ce00165cb..1c771fd56 100644 --- a/apple/OmnivoreKit/Sources/Views/Images/Images.swift +++ b/apple/OmnivoreKit/Sources/Views/Images/Images.swift @@ -5,15 +5,11 @@ public extension Image { static var omnivoreTitleLogo: Image { Image("_omnivoreTitleLogo", bundle: .module) } static var googleIcon: Image { Image("_googleIcon", bundle: .module) } - static var homeTab: Image { Image("BookmarksSimple", bundle: .module) } - static var homeTabSelected: Image { Image("_homeTabSelected", bundle: .module) } - static var profileTab: Image { Image("_profileTab", bundle: .module) } - static var profileTabSelected: Image { Image("_profileTabSelected", bundle: .module) } static var dotsThree: Image { Image("_dots-three", bundle: .module) } - static var tabSubscriptions: Image { Image("_tab_subscriptions", bundle: .module).renderingMode(.template) } + static var tabFollowing: Image { Image("_tab_following", bundle: .module).renderingMode(.template) } static var tabLibrary: Image { Image("_tab_library", bundle: .module).renderingMode(.template) } - static var tabBriefing: Image { Image("_tab_briefing", bundle: .module).renderingMode(.template) } + static var tabSearch: Image { Image("_tab_search", bundle: .module).renderingMode(.template) } static var tabHighlights: Image { Image("_tab_highlights", bundle: .module).renderingMode(.template) } static var pinRotated: Image { Image("pin-rotated", bundle: .module) } diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTab.imageset/Contents.json b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTab.imageset/Contents.json deleted file mode 100644 index 3688423af..000000000 --- a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTab.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "profile-tab.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTab.imageset/profile-tab.svg b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTab.imageset/profile-tab.svg deleted file mode 100644 index 046ff5225..000000000 --- a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTab.imageset/profile-tab.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTabSelected.imageset/Contents.json b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTabSelected.imageset/Contents.json deleted file mode 100644 index a4ca33e24..000000000 --- a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTabSelected.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "profile-tab-selected.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTabSelected.imageset/profile-tab-selected.svg b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTabSelected.imageset/profile-tab-selected.svg deleted file mode 100644 index d224db571..000000000 --- a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_profileTabSelected.imageset/profile-tab-selected.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_briefing.imageset/Group 1000002644.png b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_briefing.imageset/Group 1000002644.png deleted file mode 100644 index 3ea31a05b870e61d589bc7dd1ab39bb8994f66a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1104 zcmV-W1h4yvP)ggAmZL6Z|SoS<-m> zu)Up$ZDNcQ8{3GVFg+vjyy5+ulu9mb@Oup}@@36yFT zA6FS;6~-KJ95w*pX0y592Z2x^B_s)2oP&xafpZ3oK?{E-Uh0EDB$DvS$#=C-u!nR$ znaJBmN1sgp8tU+nmT5HhwqXfKq{D@RrK2QO`r&!V!1%B5{bYoOVQhi`Bs4nqAuy-} z$uMTu8GG9G$7g4~GU~NqSxONEAXX*FxRAg_aL%@2`|z+f3g2rO=LYI?;0y4pMq_^m zazw0z(U4xfP*I>2b+2u+Ydj3x=U5-dUm#~Jl(1`T)@{V4yU9c;1&zFKwkqBu8< zSzDT>nUFJH5!UsK&9$}rqAX9PNP0Necw8y+tAm5uB`gWgggt4J{#9TB$vMlIs5+yE z2l+Tk>UrXM9IZX>D2 zxUEj->#e(;=LuF_xN#66vi{ds_*I;bO`}=~pvQ)nxD9tPlq?75&?6-9yVkC4D=Opw zS!M>lwMbtqHkP13$5V@`7eUFHnzk?0b%o)Eu&&TzV+qfg2D2Gy1b-2UG7dB_3}R$2 zZ?lfW*C`Rj&sC?C2{MD3=y|kx+kNVs&c}iPDHDPrY)~aq=X5^ay|b#b5gX&5)ZPK@ z1MOuRaAD5K=r{`BlbLW37`bUtfqha?wt`XO&>cmG3VGBemWmZ&(O3}{jTK?hSP>SD z6=Bg>5f+UVVbS>K21t17NmeMm7r`I6VyvgD&Nm$DM{tiBopU#tiM&meZOc>Yt2pYg-Dp{ z6qx#BpTi|fbvEoSY1n)$an7{>5h@Yf#5LC~OV+7UN^4}UOSIcPo!Yi^?l`cEpSyz6 z3zS*;PweIG1dE4M6~)5L5DBRM9Q8|Zg=?Ifw%Blu<7Ts_hu@d0gvqF0l7%jkEr%L{j#J&RrfC`3GxoI?N^>Cp(2$CRu6}sP>ifvm6Ak0QsQYj@ZsJJz> zX{%w78VT=jc-hSYHwu|M1eMfyt?;fkklWUcl{5d WDUJQ*Q0Q&|0000 + + + + + + + + + + + diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_library.imageset/BookOpen.png b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_library.imageset/BookOpen.png deleted file mode 100644 index e512b518aed8c2aa92d7242b3279ca2ebc006516..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 482 zcmV<80UiE{P)t5 z!!Q(v|3us#GeS0?Y*03!6Tk+*8;X!fUl5OU0>T8a0b&Bm1{oo*R4C@yDPWT#XmV_| z)%he_j^kKge2JU`2!bF80y9yo=28t&V~GEvP+=F3$fNSlw#)Vnl-MzSS9YZVzh=NH!akp8Cbxh#Icue2vbIus%u3Xu+l zNQXkCLm|?k5a~Gy9sZjmsq%9)-2y*}`NhiAGnJ3w0Uy$u!900kBi3|1~W?$KPXhMtn*>Biy(lw+I~QPuvVgRJy|AI7dNgLSZwOndFiFCBRtES`@*7yjE71q@&Jisr#XEt%Ok!u=h5AaJjapXhV_2={gIfOOl Y16 + + + + + + + + + + diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_briefing.imageset/Contents.json b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_search.imageset/Contents.json similarity index 88% rename from apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_briefing.imageset/Contents.json rename to apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_search.imageset/Contents.json index b7cce23dc..a38781902 100644 --- a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_briefing.imageset/Contents.json +++ b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_search.imageset/Contents.json @@ -5,7 +5,7 @@ "scale" : "1x" }, { - "filename" : "Group 1000002644.png", + "filename" : "MagnifyingGlass.png", "idiom" : "universal", "scale" : "2x" }, diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_search.imageset/MagnifyingGlass.png b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_search.imageset/MagnifyingGlass.png new file mode 100644 index 0000000000000000000000000000000000000000..f41723c949453148be51fd287c96037613d3f1d9 GIT binary patch literal 552 zcmV+@0@wYCP)Dz!S&~3^#xq$PLKSvM3o~A2`xF(ln@dV0E>=)a1O8HC}JN0fP6^MvR;R?1&f32`xW9vDM?Eb9kO%r8XlM5&vE zEOHK^&yCsa@g3kXAQ3aQnbV8GpohOsli4UD-7+Ry=BKIJ%j#;}1EW)>hD8-2ka-S}W?DpWsa98q~JKF^tfZ0d%Edd@-ACUlD3+ZUoG9FA9 z%Sn+9A38mBk;jD$Db#%DXZ3{P0-JvU%>503X^hAxrQkV0+S^?M(2!EYs|2QN$uFA5 zlg`xIl^G-7gQ5+rBUc&=uxp&)YHOY#&u@65yhDkm)A_h)gQN4jjEXim34C`0jDpT+ qlvj^KC(>}(1Kjwon^qbhe^cM=9l^kj)dTea0000UB2*7Vc+MtF{qZtBx00x8M$zh3h7<&!c;9tV<=%#9wlIW|e zRDfoL5IzcjziEvH3NQv!&HkAprqKd8 zb{sxI183m6*^lAy`WQVx_nv7Y;B&OI9&{oL$zVH9G7O*zZDsUp4zA0@t zV~oc_u<>Ioh{Mo}peD6znraodTg}Csb1TN|{$tJJ3(Rmhz=r&`azZ=wqHDn_DAct0 z4v_DL;o(LvnY?DkvR#>4m8I$FF$#hs=_75G^_S%`Q$Z@zoIhGuMYDO0&S0wFb(2zQ p<@wTL+aB0h$` Date: Thu, 30 Nov 2023 16:02:22 +0800 Subject: [PATCH 02/35] Fix tab icon --- .../App/Views/Profile/ProfileView.swift | 2 +- .../App/Views/TabBar/CustomTabBar.swift | 12 ++++------ .../Sources/Views/Images/Images.swift | 1 + .../_tab_profile.imageset/Contents.json | 24 +++++++++++++++++++ .../_tab_profile.imageset/Frame.svg | 11 +++++++++ 5 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_profile.imageset/Contents.json create mode 100644 apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_profile.imageset/Frame.svg diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index 4b32de72f..a51e29f24 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -70,7 +70,7 @@ struct ProfileView: View { innerBody } .navigationTitle(LocalText.genericProfile) - .navigationBarTitleDisplayMode(.large) + .navigationBarTitleDisplayMode(.inline) #elseif os(macOS) List { innerBody diff --git a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift index 28c39586e..43d8dbe6c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift +++ b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift @@ -3,12 +3,11 @@ import SwiftUI struct CustomTabBar: View { @Binding var selectedTab: String - @Namespace var animation var body: some View { HStack(spacing: 0) { - TabBarButton(key: "following", image: Image.tabFollowing, selectedTab: $selectedTab, animation: animation) - TabBarButton(key: "inbox", image: Image.tabLibrary, selectedTab: $selectedTab, animation: animation) - TabBarButton(key: "profile", image: Image.tabHighlights, selectedTab: $selectedTab, animation: animation) + TabBarButton(key: "following", image: Image.tabFollowing, selectedTab: $selectedTab) + TabBarButton(key: "inbox", image: Image.tabLibrary, selectedTab: $selectedTab) + TabBarButton(key: "profile", image: Image.tabProfile, selectedTab: $selectedTab) } .padding(.top, 10) .padding(.bottom, 40) @@ -20,13 +19,10 @@ struct TabBarButton: View { let key: String let image: Image @Binding var selectedTab: String - var animation: Namespace.ID var body: some View { Button(action: { - withAnimation(.spring()) { - selectedTab = key - } + selectedTab = key }, label: { image .resizable() diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.swift b/apple/OmnivoreKit/Sources/Views/Images/Images.swift index 1c771fd56..d66deb6f1 100644 --- a/apple/OmnivoreKit/Sources/Views/Images/Images.swift +++ b/apple/OmnivoreKit/Sources/Views/Images/Images.swift @@ -11,6 +11,7 @@ public extension Image { static var tabLibrary: Image { Image("_tab_library", bundle: .module).renderingMode(.template) } static var tabSearch: Image { Image("_tab_search", bundle: .module).renderingMode(.template) } static var tabHighlights: Image { Image("_tab_highlights", bundle: .module).renderingMode(.template) } + static var tabProfile: Image { Image("_tab_profile", bundle: .module).renderingMode(.template) } static var pinRotated: Image { Image("pin-rotated", bundle: .module) } diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_profile.imageset/Contents.json b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_profile.imageset/Contents.json new file mode 100644 index 000000000..960bd3e1c --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_profile.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame.svg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_profile.imageset/Frame.svg b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_profile.imageset/Frame.svg new file mode 100644 index 000000000..d48db9f42 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/_tab_profile.imageset/Frame.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + From 1bff4cbd184a25f795d088fe74ce57da10dd0757 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 30 Nov 2023 17:58:13 +0800 Subject: [PATCH 03/35] post rebase fix-ups --- ...Fetcher.swift => LibraryItemFetcher.swift} | 27 +++--------- .../App/Views/Home/FetcherFilterState.swift | 29 +++++++++++++ .../App/Views/Home/HomeFeedViewIOS.swift | 18 +++----- .../App/Views/Home/HomeFeedViewModel.swift | 9 ---- .../Sources/App/Views/Home/HomeView.swift | 6 ++- .../App/Views/Home/LibraryItemFetcher.swift | 43 ------------------- .../App/Views/Home/LibraryListView.swift | 2 +- .../Sources/App/Views/LibraryTabView.swift | 42 ++++++++---------- .../Sources/Utils/UserDefaultKeys.swift | 4 +- 9 files changed, 65 insertions(+), 115 deletions(-) rename apple/OmnivoreKit/Sources/App/Views/Home/Components/{InboxFetcher.swift => LibraryItemFetcher.swift} (93%) create mode 100644 apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift delete mode 100644 apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemFetcher.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/InboxFetcher.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemFetcher.swift similarity index 93% rename from apple/OmnivoreKit/Sources/App/Views/Home/Components/InboxFetcher.swift rename to apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemFetcher.swift index 5394b2a8d..5b1471abc 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/InboxFetcher.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemFetcher.swift @@ -1,9 +1,3 @@ -// -// InboxFetcher.swift -// -// -// Created by Jackson Harper on 11/16/23. -// import Foundation @@ -14,8 +8,8 @@ import SwiftUI import Utils import Views -@MainActor final class InboxFetcher: NSObject, ObservableObject, LibraryItemFetcher { - var folder = "inbox" +@MainActor final class LibraryItemFetcher: NSObject, ObservableObject { + let folder = "inbox" @Published var items = [Models.LibraryItem]() var itemsPublisher: Published<[Models.LibraryItem]>.Publisher { $items } @@ -165,7 +159,7 @@ import Views var subPredicates = [NSPredicate]() let folderPredicate = NSPredicate( - format: "%K == %@", #keyPath(Models.LibraryItem.folder), folder + format: "%K == %@", #keyPath(Models.LibraryItem.folder), filterState.folder ) subPredicates.append(folderPredicate) @@ -220,22 +214,11 @@ import Views setItems(dataService.viewContext, fetchedResultsController.fetchedObjects ?? []) } - private func queryContainsFilter(_ filterState: FetcherFilterState) -> Bool { - if filterState.searchTerm.contains("in:inbox") || - filterState.searchTerm.contains("in:all") || - filterState.searchTerm.contains("in:archive") - { - return true - } - - return false - } - private func searchQuery(_ filterState: FetcherFilterState) -> String { let sort = LinkedItemSort(rawValue: filterState.appliedSort) ?? .newest var query = sort.queryString - if !queryContainsFilter(filterState), let queryString = filterState.appliedFilter?.filter { + if let queryString = filterState.appliedFilter?.filter { query = "\(queryString) \(sort.queryString)" } @@ -269,7 +252,7 @@ import Views } } -extension InboxFetcher: NSFetchedResultsControllerDelegate { +extension LibraryItemFetcher: NSFetchedResultsControllerDelegate { func controllerDidChangeContent(_ controller: NSFetchedResultsController) { setItems(controller.managedObjectContext, controller.fetchedObjects as? [Models.LibraryItem] ?? []) } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift b/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift new file mode 100644 index 000000000..15c1d8d0c --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift @@ -0,0 +1,29 @@ +import Foundation +import Models +import Services +import SwiftUI +import Utils + +@MainActor +class FetcherFilterState: ObservableObject { + let folder: String + + @Published var searchTerm = "" + @Published var selectedLabels = [LinkedItemLabel]() + @Published var negatedLabels = [LinkedItemLabel]() + + @Published var appliedSort = LinkedItemSort.newest.rawValue + + @Published var appliedFilter: InternalFilter? { + didSet { + let newValue = appliedFilter?.name.lowercased() + UserDefaults.standard.setValue(newValue, forKey: "lastSelected-\(folder)-filter") + } + } + + init(folder: String) { + self.folder = folder + let newValue = appliedFilter?.name.lowercased() + let appliedFilterKey = UserDefaults.standard.string(forKey: "lastSelected-\(folder)-filter") + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 86f294e6f..599322e89 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -43,10 +43,7 @@ struct AnimatingCellHeight: AnimatableModifier { @ObservedObject var viewModel: HomeFeedViewModel @State private var selection = Set() - @ObservedObject var filterState = FetcherFilterState( - appliedFilterName: UserDefaults.standard.string(forKey: UserDefaultKey.lastSelectedLinkedItemFilter.rawValue) ?? - LinkedItemFilter.inbox.rawValue - ) + @ObservedObject var filterState: FetcherFilterState func loadItems(isRefresh: Bool) { Task { await viewModel.loadItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh) } @@ -58,10 +55,8 @@ struct AnimatingCellHeight: AnimatableModifier { viewModel.fetcher.items.count > 0 && filterState.searchTerm.isEmpty && filterState.selectedLabels.isEmpty && - filterState.negatedLabels.isEmpty - // MERGE TODO - // && -// viewModel.appliedFilterName == "inbox" + filterState.negatedLabels.isEmpty && + filterState.appliedFilter?.name == "inbox" } var body: some View { @@ -95,11 +90,8 @@ struct AnimatingCellHeight: AnimatableModifier { .onChange(of: filterState.appliedSort) { _ in loadItems(isRefresh: true) } - .sheet(item: $viewModel.itemUnderLabelEdit) { _ in - NavigationView { - BriefingView() - } - // ApplyLabelsView(mode: .item(item), onSave: nil) + .sheet(item: $viewModel.itemUnderLabelEdit) { item in + ApplyLabelsView(mode: .item(item), onSave: nil) } .sheet(item: $viewModel.itemUnderTitleEdit) { item in LinkedItemMetadataEditView(item: item) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index a0dd184b2..576e90994 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -37,15 +37,6 @@ import Views @Published var filters = [InternalFilter]() - var cursor: String? - - // These are used to make sure we handle search result - // responses in the right order - var searchIdx = 0 - var receivedIdx = 0 - - var syncCursor: String? - @AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false @AppStorage(UserDefaultKey.lastSelectedFeaturedItemFilter.rawValue) var featureFilter = FeaturedItemFilter.continueReading.rawValue diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeView.swift index 5fa1372c1..f3eb86b1c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeView.swift @@ -6,13 +6,17 @@ import Views struct HomeView: View { @State private var viewModel: HomeFeedViewModel + let inboxFilterState = FetcherFilterState( + folder: "inbox" + ) + init(viewModel: HomeFeedViewModel) { self.viewModel = viewModel } var body: some View { #if os(iOS) - HomeFeedContainerView(viewModel: viewModel) + HomeFeedContainerView(viewModel: viewModel, filterState: inboxFilterState) #elseif os(macOS) HomeFeedView(viewModel: viewModel) .frame(minWidth: 320) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemFetcher.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemFetcher.swift deleted file mode 100644 index 44ea54c1e..000000000 --- a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryItemFetcher.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// File.swift -// -// -// Created by Jackson Harper on 11/16/23. -// - -import Foundation -import Models -import Services -import SwiftUI -import Utils - -@MainActor -class FetcherFilterState: ObservableObject { - @Published var searchTerm = "" - @Published var selectedLabels = [LinkedItemLabel]() - @Published var negatedLabels = [LinkedItemLabel]() - - @Published var appliedSort = LinkedItemSort.newest.rawValue - - @AppStorage(UserDefaultKey.lastSelectedLinkedItemFilter.rawValue) var appliedFilterName = "inbox" - @Published var appliedFilter: InternalFilter? { - didSet { - appliedFilterName = appliedFilter?.name.lowercased() ?? "inbox" - } - } - - init(appliedFilterName: String) { - self.appliedFilterName = appliedFilterName - } -} - -@MainActor -protocol LibraryItemFetcher { - var folder: String { get } - - var items: [Models.LibraryItem] { get } - var itemsPublisher: Published<[Models.LibraryItem]>.Publisher { get } - - func loadItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async - func loadMoreItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async -} diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift index 43c9da2f8..c28104145 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift @@ -11,7 +11,7 @@ import SwiftUI struct LibraryListView: View { @StateObject private var libraryViewModel = HomeFeedViewModel( - fetcher: InboxFetcher(), + fetcher: LibraryItemFetcher(), listConfig: LibraryListConfig( hasFeatureCards: true, leadingSwipeActions: [.pin], diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index 6af409c87..b2e8ce2e1 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -10,20 +10,21 @@ import Models import PopupView import Services import SwiftUI +import Utils import Views @MainActor struct LibraryTabView: View { @EnvironmentObject var dataService: DataService + @AppStorage(UserDefaultKey.lastSelectedTabItem.rawValue) var selectedTab = "inbox" @MainActor public init() { UITabBar.appearance().isHidden = true - UITabBar.appearance().backgroundColor = UIColor(Color.themeTabBarColor) } @StateObject private var followingViewModel = HomeFeedViewModel( - fetcher: InboxFetcher(), + fetcher: LibraryItemFetcher(), listConfig: LibraryListConfig( hasFeatureCards: false, leadingSwipeActions: [.moveToInbox], @@ -33,7 +34,7 @@ struct LibraryTabView: View { ) @StateObject private var libraryViewModel = HomeFeedViewModel( - fetcher: InboxFetcher(), + fetcher: LibraryItemFetcher(), listConfig: LibraryListConfig( hasFeatureCards: true, leadingSwipeActions: [.pin], @@ -42,38 +43,29 @@ struct LibraryTabView: View { ) ) - @StateObject private var highlightsViewModel = HomeFeedViewModel( - fetcher: InboxFetcher(), - listConfig: LibraryListConfig( - hasFeatureCards: true, - leadingSwipeActions: [.pin], - trailingSwipeActions: [.archive, .delete], - cardStyle: .highlights - ) - ) - - @State var selectedTab = "following" - var body: some View { VStack(spacing: 0) { TabView(selection: $selectedTab) { NavigationView { - HomeView(viewModel: followingViewModel) - .navigationViewStyle(.stack) - } - .tag("following") + HomeFeedContainerView( + viewModel: followingViewModel, + filterState: FetcherFilterState(folder: "following") + ) + .navigationViewStyle(.stack) + }.tag("following") NavigationView { - HomeView(viewModel: libraryViewModel) - .navigationViewStyle(.stack) - } - .tag("inbox") + HomeFeedContainerView( + viewModel: libraryViewModel, + filterState: FetcherFilterState(folder: "inbox") + ) + .navigationViewStyle(.stack) + }.tag("inbox") NavigationView { ProfileView() .navigationViewStyle(.stack) - } - .tag("profile") + }.tag("profile") } CustomTabBar(selectedTab: $selectedTab) } diff --git a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift index cc5734cc6..25959090d 100644 --- a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift +++ b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift @@ -11,7 +11,9 @@ public enum UserDefaultKey: String { case userHasDeniedPushPrimer case firebasePushToken case homeFeedlayoutPreference - case lastSelectedLinkedItemFilter + case lastSelectedTabItem + case lastSelectedInboxFilter + case lastSelectedFollowingFilter case lastSelectedFeaturedItemFilter case lastUsedAppVersion case lastUsedAppBuildNumber From 26d6702c6f2a1a5367950d24638bb7b8be21d113 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 30 Nov 2023 19:07:30 +0800 Subject: [PATCH 04/35] Update filters --- .../App/Views/Home/FetcherFilterState.swift | 10 +- .../App/Views/Home/HomeFeedViewIOS.swift | 90 ++-- .../App/Views/Home/HomeFeedViewModel.swift | 71 +-- .../Sources/App/Views/Home/HomeView.swift | 6 +- .../App/Views/Home/LibraryListView.swift | 1 + .../Sources/App/Views/LibraryTabView.swift | 16 +- .../Services/DataService/GQLSchema.swift | 405 +++++++++++++++--- .../Selections/FilterSelection.swift | 1 + .../InternalModels/InternalFilter.swift | 44 +- 9 files changed, 488 insertions(+), 156 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift b/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift index 15c1d8d0c..4ed322200 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift @@ -12,18 +12,10 @@ class FetcherFilterState: ObservableObject { @Published var selectedLabels = [LinkedItemLabel]() @Published var negatedLabels = [LinkedItemLabel]() + @Published var appliedFilter: InternalFilter? @Published var appliedSort = LinkedItemSort.newest.rawValue - @Published var appliedFilter: InternalFilter? { - didSet { - let newValue = appliedFilter?.name.lowercased() - UserDefaults.standard.setValue(newValue, forKey: "lastSelected-\(folder)-filter") - } - } - init(folder: String) { self.folder = folder - let newValue = appliedFilter?.name.lowercased() - let appliedFilterKey = UserDefaults.standard.string(forKey: "lastSelected-\(folder)-filter") } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 599322e89..bfcdd01bb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -43,20 +43,19 @@ struct AnimatingCellHeight: AnimatableModifier { @ObservedObject var viewModel: HomeFeedViewModel @State private var selection = Set() - @ObservedObject var filterState: FetcherFilterState func loadItems(isRefresh: Bool) { - Task { await viewModel.loadItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh) } + Task { await viewModel.loadItems(dataService: dataService, isRefresh: isRefresh) } } var showFeatureCards: Bool { viewModel.listConfig.hasFeatureCards && !viewModel.hideFeatureSection && viewModel.fetcher.items.count > 0 && - filterState.searchTerm.isEmpty && - filterState.selectedLabels.isEmpty && - filterState.negatedLabels.isEmpty && - filterState.appliedFilter?.name == "inbox" + viewModel.filterState.searchTerm.isEmpty && + viewModel.filterState.selectedLabels.isEmpty && + viewModel.filterState.negatedLabels.isEmpty && + viewModel.filterState.appliedFilter?.name == "inbox" } var body: some View { @@ -67,27 +66,26 @@ struct AnimatingCellHeight: AnimatableModifier { isEditMode: $isEditMode, selection: $selection, viewModel: viewModel, - filterState: filterState, showFeatureCards: showFeatureCards ) .refreshable { loadItems(isRefresh: true) } - .onChange(of: filterState.searchTerm) { _ in + .onChange(of: viewModel.filterState.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without loadItems(isRefresh: true) } - .onChange(of: filterState.selectedLabels) { _ in + .onChange(of: viewModel.filterState.selectedLabels) { _ in loadItems(isRefresh: true) } - .onChange(of: filterState.negatedLabels) { _ in + .onChange(of: viewModel.filterState.negatedLabels) { _ in loadItems(isRefresh: true) } - .onChange(of: filterState.appliedFilter) { _ in + .onChange(of: viewModel.filterState.appliedFilter) { _ in loadItems(isRefresh: true) } - .onChange(of: filterState.appliedSort) { _ in + .onChange(of: viewModel.filterState.appliedSort) { _ in loadItems(isRefresh: true) } .sheet(item: $viewModel.itemUnderLabelEdit) { item in @@ -127,10 +125,10 @@ struct AnimatingCellHeight: AnimatableModifier { if let deepLink = DeepLink.make(from: url) { switch deepLink { case let .search(query): - filterState.searchTerm = query + viewModel.filterState.searchTerm = query case let .savedSearch(named): if let filter = viewModel.findFilter(dataService, named: named) { - filterState.appliedFilter = filter + viewModel.filterState.appliedFilter = filter } case let .webAppLinkRequest(requestID): DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { @@ -158,6 +156,7 @@ struct AnimatingCellHeight: AnimatableModifier { if viewModel.fetcher.items.isEmpty { loadItems(isRefresh: false) } + await viewModel.loadFilters(dataService: dataService, filterState: viewModel.filterState) } .environment(\.editMode, self.$isEditMode) } @@ -167,7 +166,7 @@ struct AnimatingCellHeight: AnimatableModifier { ToolbarItem(placement: .barLeading) { VStack(alignment: .leading) { let showDate = isListScrolled && !listTitle.isEmpty - if let title = filterState.appliedFilter?.name { + if let title = viewModel.filterState.appliedFilter?.name { Text(title) .font(Font.system(size: showDate ? 10 : 18, weight: .semibold)) if showDate, prefersListLayout, isListScrolled || !showFeatureCards { @@ -264,7 +263,6 @@ struct AnimatingCellHeight: AnimatableModifier { @Binding var isEditMode: EditMode @Binding var selection: Set @ObservedObject var viewModel: HomeFeedViewModel - @ObservedObject var filterState: FetcherFilterState let showFeatureCards: Bool @@ -294,23 +292,21 @@ struct AnimatingCellHeight: AnimatableModifier { isEditMode: $isEditMode, selection: $selection, viewModel: viewModel, - filterState: filterState, showFeatureCards: showFeatureCards ) } else { HomeFeedGridView( viewModel: viewModel, - filterState: filterState, isListScrolled: $isListScrolled ) } }.sheet(isPresented: $viewModel.showLabelsSheet) { FilterByLabelsView( - initiallySelected: filterState.selectedLabels, - initiallyNegated: filterState.negatedLabels + initiallySelected: viewModel.filterState.selectedLabels, + initiallyNegated: viewModel.filterState.negatedLabels ) { - self.filterState.selectedLabels = $0 - self.filterState.negatedLabels = $1 + viewModel.filterState.selectedLabels = $0 + viewModel.filterState.negatedLabels = $1 } } .popup(isPresented: $viewModel.showSnackbar) { @@ -351,7 +347,6 @@ struct AnimatingCellHeight: AnimatableModifier { @Binding var selection: Set @ObservedObject var viewModel: HomeFeedViewModel - @ObservedObject var filterState: FetcherFilterState let showFeatureCards: Bool @@ -359,20 +354,20 @@ struct AnimatingCellHeight: AnimatableModifier { GeometryReader { reader in ScrollView(.horizontal, showsIndicators: false) { HStack { - if filterState.searchTerm.count > 0 { - TextChipButton.makeSearchFilterButton(title: filterState.searchTerm) { - filterState.searchTerm = "" + if viewModel.filterState.searchTerm.count > 0 { + TextChipButton.makeSearchFilterButton(title: viewModel.filterState.searchTerm) { + viewModel.filterState.searchTerm = "" }.frame(maxWidth: reader.size.width * 0.66) } else { Menu( content: { ForEach(viewModel.filters) { filter in - Button(filter.name, action: { filterState.appliedFilter = filter }) + Button(filter.name, action: { viewModel.filterState.appliedFilter = filter }) } }, label: { TextChipButton.makeMenuButton( - title: filterState.appliedFilter?.name ?? "-", + title: viewModel.filterState.appliedFilter?.name ?? "-", color: .systemGray6 ) } @@ -381,25 +376,25 @@ struct AnimatingCellHeight: AnimatableModifier { Menu( content: { ForEach(LinkedItemSort.allCases, id: \.self) { sort in - Button(sort.displayName, action: { filterState.appliedSort = sort.rawValue }) + Button(sort.displayName, action: { viewModel.filterState.appliedSort = sort.rawValue }) } }, label: { TextChipButton.makeMenuButton( - title: LinkedItemSort(rawValue: filterState.appliedSort)?.displayName ?? "Sort", + title: LinkedItemSort(rawValue: viewModel.filterState.appliedSort)?.displayName ?? "Sort", color: .systemGray6 ) } ) TextChipButton.makeAddLabelButton(color: .systemGray6, onTap: { viewModel.showLabelsSheet = true }) - ForEach(filterState.selectedLabels, id: \.self) { label in + ForEach(viewModel.filterState.selectedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: false) { - filterState.selectedLabels.removeAll { $0.id == label.id } + viewModel.filterState.selectedLabels.removeAll { $0.id == label.id } } } - ForEach(filterState.negatedLabels, id: \.self) { label in + ForEach(viewModel.filterState.negatedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: true) { - filterState.negatedLabels.removeAll { $0.id == label.id } + viewModel.filterState.negatedLabels.removeAll { $0.id == label.id } } } Spacer() @@ -569,7 +564,7 @@ struct AnimatingCellHeight: AnimatableModifier { .listRowSeparator(.hidden, edges: .all) .listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 0, trailing: horizontalInset)) - if let appliedFilter = filterState.appliedFilter, + if let appliedFilter = viewModel.filterState.appliedFilter, networkMonitor.status == .disconnected, !appliedFilter.allowLocalFetch { @@ -706,7 +701,6 @@ struct AnimatingCellHeight: AnimatableModifier { @State var isContextMenuOpen = false @ObservedObject var viewModel: HomeFeedViewModel - @ObservedObject var filterState: FetcherFilterState @Binding var isListScrolled: Bool @@ -726,27 +720,27 @@ struct AnimatingCellHeight: AnimatableModifier { } func loadItems(isRefresh: Bool) { - Task { await viewModel.loadItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh) } + Task { await viewModel.loadItems(dataService: dataService, isRefresh: isRefresh) } } var filtersHeader: some View { GeometryReader { reader in ScrollView(.horizontal, showsIndicators: false) { HStack { - if filterState.searchTerm.count > 0 { - TextChipButton.makeSearchFilterButton(title: filterState.searchTerm) { - filterState.searchTerm = "" + if viewModel.filterState.searchTerm.count > 0 { + TextChipButton.makeSearchFilterButton(title: viewModel.filterState.searchTerm) { + viewModel.filterState.searchTerm = "" }.frame(maxWidth: reader.size.width * 0.66) } else { Menu( content: { ForEach(viewModel.filters, id: \.self) { filter in - Button(filter.name, action: { filterState.appliedFilter = filter }) + Button(filter.name, action: { viewModel.filterState.appliedFilter = filter }) } }, label: { TextChipButton.makeMenuButton( - title: filterState.appliedFilter?.name ?? "-", + title: viewModel.filterState.appliedFilter?.name ?? "-", color: .systemGray6 ) } @@ -755,25 +749,25 @@ struct AnimatingCellHeight: AnimatableModifier { Menu( content: { ForEach(LinkedItemSort.allCases, id: \.self) { sort in - Button(sort.displayName, action: { filterState.appliedSort = sort.rawValue }) + Button(sort.displayName, action: { viewModel.filterState.appliedSort = sort.rawValue }) } }, label: { TextChipButton.makeMenuButton( - title: LinkedItemSort(rawValue: filterState.appliedSort)?.displayName ?? "Sort", + title: LinkedItemSort(rawValue: viewModel.filterState.appliedSort)?.displayName ?? "Sort", color: .systemGray6 ) } ) TextChipButton.makeAddLabelButton(color: .systemGray6, onTap: { viewModel.showLabelsSheet = true }) - ForEach(filterState.selectedLabels, id: \.self) { label in + ForEach(viewModel.filterState.selectedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: false) { - filterState.selectedLabels.removeAll { $0.id == label.id } + viewModel.filterState.selectedLabels.removeAll { $0.id == label.id } } } - ForEach(filterState.negatedLabels, id: \.self) { label in + ForEach(viewModel.filterState.negatedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: true) { - filterState.negatedLabels.removeAll { $0.id == label.id } + viewModel.filterState.negatedLabels.removeAll { $0.id == label.id } } } Spacer() diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 576e90994..a036cbfa0 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -37,14 +37,17 @@ import Views @Published var filters = [InternalFilter]() + @ObservedObject var filterState: FetcherFilterState + @AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false @AppStorage(UserDefaultKey.lastSelectedFeaturedItemFilter.rawValue) var featureFilter = FeaturedItemFilter.continueReading.rawValue let fetcher: LibraryItemFetcher - init(fetcher: LibraryItemFetcher, listConfig: LibraryListConfig) { + init(fetcher: LibraryItemFetcher, filterState: FetcherFilterState, listConfig: LibraryListConfig) { self.fetcher = fetcher self.listConfig = listConfig + self.filterState = filterState super.init() } @@ -64,7 +67,32 @@ import Views } } - func itemAppeared(item: Models.LibraryItem, dataService _: DataService) async { + func loadFilters(dataService: DataService, filterState: FetcherFilterState) async { + switch filterState.folder { + case "following": + updateFilters(filterState: filterState, newFilters: InternalFilter.DefaultFollowingFilters) + default: + var hasLocalResults = false + let fetchRequest: NSFetchRequest = Filter.fetchRequest() + + // Load from disk + if let results = try? dataService.viewContext.fetch(fetchRequest) { + hasLocalResults = true + updateFilters(filterState: filterState, newFilters: InternalFilter.make(from: results)) + } + + let hasResults = hasLocalResults + Task.detached { + if let downloadedFilters = try? await dataService.filters() { + await self.updateFilters(filterState: filterState, newFilters: downloadedFilters) + } else if !hasResults { + await self.updateFilters(filterState: filterState, newFilters: InternalFilter.DefaultInboxFilters) + } + } + } + } + + func itemAppeared(item: Models.LibraryItem, dataService: DataService) async { if isLoading { return } let itemIndex = fetcher.items.firstIndex(where: { $0.id == item.id }) let thresholdIndex = fetcher.items.index(fetcher.items.endIndex, offsetBy: -5) @@ -72,9 +100,9 @@ import Views // Check if user has scrolled to the last five items in the list // Make sure we aren't currently loading though, as this would get triggered when the first set // of items are presented to the user. -// if let itemIndex = itemIndex, itemIndex > thresholdIndex { -// await loadMoreItems(dataService: dataService, isRefresh: false) -// } + if let itemIndex = itemIndex, itemIndex > thresholdIndex { + await loadMoreItems(dataService: dataService, filterState: filterState, isRefresh: false) + } } func pushFeedItem(item _: Models.LibraryItem) { @@ -98,38 +126,23 @@ import Views } } - func loadFilters(dataService: DataService) async { - var hasLocalResults = false - let fetchRequest: NSFetchRequest = Filter.fetchRequest() + func updateFilters(filterState: FetcherFilterState, newFilters: [InternalFilter]) { + let appliedFilterName = UserDefaults.standard.string(forKey: "lastSelected-\(filterState.folder)-filter") ?? filterState.folder - // Load from disk - if let results = try? dataService.viewContext.fetch(fetchRequest) { - hasLocalResults = true - updateFilters(newFilters: InternalFilter.make(from: results)) - } + filters = newFilters + .filter { $0.folder == filterState.folder } + .sorted(by: { $0.position < $1.position }) + + [InternalFilter.DeletedFilter, InternalFilter.DownloadedFilter] - let hasResults = hasLocalResults - Task.detached { - if let downloadedFilters = try? await dataService.filters() { - await self.updateFilters(newFilters: downloadedFilters) - } else if !hasResults { - await self.updateFilters(newFilters: InternalFilter.DefaultFilters) - } + if let newFilter = filters.first(where: { $0.name.lowercased() == appliedFilterName }), newFilter.id != filterState.appliedFilter?.id { + filterState.appliedFilter = newFilter } } - func updateFilters(newFilters _: [InternalFilter]) { -// filters = newFilters.sorted(by: { $0.position < $1.position }) + [InternalFilter.DeletedFilter, InternalFilter.DownloadedFilter] -// if let newFilter = filters.first(where: { $0.name.lowercased() == appliedFilterName }), newFilter.id != appliedFilter?.id { -// appliedFilter = newFilter -// } - } - - func loadItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { + func loadItems(dataService: DataService, isRefresh: Bool) async { isLoading = true showLoadingBar = true - // group.addTask { await self.loadFilters(dataService: dataService) } await fetcher.loadItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh) updateFeatureFilter(context: dataService.viewContext, filter: FeaturedItemFilter(rawValue: featureFilter)) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeView.swift index f3eb86b1c..5fa1372c1 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeView.swift @@ -6,17 +6,13 @@ import Views struct HomeView: View { @State private var viewModel: HomeFeedViewModel - let inboxFilterState = FetcherFilterState( - folder: "inbox" - ) - init(viewModel: HomeFeedViewModel) { self.viewModel = viewModel } var body: some View { #if os(iOS) - HomeFeedContainerView(viewModel: viewModel, filterState: inboxFilterState) + HomeFeedContainerView(viewModel: viewModel) #elseif os(macOS) HomeFeedView(viewModel: viewModel) .frame(minWidth: 320) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift index c28104145..29811b6f8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift @@ -12,6 +12,7 @@ import SwiftUI struct LibraryListView: View { @StateObject private var libraryViewModel = HomeFeedViewModel( fetcher: LibraryItemFetcher(), + filterState: FetcherFilterState(folder: "inbox"), listConfig: LibraryListConfig( hasFeatureCards: true, leadingSwipeActions: [.pin], diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index b2e8ce2e1..130933b9e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -25,6 +25,7 @@ struct LibraryTabView: View { @StateObject private var followingViewModel = HomeFeedViewModel( fetcher: LibraryItemFetcher(), + filterState: FetcherFilterState(folder: "following"), listConfig: LibraryListConfig( hasFeatureCards: false, leadingSwipeActions: [.moveToInbox], @@ -35,6 +36,7 @@ struct LibraryTabView: View { @StateObject private var libraryViewModel = HomeFeedViewModel( fetcher: LibraryItemFetcher(), + filterState: FetcherFilterState(folder: "inbox"), listConfig: LibraryListConfig( hasFeatureCards: true, leadingSwipeActions: [.pin], @@ -47,19 +49,13 @@ struct LibraryTabView: View { VStack(spacing: 0) { TabView(selection: $selectedTab) { NavigationView { - HomeFeedContainerView( - viewModel: followingViewModel, - filterState: FetcherFilterState(folder: "following") - ) - .navigationViewStyle(.stack) + HomeFeedContainerView(viewModel: followingViewModel) + .navigationViewStyle(.stack) }.tag("following") NavigationView { - HomeFeedContainerView( - viewModel: libraryViewModel, - filterState: FetcherFilterState(folder: "inbox") - ) - .navigationViewStyle(.stack) + HomeFeedContainerView(viewModel: libraryViewModel) + .navigationViewStyle(.stack) }.tag("inbox") NavigationView { diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index f3cf55737..1a01c1269 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -5643,6 +5643,7 @@ extension Objects { let image: [String: String] let publishedAt: [String: DateTime] let title: [String: String] + let type: [String: String] let updatedAt: [String: DateTime] let url: [String: String] @@ -5692,6 +5693,10 @@ extension Objects.Feed: Decodable { if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "type": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "updatedAt": if let value = try container.decode(DateTime?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -5717,6 +5722,7 @@ extension Objects.Feed: Decodable { image = map["image"] publishedAt = map["publishedAt"] title = map["title"] + type = map["type"] updatedAt = map["updatedAt"] url = map["url"] } @@ -5738,7 +5744,7 @@ extension Fields where TypeLock == Objects.Feed { } } - func createdAt() throws -> DateTime { + func createdAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "createdAt", arguments: [] @@ -5747,12 +5753,9 @@ extension Fields where TypeLock == Objects.Feed { switch response { case let .decoding(data): - if let data = data.createdAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.createdAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } @@ -5771,7 +5774,7 @@ extension Fields where TypeLock == Objects.Feed { } } - func id() throws -> String { + func id() throws -> String? { let field = GraphQLField.leaf( name: "id", arguments: [] @@ -5780,12 +5783,9 @@ extension Fields where TypeLock == Objects.Feed { switch response { case let .decoding(data): - if let data = data.id[field.alias!] { - return data - } - throw HttpError.badpayload + return data.id[field.alias!] case .mocking: - return String.mockValue + return nil } } @@ -5837,7 +5837,22 @@ extension Fields where TypeLock == Objects.Feed { } } - func updatedAt() throws -> DateTime { + func type() throws -> String? { + let field = GraphQLField.leaf( + name: "type", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + return data.type[field.alias!] + case .mocking: + return nil + } + } + + func updatedAt() throws -> DateTime? { let field = GraphQLField.leaf( name: "updatedAt", arguments: [] @@ -5846,12 +5861,9 @@ extension Fields where TypeLock == Objects.Feed { switch response { case let .decoding(data): - if let data = data.updatedAt[field.alias!] { - return data - } - throw HttpError.badpayload + return data.updatedAt[field.alias!] case .mocking: - return DateTime.mockValue + return nil } } @@ -6643,11 +6655,11 @@ extension Selection where TypeLock == Never, Type == Never { extension Objects { struct Filter { let __typename: TypeName = .filter - let category: [String: String] let createdAt: [String: DateTime] let defaultFilter: [String: Bool] let description: [String: String] let filter: [String: String] + let folder: [String: String] let id: [String: String] let name: [String: String] let position: [String: Int] @@ -6672,10 +6684,6 @@ extension Objects.Filter: Decodable { let field = GraphQLField.getFieldNameFromAlias(alias) switch field { - case "category": - if let value = try container.decode(String?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "createdAt": if let value = try container.decode(DateTime?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -6692,6 +6700,10 @@ extension Objects.Filter: Decodable { if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "folder": + if let value = try container.decode(String?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "id": if let value = try container.decode(String?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -6722,11 +6734,11 @@ extension Objects.Filter: Decodable { } } - category = map["category"] createdAt = map["createdAt"] defaultFilter = map["defaultFilter"] description = map["description"] filter = map["filter"] + folder = map["folder"] id = map["id"] name = map["name"] position = map["position"] @@ -6736,24 +6748,6 @@ extension Objects.Filter: Decodable { } extension Fields where TypeLock == Objects.Filter { - func category() throws -> String { - let field = GraphQLField.leaf( - name: "category", - arguments: [] - ) - select(field) - - switch response { - case let .decoding(data): - if let data = data.category[field.alias!] { - return data - } - throw HttpError.badpayload - case .mocking: - return String.mockValue - } - } - func createdAt() throws -> DateTime { let field = GraphQLField.leaf( name: "createdAt", @@ -6820,6 +6814,24 @@ extension Fields where TypeLock == Objects.Filter { } } + func folder() throws -> String { + let field = GraphQLField.leaf( + name: "folder", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.folder[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return String.mockValue + } + } + func id() throws -> String { let field = GraphQLField.leaf( name: "id", @@ -13695,6 +13707,7 @@ extension Objects { let recentEmails: [String: Unions.RecentEmailsResult] let recentSearches: [String: Unions.RecentSearchesResult] let rules: [String: Unions.RulesResult] + let scanFeeds: [String: Unions.ScanFeedsResult] let search: [String: Unions.SearchResult] let sendInstallInstructions: [String: Unions.SendInstallInstructionsResult] let subscriptions: [String: Unions.SubscriptionsResult] @@ -13788,6 +13801,10 @@ extension Objects.Query: Decodable { if let value = try container.decode(Unions.RulesResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "scanFeeds": + if let value = try container.decode(Unions.ScanFeedsResult?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } case "search": if let value = try container.decode(Unions.SearchResult?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) @@ -13854,6 +13871,7 @@ extension Objects.Query: Decodable { recentEmails = map["recentEmails"] recentSearches = map["recentSearches"] rules = map["rules"] + scanFeeds = map["scanFeeds"] search = map["search"] sendInstallInstructions = map["sendInstallInstructions"] subscriptions = map["subscriptions"] @@ -14165,6 +14183,25 @@ extension Fields where TypeLock == Objects.Query { } } + func scanFeeds(input: InputObjects.ScanFeedsInput, selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "scanFeeds", + arguments: [Argument(name: "input", type: "ScanFeedsInput!", value: input)], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.scanFeeds[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } + func search(after: OptionalArgument = .absent(), first: OptionalArgument = .absent(), format: OptionalArgument = .absent(), includeContent: OptionalArgument = .absent(), query: OptionalArgument = .absent(), selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "search", @@ -14241,10 +14278,10 @@ extension Fields where TypeLock == Objects.Query { } } - func updatesSince(after: OptionalArgument = .absent(), first: OptionalArgument = .absent(), since: DateTime, sort: OptionalArgument = .absent(), selection: Selection) throws -> Type { + func updatesSince(after: OptionalArgument = .absent(), first: OptionalArgument = .absent(), folder: OptionalArgument = .absent(), since: DateTime, sort: OptionalArgument = .absent(), selection: Selection) throws -> Type { let field = GraphQLField.composite( name: "updatesSince", - arguments: [Argument(name: "after", type: "String", value: after), Argument(name: "first", type: "Int", value: first), Argument(name: "since", type: "Date!", value: since), Argument(name: "sort", type: "SortParams", value: sort)], + arguments: [Argument(name: "after", type: "String", value: after), Argument(name: "first", type: "Int", value: first), Argument(name: "folder", type: "String", value: folder), Argument(name: "since", type: "Date!", value: since), Argument(name: "sort", type: "SortParams", value: sort)], selection: selection.selection ) select(field) @@ -17454,6 +17491,137 @@ extension Selection where TypeLock == Never, Type == Never { typealias SaveSuccess = Selection } +extension Objects { + struct ScanFeedsError { + let __typename: TypeName = .scanFeedsError + let errorCodes: [String: [Enums.ScanFeedsErrorCode]] + + enum TypeName: String, Codable { + case scanFeedsError = "ScanFeedsError" + } + } +} + +extension Objects.ScanFeedsError: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.ScanFeedsErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + errorCodes = map["errorCodes"] + } +} + +extension Fields where TypeLock == Objects.ScanFeedsError { + func errorCodes() throws -> [Enums.ScanFeedsErrorCode] { + let field = GraphQLField.leaf( + name: "errorCodes", + arguments: [] + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.errorCodes[field.alias!] { + return data + } + throw HttpError.badpayload + case .mocking: + return [] + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias ScanFeedsError = Selection +} + +extension Objects { + struct ScanFeedsSuccess { + let __typename: TypeName = .scanFeedsSuccess + let feeds: [String: [Objects.Feed]] + + enum TypeName: String, Codable { + case scanFeedsSuccess = "ScanFeedsSuccess" + } + } +} + +extension Objects.ScanFeedsSuccess: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "feeds": + if let value = try container.decode([Objects.Feed]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + feeds = map["feeds"] + } +} + +extension Fields where TypeLock == Objects.ScanFeedsSuccess { + func feeds(selection: Selection) throws -> Type { + let field = GraphQLField.composite( + name: "feeds", + arguments: [], + selection: selection.selection + ) + select(field) + + switch response { + case let .decoding(data): + if let data = data.feeds[field.alias!] { + return try selection.decode(data: data) + } + throw HttpError.badpayload + case .mocking: + return selection.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias ScanFeedsSuccess = Selection +} + extension Objects { struct SearchError { let __typename: TypeName = .searchError @@ -29856,6 +30024,80 @@ extension Selection where TypeLock == Never, Type == Never { typealias SaveResult = Selection } +extension Unions { + struct ScanFeedsResult { + let __typename: TypeName + let errorCodes: [String: [Enums.ScanFeedsErrorCode]] + let feeds: [String: [Objects.Feed]] + + enum TypeName: String, Codable { + case scanFeedsError = "ScanFeedsError" + case scanFeedsSuccess = "ScanFeedsSuccess" + } + } +} + +extension Unions.ScanFeedsResult: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DynamicCodingKeys.self) + + var map = HashMap() + for codingKey in container.allKeys { + if codingKey.isTypenameKey { continue } + + let alias = codingKey.stringValue + let field = GraphQLField.getFieldNameFromAlias(alias) + + switch field { + case "errorCodes": + if let value = try container.decode([Enums.ScanFeedsErrorCode]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + case "feeds": + if let value = try container.decode([Objects.Feed]?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } + default: + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unknown key \(field)." + ) + ) + } + } + + __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!) + + errorCodes = map["errorCodes"] + feeds = map["feeds"] + } +} + +extension Fields where TypeLock == Unions.ScanFeedsResult { + func on(scanFeedsError: Selection, scanFeedsSuccess: Selection) throws -> Type { + select([GraphQLField.fragment(type: "ScanFeedsError", selection: scanFeedsError.selection), GraphQLField.fragment(type: "ScanFeedsSuccess", selection: scanFeedsSuccess.selection)]) + + switch response { + case let .decoding(data): + switch data.__typename { + case .scanFeedsError: + let data = Objects.ScanFeedsError(errorCodes: data.errorCodes) + return try scanFeedsError.decode(data: data) + case .scanFeedsSuccess: + let data = Objects.ScanFeedsSuccess(feeds: data.feeds) + return try scanFeedsSuccess.decode(data: data) + } + case .mocking: + return scanFeedsError.mock() + } + } +} + +extension Selection where TypeLock == Never, Type == Never { + typealias ScanFeedsResult = Selection +} + extension Unions { struct SearchResult { let __typename: TypeName @@ -33434,6 +33676,22 @@ extension Enums { } } +extension Enums { + /// ScanFeedsErrorCode + enum ScanFeedsErrorCode: String, CaseIterable, Codable { + case badRequest = "BAD_REQUEST" + } +} + +extension Enums { + /// ScanFeedsType + enum ScanFeedsType: String, CaseIterable, Codable { + case html = "HTML" + + case opml = "OPML" + } +} + extension Enums { /// SearchErrorCode enum SearchErrorCode: String, CaseIterable, Codable { @@ -33997,6 +34255,8 @@ extension InputObjects { struct CreateArticleInput: Encodable, Hashable { var articleSavingRequestId: OptionalArgument = .absent() + var folder: OptionalArgument = .absent() + var labels: OptionalArgument<[InputObjects.CreateLabelInput]> = .absent() var preparedDocument: OptionalArgument = .absent() @@ -34014,6 +34274,7 @@ extension InputObjects { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) if articleSavingRequestId.hasValue { try container.encode(articleSavingRequestId, forKey: .articleSavingRequestId) } + if folder.hasValue { try container.encode(folder, forKey: .folder) } if labels.hasValue { try container.encode(labels, forKey: .labels) } if preparedDocument.hasValue { try container.encode(preparedDocument, forKey: .preparedDocument) } if skipParsing.hasValue { try container.encode(skipParsing, forKey: .skipParsing) } @@ -34025,6 +34286,7 @@ extension InputObjects { enum CodingKeys: String, CodingKey { case articleSavingRequestId + case folder case labels case preparedDocument case skipParsing @@ -34733,6 +34995,8 @@ extension InputObjects { struct SaveFileInput: Encodable, Hashable { var clientRequestId: String + var folder: OptionalArgument = .absent() + var labels: OptionalArgument<[InputObjects.CreateLabelInput]> = .absent() var source: String @@ -34746,6 +35010,7 @@ extension InputObjects { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(clientRequestId, forKey: .clientRequestId) + if folder.hasValue { try container.encode(folder, forKey: .folder) } if labels.hasValue { try container.encode(labels, forKey: .labels) } try container.encode(source, forKey: .source) if state.hasValue { try container.encode(state, forKey: .state) } @@ -34755,6 +35020,7 @@ extension InputObjects { enum CodingKeys: String, CodingKey { case clientRequestId + case folder case labels case source case state @@ -34766,29 +35032,29 @@ extension InputObjects { extension InputObjects { struct SaveFilterInput: Encodable, Hashable { - var category: OptionalArgument = .absent() - var description: OptionalArgument = .absent() var filter: String + var folder: OptionalArgument = .absent() + var name: String var position: OptionalArgument = .absent() func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - if category.hasValue { try container.encode(category, forKey: .category) } if description.hasValue { try container.encode(description, forKey: .description) } try container.encode(filter, forKey: .filter) + if folder.hasValue { try container.encode(folder, forKey: .folder) } try container.encode(name, forKey: .name) if position.hasValue { try container.encode(position, forKey: .position) } } enum CodingKeys: String, CodingKey { - case category case description case filter + case folder case name case position } @@ -34799,6 +35065,8 @@ extension InputObjects { struct SavePageInput: Encodable, Hashable { var clientRequestId: String + var folder: OptionalArgument = .absent() + var labels: OptionalArgument<[InputObjects.CreateLabelInput]> = .absent() var originalContent: String @@ -34822,6 +35090,7 @@ extension InputObjects { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(clientRequestId, forKey: .clientRequestId) + if folder.hasValue { try container.encode(folder, forKey: .folder) } if labels.hasValue { try container.encode(labels, forKey: .labels) } try container.encode(originalContent, forKey: .originalContent) if parseResult.hasValue { try container.encode(parseResult, forKey: .parseResult) } @@ -34836,6 +35105,7 @@ extension InputObjects { enum CodingKeys: String, CodingKey { case clientRequestId + case folder case labels case originalContent case parseResult @@ -34854,6 +35124,8 @@ extension InputObjects { struct SaveUrlInput: Encodable, Hashable { var clientRequestId: String + var folder: OptionalArgument = .absent() + var labels: OptionalArgument<[InputObjects.CreateLabelInput]> = .absent() var locale: OptionalArgument = .absent() @@ -34873,6 +35145,7 @@ extension InputObjects { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(clientRequestId, forKey: .clientRequestId) + if folder.hasValue { try container.encode(folder, forKey: .folder) } if labels.hasValue { try container.encode(labels, forKey: .labels) } if locale.hasValue { try container.encode(locale, forKey: .locale) } if publishedAt.hasValue { try container.encode(publishedAt, forKey: .publishedAt) } @@ -34885,6 +35158,7 @@ extension InputObjects { enum CodingKeys: String, CodingKey { case clientRequestId + case folder case labels case locale case publishedAt @@ -34897,6 +35171,29 @@ extension InputObjects { } } +extension InputObjects { + struct ScanFeedsInput: Encodable, Hashable { + var opml: OptionalArgument = .absent() + + var type: Enums.ScanFeedsType + + var url: OptionalArgument = .absent() + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + if opml.hasValue { try container.encode(opml, forKey: .opml) } + try container.encode(type, forKey: .type) + if url.hasValue { try container.encode(url, forKey: .url) } + } + + enum CodingKeys: String, CodingKey { + case opml + case type + case url + } + } +} + extension InputObjects { struct SetBookmarkArticleInput: Encodable, Hashable { var articleId: String @@ -34966,6 +35263,8 @@ extension InputObjects { var syncedAt: OptionalArgument = .absent() + var taskName: OptionalArgument = .absent() + var token: String var type: OptionalArgument = .absent() @@ -34977,6 +35276,7 @@ extension InputObjects { if importItemState.hasValue { try container.encode(importItemState, forKey: .importItemState) } try container.encode(name, forKey: .name) if syncedAt.hasValue { try container.encode(syncedAt, forKey: .syncedAt) } + if taskName.hasValue { try container.encode(taskName, forKey: .taskName) } try container.encode(token, forKey: .token) if type.hasValue { try container.encode(type, forKey: .type) } } @@ -34987,6 +35287,7 @@ extension InputObjects { case importItemState case name case syncedAt + case taskName case token case type } @@ -35277,12 +35578,12 @@ extension InputObjects { extension InputObjects { struct UpdateFilterInput: Encodable, Hashable { - var category: OptionalArgument = .absent() - var description: OptionalArgument = .absent() var filter: OptionalArgument = .absent() + var folder: OptionalArgument = .absent() + var id: String var name: OptionalArgument = .absent() @@ -35293,9 +35594,9 @@ extension InputObjects { func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - if category.hasValue { try container.encode(category, forKey: .category) } if description.hasValue { try container.encode(description, forKey: .description) } if filter.hasValue { try container.encode(filter, forKey: .filter) } + if folder.hasValue { try container.encode(folder, forKey: .folder) } try container.encode(id, forKey: .id) if name.hasValue { try container.encode(name, forKey: .name) } if position.hasValue { try container.encode(position, forKey: .position) } @@ -35303,9 +35604,9 @@ extension InputObjects { } enum CodingKeys: String, CodingKey { - case category case description case filter + case folder case id case name case position diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Selections/FilterSelection.swift b/apple/OmnivoreKit/Sources/Services/DataService/Selections/FilterSelection.swift index da0b723f6..4017d7276 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Selections/FilterSelection.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Selections/FilterSelection.swift @@ -6,6 +6,7 @@ let filterSelection = Selection.Filter { InternalFilter( id: try $0.id(), name: try $0.name(), + folder: try $0.folder(), filter: try $0.filter(), visible: try $0.visible() ?? true, position: try $0.position(), diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift index ee51fd0fb..e103e690f 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift @@ -5,6 +5,7 @@ import Models public struct InternalFilter: Encodable, Identifiable, Hashable { public let id: String public let name: String + public let folder: String public let filter: String public let visible: Bool public let position: Int @@ -14,6 +15,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { InternalFilter( id: "downloaded", name: "Downloaded", + folder: "inbox", filter: "", visible: true, position: -1, @@ -25,6 +27,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { InternalFilter( id: "deleted", name: "Deleted", + folder: "inbox", filter: "in:trash", visible: true, position: -1, @@ -32,11 +35,12 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { ) } - public static var DefaultFilters: [InternalFilter] { + public static var DefaultInboxFilters: [InternalFilter] { [ InternalFilter( id: "inbox", name: "Inbox", + folder: "inbox", filter: "", visible: true, position: 0, @@ -45,6 +49,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { InternalFilter( id: "non-feed-items", name: "Non-Feed Items", + folder: "inbox", filter: "", visible: true, position: 1, @@ -53,6 +58,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { InternalFilter( id: "newsletters", name: "Newsletters", + folder: "inbox", filter: "", visible: true, position: 2, @@ -61,6 +67,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { InternalFilter( id: "feeds", name: "Feeds", + folder: "inbox", filter: "", visible: true, position: 3, @@ -69,6 +76,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { InternalFilter( id: "archived", name: "Archived", + folder: "inbox", filter: "is:archived", visible: true, position: 4, @@ -77,6 +85,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { InternalFilter( id: "files", name: "Files", + folder: "inbox", filter: "type:file", visible: true, position: 5, @@ -85,6 +94,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { InternalFilter( id: "highlighted", name: "Highlights", + folder: "inbox", filter: "has:highlights", visible: true, position: 6, @@ -93,6 +103,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { InternalFilter( id: "all", name: "All", + folder: "inbox", filter: "in:all", visible: true, position: 7, @@ -101,6 +112,29 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { ] } + public static var DefaultFollowingFilters: [InternalFilter] { + [ + InternalFilter( + id: "following", + name: "Following", + folder: "following", + filter: "in:following", + visible: true, + position: 0, + defaultFilter: true + ), + InternalFilter( + id: "rss", + name: "RSS", + folder: "following", + filter: "in:following label:RSS", + visible: true, + position: 1, + defaultFilter: true + ) + ] + } + public var shouldRemoteSearch: Bool { id != "downloaded" } @@ -182,7 +216,9 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, inArchivePredicate]) case "Deleted": let deletedPredicate = NSPredicate( - format: "%K == %i", #keyPath(Models.LibraryItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue) + format: "%K == %i", + #keyPath(Models.LibraryItem.serverSyncStatus), + Int64(ServerSyncStatus.needsDeletion.rawValue) ) return NSCompoundPredicate(andPredicateWithSubpredicates: [deletedPredicate]) case "Files": @@ -227,6 +263,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { let newFilter = existing ?? Filter(entity: Filter.entity(), insertInto: context) newFilter.id = id newFilter.name = name + newFilter.folder = folder newFilter.filter = filter newFilter.visible = visible newFilter.position = Int64(position) @@ -238,11 +275,13 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { filters.compactMap { filter in if let id = filter.id, let name = filter.name, + let folder = filter.folder, let filterStr = filter.filter { return InternalFilter( id: id, name: name, + folder: folder, filter: filterStr, visible: filter.visible, position: Int(filter.position), @@ -264,7 +303,6 @@ public extension Filter { ) var filter: Filter? - context.performAndWait { filter = (try? context.fetch(fetchRequest))?.first } From 5f1332e2d77f188adaf235ae223a6f3c6b02477b Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 1 Dec 2023 12:02:34 +0800 Subject: [PATCH 05/35] Fix filters to navigation header --- .../App/Views/Home/FetcherFilterState.swift | 9 +- .../App/Views/Home/HomeFeedViewIOS.swift | 196 +++++++++++------- .../App/Views/Home/HomeFeedViewModel.swift | 2 +- .../App/Views/Profile/ProfileView.swift | 22 +- .../InternalModels/InternalFilter.swift | 9 - .../Sources/Views/Colors/Colors.swift | 1 + 6 files changed, 146 insertions(+), 93 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift b/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift index 4ed322200..277be132a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift @@ -11,10 +11,15 @@ class FetcherFilterState: ObservableObject { @Published var searchTerm = "" @Published var selectedLabels = [LinkedItemLabel]() @Published var negatedLabels = [LinkedItemLabel]() - - @Published var appliedFilter: InternalFilter? @Published var appliedSort = LinkedItemSort.newest.rawValue + @Published var appliedFilter: InternalFilter? { + didSet { + let filterKey = UserDefaults.standard.string(forKey: "lastSelected-\(folder)-filter") ?? folder + UserDefaults.standard.setValue(appliedFilter?.name, forKey: filterKey) + } + } + init(folder: String) { self.folder = folder } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index bfcdd01bb..252817b29 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -40,10 +40,14 @@ struct AnimatingCellHeight: AnimatableModifier { @AppStorage(UserDefaultKey.homeFeedlayoutPreference.rawValue) var prefersListLayout = true @AppStorage(UserDefaultKey.openAIPrimerDisplayed.rawValue) var openAIPrimerDisplayed = false - @ObservedObject var viewModel: HomeFeedViewModel + @StateObject var viewModel: HomeFeedViewModel @State private var selection = Set() + init(viewModel: HomeFeedViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + } + func loadItems(isRefresh: Bool) { Task { await viewModel.loadItems(dataService: dataService, isRefresh: isRefresh) } } @@ -54,8 +58,9 @@ struct AnimatingCellHeight: AnimatableModifier { viewModel.fetcher.items.count > 0 && viewModel.filterState.searchTerm.isEmpty && viewModel.filterState.selectedLabels.isEmpty && - viewModel.filterState.negatedLabels.isEmpty && - viewModel.filterState.appliedFilter?.name == "inbox" + viewModel.filterState.negatedLabels.isEmpty + /* && + viewModel.filterState.appliedFilter?.name == "inbox" */ } var body: some View { @@ -66,11 +71,15 @@ struct AnimatingCellHeight: AnimatableModifier { isEditMode: $isEditMode, selection: $selection, viewModel: viewModel, + filterState: viewModel.filterState, showFeatureCards: showFeatureCards ) .refreshable { loadItems(isRefresh: true) } + .onChange(of: viewModel.filterState.appliedFilter?.id) { _ in + loadItems(isRefresh: true) + } .onChange(of: viewModel.filterState.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without @@ -168,14 +177,15 @@ struct AnimatingCellHeight: AnimatableModifier { let showDate = isListScrolled && !listTitle.isEmpty if let title = viewModel.filterState.appliedFilter?.name { Text(title) - .font(Font.system(size: showDate ? 10 : 18, weight: .semibold)) + .font(Font.system(size: showDate ? 10 : 32, weight: .semibold)) if showDate, prefersListLayout, isListScrolled || !showFeatureCards { Text(listTitle) .font(Font.system(size: 15, weight: .regular)) .foregroundColor(Color.appGrayText) } } - }.frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxWidth: .infinity, alignment: .bottomLeading) } ToolbarItem(placement: .barTrailing) { Button("", action: {}) @@ -263,6 +273,7 @@ struct AnimatingCellHeight: AnimatableModifier { @Binding var isEditMode: EditMode @Binding var selection: Set @ObservedObject var viewModel: HomeFeedViewModel + @ObservedObject var filterState: FetcherFilterState let showFeatureCards: Bool @@ -292,6 +303,7 @@ struct AnimatingCellHeight: AnimatableModifier { isEditMode: $isEditMode, selection: $selection, viewModel: viewModel, + filterState: filterState, showFeatureCards: showFeatureCards ) } else { @@ -302,11 +314,11 @@ struct AnimatingCellHeight: AnimatableModifier { } }.sheet(isPresented: $viewModel.showLabelsSheet) { FilterByLabelsView( - initiallySelected: viewModel.filterState.selectedLabels, - initiallyNegated: viewModel.filterState.negatedLabels + initiallySelected: filterState.selectedLabels, + initiallyNegated: filterState.negatedLabels ) { - viewModel.filterState.selectedLabels = $0 - viewModel.filterState.negatedLabels = $1 + filterState.selectedLabels = $0 + filterState.negatedLabels = $1 } } .popup(isPresented: $viewModel.showSnackbar) { @@ -347,9 +359,29 @@ struct AnimatingCellHeight: AnimatableModifier { @Binding var selection: Set @ObservedObject var viewModel: HomeFeedViewModel + @ObservedObject var filterState: FetcherFilterState let showFeatureCards: Bool + init(listTitle: Binding, + isListScrolled: Binding, + prefersListLayout: Binding, + isEditMode: Binding, + selection: Binding>, + viewModel: HomeFeedViewModel, + filterState: FetcherFilterState, + showFeatureCards: Bool) + { + self._listTitle = listTitle + self._isListScrolled = isListScrolled + self._prefersListLayout = prefersListLayout + self._isEditMode = isEditMode + self._selection = selection + self.viewModel = viewModel + self.filterState = filterState + self.showFeatureCards = showFeatureCards + } + var filtersHeader: some View { GeometryReader { reader in ScrollView(.horizontal, showsIndicators: false) { @@ -362,7 +394,9 @@ struct AnimatingCellHeight: AnimatableModifier { Menu( content: { ForEach(viewModel.filters) { filter in - Button(filter.name, action: { viewModel.filterState.appliedFilter = filter }) + Button(filter.name, action: { + viewModel.filterState.appliedFilter = filter + }) } }, label: { @@ -399,10 +433,19 @@ struct AnimatingCellHeight: AnimatableModifier { } Spacer() } - .padding(0) } - .listRowSeparator(.hidden) } + .padding(.top, 0) + .padding(.bottom, 10) + .padding(.leading, 15) + .listRowSpacing(0) + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + .frame(maxWidth: .infinity, minHeight: 38) + .background(Color.systemBackground) + .overlay(Rectangle() + .padding(.leading, 15) + .frame(width: nil, height: 0.5, alignment: .bottom) + .foregroundColor(isListScrolled ? Color(hex: "#3D3D3D") : Color.systemBackground), alignment: .bottom) .dynamicTypeSize(.small ... .accessibility1) } @@ -515,6 +558,7 @@ struct AnimatingCellHeight: AnimatableModifier { @State var topItem: Models.LibraryItem? func setTopItem(_ item: Models.LibraryItem) { + print("setting top item: ", item) if let date = item.savedAt, let daysAgo = Calendar.current.dateComponents([.day], from: date, to: Date()).day { if daysAgo < 1 { let formatter = DateFormatter() @@ -560,79 +604,80 @@ struct AnimatingCellHeight: AnimatableModifier { } List(selection: $selection) { - filtersHeader - .listRowSeparator(.hidden, edges: .all) - .listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 0, trailing: horizontalInset)) + Section(content: { + if let appliedFilter = viewModel.filterState.appliedFilter, + networkMonitor.status == .disconnected, + !appliedFilter.allowLocalFetch + { + HStack { + Text("This search requires an internet connection.") + .padding() + .foregroundColor(Color.white) + .frame(maxWidth: .infinity, alignment: .center) + } + .background(Color.blue) + .frame(maxWidth: .infinity, alignment: .center) + .listRowSeparator(.hidden, edges: .all) + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + } else { + if showFeatureCards { + featureCard + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + .listRowSeparator(.hidden, edges: .all) + .modifier(AnimatingCellHeight(height: 190 + 13)) + .onDisappear { + withAnimation { + isListScrolled = true + } + } + .onAppear { + withAnimation { + isListScrolled = false + } + } + } - if let appliedFilter = viewModel.filterState.appliedFilter, - networkMonitor.status == .disconnected, - !appliedFilter.allowLocalFetch - { - HStack { - Text("This search requires an internet connection.") - .padding() - .foregroundColor(Color.white) - .frame(maxWidth: .infinity, alignment: .center) - } - .background(Color.blue) - .frame(maxWidth: .infinity, alignment: .center) - .listRowSeparator(.hidden, edges: .all) - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) - } else { - if showFeatureCards { - featureCard - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) - .listRowSeparator(.hidden, edges: .all) - .modifier(AnimatingCellHeight(height: 190 + 13)) - .onDisappear { - withAnimation { - isListScrolled = true + ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { _, item in + FeedCardNavigationLink( + item: item, + isInMultiSelectMode: viewModel.isInMultiSelectMode, + viewModel: viewModel + ) + .background(GeometryReader { geometry in + Color.clear + .preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin) + }) + .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in + print("ScrollOffsetPreferenceKey.self", value, item) + if value.y < 100, value.y > 0 { + if item.savedAt != nil, topItem != item { + setTopItem(item) + } } } - .onAppear { - withAnimation { - isListScrolled = false + .listRowSeparatorTint(Color.thBorderColor) + .listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset)) + .contextMenu { + menuItems(for: item) + } + .swipeActions(edge: .leading, allowsFullSwipe: true) { + ForEach(viewModel.listConfig.leadingSwipeActions, id: \.self) { action in + swipeActionButton(action: action, item: item) } } - } - - ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { _, item in - FeedCardNavigationLink( - item: item, - isInMultiSelectMode: viewModel.isInMultiSelectMode, - viewModel: viewModel - ) - .background(GeometryReader { geometry in - Color.clear - .preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin) - }) - .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in - if value.y < 100, value.y > 0 { - if item.savedAt != nil, topItem != item { - setTopItem(item) + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + ForEach(viewModel.listConfig.trailingSwipeActions, id: \.self) { action in + swipeActionButton(action: action, item: item) } } } - .listRowSeparatorTint(Color.thBorderColor) - .listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset)) - .contextMenu { - menuItems(for: item) - } - .swipeActions(edge: .leading, allowsFullSwipe: true) { - ForEach(viewModel.listConfig.leadingSwipeActions, id: \.self) { action in - swipeActionButton(action: action, item: item) - } - } - .swipeActions(edge: .trailing, allowsFullSwipe: true) { - ForEach(viewModel.listConfig.trailingSwipeActions, id: \.self) { action in - swipeActionButton(action: action, item: item) - } - } } - } + }, header: { + filtersHeader + }) } .padding(0) - .listStyle(PlainListStyle()) + .listStyle(.plain) .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) .coordinateSpace(name: "scroll") } @@ -642,6 +687,9 @@ struct AnimatingCellHeight: AnimatableModifier { viewModel.hideFeatureSection = true } Button(LocalText.cancelGeneric, role: .cancel) { self.showHideFeatureAlert = false } + }.introspectNavigationController { nav in + nav.navigationBar.shadowImage = UIImage() + nav.navigationBar.setBackgroundImage(UIImage(), for: .default) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index a036cbfa0..2ca444e94 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -37,7 +37,7 @@ import Views @Published var filters = [InternalFilter]() - @ObservedObject var filterState: FetcherFilterState + @Published var filterState: FetcherFilterState @AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false @AppStorage(UserDefaultKey.lastSelectedFeaturedItemFilter.rawValue) var featureFilter = FeaturedItemFilter.continueReading.rawValue diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index a51e29f24..8ec1dc4f9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -69,8 +69,11 @@ struct ProfileView: View { Form { innerBody } - .navigationTitle(LocalText.genericProfile) - .navigationBarTitleDisplayMode(.inline) +// .navigationTitle("LocalText.genericProfile") +// .navigationBarTitleDisplayMode(.) + .toolbar { + toolbarItems + } #elseif os(macOS) List { innerBody @@ -80,11 +83,16 @@ struct ProfileView: View { #endif } - var dismissButton: some View { - Button( - action: { dismiss() }, - label: { Text(LocalText.genericClose) } - ) + var toolbarItems: some ToolbarContent { + Group { + ToolbarItem(placement: .barLeading) { + VStack(alignment: .leading) { + Text(LocalText.genericProfile) + .font(Font.system(size: 32, weight: .semibold)) + } + .frame(maxWidth: .infinity, alignment: .bottomLeading) + } + } } private var accountSection: some View { diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift index e103e690f..ab0e54da8 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift @@ -116,15 +116,6 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { [ InternalFilter( id: "following", - name: "Following", - folder: "following", - filter: "in:following", - visible: true, - position: 0, - defaultFilter: true - ), - InternalFilter( - id: "rss", name: "RSS", folder: "following", filter: "in:following label:RSS", diff --git a/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift b/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift index 214444c79..af29d25bb 100644 --- a/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift +++ b/apple/OmnivoreKit/Sources/Views/Colors/Colors.swift @@ -43,6 +43,7 @@ public extension Color { static var themeDisabledBG: Color { Color("_themeDisabledBG", bundle: .module) } static var themeSolidBackground: Color { Color("_themeSolidBackground", bundle: .module) } static var thBorderColor: Color { Color("thBorderColor", bundle: .module) } + static var thLibrarySeparator: Color { Color("thLibrarySeparator", bundle: .module) } static var thFeatureSeparator: Color { Color("featureSeparator", bundle: .module) } From dca79bc9131314689e59b6203cc86ecca9aec610 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 1 Dec 2023 14:21:25 +0800 Subject: [PATCH 06/35] Fullscreen read now, scroll to top from tabs --- .../Components/FeedCardNavigationLink.swift | 22 +- .../LibraryFeatureCardNavigationLink.swift | 24 ++- .../App/Views/Home/HomeFeedViewIOS.swift | 195 ++++++++++-------- .../App/Views/Home/HomeFeedViewModel.swift | 1 + .../App/Views/Profile/ProfileView.swift | 2 +- .../App/Views/TabBar/CustomTabBar.swift | 3 + .../Services/NSNotification+Operation.swift | 5 + 7 files changed, 137 insertions(+), 115 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index 8714d9b52..b454125fc 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -39,26 +39,6 @@ struct FeedCardNavigationLink: View { var body: some View { ZStack { LibraryItemCard(item: item, viewer: dataService.currentViewer) -// PresentationLink({ -// <#code#> -// } label: { -// EmptyView() -// }).opacity(0) - -// public init( -// edge: Edge = .bottom, -// prefersScaleEffect: Bool = true, -// preferredCornerRadius: CGFloat? = nil, -// isInteractive: Bool = true, -// options: Options = .init(modalPresentationCapturesStatusBarAppearance: true) -// ) { -// self.edge = edge -// self.prefersScaleEffect = prefersScaleEffect -// self.preferredCornerRadius = preferredCornerRadius -// self.isInteractive = isInteractive -// self.options = options -// } -// PresentationLink( transition: PresentationLinkTransition.slide( options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing, @@ -75,7 +55,7 @@ struct FeedCardNavigationLink: View { }, label: { EmptyView() } - ) + ).opacity(0) } .onAppear { Task { await viewModel.itemAppeared(item: item, dataService: dataService) } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift index c107f4d10..ce76a1310 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift @@ -8,6 +8,7 @@ import Models import Services import SwiftUI +import Transmission import Views struct LibraryFeatureCardNavigationLink: View { @@ -20,12 +21,23 @@ struct LibraryFeatureCardNavigationLink: View { @State var showFeatureActions = false var body: some View { - NavigationLink(destination: LinkItemDetailView( - linkedItemObjectID: item.objectID, - isPDF: item.isPDF - )) { - LibraryFeatureCard(item: item, viewer: dataService.currentViewer) - } + PresentationLink( + transition: PresentationLinkTransition.slide( + options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing, + options: + PresentationLinkTransition.Options( + modalPresentationCapturesStatusBarAppearance: true + ))), + destination: { + LinkItemDetailView( + linkedItemObjectID: item.objectID, + isPDF: item.isPDF + ) + .background(ThemeManager.currentBgColor) + }, label: { + LibraryFeatureCard(item: item, viewer: dataService.currentViewer) + } + ) .confirmationDialog("", isPresented: $showFeatureActions) { if FeaturedItemFilter(rawValue: viewModel.featureFilter) == .pinned { Button("Unpin", action: { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 252817b29..a54bc9790 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -2,6 +2,7 @@ import CoreData import Models import Services import SwiftUI +import Transmission import UserNotifications import Utils import Views @@ -80,6 +81,11 @@ struct AnimatingCellHeight: AnimatableModifier { .onChange(of: viewModel.filterState.appliedFilter?.id) { _ in loadItems(isRefresh: true) } + .onChange(of: viewModel.presentWebContainer) { _ in + if !viewModel.presentWebContainer { + viewModel.linkRequest = nil + } + } .onChange(of: viewModel.filterState.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without @@ -143,6 +149,7 @@ struct AnimatingCellHeight: AnimatableModifier { DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { withoutAnimation { viewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID) + viewModel.presentWebContainer = true } } } @@ -177,7 +184,7 @@ struct AnimatingCellHeight: AnimatableModifier { let showDate = isListScrolled && !listTitle.isEmpty if let title = viewModel.filterState.appliedFilter?.name { Text(title) - .font(Font.system(size: showDate ? 10 : 32, weight: .semibold)) + .font(Font.system(size: showDate ? 10 : 28, weight: .semibold)) if showDate, prefersListLayout, isListScrolled || !showFeatureCards { Text(listTitle) .font(Font.system(size: 15, weight: .regular)) @@ -280,21 +287,22 @@ struct AnimatingCellHeight: AnimatableModifier { var body: some View { VStack(spacing: 0) { if let linkRequest = viewModel.linkRequest { - NavigationLink( - destination: WebReaderLoadingContainer(requestID: linkRequest.serverID), - tag: linkRequest, - selection: $viewModel.linkRequest - ) { - EmptyView() - } + PresentationLink( + transition: PresentationLinkTransition.slide( + options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing, + options: + PresentationLinkTransition.Options( + modalPresentationCapturesStatusBarAppearance: true + ))), + isPresented: $viewModel.presentWebContainer, + destination: { + WebReaderLoadingContainer(requestID: linkRequest.serverID) + .background(ThemeManager.currentBgColor) + }, label: { + EmptyView() + } + ) } - NavigationLink( - destination: LinkDestination(selectedItem: viewModel.selectedItem), - isActive: $viewModel.linkIsActive - ) { - EmptyView() - } - if prefersListLayout || !enableGrid { HomeFeedListView( listTitle: $listTitle, @@ -363,6 +371,10 @@ struct AnimatingCellHeight: AnimatableModifier { let showFeatureCards: Bool + @State var shouldScrollToTop = false + @State var topItem: Models.LibraryItem? + @ObservedObject var networkMonitor = NetworkMonitor() + init(listTitle: Binding, isListScrolled: Binding, prefersListLayout: Binding, @@ -555,8 +567,6 @@ struct AnimatingCellHeight: AnimatableModifier { static func reduce(value _: inout CGPoint, nextValue _: () -> CGPoint) {} } - @State var topItem: Models.LibraryItem? - func setTopItem(_ item: Models.LibraryItem) { print("setting top item: ", item) if let date = item.savedAt, let daysAgo = Calendar.current.dateComponents([.day], from: date, to: Date()).day { @@ -592,8 +602,6 @@ struct AnimatingCellHeight: AnimatableModifier { } } - @ObservedObject var networkMonitor = NetworkMonitor() - var body: some View { let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10) VStack(spacing: 0) { @@ -603,83 +611,96 @@ struct AnimatingCellHeight: AnimatableModifier { Spacer(minLength: 2) } - List(selection: $selection) { - Section(content: { - if let appliedFilter = viewModel.filterState.appliedFilter, - networkMonitor.status == .disconnected, - !appliedFilter.allowLocalFetch - { - HStack { - Text("This search requires an internet connection.") - .padding() - .foregroundColor(Color.white) - .frame(maxWidth: .infinity, alignment: .center) - } - .background(Color.blue) - .frame(maxWidth: .infinity, alignment: .center) - .listRowSeparator(.hidden, edges: .all) - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) - } else { - if showFeatureCards { - featureCard - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) - .listRowSeparator(.hidden, edges: .all) - .modifier(AnimatingCellHeight(height: 190 + 13)) - .onDisappear { - withAnimation { - isListScrolled = true + ScrollViewReader { reader in + List(selection: $selection) { + Section(content: { + if let appliedFilter = viewModel.filterState.appliedFilter, + networkMonitor.status == .disconnected, + !appliedFilter.allowLocalFetch + { + HStack { + Text("This search requires an internet connection.") + .padding() + .foregroundColor(Color.white) + .frame(maxWidth: .infinity, alignment: .center) + } + .background(Color.blue) + .frame(maxWidth: .infinity, alignment: .center) + .listRowSeparator(.hidden, edges: .all) + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + } else { + if showFeatureCards { + featureCard + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + .listRowSeparator(.hidden, edges: .all) + .modifier(AnimatingCellHeight(height: 190 + 13)) + .onDisappear { + withAnimation { + isListScrolled = true + } } - } - .onAppear { - withAnimation { - isListScrolled = false + .onAppear { + withAnimation { + isListScrolled = false + } } - } - } + } - ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { _, item in - FeedCardNavigationLink( - item: item, - isInMultiSelectMode: viewModel.isInMultiSelectMode, - viewModel: viewModel - ) - .background(GeometryReader { geometry in - Color.clear - .preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin) - }) - .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in - print("ScrollOffsetPreferenceKey.self", value, item) - if value.y < 100, value.y > 0 { - if item.savedAt != nil, topItem != item { - setTopItem(item) + ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { _, item in + FeedCardNavigationLink( + item: item, + isInMultiSelectMode: viewModel.isInMultiSelectMode, + viewModel: viewModel + ) + .background(GeometryReader { geometry in + Color.clear + .preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin) + }) + .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in + if value.y < 100, value.y > 0 { + if item.savedAt != nil, topItem != item { + setTopItem(item) + } + } + } + .listRowSeparatorTint(Color.thBorderColor) + .listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset)) + .contextMenu { + menuItems(for: item) + } + .swipeActions(edge: .leading, allowsFullSwipe: true) { + ForEach(viewModel.listConfig.leadingSwipeActions, id: \.self) { action in + swipeActionButton(action: action, item: item) + } + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + ForEach(viewModel.listConfig.trailingSwipeActions, id: \.self) { action in + swipeActionButton(action: action, item: item) } } } - .listRowSeparatorTint(Color.thBorderColor) - .listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset)) - .contextMenu { - menuItems(for: item) - } - .swipeActions(edge: .leading, allowsFullSwipe: true) { - ForEach(viewModel.listConfig.leadingSwipeActions, id: \.self) { action in - swipeActionButton(action: action, item: item) - } - } - .swipeActions(edge: .trailing, allowsFullSwipe: true) { - ForEach(viewModel.listConfig.trailingSwipeActions, id: \.self) { action in - swipeActionButton(action: action, item: item) - } - } + } + }, header: { + filtersHeader + }) + } + .padding(0) + .listStyle(.plain) + .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) + .coordinateSpace(name: "scroll") + .onChange(of: shouldScrollToTop) { _ in + if shouldScrollToTop, let topItem = viewModel.fetcher.items.first { + print("READING POISTION: ", topItem) + withAnimation { // add animation for scroll to top + reader.scrollTo(topItem.unwrappedID, anchor: .top) // scroll } } - }, header: { - filtersHeader - }) + shouldScrollToTop = false + } + } + .onReceive(NotificationCenter.default.publisher(for: Notification.Name("ScrollToTop"))) { _ in + shouldScrollToTop = true } - .padding(0) - .listStyle(.plain) - .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) - .coordinateSpace(name: "scroll") } .alert("The Feature Section will be removed from your library. You can add it back from the filter settings in your profile.", isPresented: $showHideFeatureAlert) { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 2ca444e94..9ec6b4de5 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -18,6 +18,7 @@ import Views @Published var snoozePresented = false @Published var itemToSnoozeID: String? @Published var linkRequest: LinkRequest? + @Published var presentWebContainer = false @Published var showLoadingBar = false @Published var isInMultiSelectMode = false diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index 8ec1dc4f9..fa40fa01f 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -88,7 +88,7 @@ struct ProfileView: View { ToolbarItem(placement: .barLeading) { VStack(alignment: .leading) { Text(LocalText.genericProfile) - .font(Font.system(size: 32, weight: .semibold)) + .font(Font.system(size: 28, weight: .semibold)) } .frame(maxWidth: .infinity, alignment: .bottomLeading) } diff --git a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift index 43d8dbe6c..984c72a13 100644 --- a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift +++ b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift @@ -22,6 +22,9 @@ struct TabBarButton: View { var body: some View { Button(action: { + if selectedTab == key { + NotificationCenter.default.post(Notification(name: Notification.Name("ScrollToTop"))) + } selectedTab = key }, label: { image diff --git a/apple/OmnivoreKit/Sources/Services/NSNotification+Operation.swift b/apple/OmnivoreKit/Sources/Services/NSNotification+Operation.swift index 254661e64..7abfd17e1 100644 --- a/apple/OmnivoreKit/Sources/Services/NSNotification+Operation.swift +++ b/apple/OmnivoreKit/Sources/Services/NSNotification+Operation.swift @@ -12,6 +12,7 @@ public extension NSNotification { static let SpeakingReaderItem = Notification.Name("SpeakingReaderItem") static let DisplayProfile = Notification.Name("DisplayProfile") static let Logout = Notification.Name("Logout") + static let ScrollToTop = Notification.Name("ScrollToTop") static var pushFeedItemPublisher: NotificationCenter.Publisher { NotificationCenter.default.publisher(for: PushJSONArticle) @@ -49,6 +50,10 @@ public extension NSNotification { NotificationCenter.default.publisher(for: Logout) } + static var scrollToTopPublisher: NotificationCenter.Publisher { + NotificationCenter.default.publisher(for: ScrollToTop) + } + internal var operationMessage: String? { if let message = userInfo?["message"] as? String { return message From 2f4436a8cb760588f64e8fe956355d9fb55b11fc Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 1 Dec 2023 14:23:18 +0800 Subject: [PATCH 07/35] Remove debug line --- apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index a54bc9790..83a9ec5ac 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -690,7 +690,6 @@ struct AnimatingCellHeight: AnimatableModifier { .coordinateSpace(name: "scroll") .onChange(of: shouldScrollToTop) { _ in if shouldScrollToTop, let topItem = viewModel.fetcher.items.first { - print("READING POISTION: ", topItem) withAnimation { // add animation for scroll to top reader.scrollTo(topItem.unwrappedID, anchor: .top) // scroll } From 0edced69bf8bf4afd3f17c93673b7e60b89c2348 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 1 Dec 2023 14:30:16 +0800 Subject: [PATCH 08/35] Add empty view for scrolling --- .../Sources/App/Views/Home/HomeFeedViewIOS.swift | 7 ++++--- .../Sources/App/Views/TabBar/CustomTabBar.swift | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 83a9ec5ac..30a150ebe 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -614,6 +614,7 @@ struct AnimatingCellHeight: AnimatableModifier { ScrollViewReader { reader in List(selection: $selection) { Section(content: { + EmptyView().id("TOP") if let appliedFilter = viewModel.filterState.appliedFilter, networkMonitor.status == .disconnected, !appliedFilter.allowLocalFetch @@ -689,9 +690,9 @@ struct AnimatingCellHeight: AnimatableModifier { .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) .coordinateSpace(name: "scroll") .onChange(of: shouldScrollToTop) { _ in - if shouldScrollToTop, let topItem = viewModel.fetcher.items.first { - withAnimation { // add animation for scroll to top - reader.scrollTo(topItem.unwrappedID, anchor: .top) // scroll + if shouldScrollToTop { + withAnimation { + reader.scrollTo("TOP", anchor: .top) } } shouldScrollToTop = false diff --git a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift index 984c72a13..548f38689 100644 --- a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift +++ b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift @@ -33,7 +33,6 @@ struct TabBarButton: View { .aspectRatio(contentMode: .fit) .frame(width: 28, height: 28) .foregroundColor(selectedTab == key ? Color.blue : Color.themeTabButtonColor) - .frame(maxWidth: .infinity) }) } From 2641151a2697074581e3e8e9bdc268d316c454c4 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 1 Dec 2023 14:43:59 +0800 Subject: [PATCH 09/35] Remove debug --- apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 30a150ebe..37b654095 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -568,7 +568,6 @@ struct AnimatingCellHeight: AnimatableModifier { } func setTopItem(_ item: Models.LibraryItem) { - print("setting top item: ", item) if let date = item.savedAt, let daysAgo = Calendar.current.dateComponents([.day], from: date, to: Date()).day { if daysAgo < 1 { let formatter = DateFormatter() From af2759cab09ab38b7d363612d8865bb3f6ec04a7 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 1 Dec 2023 16:06:19 +0800 Subject: [PATCH 10/35] Hang search variables off view model to make them easier to observe --- .../App/Views/Home/FetcherFilterState.swift | 21 +-- .../App/Views/Home/HomeFeedViewIOS.swift | 129 ++++++++---------- .../App/Views/Home/HomeFeedViewModel.swift | 42 ++++-- .../App/Views/Home/LibraryListView.swift | 2 +- .../Sources/App/Views/LibraryTabView.swift | 4 +- .../App/Views/Profile/ProfileView.swift | 4 +- .../InternalModels/InternalFilter.swift | 6 +- 7 files changed, 101 insertions(+), 107 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift b/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift index 277be132a..f69105528 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/FetcherFilterState.swift @@ -5,22 +5,13 @@ import SwiftUI import Utils @MainActor -class FetcherFilterState: ObservableObject { +struct FetcherFilterState { let folder: String - @Published var searchTerm = "" - @Published var selectedLabels = [LinkedItemLabel]() - @Published var negatedLabels = [LinkedItemLabel]() - @Published var appliedSort = LinkedItemSort.newest.rawValue + let searchTerm: String + let selectedLabels: [LinkedItemLabel] + let negatedLabels: [LinkedItemLabel] + let appliedSort: String - @Published var appliedFilter: InternalFilter? { - didSet { - let filterKey = UserDefaults.standard.string(forKey: "lastSelected-\(folder)-filter") ?? folder - UserDefaults.standard.setValue(appliedFilter?.name, forKey: filterKey) - } - } - - init(folder: String) { - self.folder = folder - } + let appliedFilter: InternalFilter? } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 37b654095..5c75f1c3c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -41,12 +41,11 @@ struct AnimatingCellHeight: AnimatableModifier { @AppStorage(UserDefaultKey.homeFeedlayoutPreference.rawValue) var prefersListLayout = true @AppStorage(UserDefaultKey.openAIPrimerDisplayed.rawValue) var openAIPrimerDisplayed = false - @StateObject var viewModel: HomeFeedViewModel - + @ObservedObject var viewModel: HomeFeedViewModel @State private var selection = Set() init(viewModel: HomeFeedViewModel) { - _viewModel = StateObject(wrappedValue: viewModel) + _viewModel = ObservedObject(wrappedValue: viewModel) } func loadItems(isRefresh: Bool) { @@ -57,11 +56,10 @@ struct AnimatingCellHeight: AnimatableModifier { viewModel.listConfig.hasFeatureCards && !viewModel.hideFeatureSection && viewModel.fetcher.items.count > 0 && - viewModel.filterState.searchTerm.isEmpty && - viewModel.filterState.selectedLabels.isEmpty && - viewModel.filterState.negatedLabels.isEmpty - /* && - viewModel.filterState.appliedFilter?.name == "inbox" */ + viewModel.searchTerm.isEmpty && + viewModel.selectedLabels.isEmpty && + viewModel.negatedLabels.isEmpty && + viewModel.appliedFilter?.name == "inbox" } var body: some View { @@ -72,35 +70,31 @@ struct AnimatingCellHeight: AnimatableModifier { isEditMode: $isEditMode, selection: $selection, viewModel: viewModel, - filterState: viewModel.filterState, showFeatureCards: showFeatureCards ) .refreshable { loadItems(isRefresh: true) } - .onChange(of: viewModel.filterState.appliedFilter?.id) { _ in - loadItems(isRefresh: true) - } .onChange(of: viewModel.presentWebContainer) { _ in if !viewModel.presentWebContainer { viewModel.linkRequest = nil } } - .onChange(of: viewModel.filterState.searchTerm) { _ in + .onChange(of: viewModel.searchTerm) { _ in // Maybe we should debounce this, but // it feels like it works ok without loadItems(isRefresh: true) } - .onChange(of: viewModel.filterState.selectedLabels) { _ in + .onChange(of: viewModel.selectedLabels) { _ in loadItems(isRefresh: true) } - .onChange(of: viewModel.filterState.negatedLabels) { _ in + .onChange(of: viewModel.negatedLabels) { _ in loadItems(isRefresh: true) } - .onChange(of: viewModel.filterState.appliedFilter) { _ in + .onChange(of: viewModel.appliedFilter) { _ in loadItems(isRefresh: true) } - .onChange(of: viewModel.filterState.appliedSort) { _ in + .onChange(of: viewModel.appliedSort) { _ in loadItems(isRefresh: true) } .sheet(item: $viewModel.itemUnderLabelEdit) { item in @@ -140,10 +134,10 @@ struct AnimatingCellHeight: AnimatableModifier { if let deepLink = DeepLink.make(from: url) { switch deepLink { case let .search(query): - viewModel.filterState.searchTerm = query + viewModel.searchTerm = query case let .savedSearch(named): if let filter = viewModel.findFilter(dataService, named: named) { - viewModel.filterState.appliedFilter = filter + viewModel.appliedFilter = filter } case let .webAppLinkRequest(requestID): DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { @@ -172,7 +166,7 @@ struct AnimatingCellHeight: AnimatableModifier { if viewModel.fetcher.items.isEmpty { loadItems(isRefresh: false) } - await viewModel.loadFilters(dataService: dataService, filterState: viewModel.filterState) + await viewModel.loadFilters(dataService: dataService) } .environment(\.editMode, self.$isEditMode) } @@ -182,9 +176,9 @@ struct AnimatingCellHeight: AnimatableModifier { ToolbarItem(placement: .barLeading) { VStack(alignment: .leading) { let showDate = isListScrolled && !listTitle.isEmpty - if let title = viewModel.filterState.appliedFilter?.name { + if let title = viewModel.appliedFilter?.name { Text(title) - .font(Font.system(size: showDate ? 10 : 28, weight: .semibold)) + .font(Font.system(size: showDate ? 10 : 24, weight: .semibold)) if showDate, prefersListLayout, isListScrolled || !showFeatureCards { Text(listTitle) .font(Font.system(size: 15, weight: .regular)) @@ -280,7 +274,6 @@ struct AnimatingCellHeight: AnimatableModifier { @Binding var isEditMode: EditMode @Binding var selection: Set @ObservedObject var viewModel: HomeFeedViewModel - @ObservedObject var filterState: FetcherFilterState let showFeatureCards: Bool @@ -311,7 +304,6 @@ struct AnimatingCellHeight: AnimatableModifier { isEditMode: $isEditMode, selection: $selection, viewModel: viewModel, - filterState: filterState, showFeatureCards: showFeatureCards ) } else { @@ -322,11 +314,11 @@ struct AnimatingCellHeight: AnimatableModifier { } }.sheet(isPresented: $viewModel.showLabelsSheet) { FilterByLabelsView( - initiallySelected: filterState.selectedLabels, - initiallyNegated: filterState.negatedLabels + initiallySelected: viewModel.selectedLabels, + initiallyNegated: viewModel.negatedLabels ) { - filterState.selectedLabels = $0 - filterState.negatedLabels = $1 + viewModel.selectedLabels = $0 + viewModel.negatedLabels = $1 } } .popup(isPresented: $viewModel.showSnackbar) { @@ -367,7 +359,6 @@ struct AnimatingCellHeight: AnimatableModifier { @Binding var selection: Set @ObservedObject var viewModel: HomeFeedViewModel - @ObservedObject var filterState: FetcherFilterState let showFeatureCards: Bool @@ -375,45 +366,43 @@ struct AnimatingCellHeight: AnimatableModifier { @State var topItem: Models.LibraryItem? @ObservedObject var networkMonitor = NetworkMonitor() - init(listTitle: Binding, - isListScrolled: Binding, - prefersListLayout: Binding, - isEditMode: Binding, - selection: Binding>, - viewModel: HomeFeedViewModel, - filterState: FetcherFilterState, - showFeatureCards: Bool) - { - self._listTitle = listTitle - self._isListScrolled = isListScrolled - self._prefersListLayout = prefersListLayout - self._isEditMode = isEditMode - self._selection = selection - self.viewModel = viewModel - self.filterState = filterState - self.showFeatureCards = showFeatureCards - } +// init(listTitle: Binding, +// isListScrolled: Binding, +// prefersListLayout: Binding, +// isEditMode: Binding, +// selection: Binding>, +// viewModel: HomeFeedViewModel, +// showFeatureCards: Bool) +// { +// self._listTitle = listTitle +// self._isListScrolled = isListScrolled +// self._prefersListLayout = prefersListLayout +// self._isEditMode = isEditMode +// self._selection = selection +// self.viewModel = viewModel +// self.showFeatureCards = showFeatureCards +// } var filtersHeader: some View { GeometryReader { reader in ScrollView(.horizontal, showsIndicators: false) { HStack { - if viewModel.filterState.searchTerm.count > 0 { - TextChipButton.makeSearchFilterButton(title: viewModel.filterState.searchTerm) { - viewModel.filterState.searchTerm = "" + if viewModel.searchTerm.count > 0 { + TextChipButton.makeSearchFilterButton(title: viewModel.searchTerm) { + viewModel.searchTerm = "" }.frame(maxWidth: reader.size.width * 0.66) } else { Menu( content: { ForEach(viewModel.filters) { filter in Button(filter.name, action: { - viewModel.filterState.appliedFilter = filter + viewModel.appliedFilter = filter }) } }, label: { TextChipButton.makeMenuButton( - title: viewModel.filterState.appliedFilter?.name ?? "-", + title: viewModel.appliedFilter?.name ?? "-", color: .systemGray6 ) } @@ -422,25 +411,25 @@ struct AnimatingCellHeight: AnimatableModifier { Menu( content: { ForEach(LinkedItemSort.allCases, id: \.self) { sort in - Button(sort.displayName, action: { viewModel.filterState.appliedSort = sort.rawValue }) + Button(sort.displayName, action: { viewModel.appliedSort = sort.rawValue }) } }, label: { TextChipButton.makeMenuButton( - title: LinkedItemSort(rawValue: viewModel.filterState.appliedSort)?.displayName ?? "Sort", + title: LinkedItemSort(rawValue: viewModel.appliedSort)?.displayName ?? "Sort", color: .systemGray6 ) } ) TextChipButton.makeAddLabelButton(color: .systemGray6, onTap: { viewModel.showLabelsSheet = true }) - ForEach(viewModel.filterState.selectedLabels, id: \.self) { label in + ForEach(viewModel.selectedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: false) { - viewModel.filterState.selectedLabels.removeAll { $0.id == label.id } + viewModel.selectedLabels.removeAll { $0.id == label.id } } } - ForEach(viewModel.filterState.negatedLabels, id: \.self) { label in + ForEach(viewModel.negatedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: true) { - viewModel.filterState.negatedLabels.removeAll { $0.id == label.id } + viewModel.negatedLabels.removeAll { $0.id == label.id } } } Spacer() @@ -614,7 +603,7 @@ struct AnimatingCellHeight: AnimatableModifier { List(selection: $selection) { Section(content: { EmptyView().id("TOP") - if let appliedFilter = viewModel.filterState.appliedFilter, + if let appliedFilter = viewModel.appliedFilter, networkMonitor.status == .disconnected, !appliedFilter.allowLocalFetch { @@ -795,20 +784,20 @@ struct AnimatingCellHeight: AnimatableModifier { GeometryReader { reader in ScrollView(.horizontal, showsIndicators: false) { HStack { - if viewModel.filterState.searchTerm.count > 0 { - TextChipButton.makeSearchFilterButton(title: viewModel.filterState.searchTerm) { - viewModel.filterState.searchTerm = "" + if viewModel.searchTerm.count > 0 { + TextChipButton.makeSearchFilterButton(title: viewModel.searchTerm) { + viewModel.searchTerm = "" }.frame(maxWidth: reader.size.width * 0.66) } else { Menu( content: { ForEach(viewModel.filters, id: \.self) { filter in - Button(filter.name, action: { viewModel.filterState.appliedFilter = filter }) + Button(filter.name, action: { viewModel.appliedFilter = filter }) } }, label: { TextChipButton.makeMenuButton( - title: viewModel.filterState.appliedFilter?.name ?? "-", + title: viewModel.appliedFilter?.name ?? "-", color: .systemGray6 ) } @@ -817,25 +806,25 @@ struct AnimatingCellHeight: AnimatableModifier { Menu( content: { ForEach(LinkedItemSort.allCases, id: \.self) { sort in - Button(sort.displayName, action: { viewModel.filterState.appliedSort = sort.rawValue }) + Button(sort.displayName, action: { viewModel.appliedSort = sort.rawValue }) } }, label: { TextChipButton.makeMenuButton( - title: LinkedItemSort(rawValue: viewModel.filterState.appliedSort)?.displayName ?? "Sort", + title: LinkedItemSort(rawValue: viewModel.appliedSort)?.displayName ?? "Sort", color: .systemGray6 ) } ) TextChipButton.makeAddLabelButton(color: .systemGray6, onTap: { viewModel.showLabelsSheet = true }) - ForEach(viewModel.filterState.selectedLabels, id: \.self) { label in + ForEach(viewModel.selectedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: false) { - viewModel.filterState.selectedLabels.removeAll { $0.id == label.id } + viewModel.selectedLabels.removeAll { $0.id == label.id } } } - ForEach(viewModel.filterState.negatedLabels, id: \.self) { label in + ForEach(viewModel.negatedLabels, id: \.self) { label in TextChipButton.makeRemovableLabelButton(feedItemLabel: label, negated: true) { - viewModel.filterState.negatedLabels.removeAll { $0.id == label.id } + viewModel.negatedLabels.removeAll { $0.id == label.id } } } Spacer() diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 9ec6b4de5..7bb8dfd70 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -6,7 +6,9 @@ import Utils import Views @MainActor final class HomeFeedViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate { - var currentDetailViewModel: LinkItemDetailViewModel? + let folder: String + let fetcher: LibraryItemFetcher + let listConfig: LibraryListConfig private var fetchedResultsController: NSFetchedResultsController? @@ -31,24 +33,34 @@ import Views @Published var showCommunityModal = false @Published var featureItems = [Models.LibraryItem]() - @Published var listConfig: LibraryListConfig - @Published var showSnackbar = false @Published var snackbarOperation: SnackbarOperation? @Published var filters = [InternalFilter]() - @Published var filterState: FetcherFilterState + @Published var searchTerm = "" + @Published var selectedLabels = [LinkedItemLabel]() + @Published var negatedLabels = [LinkedItemLabel]() + @Published var appliedSort = LinkedItemSort.newest.rawValue @AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false @AppStorage(UserDefaultKey.lastSelectedFeaturedItemFilter.rawValue) var featureFilter = FeaturedItemFilter.continueReading.rawValue - let fetcher: LibraryItemFetcher + @Published var appliedFilter: InternalFilter? { + didSet { + let filterKey = UserDefaults.standard.string(forKey: "lastSelected-\(folder)-filter") ?? folder + UserDefaults.standard.setValue(appliedFilter?.name, forKey: filterKey) + } + } - init(fetcher: LibraryItemFetcher, filterState: FetcherFilterState, listConfig: LibraryListConfig) { + private var filterState: FetcherFilterState { + FetcherFilterState(folder: folder, searchTerm: searchTerm, selectedLabels: selectedLabels, negatedLabels: negatedLabels, appliedSort: appliedSort, appliedFilter: appliedFilter) + } + + init(folder: String, fetcher: LibraryItemFetcher, listConfig: LibraryListConfig) { + self.folder = folder self.fetcher = fetcher self.listConfig = listConfig - self.filterState = filterState super.init() } @@ -68,10 +80,10 @@ import Views } } - func loadFilters(dataService: DataService, filterState: FetcherFilterState) async { + func loadFilters(dataService: DataService) async { switch filterState.folder { case "following": - updateFilters(filterState: filterState, newFilters: InternalFilter.DefaultFollowingFilters) + updateFilters(newFilters: InternalFilter.DefaultFollowingFilters) default: var hasLocalResults = false let fetchRequest: NSFetchRequest = Filter.fetchRequest() @@ -79,15 +91,15 @@ import Views // Load from disk if let results = try? dataService.viewContext.fetch(fetchRequest) { hasLocalResults = true - updateFilters(filterState: filterState, newFilters: InternalFilter.make(from: results)) + updateFilters(newFilters: InternalFilter.make(from: results)) } let hasResults = hasLocalResults Task.detached { if let downloadedFilters = try? await dataService.filters() { - await self.updateFilters(filterState: filterState, newFilters: downloadedFilters) + await self.updateFilters(newFilters: downloadedFilters) } else if !hasResults { - await self.updateFilters(filterState: filterState, newFilters: InternalFilter.DefaultInboxFilters) + await self.updateFilters(newFilters: InternalFilter.DefaultInboxFilters) } } } @@ -127,7 +139,7 @@ import Views } } - func updateFilters(filterState: FetcherFilterState, newFilters: [InternalFilter]) { + func updateFilters(newFilters: [InternalFilter]) { let appliedFilterName = UserDefaults.standard.string(forKey: "lastSelected-\(filterState.folder)-filter") ?? filterState.folder filters = newFilters @@ -135,8 +147,8 @@ import Views .sorted(by: { $0.position < $1.position }) + [InternalFilter.DeletedFilter, InternalFilter.DownloadedFilter] - if let newFilter = filters.first(where: { $0.name.lowercased() == appliedFilterName }), newFilter.id != filterState.appliedFilter?.id { - filterState.appliedFilter = newFilter + if let newFilter = filters.first(where: { $0.name.lowercased() == appliedFilterName }), newFilter.id != appliedFilter?.id { + appliedFilter = newFilter } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift index 29811b6f8..e9c051a95 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/LibraryListView.swift @@ -11,8 +11,8 @@ import SwiftUI struct LibraryListView: View { @StateObject private var libraryViewModel = HomeFeedViewModel( + folder: "inbox", fetcher: LibraryItemFetcher(), - filterState: FetcherFilterState(folder: "inbox"), listConfig: LibraryListConfig( hasFeatureCards: true, leadingSwipeActions: [.pin], diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index 130933b9e..0d3ac6ef4 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -24,8 +24,8 @@ struct LibraryTabView: View { } @StateObject private var followingViewModel = HomeFeedViewModel( + folder: "following", fetcher: LibraryItemFetcher(), - filterState: FetcherFilterState(folder: "following"), listConfig: LibraryListConfig( hasFeatureCards: false, leadingSwipeActions: [.moveToInbox], @@ -35,8 +35,8 @@ struct LibraryTabView: View { ) @StateObject private var libraryViewModel = HomeFeedViewModel( + folder: "inbox", fetcher: LibraryItemFetcher(), - filterState: FetcherFilterState(folder: "inbox"), listConfig: LibraryListConfig( hasFeatureCards: true, leadingSwipeActions: [.pin], diff --git a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift index fa40fa01f..b52dd980c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Profile/ProfileView.swift @@ -69,8 +69,6 @@ struct ProfileView: View { Form { innerBody } -// .navigationTitle("LocalText.genericProfile") -// .navigationBarTitleDisplayMode(.) .toolbar { toolbarItems } @@ -88,7 +86,7 @@ struct ProfileView: View { ToolbarItem(placement: .barLeading) { VStack(alignment: .leading) { Text(LocalText.genericProfile) - .font(Font.system(size: 28, weight: .semibold)) + .font(Font.system(size: 24, weight: .semibold)) } .frame(maxWidth: .infinity, alignment: .bottomLeading) } diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift index ab0e54da8..f7b27ceed 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift @@ -2,7 +2,7 @@ import CoreData import Foundation import Models -public struct InternalFilter: Encodable, Identifiable, Hashable { +public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable { public let id: String public let name: String public let folder: String @@ -11,6 +11,10 @@ public struct InternalFilter: Encodable, Identifiable, Hashable { public let position: Int public let defaultFilter: Bool + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } + public static var DownloadedFilter: InternalFilter { InternalFilter( id: "downloaded", From bfd3365f28e9418570c33cc97392824dbb455482 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 4 Dec 2023 12:11:34 +0800 Subject: [PATCH 11/35] Handle PDFs with new Transimission presentation controllers --- apple/Omnivore.xcodeproj/project.pbxproj | 8 ++-- .../App/PDFSupport/PDFSettingsView.swift | 40 +++++++++++++++++ .../Sources/App/PDFSupport/PDFViewer.swift | 45 ++++++++++++++----- .../App/Views/Home/HomeFeedDisplayText.swift | 4 ++ .../Sources/App/Views/LibraryTabView.swift | 4 +- .../App/Views/LinkItemDetailView.swift | 5 ++- .../Sources/Models/LinkedItemSort.swift | 10 +++++ .../Views/FeedItem/LibraryItemCard.swift | 43 +++++++++++++++--- .../OmnivoreKit/Sources/Views/LocalText.swift | 2 + .../Resources/en.lproj/Localizable.strings | 2 + 10 files changed, 142 insertions(+), 21 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/PDFSupport/PDFSettingsView.swift diff --git a/apple/Omnivore.xcodeproj/project.pbxproj b/apple/Omnivore.xcodeproj/project.pbxproj index 33a580418..0e2288668 100644 --- a/apple/Omnivore.xcodeproj/project.pbxproj +++ b/apple/Omnivore.xcodeproj/project.pbxproj @@ -1384,7 +1384,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.39.0; + MARKETING_VERSION = 1.40.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; @@ -1419,7 +1419,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 12.0; - MARKETING_VERSION = 1.39.0; + MARKETING_VERSION = 1.40.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1474,7 +1474,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.39.0; + MARKETING_VERSION = 1.40.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1815,7 +1815,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.39.0; + MARKETING_VERSION = 1.40.0; PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app; PRODUCT_NAME = Omnivore; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFSettingsView.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFSettingsView.swift new file mode 100644 index 000000000..779a748bb --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFSettingsView.swift @@ -0,0 +1,40 @@ +#if os(iOS) + import Models + import PSPDFKit + import PSPDFKitUI + import SwiftUI + import Utils + import WebKit + + struct PDFSettingsView: UIViewControllerRepresentable { + @Environment(\.presentationMode) var presentationMode + + let pdfViewController: PDFViewController? + + func makeCoordinator() -> PDFSettingsViewCoordinator { + PDFSettingsViewCoordinator(self) + } + + func makeUIViewController(context _: Context) -> some UIViewController { + let settingsViewcontroller = PSPDFKitUI.PDFSettingsViewController() + settingsViewcontroller.pdfViewController = pdfViewController + + let nav = UINavigationController(rootViewController: settingsViewcontroller) + return nav + } + + func updateUIViewController(_: UIViewControllerType, context _: Context) {} + } + + class PDFSettingsViewCoordinator: NSObject, UINavigationControllerDelegate { + var parent: PDFSettingsView + + init(_ parent: PDFSettingsView) { + self.parent = parent + } + + @objc func dismiss() { + parent.presentationMode.wrappedValue.dismiss() + } + } +#endif diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift index 2eb80d27a..356a4635d 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift @@ -51,6 +51,9 @@ import Utils @State private var annotation = "" @State private var addNoteHighlight: Highlight? @State private var showAnnotationModal = false + @State private var showSettingsModal = false + + @Environment(\.dismiss) private var dismiss init(viewModel: PDFViewerViewModel) { self.viewModel = viewModel @@ -103,19 +106,13 @@ import Utils guard pdfStateObject.controllerNeedsConfig else { return } coordinator.setController(controller: controller, dataService: dataService) - // Disable the Document Editor - controller.navigationItem.setRightBarButtonItems( - [controller.thumbnailsButtonItem], - for: .thumbnails, - animated: false - ) - let barButtonItems = [ UIBarButtonItem( image: UIImage(systemName: "textformat"), style: .plain, - target: controller.settingsButtonItem.target, - action: controller.settingsButtonItem.action + target: coordinator, + action: #selector(PDFViewCoordinator.displaySettingsSheet) + ), UIBarButtonItem( image: UIImage(systemName: "book"), @@ -137,6 +134,15 @@ import Utils ) ] + let leftButtonItems = [ + UIBarButtonItem( + image: UIImage(named: "chevron-right", in: Bundle(url: ViewsPackage.bundleURL), with: nil), + style: .plain, + target: coordinator, + action: #selector(PDFViewCoordinator.pop) + ) + ] + document.areAnnotationsEnabled = true coordinator.viewer = self @@ -146,6 +152,7 @@ import Utils controller.setPageIndex(pageIndex, animated: false) } + controller.navigationItem.setLeftBarButtonItems(leftButtonItems, for: .document, animated: false) controller.navigationItem.setRightBarButtonItems(barButtonItems, for: .document, animated: false) pdfStateObject.controllerNeedsConfig = false } @@ -215,6 +222,12 @@ import Utils } .navigationViewStyle(StackNavigationViewStyle()) } + .formSheet(isPresented: $showSettingsModal, modalSize: CGSize(width: 400, height: 475)) { + NavigationView { + PDFSettingsView(pdfViewController: coordinator.controller) + } + .navigationViewStyle(StackNavigationViewStyle()) + } .fullScreenCover(isPresented: $readerView, content: { PDFReaderViewController(document: document) }) @@ -263,7 +276,7 @@ import Utils var subscriptions = Set() public var viewer: PDFViewer? - var controller: PDFViewController? + public var controller: PDFViewController? init(document: Document, viewModel: PDFViewerViewModel) { self.document = document @@ -446,12 +459,24 @@ import Utils } } + @objc public func pop() { + if let viewer = self.viewer { + viewer.dismiss() + } + } + @objc public func toggleReaderView() { if let viewer = self.viewer { viewer.readerView = !viewer.readerView } } + @objc public func displaySettingsSheet() { + if let viewer = self.viewer { + viewer.showSettingsModal = true + } + } + @objc public func toggleNotebookView() { if let viewer = self.viewer { viewer.showNotebookView = !viewer.showNotebookView diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedDisplayText.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedDisplayText.swift index 285c020fd..6d71255b0 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedDisplayText.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedDisplayText.swift @@ -38,6 +38,10 @@ public extension LinkedItemSort { return LocalText.newestGeneric case .oldest: return LocalText.oldestGeneric + case .longest: + return LocalText.longestGeneric + case .shortest: + return LocalText.shortestGeneric case .recentlyRead: return LocalText.recentlyReadGeneric case .recentlyPublished: diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index 0d3ac6ef4..f3f5eb256 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -10,6 +10,7 @@ import Models import PopupView import Services import SwiftUI +import Transmission import Utils import Views @@ -46,7 +47,7 @@ struct LibraryTabView: View { ) var body: some View { - VStack(spacing: 0) { + VStack { TabView(selection: $selectedTab) { NavigationView { HomeFeedContainerView(viewModel: followingViewModel) @@ -66,5 +67,6 @@ struct LibraryTabView: View { CustomTabBar(selectedTab: $selectedTab) } .ignoresSafeArea() + .navigationBarHidden(true) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index 8b0a1f010..8fa4a26bf 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -87,7 +87,10 @@ struct LinkItemDetailView: View { var body: some View { Group { if isPDF { - pdfContainerView + NavigationView { + pdfContainerView + .navigationBarBackButtonHidden(false) + } } else if let item = viewModel.item { WebReaderContainerView(item: item, pop: { dismiss() }) } diff --git a/apple/OmnivoreKit/Sources/Models/LinkedItemSort.swift b/apple/OmnivoreKit/Sources/Models/LinkedItemSort.swift index 4759b3a2d..5b0ced288 100644 --- a/apple/OmnivoreKit/Sources/Models/LinkedItemSort.swift +++ b/apple/OmnivoreKit/Sources/Models/LinkedItemSort.swift @@ -3,6 +3,8 @@ import Foundation public enum LinkedItemSort: String, CaseIterable { case newest case oldest + case shortest + case longest case recentlyRead case recentlyPublished } @@ -14,6 +16,10 @@ public extension LinkedItemSort { return "sort:saved" case .oldest: return "sort:saved-ASC" + case .longest: + return "sort:wordsCount-desc" + case .shortest: + return "sort:wordsCount-asc" case .recentlyRead: return "sort:read" case .recentlyPublished: @@ -27,6 +33,10 @@ public extension LinkedItemSort { return [NSSortDescriptor(keyPath: \LibraryItem.savedAt, ascending: false)] case .oldest: return [NSSortDescriptor(keyPath: \LibraryItem.savedAt, ascending: true)] + case .shortest: + return [NSSortDescriptor(keyPath: \LibraryItem.wordsCount, ascending: true)] + case .longest: + return [NSSortDescriptor(keyPath: \LibraryItem.wordsCount, ascending: false)] case .recentlyRead: return [ NSSortDescriptor(keyPath: \LibraryItem.readAt, ascending: false), diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift index 1797ac986..af4885352 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift @@ -289,12 +289,45 @@ public struct LibraryItemCard: View { return "" } + func shouldHideUrl(_ url: String?) -> Bool { + if let url = url, let origin = URL(string: url)?.host { + let hideHosts = ["storage.googleapis.com", "omnivore.app"] + if hideHosts.contains(origin) { + return true + } + } + + return false + } + + func siteName(_ originalArticleUrl: String?) -> String? { + if shouldHideUrl(originalArticleUrl) { + return nil + } + + if let url = originalArticleUrl, + let originalHost = URL(string: url)?.host?.replacingOccurrences(of: "^www\\.", with: "", options: .regularExpression) + { + return originalHost + } + + return nil + } + var byLine: some View { - Text(bylineStr) - .font(.caption2) - .foregroundColor(Color.themeLibraryItemSubtle) - .frame(maxWidth: .infinity, alignment: .leading) - .lineLimit(1) + if let origin = siteName(item.pageURLString) { + Text(bylineStr + " | " + origin) + .font(.caption2) + .foregroundColor(Color.themeLibraryItemSubtle) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else { + Text(bylineStr) + .font(.caption2) + .foregroundColor(Color.themeLibraryItemSubtle) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } } public var articleInfo: some View { diff --git a/apple/OmnivoreKit/Sources/Views/LocalText.swift b/apple/OmnivoreKit/Sources/Views/LocalText.swift index 2fc984a17..42625397e 100644 --- a/apple/OmnivoreKit/Sources/Views/LocalText.swift +++ b/apple/OmnivoreKit/Sources/Views/LocalText.swift @@ -190,6 +190,8 @@ public enum LocalText { public static let filesGeneric = localText(key: "filesGeneric") public static let newestGeneric = localText(key: "newestGeneric") public static let oldestGeneric = localText(key: "oldestGeneric") + public static let longestGeneric = localText(key: "longestGeneric") + public static let shortestGeneric = localText(key: "shortestGeneric") public static let recentlyReadGeneric = localText(key: "recentlyReadGeneric") public static let recentlyPublishedGeneric = localText(key: "recentlyPublishedGeneric") public static let clubsGeneric = localText(key: "clubsGeneric") diff --git a/apple/OmnivoreKit/Sources/Views/Resources/en.lproj/Localizable.strings b/apple/OmnivoreKit/Sources/Views/Resources/en.lproj/Localizable.strings index d69e034ce..78b678a32 100644 --- a/apple/OmnivoreKit/Sources/Views/Resources/en.lproj/Localizable.strings +++ b/apple/OmnivoreKit/Sources/Views/Resources/en.lproj/Localizable.strings @@ -189,6 +189,8 @@ "filesGeneric" = "Files"; "newestGeneric" = "Newest"; "oldestGeneric" = "Oldest"; +"longestGeneric" = "Longest"; +"shortestGeneric" = "Shortest"; "recentlyReadGeneric" = "Recently Read"; "recentlyPublishedGeneric" = "Recently Published"; "clubsGeneric" = "Clubs"; From ee375274b852afba230b52dd1cdb40490ea2928a Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 4 Dec 2023 13:02:35 +0800 Subject: [PATCH 12/35] Support move operation --- .../App/Views/Home/HomeFeedViewIOS.swift | 4 +- .../App/Views/Home/HomeFeedViewModel.swift | 11 +++ .../Models/DataModels/ServerSyncStatus.swift | 1 + .../DataService/Mutations/MoveItem.swift | 71 +++++++++++++++++++ .../Services/DataService/OfflineSync.swift | 12 ++++ .../Sources/Views/SyncingIcon.swift | 8 +-- 6 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Services/DataService/Mutations/MoveItem.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 5c75f1c3c..6aea45a0c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -59,7 +59,7 @@ struct AnimatingCellHeight: AnimatableModifier { viewModel.searchTerm.isEmpty && viewModel.selectedLabels.isEmpty && viewModel.negatedLabels.isEmpty && - viewModel.appliedFilter?.name == "inbox" + viewModel.appliedFilter?.name.lowercased() == "inbox" } var body: some View { @@ -740,7 +740,7 @@ struct AnimatingCellHeight: AnimatableModifier { case .moveToInbox: return AnyView(Button( action: { - // viewModel.addLabel(dataService: dataService, item: item, label: "Inbox", color) + viewModel.moveToFolder(dataService: dataService, item: item, folder: "inbox") }, label: { Label(title: { Text("Move to Library") }, diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 7bb8dfd70..06805fa59 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -258,6 +258,17 @@ import Views dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0, force: true) } + func moveToFolder(dataService: DataService, item: Models.LibraryItem, folder: String) { + Task { + do { + try await dataService.moveItem(itemID: item.unwrappedID, folder: folder) + snackbar("Item moved") + } catch { + snackbar("Error performing operation") + } + } + } + func bulkAction(dataService: DataService, action: BulkAction, items: [String]) { if items.count < 1 { snackbar("No items selected") diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/ServerSyncStatus.swift b/apple/OmnivoreKit/Sources/Models/DataModels/ServerSyncStatus.swift index f911cc4de..43aa729f2 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/ServerSyncStatus.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/ServerSyncStatus.swift @@ -6,4 +6,5 @@ public enum ServerSyncStatus: Int { case needsDeletion case needsCreation case needsUpdate + case needsMove } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MoveItem.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MoveItem.swift new file mode 100644 index 000000000..b85b30e58 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MoveItem.swift @@ -0,0 +1,71 @@ +import CoreData +import Foundation +import Models +import SwiftGraphQL + +public extension DataService { + func moveItem(itemID: String, folder: String) async throws { + backgroundContext.performAndWait { + if let linkedItem = Models.LibraryItem.lookup(byID: itemID, inContext: backgroundContext) { + linkedItem.folder = folder + linkedItem.serverSyncStatus = Int64(ServerSyncStatus.needsUpdate.rawValue) + } + do { + try backgroundContext.save() + logger.debug("LinkedItem updated succesfully") + } catch { + backgroundContext.rollback() + logger.debug("Failed to update LinkedItem: \(error.localizedDescription)") + } + } + + try await syncMoveToFolder(itemID: itemID, folder: folder) + } + + func syncMoveToFolder(itemID: String, folder: String) async throws { + enum MutationResult { + case result(itemID: String) + case error(errorMessage: String) + } + + let articleSavingRequestSelection = Selection.ArticleSavingRequest { + try $0.id() + } + + let selection = Selection { + try $0.on( + moveToFolderError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") }, + moveToFolderSuccess: .init { + .result(itemID: try $0.articleSavingRequest(selection: articleSavingRequestSelection)) + } + ) + } + + let mutation = Selection.Mutation { + try $0.moveToFolder( + folder: folder, + id: itemID, + selection: selection + ) + } + + let path = appEnvironment.graphqlPath + let headers = networker.defaultHeaders + + return try await withCheckedThrowingContinuation { continuation in + send(mutation, to: path, headers: headers) { queryResult in + guard let payload = try? queryResult.get() else { + continuation.resume(throwing: BasicError.message(messageText: "network error")) + return + } + + switch payload.data { + case let .result(itemID: _): + continuation.resume() + case let .error(errorMessage: errorMessage): + continuation.resume(throwing: BasicError.message(messageText: errorMessage)) + } + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift index f7417f262..2aa8ba78a 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/OfflineSync.swift @@ -157,6 +157,15 @@ public extension DataService { anchorIndex: Int(item.readingProgressAnchor), force: item.isPDF ) + case .needsMove: + item.serverSyncStatus = Int64(ServerSyncStatus.isSyncing.rawValue) + syncLinkArchiveStatus(itemID: item.unwrappedID, archived: item.isArchived) + syncLinkReadingProgress( + itemID: item.unwrappedID, + readingProgress: item.readingProgress, + anchorIndex: Int(item.readingProgressAnchor), + force: item.isPDF + ) } } } @@ -184,6 +193,9 @@ public extension DataService { } else { highlight.serverSyncStatus = Int64(ServerSyncStatus.isNSync.rawValue) } + case .needsMove: + // Highlights can't be moved + break } } } diff --git a/apple/OmnivoreKit/Sources/Views/SyncingIcon.swift b/apple/OmnivoreKit/Sources/Views/SyncingIcon.swift index 64e2432ba..1100da1e4 100644 --- a/apple/OmnivoreKit/Sources/Views/SyncingIcon.swift +++ b/apple/OmnivoreKit/Sources/Views/SyncingIcon.swift @@ -19,22 +19,18 @@ public struct SyncStatusIcon: View { private var cloudIconName: String { switch status { -// case .isNSync: -// return "checkmark.icloud" case .isNSync: return "exclamationmark.icloud" - case .isSyncing, .needsCreation, .needsDeletion, .needsUpdate: + case .isSyncing, .needsCreation, .needsDeletion, .needsUpdate, .needsMove: return "icloud" } } private var cloudIconColor: Color { switch status { -// case .isNSync: -// return .blue case .isNSync: return .red - case .isSyncing, .needsCreation, .needsDeletion, .needsUpdate: + case .isSyncing, .needsCreation, .needsDeletion, .needsUpdate, .needsMove: return .appGrayText } } From 421fc8756b97fbdc2fe49adce94a0d0439fceef0 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 4 Dec 2023 13:08:27 +0800 Subject: [PATCH 13/35] Add default filter for following --- .../Sources/App/Views/Home/HomeFeedViewModel.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 06805fa59..d175cd148 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -83,7 +83,7 @@ import Views func loadFilters(dataService: DataService) async { switch filterState.folder { case "following": - updateFilters(newFilters: InternalFilter.DefaultFollowingFilters) + updateFilters(newFilters: InternalFilter.DefaultFollowingFilters, defaultName: "rss") default: var hasLocalResults = false let fetchRequest: NSFetchRequest = Filter.fetchRequest() @@ -91,15 +91,15 @@ import Views // Load from disk if let results = try? dataService.viewContext.fetch(fetchRequest) { hasLocalResults = true - updateFilters(newFilters: InternalFilter.make(from: results)) + updateFilters(newFilters: InternalFilter.make(from: results), defaultName: "inbox") } let hasResults = hasLocalResults Task.detached { if let downloadedFilters = try? await dataService.filters() { - await self.updateFilters(newFilters: downloadedFilters) + await self.updateFilters(newFilters: downloadedFilters, defaultName: "inbox") } else if !hasResults { - await self.updateFilters(newFilters: InternalFilter.DefaultInboxFilters) + await self.updateFilters(newFilters: InternalFilter.DefaultInboxFilters, defaultName: "inbox") } } } @@ -139,8 +139,8 @@ import Views } } - func updateFilters(newFilters: [InternalFilter]) { - let appliedFilterName = UserDefaults.standard.string(forKey: "lastSelected-\(filterState.folder)-filter") ?? filterState.folder + func updateFilters(newFilters: [InternalFilter], defaultName: String) { + let appliedFilterName = UserDefaults.standard.string(forKey: "lastSelected-\(filterState.folder)-filter") ?? defaultName filters = newFilters .filter { $0.folder == filterState.folder } From b59a6bf0d6c1deb26dd27163fe7873bd0117f8ee Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 4 Dec 2023 13:34:56 +0800 Subject: [PATCH 14/35] Dont do multi select card actions --- .../templates/homeFeed/HomeFeedContainer.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx index 0ddc23e3c..01da20762 100644 --- a/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx +++ b/packages/web/components/templates/homeFeed/HomeFeedContainer.tsx @@ -407,21 +407,13 @@ export function HomeFeedContainer(): JSX.Element { } break case 'archive': - if (multiSelectMode !== 'off') { - performMultiSelectAction(BulkAction.ARCHIVE) - } else { - performActionOnItem('archive', item) - } + performActionOnItem('archive', item) break case 'unarchive': performActionOnItem('unarchive', item) break case 'delete': - if (multiSelectMode !== 'off') { - performMultiSelectAction(BulkAction.DELETE) - } else { - performActionOnItem('delete', item) - } + performActionOnItem('delete', item) break case 'mark-read': performActionOnItem('mark-read', item) From 6959f57fbecd51dab55806abc23a598c39905fd5 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 4 Dec 2023 13:35:05 +0800 Subject: [PATCH 15/35] Sentry version --- packages/api/package.json | 2 +- yarn.lock | 96 ++++++++++++++------------------------- 2 files changed, 35 insertions(+), 63 deletions(-) diff --git a/packages/api/package.json b/packages/api/package.json index ff7513d85..5a00c1135 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -38,7 +38,7 @@ "@opentelemetry/tracing": "^0.24.0", "@sendgrid/mail": "^7.6.0", "@sentry/integrations": "^7.10.0", - "@sentry/node": "^5.26.0", + "@sentry/node": "^7.9.0", "@sentry/tracing": "^7.9.0", "addressparser": "^1.0.1", "analytics-node": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index f5c3158be..ba65d3e0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5336,6 +5336,15 @@ "@sentry/types" "7.77.0" "@sentry/utils" "7.77.0" +"@sentry-internal/tracing@7.84.0": + version "7.84.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.84.0.tgz#430da253ee5b075be4ef57f20ea842c0208bc6b0" + integrity sha512-y9bGYA0OM6PEREfd+nk4UURZy29tpIw+7vQwpxWfEVs2fqq0/5TBFX/tKFb8AKUI9lVM8v0bcF0bNSCnuPQZHQ== + dependencies: + "@sentry/core" "7.84.0" + "@sentry/types" "7.84.0" + "@sentry/utils" "7.84.0" + "@sentry/browser@7.50.0": version "7.50.0" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.50.0.tgz#16c995c336322c8aec65570f90f50288678004ec" @@ -5360,17 +5369,6 @@ proxy-from-env "^1.1.0" which "^2.0.2" -"@sentry/core@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" - integrity sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg== - dependencies: - "@sentry/hub" "5.30.0" - "@sentry/minimal" "5.30.0" - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - tslib "^1.9.3" - "@sentry/core@7.50.0": version "7.50.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.50.0.tgz#88bc9cbfc0cb429a28489ece6f0be7a7006436c4" @@ -5388,14 +5386,13 @@ "@sentry/types" "7.77.0" "@sentry/utils" "7.77.0" -"@sentry/hub@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.30.0.tgz#2453be9b9cb903404366e198bd30c7ca74cdc100" - integrity sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ== +"@sentry/core@7.84.0": + version "7.84.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.84.0.tgz#01d33fc452044ffd8ea57b20f60304b9cfa2b9e1" + integrity sha512-tbuwunbBx2kSex15IHCqHDnrMfIlqPc6w/76fwkGqokz3oh9GSEGlLICwmBWL8AypWimUg13IDtFpD0TJTriWA== dependencies: - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - tslib "^1.9.3" + "@sentry/types" "7.84.0" + "@sentry/utils" "7.84.0" "@sentry/integrations@7.50.0": version "7.50.0" @@ -5417,15 +5414,6 @@ "@sentry/utils" "7.77.0" localforage "^1.8.1" -"@sentry/minimal@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.30.0.tgz#ce3d3a6a273428e0084adcb800bc12e72d34637b" - integrity sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw== - dependencies: - "@sentry/hub" "5.30.0" - "@sentry/types" "5.30.0" - tslib "^1.9.3" - "@sentry/nextjs@^7.42.0": version "7.50.0" resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.50.0.tgz#b9e7727c8f974644bb84a01f40a0adedb44ec416" @@ -5469,20 +5457,16 @@ "@sentry/utils" "7.77.0" https-proxy-agent "^5.0.0" -"@sentry/node@^5.26.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.30.0.tgz#4ca479e799b1021285d7fe12ac0858951c11cd48" - integrity sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg== +"@sentry/node@^7.9.0": + version "7.84.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.84.0.tgz#c06167106796b2b83c0a9b52fa56f8ca820034ca" + integrity sha512-Xm3fIXT3TZOQi+6uQBavI8iOehD3PkY7v0y3hog0d4lQTH88vQK9BBsI+jZEq81Em+RG/u7vZNiFo6YMTnWF7Q== dependencies: - "@sentry/core" "5.30.0" - "@sentry/hub" "5.30.0" - "@sentry/tracing" "5.30.0" - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - cookie "^0.4.1" + "@sentry-internal/tracing" "7.84.0" + "@sentry/core" "7.84.0" + "@sentry/types" "7.84.0" + "@sentry/utils" "7.84.0" https-proxy-agent "^5.0.0" - lru_map "^0.3.3" - tslib "^1.9.3" "@sentry/react@7.50.0": version "7.50.0" @@ -5516,17 +5500,6 @@ "@types/aws-lambda" "^8.10.62" "@types/express" "^4.17.14" -"@sentry/tracing@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.30.0.tgz#501d21f00c3f3be7f7635d8710da70d9419d4e1f" - integrity sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw== - dependencies: - "@sentry/hub" "5.30.0" - "@sentry/minimal" "5.30.0" - "@sentry/types" "5.30.0" - "@sentry/utils" "5.30.0" - tslib "^1.9.3" - "@sentry/tracing@^7.9.0": version "7.77.0" resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.77.0.tgz#39d7c30834f503fe9eb20ce1c8c8bd28f7d7c9ce" @@ -5534,11 +5507,6 @@ dependencies: "@sentry-internal/tracing" "7.77.0" -"@sentry/types@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402" - integrity sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw== - "@sentry/types@7.50.0": version "7.50.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.50.0.tgz#52a035cad83a80ca26fa53c09eb1241250c3df3e" @@ -5549,13 +5517,10 @@ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.77.0.tgz#c5d00fe547b89ccde59cdea59143bf145cee3144" integrity sha512-nfb00XRJVi0QpDHg+JkqrmEBHsqBnxJu191Ded+Cs1OJ5oPXEW6F59LVcBScGvMqe+WEk1a73eH8XezwfgrTsA== -"@sentry/utils@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.30.0.tgz#9a5bd7ccff85ccfe7856d493bffa64cabc41e980" - integrity sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww== - dependencies: - "@sentry/types" "5.30.0" - tslib "^1.9.3" +"@sentry/types@7.84.0": + version "7.84.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.84.0.tgz#e8db86c36c61659c3b2558f0aa8b6a073a756117" + integrity sha512-VqGLIF3JOUrk7yIXjLXJvAORkZL1e3dDX0Q1okRehwyt/5CRE+mdUTeJZkBo9P9mBwgMyvtwklzOGGrzjb4eMA== "@sentry/utils@7.50.0": version "7.50.0" @@ -5572,6 +5537,13 @@ dependencies: "@sentry/types" "7.77.0" +"@sentry/utils@7.84.0": + version "7.84.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.84.0.tgz#32861d922fa31e86dd2863a1d9dfc5a369e98952" + integrity sha512-qdUVuxnRBvaf05AU+28R+xYtZmi/Ymf8os3Njq9g4XuA+QEkZLbzmIpRK5W9Ja7vUtjOeg29Xgg43A8znde9LQ== + dependencies: + "@sentry/types" "7.84.0" + "@sentry/webpack-plugin@1.20.0": version "1.20.0" resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.20.0.tgz#e7add76122708fb6b4ee7951294b521019720e58" From 2a1336089d9e56742dafc2390eb43008ba46de7f Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 4 Dec 2023 15:54:21 +0800 Subject: [PATCH 16/35] Better naming, add use:folders option --- .../Home/Components/LibraryFeatureCardNavigationLink.swift | 2 +- .../App/Views/Home/Components/LibraryItemFetcher.swift | 1 + .../OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift | 4 ++-- .../OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift | 2 +- .../Sources/App/Views/Home/HomeFeedViewModel.swift | 4 +--- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift index ce76a1310..389a3b4a8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift @@ -51,7 +51,7 @@ struct LibraryFeatureCardNavigationLink: View { viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: true) }) Button("Remove", action: { - viewModel.removeLink(dataService: dataService, objectID: item.objectID) + viewModel.removeLibraryItem(dataService: dataService, objectID: item.objectID) }) if FeaturedItemFilter(rawValue: viewModel.featureFilter) != .pinned { Button("Mark Read", action: { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemFetcher.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemFetcher.swift index 5b1471abc..1d26dcf88 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemFetcher.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryItemFetcher.swift @@ -246,6 +246,7 @@ import Views }.joined(separator: ",")) } + query.append(" use:folders") print("QUERY: `\(query)`") return query diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 6aea45a0c..8ae879074 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -731,7 +731,7 @@ struct AnimatingCellHeight: AnimatableModifier { case .delete: return AnyView(Button( action: { - viewModel.removeLink(dataService: dataService, objectID: item.objectID) + viewModel.removeLibraryItem(dataService: dataService, objectID: item.objectID) }, label: { Label("Remove", systemImage: "trash") @@ -768,7 +768,7 @@ struct AnimatingCellHeight: AnimatableModifier { case .toggleArchiveStatus: viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: !item.isArchived) case .delete: - viewModel.removeLink(dataService: dataService, objectID: item.objectID) + viewModel.removeLibraryItem(dataService: dataService, objectID: item.objectID) case .editLabels: viewModel.itemUnderLabelEdit = item case .editTitle: diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index 23614150f..c7d456219 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -143,7 +143,7 @@ import Views Button("Remove Link", role: .destructive) { if let itemToRemove = itemToRemove { withAnimation { - viewModel.removeLink(dataService: dataService, objectID: itemToRemove.objectID) + viewModel.removeLibraryItem(dataService: dataService, objectID: itemToRemove.objectID) self.itemToRemove = nil } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index d175cd148..325b8c0a2 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -17,8 +17,6 @@ import Views @Published var itemUnderLabelEdit: Models.LibraryItem? @Published var itemUnderTitleEdit: Models.LibraryItem? @Published var itemForHighlightsView: Models.LibraryItem? - @Published var snoozePresented = false - @Published var itemToSnoozeID: String? @Published var linkRequest: LinkRequest? @Published var presentWebContainer = false @Published var showLoadingBar = false @@ -193,7 +191,7 @@ import Views snackbar(archived ? "Link archived" : "Link moved to Inbox") } - func removeLink(dataService: DataService, objectID: NSManagedObjectID) { + func removeLibraryItem(dataService: DataService, objectID: NSManagedObjectID) { removeLibraryItemAction(dataService: dataService, objectID: objectID) } From 99406401cc3d36d895b2d70584704f5226f2dc93 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 4 Dec 2023 16:21:54 +0800 Subject: [PATCH 17/35] Add a local filter for RSS --- .../Sources/Services/InternalModels/InternalFilter.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift index f7b27ceed..f8ca774da 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift @@ -157,6 +157,11 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable { ) switch name { + case "RSS": + let feedLabelPredicate = NSPredicate( + format: "SUBQUERY(labels, $label, $label.name == \"RSS\").@count > 0" + ) + return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, undeletedPredicate, feedLabelPredicate]) case "Inbox": // non-archived items return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate]) From 01fe284d1013ed8a5a69f8ec4d1e3bb7142928d8 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 4 Dec 2023 16:28:21 +0800 Subject: [PATCH 18/35] Fix remove filter --- .../Services/InternalModels/InternalFilter.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift index f8ca774da..f2b3431c4 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift @@ -187,25 +187,25 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable { let localPDFURL = NSPredicate( format: "localPDF.length > 0" ) - let downloadedPDF = NSCompoundPredicate(andPredicateWithSubpredicates: [isPDFPredicate, localPDFURL]) + let downloadedPDF = NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, isPDFPredicate, localPDFURL]) return NSCompoundPredicate(orPredicateWithSubpredicates: [hasHTMLContent, downloadedPDF]) case "Newsletters": // non-archived or deleted items with the Newsletter label let newsletterLabelPredicate = NSPredicate( format: "SUBQUERY(labels, $label, $label.name == \"Newsletter\").@count > 0" ) - return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, newsletterLabelPredicate]) + return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate, newsletterLabelPredicate]) case "Feeds": let feedLabelPredicate = NSPredicate( format: "SUBQUERY(labels, $label, $label.name == \"RSS\").@count > 0" ) - return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, feedLabelPredicate]) + return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate, feedLabelPredicate]) case "Recommended": // non-archived or deleted items with the Newsletter label let recommendedPredicate = NSPredicate( format: "recommendations.@count > 0" ) - return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, recommendedPredicate]) + return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate, recommendedPredicate]) case "All": // include everything undeleted return undeletedPredicate @@ -216,9 +216,9 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable { return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, inArchivePredicate]) case "Deleted": let deletedPredicate = NSPredicate( - format: "%K == %i", - #keyPath(Models.LibraryItem.serverSyncStatus), - Int64(ServerSyncStatus.needsDeletion.rawValue) + format: "%K == %i OR %K == \"DELETED\"", + #keyPath(Models.LibraryItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue), + #keyPath(Models.LibraryItem.state) ) return NSCompoundPredicate(andPredicateWithSubpredicates: [deletedPredicate]) case "Files": @@ -232,6 +232,7 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable { format: "highlights.@count > 0" ) return NSCompoundPredicate(andPredicateWithSubpredicates: [ + undeletedPredicate, hasHighlightsPredicate ]) default: From 898b144710d76da41030662c0e0d72ab64afceda Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 4 Dec 2023 16:36:13 +0800 Subject: [PATCH 19/35] No deleted filter for RSS --- .../OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 325b8c0a2..887b73d0d 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -143,7 +143,7 @@ import Views filters = newFilters .filter { $0.folder == filterState.folder } .sorted(by: { $0.position < $1.position }) - + [InternalFilter.DeletedFilter, InternalFilter.DownloadedFilter] + + (folder == "inbox" ? [InternalFilter.DeletedFilter, InternalFilter.DownloadedFilter] : [InternalFilter.DownloadedFilter]) if let newFilter = filters.first(where: { $0.name.lowercased() == appliedFilterName }), newFilter.id != appliedFilter?.id { appliedFilter = newFilter From d6b30b593d869e26eec9afa43919ef4b26163db7 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 4 Dec 2023 16:52:06 +0800 Subject: [PATCH 20/35] Update move API, update savedAt when moving --- .../Services/DataService/GQLSchema.swift | 37 +++++++++---------- .../DataService/Mutations/MoveItem.swift | 17 +++++---- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift index 1a01c1269..d7f6117ab 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/GQLSchema.swift @@ -10992,7 +10992,7 @@ extension Selection where TypeLock == Never, Type == Never { extension Objects { struct MoveToFolderSuccess { let __typename: TypeName = .moveToFolderSuccess - let articleSavingRequest: [String: Objects.ArticleSavingRequest] + let success: [String: Bool] enum TypeName: String, Codable { case moveToFolderSuccess = "MoveToFolderSuccess" @@ -11012,8 +11012,8 @@ extension Objects.MoveToFolderSuccess: Decodable { let field = GraphQLField.getFieldNameFromAlias(alias) switch field { - case "articleSavingRequest": - if let value = try container.decode(Objects.ArticleSavingRequest?.self, forKey: codingKey) { + case "success": + if let value = try container.decode(Bool?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } default: @@ -11026,27 +11026,26 @@ extension Objects.MoveToFolderSuccess: Decodable { } } - articleSavingRequest = map["articleSavingRequest"] + success = map["success"] } } extension Fields where TypeLock == Objects.MoveToFolderSuccess { - func articleSavingRequest(selection: Selection) throws -> Type { - let field = GraphQLField.composite( - name: "articleSavingRequest", - arguments: [], - selection: selection.selection + func success() throws -> Bool { + let field = GraphQLField.leaf( + name: "success", + arguments: [] ) select(field) switch response { case let .decoding(data): - if let data = data.articleSavingRequest[field.alias!] { - return try selection.decode(data: data) + if let data = data.success[field.alias!] { + return data } throw HttpError.badpayload case .mocking: - return selection.mock() + return Bool.mockValue } } } @@ -29053,8 +29052,8 @@ extension Selection where TypeLock == Never, Type == Never { extension Unions { struct MoveToFolderResult { let __typename: TypeName - let articleSavingRequest: [String: Objects.ArticleSavingRequest] let errorCodes: [String: [Enums.MoveToFolderErrorCode]] + let success: [String: Bool] enum TypeName: String, Codable { case moveToFolderError = "MoveToFolderError" @@ -29075,14 +29074,14 @@ extension Unions.MoveToFolderResult: Decodable { let field = GraphQLField.getFieldNameFromAlias(alias) switch field { - case "articleSavingRequest": - if let value = try container.decode(Objects.ArticleSavingRequest?.self, forKey: codingKey) { - map.set(key: field, hash: alias, value: value as Any) - } case "errorCodes": if let value = try container.decode([Enums.MoveToFolderErrorCode]?.self, forKey: codingKey) { map.set(key: field, hash: alias, value: value as Any) } + case "success": + if let value = try container.decode(Bool?.self, forKey: codingKey) { + map.set(key: field, hash: alias, value: value as Any) + } default: throw DecodingError.dataCorrupted( DecodingError.Context( @@ -29095,8 +29094,8 @@ extension Unions.MoveToFolderResult: Decodable { __typename = try container.decode(TypeName.self, forKey: DynamicCodingKeys(stringValue: "__typename")!) - articleSavingRequest = map["articleSavingRequest"] errorCodes = map["errorCodes"] + success = map["success"] } } @@ -29111,7 +29110,7 @@ extension Fields where TypeLock == Unions.MoveToFolderResult { let data = Objects.MoveToFolderError(errorCodes: data.errorCodes) return try moveToFolderError.decode(data: data) case .moveToFolderSuccess: - let data = Objects.MoveToFolderSuccess(articleSavingRequest: data.articleSavingRequest) + let data = Objects.MoveToFolderSuccess(success: data.success) return try moveToFolderSuccess.decode(data: data) } case .mocking: diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MoveItem.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MoveItem.swift index b85b30e58..0f8b74fe0 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MoveItem.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/MoveItem.swift @@ -8,6 +8,7 @@ public extension DataService { backgroundContext.performAndWait { if let linkedItem = Models.LibraryItem.lookup(byID: itemID, inContext: backgroundContext) { linkedItem.folder = folder + linkedItem.savedAt = Date() linkedItem.serverSyncStatus = Int64(ServerSyncStatus.needsUpdate.rawValue) } do { @@ -24,19 +25,15 @@ public extension DataService { func syncMoveToFolder(itemID: String, folder: String) async throws { enum MutationResult { - case result(itemID: String) + case result(success: Bool) case error(errorMessage: String) } - let articleSavingRequestSelection = Selection.ArticleSavingRequest { - try $0.id() - } - let selection = Selection { try $0.on( moveToFolderError: .init { .error(errorMessage: try $0.errorCodes().first?.rawValue ?? "Unknown Error") }, moveToFolderSuccess: .init { - .result(itemID: try $0.articleSavingRequest(selection: articleSavingRequestSelection)) + .result(success: try $0.success()) } ) } @@ -60,8 +57,12 @@ public extension DataService { } switch payload.data { - case let .result(itemID: _): - continuation.resume() + case let .result(success: success): + if success { + continuation.resume() + } else { + continuation.resume(throwing: BasicError.message(messageText: "operation failed")) + } case let .error(errorMessage: errorMessage): continuation.resume(throwing: BasicError.message(messageText: errorMessage)) } From f48a36de795e5aac4494b74655a7075996fb63b5 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 6 Dec 2023 09:57:28 +0800 Subject: [PATCH 21/35] Fixes for the splitview and app intents --- apple/Omnivore.xcodeproj/project.pbxproj | 4 + .../Intents/SavePasteboardToOmnivore.swift | 32 --- .../Share/ShareExtensionViewModel.swift | 4 +- .../Share/Views/ShareExtensionView.swift | 2 +- .../OmnivoreKit/Sources/App/AppIntents.swift | 49 +++++ .../Components/FeedCardNavigationLink.swift | 3 +- .../App/Views/Home/HomeFeedViewIOS.swift | 13 +- .../App/Views/Home/HomeFeedViewModel.swift | 2 +- .../Sources/App/Views/LibrarySidebar.swift | 205 ++++++++++++++++++ .../Sources/App/Views/LibrarySplitView.swift | 56 +++++ .../App/Views/LinkItemDetailView.swift | 2 + .../App/Views/PrimaryContentView.swift | 105 +-------- .../App/Views/RemoveLibraryItemAction.swift | 2 +- .../WebReader/WebReaderLoadingContainer.swift | 16 +- .../DataService/Mutations/RemoveLink.swift | 2 +- apple/Sources/AppIntents.swift | 74 +++++++ 16 files changed, 415 insertions(+), 156 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/App/AppExtensions/Intents/SavePasteboardToOmnivore.swift create mode 100644 apple/OmnivoreKit/Sources/App/AppIntents.swift create mode 100644 apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift create mode 100644 apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift create mode 100644 apple/Sources/AppIntents.swift diff --git a/apple/Omnivore.xcodeproj/project.pbxproj b/apple/Omnivore.xcodeproj/project.pbxproj index 0e2288668..6e53e68b2 100644 --- a/apple/Omnivore.xcodeproj/project.pbxproj +++ b/apple/Omnivore.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 42321E892714E6B00056429F /* views in Resources */ = {isa = PBXBuildFile; fileRef = 42321E842714E6B00056429F /* views */; }; 42321E8A2714E6B00056429F /* views in Resources */ = {isa = PBXBuildFile; fileRef = 42321E842714E6B00056429F /* views */; }; 42704E7328E6BDB000C8C73E /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42704E7228E6BDAF00C8C73E /* SnapshotHelper.swift */; }; + 42B4483A2B1DD8AC00CEC5A0 /* AppIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B448392B1DD8AC00CEC5A0 /* AppIntents.swift */; }; 42E2BFB428E458E0007F29B2 /* AppStoreScreenshots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E2BFB328E458E0007F29B2 /* AppStoreScreenshots.swift */; }; 42FF1B33271154A700B38C38 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42FF1AEB271154A600B38C38 /* SafariWebExtensionHandler.swift */; }; 42FF1B34271154A700B38C38 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42FF1AEB271154A600B38C38 /* SafariWebExtensionHandler.swift */; }; @@ -212,6 +213,7 @@ 42321E822714E6B00056429F /* scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = scripts; sourceTree = ""; }; 42321E842714E6B00056429F /* views */ = {isa = PBXFileReference; lastKnownFileType = folder; path = views; sourceTree = ""; }; 42704E7228E6BDAF00C8C73E /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = ""; }; + 42B448392B1DD8AC00CEC5A0 /* AppIntents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppIntents.swift; sourceTree = ""; }; 42E2BFB128E458E0007F29B2 /* AppStoreScreenshots.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AppStoreScreenshots.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 42E2BFB328E458E0007F29B2 /* AppStoreScreenshots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreScreenshots.swift; sourceTree = ""; }; 42FF1AEB271154A600B38C38 /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = ""; }; @@ -489,6 +491,7 @@ 7C54279684D6252C9CD18562 /* Sources */ = { isa = PBXGroup; children = ( + 42B448392B1DD8AC00CEC5A0 /* AppIntents.swift */, 0480E71B26D95096006CAE2F /* AppDelegate.swift */, D81BE98F0CB588F5FC577A13 /* MainApp.swift */, 42FF1AEA271154A600B38C38 /* SafariExtension */, @@ -1073,6 +1076,7 @@ buildActionMask = 2147483647; files = ( 0480E71C26D95096006CAE2F /* AppDelegate.swift in Sources */, + 42B4483A2B1DD8AC00CEC5A0 /* AppIntents.swift in Sources */, 04920CC6279671EF003EC1B6 /* PushNotificationConfig.swift in Sources */, CA7EE773095F267516D7AC98 /* MainApp.swift in Sources */, ); diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Intents/SavePasteboardToOmnivore.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Intents/SavePasteboardToOmnivore.swift deleted file mode 100644 index ba4307507..000000000 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Intents/SavePasteboardToOmnivore.swift +++ /dev/null @@ -1,32 +0,0 @@ -#if os(iOS) - import AppIntents - import Services - import SwiftUI - - @available(iOS 16.0, *) - struct SaveLinkToOmnivoreIntent: AppIntent { - static var title: LocalizedStringResource = "Show Sport Progress" - - 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 { - 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/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift index bc99263ed..a2759fea8 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -51,8 +51,8 @@ public class ShareExtensionViewModel: ObservableObject { dataService.archiveLink(objectID: objectID, archived: archived) } - func removeLink(dataService: DataService, objectID: NSManagedObjectID) { - dataService.removeLink(objectID: objectID) + func removeLibraryItem(dataService: DataService, objectID: NSManagedObjectID) { + dataService.removeLibraryItem(objectID: objectID) } func submitTitleEdit(dataService: DataService, itemID: String, title: String, description: String) { diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift index 557124c08..cc5fcb6a6 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift @@ -220,7 +220,7 @@ public struct ShareExtensionView: View { Button( action: { if let linkedItem = self.viewModel.linkedItem { - self.viewModel.removeLink(dataService: self.viewModel.services.dataService, objectID: linkedItem.objectID) + self.viewModel.removeLibraryItem(dataService: self.viewModel.services.dataService, objectID: linkedItem.objectID) messageText = "Link Removed" DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { extensionContext?.completeRequest(returningItems: [], completionHandler: nil) diff --git a/apple/OmnivoreKit/Sources/App/AppIntents.swift b/apple/OmnivoreKit/Sources/App/AppIntents.swift new file mode 100644 index 000000000..4ce8ebb0c --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/AppIntents.swift @@ -0,0 +1,49 @@ +#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/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index b454125fc..4154a14cc 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -51,11 +51,10 @@ struct FeedCardNavigationLink: View { linkedItemObjectID: item.objectID, isPDF: item.isPDF ) - .background(ThemeManager.currentBgColor) }, label: { EmptyView() } - ).opacity(0) + ) } .onAppear { Task { await viewModel.itemAppeared(item: item, dataService: dataService) } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 8ae879074..3027bbcf9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -188,14 +188,13 @@ struct AnimatingCellHeight: AnimatableModifier { } .frame(maxWidth: .infinity, alignment: .bottomLeading) } + ToolbarItem(placement: .barTrailing) { - Button("", action: {}) - .disabled(true) - .overlay { - if viewModel.isLoading, !prefersListLayout, enableGrid { - ProgressView() - } - } + if UIDevice.isIPad, viewModel.folder == "inbox" { + Button(action: { addLinkPresented = true }, label: { + Label("Add Link", systemImage: "plus") + }) + } } ToolbarItem(placement: UIDevice.isIPhone ? .barLeading : .barTrailing) { if enableGrid { diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 887b73d0d..a71048cf5 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -79,7 +79,7 @@ import Views } func loadFilters(dataService: DataService) async { - switch filterState.folder { + switch folder { case "following": updateFilters(newFilters: InternalFilter.DefaultFollowingFilters, defaultName: "rss") default: diff --git a/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift b/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift new file mode 100644 index 000000000..09a7b8bfa --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift @@ -0,0 +1,205 @@ + +import Foundation +import Services +import SwiftUI + +@MainActor struct LibrarySidebar: View { + @ObservedObject var inboxViewModel: HomeFeedViewModel + @ObservedObject var followingViewModel: HomeFeedViewModel + + @EnvironmentObject var dataService: DataService + + @State private var addLinkPresented = false + @State private var showProfile = false + @State private var selection: String? + + @State private var selectedFilter: InternalFilter? + + @AppStorage("inboxActive") private var inboxActive = true + @AppStorage("followingActive") private var followingActive = false + + @AppStorage("inboxMenuState") var inboxMenuState = "open" + @AppStorage("followingMenuState") var followingMenuState = "open" + + func createInboxViewModel(_ filter: InternalFilter) -> HomeFeedViewModel { + let result = HomeFeedViewModel( + folder: "inbox", + fetcher: LibraryItemFetcher(), + listConfig: LibraryListConfig( + hasFeatureCards: true, + leadingSwipeActions: [.pin], + trailingSwipeActions: [.archive, .delete], + cardStyle: .library + ) + ) + result.appliedFilter = filter + return result + } + + func createFollowingViewModel(_ filter: InternalFilter) -> HomeFeedViewModel { + let result = HomeFeedViewModel( + folder: "following", + fetcher: LibraryItemFetcher(), + listConfig: LibraryListConfig( + hasFeatureCards: false, + leadingSwipeActions: [.moveToInbox], + trailingSwipeActions: [.archive, .delete], + cardStyle: .library + ) + ) + result.appliedFilter = filter + return result + } + + var innerBody: some View { + ZStack { + NavigationLink("", destination: HomeView(viewModel: inboxViewModel), isActive: $inboxActive) + NavigationLink("", destination: HomeView(viewModel: followingViewModel), isActive: $followingActive) + + List { + Section { + Button(action: { inboxMenuState = inboxMenuState == "open" ? "closed" : "open" }, label: { + HStack { + Image.tabLibrary + Text("Library") + Spacer() + + if inboxMenuState == "open" { + Image(systemName: "chevron.down") + } else { + Image(systemName: "chevron.right") + } + } + }) + + if inboxMenuState == "open" { + ForEach(inboxViewModel.filters, id: \.self) { filter in + Button(action: { + inboxViewModel.appliedFilter = filter + selectedFilter = filter + followingActive = false + inboxActive = true + }, label: { + HStack { + Spacer().frame(width: 35) + Text(filter.name) + .lineLimit(1) + } + }) + .listRowBackground( + selectedFilter == filter && inboxActive + ? Color.systemBackground.cornerRadius(8) : Color.clear.cornerRadius(8) + ) + } + } + } + + Section { + Button(action: { followingMenuState = followingMenuState == "open" ? "closed" : "open" }, label: { + HStack { + Image.tabLibrary + Text("Following") + Spacer() + + if followingMenuState == "open" { + Image(systemName: "chevron.down") + } else { + Image(systemName: "chevron.right") + } + } + }) + + if followingMenuState == "open" { + ForEach(followingViewModel.filters, id: \.self) { filter in + Button(action: { + followingViewModel.appliedFilter = filter + selectedFilter = filter + inboxActive = false + followingActive = true + }, label: { + HStack { + Spacer().frame(width: 35) + Text(filter.name) + .lineLimit(1) + } + }) + .listRowBackground( + selectedFilter == filter && followingActive + ? Color.systemBackground.cornerRadius(8) : Color.clear.cornerRadius(8) + ) + } + } + } + } + .listStyle(.sidebar) + .dynamicTypeSize(.small ... .large) + .sheet(isPresented: $addLinkPresented) { + NavigationView { + LibraryAddLinkView() + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + #endif + } + } + } + .sheet(isPresented: $showProfile) { + NavigationView { + ProfileView() + .toolbar { + ToolbarItem(placement: .barTrailing) { + Button(action: { showProfile = false }, label: { + Text("Close") + .bold() + }) + } + } + } + }.onAppear { + Task { + await inboxViewModel.loadFilters(dataService: dataService) + await followingViewModel.loadFilters(dataService: dataService) + + if inboxActive { + selectedFilter = inboxViewModel.appliedFilter + } else { + selectedFilter = followingViewModel.appliedFilter + } + } + }.onChange(of: inboxViewModel.appliedFilter) { filter in + // When the user uses the dropdown menu to change filter we need to update in the sidebar + if inboxActive, filter != selectedFilter { + selectedFilter = filter + } + }.onChange(of: followingViewModel.appliedFilter) { filter in + if followingActive, filter != selectedFilter { + selectedFilter = filter + } + } + } + + var body: some View { + #if os(iOS) + innerBody + .toolbar { + ToolbarItem(placement: .barTrailing) { + Button(action: { showProfile = true }, label: { Image.tabProfile }) + } + } + #elseif os(macOS) + innerBody + .frame(minWidth: 200) + .toolbar { + ToolbarItem { + Button( + action: { + NSApp.keyWindow?.firstResponder?.tryToPerform( + #selector(NSSplitViewController.toggleSidebar(_:)), with: nil + ) + }, + label: { Label(LocalText.navigationSelectSidebarToggle, systemImage: "sidebar.left") } + ) + } + } + #endif + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift b/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift new file mode 100644 index 000000000..f010f039a --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift @@ -0,0 +1,56 @@ +import Foundation +import SwiftUI + +@MainActor +public struct LibrarySplitView: View { + @StateObject private var inboxViewModel = HomeFeedViewModel( + folder: "inbox", + fetcher: LibraryItemFetcher(), + listConfig: LibraryListConfig( + hasFeatureCards: true, + leadingSwipeActions: [.pin], + trailingSwipeActions: [.archive, .delete], + cardStyle: .library + ) + ) + + @StateObject private var followingViewModel = HomeFeedViewModel( + folder: "following", + fetcher: LibraryItemFetcher(), + listConfig: LibraryListConfig( + hasFeatureCards: false, + leadingSwipeActions: [.moveToInbox], + trailingSwipeActions: [.archive, .delete], + cardStyle: .library + ) + ) + + #if os(iOS) + public var body: some View { + NavigationView { + LibrarySidebar(inboxViewModel: inboxViewModel, followingViewModel: followingViewModel) + .navigationBarTitleDisplayMode(.inline) + .tag("inbox") + + HomeFeedContainerView(viewModel: inboxViewModel) + .navigationViewStyle(.stack) + .tag("following") + } + .navigationBarTitleDisplayMode(.inline) + .accentColor(.appGrayTextContrast) + .introspectSplitViewController { + $0.preferredPrimaryColumnWidth = 230 + $0.displayModeButtonVisibility = .always + } + } + #endif + + #if os(macOS) + public var body: some View { + NavigationView { + LibraryListView() + Text(LocalText.navigationSelectLink) + } + } + #endif +} diff --git a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift index 8fa4a26bf..d3b66c479 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LinkItemDetailView.swift @@ -91,8 +91,10 @@ struct LinkItemDetailView: View { pdfContainerView .navigationBarBackButtonHidden(false) } + .navigationViewStyle(.stack) } else if let item = viewModel.item { WebReaderContainerView(item: item, pop: { dismiss() }) + .background(ThemeManager.currentBgColor) } } .ignoresSafeArea(.all, edges: .bottom) diff --git a/apple/OmnivoreKit/Sources/App/Views/PrimaryContentView.swift b/apple/OmnivoreKit/Sources/App/Views/PrimaryContentView.swift index 6ce2f22d4..95ff33563 100644 --- a/apple/OmnivoreKit/Sources/App/Views/PrimaryContentView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/PrimaryContentView.swift @@ -4,11 +4,6 @@ import SwiftUI import Views @MainActor public struct PrimaryContentView: View { - let categories = [ - PrimaryContentCategory.feed, - PrimaryContentCategory.profile - ] - @State var searchTerm: String = "" public var body: some View { @@ -18,8 +13,9 @@ import Views public var innerBody: some View { #if os(iOS) if UIDevice.isIPad { - return AnyView(LibraryTabView()) - // return AnyView(splitView) + return AnyView( + LibrarySplitView() + ) } else { return AnyView( LibraryTabView() @@ -29,99 +25,4 @@ import Views return AnyView(splitView) #endif } - - #if os(macOS) - private var splitView: some View { - NavigationView { - PrimaryContentCategory.feed.destinationView - Text(LocalText.navigationSelectLink) - } - } - #endif - - #if os(iOS) - private var splitView: some View { - NavigationView { - // The first column is the sidebar. - PrimaryContentSidebar(categories: categories) - .navigationBarTitleDisplayMode(.inline) - - // Second column is the Primary Nav Stack - PrimaryContentCategory.feed.destinationView - .navigationBarTitleDisplayMode(.inline) - } - .navigationBarTitleDisplayMode(.inline) - .accentColor(.appGrayTextContrast) - .introspectSplitViewController { - $0.preferredPrimaryColumnWidth = 160 - $0.displayModeButtonVisibility = .always - } - } - #endif -} - -@MainActor struct PrimaryContentSidebar: View { - @State private var addLinkPresented = false - @State private var showProfile = false - @State private var selectedCategory: PrimaryContentCategory? - let categories: [PrimaryContentCategory] - - var innerBody: some View { - List { - NavigationLink( - destination: PrimaryContentCategory.feed.destinationView, - tag: PrimaryContentCategory.feed, - selection: $selectedCategory, - label: { PrimaryContentCategory.feed.listLabel } - ) - .listRowBackground(Color.systemBackground.cornerRadius(8)) - - Button(action: { showProfile = true }, label: { - PrimaryContentCategory.profile.listLabel - }) - - Button(action: { addLinkPresented = true }, label: { - Label("Add Link", systemImage: "plus.circle") - }) - } - .dynamicTypeSize(.small ... .large) - .listStyle(.sidebar) - .sheet(isPresented: $addLinkPresented) { - NavigationView { - LibraryAddLinkView() - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif - } - } - .sheet(isPresented: $showProfile) { - NavigationView { - PrimaryContentCategory.profile.destinationView - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif - } - } - } - - var body: some View { - #if os(iOS) - innerBody - #elseif os(macOS) - innerBody - .frame(minWidth: 200) - .toolbar { - ToolbarItem { - Button( - action: { - NSApp.keyWindow?.firstResponder?.tryToPerform( - #selector(NSSplitViewController.toggleSidebar(_:)), with: nil - ) - }, - label: { Label(LocalText.navigationSelectSidebarToggle, systemImage: "sidebar.left") } - ) - } - } - #endif - } } diff --git a/apple/OmnivoreKit/Sources/App/Views/RemoveLibraryItemAction.swift b/apple/OmnivoreKit/Sources/App/Views/RemoveLibraryItemAction.swift index 763b4b191..96aad9260 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RemoveLibraryItemAction.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RemoveLibraryItemAction.swift @@ -25,7 +25,7 @@ func removeLibraryItemAction(dataService: DataService, objectID: NSManagedObject let canceled = Task.isCancelled if !canceled { print("syncing link deletion") - dataService.removeLink(objectID: objectID, sync: true) + dataService.removeLibraryItem(objectID: objectID, sync: true) } } catch { print("error running task: ", error) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift index ae05dd6f0..2775571af 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -48,7 +48,7 @@ public struct WebReaderLoadingContainer: View { .navigationBarHidden(true) .navigationViewStyle(.stack) .accentColor(.appGrayTextContrast) - .task { viewModel.trackReadEvent() } + .onAppear { viewModel.trackReadEvent() } #else if let pdfURL = pdfItem.pdfURL { PDFWrapperView(pdfURL: pdfURL) @@ -60,17 +60,19 @@ public struct WebReaderLoadingContainer: View { .navigationViewStyle(.stack) #endif .accentColor(.appGrayTextContrast) - .task { viewModel.trackReadEvent() } + .onAppear { viewModel.trackReadEvent() } } } else if let errorMessage = viewModel.errorMessage { Text(errorMessage) } else { ProgressView() - .task { - if let username = dataService.currentViewer?.username { - await viewModel.loadItem(dataService: dataService, username: username, requestID: requestID) - } else { - viewModel.errorMessage = "You are not logged in." + .onAppear { + Task { + if let username = dataService.currentViewer?.username { + await viewModel.loadItem(dataService: dataService, username: username, requestID: requestID) + } else { + viewModel.errorMessage = "You are not logged in." + } } } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLink.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLink.swift index eeccc08da..660d08fda 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLink.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/RemoveLink.swift @@ -4,7 +4,7 @@ import Models import SwiftGraphQL public extension DataService { - func removeLink(objectID: NSManagedObjectID, sync: Bool = true) { + func removeLibraryItem(objectID: NSManagedObjectID, sync: Bool = true) { // First try to get the item synchronously, this is used later to delete files // Then we can async update core data and make the API call to sync the deletion diff --git a/apple/Sources/AppIntents.swift b/apple/Sources/AppIntents.swift new file mode 100644 index 000000000..2152529bb --- /dev/null +++ b/apple/Sources/AppIntents.swift @@ -0,0 +1,74 @@ +#if os(iOS) + import App + import AppIntents + import Firebase + import FirebaseMessaging + import Foundation + import Models + import Services + import UIKit + import Utils + + @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 & 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) + } + return .result(dialog: "Error saving link") + } + } + + @available(iOS 16.4, *) + struct ReadInOmnivoreIntent: ForegroundContinuableIntent { + static var title: LocalizedStringResource = "Save and read a URL in Omnivore" + static var openAppWhenRun: Bool = false + + @Parameter(title: "link") + var link: URL + + @MainActor + func perform() async throws -> some IntentResult & ProvidesDialog { + let requestId = UUID().uuidString.lowercased() + _ = try? await Services().dataService.saveURL(id: requestId, url: link.absoluteString) + + throw needsToContinueInForegroundError("Please continue to open the app.") { + UIApplication.shared.open(URL(string: "omnivore://read/\(requestId)")!) + } + + return .result(dialog: "I opened the app.") + } + } + +#endif From 89f21ad2284d1d712a27a0ac0df024b236cf84e1 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 6 Dec 2023 11:25:04 +0800 Subject: [PATCH 22/35] Use task instead of onAppear for async, add some PDF download debugging --- .../Components/FeedCardNavigationLink.swift | 14 ++++---------- .../App/Views/Home/HomeFeedViewModel.swift | 3 ++- .../App/Views/Labels/ApplyLabelsView.swift | 18 ++++++++---------- .../Sources/App/Views/LibrarySidebar.swift | 16 +++++++--------- .../WebReader/WebReaderLoadingContainer.swift | 12 +++++------- .../DataService/Public/PDFLoading.swift | 9 ++++++++- 6 files changed, 34 insertions(+), 38 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index 4154a14cc..a52d67cfd 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -56,8 +56,8 @@ struct FeedCardNavigationLink: View { } ) } - .onAppear { - Task { await viewModel.itemAppeared(item: item, dataService: dataService) } + .task { + await viewModel.itemAppeared(item: item, dataService: dataService) } } } @@ -92,14 +92,8 @@ struct GridCardNavigationLink: View { GridCard(item: item, isContextMenuOpen: $isContextMenuOpen, actionHandler: actionHandler) } ) -// NavigationLink(destination: LinkItemDetailView( -// linkedItemObjectID: item.objectID, -// isPDF: item.isPDF -// )) { -// -// } - .onAppear { - Task { await viewModel.itemAppeared(item: item, dataService: dataService) } + .task { + await viewModel.itemAppeared(item: item, dataService: dataService) } .aspectRatio(1.0, contentMode: .fill) .background( diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index a71048cf5..03f63bd66 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -262,7 +262,7 @@ import Views try await dataService.moveItem(itemID: item.unwrappedID, folder: folder) snackbar("Item moved") } catch { - snackbar("Error performing operation") + snackbar("Error moving item to \(folder)") } } } @@ -277,6 +277,7 @@ import Views try await dataService.bulkAction(action: action, items: items) snackbar("Operation completed") } catch { + print("ERROR: ", error) snackbar("Error performing operation") } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index 8496a693e..735e8c8cc 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -106,16 +106,14 @@ struct ApplyLabelsView: View { .sheet(isPresented: $viewModel.showCreateLabelModal) { CreateLabelView(viewModel: viewModel, newLabelName: viewModel.labelSearchFilter) } - .onAppear { - Task { - switch mode { - case let .item(feedItem): - await viewModel.loadLabels(dataService: dataService, item: feedItem) - case let .highlight(highlight): - await viewModel.loadLabels(dataService: dataService, highlight: highlight) - case let .list(labels): - await viewModel.loadLabels(dataService: dataService, initiallySelectedLabels: labels) - } + .task { + switch mode { + case let .item(feedItem): + await viewModel.loadLabels(dataService: dataService, item: feedItem) + case let .highlight(highlight): + await viewModel.loadLabels(dataService: dataService, highlight: highlight) + case let .list(labels): + await viewModel.loadLabels(dataService: dataService, initiallySelectedLabels: labels) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift b/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift index 09a7b8bfa..b8b590248 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift @@ -154,16 +154,14 @@ import SwiftUI } } } - }.onAppear { - Task { - await inboxViewModel.loadFilters(dataService: dataService) - await followingViewModel.loadFilters(dataService: dataService) + }.task { + await inboxViewModel.loadFilters(dataService: dataService) + await followingViewModel.loadFilters(dataService: dataService) - if inboxActive { - selectedFilter = inboxViewModel.appliedFilter - } else { - selectedFilter = followingViewModel.appliedFilter - } + if inboxActive { + selectedFilter = inboxViewModel.appliedFilter + } else { + selectedFilter = followingViewModel.appliedFilter } }.onChange(of: inboxViewModel.appliedFilter) { filter in // When the user uses the dropdown menu to change filter we need to update in the sidebar diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift index 2775571af..2509cb9a6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderLoadingContainer.swift @@ -66,13 +66,11 @@ public struct WebReaderLoadingContainer: View { Text(errorMessage) } else { ProgressView() - .onAppear { - Task { - if let username = dataService.currentViewer?.username { - await viewModel.loadItem(dataService: dataService, username: username, requestID: requestID) - } else { - viewModel.errorMessage = "You are not logged in." - } + .task { + if let username = dataService.currentViewer?.username { + await viewModel.loadItem(dataService: dataService, username: username, requestID: requestID) + } else { + viewModel.errorMessage = "You are not logged in." } } } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift index e892c3299..3273ea68a 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift @@ -9,7 +9,14 @@ public extension DataService { throw BasicError.message(messageText: "No PDF URL found") } - let result: (Data, URLResponse)? = try? await URLSession.shared.data(from: url) + var result: (Data, URLResponse)? + do { + let request = URLRequest(url: url, timeoutInterval: 120) + result = try await URLSession.shared.data(for: request) + } catch { + print("ERROR DOWNLOADING PDF DATA: ", error) + print("URL", url) + } guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else { throw BasicError.message(messageText: "pdfFetch failed. no response or bad status code.") From 663ea1d3f88b86586692f586e7e16efc2c4962aa Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 6 Dec 2023 14:01:43 +0800 Subject: [PATCH 23/35] Move miniplayer around to work with new layout on iPhone --- .../AudioPlayer/AudioToolbarButton.swift | 44 +++++ .../App/Views/AudioPlayer/MiniPlayer.swift | 147 +--------------- .../Views/AudioPlayer/MiniPlayerViewer.swift | 157 ++++++++++++++++++ .../App/Views/Labels/MarqueTextView.swift | 98 ----------- .../Sources/App/Views/LibraryTabView.swift | 20 ++- .../Sources/App/Views/RootView/RootView.swift | 3 - .../Views/WebReader/WebReaderContainer.swift | 14 ++ .../DataService/Public/PDFLoading.swift | 1 - 8 files changed, 235 insertions(+), 249 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/AudioPlayer/AudioToolbarButton.swift create mode 100644 apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayerViewer.swift delete mode 100644 apple/OmnivoreKit/Sources/App/Views/Labels/MarqueTextView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/AudioToolbarButton.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/AudioToolbarButton.swift new file mode 100644 index 000000000..d7d961e87 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/AudioToolbarButton.swift @@ -0,0 +1,44 @@ + +import Foundation +import SwiftUI + +struct AudioToolbarButton: View { + @State private var drawingHeight = true + + var animation: Animation { + .linear(duration: 0.5).repeatForever() + } + + var body: some View { + ZStack { + Circle() + .stroke(Color.black) + .frame(width: 21, height: 21) + +// HStack(spacing: 1) { +// bar(low: 0.4) +// .animation(animation.speed(1.5), value: drawingHeight) +// bar(low: 0.3) +// .animation(animation.speed(1.2), value: drawingHeight) +// bar(low: 0.5) +// .animation(animation.speed(1.0), value: drawingHeight) +// bar(low: 0.3) +// .animation(animation.speed(1.7), value: drawingHeight) +// // bar(low: 0.5) +// // .animation(animation.speed(1.0), value: drawingHeight) +// } +// .frame(width: 20) +// .onAppear { +// drawingHeight.toggle() +// } + } + } + + func bar(low: CGFloat = 0.0, high: CGFloat = 1.0) -> some View { + RoundedRectangle(cornerRadius: 3) + .fill(Color.black) + .frame(width: 1) + .frame(height: (drawingHeight ? high : low) * 15) + .frame(height: 15, alignment: .bottom) + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift index 861da9bdb..e86a00351 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift @@ -20,150 +20,6 @@ self.presentingView = AnyView(presentingView) } - var playPauseButtonImage: String { - switch audioController.state { - case .playing: - return "pause.circle" - case .paused: - return "play.circle" - case .reachedEnd: - return "gobackward" - default: - return "" - } - } - - var playPauseButtonItem: some View { - if audioController.playbackError { - return AnyView(Color.clear) - } - if let itemID = audioController.itemAudioProperties?.itemID, audioController.isLoadingItem(itemID: itemID) { - return AnyView(ProgressView()) - } else { - return AnyView(Button( - action: { - switch audioController.state { - case .playing: - audioController.pause() - case .paused: - audioController.unpause() - case .reachedEnd: - audioController.seek(to: 0.0) - audioController.unpause() - default: - break - } - }, - label: { - Image(systemName: playPauseButtonImage) - .resizable(resizingMode: Image.ResizingMode.stretch) - .aspectRatio(contentMode: .fit) - .font(Font.title.weight(.light)) - } - )) - } - } - - var stopButton: some View { - Button( - action: { - audioController.stop() - }, - label: { - ZStack { - Circle() - .foregroundColor(Color(hex: "#3D3D3D")) - - Image(systemName: "xmark") - .resizable(resizingMode: Image.ResizingMode.stretch) - .foregroundColor(Color(hex: "#D9D9D9")) - .aspectRatio(contentMode: .fit) - .font(Font.title.weight(.medium)) - .frame(width: 14, height: 14) - } - } - ) - .background(Color.clear) - .buttonStyle(PlainButtonStyle()) - } - - func artwork(_ itemAudioProperties: LinkedItemAudioProperties, forDimensions dim: Double) -> some View { - if let imageURL = itemAudioProperties.imageURL { - return AnyView(AsyncImage(url: imageURL) { phase in - if let image = phase.image { - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: dim, height: dim) - .cornerRadius(6) - } else if phase.error != nil { - defaultArtwork(forDimensions: dim) - } else { - Color.appButtonBackground - .frame(width: dim, height: dim) - .cornerRadius(6) - } - }) - } - return AnyView(defaultArtwork(forDimensions: dim)) - } - - func defaultArtwork(forDimensions dim: Double) -> some View { - ZStack(alignment: .center) { - Color.appButtonBackground - .frame(width: dim, height: dim) - .cornerRadius(6) - - Image.headphones - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: dim / 2, height: dim / 2) - } - } - - func playerContent(_ itemAudioProperties: LinkedItemAudioProperties) -> some View { - VStack(spacing: 0) { - HStack(alignment: .center, spacing: 15) { - if audioController.playbackError { - Text("There was an error playing back your audio.").foregroundColor(Color.red).font(.footnote) - Spacer(minLength: 0) - } else { - artwork(itemAudioProperties, forDimensions: 50) - - Text(itemAudioProperties.title) - .font(Font.system(size: 17, weight: .medium)) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(2) - .foregroundColor(.appGrayTextContrast) - .frame(maxHeight: 40, alignment: .leading) - - Spacer(minLength: 0) - - playPauseButtonItem - .frame(width: 40, height: 40) - .foregroundColor(.themeAudioPlayerGray) - } - stopButton - .frame(width: 40, height: 40) - .foregroundColor(.themeAudioPlayerGray) - } - .padding(16) - .frame(maxHeight: .infinity) - } - .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .background( - Color.systemBackground - .shadow(color: .gray.opacity(0.33), radius: 8, x: 0, y: 4) - .mask(Rectangle().padding(.top, -20)) - ) - .onTapGesture { - withAnimation(.easeIn(duration: 0.08)) { expanded = true } - }.fullScreenCover(isPresented: $expanded) { - ExpandedPlayer() - } - .offset(y: expanded ? 110 : 0) - } - public var body: some View { ZStack(alignment: .center) { presentingView @@ -174,8 +30,7 @@ VStack { Spacer(minLength: 0) - playerContent(itemAudioProperties) - .frame(maxHeight: expanded ? 0 : 110) + MiniPlayerViewer(itemAudioProperties: itemAudioProperties) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayerViewer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayerViewer.swift new file mode 100644 index 000000000..11451ee2e --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayerViewer.swift @@ -0,0 +1,157 @@ +#if os(iOS) + + import Foundation + import Models + import Services + import SwiftUI + import Utils + import Views + + public struct MiniPlayerViewer: View { + @EnvironmentObject var audioController: AudioController + @Environment(\.colorScheme) private var colorScheme: ColorScheme + + @State var expanded = true + + let itemAudioProperties: LinkedItemAudioProperties + + var playPauseButtonImage: String { + switch audioController.state { + case .playing: + return "pause.circle" + case .paused: + return "play.circle" + case .reachedEnd: + return "gobackward" + default: + return "" + } + } + + var playPauseButtonItem: some View { + if audioController.playbackError { + return AnyView(Color.clear) + } + if let itemID = audioController.itemAudioProperties?.itemID, audioController.isLoadingItem(itemID: itemID) { + return AnyView(ProgressView()) + } else { + return AnyView(Button( + action: { + switch audioController.state { + case .playing: + audioController.pause() + case .paused: + audioController.unpause() + case .reachedEnd: + audioController.seek(to: 0.0) + audioController.unpause() + default: + break + } + }, + label: { + Image(systemName: playPauseButtonImage) + .resizable(resizingMode: Image.ResizingMode.stretch) + .aspectRatio(contentMode: .fit) + .font(Font.title.weight(.light)) + } + )) + } + } + + var stopButton: some View { + Button( + action: { + audioController.stop() + }, + label: { + ZStack { + Circle() + .foregroundColor(Color(hex: "#3D3D3D")) + + Image(systemName: "xmark") + .resizable(resizingMode: Image.ResizingMode.stretch) + .foregroundColor(Color(hex: "#D9D9D9")) + .aspectRatio(contentMode: .fit) + .font(Font.title.weight(.medium)) + .frame(width: 14, height: 14) + } + } + ) + .background(Color.clear) + .buttonStyle(PlainButtonStyle()) + } + + func artwork(_ itemAudioProperties: LinkedItemAudioProperties, forDimensions dim: Double) -> some View { + if let imageURL = itemAudioProperties.imageURL { + return AnyView(AsyncImage(url: imageURL) { phase in + if let image = phase.image { + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: dim, height: dim) + .cornerRadius(6) + } else if phase.error != nil { + defaultArtwork(forDimensions: dim) + } else { + Color.appButtonBackground + .frame(width: dim, height: dim) + .cornerRadius(6) + } + }) + } + return AnyView(defaultArtwork(forDimensions: dim)) + } + + func defaultArtwork(forDimensions dim: Double) -> some View { + ZStack(alignment: .center) { + Color.appButtonBackground + .frame(width: dim, height: dim) + .cornerRadius(6) + + Image.headphones + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: dim / 2, height: dim / 2) + } + } + + public var body: some View { + VStack(spacing: 0) { + HStack(alignment: .center, spacing: 15) { + if audioController.playbackError { + Text("There was an error playing back your audio.").foregroundColor(Color.red).font(.footnote) + Spacer(minLength: 0) + } else { + artwork(itemAudioProperties, forDimensions: 50) + + Text(itemAudioProperties.title) + .font(Font.system(size: 17, weight: .medium)) + .fixedSize(horizontal: false, vertical: true) + .lineLimit(2) + .foregroundColor(.appGrayTextContrast) + .frame(maxHeight: 40, alignment: .leading) + + Spacer(minLength: 0) + + playPauseButtonItem + .frame(width: 40, height: 40) + .foregroundColor(.themeAudioPlayerGray) + } + stopButton + .frame(width: 40, height: 40) + .foregroundColor(.themeAudioPlayerGray) + } + .padding(.vertical, 5) + .padding(.horizontal, 15) + .frame(maxHeight: .infinity) + } + .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .background( + Color.themeTabBarColor + ) + .frame(height: 60) + } + } + +#endif diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/MarqueTextView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/MarqueTextView.swift deleted file mode 100644 index cee36950b..000000000 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/MarqueTextView.swift +++ /dev/null @@ -1,98 +0,0 @@ -#if os(iOS) - import SwiftUI - - // Mostly from: https://kavsoft.dev/swiftui_3.0_marquee_text_animation with some customizations - - struct Marquee: View { - var text: String - var font: UIFont - - // Storing Text Size - @State var storedSize: CGSize = .zero - @State var offset: CGFloat = 0 - @State var animatedText: String = "" - - var animationSpeed: Double = 0.03 - var delayTime: Double = 3.0 - - var body: some View { - // Since it scrolls horizontal using ScrollView - GeometryReader { proxy in - - let size = proxy.size - - let condition = textSize(text: text).width < (size.width - 50) - - ScrollView(condition ? .init() : .horizontal, showsIndicators: false) { - HStack(alignment: .center) { - Spacer(minLength: 0) - Text(condition ? text : animatedText) - .font(Font(font)) - .offset(x: condition ? 0 : offset) - .padding(.horizontal, 15) - Spacer(minLength: 0) - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - } - .frame(height: storedSize.height) - .overlay(content: { - // swiftlint:disable line_length - HStack { - let color: Color = .systemBackground - - LinearGradient(colors: [color, color.opacity(0.7), color.opacity(0.5), color.opacity(0.3)], startPoint: .leading, endPoint: .trailing) - .frame(width: 8) - - Spacer() - - LinearGradient(colors: [color, color.opacity(0.7), color.opacity(0.5), color.opacity(0.3)].reversed(), startPoint: .leading, endPoint: .trailing) - .frame(width: 8) - // swiftlint:enable line_length - } - }) - .disabled(true) - .onAppear { - startAnimation(text: text) - } - .onReceive(Timer.publish(every: (animationSpeed * storedSize.width) + delayTime, - on: .main, - in: .default).autoconnect() - ) { _ in - offset = 0 - withAnimation(.linear(duration: animationSpeed * storedSize.width).delay(delayTime)) { - offset = -storedSize.width - } - } - .onChange(of: text) { newValue in - animatedText = "" - offset = 0 - startAnimation(text: newValue) - } - } - - func startAnimation(text: String) { - // Double the text with some spacing so that we can create a continuous loop - animatedText.append(text) - (1 ... 15).forEach { _ in - animatedText.append(" ") - } - storedSize = textSize(text: animatedText) - animatedText.append(text) - - let timing: Double = (animationSpeed * storedSize.width) - withAnimation(.linear(duration: timing).delay(delayTime)) { - offset = -storedSize.width - } - } - - func textSize(text: String) -> CGSize { - let attributes = [NSAttributedString.Key.font: font] - - let size = (text as NSString).size(withAttributes: attributes) - - return size - } - } - -#endif diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index f3f5eb256..00529839a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -17,8 +17,12 @@ import Views @MainActor struct LibraryTabView: View { @EnvironmentObject var dataService: DataService + @EnvironmentObject var audioController: AudioController + @AppStorage(UserDefaultKey.lastSelectedTabItem.rawValue) var selectedTab = "inbox" + @State var showExpandedAudioPlayer = false + @MainActor public init() { UITabBar.appearance().isHidden = true @@ -47,7 +51,7 @@ struct LibraryTabView: View { ) var body: some View { - VStack { + VStack(spacing: 0) { TabView(selection: $selectedTab) { NavigationView { HomeFeedContainerView(viewModel: followingViewModel) @@ -64,7 +68,21 @@ struct LibraryTabView: View { .navigationViewStyle(.stack) }.tag("profile") } + if let audioProperties = audioController.itemAudioProperties { + MiniPlayerViewer(itemAudioProperties: audioProperties) + .onTapGesture { + showExpandedAudioPlayer = true + } + .padding(0) + Color(hex: "#3D3D3D") + .frame(height: 1) + .frame(maxWidth: .infinity) + } CustomTabBar(selectedTab: $selectedTab) + .padding(0) + } + .fullScreenCover(isPresented: $showExpandedAudioPlayer) { + ExpandedPlayer() } .ignoresSafeArea() .navigationBarHidden(true) diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift index c48252a58..144c374e0 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift @@ -51,9 +51,6 @@ struct InnerRootView: View { @ViewBuilder private var innerBody: some View { if authenticator.isLoggedIn { PrimaryContentView() - #if os(iOS) - .miniPlayer() - #endif } else { WelcomeView() .accessibilityElement() diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 3e96ab26d..5688277f6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -25,6 +25,7 @@ struct WebReaderContainerView: View { @State var readerSettingsChangedTransactionID: UUID? @State var annotationSaveTransactionID: UUID? @State var showNavBarActionID: UUID? + @State var showExpandedAudioPlayer = false @State var shareActionID: UUID? @State var annotation = String() @State var showBottomBar = false @@ -463,6 +464,9 @@ struct WebReaderContainerView: View { .fullScreenCover(item: $safariWebLink) { SafariView(url: $0.url) } + .fullScreenCover(isPresented: $showExpandedAudioPlayer) { + ExpandedPlayer() + } #endif .alert(errorAlertMessage ?? LocalText.readerError, isPresented: $showErrorAlertMessage) { Button(LocalText.genericOk, role: .cancel, action: { @@ -592,7 +596,17 @@ struct WebReaderContainerView: View { self.bottomBarOpacity = 0 } } + if let audioProperties = audioController.itemAudioProperties { + MiniPlayerViewer(itemAudioProperties: audioProperties) + .padding(.top, 10) + .padding(.bottom, 40) + .background(Color.themeTabBarColor) + .onTapGesture { + showExpandedAudioPlayer = true + } + } } + #endif } #if os(macOS) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift index 3273ea68a..4e4209032 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift @@ -15,7 +15,6 @@ public extension DataService { result = try await URLSession.shared.data(for: request) } catch { print("ERROR DOWNLOADING PDF DATA: ", error) - print("URL", url) } guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else { From 73341ad578515c55d8d251486938dc4dff513694 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 6 Dec 2023 16:52:05 +0800 Subject: [PATCH 24/35] mini player for ipad layouts --- ...Player.swift => ExpandedAudioPlayer.swift} | 2 +- .../App/Views/Home/HomeFeedViewIOS.swift | 86 ++++++++++++------- .../Sources/App/Views/LibrarySidebar.swift | 2 +- .../Sources/App/Views/LibrarySplitView.swift | 5 ++ .../Sources/App/Views/LibraryTabView.swift | 2 +- .../Views/WebReader/WebReaderContainer.swift | 2 +- 6 files changed, 63 insertions(+), 36 deletions(-) rename apple/OmnivoreKit/Sources/App/Views/AudioPlayer/{ExpandedPlayer.swift => ExpandedAudioPlayer.swift} (99%) diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedPlayer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift similarity index 99% rename from apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedPlayer.swift rename to apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift index 977326be6..a60a904ea 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedPlayer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift @@ -6,7 +6,7 @@ import Views // swiftlint:disable file_length type_body_length - public struct ExpandedPlayer: View { + public struct ExpandedAudioPlayer: View { @EnvironmentObject var audioController: AudioController @Environment(\.colorScheme) private var colorScheme: ColorScheme @Environment(\.dismiss) private var dismiss diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 3027bbcf9..b5004f211 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -34,6 +34,7 @@ struct AnimatingCellHeight: AnimatableModifier { @State var listTitle = "" @State var isEditMode: EditMode = .inactive @State var showOpenAIVoices = false + @State var showExpandedAudioPlayer = false @EnvironmentObject var dataService: DataService @EnvironmentObject var audioController: AudioController @@ -63,39 +64,57 @@ struct AnimatingCellHeight: AnimatableModifier { } var body: some View { - HomeFeedView( - listTitle: $listTitle, - isListScrolled: $isListScrolled, - prefersListLayout: $prefersListLayout, - isEditMode: $isEditMode, - selection: $selection, - viewModel: viewModel, - showFeatureCards: showFeatureCards - ) - .refreshable { - loadItems(isRefresh: true) - } - .onChange(of: viewModel.presentWebContainer) { _ in - if !viewModel.presentWebContainer { - viewModel.linkRequest = nil + ZStack { + HomeFeedView( + listTitle: $listTitle, + isListScrolled: $isListScrolled, + prefersListLayout: $prefersListLayout, + isEditMode: $isEditMode, + selection: $selection, + viewModel: viewModel, + showFeatureCards: showFeatureCards + ) + .refreshable { + loadItems(isRefresh: true) + } + .onChange(of: viewModel.presentWebContainer) { _ in + if !viewModel.presentWebContainer { + viewModel.linkRequest = nil + } + } + .onChange(of: viewModel.searchTerm) { _ in + // Maybe we should debounce this, but + // it feels like it works ok without + loadItems(isRefresh: true) + } + .onChange(of: viewModel.selectedLabels) { _ in + loadItems(isRefresh: true) + } + .onChange(of: viewModel.negatedLabels) { _ in + loadItems(isRefresh: true) + } + .onChange(of: viewModel.appliedFilter) { _ in + loadItems(isRefresh: true) + } + .onChange(of: viewModel.appliedSort) { _ in + loadItems(isRefresh: true) + } + + if UIDevice.isIPad { + VStack(spacing: 0) { + Spacer() + + if let audioProperties = audioController.itemAudioProperties { + MiniPlayerViewer(itemAudioProperties: audioProperties) + .padding(.top, 10) + .padding(.bottom, 20) + .background(Color.themeTabBarColor) + .onTapGesture { + showExpandedAudioPlayer = true + } + } + } } - } - .onChange(of: viewModel.searchTerm) { _ in - // Maybe we should debounce this, but - // it feels like it works ok without - loadItems(isRefresh: true) - } - .onChange(of: viewModel.selectedLabels) { _ in - loadItems(isRefresh: true) - } - .onChange(of: viewModel.negatedLabels) { _ in - loadItems(isRefresh: true) - } - .onChange(of: viewModel.appliedFilter) { _ in - loadItems(isRefresh: true) - } - .onChange(of: viewModel.appliedSort) { _ in - loadItems(isRefresh: true) } .sheet(item: $viewModel.itemUnderLabelEdit) { item in ApplyLabelsView(mode: .item(item), onSave: nil) @@ -106,6 +125,9 @@ struct AnimatingCellHeight: AnimatableModifier { .sheet(item: $viewModel.itemForHighlightsView) { item in NotebookView(itemObjectID: item.objectID, hasHighlightMutations: $hasHighlightMutations) } + .fullScreenCover(isPresented: $showExpandedAudioPlayer) { + ExpandedAudioPlayer() + } .sheet(isPresented: $showOpenAIVoices) { OpenAIVoicesModal(audioController: audioController) } diff --git a/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift b/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift index b8b590248..e50b7c169 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift @@ -97,7 +97,7 @@ import SwiftUI Section { Button(action: { followingMenuState = followingMenuState == "open" ? "closed" : "open" }, label: { HStack { - Image.tabLibrary + Image.tabFollowing Text("Following") Spacer() diff --git a/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift b/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift index f010f039a..773338c9d 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift @@ -1,8 +1,11 @@ import Foundation +import Services import SwiftUI @MainActor public struct LibrarySplitView: View { + @EnvironmentObject var audioController: AudioController + @StateObject private var inboxViewModel = HomeFeedViewModel( folder: "inbox", fetcher: LibraryItemFetcher(), @@ -25,6 +28,8 @@ public struct LibrarySplitView: View { ) ) + @State var selected = "home" + #if os(iOS) public var body: some View { NavigationView { diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index 00529839a..85b8d63eb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -82,7 +82,7 @@ struct LibraryTabView: View { .padding(0) } .fullScreenCover(isPresented: $showExpandedAudioPlayer) { - ExpandedPlayer() + ExpandedAudioPlayer() } .ignoresSafeArea() .navigationBarHidden(true) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 5688277f6..854d4d3cf 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -465,7 +465,7 @@ struct WebReaderContainerView: View { SafariView(url: $0.url) } .fullScreenCover(isPresented: $showExpandedAudioPlayer) { - ExpandedPlayer() + ExpandedAudioPlayer() } #endif .alert(errorAlertMessage ?? LocalText.readerError, isPresented: $showErrorAlertMessage) { From 0c8eecf3fb3f0f4211198a156aaa1aed1be52b31 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 6 Dec 2023 18:12:13 +0800 Subject: [PATCH 25/35] Byline in grid cards, improve safe area handling --- .../Sources/App/PrimaryContentCategory.swift | 9 ---- .../Sources/App/Views/LibrarySplitView.swift | 1 + .../Sources/App/Views/LibraryTabView.swift | 3 +- .../App/Views/TabBar/CustomTabBar.swift | 2 +- .../Sources/Views/FeedItem/CardUtils.swift | 27 +++++++++++ .../Sources/Views/FeedItem/GridCard.swift | 46 ++++++++++++------- .../Views/FeedItem/LibraryItemCard.swift | 27 +---------- 7 files changed, 61 insertions(+), 54 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Views/FeedItem/CardUtils.swift diff --git a/apple/OmnivoreKit/Sources/App/PrimaryContentCategory.swift b/apple/OmnivoreKit/Sources/App/PrimaryContentCategory.swift index 73a5d0c1e..f2a7b0940 100644 --- a/apple/OmnivoreKit/Sources/App/PrimaryContentCategory.swift +++ b/apple/OmnivoreKit/Sources/App/PrimaryContentCategory.swift @@ -35,15 +35,6 @@ enum PrimaryContentCategory: Identifiable, Hashable, Equatable { Label { Text(title) } icon: { image.renderingMode(.template) } } - @MainActor @ViewBuilder var destinationView: some View { - switch self { - case .feed: - LibraryListView() - case .profile: - ProfileView() - } - } - func hash(into hasher: inout Hasher) { hasher.combine(id) } diff --git a/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift b/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift index 773338c9d..90e72d7f6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift @@ -39,6 +39,7 @@ public struct LibrarySplitView: View { HomeFeedContainerView(viewModel: inboxViewModel) .navigationViewStyle(.stack) + .navigationBarTitleDisplayMode(.inline) .tag("following") } .navigationBarTitleDisplayMode(.inline) diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index 85b8d63eb..fd36257a8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -55,11 +55,13 @@ struct LibraryTabView: View { TabView(selection: $selectedTab) { NavigationView { HomeFeedContainerView(viewModel: followingViewModel) + .navigationBarTitleDisplayMode(.inline) .navigationViewStyle(.stack) }.tag("following") NavigationView { HomeFeedContainerView(viewModel: libraryViewModel) + .navigationBarTitleDisplayMode(.inline) .navigationViewStyle(.stack) }.tag("inbox") @@ -84,7 +86,6 @@ struct LibraryTabView: View { .fullScreenCover(isPresented: $showExpandedAudioPlayer) { ExpandedAudioPlayer() } - .ignoresSafeArea() .navigationBarHidden(true) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift index 548f38689..5d32c63b9 100644 --- a/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift +++ b/apple/OmnivoreKit/Sources/App/Views/TabBar/CustomTabBar.swift @@ -10,7 +10,7 @@ struct CustomTabBar: View { TabBarButton(key: "profile", image: Image.tabProfile, selectedTab: $selectedTab) } .padding(.top, 10) - .padding(.bottom, 40) + .padding(.bottom, 10) .background(Color.themeTabBarColor) } } diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/CardUtils.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/CardUtils.swift new file mode 100644 index 000000000..defdcd95e --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/CardUtils.swift @@ -0,0 +1,27 @@ + +import Foundation + +func cardShouldHideUrl(_ url: String?) -> Bool { + if let url = url, let origin = URL(string: url)?.host { + let hideHosts = ["storage.googleapis.com", "omnivore.app"] + if hideHosts.contains(origin) { + return true + } + } + + return false +} + +func cardSiteName(_ originalArticleUrl: String?) -> String? { + if cardShouldHideUrl(originalArticleUrl) { + return nil + } + + if let url = originalArticleUrl, + let originalHost = URL(string: url)?.host?.replacingOccurrences(of: "^www\\.", with: "", options: .regularExpression) + { + return originalHost + } + + return nil +} diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift index 5ee53b457..17e4a1396 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift @@ -123,6 +123,34 @@ public struct GridCard: View { } } + var bylineStr: String { + // It seems like it could be cleaner just having author, instead of + // concating, maybe we fall back + if let author = item.author { + return author + } else if let publisherDisplayName = item.publisherDisplayName { + return publisherDisplayName + } + + return "" + } + + var byLine: some View { + if let origin = cardSiteName(item.pageURLString) { + Text(bylineStr + " | " + origin) + .font(.caption2) + .foregroundColor(Color.themeLibraryItemSubtle) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else { + Text(bylineStr) + .font(.caption2) + .foregroundColor(Color.themeLibraryItemSubtle) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } + } + public var body: some View { GeometryReader { geo in VStack(alignment: .leading, spacing: 0) { @@ -138,23 +166,7 @@ public struct GridCard: View { .lineLimit(1) } - HStack { - if let author = item.author { - Text("by \(author)") - .font(.appCaptionTwo) - .foregroundColor(.appGrayText) - .lineLimit(1) - } - - if let publisherDisplayName = item.publisherDisplayName { - Text(publisherDisplayName) - .font(.appCaptionTwo) - .foregroundColor(.appGrayText) - .lineLimit(1) - } - - Spacer() - } + byLine } .frame(height: 30) .padding(.horizontal, 10) diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift index af4885352..ac052ab9e 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift @@ -289,33 +289,8 @@ public struct LibraryItemCard: View { return "" } - func shouldHideUrl(_ url: String?) -> Bool { - if let url = url, let origin = URL(string: url)?.host { - let hideHosts = ["storage.googleapis.com", "omnivore.app"] - if hideHosts.contains(origin) { - return true - } - } - - return false - } - - func siteName(_ originalArticleUrl: String?) -> String? { - if shouldHideUrl(originalArticleUrl) { - return nil - } - - if let url = originalArticleUrl, - let originalHost = URL(string: url)?.host?.replacingOccurrences(of: "^www\\.", with: "", options: .regularExpression) - { - return originalHost - } - - return nil - } - var byLine: some View { - if let origin = siteName(item.pageURLString) { + if let origin = cardSiteName(item.pageURLString) { Text(bylineStr + " | " + origin) .font(.caption2) .foregroundColor(Color.themeLibraryItemSubtle) From 7bece652ae0a5882b30e91af60cc28160f207695 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 7 Dec 2023 09:55:28 +0800 Subject: [PATCH 26/35] Make safari view ignore safe area so theres no bottom gap --- .../Sources/App/Views/WebReader/WebReaderContainer.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 854d4d3cf..2649cca8d 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -463,6 +463,7 @@ struct WebReaderContainerView: View { #if os(iOS) .fullScreenCover(item: $safariWebLink) { SafariView(url: $0.url) + .ignoresSafeArea(.all, edges: .bottom) } .fullScreenCover(isPresented: $showExpandedAudioPlayer) { ExpandedAudioPlayer() From 8b7a59dc3cc3b507c3046e2e6b076458e45217ad Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 7 Dec 2023 10:36:44 +0800 Subject: [PATCH 27/35] Query for new highlights when calling findHighlight for PDFs This fixes an issue where you could not create a note on a new PDF highlight. --- apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift | 2 +- .../Sources/App/PDFSupport/PDFViewerViewModel.swift | 7 +++++-- apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift index 356a4635d..b7696be7f 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewer.swift @@ -178,7 +178,7 @@ import Utils if let highlight = annotations?.compactMap({ $0 as? HighlightAnnotation }).first, let customHighlight = highlight.customData?["omnivoreHighlight"] as? [String: String], let highlightID = customHighlight["id"]?.lowercased(), - let selectedHighlight = viewModel.findHighlight(highlightID: highlightID) + let selectedHighlight = viewModel.findHighlight(dataService: dataService, highlightID: highlightID) { addNoteHighlight = selectedHighlight annotation = selectedHighlight.annotation ?? "" diff --git a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift index 32ca44549..c1ba16706 100644 --- a/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/PDFSupport/PDFViewerViewModel.swift @@ -12,9 +12,11 @@ final class PDFViewerViewModel: ObservableObject { var snackbarMessage: String? let pdfItem: PDFItem + var highlights: [Highlight] init(pdfItem: PDFItem) { self.pdfItem = pdfItem + self.highlights = pdfItem.highlights } func snackbar(message: String) { @@ -22,8 +24,9 @@ final class PDFViewerViewModel: ObservableObject { showSnackbar = true } - func findHighlight(highlightID: String) -> Highlight? { - pdfItem.highlights.first { $0.id == highlightID } + func findHighlight(dataService: DataService, highlightID: String) -> Highlight? { + let libraryItem = LibraryItem.lookup(byID: pdfItem.itemID, inContext: dataService.viewContext) + return libraryItem?.highlights.asArray(of: Highlight.self).first { $0.id == highlightID } } func loadHighlightPatches(completion onComplete: @escaping ([String]) -> Void) { diff --git a/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift b/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift index 3dcc5c39e..4b5601d9e 100644 --- a/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift +++ b/apple/OmnivoreKit/Sources/Models/DataModels/PDFItem.swift @@ -15,7 +15,7 @@ public struct PDFItem { public let isArchived: Bool public let isRead: Bool public let downloadURL: String - public let highlights: [Highlight] + public var highlights: [Highlight] public static func make(item: LibraryItem) -> PDFItem? { guard item.isPDF else { return nil } From d48c4139e39ccaf26ed12721ddeb1f3ee4099b79 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 7 Dec 2023 13:14:47 +0800 Subject: [PATCH 28/35] Add ability to open items on archive.today --- .../App/Views/Home/OpenAIVoicesModal.swift | 8 -- .../WebReader/OpenArchiveTodayView.swift | 94 +++++++++++++++++++ .../Views/WebReader/WebReaderContainer.swift | 10 ++ 3 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/WebReader/OpenArchiveTodayView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/OpenAIVoicesModal.swift b/apple/OmnivoreKit/Sources/App/Views/Home/OpenAIVoicesModal.swift index 4388749ef..e920387c3 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/OpenAIVoicesModal.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/OpenAIVoicesModal.swift @@ -1,12 +1,4 @@ // swiftlint:disable line_length - -// -// CommunityModal.swift -// -// -// Created by Jackson Harper on 12/7/22. -// - #if os(iOS) import Foundation import Models diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/OpenArchiveTodayView.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/OpenArchiveTodayView.swift new file mode 100644 index 000000000..e74f800c8 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/OpenArchiveTodayView.swift @@ -0,0 +1,94 @@ +// swiftlint:disable line_length +import Foundation +import Models +import SwiftUI +import Views +import WebKit + +struct OpenArchiveTodayView: View { + let item: Models.LibraryItem + + @State private var date = Date() + @State private var showSafariBrowser = false + @AppStorage("OpenArchiveTodayView::useInAppBrowser") var useInAppBrowser = false + + let message = """ + [Archive.today](https://archive.today) is a time capsule for web pages! It takes a 'snapshot' of a webpage that will always be online even if the original page disappears. + """ + + init(item: Models.LibraryItem) { + self.item = item + self.date = item.savedAt ?? Date() + } + + var archiveURL: URL? { + if let pageURL = item.pageURLString, let savedAt = item.savedAt { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + let formattedDate = dateFormatter.string(from: savedAt) + + let str = "https://archive.today/\(formattedDate)/\(pageURL)" + return URL(string: str) + } + return nil + } + + var innerBody: some View { + VStack { + let parsedMessage = try? AttributedString(markdown: message.trimmingCharacters(in: .whitespacesAndNewlines), + options: .init(interpretedSyntax: .inlineOnly)) + + Rectangle() + .fill(Color.secondarySystemGroupedBackground) + .cornerRadius(10) + .overlay( + Text(parsedMessage ?? "") + .multilineTextAlignment(.leading) + .foregroundColor(Color.appGrayTextContrast) + .accentColor(.blue) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(10) + ) + + Spacer() + + DatePicker( + "Archived Date", + selection: $date, + displayedComponents: [.date, .hourAndMinute] + ).padding(.top, 20) + + Toggle("Use in-app Browser", isOn: $useInAppBrowser) + + Divider().padding(.vertical, 20) + + Button( + action: { + if useInAppBrowser { + showSafariBrowser = true + } else { + if let archiveURL = archiveURL { + UIApplication.shared.open(archiveURL) + } + } + }, + label: { Text("Open on Archive.today").padding(5) } + ) + .buttonStyle(RoundedRectButtonStyle(color: Color.blue, textColor: Color.white)) + .frame(maxWidth: .infinity) + }.padding(20) + } + + var body: some View { + NavigationView { + innerBody + .navigationTitle("Open on Archive.today") + .navigationBarTitleDisplayMode(.inline) + }.sheet(isPresented: $showSafariBrowser) { + if let archiveURL = archiveURL { + SafariView(url: archiveURL) + .ignoresSafeArea(.all, edges: .bottom) + } + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 2649cca8d..31846163a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -33,6 +33,7 @@ struct WebReaderContainerView: View { @State private var errorAlertMessage: String? @State private var showErrorAlertMessage = false @State private var showRecommendSheet = false + @State private var showOpenArchiveSheet = false @State private var lastScrollPercentage: Int? @State private var isRecovering = false @@ -247,6 +248,12 @@ struct WebReaderContainerView: View { }, label: { Label("Open Original", systemImage: "safari") } ) + Button( + action: { + showOpenArchiveSheet = true + }, + label: { Label("Open on Archive.today", systemImage: "globe") } + ) Button( action: share, label: { Label("Share Original", systemImage: "square.and.arrow.up") } @@ -489,6 +496,9 @@ struct WebReaderContainerView: View { showRecommendSheet = false } } + .formSheet(isPresented: $showOpenArchiveSheet) { + OpenArchiveTodayView(item: item) + } #endif .sheet(isPresented: $showHighlightAnnotationModal) { NavigationView { From 43c8e5cabb8d7cb0a9a410fff7117dfe61f7fbb8 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 7 Dec 2023 13:14:59 +0800 Subject: [PATCH 29/35] Use title for the full screen text to speech view --- .../App/Views/AudioPlayer/ExpandedAudioPlayer.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift index a60a904ea..7ff57855e 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/ExpandedAudioPlayer.swift @@ -416,14 +416,9 @@ NavigationView { innerBody .background(Color.themeDisabledBG) - .navigationTitle(LocalText.textToSpeechGeneric) + .navigationTitle(audioController.itemAudioProperties?.title ?? LocalText.textToSpeechGeneric) .navigationBarItems(trailing: Button(action: { dismiss() }, label: { Text("Hide") })) .navigationBarTitleDisplayMode(NavigationBarItem.TitleDisplayMode.inline) - // .searchable(text: $queryString, placement: .navigationBarDrawer(displayMode: .always)) { - // // print("searching: ", queryString) - // Text("content") - // } - // } } } From 058ddb0a62f481240a06e0efe791b099f51b699a Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 7 Dec 2023 14:49:51 +0800 Subject: [PATCH 30/35] Empty views in nav bar are causing weird issues --- .../Sources/App/Views/Home/HomeFeedViewIOS.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index b5004f211..766fb6840 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -226,8 +226,6 @@ struct AnimatingCellHeight: AnimatableModifier { Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet") } ) - } else { - EmptyView() } } ToolbarItem(placement: .barTrailing) { @@ -235,10 +233,6 @@ struct AnimatingCellHeight: AnimatableModifier { action: { searchPresented = true }, label: { Image(systemName: "magnifyingglass") - .resizable() - .frame(width: 18, height: 18) - .padding(.vertical) - .foregroundColor(.appGrayTextContrast) } ) } @@ -261,8 +255,6 @@ struct AnimatingCellHeight: AnimatableModifier { Image.utilityMenu }) .foregroundColor(.appGrayTextContrast) - } else { - EmptyView() } } ToolbarItemGroup(placement: .bottomBar) { From cde5d616e8258b87b145cdcda5190c1bcc9eb9b3 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 7 Dec 2023 16:26:14 +0800 Subject: [PATCH 31/35] Dont redownload cached items --- .../Home/Components/FollowingFetcher.swift | 257 ------------------ .../Services/DataService/ContentLoading.swift | 2 +- 2 files changed, 1 insertion(+), 258 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/App/Views/Home/Components/FollowingFetcher.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FollowingFetcher.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FollowingFetcher.swift deleted file mode 100644 index d7ad556da..000000000 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FollowingFetcher.swift +++ /dev/null @@ -1,257 +0,0 @@ -//// -//// FollowingFetcher.swift -//// -//// -//// Created by Jackson Harper on 11/16/23. -//// -// -// import Foundation -// -// import CoreData -// import Models -// import Services -// import SwiftUI -// import Utils -// import Views -// -// @MainActor final class FollowingFetcher: NSObject, ObservableObject, LibraryItemFetcher { -// var folder = "following" -// -// @Published var items = [Models.LibraryItem]() -// var itemsPublisher: Published<[Models.LibraryItem]>.Publisher { $items } -// -// private var fetchedResultsController: NSFetchedResultsController? -// -// var cursor: String? -// -// // These are used to make sure we handle search result -// // responses in the right order -// var searchIdx = 0 -// var receivedIdx = 0 -// -// var syncCursor: String? -// -// func setItems(_: NSManagedObjectContext, _ items: [Models.LibraryItem]) { -// self.items = items -// } -// -// func loadCurrentViewer(dataService: DataService) async { -// // Cache the viewer -// if dataService.currentViewer == nil { -// _ = try? await dataService.fetchViewer() -// } -// } -// -// func loadLabels(dataService: DataService) async { -// let fetchRequest: NSFetchRequest = LinkedItemLabel.fetchRequest() -// fetchRequest.fetchLimit = 1 -// -// if (try? dataService.viewContext.count(for: fetchRequest)) == 0 { -// _ = try? await dataService.labels() -// } -// } -// -// func syncItems(dataService: DataService) async { -// let syncStart = Date.now -// let lastSyncDate = dataService.lastItemSyncTime -// -// try? await dataService.syncOfflineItemsWithServerIfNeeded() -// -// let syncResult = try? await dataService.syncLinkedItems(since: lastSyncDate, -// cursor: nil) -// -// syncCursor = syncResult?.cursor -// if let syncResult = syncResult, syncResult.hasMore { -// dataService.syncLinkedItemsInBackground(since: lastSyncDate) { -// // do nothing -// } -// } else { -// dataService.lastItemSyncTime = syncStart -// } -// -// // If possible start prefetching new pages in the background -// if -// let itemIDs = syncResult?.updatedItemIDs, -// let username = dataService.currentViewer?.username, -// !itemIDs.isEmpty -// { -// Task.detached(priority: .background) { -// await dataService.prefetchPages(itemIDs: itemIDs, username: username) -// } -// } -// } -// -// func loadSearchQuery(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { -// let thisSearchIdx = searchIdx -// searchIdx += 1 -// -// if thisSearchIdx > 0, thisSearchIdx <= receivedIdx { -// return -// } -// -// let queryResult = try? await dataService.loadLinkedItems( -// limit: 10, -// searchQuery: searchQuery(filterState), -// cursor: isRefresh ? nil : cursor -// ) -// -// let filter = LinkedItemFilter(rawValue: filterState.appliedFilter) -// -// if let queryResult = queryResult { -// let newItems: [Models.LibraryItem] = { -// var itemObjects = [Models.LibraryItem]() -// dataService.viewContext.performAndWait { -// itemObjects = queryResult.itemIDs.compactMap { dataService.viewContext.object(with: $0) as? Models.LibraryItem } -// } -// return itemObjects -// }() -// -// print("RESULTS OF SEARCH: ", newItems) -// -// if filterState.searchTerm.replacingOccurrences(of: " ", with: "").isEmpty, filter?.allowLocalFetch ?? false { -// updateFetchController(dataService: dataService, filterState: filterState) -// } else { -// // Don't use FRC for searching. Use server results directly. -// if fetchedResultsController != nil { -// fetchedResultsController = nil -// setItems(dataService.viewContext, []) -// } -// setItems(dataService.viewContext, isRefresh ? newItems : items + newItems) -// } -// -// receivedIdx = thisSearchIdx -// cursor = queryResult.cursor -//// if let username = dataService.currentViewer?.username { -//// await dataService.prefetchPages(itemIDs: newItems.map(\.unwrappedID), username: username) -//// } -// } else { -// updateFetchController(dataService: dataService, filterState: filterState) -// } -// } -// -// func loadItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { -// await withTaskGroup(of: Void.self) { group in -// group.addTask { await self.loadCurrentViewer(dataService: dataService) } -// group.addTask { await self.loadLabels(dataService: dataService) } -// group.addTask { await self.syncItems(dataService: dataService) } -// group.addTask { await self.updateFetchController(dataService: dataService, filterState: filterState) } -// await group.waitForAll() -// } -// -// let filter = LinkedItemFilter(rawValue: filterState.appliedFilter) -// let shouldSearch = items.count < 1 || isRefresh && filter != LinkedItemFilter.downloaded -// if shouldSearch { -// await loadSearchQuery(dataService: dataService, filterState: filterState, isRefresh: isRefresh) -// } else { -// updateFetchController(dataService: dataService, filterState: filterState) -// } -// } -// -// func loadMoreItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async { -// let filter = LinkedItemFilter(rawValue: filterState.appliedFilter) -// if filter != LinkedItemFilter.downloaded { -// await loadSearchQuery(dataService: dataService, filterState: filterState, isRefresh: isRefresh) -// } -// } -// -// private func fetchRequest(_ filterState: FetcherFilterState) -> NSFetchRequest { -// let fetchRequest: NSFetchRequest = LibraryItem.fetchRequest() -// -// var subPredicates = [NSPredicate]() -// -// let folderPredicate = NSPredicate( -// format: "%K == %@", #keyPath(Models.LibraryItem.folder), folder -// ) -// subPredicates.append(folderPredicate) -// -// if !filterState.selectedLabels.isEmpty { -// var labelSubPredicates = [NSPredicate]() -// -// for label in filterState.selectedLabels { -// labelSubPredicates.append( -// NSPredicate(format: "SUBQUERY(labels, $label, $label.id == \"\(label.unwrappedID)\").@count > 0") -// ) -// } -// -// subPredicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: labelSubPredicates)) -// } -// -// if !filterState.negatedLabels.isEmpty { -// var labelSubPredicates = [NSPredicate]() -// -// for label in filterState.negatedLabels { -// labelSubPredicates.append( -// NSPredicate(format: "SUBQUERY(labels, $label, $label.id == \"\(label.unwrappedID)\").@count == 0") -// ) -// } -// -// subPredicates.append(NSCompoundPredicate(orPredicateWithSubpredicates: labelSubPredicates)) -// } -// -// fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: subPredicates) -// fetchRequest.sortDescriptors = (LinkedItemSort(rawValue: filterState.appliedSort) ?? .newest).sortDescriptors -// -// return fetchRequest -// } -// -// private func updateFetchController(dataService: DataService, filterState: FetcherFilterState) { -// fetchedResultsController = NSFetchedResultsController( -// fetchRequest: fetchRequest(filterState), -// managedObjectContext: dataService.viewContext, -// sectionNameKeyPath: nil, -// cacheName: nil -// ) -// -// guard let fetchedResultsController = fetchedResultsController else { -// return -// } -// -// fetchedResultsController.delegate = self -// try? fetchedResultsController.performFetch() -// setItems(dataService.viewContext, fetchedResultsController.fetchedObjects ?? []) -// } -// -// private func searchQuery(_ filterState: FetcherFilterState) -> String { -// let sort = LinkedItemSort(rawValue: filterState.appliedSort) ?? .newest -//// var query = sort.queryString -// -// var query = "in:following \(sort.queryString)" -//// if !queryContainsFilter(filterState), let filter = LinkedItemFilter(rawValue: filterState.appliedFilter) { -//// query = "\(filter.queryString) \(sort.queryString)" -//// } -// -// if !filterState.searchTerm.isEmpty { -// query.append(" \(filterState.searchTerm)") -// } -// -// if !filterState.selectedLabels.isEmpty { -// query.append(" label:") -// query.append(filterState.selectedLabels.compactMap { label in -// if let name = label.name { -// return "\"\(name)\"" -// } -// return nil -// }.joined(separator: ",")) -// } -// -// if !filterState.negatedLabels.isEmpty { -// query.append(" !label:") -// query.append(filterState.negatedLabels.compactMap { label in -// if let name = label.name { -// return "\"\(name)\"" -// } -// return nil -// }.joined(separator: ",")) -// } -// -// print("QUERY: `\(query)`") -// -// return query -// } -// } -// -// extension FollowingFetcher: NSFetchedResultsControllerDelegate { -// func controllerDidChangeContent(_ controller: NSFetchedResultsController) { -// setItems(controller.managedObjectContext, controller.fetchedObjects as? [Models.LibraryItem] ?? []) -// } -// } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift index 2affe061d..147186e8c 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/ContentLoading.swift @@ -10,7 +10,7 @@ struct PendingLink { extension DataService { func prefetchPage(pendingLink: PendingLink, username: String) async { - let content = try? await loadArticleContent(username: username, itemID: pendingLink.itemID, useCache: false) + let content = try? await loadArticleContent(username: username, itemID: pendingLink.itemID, useCache: true) if content?.contentStatus == .processing, pendingLink.retryCount < 7 { let retryDelayInNanoSeconds = UInt64(pendingLink.retryCount * 2 * 1_000_000_000) From 80d48094ee3350cf4c2b6d7bee24e515ad32d9ea Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 7 Dec 2023 16:50:48 +0800 Subject: [PATCH 32/35] Less fallback images, dont use shimmering view --- .../App/Views/Home/HomeFeedViewIOS.swift | 14 +---- .../Views/FeedItem/LibraryItemCard.swift | 19 ++----- .../Sources/Views/ShimmerView.swift | 54 ------------------- 3 files changed, 6 insertions(+), 81 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/Views/ShimmerView.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 766fb6840..3eed0e2d0 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -606,12 +606,7 @@ struct AnimatingCellHeight: AnimatableModifier { var body: some View { let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10) VStack(spacing: 0) { - if viewModel.showLoadingBar { - ShimmeringLoader() - } else { - Spacer(minLength: 2) - } - + Color.systemBackground.frame(height: 1) ScrollViewReader { reader in List(selection: $selection) { Section(content: { @@ -851,12 +846,7 @@ struct AnimatingCellHeight: AnimatableModifier { var body: some View { VStack(alignment: .leading) { - if viewModel.showLoadingBar { - ShimmeringLoader() - } else { - Spacer(minLength: 2) - } - + Color.systemBackground.frame(height: 1) filtersHeader .onAppear { withAnimation { diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift index ac052ab9e..ee064fc15 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryItemCard.swift @@ -256,27 +256,16 @@ public struct LibraryItemCard: View { } } } else { - fallbackImage + Color.clear + .frame(width: 50, height: 75) + .cornerRadius(5) + .padding(.top, 2) } } .padding(.top, 10) .cornerRadius(5) } - var fallbackImage: some View { - HStack { - Text(item.unwrappedTitle.prefix(1)) - .font(Font.system(size: 32, weight: .bold)) - .frame(alignment: .bottomLeading) - .foregroundColor(Gradient.randomColor(str: item.unwrappedTitle, offset: 1)) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Gradient.randomColor(str: item.unwrappedTitle, offset: 0)) - .background(LinearGradient(gradient: Gradient(fromStr: item.unwrappedTitle)!, startPoint: .top, endPoint: .bottom)) - .cornerRadius(5) - .frame(width: 50, height: 75) - } - var bylineStr: String { // It seems like it could be cleaner just having author, instead of // concating, maybe we fall back diff --git a/apple/OmnivoreKit/Sources/Views/ShimmerView.swift b/apple/OmnivoreKit/Sources/Views/ShimmerView.swift deleted file mode 100644 index b977259f7..000000000 --- a/apple/OmnivoreKit/Sources/Views/ShimmerView.swift +++ /dev/null @@ -1,54 +0,0 @@ -import SwiftUI - -public struct ShimmeringLoader: View { - @State private var phase: CGFloat = 0 - - public init() {} - - public var body: some View { - ZStack { - Color.systemBackground - Color.appGraySolid - .contentShape(Rectangle()) - .modifier(AnimatedMask(phase: phase).animation( - Animation.linear(duration: 2.0) - .repeatForever(autoreverses: false) - )) - .onAppear { phase = 0.8 } - } - .frame(height: 2) - .frame(maxWidth: .infinity) - } - - /// An animatable modifier to interpolate between `phase` values. - struct AnimatedMask: AnimatableModifier { - var phase: CGFloat = 0 - - var animatableData: CGFloat { - get { phase } - set { phase = newValue } - } - - func body(content: Content) -> some View { - content - .mask(GradientMask(phase: phase).scaleEffect(3)) - } - } - - /// An animatable gradient between transparent and opaque to use as mask. - /// The `phase` parameter shifts the gradient, moving the opaque band. - struct GradientMask: View { - let phase: CGFloat - let centerColor = Color.appGraySolid - let edgeColor = Color.clear - - var body: some View { - LinearGradient(gradient: - Gradient(stops: [ - .init(color: edgeColor, location: phase), - .init(color: centerColor, location: phase + 0.1), - .init(color: edgeColor, location: phase + 0.2) - ]), startPoint: .leading, endPoint: .trailing) - } - } -} From 44b2a4a0ed9aca30ec4db5227145e4bee3c7b763 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 7 Dec 2023 17:59:28 +0800 Subject: [PATCH 33/35] Remove jest --- packages/web/jest.config.js | 15 --------------- packages/web/jest.setup.js | 5 ----- packages/web/package.json | 5 +---- 3 files changed, 1 insertion(+), 24 deletions(-) delete mode 100644 packages/web/jest.config.js delete mode 100644 packages/web/jest.setup.js diff --git a/packages/web/jest.config.js b/packages/web/jest.config.js deleted file mode 100644 index fd04a1cea..000000000 --- a/packages/web/jest.config.js +++ /dev/null @@ -1,15 +0,0 @@ -const nextJest = require('next/jest') - -const createJestConfig = nextJest({ - // Provide the path to your Next.js app to load next.config.js and .env files in your test environment - dir: './', -}) - -// Add any custom config to be passed to Jest -const customJestConfig = { - setupFilesAfterEnv: ['/jest.setup.js'], - testEnvironment: 'jest-environment-jsdom', -} - -// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async -module.exports = createJestConfig(customJestConfig) diff --git a/packages/web/jest.setup.js b/packages/web/jest.setup.js deleted file mode 100644 index 76e9471e9..000000000 --- a/packages/web/jest.setup.js +++ /dev/null @@ -1,5 +0,0 @@ -// Optional: configure or set up a testing framework before each test. -// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js` - -// Learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom/extend-expect' diff --git a/packages/web/package.json b/packages/web/package.json index fe753cbdc..15ae7026b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -10,9 +10,6 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test": "jest", - "test:watch": "jest --watch", - "test:build": "jest && next build", "test:typecheck": "tsc --noEmit", "upgrade-psdpdfkit": "cp -R '../../node_modules/pspdfkit/dist/pspdfkit-lib' public/pspdfkit-lib", "storybook": "start-storybook -p 6006 -s ./public", @@ -111,4 +108,4 @@ "volta": { "extends": "../../package.json" } -} \ No newline at end of file +} From b1283e058bf0bc60f74afd03b6bc9a6ad9aca185 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 7 Dec 2023 18:26:46 +0800 Subject: [PATCH 34/35] Revert sentry package change --- packages/api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/package.json b/packages/api/package.json index 5a00c1135..ff7513d85 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -38,7 +38,7 @@ "@opentelemetry/tracing": "^0.24.0", "@sendgrid/mail": "^7.6.0", "@sentry/integrations": "^7.10.0", - "@sentry/node": "^7.9.0", + "@sentry/node": "^5.26.0", "@sentry/tracing": "^7.9.0", "addressparser": "^1.0.1", "analytics-node": "^6.0.0", From c45ca2c1f9cba0620a671bd74a759f066f4bbcb3 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 7 Dec 2023 18:27:47 +0800 Subject: [PATCH 35/35] Remove yarn changes --- yarn.lock | 96 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/yarn.lock b/yarn.lock index ba65d3e0a..f5c3158be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5336,15 +5336,6 @@ "@sentry/types" "7.77.0" "@sentry/utils" "7.77.0" -"@sentry-internal/tracing@7.84.0": - version "7.84.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.84.0.tgz#430da253ee5b075be4ef57f20ea842c0208bc6b0" - integrity sha512-y9bGYA0OM6PEREfd+nk4UURZy29tpIw+7vQwpxWfEVs2fqq0/5TBFX/tKFb8AKUI9lVM8v0bcF0bNSCnuPQZHQ== - dependencies: - "@sentry/core" "7.84.0" - "@sentry/types" "7.84.0" - "@sentry/utils" "7.84.0" - "@sentry/browser@7.50.0": version "7.50.0" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.50.0.tgz#16c995c336322c8aec65570f90f50288678004ec" @@ -5369,6 +5360,17 @@ proxy-from-env "^1.1.0" which "^2.0.2" +"@sentry/core@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" + integrity sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg== + dependencies: + "@sentry/hub" "5.30.0" + "@sentry/minimal" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" + "@sentry/core@7.50.0": version "7.50.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.50.0.tgz#88bc9cbfc0cb429a28489ece6f0be7a7006436c4" @@ -5386,13 +5388,14 @@ "@sentry/types" "7.77.0" "@sentry/utils" "7.77.0" -"@sentry/core@7.84.0": - version "7.84.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.84.0.tgz#01d33fc452044ffd8ea57b20f60304b9cfa2b9e1" - integrity sha512-tbuwunbBx2kSex15IHCqHDnrMfIlqPc6w/76fwkGqokz3oh9GSEGlLICwmBWL8AypWimUg13IDtFpD0TJTriWA== +"@sentry/hub@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.30.0.tgz#2453be9b9cb903404366e198bd30c7ca74cdc100" + integrity sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ== dependencies: - "@sentry/types" "7.84.0" - "@sentry/utils" "7.84.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" "@sentry/integrations@7.50.0": version "7.50.0" @@ -5414,6 +5417,15 @@ "@sentry/utils" "7.77.0" localforage "^1.8.1" +"@sentry/minimal@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.30.0.tgz#ce3d3a6a273428e0084adcb800bc12e72d34637b" + integrity sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw== + dependencies: + "@sentry/hub" "5.30.0" + "@sentry/types" "5.30.0" + tslib "^1.9.3" + "@sentry/nextjs@^7.42.0": version "7.50.0" resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.50.0.tgz#b9e7727c8f974644bb84a01f40a0adedb44ec416" @@ -5457,16 +5469,20 @@ "@sentry/utils" "7.77.0" https-proxy-agent "^5.0.0" -"@sentry/node@^7.9.0": - version "7.84.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.84.0.tgz#c06167106796b2b83c0a9b52fa56f8ca820034ca" - integrity sha512-Xm3fIXT3TZOQi+6uQBavI8iOehD3PkY7v0y3hog0d4lQTH88vQK9BBsI+jZEq81Em+RG/u7vZNiFo6YMTnWF7Q== +"@sentry/node@^5.26.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.30.0.tgz#4ca479e799b1021285d7fe12ac0858951c11cd48" + integrity sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg== dependencies: - "@sentry-internal/tracing" "7.84.0" - "@sentry/core" "7.84.0" - "@sentry/types" "7.84.0" - "@sentry/utils" "7.84.0" + "@sentry/core" "5.30.0" + "@sentry/hub" "5.30.0" + "@sentry/tracing" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + cookie "^0.4.1" https-proxy-agent "^5.0.0" + lru_map "^0.3.3" + tslib "^1.9.3" "@sentry/react@7.50.0": version "7.50.0" @@ -5500,6 +5516,17 @@ "@types/aws-lambda" "^8.10.62" "@types/express" "^4.17.14" +"@sentry/tracing@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.30.0.tgz#501d21f00c3f3be7f7635d8710da70d9419d4e1f" + integrity sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw== + dependencies: + "@sentry/hub" "5.30.0" + "@sentry/minimal" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" + "@sentry/tracing@^7.9.0": version "7.77.0" resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.77.0.tgz#39d7c30834f503fe9eb20ce1c8c8bd28f7d7c9ce" @@ -5507,6 +5534,11 @@ dependencies: "@sentry-internal/tracing" "7.77.0" +"@sentry/types@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402" + integrity sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw== + "@sentry/types@7.50.0": version "7.50.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.50.0.tgz#52a035cad83a80ca26fa53c09eb1241250c3df3e" @@ -5517,10 +5549,13 @@ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.77.0.tgz#c5d00fe547b89ccde59cdea59143bf145cee3144" integrity sha512-nfb00XRJVi0QpDHg+JkqrmEBHsqBnxJu191Ded+Cs1OJ5oPXEW6F59LVcBScGvMqe+WEk1a73eH8XezwfgrTsA== -"@sentry/types@7.84.0": - version "7.84.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.84.0.tgz#e8db86c36c61659c3b2558f0aa8b6a073a756117" - integrity sha512-VqGLIF3JOUrk7yIXjLXJvAORkZL1e3dDX0Q1okRehwyt/5CRE+mdUTeJZkBo9P9mBwgMyvtwklzOGGrzjb4eMA== +"@sentry/utils@5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.30.0.tgz#9a5bd7ccff85ccfe7856d493bffa64cabc41e980" + integrity sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww== + dependencies: + "@sentry/types" "5.30.0" + tslib "^1.9.3" "@sentry/utils@7.50.0": version "7.50.0" @@ -5537,13 +5572,6 @@ dependencies: "@sentry/types" "7.77.0" -"@sentry/utils@7.84.0": - version "7.84.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.84.0.tgz#32861d922fa31e86dd2863a1d9dfc5a369e98952" - integrity sha512-qdUVuxnRBvaf05AU+28R+xYtZmi/Ymf8os3Njq9g4XuA+QEkZLbzmIpRK5W9Ja7vUtjOeg29Xgg43A8znde9LQ== - dependencies: - "@sentry/types" "7.84.0" - "@sentry/webpack-plugin@1.20.0": version "1.20.0" resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.20.0.tgz#e7add76122708fb6b4ee7951294b521019720e58"