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]>
325 lines
12 KiB
Dart
325 lines
12 KiB
Dart
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,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
});
|
||
}
|
||
}
|