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",