feat: federnde Übergänge und Animationen, Entwürfe je Raum

- Handy: Chat gleitet federnd über die Chatliste (Liste bleibt aufgebaut,
  Scrollposition erhalten), beim Zurück wieder hinaus; Liste schon während
  des Hinausgleitens bedienbar. Mitgliederleiste gleitet herein.
- Neue/gesendete Nachrichten ploppen weich auf (nur live, nicht beim Öffnen
  oder Nachladen), Reaktionen ploppen auf und hüpfen beim Hochzählen,
  Ungelesen-Zähler hüpft, Scroll-nach-unten-Knopf federt (Handy größer),
  Antwort-Leiste klappt federnd auf/zu, Senden-Pfeil ploppt, Vollansicht
  zoomt auf. Alles skaliert mit Einstellung „Animationen“ (0 % = aus).
- Fix: Beim Raumwechsel blieben Antwort-Ziel, Auswahl und getippter Text am
  neuen Raum hängen (Antwort auf Raum A konnte in Raum B rausgehen). Jetzt
  eigenes Eingabefeld je Raum mit Entwurf im Speicher.
- Antwort-Leiste: größeres Schließen-X auf dem Handy.
- Tests: test/spring_motion_test.dart, UI-Durchlauf mit Übergängen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Bernd Steckmeister
2026-10-07 22:07:17 +02:00
co-authored by Claude Opus 5.5
parent 1d0ccfde8b
commit ae6f651f4c
14 changed files with 704 additions and 41 deletions
+34 -13
View File
@@ -17,6 +17,8 @@ import 'package:pyramid/features/chat/chat_view.dart';
import 'package:pyramid/features/members/members_panel.dart';
import 'package:pyramid/features/rooms/rooms_panel.dart';
import 'package:pyramid/features/spaces/spaces_rail.dart';
import 'package:pyramid/layout/mobile_page_stack.dart';
import 'package:pyramid/widgets/spring_press.dart' show motionDuration;
import 'package:pyramid/widgets/search_modal.dart';
import 'package:pyramid/widgets/settings_modal.dart';
import 'package:pyramid/widgets/share_target_dialog.dart';
@@ -335,14 +337,18 @@ class _MobileShell extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final voip = ref.watch(callSignalingProvider);
Widget body;
if (viewMode == ViewMode.voice) {
body = const VoiceChannelView();
} else if (activeRoomId != null) {
body = ChatView(roomId: activeRoomId!);
} else {
body = const RoomsPanel();
}
// Die Chatliste bleibt aufgebaut; Chat bzw. Sprachkanal gleiten federnd
// darüber (Bernds Wunsch 2026-10-07: mehr Übergänge/Elastizität).
final Widget? front = viewMode == ViewMode.voice
? const VoiceChannelView()
: activeRoomId != null
? ChatView(roomId: activeRoomId!)
: null;
final body = MobilePageStack(
base: const RoomsPanel(),
front: front,
background: pt.bg1,
);
// Symbole der Statusleiste passend zum Theme (hell auf dunkel und umgekehrt).
final overlayStyle = (pt.isDark
@@ -387,7 +393,12 @@ class _MobileShell extends ConsumerWidget {
Positioned.fill(
child: GestureDetector(
onTap: () => ref.read(membersPanelOpenProvider.notifier).state = false,
child: Container(color: Colors.black.withAlpha(110)),
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: 1),
duration: motionDuration(context, 220),
builder: (_, v, _) =>
ColoredBox(color: Colors.black.withAlpha((110 * v).round())),
),
),
),
Positioned(
@@ -396,10 +407,20 @@ class _MobileShell extends ConsumerWidget {
bottom: 0,
child: Builder(builder: (ctx) {
final w = MediaQuery.sizeOf(ctx).width;
return MembersPanel(
roomId: activeRoomId!,
width: (w * 0.82).clamp(240.0, 320.0),
onClose: () => ref.read(membersPanelOpenProvider.notifier).state = false,
// Gleitet von rechts herein.
return TweenAnimationBuilder<double>(
tween: Tween(begin: 1, end: 0),
duration: motionDuration(ctx, 340),
curve: Curves.easeOutCubic,
builder: (_, v, child) => FractionalTranslation(
translation: Offset(v, 0),
child: child,
),
child: MembersPanel(
roomId: activeRoomId!,
width: (w * 0.82).clamp(240.0, 320.0),
onClose: () => ref.read(membersPanelOpenProvider.notifier).state = false,
),
);
}),
),