Followed Tags + Lists tab. + sidebar customization

This commit is contained in:
Thomas Ricouard 2024-01-10 13:26:55 +01:00
parent 6246d7b0a5
commit 0da8228e61
10 changed files with 329 additions and 18 deletions

View file

@ -83,6 +83,7 @@
9FB143D22983104A00A27BB1 /* glass.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9F2A542D296B1CC0009B2D7C /* glass.wav */; }; 9FB143D22983104A00A27BB1 /* glass.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9F2A542D296B1CC0009B2D7C /* glass.wav */; };
9FB183222AE9268800BBB692 /* IceCubesApp+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB183212AE9268800BBB692 /* IceCubesApp+Menu.swift */; }; 9FB183222AE9268800BBB692 /* IceCubesApp+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB183212AE9268800BBB692 /* IceCubesApp+Menu.swift */; };
9FB183292AE9449100BBB692 /* IceCubesApp+Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB183282AE9449100BBB692 /* IceCubesApp+Scene.swift */; }; 9FB183292AE9449100BBB692 /* IceCubesApp+Scene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB183282AE9449100BBB692 /* IceCubesApp+Scene.swift */; };
9FBA1D352B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA1D342B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift */; };
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */; }; 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */; };
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; }; 9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; };
9FC14EF22B494D180006CEE1 /* TagsGroupSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC14EF12B494D180006CEE1 /* TagsGroupSettingView.swift */; }; 9FC14EF22B494D180006CEE1 /* TagsGroupSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC14EF12B494D180006CEE1 /* TagsGroupSettingView.swift */; };
@ -227,6 +228,7 @@
9FAE4ACA293783B000772766 /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = "<group>"; }; 9FAE4ACA293783B000772766 /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = "<group>"; };
9FB183212AE9268800BBB692 /* IceCubesApp+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IceCubesApp+Menu.swift"; sourceTree = "<group>"; }; 9FB183212AE9268800BBB692 /* IceCubesApp+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IceCubesApp+Menu.swift"; sourceTree = "<group>"; };
9FB183282AE9449100BBB692 /* IceCubesApp+Scene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IceCubesApp+Scene.swift"; sourceTree = "<group>"; }; 9FB183282AE9449100BBB692 /* IceCubesApp+Scene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IceCubesApp+Scene.swift"; sourceTree = "<group>"; };
9FBA1D342B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarEntriesSettingsView.swift; sourceTree = "<group>"; };
9FBFE639292A715500C250E9 /* Ice Cubes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ice Cubes.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 9FBFE639292A715500C250E9 /* Ice Cubes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ice Cubes.app"; sourceTree = BUILT_PRODUCTS_DIR; };
9FBFE63C292A715500C250E9 /* IceCubesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesApp.swift; sourceTree = "<group>"; }; 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceCubesApp.swift; sourceTree = "<group>"; };
9FC14EF12B494D180006CEE1 /* TagsGroupSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsGroupSettingView.swift; sourceTree = "<group>"; }; 9FC14EF12B494D180006CEE1 /* TagsGroupSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsGroupSettingView.swift; sourceTree = "<group>"; };
@ -531,6 +533,7 @@
9FC14EF12B494D180006CEE1 /* TagsGroupSettingView.swift */, 9FC14EF12B494D180006CEE1 /* TagsGroupSettingView.swift */,
9FC14EF32B494D940006CEE1 /* RemoteTimelinesSettingView.swift */, 9FC14EF32B494D940006CEE1 /* RemoteTimelinesSettingView.swift */,
9FC14EF52B494DFF0006CEE1 /* RecenTagsSettingView.swift */, 9FC14EF52B494DFF0006CEE1 /* RecenTagsSettingView.swift */,
9FBA1D342B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift */,
); );
path = Settings; path = Settings;
sourceTree = "<group>"; sourceTree = "<group>";
@ -852,6 +855,7 @@
9F6028562B3F36AE00476078 /* AppView.swift in Sources */, 9F6028562B3F36AE00476078 /* AppView.swift in Sources */,
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */, 9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */,
9F1E8B47298EBCBB00609F80 /* HapticSettingsView.swift in Sources */, 9F1E8B47298EBCBB00609F80 /* HapticSettingsView.swift in Sources */,
9FBA1D352B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift in Sources */,
9F2A5411296A1429009B2D7C /* PushNotificationsView.swift in Sources */, 9F2A5411296A1429009B2D7C /* PushNotificationsView.swift in Sources */,
9F6028582B3F3B7600476078 /* ToolbarTab.swift in Sources */, 9F6028582B3F3B7600476078 /* ToolbarTab.swift in Sources */,
); );

View file

