From dded89bf324a605690ea07965df46915b2d00ef3 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Wed, 27 Dec 2023 14:16:50 +0800 Subject: [PATCH] WIP allow a single viewmodel for iPad split screen view --- .../App/Views/Home/HomeFeedViewIOS.swift | 70 ++++---- .../App/Views/Home/HomeFeedViewModel.swift | 154 +++++++++++------- .../Sources/App/Views/LibrarySidebar.swift | 29 ++-- .../Sources/App/Views/LibrarySplitView.swift | 85 +++++----- .../Sources/App/Views/LibraryTabView.swift | 36 ++-- .../InternalModels/InternalFilter.swift | 46 +++++- 6 files changed, 247 insertions(+), 173 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index 71424ba16..6170e1158 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -19,23 +19,23 @@ struct FiltersHeader: View { viewModel.searchTerm = "" }.frame(maxWidth: reader.size.width * 0.66) } else { - if UIDevice.isIPhone { - Menu( - content: { - ForEach(viewModel.filters) { filter in - Button(filter.name, action: { - viewModel.appliedFilter = filter - }) - } - }, - label: { - TextChipButton.makeMenuButton( - title: viewModel.appliedFilter?.name ?? "-", - color: .systemGray6 - ) + // if UIDevice.isIPhone { + Menu( + content: { + ForEach(viewModel.filters.filter { $0.folder == viewModel.currentFolder }) { filter in + Button(filter.name, action: { + viewModel.appliedFilter = filter + }) } - ).buttonStyle(.plain) - } + }, + label: { + TextChipButton.makeMenuButton( + title: viewModel.appliedFilter?.name ?? "-", + color: .systemGray6 + ) + } + ).buttonStyle(.plain) + // } } Menu( content: { @@ -126,7 +126,7 @@ struct AnimatingCellHeight: AnimatableModifier { var showFeatureCards: Bool { isEditMode == .inactive && - viewModel.listConfig.hasFeatureCards && + (viewModel.currentListConfig?.hasFeatureCards ?? false) && !viewModel.hideFeatureSection && viewModel.fetcher.items.count > 0 && viewModel.searchTerm.isEmpty && @@ -298,9 +298,9 @@ struct AnimatingCellHeight: AnimatableModifier { Button( action: { - if viewModel.folder == "inbox" { + if viewModel.currentFolder == "inbox" { showAddLinkView = true - } else if viewModel.folder == "following" { + } else if viewModel.currentFolder == "following" { showAddFeedView = true } }, @@ -368,7 +368,7 @@ struct AnimatingCellHeight: AnimatableModifier { var body: some View { VStack(spacing: 0) { - if let linkRequest = viewModel.linkRequest, viewModel.listConfig.hasReadNowSection { + if let linkRequest = viewModel.linkRequest, viewModel.currentListConfig?.hasReadNowSection ?? false { PresentationLink( transition: PresentationLinkTransition.slide( options: PresentationLinkTransition.SlideTransitionOptions(edge: .trailing, @@ -617,7 +617,7 @@ struct AnimatingCellHeight: AnimatableModifier { } var emptyState: some View { - if viewModel.folder == "following" { + if viewModel.currentFolder == "following" { return AnyView( VStack(alignment: .center, spacing: 20) { Text("You don't have any Feed items.") @@ -657,7 +657,7 @@ struct AnimatingCellHeight: AnimatableModifier { } var listItems: some View { - ForEach(viewModel.fetcher.items, id: \.unwrappedID) { item in + ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { idx, item in let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10) LibraryItemListNavigationLink( @@ -681,22 +681,26 @@ struct AnimatingCellHeight: AnimatableModifier { menuItems(for: item) } .swipeActions(edge: .leading, allowsFullSwipe: true) { - ForEach(viewModel.listConfig.leadingSwipeActions, id: \.self) { action in - swipeActionButton(action: action, item: item) + if let listConfig = viewModel.currentListConfig { + ForEach(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) + if let listConfig = viewModel.currentListConfig { + ForEach(listConfig.trailingSwipeActions, id: \.self) { action in + swipeActionButton(action: action, item: item) + } + } + } + .onAppear { + if idx >= viewModel.fetcher.items.count - 5 { + Task { + await viewModel.loadMore(dataService: dataService) + } } } -// .onAppear { -// if idx >= viewModel.fetcher.items.count - 5 { -// Task { -// await viewModel.loadMore(dataService: dataService) -// } -// } -// } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index a1c21f18f..609a5e9a0 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -6,9 +6,8 @@ import Utils import Views @MainActor final class HomeFeedViewModel: NSObject, ObservableObject { - let folder: String + let filterKey: String @ObservedObject var fetcher: LibraryItemFetcher - let listConfig: LibraryListConfig private var fetchedResultsController: NSFetchedResultsController? @@ -40,63 +39,90 @@ import Views @AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false @AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false - @Published var appliedFilter: InternalFilter? { - didSet { - let filterKey = UserDefaults.standard.string(forKey: "lastSelected-\(folder)-filter") ?? folder - UserDefaults.standard.setValue(appliedFilter?.name, forKey: filterKey) - } - } + @Published var appliedFilter: InternalFilter? /* { + didSet { + if let folder = appliedFilter.folder, let filterKey = UserDefaults.standard.string(forKey: "lastSelected-\(folder))-filter") { + UserDefaults.standard.setValue(appliedFilter?.name, forKey: filterKey) + } + } + } */ - private var filterState: FetcherFilterState { - FetcherFilterState(folder: folder, searchTerm: searchTerm, selectedLabels: selectedLabels, negatedLabels: negatedLabels, appliedSort: appliedSort, appliedFilter: appliedFilter) - } + let folderConfigs: [String: LibraryListConfig] + + init(filterKey: String, fetcher: LibraryItemFetcher, folderConfigs: [String: LibraryListConfig]) { + self.filterKey = filterKey - init(folder: String, fetcher: LibraryItemFetcher, listConfig: LibraryListConfig) { - self.folder = folder self.fetcher = fetcher - self.listConfig = listConfig + self.folderConfigs = folderConfigs + +// if let filterKey = UserDefaults.standard.string(forKey: "lastSelected-\(filterKey))-filter") { +// UserDefaults.standard.setValue(appliedFilter?.name, forKey: filterKey) +// } + super.init() } + private var filterState: FetcherFilterState? { + if let appliedFilter = appliedFilter { + return FetcherFilterState( + folder: appliedFilter.folder, + searchTerm: searchTerm, + selectedLabels: selectedLabels, + negatedLabels: negatedLabels, + appliedSort: appliedSort, + appliedFilter: appliedFilter + ) + } + return nil + } + + var currentFolder: String? { + appliedFilter?.folder + } + + var currentListConfig: LibraryListConfig? { + if let currentFolder = currentFolder { + return folderConfigs[currentFolder] + } + return nil + } + func loadFilters(dataService: DataService) async { - switch folder { - case "following": - updateFilters(newFilters: InternalFilter.DefaultFollowingFilters, defaultName: "following") - default: - var hasLocalResults = false - let fetchRequest: NSFetchRequest = Filter.fetchRequest() + var hasLocalResults = false + let fetchRequest: NSFetchRequest = Filter.fetchRequest() - // Load from disk - if let results = try? dataService.viewContext.fetch(fetchRequest) { - hasLocalResults = true - updateFilters(newFilters: InternalFilter.make(from: results), defaultName: "inbox") - } + // Load from disk + if let results = try? dataService.viewContext.fetch(fetchRequest) { + hasLocalResults = true + updateFilters(newFilters: InternalFilter.make(from: results)) + } - let hasResults = hasLocalResults - Task.detached { - if let downloadedFilters = try? await dataService.filters() { - await self.updateFilters(newFilters: downloadedFilters, defaultName: "inbox") - } else if !hasResults { - await self.updateFilters(newFilters: InternalFilter.DefaultInboxFilters, defaultName: "inbox") - } + 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.DefaultInboxFilters) } } } func loadMore(dataService: DataService, loadCursor: String? = nil) async { - if isLoading { return } + if let filterState = filterState { + if isLoading { return } - let start = Date.now - if let lastMoreFetched, lastMoreFetched.timeIntervalSinceNow > -4 { - print("skipping fetching more as last fetch was too recent: ", lastMoreFetched) - return + let start = Date.now + if let lastMoreFetched, lastMoreFetched.timeIntervalSinceNow > -4 { + print("skipping fetching more as last fetch was too recent: ", lastMoreFetched) + return + } + + isLoading = true + await fetcher.loadMoreItems(dataService: dataService, filterState: filterState, loadCursor: loadCursor) + isLoading = false + + lastMoreFetched = start } - - isLoading = true - await fetcher.loadMoreItems(dataService: dataService, filterState: filterState, loadCursor: loadCursor) - isLoading = false - - lastMoreFetched = start } func pushFeedItem(item _: Models.LibraryItem) { @@ -120,24 +146,36 @@ import Views } } - func updateFilters(newFilters: [InternalFilter], defaultName: String) { - let appliedFilterName = UserDefaults.standard.string(forKey: "lastSelected-\(filterState.folder)-filter") ?? defaultName + var defaultFilters: [InternalFilter] { + [InternalFilter.InboxUnreadFilter, + InternalFilter.InboxDeletedFilter, + InternalFilter.InboxDownloadedFilter] + + InternalFilter.DefaultFollowingFilters + } + + func updateFilters(newFilters: [InternalFilter]) { + let availableFolders = folderConfigs.keys + let appliedFilterName = UserDefaults.standard.string(forKey: filterKey) filters = newFilters - .filter { $0.folder == filterState.folder } + .filter { availableFolders.contains($0.folder) } .sorted(by: { $0.position < $1.position }) - + (folder == "inbox" ? [InternalFilter.UnreadFilter, InternalFilter.DeletedFilter, InternalFilter.DownloadedFilter] : [InternalFilter.DownloadedFilter]) + + defaultFilters if let newFilter = filters.first(where: { $0.name.lowercased() == appliedFilterName }), newFilter.id != appliedFilter?.id { appliedFilter = newFilter + } else { + appliedFilter = filters.first(where: { availableFolders.contains($0.folder) }) } } func loadNewItems(dataService: DataService) async { - await fetcher.loadNewItems( - dataService: dataService, - filterState: filterState - ) + if let filterState = filterState { + await fetcher.loadNewItems( + dataService: dataService, + filterState: filterState + ) + } objectWillChange.send() } @@ -145,12 +183,14 @@ import Views isLoading = true showLoadingBar = isRefresh - await fetcher.loadItems( - dataService: dataService, - filterState: filterState, - isRefresh: isRefresh, - forceRemote: forceRemote - ) + if let filterState = filterState { + await fetcher.loadItems( + dataService: dataService, + filterState: filterState, + isRefresh: isRefresh, + forceRemote: forceRemote + ) + } isLoading = false showLoadingBar = false diff --git a/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift b/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift index b21cc0ba7..3b6862eb8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibrarySidebar.swift @@ -4,9 +4,7 @@ import Services import SwiftUI @MainActor struct LibrarySidebar: View { - @ObservedObject var inboxViewModel: HomeFeedViewModel - @ObservedObject var followingViewModel: HomeFeedViewModel - + @ObservedObject var viewModel: HomeFeedViewModel @EnvironmentObject var dataService: DataService @State private var addLinkPresented = false @@ -23,8 +21,8 @@ import SwiftUI var innerBody: some View { ZStack { - NavigationLink("", destination: HomeView(viewModel: inboxViewModel), isActive: $inboxActive) - NavigationLink("", destination: HomeView(viewModel: followingViewModel), isActive: $followingActive) +// NavigationLink("", destination: HomeView(viewModel: inboxViewModel), isActive: $inboxActive) +// NavigationLink("", destination: HomeView(viewModel: followingViewModel), isActive: $followingActive) List { Section { @@ -43,9 +41,9 @@ import SwiftUI }) if inboxMenuState == "open" { - ForEach(inboxViewModel.filters, id: \.self) { filter in + ForEach(viewModel.filters.filter { $0.folder == "inbox" }, id: \.self) { filter in Button(action: { - inboxViewModel.appliedFilter = filter + viewModel.appliedFilter = filter selectedFilter = filter followingActive = false inboxActive = true @@ -80,9 +78,9 @@ import SwiftUI }) if followingMenuState == "open" { - ForEach(followingViewModel.filters, id: \.self) { filter in + ForEach(viewModel.filters.filter { $0.folder == "following" }, id: \.self) { filter in Button(action: { - followingViewModel.appliedFilter = filter + viewModel.appliedFilter = filter selectedFilter = filter inboxActive = false followingActive = true @@ -125,23 +123,18 @@ import SwiftUI } } }.task { - await inboxViewModel.loadFilters(dataService: dataService) - await followingViewModel.loadFilters(dataService: dataService) + await viewModel.loadFilters(dataService: dataService) if inboxActive { - selectedFilter = inboxViewModel.appliedFilter + selectedFilter = viewModel.appliedFilter } else { - selectedFilter = followingViewModel.appliedFilter + selectedFilter = viewModel.appliedFilter } - }.onChange(of: inboxViewModel.appliedFilter) { filter in + }.onChange(of: viewModel.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 - } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift b/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift index 5d426a6bb..12ff7edb6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibrarySplitView.swift @@ -7,28 +7,25 @@ import SwiftUI public struct LibrarySplitView: View { @EnvironmentObject var dataService: DataService - @StateObject private var inboxViewModel = HomeFeedViewModel( - folder: "inbox", + @StateObject private var viewModel = HomeFeedViewModel( + filterKey: "lastSelected", fetcher: LibraryItemFetcher(), - listConfig: LibraryListConfig( - hasFeatureCards: true, - hasReadNowSection: true, - leadingSwipeActions: [.pin], - trailingSwipeActions: [.archive, .delete], - cardStyle: .library - ) - ) - - @StateObject private var followingViewModel = HomeFeedViewModel( - folder: "following", - fetcher: LibraryItemFetcher(), - listConfig: LibraryListConfig( - hasFeatureCards: false, - hasReadNowSection: false, - leadingSwipeActions: [.moveToInbox], - trailingSwipeActions: [.delete], - cardStyle: .library - ) + folderConfigs: [ + "inbox": LibraryListConfig( + hasFeatureCards: true, + hasReadNowSection: true, + leadingSwipeActions: [.pin], + trailingSwipeActions: [.archive, .delete], + cardStyle: .library + ), + "following": LibraryListConfig( + hasFeatureCards: false, + hasReadNowSection: false, + leadingSwipeActions: [.moveToInbox], + trailingSwipeActions: [.delete], + cardStyle: .library + ) + ] ) private let syncManager = LibrarySyncManager() @@ -36,11 +33,11 @@ public struct LibrarySplitView: View { #if os(iOS) public var body: some View { NavigationView { - LibrarySidebar(inboxViewModel: inboxViewModel, followingViewModel: followingViewModel) + LibrarySidebar(viewModel: viewModel) .navigationBarTitleDisplayMode(.inline) .navigationTitle("") - HomeFeedContainerView(viewModel: inboxViewModel) + HomeFeedContainerView(viewModel: viewModel) .navigationViewStyle(.stack) .navigationBarTitleDisplayMode(.inline) } @@ -50,27 +47,27 @@ public struct LibrarySplitView: View { $0.preferredPrimaryColumnWidth = 230 $0.displayModeButtonVisibility = .always } - .onOpenURL { url in - inboxViewModel.linkRequest = nil - if let deepLink = DeepLink.make(from: url) { - switch deepLink { - case let .search(query): - inboxViewModel.searchTerm = query - case let .savedSearch(named): - if let filter = inboxViewModel.findFilter(dataService, named: named) { - inboxViewModel.appliedFilter = filter - } - case let .webAppLinkRequest(requestID): - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { - withoutAnimation { - inboxViewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID) - inboxViewModel.presentWebContainer = true - } - } - } - } - // selectedTab = "inbox" - } +// .onOpenURL { url in +// inboxViewModel.linkRequest = nil +// if let deepLink = DeepLink.make(from: url) { +// switch deepLink { +// case let .search(query): +// inboxViewModel.searchTerm = query +// case let .savedSearch(named): +// if let filter = inboxViewModel.findFilter(dataService, named: named) { +// inboxViewModel.appliedFilter = filter +// } +// case let .webAppLinkRequest(requestID): +// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { +// withoutAnimation { +// inboxViewModel.linkRequest = LinkRequest(id: UUID(), serverID: requestID) +// inboxViewModel.presentWebContainer = true +// } +// } +// } +// } +// // selectedTab = "inbox" +// } .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in Task { await syncManager.syncUpdates(dataService: dataService) diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index a307922c0..c6e11af42 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -32,27 +32,31 @@ struct LibraryTabView: View { } @StateObject private var inboxViewModel = HomeFeedViewModel( - folder: "inbox", + filterKey: "lastSelectedFilter-inbox", fetcher: LibraryItemFetcher(), - listConfig: LibraryListConfig( - hasFeatureCards: true, - hasReadNowSection: true, - leadingSwipeActions: [.pin], - trailingSwipeActions: [.archive, .delete], - cardStyle: .library - ) + folderConfigs: [ + "inbox": LibraryListConfig( + hasFeatureCards: true, + hasReadNowSection: true, + leadingSwipeActions: [.pin], + trailingSwipeActions: [.archive, .delete], + cardStyle: .library + ) + ] ) @StateObject private var followingViewModel = HomeFeedViewModel( - folder: "following", + filterKey: "lastSelectedFilter-following", fetcher: LibraryItemFetcher(), - listConfig: LibraryListConfig( - hasFeatureCards: false, - hasReadNowSection: false, - leadingSwipeActions: [.moveToInbox], - trailingSwipeActions: [.delete], - cardStyle: .library - ) + folderConfigs: [ + "following": LibraryListConfig( + hasFeatureCards: false, + hasReadNowSection: false, + leadingSwipeActions: [.moveToInbox], + trailingSwipeActions: [.delete], + cardStyle: .library + ) + ] ) var currentViewModel: HomeFeedViewModel? { diff --git a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift index a87be224c..0b3576e6d 100644 --- a/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift +++ b/apple/OmnivoreKit/Sources/Services/InternalModels/InternalFilter.swift @@ -15,9 +15,9 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable { lhs.id == rhs.id } - public static var DownloadedFilter: InternalFilter { + public static var InboxDownloadedFilter: InternalFilter { InternalFilter( - id: "downloaded", + id: "inbox-downloaded", name: "Downloaded", folder: "inbox", filter: "", @@ -27,9 +27,9 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable { ) } - public static var DeletedFilter: InternalFilter { + public static var InboxDeletedFilter: InternalFilter { InternalFilter( - id: "deleted", + id: "inbox-deleted", name: "Deleted", folder: "inbox", filter: "in:trash", @@ -39,7 +39,43 @@ public struct InternalFilter: Encodable, Identifiable, Hashable, Equatable { ) } - public static var UnreadFilter: InternalFilter { + public static var InboxUnreadFilter: InternalFilter { + InternalFilter( + id: "inbox-unread", + name: "Unread", + folder: "inbox", + filter: "in:inbox is:unread", + visible: true, + position: -1, + defaultFilter: true + ) + } + + public static var FollowingDownloadedFilter: InternalFilter { + InternalFilter( + id: "following-downloaded", + name: "Downloaded", + folder: "following", + filter: "", + visible: true, + position: -1, + defaultFilter: true + ) + } + + public static var FollowingDeletedFilter: InternalFilter { + InternalFilter( + id: "following-deleted", + name: "Deleted", + folder: "following", + filter: "in:trash", + visible: true, + position: -1, + defaultFilter: true + ) + } + + public static var FollowingUnreadFilter: InternalFilter { InternalFilter( id: "unread", name: "Unread",