Skip to content

Commit 24e2a63

Browse files
feat (settings): inital new settings UI
Follows macOS Ventura settings style, enabled by default
1 parent c2d91b2 commit 24e2a63

File tree

4 files changed

+246
-72
lines changed

4 files changed

+246
-72
lines changed

Swiftcord/SwiftcordApp.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,22 @@ struct SwiftcordApp: App {
9090
}
9191
}
9292
.commands {
93-
#if !APP_STORE
9493
CommandGroup(after: .appInfo) {
94+
#if !APP_STORE
9595
CheckForUpdatesView(updaterViewModel: updaterViewModel)
96+
#endif
97+
if #available(macOS 13, *) {
98+
SettingsCommands()
99+
}
96100
}
97-
#endif
98101

99102
SidebarCommands()
100103
NavigationCommands(state: state, gateway: gateway)
101104
}
102105
.windowStyle(.hiddenTitleBar)
103106
.windowToolbarStyle(.unified)
104107

105-
Settings {
108+
WindowGroup(id: "settings") { // Identify the window group.
106109
SettingsView()
107110
.environmentObject(gateway)
108111
.environmentObject(state)
@@ -112,7 +115,18 @@ struct SwiftcordApp: App {
112115
? .dark
113116
: (selectedTheme == "light" ? .light : .none)
114117
)
115-
// .environment(\.locale, .init(identifier: "zh-Hans"))
116118
}
117119
}
118120
}
121+
122+
@available(macOS 13, *)
123+
struct SettingsCommands: View {
124+
@Environment(\.openWindow) private var openWindow
125+
126+
var body: some View {
127+
Divider()
128+
Button("Settings") {
129+
openWindow(id: "settings")
130+
}.keyboardShortcut(",", modifiers: .command)
131+
}
132+
}

Swiftcord/Views/Settings/App/AppSettingsAdvancedView.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import AppCenterAnalytics
1010

1111
struct AppSettingsAdvancedView: View {
1212
@AppStorage("local.analytics") private var analyticsEnabled = true
13+
@AppStorage("local.newSettingsUI") private var newSettingsUI = true
1314
@State private var hasToggledAnalytics = false
1415

1516
var body: some View {
@@ -37,6 +38,23 @@ struct AppSettingsAdvancedView: View {
3738
Divider()
3839

3940
Text("settings.app.advanced.crashes")
41+
42+
Divider()
43+
44+
Text("Interface Trial")
45+
.font(.headline)
46+
.textCase(.uppercase)
47+
.opacity(0.75)
48+
VStack(alignment: .leading) {
49+
Toggle(isOn: $newSettingsUI) {
50+
Text("Try new Settings UI beta").frame(maxWidth: .infinity, alignment: .leading)
51+
}
52+
.toggleStyle(.switch)
53+
.tint(.green)
54+
if newSettingsUI {
55+
Text("Keep in mind that this new UI is not yet fully functional").font(.caption)
56+
}
57+
}
4058
}
4159
}
4260
}

Swiftcord/Views/Settings/SettingsView.swift

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,23 @@ import DiscordKitCore
1212
struct SettingsView: View {
1313
@EnvironmentObject var gateway: DiscordGateway
1414

15+
@AppStorage("local.newSettingsUI") private var newUI = true
16+
1517
var body: some View {
1618
if let user = gateway.cache.user {
17-
SettingsWithUserView(user: user)
19+
if #available(macOS 13.0, *), newUI {
20+
ModernSettings(user: user)
21+
} else {
22+
LegacySettings(user: user)
23+
}
1824
} else {
1925
NoGatewayView()
2026
}
2127
}
2228
}
2329

