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 @@
-
+
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