feat(settings): Preferences section skeleton + Language setting
Marks .preferences implemented, adds the section header/description and a Display subsection. Language is real and wired end-to-end (users.update language field, high confidence per Outline's public API docs) — the rest of the toggles land in follow-up commits, one setting at a time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,8 @@ struct SettingsView: View {
|
||||
@State private var editableName = ""
|
||||
@State private var isSavingName = false
|
||||
@State private var nameErrorMessage: String?
|
||||
@State private var isSavingLanguage = false
|
||||
@State private var languageErrorMessage: String?
|
||||
|
||||
/// Full Local Sync and cache-clearing both need a real connection to be
|
||||
/// safe — clearing while offline (or letting Full Local Sync think it
|
||||
@@ -71,6 +73,7 @@ struct SettingsView: View {
|
||||
switch section {
|
||||
case .appearance: appearanceDetail
|
||||
case .profile: profileDetail
|
||||
case .preferences: preferencesDetail
|
||||
case .offlineSync: offlineSyncDetail
|
||||
case .advanced: advancedDetail
|
||||
case .about: aboutDetail
|
||||
@@ -231,6 +234,84 @@ struct SettingsView: View {
|
||||
.frame(maxWidth: 420, alignment: .leading)
|
||||
}
|
||||
|
||||
// MARK: - Preferences
|
||||
|
||||
private var preferencesDetail: some View {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
sectionHeader
|
||||
Text("Manage settings that affect your personal experience.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
preferencesSubsection("Display") {
|
||||
languageRow
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 480, alignment: .leading)
|
||||
.task { await refreshProfile() }
|
||||
}
|
||||
|
||||
private func preferencesSubsection<Content: View>(
|
||||
_ title: String,
|
||||
@ViewBuilder content: () -> Content
|
||||
) -> some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
private var languageRow: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Language")
|
||||
Text("Choose the interface language. Community translations are accepted through our translation portal.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
if isSavingLanguage {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Picker("Language", selection: languageBinding) {
|
||||
ForEach(OutlineLocale.all) { locale in
|
||||
Text(locale.label).tag(locale.code)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.frame(width: 200)
|
||||
}
|
||||
}
|
||||
if let languageErrorMessage {
|
||||
Text(languageErrorMessage)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var languageBinding: Binding<String> {
|
||||
Binding(
|
||||
get: { session.userLanguage ?? "en_US" },
|
||||
set: { newValue in Task { await saveLanguage(newValue) } }
|
||||
)
|
||||
}
|
||||
|
||||
private func saveLanguage(_ code: String) async {
|
||||
guard code != session.userLanguage, let apiClient = session.apiClient, let userId = session.userId else { return }
|
||||
isSavingLanguage = true
|
||||
defer { isSavingLanguage = false }
|
||||
do {
|
||||
let updated = try await apiClient.updateUserLanguage(UpdateUserLanguageRequest(id: userId, language: code))
|
||||
session.applyUpdatedProfile(updated)
|
||||
languageErrorMessage = nil
|
||||
} catch {
|
||||
languageErrorMessage = outlineErrorMessage(error, fallback: "Couldn't update your language.")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Offline & Sync
|
||||
|
||||
private var offlineSyncDetail: some View {
|
||||
|
||||
@@ -122,7 +122,7 @@ enum SettingsSection: String, CaseIterable, Identifiable, Hashable {
|
||||
/// placeholder until its content is specified and built.
|
||||
var isImplemented: Bool {
|
||||
switch self {
|
||||
case .appearance, .offlineSync, .advanced, .about, .profile:
|
||||
case .appearance, .offlineSync, .advanced, .about, .profile, .preferences:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import Foundation
|
||||
|
||||
/// Outline's interface-language options. Not exhaustive — Outline accepts
|
||||
/// community translations via its own translation portal, so the real list
|
||||
/// on any given server can be longer than this; this covers the common
|
||||
/// cases and falls back to showing whatever code the server already has
|
||||
/// set even if it isn't in this list.
|
||||
struct OutlineLocale: Identifiable, Hashable {
|
||||
let code: String
|
||||
let label: String
|
||||
|
||||
var id: String { code }
|
||||
|
||||
static let all: [OutlineLocale] = [
|
||||
OutlineLocale(code: "en_US", label: "English (US)"),
|
||||
OutlineLocale(code: "de_DE", label: "Deutsch"),
|
||||
OutlineLocale(code: "fr_FR", label: "Français"),
|
||||
OutlineLocale(code: "es_ES", label: "Español"),
|
||||
OutlineLocale(code: "pt_PT", label: "Português"),
|
||||
OutlineLocale(code: "pt_BR", label: "Português (Brasil)"),
|
||||
OutlineLocale(code: "it_IT", label: "Italiano"),
|
||||
OutlineLocale(code: "nl_NL", label: "Nederlands"),
|
||||
OutlineLocale(code: "pl_PL", label: "Polski"),
|
||||
OutlineLocale(code: "ru_RU", label: "Русский"),
|
||||
OutlineLocale(code: "ja_JP", label: "日本語"),
|
||||
OutlineLocale(code: "ko_KR", label: "한국어"),
|
||||
OutlineLocale(code: "zh_CN", label: "中文 (简体)"),
|
||||
OutlineLocale(code: "zh_TW", label: "中文 (繁體)"),
|
||||
]
|
||||
|
||||
static func label(for code: String) -> String {
|
||||
all.first(where: { $0.code == code })?.label ?? code
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user