Files
pyramid/lib/widgets/settings_modal.dart
T
Bernd Steckmeister 5d772389d6 refactor: Gott-Datei settings_modal.dart in 12 Dateien aufgeteilt
Reine Verschiebung per Dart part/part-of (keine Umbenennung, keine
Logikänderung) - settings_modal.dart schrumpft von 5541 auf 270 Zeilen.
Verifiziert per automatisiertem Blockvergleich gegen das Original und
flutter analyze (Baseline unverändert: 1 bekannter Hinweis).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 10:56:48 +02:00

271 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/core/livekit_call_manager.dart';
import 'package:pyramid/core/matrix_client.dart';
import 'package:pyramid/core/voip_manager.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';
import 'package:shared_preferences/shared_preferences.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/settings_encryption.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),
};
}
}