Add favorites / bookmarks tab on macOS / iPadOS

This commit is contained in:
Thomas Ricouard 2023-12-28 09:37:02 +01:00
parent f79580f746
commit b0ba6c15da
5 changed files with 164 additions and 1 deletions

View file

@ -13,6 +13,7 @@
069709A8298C87B5006E4CB5 /* OpenDyslexic-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 069709A7298C87B5006E4CB5 /* OpenDyslexic-Regular.otf */; }; 069709A8298C87B5006E4CB5 /* OpenDyslexic-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 069709A7298C87B5006E4CB5 /* OpenDyslexic-Regular.otf */; };
069709AA298C9AD7006E4CB5 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 069709A9298C9AD7006E4CB5 /* AboutView.swift */; }; 069709AA298C9AD7006E4CB5 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 069709A9298C9AD7006E4CB5 /* AboutView.swift */; };
639CDF9C296AC82F00C35E58 /* SafariRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639CDF9B296AC82F00C35E58 /* SafariRouter.swift */; }; 639CDF9C296AC82F00C35E58 /* SafariRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 639CDF9B296AC82F00C35E58 /* SafariRouter.swift */; };
9F15D6002B3D6A850008C220 /* NavigationTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F15D5FF2B3D6A850008C220 /* NavigationTab.swift */; };
9F18801229AE477F00D85459 /* tabSelection.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9F18800A29AE477E00D85459 /* tabSelection.wav */; }; 9F18801229AE477F00D85459 /* tabSelection.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9F18800A29AE477E00D85459 /* tabSelection.wav */; };
9F18801329AE477F00D85459 /* share.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9F18800B29AE477E00D85459 /* share.wav */; }; 9F18801329AE477F00D85459 /* share.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9F18800B29AE477E00D85459 /* share.wav */; };
9F18801429AE477F00D85459 /* bookmark.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9F18800C29AE477E00D85459 /* bookmark.wav */; }; 9F18801429AE477F00D85459 /* bookmark.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9F18800C29AE477E00D85459 /* bookmark.wav */; };
@ -160,6 +161,7 @@
74D6453829945F6200B47B92 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 74D6453829945F6200B47B92 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = "<group>"; };
8C27D979298471E900CDF593 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 8C27D979298471E900CDF593 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = "<group>"; };
9664F1A8299BA5F700CBE70E /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 9664F1A8299BA5F700CBE70E /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; };
9F15D5FF2B3D6A850008C220 /* NavigationTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationTab.swift; sourceTree = "<group>"; };
9F18800A29AE477E00D85459 /* tabSelection.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = tabSelection.wav; sourceTree = "<group>"; }; 9F18800A29AE477E00D85459 /* tabSelection.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = tabSelection.wav; sourceTree = "<group>"; };
9F18800B29AE477E00D85459 /* share.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = share.wav; sourceTree = "<group>"; }; 9F18800B29AE477E00D85459 /* share.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = share.wav; sourceTree = "<group>"; };
9F18800C29AE477E00D85459 /* bookmark.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = bookmark.wav; sourceTree = "<group>"; }; 9F18800C29AE477E00D85459 /* bookmark.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = bookmark.wav; sourceTree = "<group>"; };
@ -424,6 +426,7 @@
9F55C68C2955968700F94077 /* ExploreTab.swift */, 9F55C68C2955968700F94077 /* ExploreTab.swift */,
9F2B92F5295AE04800DE16D0 /* Tabs.swift */, 9F2B92F5295AE04800DE16D0 /* Tabs.swift */,
9F4A48182976B21900A1A038 /* ProfileTab.swift */, 9F4A48182976B21900A1A038 /* ProfileTab.swift */,
9F15D5FF2B3D6A850008C220 /* NavigationTab.swift */,
); );
path = Tabs; path = Tabs;
sourceTree = "<group>"; sourceTree = "<group>";
@ -830,6 +833,7 @@
FA31A9AB2A66BF7C00D5F662 /* EditTagGroupView.swift in Sources */, FA31A9AB2A66BF7C00D5F662 /* EditTagGroupView.swift in Sources */,
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */, 9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */,
9F398AA62935FE8A00A889F2 /* AppRegistry.swift in Sources */, 9F398AA62935FE8A00A889F2 /* AppRegistry.swift in Sources */,
9F15D6002B3D6A850008C220 /* NavigationTab.swift in Sources */,
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */, 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */,
9F4A48192976B21900A1A038 /* ProfileTab.swift in Sources */, 9F4A48192976B21900A1A038 /* ProfileTab.swift in Sources */,
9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */, 9F2B92FA295DA7D700DE16D0 /* AddAccountsView.swift in Sources */,

View file

@ -0,0 +1,22 @@
import SwiftUI
import Env
@MainActor
struct NavigationTab<Content: View>: View {
var content: () -> Content
@State private var routerPath = RouterPath()
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
NavigationStack(path: $routerPath.path) {
content()
.withEnvironments()
.withAppRouter()
.environment(routerPath)
}
}
}

View file

