fix: fullscreen play-pause button, search clear providers, video volume persistence & codec fallback (v3.1.6)
Build and Release / Build Windows x64 (push) Has been cancelled

This commit is contained in:
2026-09-04 23:09:23 +03:00
parent 295499ca4b
commit 15bebd6e47
6 changed files with 103 additions and 50 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/RarDog/Lunaris/releases"><img src="https://img.shields.io/badge/Version-3.1.5-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.1.5" /></a> <a href="https://github.com/RarDog/Lunaris/releases"><img src="https://img.shields.io/badge/Version-3.1.6-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.1.6" /></a>
<img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" /> <img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" />
<img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows-00C853?style=for-the-badge" alt="Platforms" /> <img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows-00C853?style=for-the-badge" alt="Platforms" />
<a href="https://github.com/RarDog/Lunaris/wiki"><img src="https://img.shields.io/badge/Wiki-Documentation-FF6F00?style=for-the-badge&logo=gitbook&logoColor=white" alt="Wiki" /></a> <a href="https://github.com/RarDog/Lunaris/wiki"><img src="https://img.shields.io/badge/Wiki-Documentation-FF6F00?style=for-the-badge&logo=gitbook&logoColor=white" alt="Wiki" /></a>
+2 -2
View File
@@ -1,3 +1,3 @@
const appDisplayVersion = '3.1.5'; const appDisplayVersion = '3.1.6';
const appBuildNumber = 66; const appBuildNumber = 67;
const appReleaseChannel = 'Stable'; const appReleaseChannel = 'Stable';
+10
View File
@@ -1,4 +1,14 @@
const lunarisChangelog = [ const lunarisChangelog = [
LunarisChange(
version: '3.1.6',
title: 'Надежный полноэкранный плеер, сохранение провайдеров в поиске, громкость и устранение ошибки кодека',
bullets: [
'Полноэкранный плеер — отзывчивая кнопка Пуск/Пауза: центральная кнопка управления воспроизведением полностью переработана на непроницаемый GestureDetector со сквиркл-откликом, гарантируя мгновенное срабатывание без перехвата внешними жестами.',
'Сохранение источников при нажатии на крестик в поиске: клик по кнопке сброса в строке поиска теперь очищает только поисковый запрос и теги, никогда не сбрасывая выбранные источники до «Все».',
'Безупречное сохранение громкости: громкость видеоплеера корректно инициализируется из настроек, сохраняется при регулировках жестом и переходе между постами, не сбрасываясь на 100% при открытии нового видео и перезапуске приложения.',
'Автоматический fallback для ошибки Could not open codec: видеоплеер получил безопасные профили декодирования и автопереключение на программный лавк-декодер (FFmpeg software fallback) при редких сбоях аппаратного ускорения.',
],
),
LunarisChange( LunarisChange(
version: '3.1.5', version: '3.1.5',
title: 'Сохранение громкости видео, исправление полноэкранного плеера, фильтрации и удобства комментариев', title: 'Сохранение громкости видео, исправление полноэкранного плеера, фильтрации и удобства комментариев',
@@ -104,7 +104,7 @@ class _FeedToolbarState extends State<FeedToolbar> {
}, },
onCleared: () { onCleared: () {
_query = ''; _query = '';
widget.onClearFilters(); widget.onSearch('');
}, },
), ),
), ),
@@ -235,7 +235,7 @@ class _FeedToolbarState extends State<FeedToolbar> {
}, },
onCleared: () { onCleared: () {
_query = ''; _query = '';
widget.onClearFilters(); widget.onSearch('');
}, },
), ),
), ),
@@ -115,6 +115,27 @@ class _PostMediaViewerState extends State<PostMediaViewer>
unawaited(_retryVideo()); 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 @override
@@ -177,6 +198,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
onVolumeChanged: (vol) { onVolumeChanged: (vol) {
_currentVolume = vol; _currentVolume = vol;
widget.onVolumeChanged?.call(vol); widget.onVolumeChanged?.call(vol);
_emitPlaybackPreferences();
}, },
fullscreen: widget.fullscreen, fullscreen: widget.fullscreen,
errorMessage: _videoError, errorMessage: _videoError,
@@ -338,6 +360,12 @@ class _PostMediaViewerState extends State<PostMediaViewer>
MediaKit.ensureInitialized(); MediaKit.ensureInitialized();
_player = Player(); _player = Player();
_controller = VideoController(_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(); _applyVolume();
_player!.setPlaylistMode( _player!.setPlaylistMode(
_loopVideo ? PlaylistMode.single : PlaylistMode.none, _loopVideo ? PlaylistMode.single : PlaylistMode.none,
@@ -381,6 +409,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
), ),
play: false, play: false,
); );
await _applyVolume();
if (initialPosition > Duration.zero) { if (initialPosition > Duration.zero) {
await player.seek(initialPosition); await player.seek(initialPosition);
} }
@@ -396,14 +425,22 @@ class _PostMediaViewerState extends State<PostMediaViewer>
await _openVideo(play: play); await _openVideo(play: play);
return; 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 && if (!_retriedFormatError &&
(message.contains('Failed to recognize file format') || (lower.contains('codec') ||
message.contains('Demuxer error') || lower.contains('could not open') ||
message.contains('Could not open') || lower.contains('couldnt open') ||
message.contains('Stream format not recognized') || lower.contains('could not initialize codec') ||
message.contains('format unrecognized'))) { lower.contains('demuxer error') ||
lower.contains('format unrecognized') ||
lower.contains('stream format not recognized') ||
lower.contains('failed to recognize file format'))) {
_retriedFormatError = true; _retriedFormatError = true;
try {
final native = _player?.platform as dynamic;
await native?.setProperty('hwdec', 'no', waitForInitialization: false);
} catch (_) {}
await Future<void>.delayed(const Duration(milliseconds: 300)); await Future<void>.delayed(const Duration(milliseconds: 300));
if (mounted && _player != null) { if (mounted && _player != null) {
_videoIndex = 0; _videoIndex = 0;
@@ -419,6 +456,10 @@ class _PostMediaViewerState extends State<PostMediaViewer>
Future<void> _retryVideo() async { Future<void> _retryVideo() async {
_videoIndex = 0; _videoIndex = 0;
_retriedFormatError = false; _retriedFormatError = false;
try {
final native = _player?.platform as dynamic;
await native?.setProperty('hwdec', 'no', waitForInitialization: false);
} catch (_) {}
await _openVideo(play: true); await _openVideo(play: true);
} }
@@ -732,6 +773,7 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> {
_loopVideo = widget.loopVideo; _loopVideo = widget.loopVideo;
_halfVolume = widget.halfVolume; _halfVolume = widget.halfVolume;
_currentVolume = widget.initialVolume; _currentVolume = widget.initialVolume;
_applyVolume();
_isLandscape = widget.aspectRatio > 1.05; _isLandscape = widget.aspectRatio > 1.05;
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
_applyOrientation(); _applyOrientation();
@@ -996,9 +1038,17 @@ class _VideoSurfaceState extends State<_VideoSurface> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_currentVolume = widget.player.state.volume > 0 _currentVolume = widget.initialVolume;
? widget.player.state.volume widget.player.setVolume(_currentVolume);
: widget.initialVolume; }
@override
void didUpdateWidget(covariant _VideoSurface oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialVolume != widget.initialVolume) {
_currentVolume = widget.initialVolume;
widget.player.setVolume(_currentVolume);
}
} }
@override @override
@@ -1495,9 +1545,9 @@ class _RoundControlButton extends StatelessWidget {
: Colors.white; : Colors.white;
return Tooltip( return Tooltip(
message: tooltip, message: tooltip,
child: InkResponse( child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onPressed, onTap: onPressed,
radius: size / 2 + 4,
child: AnimatedContainer( child: AnimatedContainer(
duration: AppMotion.duration(context, 140), duration: AppMotion.duration(context, 140),
width: size, width: size,
@@ -1513,14 +1563,17 @@ class _RoundControlButton extends StatelessWidget {
boxShadow: [ boxShadow: [
if (emphasized) if (emphasized)
BoxShadow( BoxShadow(
color: scheme.primary.withValues(alpha: 0.35), color: scheme.primary.withValues(alpha: 0.45),
blurRadius: 14, blurRadius: 28,
spreadRadius: 2,
), ),
], ],
), ),
child: Center(
child: Icon(icon, color: foreground, size: iconSize), child: Icon(icon, color: foreground, size: iconSize),
), ),
), ),
),
); );
} }
} }
@@ -1918,41 +1971,31 @@ class _VideoControls extends StatelessWidget {
}, },
), ),
const SizedBox(width: 32), const SizedBox(width: 32),
Container( _RoundControlButton(
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 ? 'Пауза' : 'Воспроизведение', tooltip: playing ? 'Пауза' : 'Воспроизведение',
icon: playing
? Icons.pause_rounded
: Icons.play_arrow_rounded,
size: 68,
iconSize: 42, iconSize: 42,
color: scheme.onPrimary, emphasized: true,
onPressed: () async { onPressed: () async {
try {
if (player.state.completed) {
await player.seek(Duration.zero);
await player.play();
} else {
await player.playOrPause();
}
} catch (_) {
if (player.state.playing) { if (player.state.playing) {
await player.pause(); await player.pause();
} else { } else {
if (player.state.completed) {
await player.seek(Duration.zero);
}
await player.play(); await player.play();
} }
}
onInteract?.call(); onInteract?.call();
}, },
icon: Icon(
playing
? Icons.pause_rounded
: Icons.play_arrow_rounded,
),
),
), ),
const SizedBox(width: 32), const SizedBox(width: 32),
_RoundControlButton( _RoundControlButton(
+1 -1
View File
@@ -1,7 +1,7 @@
name: gel_rule_app name: gel_rule_app
description: Local backend/core layer for a Flutter booru browser app. description: Local backend/core layer for a Flutter booru browser app.
publish_to: "none" publish_to: "none"
version: 3.1.5+66 version: 3.1.6+67
environment: environment:
sdk: ">=3.3.0 <4.0.0" sdk: ">=3.3.0 <4.0.0"