Files
pyramid/lib/widgets/settings/settings_appearance.dart
T
Bernd SteckmeisterandClaude Opus 5.5 391ebf0ffa feat(ui): untere Leiste (Chats/Server/Du) als Option, Server-Uebersicht, Du-Tab, Feinschliff
- Einstellung 'Navigation: Seitenmenue | Untere Leiste' (Handy, Standard
  bleibt Seitenmenue): Tabs Chats/Server/Du, Server-Uebersicht als Karten,
  Du-Tab mit Profil + Einstellungen, Zurueck-Taste ueber die Tabs
- CallDock als eigenes Widget (Anrufleiste auch ueber der Tab-Leiste)
- Sprachkanaele am Handy als Kachel mit 'wer ist drin'
- Suchfeld mit Lupe, 'Kanaele durchsuchen' im Server, Filter-Chips am PC
  umbrechend, '#' nur bei Server-Kanaelen, englische Reste entfernt
- Anrufansicht merkt sich Bubbles/Kacheln, Bubbles mit leichtem Glanz
- UI-Tour: Gruppenanruf (echter zweiter LiveKit-Teilnehmer), Bubbles mit
  5/7 Personen, untere Leiste; Version 0.1.42

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 14:22:02 +02:00

345 lines
13 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.
part of '../settings_modal.dart';
class _AppearanceSection extends StatelessWidget {
final PyramidTheme pt;
final ThemeVariant variant;
final double? textScale;
final MessageStyle messageStyle;
final int accentIdx;
final double radius;
final double density;
final double motion;
final WidgetRef ref;
const _AppearanceSection({
required this.pt,
required this.variant,
required this.textScale,
required this.messageStyle,
required this.accentIdx,
required this.radius,
required this.density,
required this.motion,
required this.ref,
});
@override
Widget build(BuildContext context) {
final mobile = isMobileLayout(MediaQuery.sizeOf(context));
final autoScale = defaultTextScale(mobile: mobile);
final effectiveScale = textScale ?? autoScale;
final variants = ThemeVariant.values;
final styles = MessageStyle.values;
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SectionTitle(
title: 'Erscheinungsbild',
subtitle: 'Gestalte Pyramid nach deinem Geschmack.',
pt: pt),
_SettingsGroup(title: 'Theme', pt: pt, children: [
_AdaptiveSettingsRow(
title: 'Farbmodus',
desc: 'Dunkel = weiches Dunkelgrau, Schwarz = wie früher (spart '
'auf OLED-Displays Akku)',
pt: pt,
control: _SegControl(
options: [for (final v in variants) v.label],
selected: variants.indexOf(variant),
pt: pt,
onSelect: (i) {
ref.read(themeVariantProvider.notifier).state = variants[i];
saveThemePrefs(variant: variants[i]);
},
),
),
_Divider(pt: pt),
_SettingsRow(
title: 'Akzentfarbe',
desc: 'Farbe für Hervorhebungen und Buttons',
pt: pt,
control: Row(
children: accentPresets.asMap().entries.map((e) {
final i = e.key;
final preset = e.value;
final isActive = accentIdx == i;
return GestureDetector(
onTap: () {
ref.read(accentProvider.notifier).state = i;
saveThemePrefs(accentIdx: i);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 22,
height: 22,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
color: preset.color,
shape: BoxShape.circle,
border: isActive ? Border.all(color: Colors.white, width: 2) : null,
boxShadow: isActive
? [BoxShadow(color: preset.color.withAlpha(100), blurRadius: 8)]
: null,
),
),
);
}).toList(),
),
),
]),
const SizedBox(height: 20),
_SettingsGroup(title: 'Text & Chat', pt: pt, children: [
_AdaptiveSettingsRow(
title: 'Schriftgröße',
desc: textScale == null
? 'Automatisch (${(autoScale * 100).round()} %) – zusätzlich '
'zur Schriftgröße des Geräts'
: '${(effectiveScale * 100).round()} %',
pt: pt,
control: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: mobile ? 250 : 170,
child: SliderTheme(
data: SliderThemeData(
activeTrackColor: pt.accent,
thumbColor: pt.accent,
inactiveTrackColor: pt.border,
trackHeight: 3,
thumbShape:
const RoundSliderThumbShape(enabledThumbRadius: 7),
),
child: Slider(
value: effectiveScale.clamp(minTextScale, maxTextScale),
min: minTextScale,
max: maxTextScale,
divisions: ((maxTextScale - minTextScale) / 0.05).round(),
onChanged: (v) {
final rounded = (v * 20).round() / 20;
ref.read(textScaleProvider.notifier).state = rounded;
saveThemePrefs(textScale: rounded);
},
),
),
),
if (textScale != null)
TextButton(
onPressed: () {
ref.read(textScaleProvider.notifier).state = null;
saveThemePrefs(resetTextScale: true);
},
child: Text('Standard',
style: TextStyle(color: pt.accent, fontSize: 12)),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(14, 0, 14, 12),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: pt.bg3,
borderRadius: BorderRadius.circular(pt.rBase),
),
child: Text(
'Vorschau: So groß erscheinen Nachrichten im Chat.',
style: TextStyle(color: pt.fg, fontSize: 14),
),
),
),
_Divider(pt: pt),
_AdaptiveSettingsRow(
title: 'Nachrichten',
desc: 'Automatisch: Handy Sprechblasen, PC Liste',
pt: pt,
control: _SegControl(
options: [for (final m in styles) m.label],
selected: styles.indexOf(messageStyle),
pt: pt,
onSelect: (i) {
ref.read(messageStyleProvider.notifier).state = styles[i];
saveThemePrefs(messageStyle: styles[i]);
},
),
),
]),
const SizedBox(height: 20),
if (isMobileLayout(MediaQuery.sizeOf(context))) ...[
_SettingsGroup(title: 'Navigation', pt: pt, children: [
Consumer(builder: (context, ref, _) {
final bottom = ref.watch(bottomNavProvider);
return _AdaptiveSettingsRow(
title: 'Hauptmenü',
desc: 'Seitenmenü (☰) oder Leiste unten mit Chats · Server · Du',
pt: pt,
control: _SegControl(
options: const ['Seitenmenü', 'Untere Leiste'],
selected: bottom ? 1 : 0,
pt: pt,
onSelect: (i) =>
ref.read(bottomNavProvider.notifier).set(i == 1),
),
);
}),
]),
const SizedBox(height: 20),
],
_SettingsGroup(title: 'Form & Bewegung', pt: pt, children: [
_SettingsRow(
title: 'Eckenrundung',
desc: '${radius.round()} px',
pt: pt,
control: SizedBox(
width: 150,
child: SliderTheme(
data: SliderThemeData(
activeTrackColor: pt.accent,
thumbColor: pt.accent,
inactiveTrackColor: pt.border,
trackHeight: 3,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
),
child: Slider(
value: radius,
min: 4,
max: 20,
onChanged: (v) {
ref.read(radiusProvider.notifier).state = v;
saveThemePrefs(radius: v);
},
),
),
),
),
_Divider(pt: pt),
_SettingsRow(
title: 'Animationen',
desc: '${(motion * 100).round()}%',
pt: pt,
control: SizedBox(
width: 150,
child: SliderTheme(
data: SliderThemeData(
activeTrackColor: pt.accent,
thumbColor: pt.accent,
inactiveTrackColor: pt.border,
trackHeight: 3,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
),
child: Slider(
value: motion,
min: 0,
max: 1,
onChanged: (v) {
ref.read(motionProvider.notifier).state = v;
saveThemePrefs(motion: v);
},
),
),
),
),
]),
const SizedBox(height: 20),
_SettingsGroup(title: 'Vorschau', pt: pt, children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: Column(
children: [
Container(
height: 100,
decoration: BoxDecoration(
color: pt.bg3,
borderRadius: BorderRadius.circular(pt.rBase),
border: Border.all(color: pt.border),
),
child: const Center(child: PyramidLoader(size: 70, useLottie: true)),
),
const SizedBox(height: 6),
Text('Lottie', style: TextStyle(color: pt.fgMuted, fontSize: 11)),
],
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
children: [
Container(
height: 100,
decoration: BoxDecoration(
color: pt.bg3,
borderRadius: BorderRadius.circular(pt.rBase),
border: Border.all(color: pt.border),
),
child: const Center(child: PyramidLoader(size: 70, useLottie: false)),
),
const SizedBox(height: 6),
Text('SVG', style: TextStyle(color: pt.fgMuted, fontSize: 11)),
],
),
),
],
),
),
]),
],
),
);
}
}
/// Wie [_SettingsRow], aber auf schmalen Bildschirmen steht das Bedienelement
/// UNTER dem Text statt daneben (sonst quetschen sich Schalter mit drei
/// Optionen auf dem Handy).
class _AdaptiveSettingsRow extends StatelessWidget {
final String title;
final String? desc;
final PyramidTheme pt;
final Widget control;
const _AdaptiveSettingsRow({
required this.title,
required this.pt,
required this.control,
this.desc,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
if (constraints.maxWidth >= 520) {
return _SettingsRow(title: title, desc: desc, pt: pt, control: control);
}
// Volle Breite: _SettingsGroup zentriert seine Kinder sonst.
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: TextStyle(
color: pt.fg, fontSize: 13, fontWeight: FontWeight.w500)),
if (desc != null) ...[
const SizedBox(height: 2),
Text(desc!, style: TextStyle(color: pt.fgDim, fontSize: 12)),
],
const SizedBox(height: 10),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: control,
),
],
),
);
});
}
}