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 createState() => _SettingsModalState(); } class _SettingsModalState extends ConsumerState { // 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), }; } }