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]>
254 lines
8.0 KiB
Dart
254 lines
8.0 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
/// Farbschema der App (Einstellungen → Erscheinungsbild → Farbmodus).
|
||
enum ThemeVariant {
|
||
/// „Weiches Dunkel“ – Dunkelgrau statt Fast-Schwarz, mehr Kontrast bei
|
||
/// Text. Standard seit 2026-10-07 (Bernds Entscheidung).
|
||
soft,
|
||
|
||
/// Das bisherige Fast-Schwarz (spart auf OLED-Displays etwas Akku).
|
||
black,
|
||
|
||
/// Hell.
|
||
light;
|
||
|
||
bool get isDark => this != ThemeVariant.light;
|
||
|
||
String get label => switch (this) {
|
||
ThemeVariant.soft => 'Dunkel',
|
||
ThemeVariant.black => 'Schwarz',
|
||
ThemeVariant.light => 'Hell',
|
||
};
|
||
|
||
static ThemeVariant fromName(String? name) =>
|
||
ThemeVariant.values.firstWhere((v) => v.name == name,
|
||
orElse: () => ThemeVariant.soft);
|
||
}
|
||
|
||
/// Die Flächen- und Textfarben eines [ThemeVariant].
|
||
class PyramidPalette {
|
||
const PyramidPalette({
|
||
required this.bg0,
|
||
required this.bg1,
|
||
required this.bg2,
|
||
required this.bg3,
|
||
required this.bgHover,
|
||
required this.bgActive,
|
||
required this.border,
|
||
required this.borderStrong,
|
||
required this.fg,
|
||
required this.fgMuted,
|
||
required this.fgDim,
|
||
});
|
||
|
||
final Color bg0, bg1, bg2, bg3, bgHover, bgActive, border, borderStrong;
|
||
final Color fg, fgMuted, fgDim;
|
||
|
||
/// Weiches Dunkel: alle Flächen ~10 % heller als „Schwarz“, Nebentexte
|
||
/// deutlich kontrastreicher (fgDim auf bg1 ≈ 5:1 statt ≈ 3,8:1).
|
||
static const soft = PyramidPalette(
|
||
bg0: Color(0xFF15161A),
|
||
bg1: Color(0xFF1B1C21),
|
||
bg2: Color(0xFF212228),
|
||
bg3: Color(0xFF282A31),
|
||
bgHover: Color(0xFF2F3139),
|
||
bgActive: Color(0xFF373945),
|
||
border: Color(0xFF2C2E36),
|
||
borderStrong: Color(0xFF3A3C46),
|
||
fg: Color(0xFFF2F2F5),
|
||
fgMuted: Color(0xFFB7B7C2),
|
||
fgDim: Color(0xFF8B8B99),
|
||
);
|
||
|
||
static const black = PyramidPalette(
|
||
bg0: PyramidColors.bg0,
|
||
bg1: PyramidColors.bg1,
|
||
bg2: PyramidColors.bg2,
|
||
bg3: PyramidColors.bg3,
|
||
bgHover: PyramidColors.bgHover,
|
||
bgActive: PyramidColors.bgActive,
|
||
border: PyramidColors.border,
|
||
borderStrong: PyramidColors.borderStrong,
|
||
fg: PyramidColors.fg,
|
||
fgMuted: PyramidColors.fgMuted,
|
||
fgDim: PyramidColors.fgDim,
|
||
);
|
||
|
||
static const light = PyramidPalette(
|
||
bg0: PyramidColors.bg0Light,
|
||
bg1: PyramidColors.bg1Light,
|
||
bg2: PyramidColors.bg2Light,
|
||
bg3: PyramidColors.bg3Light,
|
||
bgHover: PyramidColors.bgHoverLight,
|
||
bgActive: PyramidColors.bgActiveLight,
|
||
border: PyramidColors.borderLight,
|
||
borderStrong: PyramidColors.borderStrongLight,
|
||
fg: PyramidColors.fgLight,
|
||
fgMuted: PyramidColors.fgMutedLight,
|
||
fgDim: PyramidColors.fgDimLight,
|
||
);
|
||
|
||
static PyramidPalette of(ThemeVariant variant) => switch (variant) {
|
||
ThemeVariant.soft => soft,
|
||
ThemeVariant.black => black,
|
||
ThemeVariant.light => light,
|
||
};
|
||
}
|
||
|
||
// Pyramid design tokens — matching mockup CSS variables exactly.
|
||
// Die dunklen Werte hier sind die Variante „Schwarz“; Widgets sollten die
|
||
// Farben über PyramidTheme.of(context) holen (folgt dem gewählten Schema).
|
||
class PyramidColors {
|
||
const PyramidColors._();
|
||
|
||
// Dark theme
|
||
static const bg0 = Color(0xFF0A0A0C);
|
||
static const bg1 = Color(0xFF111114);
|
||
static const bg2 = Color(0xFF17171C);
|
||
static const bg3 = Color(0xFF1E1E25);
|
||
static const bgHover = Color(0xFF232330);
|
||
static const bgActive = Color(0xFF2A2A38);
|
||
static const border = Color(0xFF25252E);
|
||
static const borderStrong = Color(0xFF32323D);
|
||
static const fg = Color(0xFFECECF0);
|
||
static const fgMuted = Color(0xFFA4A4B0);
|
||
static const fgDim = Color(0xFF6F6F7D);
|
||
|
||
// Light theme
|
||
static const bg0Light = Color(0xFFF5F4F0);
|
||
static const bg1Light = Color(0xFFFFFFFF);
|
||
static const bg2Light = Color(0xFFFAF9F5);
|
||
static const bg3Light = Color(0xFFF0EEEA);
|
||
static const bgHoverLight = Color(0xFFE9E7E1);
|
||
static const bgActiveLight = Color(0xFFDFDCD3);
|
||
static const borderLight = Color(0xFFE5E2DC);
|
||
static const borderStrongLight = Color(0xFFD3CFC6);
|
||
static const fgLight = Color(0xFF1A1A1F);
|
||
static const fgMutedLight = Color(0xFF54545E);
|
||
static const fgDimLight = Color(0xFF8A8A92);
|
||
|
||
// Semantic colors (same in both themes)
|
||
static const danger = Color(0xFFE5484D);
|
||
static const success = Color(0xFF30A46C);
|
||
static const online = Color(0xFF4ADE80);
|
||
static const away = Color(0xFFFACC15);
|
||
static const busy = Color(0xFFF87171);
|
||
|
||
// Accent presets — default: amber hsl(42 95% 58%)
|
||
static const accentAmber = Color(0xFFF5A614);
|
||
static const accentCoral = Color(0xFFED6951);
|
||
static const accentMint = Color(0xFF3DB88A);
|
||
static const accentViolet = Color(0xFFB67EEA);
|
||
static const accentCobalt = Color(0xFF5BA3F5);
|
||
static const accentLime = Color(0xFF8EC63F);
|
||
|
||
static const accentFg = Color(0xFF0B0B0D);
|
||
}
|
||
|
||
class PyramidTheme extends InheritedWidget {
|
||
final ThemeVariant variant;
|
||
final Color accent;
|
||
final double radius;
|
||
final double density;
|
||
final double motion;
|
||
|
||
const PyramidTheme({
|
||
super.key,
|
||
required super.child,
|
||
this.variant = ThemeVariant.soft,
|
||
this.accent = PyramidColors.accentAmber,
|
||
this.radius = 12,
|
||
this.density = 1,
|
||
this.motion = 1,
|
||
});
|
||
|
||
static PyramidTheme of(BuildContext context) {
|
||
return context.dependOnInheritedWidgetOfExactType<PyramidTheme>()!;
|
||
}
|
||
|
||
bool get isDark => variant.isDark;
|
||
PyramidPalette get palette => PyramidPalette.of(variant);
|
||
|
||
Color get bg0 => palette.bg0;
|
||
Color get bg1 => palette.bg1;
|
||
Color get bg2 => palette.bg2;
|
||
Color get bg3 => palette.bg3;
|
||
Color get bgHover => palette.bgHover;
|
||
Color get bgActive => palette.bgActive;
|
||
Color get border => palette.border;
|
||
Color get borderStrong => palette.borderStrong;
|
||
Color get fg => palette.fg;
|
||
Color get fgMuted => palette.fgMuted;
|
||
Color get fgDim => palette.fgDim;
|
||
Color get danger => PyramidColors.danger;
|
||
Color get online => PyramidColors.online;
|
||
Color get away => PyramidColors.away;
|
||
Color get busy => PyramidColors.busy;
|
||
Color get accentSoft => accent.withAlpha(36); // ~14%
|
||
Color get accentGlow => accent.withAlpha(89); // ~35%
|
||
Color get accentFg => PyramidColors.accentFg;
|
||
|
||
double get rSm => radius * 0.6;
|
||
double get rBase => radius;
|
||
double get rLg => radius * 1.5;
|
||
double get rXl => radius * 2;
|
||
|
||
double get rowPadY => 10 * density;
|
||
double get rowPadX => 12 * density;
|
||
double get msgGap => 16 * density;
|
||
|
||
Duration get durationFast => const Duration(milliseconds: 150);
|
||
Duration get durationNormal => const Duration(milliseconds: 220);
|
||
Curve get springBounce => Curves.easeOutBack;
|
||
Curve get springSoft => Curves.easeOutCubic;
|
||
|
||
@override
|
||
bool updateShouldNotify(PyramidTheme oldWidget) =>
|
||
variant != oldWidget.variant ||
|
||
accent != oldWidget.accent ||
|
||
radius != oldWidget.radius ||
|
||
density != oldWidget.density ||
|
||
motion != oldWidget.motion;
|
||
}
|
||
|
||
// Inter ist als Asset gebündelt (pubspec `fonts:`) — kein Laufzeit-Download.
|
||
TextStyle _base(Color color) => TextStyle(fontFamily: 'Inter', color: color);
|
||
|
||
ThemeData buildPyramidThemeData(ThemeVariant variant) {
|
||
final palette = PyramidPalette.of(variant);
|
||
final isDark = variant.isDark;
|
||
final fg = palette.fg;
|
||
final bg1 = palette.bg1;
|
||
final accent = PyramidColors.accentAmber;
|
||
|
||
return ThemeData(
|
||
useMaterial3: true,
|
||
fontFamily: 'Inter',
|
||
brightness: isDark ? Brightness.dark : Brightness.light,
|
||
scaffoldBackgroundColor: bg1,
|
||
colorScheme: ColorScheme(
|
||
brightness: isDark ? Brightness.dark : Brightness.light,
|
||
primary: accent,
|
||
onPrimary: PyramidColors.accentFg,
|
||
secondary: accent,
|
||
onSecondary: PyramidColors.accentFg,
|
||
error: PyramidColors.danger,
|
||
onError: Colors.white,
|
||
surface: bg1,
|
||
onSurface: fg,
|
||
),
|
||
textTheme: TextTheme(
|
||
bodyMedium: _base(fg).copyWith(fontSize: 14),
|
||
bodySmall: _base(fg).copyWith(fontSize: 12, color: palette.fgMuted),
|
||
labelSmall: _base(fg).copyWith(fontSize: 11, color: palette.fgDim),
|
||
),
|
||
dividerTheme: DividerThemeData(
|
||
color: palette.border,
|
||
thickness: 1,
|
||
space: 1,
|
||
),
|
||
splashFactory: NoSplash.splashFactory,
|
||
highlightColor: Colors.transparent,
|
||
);
|
||
}
|