0.10.6
This commit is contained in:
commit
92660a4ba0
228 files changed
+24496
No files matched your search
@@ -0,0 +1,194 @@
|
||||
import PhotosUI
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@ObservedObject var model: AppModel
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var showingSignOutConfirmation = false
|
||||
@State private var forgetHistory = false
|
||||
@State private var avatarItem: PhotosPickerItem?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
if let account = model.account {
|
||||
Section("Профиль") {
|
||||
HStack(spacing: 14) {
|
||||
AvatarView(
|
||||
conversation: Conversation(
|
||||
jid: account.normalizedJID,
|
||||
displayName: account.displayName.isEmpty ? nil : account.displayName
|
||||
),
|
||||
imageData: model.avatarData(for: account.normalizedJID),
|
||||
size: 64
|
||||
)
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(account.displayName.isEmpty ? account.normalizedJID : account.displayName)
|
||||
.font(.headline)
|
||||
PhotosPicker(selection: $avatarItem, matching: .images) {
|
||||
Label("Обновить аватар", systemImage: "photo.badge.plus")
|
||||
}
|
||||
.disabled(model.isUpdatingAvatar)
|
||||
}
|
||||
Spacer()
|
||||
if model.isUpdatingAvatar {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Аккаунт") {
|
||||
LabeledContent("JID", value: account.normalizedJID)
|
||||
LabeledContent("Ресурс", value: account.effectiveResource)
|
||||
if let host = account.manualHost {
|
||||
LabeledContent("Сервер", value: "\(host):\(account.manualPort ?? (account.usesDirectTLS ? 5223 : 5222))")
|
||||
} else {
|
||||
LabeledContent("Обнаружение", value: "DNS SRV")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Соединение") {
|
||||
HStack {
|
||||
Label(connectionTitle, systemImage: connectionIcon)
|
||||
.foregroundStyle(connectionColor)
|
||||
Spacer()
|
||||
if model.connectionStatus != .connected {
|
||||
Button("Подключить") { Task { await model.reconnect() } }
|
||||
}
|
||||
}
|
||||
LabeledContent("История MAM", value: model.isArchiveSyncing ? "Синхронизация" : "Готово")
|
||||
}
|
||||
|
||||
Section("Шифрование") {
|
||||
Toggle("OMEMO по умолчанию", isOn: Binding(
|
||||
get: { model.globalEncryptionEnabled },
|
||||
set: { model.setGlobalEncryptionEnabled($0) }
|
||||
))
|
||||
Text("Для отдельного чата режим можно изменить через значок замка в его панели.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
if !model.globalEncryptionEnabled {
|
||||
Label("Новые сообщения без отдельного правила отправляются открытым текстом.", systemImage: "exclamationmark.triangle.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
LabeledContent("Состояние", value: model.isOMEMOReady ? "Активно" : "Подготовка")
|
||||
if let fingerprint = model.ownFingerprint {
|
||||
VStack(alignment: .leading, spacing: 7) {
|
||||
Text("Отпечаток этого устройства")
|
||||
.font(.subheadline)
|
||||
Text(fingerprint.chunked(every: 8).joined(separator: " "))
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Уведомления") {
|
||||
Label("Локальные уведомления включаются после входа", systemImage: "bell.badge")
|
||||
Text("Для доставки при выгруженном iOS-приложении требуется APNs и XEP-0357 push-шлюз приложения. Обычный XMPP-сокет не может постоянно работать в фоне.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Section("Приватность чата") {
|
||||
Toggle("Показывать статус набора", isOn: Binding(
|
||||
get: { model.typingIndicatorsEnabled },
|
||||
set: { model.setTypingIndicatorsEnabled($0) }
|
||||
))
|
||||
Text("Когда включено, Luma показывает «печатает…» и отправляет собеседникам стандартные XMPP-состояния набора.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Удалить локальную историю при выходе", isOn: $forgetHistory)
|
||||
|
||||
Button(role: .destructive) {
|
||||
showingSignOutConfirmation = true
|
||||
} label: {
|
||||
Label("Выйти из аккаунта", systemImage: "rectangle.portrait.and.arrow.right")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Настройки")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Готово") { dismiss() }
|
||||
}
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Выйти из Luma?",
|
||||
isPresented: $showingSignOutConfirmation,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Выйти", role: .destructive) {
|
||||
Task {
|
||||
await model.signOut(forgetHistory: forgetHistory)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Button("Отмена", role: .cancel) {}
|
||||
} message: {
|
||||
Text(forgetHistory
|
||||
? "Пароль и локальная история будут удалены. Серверная история MAM не изменится."
|
||||
: "Пароль будет удалён из Keychain. Серверная история MAM не изменится.")
|
||||
}
|
||||
.onChange(of: avatarItem) { _, item in
|
||||
guard let item else { return }
|
||||
Task {
|
||||
do {
|
||||
guard let data = try await item.loadTransferable(type: Data.self) else { return }
|
||||
await model.updateOwnAvatar(from: data)
|
||||
avatarItem = nil
|
||||
} catch {
|
||||
model.errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 500, minHeight: 560)
|
||||
#endif
|
||||
}
|
||||
|
||||
private var connectionTitle: String {
|
||||
switch model.connectionStatus {
|
||||
case .connected: return "Подключено"
|
||||
case .connecting: return "Подключение"
|
||||
case .disconnected: return "Отключено"
|
||||
}
|
||||
}
|
||||
|
||||
private var connectionIcon: String {
|
||||
switch model.connectionStatus {
|
||||
case .connected: return "checkmark.circle.fill"
|
||||
case .connecting: return "arrow.triangle.2.circlepath"
|
||||
case .disconnected: return "xmark.circle.fill"
|
||||
}
|
||||
}
|
||||
|
||||
private var connectionColor: Color {
|
||||
switch model.connectionStatus {
|
||||
case .connected: return .green
|
||||
case .connecting: return .orange
|
||||
case .disconnected: return .red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
func chunked(every size: Int) -> [String] {
|
||||
guard size > 0 else { return [self] }
|
||||
var result: [String] = []
|
||||
var cursor = startIndex
|
||||
while cursor < endIndex {
|
||||
let end = index(cursor, offsetBy: size, limitedBy: endIndex) ?? endIndex
|
||||
result.append(String(self[cursor..<end]))
|
||||
cursor = end
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user