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]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a1d64ec813
commit
c5f10901e6
+24
-6
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:pyramid/core/app_state.dart';
|
||||
import 'package:pyramid/core/appearance.dart';
|
||||
import 'package:pyramid/core/notification_service.dart' show checkPendingReply, setAppForeground;
|
||||
import 'package:pyramid/core/router.dart';
|
||||
import 'package:pyramid/core/settings_prefs.dart';
|
||||
@@ -52,7 +53,9 @@ class _PyramidAppState extends ConsumerState<PyramidApp>
|
||||
Future<void> _loadPersistedPrefs() async {
|
||||
final saved = await loadThemePrefs();
|
||||
if (!mounted) return;
|
||||
ref.read(themeModeProvider.notifier).state = saved.isDark;
|
||||
ref.read(themeVariantProvider.notifier).state = saved.variant;
|
||||
ref.read(textScaleProvider.notifier).state = saved.textScale;
|
||||
ref.read(messageStyleProvider.notifier).state = saved.messageStyle;
|
||||
ref.read(accentProvider.notifier).state = saved.accentIdx;
|
||||
ref.read(radiusProvider.notifier).state = saved.radius;
|
||||
ref.read(motionProvider.notifier).state = saved.motion;
|
||||
@@ -75,7 +78,8 @@ class _PyramidAppState extends ConsumerState<PyramidApp>
|
||||
}
|
||||
|
||||
final router = ref.watch(routerProvider);
|
||||
final isDark = ref.watch(themeModeProvider);
|
||||
final variant = ref.watch(themeVariantProvider);
|
||||
final textScale = ref.watch(textScaleProvider);
|
||||
final accentIdx = ref.watch(accentProvider);
|
||||
final radius = ref.watch(radiusProvider);
|
||||
final density = ref.watch(densityProvider);
|
||||
@@ -83,17 +87,31 @@ class _PyramidAppState extends ConsumerState<PyramidApp>
|
||||
final accent = accentPresets[accentIdx].color;
|
||||
|
||||
return PyramidTheme(
|
||||
isDark: isDark,
|
||||
variant: variant,
|
||||
accent: accent,
|
||||
radius: radius,
|
||||
density: density,
|
||||
motion: motion,
|
||||
child: MaterialApp.router(
|
||||
title: 'Pyramid',
|
||||
theme: buildPyramidThemeData(false),
|
||||
darkTheme: buildPyramidThemeData(true),
|
||||
themeMode: isDark ? ThemeMode.dark : ThemeMode.light,
|
||||
theme: buildPyramidThemeData(ThemeVariant.light),
|
||||
darkTheme: buildPyramidThemeData(
|
||||
variant.isDark ? variant : ThemeVariant.soft),
|
||||
themeMode: variant.isDark ? ThemeMode.dark : ThemeMode.light,
|
||||
routerConfig: router,
|
||||
// Schriftgröße: Pyramid-Regler × Handy-/Systemeinstellung, für alle
|
||||
// Seiten und Dialoge (liegt über dem Navigator).
|
||||
builder: (context, child) {
|
||||
final mq = MediaQuery.of(context);
|
||||
final factor = textScale ??
|
||||
defaultTextScale(mobile: isMobileLayout(mq.size));
|
||||
return MediaQuery(
|
||||
data: mq.copyWith(
|
||||
textScaler: PyramidTextScaler(mq.textScaler, factor),
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:pyramid/core/appearance.dart';
|
||||
import 'package:pyramid/core/settings_prefs.dart';
|
||||
import 'package:pyramid/core/theme.dart';
|
||||
import 'package:pyramid/core/matrix_client.dart';
|
||||
@@ -7,7 +8,14 @@ import 'package:pyramid/features/rooms/rooms_provider.dart';
|
||||
|
||||
// ─── Theme state ──────────────────────────────────────────────────────────────
|
||||
|
||||
final themeModeProvider = StateProvider<bool>((ref) => true); // true = dark
|
||||
final themeVariantProvider =
|
||||
StateProvider<ThemeVariant>((ref) => ThemeVariant.soft);
|
||||
|
||||
/// Pyramid-Schriftfaktor; `null` = automatisch (Handy größer, PC 1,0).
|
||||
final textScaleProvider = StateProvider<double?>((ref) => null);
|
||||
|
||||
final messageStyleProvider =
|
||||
StateProvider<MessageStyle>((ref) => MessageStyle.auto);
|
||||
final accentProvider = StateProvider<int>((ref) => 0); // index into accent presets
|
||||
|
||||
const accentPresets = [
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Darstellungs-Einstellungen, die nicht reine Farben sind (die stehen in
|
||||
/// theme.dart): Handy-Layout-Erkennung, Schriftgröße, Nachrichten-Darstellung.
|
||||
|
||||
/// Unterhalb dieser Breite (oder sehr flach, z. B. Handy quer) zeigt Pyramid
|
||||
/// das Handy-Layout (eine Spalte, Chat als eigene Seite).
|
||||
bool isMobileLayout(Size size) => size.width < 900 || size.height < 500;
|
||||
|
||||
// ── Schriftgröße ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Standard-Schriftfaktor, solange der Nutzer nichts eingestellt hat: auf dem
|
||||
/// Handy größer (Bernds Wunsch 2026-10-07: ≈ 16 statt 14 px), am PC wie bisher.
|
||||
double defaultTextScale({required bool mobile}) => mobile ? 1.15 : 1.0;
|
||||
|
||||
const minTextScale = 0.85;
|
||||
const maxTextScale = 1.5;
|
||||
|
||||
/// Kombiniert die Schriftgröße des Systems (Handy-Einstellung) mit dem
|
||||
/// Pyramid-eigenen Regler: erst Pyramid-Faktor, dann die (ggf. nichtlineare)
|
||||
/// Systemskalierung.
|
||||
class PyramidTextScaler extends TextScaler {
|
||||
const PyramidTextScaler(this.system, this.factor);
|
||||
|
||||
final TextScaler system;
|
||||
final double factor;
|
||||
|
||||
@override
|
||||
double scale(double fontSize) => system.scale(fontSize * factor);
|
||||
|
||||
@override
|
||||
// ignore: deprecated_member_use
|
||||
double get textScaleFactor => system.textScaleFactor * factor;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is PyramidTextScaler &&
|
||||
other.system == system &&
|
||||
other.factor == factor;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(system, factor);
|
||||
}
|
||||
|
||||
// ── Nachrichten-Darstellung ────────────────────────────────────────────────
|
||||
|
||||
/// Wie Nachrichten im Chat aussehen (Einstellungen → Erscheinungsbild).
|
||||
enum MessageStyle {
|
||||
/// Handy: Sprechblasen, PC: Liste.
|
||||
auto,
|
||||
|
||||
/// Sprechblasen wie WhatsApp/Signal (eigene rechts, andere links).
|
||||
bubbles,
|
||||
|
||||
/// Liste wie Discord/Element (Name + Text untereinander).
|
||||
list;
|
||||
|
||||
String get label => switch (this) {
|
||||
MessageStyle.auto => 'Automatisch',
|
||||
MessageStyle.bubbles => 'Sprechblasen',
|
||||
MessageStyle.list => 'Liste',
|
||||
};
|
||||
|
||||
/// Tatsächlich anzuwendende Darstellung.
|
||||
bool useBubbles({required bool mobile}) => switch (this) {
|
||||
MessageStyle.auto => mobile,
|
||||
MessageStyle.bubbles => true,
|
||||
MessageStyle.list => false,
|
||||
};
|
||||
|
||||
static MessageStyle fromName(String? name) => MessageStyle.values
|
||||
.firstWhere((v) => v.name == name, orElse: () => MessageStyle.auto);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:pyramid/core/appearance.dart';
|
||||
import 'package:pyramid/core/theme.dart';
|
||||
|
||||
// ── Generic SharedPreferences-backed bool notifier ──────────────────────────
|
||||
|
||||
@@ -143,26 +145,58 @@ class VoicePrefs {
|
||||
// ── Theme persistence helpers ────────────────────────────────────────────────
|
||||
|
||||
Future<void> saveThemePrefs({
|
||||
bool? isDark,
|
||||
ThemeVariant? variant,
|
||||
int? accentIdx,
|
||||
double? radius,
|
||||
double? motion,
|
||||
double? textScale,
|
||||
bool resetTextScale = false,
|
||||
MessageStyle? messageStyle,
|
||||
}) async {
|
||||
final p = await SharedPreferences.getInstance();
|
||||
if (isDark != null) await p.setBool('theme_dark', isDark);
|
||||
if (variant != null) {
|
||||
await p.setString('theme_variant', variant.name);
|
||||
// Altes Feld mitpflegen (falls jemand auf eine ältere Version zurückgeht).
|
||||
await p.setBool('theme_dark', variant.isDark);
|
||||
}
|
||||
if (accentIdx != null) await p.setInt('theme_accent', accentIdx);
|
||||
if (radius != null) await p.setDouble('theme_radius', radius);
|
||||
if (motion != null) await p.setDouble('theme_motion', motion);
|
||||
if (resetTextScale) {
|
||||
await p.remove('ui_text_scale');
|
||||
} else if (textScale != null) {
|
||||
await p.setDouble('ui_text_scale', textScale);
|
||||
}
|
||||
if (messageStyle != null) {
|
||||
await p.setString('chat_message_style', messageStyle.name);
|
||||
}
|
||||
}
|
||||
|
||||
Future<({bool isDark, int accentIdx, double radius, double motion})>
|
||||
loadThemePrefs() async {
|
||||
Future<
|
||||
({
|
||||
ThemeVariant variant,
|
||||
int accentIdx,
|
||||
double radius,
|
||||
double motion,
|
||||
double? textScale,
|
||||
MessageStyle messageStyle,
|
||||
})> loadThemePrefs() async {
|
||||
final p = await SharedPreferences.getInstance();
|
||||
// Vor 2026-10-07 gab es nur hell/dunkel ('theme_dark'). Wer dunkel hatte,
|
||||
// bekommt jetzt das neue „Weiche Dunkel“ (Bernds Entscheidung).
|
||||
final storedVariant = p.getString('theme_variant');
|
||||
final variant = storedVariant != null
|
||||
? ThemeVariant.fromName(storedVariant)
|
||||
: (p.getBool('theme_dark') ?? true)
|
||||
? ThemeVariant.soft
|
||||
: ThemeVariant.light;
|
||||
return (
|
||||
isDark: p.getBool('theme_dark') ?? true,
|
||||
variant: variant,
|
||||
accentIdx: p.getInt('theme_accent') ?? 0,
|
||||
radius: p.getDouble('theme_radius') ?? 12.0,
|
||||
motion: p.getDouble('theme_motion') ?? 1.0,
|
||||
textScale: p.getDouble('ui_text_scale'),
|
||||
messageStyle: MessageStyle.fromName(p.getString('chat_message_style')),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+123
-21
@@ -1,6 +1,103 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// Pyramid design tokens — matching mockup CSS variables exactly
|
||||
/// 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._();
|
||||
|
||||
@@ -49,7 +146,7 @@ class PyramidColors {
|
||||
}
|
||||
|
||||
class PyramidTheme extends InheritedWidget {
|
||||
final bool isDark;
|
||||
final ThemeVariant variant;
|
||||
final Color accent;
|
||||
final double radius;
|
||||
final double density;
|
||||
@@ -58,7 +155,7 @@ class PyramidTheme extends InheritedWidget {
|
||||
const PyramidTheme({
|
||||
super.key,
|
||||
required super.child,
|
||||
this.isDark = true,
|
||||
this.variant = ThemeVariant.soft,
|
||||
this.accent = PyramidColors.accentAmber,
|
||||
this.radius = 12,
|
||||
this.density = 1,
|
||||
@@ -69,17 +166,20 @@ class PyramidTheme extends InheritedWidget {
|
||||
return context.dependOnInheritedWidgetOfExactType<PyramidTheme>()!;
|
||||
}
|
||||
|
||||
Color get bg0 => isDark ? PyramidColors.bg0 : PyramidColors.bg0Light;
|
||||
Color get bg1 => isDark ? PyramidColors.bg1 : PyramidColors.bg1Light;
|
||||
Color get bg2 => isDark ? PyramidColors.bg2 : PyramidColors.bg2Light;
|
||||
Color get bg3 => isDark ? PyramidColors.bg3 : PyramidColors.bg3Light;
|
||||
Color get bgHover => isDark ? PyramidColors.bgHover : PyramidColors.bgHoverLight;
|
||||
Color get bgActive => isDark ? PyramidColors.bgActive : PyramidColors.bgActiveLight;
|
||||
Color get border => isDark ? PyramidColors.border : PyramidColors.borderLight;
|
||||
Color get borderStrong => isDark ? PyramidColors.borderStrong : PyramidColors.borderStrongLight;
|
||||
Color get fg => isDark ? PyramidColors.fg : PyramidColors.fgLight;
|
||||
Color get fgMuted => isDark ? PyramidColors.fgMuted : PyramidColors.fgMutedLight;
|
||||
Color get fgDim => isDark ? PyramidColors.fgDim : PyramidColors.fgDimLight;
|
||||
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;
|
||||
@@ -104,7 +204,7 @@ class PyramidTheme extends InheritedWidget {
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(PyramidTheme oldWidget) =>
|
||||
isDark != oldWidget.isDark ||
|
||||
variant != oldWidget.variant ||
|
||||
accent != oldWidget.accent ||
|
||||
radius != oldWidget.radius ||
|
||||
density != oldWidget.density ||
|
||||
@@ -114,9 +214,11 @@ class PyramidTheme extends InheritedWidget {
|
||||
// Inter ist als Asset gebündelt (pubspec `fonts:`) — kein Laufzeit-Download.
|
||||
TextStyle _base(Color color) => TextStyle(fontFamily: 'Inter', color: color);
|
||||
|
||||
ThemeData buildPyramidThemeData(bool isDark) {
|
||||
final fg = isDark ? PyramidColors.fg : PyramidColors.fgLight;
|
||||
final bg1 = isDark ? PyramidColors.bg1 : PyramidColors.bg1Light;
|
||||
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(
|
||||
@@ -137,11 +239,11 @@ ThemeData buildPyramidThemeData(bool isDark) {
|
||||
),
|
||||
textTheme: TextTheme(
|
||||
bodyMedium: _base(fg).copyWith(fontSize: 14),
|
||||
bodySmall: _base(fg).copyWith(fontSize: 12, color: isDark ? PyramidColors.fgMuted : PyramidColors.fgMutedLight),
|
||||
labelSmall: _base(fg).copyWith(fontSize: 11, color: isDark ? PyramidColors.fgDim : PyramidColors.fgDimLight),
|
||||
bodySmall: _base(fg).copyWith(fontSize: 12, color: palette.fgMuted),
|
||||
labelSmall: _base(fg).copyWith(fontSize: 11, color: palette.fgDim),
|
||||
),
|
||||
dividerTheme: DividerThemeData(
|
||||
color: isDark ? PyramidColors.border : PyramidColors.borderLight,
|
||||
color: palette.border,
|
||||
thickness: 1,
|
||||
space: 1,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user