Files
pyramid/lib/widgets/settings/settings_appearance.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

325 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.
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),
_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,
),
],
),
);
});
}
}