From 15bebd6e4708810f5989d1e711a27f314056c8cf Mon Sep 17 00:00:00 2001 From: RarDog Date: Fri, 4 Sep 2026 23:09:23 +0300 Subject: [PATCH] fix: fullscreen play-pause button, search clear providers, video volume persistence & codec fallback (v3.1.6) --- README.md | 2 +- lib/app/app_version.dart | 4 +- lib/app/changelog.dart | 10 ++ .../presentation/widgets/feed_toolbar.dart | 4 +- .../widgets/post_media_viewer.dart | 131 ++++++++++++------ pubspec.yaml | 2 +- 6 files changed, 103 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 66eb9b1..fb6383a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

- Version 3.1.5 + Version 3.1.6 Flutter Platforms Wiki diff --git a/lib/app/app_version.dart b/lib/app/app_version.dart index 29348d0..7d9c00d 100644 --- a/lib/app/app_version.dart +++ b/lib/app/app_version.dart @@ -1,3 +1,3 @@ -const appDisplayVersion = '3.1.5'; -const appBuildNumber = 66; +const appDisplayVersion = '3.1.6'; +const appBuildNumber = 67; const appReleaseChannel = 'Stable'; diff --git a/lib/app/changelog.dart b/lib/app/changelog.dart index 96ee15f..80e3994 100644 --- a/lib/app/changelog.dart +++ b/lib/app/changelog.dart @@ -1,4 +1,14 @@ const lunarisChangelog = [ + LunarisChange( + version: '3.1.6', + title: 'Надежный полноэкранный плеер, сохранение провайдеров в поиске, громкость и устранение ошибки кодека', + bullets: [ + 'Полноэкранный плеер — отзывчивая кнопка Пуск/Пауза: центральная кнопка управления воспроизведением полностью переработана на непроницаемый GestureDetector со сквиркл-откликом, гарантируя мгновенное срабатывание без перехвата внешними жестами.', + 'Сохранение источников при нажатии на крестик в поиске: клик по кнопке сброса в строке поиска теперь очищает только поисковый запрос и теги, никогда не сбрасывая выбранные источники до «Все».', + 'Безупречное сохранение громкости: громкость видеоплеера корректно инициализируется из настроек, сохраняется при регулировках жестом и переходе между постами, не сбрасываясь на 100% при открытии нового видео и перезапуске приложения.', + 'Автоматический fallback для ошибки Could not open codec: видеоплеер получил безопасные профили декодирования и автопереключение на программный лавк-декодер (FFmpeg software fallback) при редких сбоях аппаратного ускорения.', + ], + ), LunarisChange( version: '3.1.5', title: 'Сохранение громкости видео, исправление полноэкранного плеера, фильтрации и удобства комментариев', diff --git a/lib/features/feed/presentation/widgets/feed_toolbar.dart b/lib/features/feed/presentation/widgets/feed_toolbar.dart index e39b5d8..d958e85 100644 --- a/lib/features/feed/presentation/widgets/feed_toolbar.dart +++ b/lib/features/feed/presentation/widgets/feed_toolbar.dart @@ -104,7 +104,7 @@ class _FeedToolbarState extends State { }, onCleared: () { _query = ''; - widget.onClearFilters(); + widget.onSearch(''); }, ), ), @@ -235,7 +235,7 @@ class _FeedToolbarState extends State { }, onCleared: () { _query = ''; - widget.onClearFilters(); + widget.onSearch(''); }, ), ), diff --git a/lib/features/post/presentation/widgets/post_media_viewer.dart b/lib/features/post/presentation/widgets/post_media_viewer.dart index f65f99d..419b993 100644 --- a/lib/features/post/presentation/widgets/post_media_viewer.dart +++ b/lib/features/post/presentation/widgets/post_media_viewer.dart @@ -115,6 +115,27 @@ class _PostMediaViewerState extends State unawaited(_retryVideo()); } } + if (oldWidget.initialVolume != widget.initialVolume) { + _currentVolume = widget.initialVolume; + unawaited(_applyVolume()); + } + if (oldWidget.initialMuted != widget.initialMuted) { + _muted = widget.initialMuted; + unawaited(_applyVolume()); + } + if (oldWidget.initialHalfVolume != widget.initialHalfVolume) { + _halfVolume = widget.initialHalfVolume; + unawaited(_applyVolume()); + } + if (oldWidget.initialLoop != widget.initialLoop) { + _loopVideo = widget.initialLoop; + unawaited(_player?.setPlaylistMode( + _loopVideo ? PlaylistMode.single : PlaylistMode.none, + )); + } + if (oldWidget.initialCoverVideo != widget.initialCoverVideo) { + _coverVideo = widget.initialCoverVideo; + } } @override @@ -177,6 +198,7 @@ class _PostMediaViewerState extends State onVolumeChanged: (vol) { _currentVolume = vol; widget.onVolumeChanged?.call(vol); + _emitPlaybackPreferences(); }, fullscreen: widget.fullscreen, errorMessage: _videoError, @@ -338,6 +360,12 @@ class _PostMediaViewerState extends State MediaKit.ensureInitialized(); _player = Player(); _controller = VideoController(_player!); + try { + final native = _player!.platform as dynamic; + native?.setProperty('hwdec', 'auto-safe', waitForInitialization: false); + native?.setProperty('vd-lavc-software-fallback', 'yes', waitForInitialization: false); + native?.setProperty('vd-lavc-check-hw-profile', 'yes', waitForInitialization: false); + } catch (_) {} _applyVolume(); _player!.setPlaylistMode( _loopVideo ? PlaylistMode.single : PlaylistMode.none, @@ -381,6 +409,7 @@ class _PostMediaViewerState extends State ), play: false, ); + await _applyVolume(); if (initialPosition > Duration.zero) { await player.seek(initialPosition); } @@ -396,14 +425,22 @@ class _PostMediaViewerState extends State await _openVideo(play: play); return; } - // If format error occurred on initial load (often due to async referer headers), retry once automatically + final lower = message.toLowerCase(); + // If format or codec error occurred (e.g. unsupported hwdec profile), fallback to software decoding and retry once if (!_retriedFormatError && - (message.contains('Failed to recognize file format') || - message.contains('Demuxer error') || - message.contains('Could not open') || - message.contains('Stream format not recognized') || - message.contains('format unrecognized'))) { + (lower.contains('codec') || + lower.contains('could not open') || + lower.contains('couldnt open') || + lower.contains('could not initialize codec') || + lower.contains('demuxer error') || + lower.contains('format unrecognized') || + lower.contains('stream format not recognized') || + lower.contains('failed to recognize file format'))) { _retriedFormatError = true; + try { + final native = _player?.platform as dynamic; + await native?.setProperty('hwdec', 'no', waitForInitialization: false); + } catch (_) {} await Future.delayed(const Duration(milliseconds: 300)); if (mounted && _player != null) { _videoIndex = 0; @@ -419,6 +456,10 @@ class _PostMediaViewerState extends State Future _retryVideo() async { _videoIndex = 0; _retriedFormatError = false; + try { + final native = _player?.platform as dynamic; + await native?.setProperty('hwdec', 'no', waitForInitialization: false); + } catch (_) {} await _openVideo(play: true); } @@ -732,6 +773,7 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> { _loopVideo = widget.loopVideo; _halfVolume = widget.halfVolume; _currentVolume = widget.initialVolume; + _applyVolume(); _isLandscape = widget.aspectRatio > 1.05; SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); _applyOrientation(); @@ -996,9 +1038,17 @@ class _VideoSurfaceState extends State<_VideoSurface> { @override void initState() { super.initState(); - _currentVolume = widget.player.state.volume > 0 - ? widget.player.state.volume - : widget.initialVolume; + _currentVolume = widget.initialVolume; + widget.player.setVolume(_currentVolume); + } + + @override + void didUpdateWidget(covariant _VideoSurface oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.initialVolume != widget.initialVolume) { + _currentVolume = widget.initialVolume; + widget.player.setVolume(_currentVolume); + } } @override @@ -1495,9 +1545,9 @@ class _RoundControlButton extends StatelessWidget { : Colors.white; return Tooltip( message: tooltip, - child: InkResponse( + child: GestureDetector( + behavior: HitTestBehavior.opaque, onTap: onPressed, - radius: size / 2 + 4, child: AnimatedContainer( duration: AppMotion.duration(context, 140), width: size, @@ -1513,12 +1563,15 @@ class _RoundControlButton extends StatelessWidget { boxShadow: [ if (emphasized) BoxShadow( - color: scheme.primary.withValues(alpha: 0.35), - blurRadius: 14, + color: scheme.primary.withValues(alpha: 0.45), + blurRadius: 28, + spreadRadius: 2, ), ], ), - child: Icon(icon, color: foreground, size: iconSize), + child: Center( + child: Icon(icon, color: foreground, size: iconSize), + ), ), ), ); @@ -1918,41 +1971,31 @@ class _VideoControls extends StatelessWidget { }, ), const SizedBox(width: 32), - Container( - width: 68, - height: 68, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: scheme.primary.withValues(alpha: 0.94), - boxShadow: [ - BoxShadow( - color: scheme.primary.withValues(alpha: 0.45), - blurRadius: 28, - spreadRadius: 2, - ), - ], - ), - child: IconButton( - tooltip: playing ? 'Пауза' : 'Воспроизведение', - iconSize: 42, - color: scheme.onPrimary, - onPressed: () async { + _RoundControlButton( + tooltip: playing ? 'Пауза' : 'Воспроизведение', + icon: playing + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + size: 68, + iconSize: 42, + emphasized: true, + onPressed: () async { + try { + if (player.state.completed) { + await player.seek(Duration.zero); + await player.play(); + } else { + await player.playOrPause(); + } + } catch (_) { if (player.state.playing) { await player.pause(); } else { - if (player.state.completed) { - await player.seek(Duration.zero); - } await player.play(); } - onInteract?.call(); - }, - icon: Icon( - playing - ? Icons.pause_rounded - : Icons.play_arrow_rounded, - ), - ), + } + onInteract?.call(); + }, ), const SizedBox(width: 32), _RoundControlButton( diff --git a/pubspec.yaml b/pubspec.yaml index cf0055c..0a56959 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.5+66 +version: 3.1.6+67 environment: sdk: ">=3.3.0 <4.0.0"