Files
pyramid/lib/widgets/settings_modal.dart
T
Bernd SteckmeisterandClaude Opus 5.5 c5f10901e6 feat: UI-Umbau - weiches Dunkel, Schriftgroessen-Regler, Sprechblasen umschaltbar, Einstellungen als Vollbild
Bernds Entscheidungen 2026-10-07:
- theme.dart: ThemeVariant (Dunkel = neues weiches Dunkelgrau als Standard,
  Schwarz = bisheriges Fast-Schwarz, Hell) mit PyramidPalette; Nebentexte
  kontrastreicher. Alte Einstellung theme_dark wird uebernommen
  (dunkel -> weiches Dunkel). Fest verdrahtete Dunkelfarben im Profil-Popup
  und GIF-Picker folgen jetzt dem Farbmodus.
- appearance.dart: Schriftgroesse = Pyramid-Regler x Systemeinstellung
  (Handy-Standard 115 %, PC 100 %), MessageStyle Automatisch/Sprechblasen/
  Liste, gemeinsame isMobileLayout-Regel.
- message_bubbles.dart: Sprechblasen-Darstellung (eigene rechts in
  Akzentfarbe, Gruppen mit Name + Avatar, Bilder ohne Blase in voller Groesse);
  Listen-Darstellung unveraendert, Weiche in MessageGroup.
- Formatierte Nachrichten (u. a. Antworten) nutzten RichText ohne
  Schriftskalierung -> erschienen kleiner; jetzt Text.rich.
- Einstellungen auf dem Handy als ganze Seite, Schalter unter dem Text.
- UI-Durchlauf um Liste/Hell/Gruppe/Einstellungen erweitert; Tests fuer
  Theme-Varianten, Uebernahme alter Werte, Schriftfaktor, Darstellung.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 17:16:07 +02:00

287 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/appearance.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(themeVariantProvider).isDark;
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: () {},
// Handy: ganze Seite (wie System-Einstellungen) statt schwebender
// Karte mit abgedunkeltem Rand – mehr Platz, weniger Gequetsche.
child: Container(
constraints: isMobile
? null
: const BoxConstraints(maxWidth: 820, maxHeight: 660),
width: isMobile ? size.width : 740,
height: isMobile ? size.height : 560,
decoration: BoxDecoration(
color: pt.bg1,
borderRadius: BorderRadius.circular(isMobile ? 0 : pt.rXl),
border: isMobile ? null : Border.all(color: pt.border),
boxShadow: isMobile
? null
: [
BoxShadow(
color: Colors.black.withAlpha(130),
blurRadius: 40,
offset: const Offset(0, 12),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(isMobile ? 0 : 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) {
final variant = ref.watch(themeVariantProvider);
final textScale = ref.watch(textScaleProvider);
final messageStyle = ref.watch(messageStyleProvider);
return switch (section) {
'appearance' => _AppearanceSection(
pt: pt,
variant: variant,
textScale: textScale,
messageStyle: messageStyle,
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,
variant: variant,
textScale: textScale,
messageStyle: messageStyle,
accentIdx: accentIdx,
radius: radius,
density: density,
motion: motion,
ref: ref),
};
}
}