2430
private extension SettingsView {
25-
struct SettingsWithUserView: View {
31+
struct LegacySettings: View {
2632
let user: CurrentUser
2733

2834
var body: some View {
@@ -50,6 +56,150 @@ private extension SettingsView {
5056
.frame(width: 900, height: 600)
5157
}
5258
}
59+
@available(macOS 13, *)
60+
struct ModernSettings: View {
61+
let user: CurrentUser
62+
63+
private struct Page: Hashable, Identifiable {
64+
internal init(_ name: Name, icon: Icon? = nil, children: [SettingsView.ModernSettings.Page] = []) {
65+
self.children = children
66+
self.name = name
67+
self.icon = icon
68+
}
69+
70+
var id: String { name.rawValue }
71+
72+
let children: [Page]
73+
let name: Name
74+
var nameString: LocalizedStringKey { LocalizedStringKey(name.rawValue) }
75+
let icon: Icon?
76+
77+
struct Icon: Hashable {
78+
let baseColor: Color
79+
let icon: String
80+
}
81+
82+
enum Name: String {
83+
// MARK: User Settings
84+
case userSection = "User Settings"
85+
case account = "My Account"
86+
case profile = "User Profile"
87+
case privacy = "Privacy & Safety"
88+
case apps = "Authorized Apps"
89+
case connections = "Connections"
90+
case logOut = "Log Out"
91+
// MARK: Payment Settings
92+
case paymentSection = "Payment Settings"
93+
case nitro = "Nitro"
94+
case boost = "Server Boost"
95+
case subscriptions = "Subscriptions"
96+
case gift = "Gift Inventory"
97+
case billing = "Billing"
98+
// MARK: App Settings
99+
case appSection = "App Settings"
100+
case appearance = "settings.app.appearance"
101+
case accessibility = "settings.app.accessibility"
102+
case voiceVideo = "settings.app.voiceVideo"
103+
case textImages = "settings.app.textImages"
104+
case notifs = "settings.app.notifs"
105+
case keybinds = "settings.app.keybinds"
106+
case lang = "settings.app.lang"
107+
case streamer = "settings.app.streamer"
108+
case advanced = "settings.app.advanced"
109+
}
110+
}
111+
private static let pages: [Page] = [
112+
.init(.userSection, children: [
113+
.init(.account, icon: .init(baseColor: .blue, icon: "person.fill")),
114+
.init(.profile, icon: .init(baseColor: .blue, icon: "person.crop.circle")),
115+
.init(.privacy, icon: .init(baseColor: .red, icon: "shield.lefthalf.filled"))
116+
]),
117+
.init(.paymentSection, children: [
118+
.init(.nitro, icon: .init(baseColor: .accentColor, icon: "person.crop.circle")),
119+
.init(.boost, icon: .init(baseColor: .green, icon: "person.crop.circle")),
120+
.init(.subscriptions, icon: .init(baseColor: .blue, icon: "person.crop.circle")),
121+
.init(.gift, icon: .init(baseColor: .blue, icon: "person.crop.circle")),
122+
.init(.billing, icon: .init(baseColor: .blue, icon: "person.crop.circle"))
123+
]),
124+
.init(.appSection, children: [
125+
.init(.appearance, icon: .init(baseColor: .black, icon: "person.crop.circle")),
126+
.init(.accessibility, icon: .init(baseColor: .blue, icon: "person.crop.circle")),
127+
.init(.voiceVideo, icon: .init(baseColor: .blue, icon: "person.crop.circle")),
128+
.init(.textImages, icon: .init(baseColor: .blue, icon: "person.crop.circle")),
129+
.init(.notifs, icon: .init(baseColor: .blue, icon: "person.crop.circle")),
130+
.init(.keybinds, icon: .init(baseColor: .blue, icon: "person.crop.circle")),
131+
.init(.lang, icon: .init(baseColor: .blue, icon: "person.crop.circle")),
132+
.init(.streamer, icon: .init(baseColor: .blue, icon: "person.crop.circle")),
133+
.init(.advanced, icon: .init(baseColor: .blue, icon: "person.crop.circle"))
134+
])
135+
]
136+
137+
@State private var selectedPage = pages.first!.children.first!
138+
@State private var filter = ""
139+
140+
@ViewBuilder
141+
private func navigationItem(item: Page) -> some View {
142+
if filter.isEmpty || item.name.rawValue.lowercased().contains(filter.lowercased()) {
143+
NavigationLink(value: item) {
144+
if item.name == .account {
145+
HStack {
146+
BetterImageView(url: user.avatarURL(size: 160))
147+
.frame(width: 40, height: 40)
148+
.clipShape(Circle())
149+
VStack(alignment: .leading) {
150+
Text(user.username).font(.headline)
151+
Text("Discord Account").font(.caption)
152+
}
153+
}
154+
} else {
155+
Label {
156+
Text(item.nameString)
157+
} icon: {
158+
if let icon = item.icon {
159+
Image(systemName: icon.icon)
160+
.foregroundColor(.primary)
161+
.frame(width: 20, height: 20)
162+
.background(RoundedRectangle(cornerRadius: 5, style: .continuous).fill(icon.baseColor.gradient))
163+
} else {
164+
EmptyView()
165+
}
166+
}
167+
}
168+
}
169+
}
170+
}
171+
172+
var body: some View {
173+
NavigationSplitView {
174+
List(Self.pages, selection: $selectedPage) { category in
175+
Section(category.nameString) {
176+
ForEach(category.children) { child in
177+
navigationItem(item: child)
178+
}
179+
}
180+
}.frame(idealWidth: 215)
181+
} detail: {
182+
ScrollView {
183+
Group {
184+
switch selectedPage.name {
185+
case .account:
186+
UserSettingsAccountView(user: user)
187+
case .profile:
188+
UserSettingsProfileView(user: user)
189+
case .privacy:
190+
UserSettingsPrivacySafetyView()
191+
case .advanced:
192+
AppSettingsAdvancedView()
193+
default:
194+
Text("Unimplemented view: \(selectedPage.name.rawValue)")
195+
}
196+
}.padding(20)
197+
}
198+
}
199+
.searchable(text: $filter, placement: .sidebar)
200+
.navigationTitle(selectedPage.nameString)
201+
}
202+
}
53203

54204
struct NoGatewayView: View {
55205
var body: some View {

0 commit comments

Comments
 (0)