@ -10,6 +10,8 @@ enum Tab: Int, Identifiable, Hashable {
case timeline, notifications, mentions, explore, messages, settings, other case timeline, notifications, mentions, explore, messages, settings, other
case trending, federated, local case trending, federated, local
case profile case profile
case bookmarks
case favorites
nonisolated var id: Int { nonisolated var id: Int {
rawValue rawValue
@ -22,7 +24,7 @@ enum Tab: Int, Identifiable, Hashable {
static func loggedInTabs() -> [Tab] { static func loggedInTabs() -> [Tab] {
if UIDevice.current.userInterfaceIdiom == .pad || if UIDevice.current.userInterfaceIdiom == .pad ||
UIDevice.current.userInterfaceIdiom == .mac { UIDevice.current.userInterfaceIdiom == .mac {
[.timeline, .trending, .federated, .local, .notifications, .mentions, .explore, .messages, .settings] [.timeline, .trending, .federated, .local, .notifications, .mentions, .explore, .messages, .bookmarks, .favorites, .settings]
} else if UIDevice.current.userInterfaceIdiom == .vision { } else if UIDevice.current.userInterfaceIdiom == .vision {
[.profile, .timeline, .trending, .federated, .local, .notifications, .mentions, .explore, .messages, .settings] [.profile, .timeline, .trending, .federated, .local, .notifications, .mentions, .explore, .messages, .settings]
} else { } else {
@ -53,6 +55,14 @@ enum Tab: Int, Identifiable, Hashable {
SettingsTabs(popToRootTab: popToRootTab, isModal: false) SettingsTabs(popToRootTab: popToRootTab, isModal: false)
case .profile: case .profile:
ProfileTab(popToRootTab: popToRootTab) ProfileTab(popToRootTab: popToRootTab)
case .bookmarks:
NavigationTab {
AccountStatusesListView(mode: .bookmarks)
}
case .favorites:
NavigationTab {
AccountStatusesListView(mode: .favorites)
}
case .other: case .other:
EmptyView() EmptyView()
} }
@ -81,6 +91,10 @@ enum Tab: Int, Identifiable, Hashable {
Label("tab.settings", systemImage: iconName) Label("tab.settings", systemImage: iconName)
case .profile: case .profile:
Label("tab.profile", systemImage: iconName) Label("tab.profile", systemImage: iconName)
case .bookmarks:
Label("accessibility.tabs.profile.picker.bookmarks", systemImage: iconName)
case .favorites:
Label("accessibility.tabs.profile.picker.favorites", systemImage: iconName)
case .other: case .other:
EmptyView() EmptyView()
} }
@ -108,6 +122,10 @@ enum Tab: Int, Identifiable, Hashable {
"gear" "gear"
case .profile: case .profile:
"person.crop.circle" "person.crop.circle"
case .bookmarks:
"bookmark"
case .favorites:
"star"
case .other: case .other:
"" ""
} }

View file

@ -0,0 +1,42 @@
import Status
import Network
import SwiftUI
import Env
import Models
import DesignSystem
@MainActor
public struct AccountStatusesListView: View {
@Environment(Theme.self) private var theme
@Environment(Client.self) private var client
@Environment(RouterPath.self) private var routerPath
@State private var viewModel: AccountStatusesListViewModel
@State private var isLoaded = false
public init(mode: AccountStatusesListViewModel.Mode) {
_viewModel = .init(initialValue: .init(mode: mode))
}
public var body: some View {
List {
StatusesListView(fetcher: viewModel, client: client, routerPath: routerPath)
}
.listStyle(.plain)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
#endif
.navigationTitle(viewModel.mode.title)
.navigationBarTitleDisplayMode(.inline)
.refreshable {
await viewModel.fetchNewestStatuses()
}
.task {
guard !isLoaded else { return }
viewModel.client = client
await viewModel.fetchNewestStatuses()
isLoaded = true
}
}
}

View file

@ -0,0 +1,77 @@
import SwiftUI
import Models
import Status
import Network
import Env
@MainActor
@Observable
public class AccountStatusesListViewModel: StatusesFetcher {
public enum Mode {
case bookmarks, favorites
var title: LocalizedStringKey {
switch self {
case .bookmarks:
"accessibility.tabs.profile.picker.bookmarks"
case .favorites:
"accessibility.tabs.profile.picker.favorites"
}
}
func endpoint(sinceId: String?) -> Endpoint {
switch self {
case .bookmarks:
Accounts.bookmarks(sinceId: sinceId)
case .favorites:
Accounts.favorites(sinceId: sinceId)
}
}
}
let mode: Mode
public var statusesState: StatusesState = .loading
var statuses: [Status] = []
var nextPage: LinkHandler?
var client: Client?
init(mode: Mode) {
self.mode = mode
}
public func fetchNewestStatuses() async {
guard let client else { return }
statusesState = .loading
do {
(statuses, nextPage) = try await client.getWithLink(endpoint: mode.endpoint(sinceId: nil))
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
statusesState = .display(statuses: statuses,
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
} catch {
statusesState = .error(error: error)
}
}
public func fetchNextPage() async {
guard let client, let nextId = nextPage?.maxId else { return }
statusesState = .display(statuses: statuses,
nextPageState: .loadingNextPage)
do {
var newStatuses: [Status] = []
(newStatuses, nextPage) = try await client.getWithLink(endpoint: mode.endpoint(sinceId: nextId))
statuses.append(contentsOf: newStatuses)
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
statusesState = .display(statuses: statuses,
nextPageState: nextPage?.maxId != nil ? .hasNextPage : .none)
} catch { }
}
public func statusDidAppear(status: Status) {
}
public func statusDidDisappear(status: Status) {
}
}