feat: Handy - Platz fuer Statusleiste, randlose Bild-Vollansicht mit Gesten, groessere Medien im Chat

Bernds Beta-Feedback:
- main.dart: edgeToEdge statt immersiveSticky (Statusleiste schob sich ueber
  den Inhalt); MobileShell mit SafeArea + Statusleisten-Symbolen passend
  zum Theme, Hintergrund bg1 = Kopfzeilenfarbe.
- MediaViewer: Handy randlos auf Schwarz (vorher 24-px-Rand, halbtransparent);
  Tippen neben das Bild schliesst, aufs Bild blendet Leiste ein/aus; Wischen
  hoch/runter schliesst (nur ohne Zoom, ueber InteractiveViewer-Callbacks,
  kein Gestenkonflikt mit Zwei-Finger-Zoom); Doppeltipp zoomt an der Stelle.
- Chat: Bilder auf dem Handy bis halbe Bildschirmhoehe statt fest 320 px;
  Videos im echten Seitenverhaeltnis statt fest 16:9.
- UI-Durchlauf integration_test/mobile_ui_screens_test.dart (Handy-Format,
  Testprofil, Screenshots nach build/ui_screens/) + Testinhalte-Helfer
  test/live_ui_fixture_test.dart; scripts/test.ps1 -Ui.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Bernd Steckmeister
2026-10-07 16:52:10 +02:00
co-authored by Claude Opus 5.5
parent ef1d881967
commit a1d64ec813
11 changed files with 735 additions and 63 deletions
+32 -2
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'dart:typed_data';
@@ -419,6 +420,15 @@ class _ChunkyBarsPainter extends CustomPainter {
//
// Variant: minimal — Controls immer sichtbar (overlay auf Video, kein Auto-Hide)
/// Maximale Höhe für Bilder/Videos im Chat. Auf dem Handy bis zur halben
/// Bildschirmhöhe – vorher galten überall 320 px, wodurch Hochkant-Fotos auf
/// dem Handy nur ~180 px schmal waren (Bernds Feedback 2026-10-07).
double inlineMediaMaxHeight(BuildContext context) {
final screen = MediaQuery.sizeOf(context);
final isMobile = screen.width < 900 || screen.height < 500;
return isMobile ? (screen.height * 0.5).clamp(240.0, 560.0) : 320.0;
}
class PyramidVideoPlayer extends StatefulWidget {
final Uint8List bytes;
final String filename;
@@ -441,6 +451,10 @@ class _PyramidVideoPlayerState extends State<PyramidVideoPlayer> {
late final Player _player;
late final VideoController _controller;
bool _loading = true;
// Echtes Seitenverhältnis, sobald das Video seine Größe kennt (vorher fest
// 16:9 → Hochkant-Handyvideos klein zwischen schwarzen Balken).
double? _aspect;
final _subs = <StreamSubscription<int?>>[];
double _speed = 1.0;
String _quality = 'Auto';
@@ -452,9 +466,19 @@ class _PyramidVideoPlayerState extends State<PyramidVideoPlayer> {
super.initState();
_player = Player();
_controller = VideoController(_player);
_subs.add(_player.stream.width.listen((_) => _updateAspect()));
_subs.add(_player.stream.height.listen((_) => _updateAspect()));
_init();
}
void _updateAspect() {
final w = _player.state.width;
final h = _player.state.height;
if (w == null || h == null || w <= 0 || h <= 0) return;
final aspect = w / h;
if (mounted && aspect != _aspect) setState(() => _aspect = aspect);
}
Future<void> _init() async {
final ext = widget.filename.contains('.') ? widget.filename.split('.').last : 'mp4';
final dir = await getTemporaryDirectory();
@@ -466,6 +490,9 @@ class _PyramidVideoPlayerState extends State<PyramidVideoPlayer> {
@override
void dispose() {
for (final s in _subs) {
s.cancel();
}
_player.dispose();
super.dispose();
}
@@ -506,7 +533,10 @@ class _PyramidVideoPlayerState extends State<PyramidVideoPlayer> {
return Container(
margin: const EdgeInsets.only(top: 4, bottom: 4),
constraints: const BoxConstraints(maxWidth: 420),
constraints: BoxConstraints(
maxWidth: 420,
maxHeight: inlineMediaMaxHeight(context),
),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(pt.rLg),
@@ -514,7 +544,7 @@ class _PyramidVideoPlayerState extends State<PyramidVideoPlayer> {
),
clipBehavior: Clip.hardEdge,
child: AspectRatio(
aspectRatio: 16 / 9,
aspectRatio: _aspect ?? 16 / 9,
child: StreamBuilder<bool>(
stream: _player.stream.playing,
builder: (_, playSnap) => StreamBuilder<Duration>(