6128953e28
Vorbereitung fuer Schnitt-Doc Schritt 3 (verification-Modul), ohne den Verschluesselungs-heiligen Code anzufassen: megolm_crypto (Export/Import- Krypto-Helfer), encryption_section (Enums + _EncryptionSection), recovery_key_dialogs (_ChangeRecoveryKeyDialog/_ShowKeyBody/_ConfirmKeyBody), fingerprint_passphrase (_FingerprintGroup/_PassphraseDialog). Rekonstruktion zeichenidentisch (1468 Code-Zeilen, ordinaler Vergleich), analyze sauber, 29 Tests gruen, App-Start ok (auth_log 17:43:14 loggedIn). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
273 lines
11 KiB
Dart
273 lines
11 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
|
|
import 'package:crypto/crypto.dart' as mcrypto;
|
|
import 'package:pyramid/core/fcm_push_service.dart';
|
|
import 'package:pyramid/core/notification_service.dart' show openBatteryOptimizationSettings;
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/foundation.dart' show compute;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:matrix/encryption/ssss.dart';
|
|
import 'package:matrix/encryption/utils/bootstrap.dart';
|
|
import 'package:matrix/encryption/utils/key_verification.dart' show KeyVerification, KeyVerificationEmoji, KeyVerificationState;
|
|
import 'package:matrix/matrix.dart';
|
|
import 'package:pretty_qr_code/pretty_qr_code.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:pointycastle/export.dart' as pc;
|
|
import 'package:pyramid/core/app_state.dart';
|
|
import 'package:pyramid/core/auth_log.dart';
|
|
import 'package:pyramid/core/e2ee_diagnostics.dart';
|
|
import 'package:pyramid/features/auth/bootstrap_dialog.dart' show isKeyBackupMissing;
|
|
import 'package:pyramid/features/call_signaling/call_signaling_service.dart';
|
|
import 'package:pyramid/features/voice_channel/voice_channel_service.dart';
|
|
import 'package:pyramid/core/matrix_client.dart';
|
|
import 'package:pyramid/core/settings_prefs.dart';
|
|
import 'package:pyramid/core/theme.dart';
|
|
import 'package:pyramid/core/update_checker.dart';
|
|
import 'package:pyramid/widgets/update_download_dialog.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:pyramid/widgets/banner_crop_dialog.dart';
|
|
import 'package:pyramid/widgets/hover_region.dart';
|
|
import 'package:pyramid/widgets/mxc_image.dart';
|
|
import 'package:pyramid/widgets/pyramid_loader.dart';
|
|
|
|
part 'settings/settings_shared.dart';
|
|
part 'settings/settings_profile.dart';
|
|
part 'settings/settings_notifications.dart';
|
|
part 'settings/settings_appearance.dart';
|
|
part 'settings/settings_voice.dart';
|
|
part 'settings/settings_keybinds.dart';
|
|
part 'settings/settings_privacy.dart';
|
|
part 'settings/settings_sessions.dart';
|
|
part 'settings/settings_verification.dart';
|
|
part 'settings/encryption/megolm_crypto.dart';
|
|
part 'settings/encryption/encryption_section.dart';
|
|
part 'settings/encryption/recovery_key_dialogs.dart';
|
|
part 'settings/encryption/fingerprint_passphrase.dart';
|
|
part 'settings/settings_account.dart';
|
|
part 'settings/settings_about.dart';
|
|
|
|
class SettingsModal extends ConsumerStatefulWidget {
|
|
final VoidCallback onClose;
|
|
const SettingsModal({super.key, required this.onClose});
|
|
|
|
@override
|
|
ConsumerState<SettingsModal> createState() => _SettingsModalState();
|
|
}
|
|
|
|
class _SettingsModalState extends ConsumerState<SettingsModal> {
|
|
// null = show the category list (mobile). On desktop it falls back to the
|
|
// first section so content is always visible.
|
|
String? _section;
|
|
|
|
// Categories shared by the desktop sidebar and the mobile list/detail.
|
|
static const List<(String, String, IconData)> _categories = [
|
|
('profile', 'Mein Profil', Icons.person_outline_rounded),
|
|
('notifications', 'Benachrichtigungen', Icons.notifications_none_rounded),
|
|
('appearance', 'Erscheinungsbild', Icons.palette_outlined),
|
|
('voice', 'Sprache & Video', Icons.headphones_rounded),
|
|
('keys', 'Tastaturkürzel', Icons.keyboard_rounded),
|
|
('privacy', 'Datenschutz', Icons.lock_outline_rounded),
|
|
('sessions', 'Sitzungen', Icons.devices_rounded),
|
|
('encryption', 'Verschlüsselung', Icons.shield_outlined),
|
|
('about', 'Über Pyramid', Icons.info_outline_rounded),
|
|
];
|
|
|
|
String _titleFor(String id) =>
|
|
_categories.firstWhere((c) => c.$1 == id, orElse: () => (id, id, Icons.settings)).$2;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final pt = PyramidTheme.of(context);
|
|
final isDark = ref.watch(themeModeProvider);
|
|
final accentIdx = ref.watch(accentProvider);
|
|
final radius = ref.watch(radiusProvider);
|
|
final density = ref.watch(densityProvider);
|
|
final motion = ref.watch(motionProvider);
|
|
|
|
final size = MediaQuery.sizeOf(context);
|
|
final isMobile = size.width < 700;
|
|
|
|
return GestureDetector(
|
|
onTap: widget.onClose,
|
|
child: Container(
|
|
color: Colors.black.withAlpha(150),
|
|
child: Center(
|
|
child: GestureDetector(
|
|
onTap: () {},
|
|
child: Container(
|
|
constraints: const BoxConstraints(maxWidth: 820, maxHeight: 660),
|
|
width: isMobile ? size.width * 0.97 : 740,
|
|
height: isMobile ? size.height * 0.92 : 560,
|
|
decoration: BoxDecoration(
|
|
color: pt.bg1,
|
|
borderRadius: BorderRadius.circular(pt.rXl),
|
|
border: Border.all(color: pt.border),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withAlpha(130),
|
|
blurRadius: 40,
|
|
offset: const Offset(0, 12),
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(pt.rXl),
|
|
child: isMobile
|
|
? _mobileLayout(pt, isDark, accentIdx, radius, density, motion)
|
|
: _desktopLayout(pt, isDark, accentIdx, radius, density, motion),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Desktop: sidebar + content side by side ──────────────────────────────
|
|
Widget _desktopLayout(PyramidTheme pt, bool isDark, int accentIdx,
|
|
double radius, double density, double motion) {
|
|
final active = _section ?? 'appearance';
|
|
return Row(
|
|
children: [
|
|
Container(
|
|
width: 200,
|
|
decoration: BoxDecoration(
|
|
color: pt.bg2,
|
|
border: Border(right: BorderSide(color: pt.border)),
|
|
),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 16),
|
|
_NavSection('Account', pt),
|
|
_NavItem('profile', 'Mein Profil', Icons.person_outline_rounded, active, pt, () => setState(() => _section = 'profile')),
|
|
_NavItem('notifications', 'Benachrichtigungen', Icons.notifications_none_rounded, active, pt, () => setState(() => _section = 'notifications')),
|
|
const SizedBox(height: 14),
|
|
_NavSection('App', pt),
|
|
_NavItem('appearance', 'Erscheinungsbild', Icons.palette_outlined, active, pt, () => setState(() => _section = 'appearance')),
|
|
_NavItem('voice', 'Sprache & Video', Icons.headphones_rounded, active, pt, () => setState(() => _section = 'voice')),
|
|
_NavItem('keys', 'Tastaturkürzel', Icons.keyboard_rounded, active, pt, () => setState(() => _section = 'keys')),
|
|
const SizedBox(height: 14),
|
|
_NavSection('Sicherheit', pt),
|
|
_NavItem('privacy', 'Datenschutz', Icons.lock_outline_rounded, active, pt, () => setState(() => _section = 'privacy')),
|
|
_NavItem('sessions', 'Sitzungen', Icons.devices_rounded, active, pt, () => setState(() => _section = 'sessions')),
|
|
_NavItem('encryption', 'Verschlüsselung', Icons.shield_outlined, active, pt, () => setState(() => _section = 'encryption')),
|
|
const SizedBox(height: 14),
|
|
_NavItem('about', 'Über Pyramid', Icons.info_outline_rounded, active, pt, () => setState(() => _section = 'about')),
|
|
const Divider(height: 24, indent: 12, endIndent: 12),
|
|
_LogoutNavButton(pt: pt, isMobile: false, onClose: widget.onClose),
|
|
const SizedBox(height: 16),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Stack(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(28),
|
|
child: _buildContent(pt, isDark, accentIdx, radius, density, motion, active),
|
|
),
|
|
Positioned(
|
|
top: 8,
|
|
right: 8,
|
|
child: PyrIconBtn(
|
|
icon: const Icon(Icons.close_rounded, size: 20),
|
|
tooltip: 'Schließen',
|
|
onPressed: widget.onClose,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ── Mobile: full-width category list → detail with a back arrow ───────────
|
|
Widget _mobileLayout(PyramidTheme pt, bool isDark, int accentIdx,
|
|
double radius, double density, double motion) {
|
|
final section = _section;
|
|
if (section == null) {
|
|
// Category list
|
|
return Column(
|
|
children: [
|
|
_MobileHeader(title: 'Einstellungen', pt: pt, onClose: widget.onClose),
|
|
Expanded(
|
|
child: ListView(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
children: [
|
|
for (final c in _categories)
|
|
_MobileCategoryTile(
|
|
label: c.$2,
|
|
icon: c.$3,
|
|
pt: pt,
|
|
onTap: () => setState(() => _section = c.$1),
|
|
),
|
|
const Divider(height: 24, indent: 16, endIndent: 16),
|
|
_LogoutNavButton(pt: pt, isMobile: false, onClose: widget.onClose),
|
|
const SizedBox(height: 16),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
// Detail view with a back arrow
|
|
return Column(
|
|
children: [
|
|
_MobileHeader(
|
|
title: _titleFor(section),
|
|
pt: pt,
|
|
onBack: () => setState(() => _section = null),
|
|
onClose: widget.onClose,
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
|
child: _buildContent(pt, isDark, accentIdx, radius, density, motion, section),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildContent(PyramidTheme pt, bool isDark, int accentIdx,
|
|
double radius, double density, double motion, String section) {
|
|
return switch (section) {
|
|
'appearance' => _AppearanceSection(
|
|
pt: pt,
|
|
isDark: isDark,
|
|
accentIdx: accentIdx,
|
|
radius: radius,
|
|
density: density,
|
|
motion: motion,
|
|
ref: ref),
|
|
'profile' => _ProfileSection(pt: pt),
|
|
'notifications' => _NotificationsSection(pt: pt),
|
|
'voice' => _VoiceSection(pt: pt),
|
|
'keys' => _KeysSection(pt: pt),
|
|
'privacy' => _PrivacySection(pt: pt),
|
|
'sessions' => _SessionsSection(pt: pt),
|
|
'encryption' => _EncryptionSection(pt: pt, ref: ref),
|
|
'about' => _AboutSection(pt: pt),
|
|
_ => _AppearanceSection(
|
|
pt: pt,
|
|
isDark: isDark,
|
|
accentIdx: accentIdx,
|
|
radius: radius,
|
|
density: density,
|
|
motion: motion,
|
|
ref: ref),
|
|
};
|
|
}
|
|
}
|