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