Compare commits

...
2 Commits
Author SHA1 Message Date
RarDog a88a8b5050 feat: fix fullscreen video playback retention, orientation layout and clean up UI (v3.2.0)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-05 09:53:10 +03:00
RarDog a039f09ec3 feat: Fullscreen 2.0 with instant handoff, screen brightness gestures, and real-time scrub preview (v3.1.9)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 23:43:46 +03:00
7 changed files with 455 additions and 244 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.8-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.1.8" /></a> <a href="https://github.com/RarDog/Lunaris/releases"><img src="https://img.shields.io/badge/Version-3.2.0-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.2.0" /></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.8'; const appDisplayVersion = '3.2.0';
const appBuildNumber = 69; const appBuildNumber = 71;
const appReleaseChannel = 'Stable'; const appReleaseChannel = 'Stable';
+20
View File
@@ -1,4 +1,24 @@
const lunarisChangelog = [ const lunarisChangelog = [
LunarisChange(
version: '3.2.0',
title: 'Стабильный полноэкранный режим: сохранение воспроизведения, устранение вылетов плеера и чистый интерфейс',
bullets: [
'Сохранение воспроизведения видео при повороте экрана: исправлена логика адаптивности на смартфонах — альбомная ориентация больше не сбрасывает мобильный интерфейс и не уничтожает запущенный плеер.',
'Защита жизненного цикла Player в фоне: плеер не уничтожается при переходе в полный экран, а кнопки «Пуск/Пауза» и перемотки работают мгновенно и безотказно.',
'Чистый кинематографичный интерфейс: удалены старые дублирующиеся кнопки в правом верхнем углу; кнопка смены ориентации (альбомный/портретный режим) аккуратно интегрирована в верхнюю панель управления.',
'Быстрое скрытие и показ контролов: касание свободной области видео плавно переключает видимость панели управления, а при нажатии на кнопки таймер автоскрытия автоматически обновляется.',
],
),
LunarisChange(
version: '3.1.9',
title: 'Полноэкранный плеер 2.0: мгновенный переход без задержек, жесты яркости и скраббинг с превью',
bullets: [
'Мгновенный переход в полноэкранный режим: убраны промежуточные задержки и наложения текстур, вход и выход происходят без малейших задержек.',
'Двухзонное жестовое управление: левая сторона экрана регулирует яркость подсветки с индикатором солнца, правая сторона — громкость звука.',
'Плавный скраббинг с плавающим таймингом: перемещение по таймлайну видео теперь отображает интерактивную плашку точного времени, а контролы не исчезают во время удержания ползунка.',
'Сброс яркости: при выходе из полноэкранного режима системный уровень яркости экрана аккуратно восстанавливается.',
],
),
LunarisChange( LunarisChange(
version: '3.1.8', version: '3.1.8',
title: 'Безупречный полноэкранный плеер: устранение остановки при развертывании и отзывчивые кнопки управления', title: 'Безупречный полноэкранный плеер: устранение остановки при развертывании и отзывчивые кнопки управления',
+14 -4
View File
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class AppBreakpoints { class AppBreakpoints {
@@ -6,11 +8,19 @@ class AppBreakpoints {
} }
class Responsive { class Responsive {
static bool isMobile(BuildContext context) => static bool isMobile(BuildContext context) {
MediaQuery.sizeOf(context).width < AppBreakpoints.mobile; if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
return MediaQuery.sizeOf(context).shortestSide < 600;
}
return MediaQuery.sizeOf(context).width < AppBreakpoints.mobile;
}
static bool isDesktop(BuildContext context) => static bool isDesktop(BuildContext context) {
MediaQuery.sizeOf(context).width >= AppBreakpoints.desktop; if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
return MediaQuery.sizeOf(context).shortestSide >= 600;
}
return MediaQuery.sizeOf(context).width >= AppBreakpoints.desktop;
}
static int columnsFor( static int columnsFor(
BuildContext context, { BuildContext context, {
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -142,7 +143,9 @@ class _PostMediaViewerState extends State<PostMediaViewer>
@override @override
void dispose() { void dispose() {
if (!_inFullscreen) {
_disposeVideo(); _disposeVideo();
}
super.dispose(); super.dispose();
} }
@@ -204,6 +207,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
}, },
fullscreen: widget.fullscreen, fullscreen: widget.fullscreen,
errorMessage: _videoError, errorMessage: _videoError,
onTapSurface: _toggleControls,
onInteract: _showControls, onInteract: _showControls,
onRetry: _retryVideo, onRetry: _retryVideo,
onToggleFit: () { onToggleFit: () {
@@ -552,6 +556,16 @@ class _PostMediaViewerState extends State<PostMediaViewer>
}); });
} }
void _toggleControls() {
if (!mounted) return;
if (_controlsVisible) {
_hideTimer?.cancel();
setState(() => _controlsVisible = false);
} else {
_showControls();
}
}
void _showControls() { void _showControls() {
if (!mounted) return; if (!mounted) return;
setState(() => _controlsVisible = true); setState(() => _controlsVisible = true);
@@ -578,6 +592,8 @@ class _PostMediaViewerState extends State<PostMediaViewer>
.push<VideoPlaybackSnapshot>( .push<VideoPlaybackSnapshot>(
PageRouteBuilder( PageRouteBuilder(
opaque: true, opaque: true,
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
pageBuilder: (_, __, ___) => _FullscreenVideoPage( pageBuilder: (_, __, ___) => _FullscreenVideoPage(
player: player, player: player,
controller: controller, controller: controller,
@@ -596,13 +612,14 @@ class _PostMediaViewerState extends State<PostMediaViewer>
unawaited(_applyPlaybackSnapshot(snapshot)); unawaited(_applyPlaybackSnapshot(snapshot));
}, },
), ),
transitionsBuilder: (_, animation, __, child) => FadeTransition(
opacity: animation,
child: child,
),
), ),
); );
if (mounted) setState(() => _inFullscreen = false); if (mounted) {
setState(() => _inFullscreen = false);
} else {
player.dispose();
return;
}
if (result != null) { if (result != null) {
await _applyPlaybackSnapshot(result); await _applyPlaybackSnapshot(result);
if (result.playing && !player.state.playing) { if (result.playing && !player.state.playing) {
@@ -817,11 +834,28 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> {
_showControls(); _showControls();
} }
void _toggleControls() {
if (!mounted) return;
if (_controlsVisible) {
_hideTimer?.cancel();
setState(() => _controlsVisible = false);
} else {
_showControls();
}
}
@override @override
void dispose() { void dispose() {
_hideTimer?.cancel(); _hideTimer?.cancel();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
Future.delayed(const Duration(milliseconds: 300), () {
SystemChrome.setPreferredOrientations([]);
});
} else {
SystemChrome.setPreferredOrientations(DeviceOrientation.values); SystemChrome.setPreferredOrientations(DeviceOrientation.values);
}
super.dispose(); super.dispose();
} }
@@ -858,9 +892,6 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> {
}, },
child: Focus( child: Focus(
autofocus: true, autofocus: true,
child: Stack(
children: [
Positioned.fill(
child: _VideoSurface( child: _VideoSurface(
player: widget.player, player: widget.player,
controller: widget.controller, controller: widget.controller,
@@ -877,7 +908,10 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> {
}, },
fullscreen: true, fullscreen: true,
errorMessage: widget.errorMessage, errorMessage: widget.errorMessage,
onTapSurface: _toggleControls,
onInteract: _showControls, onInteract: _showControls,
isLandscape: _isLandscape,
onToggleOrientation: _toggleOrientation,
onRetry: widget.onRetry, onRetry: widget.onRetry,
onToggleFit: () { onToggleFit: () {
setState(() => _coverVideo = !_coverVideo); setState(() => _coverVideo = !_coverVideo);
@@ -913,38 +947,6 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> {
).pop(_snapshot()), ).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),
),
],
),
),
),
],
),
),
), ),
), ),
), ),
@@ -1005,6 +1007,7 @@ class _VideoSurface extends StatefulWidget {
this.onVolumeChanged, this.onVolumeChanged,
required this.fullscreen, required this.fullscreen,
required this.errorMessage, required this.errorMessage,
this.onTapSurface,
required this.onInteract, required this.onInteract,
required this.onRetry, required this.onRetry,
required this.onToggleFit, required this.onToggleFit,
@@ -1012,6 +1015,8 @@ class _VideoSurface extends StatefulWidget {
required this.onToggleHalfVolume, required this.onToggleHalfVolume,
required this.onToggleLoop, required this.onToggleLoop,
required this.onFullscreen, required this.onFullscreen,
this.isLandscape,
this.onToggleOrientation,
}); });
final Player player; final Player player;
@@ -1026,6 +1031,7 @@ class _VideoSurface extends StatefulWidget {
final ValueChanged<double>? onVolumeChanged; final ValueChanged<double>? onVolumeChanged;
final bool fullscreen; final bool fullscreen;
final String? errorMessage; final String? errorMessage;
final VoidCallback? onTapSurface;
final VoidCallback onInteract; final VoidCallback onInteract;
final VoidCallback onRetry; final VoidCallback onRetry;
final VoidCallback onToggleFit; final VoidCallback onToggleFit;
@@ -1033,6 +1039,8 @@ class _VideoSurface extends StatefulWidget {
final VoidCallback onToggleHalfVolume; final VoidCallback onToggleHalfVolume;
final VoidCallback onToggleLoop; final VoidCallback onToggleLoop;
final VoidCallback onFullscreen; final VoidCallback onFullscreen;
final bool? isLandscape;
final VoidCallback? onToggleOrientation;
@override @override
State<_VideoSurface> createState() => _VideoSurfaceState(); State<_VideoSurface> createState() => _VideoSurfaceState();
@@ -1052,6 +1060,10 @@ class _VideoSurfaceState extends State<_VideoSurface> {
double _currentVolume = 100; double _currentVolume = 100;
Timer? _volumeTimer; Timer? _volumeTimer;
bool _showBrightnessIndicator = false;
double _currentBrightness = 1.0;
Timer? _brightnessTimer;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -1073,6 +1085,7 @@ class _VideoSurfaceState extends State<_VideoSurface> {
_seekLeftTimer?.cancel(); _seekLeftTimer?.cancel();
_seekRightTimer?.cancel(); _seekRightTimer?.cancel();
_volumeTimer?.cancel(); _volumeTimer?.cancel();
_brightnessTimer?.cancel();
super.dispose(); super.dispose();
} }
@@ -1144,6 +1157,19 @@ class _VideoSurfaceState extends State<_VideoSurface> {
}); });
} }
void _adjustBrightness(double deltaY) {
if (_isLocked) return;
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);
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final player = widget.player; final player = widget.player;
@@ -1164,11 +1190,21 @@ class _VideoSurfaceState extends State<_VideoSurface> {
resumeUponEnteringForegroundMode: false, 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( Positioned.fill(
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTapDown: (details) => lastTapDown = details.localPosition, onTapDown: (details) => lastTapDown = details.localPosition,
onTap: widget.onInteract, onTap: widget.onTapSurface ?? widget.onInteract,
onDoubleTap: () { onDoubleTap: () {
if (lastTapDown != null) { if (lastTapDown != null) {
_onDoubleTapAt( _onDoubleTapAt(
@@ -1182,7 +1218,9 @@ class _VideoSurfaceState extends State<_VideoSurface> {
onVerticalDragUpdate: (details) { onVerticalDragUpdate: (details) {
final x = details.localPosition.dx; final x = details.localPosition.dx;
final width = MediaQuery.sizeOf(context).width; final width = MediaQuery.sizeOf(context).width;
if (x > width * 0.4) { if (x < width * 0.45) {
_adjustBrightness(details.primaryDelta ?? 0.0);
} else if (x > width * 0.55) {
_adjustVolume(details.primaryDelta ?? 0.0); _adjustVolume(details.primaryDelta ?? 0.0);
} }
}, },
@@ -1202,6 +1240,8 @@ class _VideoSurfaceState extends State<_VideoSurface> {
_DoubleTapSeekRipple(isLeft: false, visible: _showRightSeek), _DoubleTapSeekRipple(isLeft: false, visible: _showRightSeek),
if (_showVolumeIndicator) if (_showVolumeIndicator)
_VolumeGestureBadge(volume: _currentVolume), _VolumeGestureBadge(volume: _currentVolume),
if (_showBrightnessIndicator)
_BrightnessGestureBadge(brightness: _currentBrightness),
if (_isSpeedBoosted) if (_isSpeedBoosted)
const _SpeedBoostOverlayBadge(), const _SpeedBoostOverlayBadge(),
if (widget.errorMessage != null) if (widget.errorMessage != null)
@@ -1226,6 +1266,8 @@ class _VideoSurfaceState extends State<_VideoSurface> {
coverVideo: widget.coverVideo, coverVideo: widget.coverVideo,
fullscreen: widget.fullscreen, fullscreen: widget.fullscreen,
isLocked: _isLocked, isLocked: _isLocked,
isLandscape: widget.isLandscape,
onToggleOrientation: widget.onToggleOrientation,
onToggleLock: () => setState(() => _isLocked = !_isLocked), onToggleLock: () => setState(() => _isLocked = !_isLocked),
onToggleFit: widget.onToggleFit, onToggleFit: widget.onToggleFit,
onToggleMute: widget.onToggleMute, onToggleMute: widget.onToggleMute,
@@ -1781,6 +1823,68 @@ class _VolumeGestureBadge extends StatelessWidget {
} }
} }
class _BrightnessGestureBadge extends StatelessWidget {
const _BrightnessGestureBadge({required this.brightness});
final double brightness;
@override
Widget build(BuildContext context) {
final pct = (brightness * 100).round().clamp(0, 100);
final icon = pct < 33
? Icons.brightness_low_rounded
: pct < 66
? Icons.brightness_medium_rounded
: Icons.brightness_high_rounded;
return Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(22),
border: Border.all(color: Colors.white.withValues(alpha: 0.16)),
boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 24),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
color: Colors.white,
size: 26,
),
const SizedBox(width: 12),
SizedBox(
width: 96,
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: brightness.clamp(0.0, 1.0),
minHeight: 6,
backgroundColor: Colors.white24,
valueColor: const AlwaysStoppedAnimation(Colors.white),
),
),
),
const SizedBox(width: 10),
Text(
'$pct%',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 13,
),
),
],
),
),
);
}
}
class _DoubleTapSeekRipple extends StatelessWidget { class _DoubleTapSeekRipple extends StatelessWidget {
const _DoubleTapSeekRipple({ const _DoubleTapSeekRipple({
required this.isLeft, required this.isLeft,
@@ -1840,7 +1944,7 @@ class _DoubleTapSeekRipple extends StatelessWidget {
} }
} }
class _VideoControls extends StatelessWidget { class _VideoControls extends StatefulWidget {
const _VideoControls({ const _VideoControls({
required this.player, required this.player,
required this.muted, required this.muted,
@@ -1849,6 +1953,8 @@ class _VideoControls extends StatelessWidget {
required this.coverVideo, required this.coverVideo,
required this.fullscreen, required this.fullscreen,
required this.isLocked, required this.isLocked,
this.isLandscape,
this.onToggleOrientation,
required this.onToggleLock, required this.onToggleLock,
required this.onToggleFit, required this.onToggleFit,
required this.onToggleMute, required this.onToggleMute,
@@ -1866,6 +1972,8 @@ class _VideoControls extends StatelessWidget {
final bool coverVideo; final bool coverVideo;
final bool fullscreen; final bool fullscreen;
final bool isLocked; final bool isLocked;
final bool? isLandscape;
final VoidCallback? onToggleOrientation;
final VoidCallback onToggleLock; final VoidCallback onToggleLock;
final VoidCallback onToggleFit; final VoidCallback onToggleFit;
final VoidCallback onToggleMute; final VoidCallback onToggleMute;
@@ -1875,11 +1983,18 @@ class _VideoControls extends StatelessWidget {
final void Function(Duration) onSeekBy; final void Function(Duration) onSeekBy;
final VoidCallback? onInteract; final VoidCallback? onInteract;
@override
State<_VideoControls> createState() => _VideoControlsState();
}
class _VideoControlsState extends State<_VideoControls> {
double? _scrubValue;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
if (isLocked) { if (widget.isLocked) {
return Align( return Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: SafeArea( child: SafeArea(
@@ -1891,7 +2006,7 @@ class _VideoControls extends StatelessWidget {
size: 44, size: 44,
iconSize: 24, iconSize: 24,
emphasized: true, emphasized: true,
onPressed: onToggleLock, onPressed: widget.onToggleLock,
), ),
), ),
), ),
@@ -1899,8 +2014,8 @@ class _VideoControls extends StatelessWidget {
} }
return StreamBuilder<double>( return StreamBuilder<double>(
stream: player.stream.rate, stream: widget.player.stream.rate,
initialData: player.state.rate, initialData: widget.player.state.rate,
builder: (context, rateSnapshot) { builder: (context, rateSnapshot) {
final currentRate = rateSnapshot.data ?? 1.0; final currentRate = rateSnapshot.data ?? 1.0;
@@ -1922,56 +2037,71 @@ class _VideoControls extends StatelessWidget {
), ),
), ),
child: SafeArea( child: SafeArea(
top: fullscreen, top: widget.fullscreen,
bottom: false, bottom: false,
left: fullscreen, left: widget.fullscreen,
right: fullscreen, right: widget.fullscreen,
child: Row( child: Row(
children: [ children: [
if (fullscreen) ...[ if (widget.fullscreen) ...[
_RoundControlButton( _RoundControlButton(
tooltip: 'Закрыть', tooltip: 'Закрыть',
icon: Icons.arrow_back_rounded, icon: Icons.arrow_back_rounded,
onPressed: onFullscreen, onPressed: widget.onFullscreen,
),
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), const SizedBox(width: 8),
], ],
_RoundControlButton( _RoundControlButton(
tooltip: 'Заблокировать экран', tooltip: 'Заблокировать экран',
icon: Icons.lock_outline_rounded, icon: Icons.lock_outline_rounded,
onPressed: onToggleLock, onPressed: widget.onToggleLock,
), ),
const Spacer(), const Spacer(),
_SpeedBadgeButton( _SpeedBadgeButton(
currentRate: currentRate, currentRate: currentRate,
onSelected: (newRate) => player.setRate(newRate), onSelected: (newRate) => widget.player.setRate(newRate),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
_RoundControlButton( _RoundControlButton(
tooltip: loopVideo ? 'Выключить повтор' : 'Повтор видео', tooltip: widget.loopVideo ? 'Выключить повтор' : 'Повтор видео',
selected: loopVideo, selected: widget.loopVideo,
icon: loopVideo icon: widget.loopVideo
? Icons.repeat_one_on_rounded ? Icons.repeat_one_on_rounded
: Icons.repeat_one_rounded, : Icons.repeat_one_rounded,
onPressed: onToggleLoop, onPressed: widget.onToggleLoop,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
_RoundControlButton( _RoundControlButton(
tooltip: coverVideo ? 'Вписать' : 'Заполнить', tooltip: widget.coverVideo ? 'Вписать' : 'Заполнить',
selected: coverVideo, selected: widget.coverVideo,
icon: coverVideo icon: widget.coverVideo
? Icons.fit_screen_rounded ? Icons.fit_screen_rounded
: Icons.crop_free_rounded, : Icons.crop_free_rounded,
onPressed: onToggleFit, onPressed: widget.onToggleFit,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
_RoundControlButton( _RoundControlButton(
tooltip: muted ? 'Включить звук' : 'Выключить звук', tooltip: widget.muted ? 'Включить звук' : 'Выключить звук',
selected: !muted, selected: !widget.muted,
icon: muted icon: widget.muted
? Icons.volume_off_rounded ? Icons.volume_off_rounded
: Icons.volume_up_rounded, : Icons.volume_up_rounded,
onPressed: onToggleMute, onPressed: widget.onToggleMute,
), ),
], ],
), ),
@@ -1982,8 +2112,8 @@ class _VideoControls extends StatelessWidget {
// Center play/pause with quick 10s buttons // Center play/pause with quick 10s buttons
Center( Center(
child: StreamBuilder<bool>( child: StreamBuilder<bool>(
stream: player.stream.playing, stream: widget.player.stream.playing,
initialData: player.state.playing, initialData: widget.player.state.playing,
builder: (context, playingSnapshot) { builder: (context, playingSnapshot) {
final playing = playingSnapshot.data ?? false; final playing = playingSnapshot.data ?? false;
return Row( return Row(
@@ -1995,8 +2125,8 @@ class _VideoControls extends StatelessWidget {
size: 46, size: 46,
iconSize: 26, iconSize: 26,
onPressed: () { onPressed: () {
onSeekBy(const Duration(seconds: -10)); widget.onSeekBy(const Duration(seconds: -10));
onInteract?.call(); widget.onInteract?.call();
}, },
), ),
const SizedBox(width: 32), const SizedBox(width: 32),
@@ -2010,22 +2140,20 @@ class _VideoControls extends StatelessWidget {
emphasized: true, emphasized: true,
onPressed: () async { onPressed: () async {
try { try {
if (player.state.playing) { if (widget.player.state.playing) {
await player.pause(); await widget.player.pause();
} else { } else {
if (player.state.completed) { if (widget.player.state.completed) {
await player.seek(Duration.zero); await widget.player.seek(Duration.zero);
} }
await player.play(); await widget.player.play();
} }
} catch (_) { } catch (_) {
if (player.state.playing) { try {
await player.pause(); await widget.player.playOrPause();
} else { } catch (_) {}
await player.play();
} }
} widget.onInteract?.call();
onInteract?.call();
}, },
), ),
const SizedBox(width: 32), const SizedBox(width: 32),
@@ -2035,8 +2163,8 @@ class _VideoControls extends StatelessWidget {
size: 46, size: 46,
iconSize: 26, iconSize: 26,
onPressed: () { onPressed: () {
onSeekBy(const Duration(seconds: 10)); widget.onSeekBy(const Duration(seconds: 10));
onInteract?.call(); widget.onInteract?.call();
}, },
), ),
], ],
@@ -2055,7 +2183,7 @@ class _VideoControls extends StatelessWidget {
14, 14,
8, 8,
14, 14,
fullscreen ? 10 : 4, widget.fullscreen ? 10 : 4,
), ),
decoration: const BoxDecoration( decoration: const BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
@@ -2066,23 +2194,23 @@ class _VideoControls extends StatelessWidget {
), ),
child: SafeArea( child: SafeArea(
top: false, top: false,
bottom: fullscreen, bottom: widget.fullscreen,
left: fullscreen, left: widget.fullscreen,
right: fullscreen, right: widget.fullscreen,
child: StreamBuilder<Duration>( child: StreamBuilder<Duration>(
stream: player.stream.duration, stream: widget.player.stream.duration,
initialData: player.state.duration, initialData: widget.player.state.duration,
builder: (context, durationSnapshot) { builder: (context, durationSnapshot) {
final duration = durationSnapshot.data ?? Duration.zero; final duration = durationSnapshot.data ?? Duration.zero;
return StreamBuilder<Duration>( return StreamBuilder<Duration>(
stream: player.stream.position, stream: widget.player.stream.position,
initialData: player.state.position, initialData: widget.player.state.position,
builder: (context, positionSnapshot) { builder: (context, positionSnapshot) {
final position = final position =
positionSnapshot.data ?? Duration.zero; positionSnapshot.data ?? Duration.zero;
return StreamBuilder<Duration>( return StreamBuilder<Duration>(
stream: player.stream.buffer, stream: widget.player.stream.buffer,
initialData: player.state.buffer, initialData: widget.player.state.buffer,
builder: (context, bufferSnapshot) { builder: (context, bufferSnapshot) {
final buffer = final buffer =
bufferSnapshot.data ?? Duration.zero; bufferSnapshot.data ?? Duration.zero;
@@ -2095,12 +2223,42 @@ class _VideoControls extends StatelessWidget {
final bufferMs = buffer.inMilliseconds final bufferMs = buffer.inMilliseconds
.clamp(0, maxMs.toInt()) .clamp(0, maxMs.toInt())
.toDouble(); .toDouble();
final displayMs =
(_scrubValue ?? valueMs).clamp(0.0, maxMs);
return Row( return Column(
crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min,
children: [
if (_scrubValue != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
margin: const EdgeInsets.only(bottom: 6),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: scheme.primary,
),
),
child: Text(
'${_format(Duration(milliseconds: displayMs.round()))} / ${_format(duration)}',
style: TextStyle(
color: scheme.primary,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
Row(
crossAxisAlignment:
CrossAxisAlignment.center,
children: [ children: [
Text( Text(
_format(position), _format(Duration(
milliseconds: displayMs.round())),
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 12, fontSize: 12,
@@ -2121,7 +2279,8 @@ class _VideoControls extends StatelessWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: borderRadius:
BorderRadius.circular(2), BorderRadius.circular(2),
child: LinearProgressIndicator( child:
LinearProgressIndicator(
value: (bufferMs / maxMs) value: (bufferMs / maxMs)
.clamp(0.0, 1.0), .clamp(0.0, 1.0),
minHeight: 3.5, minHeight: 3.5,
@@ -2129,15 +2288,17 @@ class _VideoControls extends StatelessWidget {
Colors.transparent, Colors.transparent,
valueColor: valueColor:
AlwaysStoppedAnimation( AlwaysStoppedAnimation(
Colors.white Colors.white.withValues(
.withValues(alpha: 0.28), alpha: 0.28),
), ),
), ),
), ),
), ),
SliderTheme( SliderTheme(
data: SliderTheme.of(context).copyWith( data: SliderTheme.of(context)
activeTrackColor: scheme.primary, .copyWith(
activeTrackColor:
scheme.primary,
inactiveTrackColor: Colors.white inactiveTrackColor: Colors.white
.withValues(alpha: 0.2), .withValues(alpha: 0.2),
trackHeight: 3.5, trackHeight: 3.5,
@@ -2154,16 +2315,34 @@ class _VideoControls extends StatelessWidget {
), ),
), ),
child: Slider( child: Slider(
value: valueMs, value: displayMs,
max: maxMs, max: maxMs,
onChanged: duration == Duration.zero onChangeStart: (value) {
setState(() =>
_scrubValue = value);
widget.onInteract?.call();
},
onChanged: duration ==
Duration.zero
? null ? null
: (value) => player.seek( : (value) {
setState(() =>
_scrubValue =
value);
widget.onInteract
?.call();
},
onChangeEnd: (value) {
widget.player.seek(
Duration( Duration(
milliseconds: milliseconds:
value.round(), value.round(),
), ),
), );
setState(
() => _scrubValue = null);
widget.onInteract?.call();
},
), ),
), ),
], ],
@@ -2173,7 +2352,8 @@ class _VideoControls extends StatelessWidget {
Text( Text(
_format(duration), _format(duration),
style: TextStyle( style: TextStyle(
color: Colors.white.withValues(alpha: 0.72), color: Colors.white
.withValues(alpha: 0.72),
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
letterSpacing: 0.3, letterSpacing: 0.3,
@@ -2181,13 +2361,15 @@ class _VideoControls extends StatelessWidget {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
_RoundControlButton( _RoundControlButton(
tooltip: fullscreen tooltip: widget.fullscreen
? 'Выйти из полноэкранного режима' ? 'Выйти из полноэкранного режима'
: 'Полноэкранный режим', : 'Полноэкранный режим',
icon: fullscreen icon: widget.fullscreen
? Icons.fullscreen_exit_rounded ? Icons.fullscreen_exit_rounded
: Icons.fullscreen_rounded, : Icons.fullscreen_rounded,
onPressed: onFullscreen, onPressed: widget.onFullscreen,
),
],
), ),
], ],
); );
+4 -4
View File
@@ -899,18 +899,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: screen_brightness_android name: screen_brightness_android
sha256: "2df5e0488a22a73271a43d92dbcd80643df6910cf69690faacaf9929ae9ade3b" sha256: "2008ad8e9527cc968f7a4de1ec58b476d495b3c612a149dbd6550c4f046da147"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.6"
screen_brightness_platform_interface: screen_brightness_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: screen_brightness_platform_interface name: screen_brightness_platform_interface
sha256: "59d50850d6735d677780fc7359c8e997d0ff6df91c8465161c9e617a7b0a11d8" sha256: "2de60c0ba569b898950029cc1f7e9dd72bda44a22beb5054aac331cb6fce2ff2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:
+1 -2
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.8+69 version: 3.2.0+71
environment: environment:
sdk: ">=3.3.0 <4.0.0" sdk: ">=3.3.0 <4.0.0"
@@ -33,7 +33,6 @@ dependencies:
media_kit_libs_android_video: ^1.3.8 media_kit_libs_android_video: ^1.3.8
share_plus: ^10.0.0 share_plus: ^10.0.0
dynamic_color: ^1.7.0 dynamic_color: ^1.7.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter