diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index 4c4cc5ce..24409e4f 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -83,6 +83,7 @@ 9FB143D22983104A00A27BB1 /* glass.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9F2A542D296B1CC0009B2D7C /* glass.wav */; }; 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 */; }; + 9FBA1D352B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA1D342B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift */; }; 9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBFE63C292A715500C250E9 /* IceCubesApp.swift */; }; 9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; }; 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 = ""; }; 9FB183212AE9268800BBB692 /* IceCubesApp+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IceCubesApp+Menu.swift"; sourceTree = ""; }; 9FB183282AE9449100BBB692 /* IceCubesApp+Scene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IceCubesApp+Scene.swift"; sourceTree = ""; }; + 9FBA1D342B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarEntriesSettingsView.swift; sourceTree = ""; }; 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 = ""; }; 9FC14EF12B494D180006CEE1 /* TagsGroupSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsGroupSettingView.swift; sourceTree = ""; }; @@ -531,6 +533,7 @@ 9FC14EF12B494D180006CEE1 /* TagsGroupSettingView.swift */, 9FC14EF32B494D940006CEE1 /* RemoteTimelinesSettingView.swift */, 9FC14EF52B494DFF0006CEE1 /* RecenTagsSettingView.swift */, + 9FBA1D342B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift */, ); path = Settings; sourceTree = ""; @@ -852,6 +855,7 @@ 9F6028562B3F36AE00476078 /* AppView.swift in Sources */, 9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */, 9F1E8B47298EBCBB00609F80 /* HapticSettingsView.swift in Sources */, + 9FBA1D352B4E9B7E00ADB568 /* SidebarEntriesSettingsView.swift in Sources */, 9F2A5411296A1429009B2D7C /* PushNotificationsView.swift in Sources */, 9F6028582B3F3B7600476078 /* ToolbarTab.swift in Sources */, ); diff --git a/IceCubesApp/App/Main/AppView.swift b/IceCubesApp/App/Main/AppView.swift index 628f827c..32880c6f 100644 --- a/IceCubesApp/App/Main/AppView.swift +++ b/IceCubesApp/App/Main/AppView.swift @@ -26,6 +26,7 @@ struct AppView: View { @State var popToRootTab: Tab = .other @State var iosTabs = iOSTabs.shared + @State var sidebarTabs = SidebarTabs.shared var body: some View { #if os(visionOS) @@ -40,10 +41,15 @@ struct AppView: View { } var availableTabs: [Tab] { - if UIDevice.current.userInterfaceIdiom == .phone || horizontalSizeClass == .compact { - return appAccountsManager.currentClient.isAuth ? iosTabs.tabs : Tab.loggedOutTab() + guard appAccountsManager.currentClient.isAuth else { + 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 { diff --git a/IceCubesApp/App/SideBarView.swift b/IceCubesApp/App/SideBarView.swift index 7b72e4dd..cc5a7f41 100644 --- a/IceCubesApp/App/SideBarView.swift +++ b/IceCubesApp/App/SideBarView.swift @@ -17,11 +17,13 @@ struct SideBarView: View { @Environment(StreamWatcher.self) private var watcher @Environment(UserPreferences.self) private var userPreferences @Environment(RouterPath.self) private var routerPath - + @Binding var selectedTab: Tab @Binding var popToRootTab: Tab var tabs: [Tab] @ViewBuilder var content: () -> Content + + @State private var sidebarTabs = SidebarTabs.shared private func badgeFor(tab: Tab) -> Int { if tab == .notifications, selectedTab != tab, @@ -107,7 +109,7 @@ struct SideBarView: View { private var tabsView: some View { ForEach(tabs) { tab in - if tab != .profile { + if tab != .profile && sidebarTabs.isEnabled(tab) { Button { // ensure keyboard is always dismissed when selecting a tab hideKeyboard() diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index c24ed401..5d30a3a8 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -164,6 +164,10 @@ struct SettingsTabs: View { NavigationLink(destination: TabbarEntriesSettingsView()) { 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()) { Label("settings.general.translate", systemImage: "captions.bubble") diff --git a/IceCubesApp/App/Tabs/Settings/SidebarEntriesSettingsView.swift b/IceCubesApp/App/Tabs/Settings/SidebarEntriesSettingsView.swift new file mode 100644 index 00000000..790eeac8 --- /dev/null +++ b/IceCubesApp/App/Tabs/Settings/SidebarEntriesSettingsView.swift @@ -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) + } +} diff --git a/IceCubesApp/App/Tabs/Tabs.swift b/IceCubesApp/App/Tabs/Tabs.swift index 0b4ad290..9692f18e 100644 --- a/IceCubesApp/App/Tabs/Tabs.swift +++ b/IceCubesApp/App/Tabs/Tabs.swift @@ -6,13 +6,15 @@ import StatusKit import SwiftUI @MainActor -enum Tab: Int, Identifiable, Hashable, CaseIterable { +enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable { case timeline, notifications, mentions, explore, messages, settings, other case trending, federated, local case profile case bookmarks case favorites case post + case followedTags + case lists nonisolated var id: Int { rawValue @@ -21,16 +23,9 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable { static func loggedOutTab() -> [Tab] { [.timeline, .settings] } - - static func loggedInTabs() -> [Tab] { - if UIDevice.current.userInterfaceIdiom == .pad || - 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] - } + + static func visionOSTab() -> [Tab] { + [.profile, .timeline, .notifications, .mentions, .explore, .messages, .settings] } @ViewBuilder @@ -64,6 +59,14 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable { NavigationTab { AccountStatusesListView(mode: .favorites) } + case .followedTags: + NavigationTab { + FollowedTagsListView() + } + case .lists: + NavigationTab { + ListsListView() + } case .post: VStack { } case .other: @@ -100,6 +103,10 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable { Label("accessibility.tabs.profile.picker.favorites", systemImage: iconName) case .post: Label("menu.new-post", systemImage: iconName) + case .followedTags: + Label("timeline.filter.tags", systemImage: iconName) + case .lists: + Label("timeline.filter.lists", systemImage: iconName) case .other: EmptyView() @@ -134,12 +141,61 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable { "star" case .post: "square.and.pencil" + case .followedTags: + "tag" + case .lists: + "list.bullet" 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 class iOSTabs { enum TabEntries: String { diff --git a/IceCubesApp/Resources/Localization/Localizable.xcstrings b/IceCubesApp/Resources/Localization/Localizable.xcstrings index 388561d6..9cf89088 100644 --- a/IceCubesApp/Resources/Localization/Localizable.xcstrings +++ b/IceCubesApp/Resources/Localization/Localizable.xcstrings @@ -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" : { "localizations" : { "be" : { @@ -75118,4 +75237,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Packages/Account/Sources/Account/Lists/ListsListView.swift b/Packages/Account/Sources/Account/Lists/ListsListView.swift new file mode 100644 index 00000000..b9fa1995 --- /dev/null +++ b/Packages/Account/Sources/Account/Lists/ListsListView.swift @@ -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) + } +} + diff --git a/Packages/Account/Sources/Account/Tags/FollowedTagsListView.swift b/Packages/Account/Sources/Account/Tags/FollowedTagsListView.swift new file mode 100644 index 00000000..7cbb8ed4 --- /dev/null +++ b/Packages/Account/Sources/Account/Tags/FollowedTagsListView.swift @@ -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) + } +} + diff --git a/Packages/Env/Sources/Env/CurrentAccount.swift b/Packages/Env/Sources/Env/CurrentAccount.swift index 4b2aa2e7..d48c3e22 100644 --- a/Packages/Env/Sources/Env/CurrentAccount.swift +++ b/Packages/Env/Sources/Env/CurrentAccount.swift @@ -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 } lists.removeAll(where: { $0.id == list.id }) let response = try? await client.delete(endpoint: Lists.list(id: list.id))