@ -26,6 +26,7 @@ struct AppView: View {
@State var popToRootTab: Tab = .other @State var popToRootTab: Tab = .other
@State var iosTabs = iOSTabs.shared @State var iosTabs = iOSTabs.shared
@State var sidebarTabs = SidebarTabs.shared
var body: some View { var body: some View {
#if os(visionOS) #if os(visionOS)
@ -40,10 +41,15 @@ struct AppView: View {
} }
var availableTabs: [Tab] { var availableTabs: [Tab] {
if UIDevice.current.userInterfaceIdiom == .phone || horizontalSizeClass == .compact { guard appAccountsManager.currentClient.isAuth else {
return appAccountsManager.currentClient.isAuth ? iosTabs.tabs : Tab.loggedOutTab() return Tab.loggedOutTab()
} }
return appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab() if UIDevice.current.userInterfaceIdiom == .phone || horizontalSizeClass == .compact {
return iosTabs.tabs
} else if UIDevice.current.userInterfaceIdiom == .vision {
return Tab.visionOSTab()
}
return sidebarTabs.tabs.map{ $0.tab }
} }
var tabBarView: some View { var tabBarView: some View {

View file

@ -17,11 +17,13 @@ struct SideBarView<Content: View>: View {
@Environment(StreamWatcher.self) private var watcher @Environment(StreamWatcher.self) private var watcher
@Environment(UserPreferences.self) private var userPreferences @Environment(UserPreferences.self) private var userPreferences
@Environment(RouterPath.self) private var routerPath @Environment(RouterPath.self) private var routerPath
@Binding var selectedTab: Tab @Binding var selectedTab: Tab
@Binding var popToRootTab: Tab @Binding var popToRootTab: Tab
var tabs: [Tab] var tabs: [Tab]
@ViewBuilder var content: () -> Content @ViewBuilder var content: () -> Content
@State private var sidebarTabs = SidebarTabs.shared
private func badgeFor(tab: Tab) -> Int { private func badgeFor(tab: Tab) -> Int {
if tab == .notifications, selectedTab != tab, if tab == .notifications, selectedTab != tab,
@ -107,7 +109,7 @@ struct SideBarView<Content: View>: View {
private var tabsView: some View { private var tabsView: some View {
ForEach(tabs) { tab in ForEach(tabs) { tab in
if tab != .profile { if tab != .profile && sidebarTabs.isEnabled(tab) {
Button { Button {
// ensure keyboard is always dismissed when selecting a tab // ensure keyboard is always dismissed when selecting a tab
hideKeyboard() hideKeyboard()

View file

@ -164,6 +164,10 @@ struct SettingsTabs: View {
NavigationLink(destination: TabbarEntriesSettingsView()) { NavigationLink(destination: TabbarEntriesSettingsView()) {
Label("settings.general.tabbarEntries", systemImage: "platter.filled.bottom.iphone") Label("settings.general.tabbarEntries", systemImage: "platter.filled.bottom.iphone")
} }
} else if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
NavigationLink(destination: SidebarEntriesSettingsView()) {
Label("settings.general.sidebarEntries", systemImage: "sidebar.squares.leading")
}
} }
NavigationLink(destination: TranslationSettingsView()) { NavigationLink(destination: TranslationSettingsView()) {
Label("settings.general.translate", systemImage: "captions.bubble") Label("settings.general.translate", systemImage: "captions.bubble")

View file

@ -0,0 +1,39 @@
import DesignSystem
import Env
import SwiftUI
struct SidebarEntriesSettingsView: View {
@Environment(Theme.self) private var theme
@Environment(UserPreferences.self) private var userPreferences
@State private var sidebarTabs = SidebarTabs.shared
var body: some View {
@Bindable var userPreferences = userPreferences
Form {
Section {
ForEach($sidebarTabs.tabs, id: \.tab) { $tab in
if tab.tab != .profile && tab.tab != .settings {
Toggle(isOn: $tab.enabled) {
tab.tab.label
}
}
}
.onMove(perform: move)
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
.environment(\.editMode, .constant(.active))
.navigationTitle("settings.general.sidebarEntries")
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
}
func move(from source: IndexSet, to destination: Int) {
sidebarTabs.tabs.move(fromOffsets: source, toOffset: destination)
}
}

View file

@ -6,13 +6,15 @@ import StatusKit
import SwiftUI import SwiftUI
@MainActor @MainActor
enum Tab: Int, Identifiable, Hashable, CaseIterable { enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
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 bookmarks
case favorites case favorites
case post case post
case followedTags
case lists
nonisolated var id: Int { nonisolated var id: Int {
rawValue rawValue
@ -21,16 +23,9 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable {
static func loggedOutTab() -> [Tab] { static func loggedOutTab() -> [Tab] {
[.timeline, .settings] [.timeline, .settings]
} }
static func loggedInTabs() -> [Tab] { static func visionOSTab() -> [Tab] {
if UIDevice.current.userInterfaceIdiom == .pad || [.profile, .timeline, .notifications, .mentions, .explore, .messages, .settings]
UIDevice.current.userInterfaceIdiom == .mac {
[.timeline, .trending, .federated, .local, .notifications, .mentions, .explore, .messages, .bookmarks, .favorites, .profile, .settings]
} else if UIDevice.current.userInterfaceIdiom == .vision {
[.profile, .timeline, .notifications, .mentions, .explore, .messages, .settings]
} else {
[.timeline, .notifications, .explore, .messages, .profile]
}
} }
@ViewBuilder @ViewBuilder
@ -64,6 +59,14 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable {
NavigationTab { NavigationTab {
AccountStatusesListView(mode: .favorites) AccountStatusesListView(mode: .favorites)
} }
case .followedTags:
NavigationTab {
FollowedTagsListView()
}
case .lists:
NavigationTab {
ListsListView()
}
case .post: case .post:
VStack { } VStack { }
case .other: case .other:
@ -100,6 +103,10 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable {
Label("accessibility.tabs.profile.picker.favorites", systemImage: iconName) Label("accessibility.tabs.profile.picker.favorites", systemImage: iconName)
case .post: case .post:
Label("menu.new-post", systemImage: iconName) Label("menu.new-post", systemImage: iconName)
case .followedTags:
Label("timeline.filter.tags", systemImage: iconName)
case .lists:
Label("timeline.filter.lists", systemImage: iconName)
case .other: case .other:
EmptyView() EmptyView()
@ -134,12 +141,61 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable {
"star" "star"
case .post: case .post:
"square.and.pencil" "square.and.pencil"
case .followedTags:
"tag"
case .lists:
"list.bullet"
case .other: case .other:
"" ""
} }
} }
} }
@Observable
class SidebarTabs {
struct SidedebarTab: Hashable, Codable {
let tab: Tab
var enabled: Bool
}
class Storage {
@AppStorage("sidebar_tabs") var tabs: [SidedebarTab] = [
.init(tab: .timeline, enabled: true),
.init(tab: .trending, enabled: true),
.init(tab: .federated, enabled: true),
.init(tab: .local, enabled: true),
.init(tab: .notifications, enabled: true),
.init(tab: .mentions, enabled: true),
.init(tab: .messages, enabled: true),
.init(tab: .explore, enabled: true),
.init(tab: .bookmarks, enabled: true),
.init(tab: .favorites, enabled: true),
.init(tab: .followedTags, enabled: true),
.init(tab: .lists, enabled: true),
.init(tab: .settings, enabled: true),
.init(tab: .profile, enabled: true),
]
}
private let storage = Storage()
public static let shared = SidebarTabs()
var tabs: [SidedebarTab] {
didSet {
storage.tabs = tabs
}
}
func isEnabled(_ tab: Tab) -> Bool {
tabs.first(where: { $0.tab.id == tab.id })?.enabled == true
}
private init() {
tabs = storage.tabs
}
}
@Observable @Observable
class iOSTabs { class iOSTabs {
enum TabEntries: String { enum TabEntries: String {

View file

@ -47558,6 +47558,125 @@
} }
} }
}, },
"settings.general.sidebarEntries" : {
"extractionState" : "manual",
"localizations" : {
"be" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"ca" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"de" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sidebar Customizations"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sidebar Customizations"
}
},
"es" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"eu" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"fr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"it" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"ja" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"ko" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"nb" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"nl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"pl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"tr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"uk" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Sidebar Customizations"
}
}
}
},
"settings.general.swipeactions" : { "settings.general.swipeactions" : {
"localizations" : { "localizations" : {
"be" : { "be" : {
@ -75118,4 +75237,4 @@
} }
}, },
"version" : "1.0" "version" : "1.0"
} }

View file

@ -0,0 +1,46 @@
import DesignSystem
import Models
import SwiftUI
import Env
public struct ListsListView: View {
@Environment(CurrentAccount.self) private var currentAccount
@Environment(Theme.self) private var theme
public init() {}
public var body: some View {
List {
ForEach(currentAccount.lists) { list in
NavigationLink(value: RouterDestination.list(list: list)) {
Text(list.title)
.font(.scaledHeadline)
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
.onDelete { index in
if let index = index.first {
Task {
await currentAccount.deleteList(currentAccount.lists[index])
}
}
}
}
.task {
await currentAccount.fetchLists()
}
.refreshable {
await currentAccount.fetchLists()
}
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
.listStyle(.plain)
.navigationTitle("timeline.filter.lists")
.navigationBarTitleDisplayMode(.inline)
}
}

View file

@ -0,0 +1,35 @@
import DesignSystem
import Models
import SwiftUI
import Env
public struct FollowedTagsListView: View {
@Environment(CurrentAccount.self) private var currentAccount
@Environment(Theme.self) private var theme
public init() {}
public var body: some View {
List(currentAccount.tags) { tag in
TagRowView(tag: tag)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
.padding(.vertical, 4)
}
.task {
await currentAccount.fetchFollowedTags()
}
.refreshable {
await currentAccount.fetchFollowedTags()
}
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
.listStyle(.plain)
.navigationTitle("timeline.filter.tags")
.navigationBarTitleDisplayMode(.inline)
}
}

View file

@ -88,7 +88,7 @@ import Observation
} }
} }
public func deleteList(list: Models.List) async { public func deleteList(_ list: Models.List) async {
guard let client else { return } guard let client else { return }
lists.removeAll(where: { $0.id == list.id }) lists.removeAll(where: { $0.id == list.id })
let response = try? await client.delete(endpoint: Lists.list(id: list.id)) let response = try? await client.delete(endpoint: Lists.list(id: list.id))