Files
pyramid/lib/core/theme.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

254 lines
8.0 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 '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,
);
}