From a88a8b5050e5cad9ebe032cc18e577053910b937 Mon Sep 17 00:00:00 2001 From: RarDog Date: Sat, 5 Sep 2026 09:53:10 +0300 Subject: [PATCH] feat: fix fullscreen video playback retention, orientation layout and clean up UI (v3.2.0) --- README.md | 2 +- lib/app/app_version.dart | 4 +- lib/app/changelog.dart | 10 + lib/app/responsive.dart | 18 +- .../widgets/post_media_viewer.dart | 254 ++++++++++-------- pubspec.lock | 40 --- pubspec.yaml | 4 +- 7 files changed, 168 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index 2ec63e0..6fdd1d9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

- Version 3.1.9 + Version 3.2.0 Flutter Platforms Wiki diff --git a/lib/app/app_version.dart b/lib/app/app_version.dart index 5819934..9564949 100644 --- a/lib/app/app_version.dart +++ b/lib/app/app_version.dart @@ -1,3 +1,3 @@ -const appDisplayVersion = '3.1.9'; -const appBuildNumber = 70; +const appDisplayVersion = '3.2.0'; +const appBuildNumber = 71; const appReleaseChannel = 'Stable'; diff --git a/lib/app/changelog.dart b/lib/app/changelog.dart index 7d3a0ff..7341d82 100644 --- a/lib/app/changelog.dart +++ b/lib/app/changelog.dart @@ -1,4 +1,14 @@ const lunarisChangelog = [ + LunarisChange( + version: '3.2.0', + title: 'Стабильный полноэкранный режим: сохранение воспроизведения, устранение вылетов плеера и чистый интерфейс', + bullets: [ + 'Сохранение воспроизведения видео при повороте экрана: исправлена логика адаптивности на смартфонах — альбомная ориентация больше не сбрасывает мобильный интерфейс и не уничтожает запущенный плеер.', + 'Защита жизненного цикла Player в фоне: плеер не уничтожается при переходе в полный экран, а кнопки «Пуск/Пауза» и перемотки работают мгновенно и безотказно.', + 'Чистый кинематографичный интерфейс: удалены старые дублирующиеся кнопки в правом верхнем углу; кнопка смены ориентации (альбомный/портретный режим) аккуратно интегрирована в верхнюю панель управления.', + 'Быстрое скрытие и показ контролов: касание свободной области видео плавно переключает видимость панели управления, а при нажатии на кнопки таймер автоскрытия автоматически обновляется.', + ], + ), LunarisChange( version: '3.1.9', title: 'Полноэкранный плеер 2.0: мгновенный переход без задержек, жесты яркости и скраббинг с превью', diff --git a/lib/app/responsive.dart b/lib/app/responsive.dart index 5e2fa30..4f4f98e 100644 --- a/lib/app/responsive.dart +++ b/lib/app/responsive.dart @@ -1,3 +1,5 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; class AppBreakpoints { @@ -6,11 +8,19 @@ class AppBreakpoints { } class Responsive { - static bool isMobile(BuildContext context) => - MediaQuery.sizeOf(context).width < AppBreakpoints.mobile; + static bool isMobile(BuildContext context) { + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + return MediaQuery.sizeOf(context).shortestSide < 600; + } + return MediaQuery.sizeOf(context).width < AppBreakpoints.mobile; + } - static bool isDesktop(BuildContext context) => - MediaQuery.sizeOf(context).width >= AppBreakpoints.desktop; + static bool isDesktop(BuildContext context) { + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + return MediaQuery.sizeOf(context).shortestSide >= 600; + } + return MediaQuery.sizeOf(context).width >= AppBreakpoints.desktop; + } static int columnsFor( BuildContext context, { diff --git a/lib/features/post/presentation/widgets/post_media_viewer.dart b/lib/features/post/presentation/widgets/post_media_viewer.dart index 48139e3..237c041 100644 --- a/lib/features/post/presentation/widgets/post_media_viewer.dart +++ b/lib/features/post/presentation/widgets/post_media_viewer.dart @@ -3,12 +3,12 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; -import 'package:screen_brightness/screen_brightness.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../../app/motion.dart'; @@ -143,7 +143,9 @@ class _PostMediaViewerState extends State @override void dispose() { - _disposeVideo(); + if (!_inFullscreen) { + _disposeVideo(); + } super.dispose(); } @@ -205,6 +207,7 @@ class _PostMediaViewerState extends State }, fullscreen: widget.fullscreen, errorMessage: _videoError, + onTapSurface: _toggleControls, onInteract: _showControls, onRetry: _retryVideo, onToggleFit: () { @@ -553,6 +556,16 @@ class _PostMediaViewerState extends State }); } + void _toggleControls() { + if (!mounted) return; + if (_controlsVisible) { + _hideTimer?.cancel(); + setState(() => _controlsVisible = false); + } else { + _showControls(); + } + } + void _showControls() { if (!mounted) return; setState(() => _controlsVisible = true); @@ -601,7 +614,12 @@ class _PostMediaViewerState extends State ), ), ); - if (mounted) setState(() => _inFullscreen = false); + if (mounted) { + setState(() => _inFullscreen = false); + } else { + player.dispose(); + return; + } if (result != null) { await _applyPlaybackSnapshot(result); if (result.playing && !player.state.playing) { @@ -816,14 +834,28 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> { _showControls(); } + void _toggleControls() { + if (!mounted) return; + if (_controlsVisible) { + _hideTimer?.cancel(); + setState(() => _controlsVisible = false); + } else { + _showControls(); + } + } + @override void dispose() { _hideTimer?.cancel(); - try { - ScreenBrightness.instance.resetApplicationScreenBrightness(); - } catch (_) {} SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - SystemChrome.setPreferredOrientations(DeviceOrientation.values); + if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + Future.delayed(const Duration(milliseconds: 300), () { + SystemChrome.setPreferredOrientations([]); + }); + } else { + SystemChrome.setPreferredOrientations(DeviceOrientation.values); + } super.dispose(); } @@ -860,91 +892,59 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> { }, child: Focus( autofocus: true, - child: Stack( - children: [ - Positioned.fill( - child: _VideoSurface( - player: widget.player, - controller: widget.controller, - aspectRatio: widget.aspectRatio, - controlsVisible: _controlsVisible, - coverVideo: _coverVideo, - muted: _muted, - loopVideo: _loopVideo, - halfVolume: _halfVolume, - initialVolume: _currentVolume, - onVolumeChanged: (vol) { - _currentVolume = vol; - widget.onChanged(_snapshot()); - }, - fullscreen: true, - errorMessage: widget.errorMessage, - onInteract: _showControls, - onRetry: widget.onRetry, - onToggleFit: () { - setState(() => _coverVideo = !_coverVideo); - widget.onChanged(_snapshot()); - _showControls(); - }, - onToggleMute: () async { - setState(() => _muted = !_muted); - await _applyVolume(); - widget.onChanged(_snapshot()); - _showControls(); - }, - onToggleHalfVolume: () async { - setState(() { - _halfVolume = !_halfVolume; - if (_halfVolume) _muted = false; - }); - await _applyVolume(); - widget.onChanged(_snapshot()); - _showControls(); - }, - onToggleLoop: () async { - setState(() => _loopVideo = !_loopVideo); - await widget.player.setPlaylistMode( - _loopVideo ? PlaylistMode.single : PlaylistMode.none, - ); - widget.onChanged(_snapshot()); - _showControls(); - }, - onFullscreen: () => Navigator.of( - context, - rootNavigator: true, - ).pop(_snapshot()), - ), - ), - Positioned( - top: 12, - right: 12, - child: SafeArea( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton.filledTonal( - tooltip: _isLandscape - ? 'Портретная ориентация' - : 'Альбомная ориентация', - onPressed: _toggleOrientation, - icon: Icon(_isLandscape - ? Icons.screen_lock_portrait_rounded - : Icons.screen_lock_landscape_rounded), - ), - const SizedBox(width: 8), - IconButton.filledTonal( - tooltip: 'Close fullscreen', - onPressed: () => Navigator.of( - context, - rootNavigator: true, - ).pop(_snapshot()), - icon: const Icon(Icons.close_rounded), - ), - ], - ), - ), - ), - ], + child: _VideoSurface( + player: widget.player, + controller: widget.controller, + aspectRatio: widget.aspectRatio, + controlsVisible: _controlsVisible, + coverVideo: _coverVideo, + muted: _muted, + loopVideo: _loopVideo, + halfVolume: _halfVolume, + initialVolume: _currentVolume, + onVolumeChanged: (vol) { + _currentVolume = vol; + widget.onChanged(_snapshot()); + }, + fullscreen: true, + errorMessage: widget.errorMessage, + onTapSurface: _toggleControls, + onInteract: _showControls, + isLandscape: _isLandscape, + onToggleOrientation: _toggleOrientation, + onRetry: widget.onRetry, + onToggleFit: () { + setState(() => _coverVideo = !_coverVideo); + widget.onChanged(_snapshot()); + _showControls(); + }, + onToggleMute: () async { + setState(() => _muted = !_muted); + await _applyVolume(); + widget.onChanged(_snapshot()); + _showControls(); + }, + onToggleHalfVolume: () async { + setState(() { + _halfVolume = !_halfVolume; + if (_halfVolume) _muted = false; + }); + await _applyVolume(); + widget.onChanged(_snapshot()); + _showControls(); + }, + onToggleLoop: () async { + setState(() => _loopVideo = !_loopVideo); + await widget.player.setPlaylistMode( + _loopVideo ? PlaylistMode.single : PlaylistMode.none, + ); + widget.onChanged(_snapshot()); + _showControls(); + }, + onFullscreen: () => Navigator.of( + context, + rootNavigator: true, + ).pop(_snapshot()), ), ), ), @@ -1007,6 +1007,7 @@ class _VideoSurface extends StatefulWidget { this.onVolumeChanged, required this.fullscreen, required this.errorMessage, + this.onTapSurface, required this.onInteract, required this.onRetry, required this.onToggleFit, @@ -1014,6 +1015,8 @@ class _VideoSurface extends StatefulWidget { required this.onToggleHalfVolume, required this.onToggleLoop, required this.onFullscreen, + this.isLandscape, + this.onToggleOrientation, }); final Player player; @@ -1028,6 +1031,7 @@ class _VideoSurface extends StatefulWidget { final ValueChanged? onVolumeChanged; final bool fullscreen; final String? errorMessage; + final VoidCallback? onTapSurface; final VoidCallback onInteract; final VoidCallback onRetry; final VoidCallback onToggleFit; @@ -1035,6 +1039,8 @@ class _VideoSurface extends StatefulWidget { final VoidCallback onToggleHalfVolume; final VoidCallback onToggleLoop; final VoidCallback onFullscreen; + final bool? isLandscape; + final VoidCallback? onToggleOrientation; @override State<_VideoSurface> createState() => _VideoSurfaceState(); @@ -1055,7 +1061,7 @@ class _VideoSurfaceState extends State<_VideoSurface> { Timer? _volumeTimer; bool _showBrightnessIndicator = false; - double _currentBrightness = 0.5; + double _currentBrightness = 1.0; Timer? _brightnessTimer; @override @@ -1063,14 +1069,6 @@ class _VideoSurfaceState extends State<_VideoSurface> { super.initState(); _currentVolume = widget.initialVolume; widget.player.setVolume(_currentVolume); - _initBrightness(); - } - - Future _initBrightness() async { - try { - final b = await ScreenBrightness.instance.application; - if (mounted) setState(() => _currentBrightness = b); - } catch (_) {} } @override @@ -1159,14 +1157,13 @@ class _VideoSurfaceState extends State<_VideoSurface> { }); } - void _adjustBrightness(double deltaY) async { + void _adjustBrightness(double deltaY) { if (_isLocked) return; - final newB = (_currentBrightness - deltaY * 0.004).clamp(0.02, 1.0); - _currentBrightness = newB; - try { - await ScreenBrightness.instance.setApplicationScreenBrightness(newB); - } catch (_) {} - setState(() => _showBrightnessIndicator = true); + final newB = (_currentBrightness - deltaY * 0.004).clamp(0.08, 1.0); + setState(() { + _currentBrightness = newB; + _showBrightnessIndicator = true; + }); _brightnessTimer?.cancel(); _brightnessTimer = Timer(const Duration(milliseconds: 1100), () { if (mounted) setState(() => _showBrightnessIndicator = false); @@ -1193,11 +1190,21 @@ class _VideoSurfaceState extends State<_VideoSurface> { resumeUponEnteringForegroundMode: false, ), ), + if (_currentBrightness < 0.99) + Positioned.fill( + child: IgnorePointer( + child: ColoredBox( + color: Colors.black.withValues( + alpha: (1.0 - _currentBrightness).clamp(0.0, 0.92), + ), + ), + ), + ), Positioned.fill( child: GestureDetector( behavior: HitTestBehavior.opaque, onTapDown: (details) => lastTapDown = details.localPosition, - onTap: widget.onInteract, + onTap: widget.onTapSurface ?? widget.onInteract, onDoubleTap: () { if (lastTapDown != null) { _onDoubleTapAt( @@ -1259,6 +1266,8 @@ class _VideoSurfaceState extends State<_VideoSurface> { coverVideo: widget.coverVideo, fullscreen: widget.fullscreen, isLocked: _isLocked, + isLandscape: widget.isLandscape, + onToggleOrientation: widget.onToggleOrientation, onToggleLock: () => setState(() => _isLocked = !_isLocked), onToggleFit: widget.onToggleFit, onToggleMute: widget.onToggleMute, @@ -1944,6 +1953,8 @@ class _VideoControls extends StatefulWidget { required this.coverVideo, required this.fullscreen, required this.isLocked, + this.isLandscape, + this.onToggleOrientation, required this.onToggleLock, required this.onToggleFit, required this.onToggleMute, @@ -1961,6 +1972,8 @@ class _VideoControls extends StatefulWidget { final bool coverVideo; final bool fullscreen; final bool isLocked; + final bool? isLandscape; + final VoidCallback? onToggleOrientation; final VoidCallback onToggleLock; final VoidCallback onToggleFit; final VoidCallback onToggleMute; @@ -2038,6 +2051,21 @@ class _VideoControlsState extends State<_VideoControls> { ), const SizedBox(width: 8), ], + if (widget.fullscreen && widget.onToggleOrientation != null) ...[ + _RoundControlButton( + tooltip: (widget.isLandscape ?? false) + ? 'Портретная ориентация' + : 'Альбомная ориентация', + icon: (widget.isLandscape ?? false) + ? Icons.screen_lock_portrait_rounded + : Icons.screen_lock_landscape_rounded, + onPressed: () { + widget.onToggleOrientation?.call(); + widget.onInteract?.call(); + }, + ), + const SizedBox(width: 8), + ], _RoundControlButton( tooltip: 'Заблокировать экран', icon: Icons.lock_outline_rounded, @@ -2121,11 +2149,9 @@ class _VideoControlsState extends State<_VideoControls> { await widget.player.play(); } } catch (_) { - if (widget.player.state.playing) { - await widget.player.pause(); - } else { - await widget.player.play(); - } + try { + await widget.player.playOrPause(); + } catch (_) {} } widget.onInteract?.call(); }, diff --git a/pubspec.lock b/pubspec.lock index 4f73c64..9c86952 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -895,14 +895,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - screen_brightness: - dependency: "direct main" - description: - name: screen_brightness - sha256: e0edf92c08889e8f493cde291e7c687db2b4a1471f2371c074070b75d7c7d79b - url: "https://pub.dev" - source: hosted - version: "2.1.11" screen_brightness_android: dependency: transitive description: @@ -911,30 +903,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.6" - screen_brightness_ios: - dependency: transitive - description: - name: screen_brightness_ios - sha256: "352d355e8523a186ba1e494b74218e5ca96e51975a0630b8ed89fbb7ff609762" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - screen_brightness_macos: - dependency: transitive - description: - name: screen_brightness_macos - sha256: b0957237b842d846a84363b69f229b339a8f91faced55041e245b2ebff7b0142 - url: "https://pub.dev" - source: hosted - version: "2.1.4" - screen_brightness_ohos: - dependency: transitive - description: - name: screen_brightness_ohos - sha256: "569a2c97909a50894b81487619eb7654f5358443a5af05f840c19261f49c0940" - url: "https://pub.dev" - source: hosted - version: "2.1.4" screen_brightness_platform_interface: dependency: transitive description: @@ -943,14 +911,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" - screen_brightness_windows: - dependency: transitive - description: - name: screen_brightness_windows - sha256: "368b9c822e1671de81616e48150006e39eff2a434957e47ee638b09a32b2297c" - url: "https://pub.dev" - source: hosted - version: "2.1.2" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b91fd31..c3b6731 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: gel_rule_app description: Local backend/core layer for a Flutter booru browser app. publish_to: "none" -version: 3.1.9+70 +version: 3.2.0+71 environment: sdk: ">=3.3.0 <4.0.0" @@ -33,8 +33,6 @@ dependencies: media_kit_libs_android_video: ^1.3.8 share_plus: ^10.0.0 dynamic_color: ^1.7.0 - screen_brightness: ^2.1.11 - dev_dependencies: flutter_test: sdk: flutter