Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a88a8b5050 |
@@ -10,7 +10,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/RarDog/Lunaris/releases"><img src="https://img.shields.io/badge/Version-3.1.9-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.1.9" /></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/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>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
const appDisplayVersion = '3.1.9';
|
||||
const appBuildNumber = 70;
|
||||
const appDisplayVersion = '3.2.0';
|
||||
const appBuildNumber = 71;
|
||||
const appReleaseChannel = 'Stable';
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
const lunarisChangelog = [
|
||||
LunarisChange(
|
||||
version: '3.2.0',
|
||||
title: 'Стабильный полноэкранный режим: сохранение воспроизведения, устранение вылетов плеера и чистый интерфейс',
|
||||
bullets: [
|
||||
'Сохранение воспроизведения видео при повороте экрана: исправлена логика адаптивности на смартфонах — альбомная ориентация больше не сбрасывает мобильный интерфейс и не уничтожает запущенный плеер.',
|
||||
'Защита жизненного цикла Player в фоне: плеер не уничтожается при переходе в полный экран, а кнопки «Пуск/Пауза» и перемотки работают мгновенно и безотказно.',
|
||||
'Чистый кинематографичный интерфейс: удалены старые дублирующиеся кнопки в правом верхнем углу; кнопка смены ориентации (альбомный/портретный режим) аккуратно интегрирована в верхнюю панель управления.',
|
||||
'Быстрое скрытие и показ контролов: касание свободной области видео плавно переключает видимость панели управления, а при нажатии на кнопки таймер автоскрытия автоматически обновляется.',
|
||||
],
|
||||
),
|
||||
LunarisChange(
|
||||
version: '3.1.9',
|
||||
title: 'Полноэкранный плеер 2.0: мгновенный переход без задержек, жесты яркости и скраббинг с превью',
|
||||
|
||||
+14
-4
@@ -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, {
|
||||
|
||||
@@ -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<PostMediaViewer>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!_inFullscreen) {
|
||||
_disposeVideo();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -205,6 +207,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
|
||||
},
|
||||
fullscreen: widget.fullscreen,
|
||||
errorMessage: _videoError,
|
||||
onTapSurface: _toggleControls,
|
||||
onInteract: _showControls,
|
||||
onRetry: _retryVideo,
|
||||
onToggleFit: () {
|
||||
@@ -553,6 +556,16 @@ class _PostMediaViewerState extends State<PostMediaViewer>
|
||||
});
|
||||
}
|
||||
|
||||
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<PostMediaViewer>
|
||||
),
|
||||
),
|
||||
);
|
||||
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);
|
||||
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,9 +892,6 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> {
|
||||
},
|
||||
child: Focus(
|
||||
autofocus: true,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: _VideoSurface(
|
||||
player: widget.player,
|
||||
controller: widget.controller,
|
||||
@@ -879,7 +908,10 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> {
|
||||
},
|
||||
fullscreen: true,
|
||||
errorMessage: widget.errorMessage,
|
||||
onTapSurface: _toggleControls,
|
||||
onInteract: _showControls,
|
||||
isLandscape: _isLandscape,
|
||||
onToggleOrientation: _toggleOrientation,
|
||||
onRetry: widget.onRetry,
|
||||
onToggleFit: () {
|
||||
setState(() => _coverVideo = !_coverVideo);
|
||||
@@ -915,38 +947,6 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> {
|
||||
).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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -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<double>? 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<void> _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);
|
||||
final newB = (_currentBrightness - deltaY * 0.004).clamp(0.08, 1.0);
|
||||
setState(() {
|
||||
_currentBrightness = newB;
|
||||
try {
|
||||
await ScreenBrightness.instance.setApplicationScreenBrightness(newB);
|
||||
} catch (_) {}
|
||||
setState(() => _showBrightnessIndicator = true);
|
||||
_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();
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
+1
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user