WIP allow a single viewmodel for iPad split screen view

This commit is contained in:
Jackson Harper
2023-12-27 14:16:50 +08:00
parent 470cd5ddf2
commit dded89bf32
6 changed files with 247 additions and 173 deletions

View File

@ -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)
// }
// }
// }
}
}

View File

@ -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<Models.LibraryItem>?
@ -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<Models.Filter> = Filter.fetchRequest()
var hasLocalResults = false
let fetchRequest: NSFetchRequest<Models.Filter> = 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

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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? {

View File

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