mirror of
https://github.com/nini22P/iris.git
synced 2026-09-03 06:24:07 +08:00
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,3 +1,17 @@
|
||||
## v1.5.1
|
||||
|
||||
### Changelog
|
||||
|
||||
* Adjusted the playback control bar style in compact layout
|
||||
* Clicking “Check update” in the Microsoft Store version redirects to the Store page
|
||||
* Fixed forward and backward issues
|
||||
|
||||
### 更新日志
|
||||
|
||||
* 调整了紧凑布局下的播放控制栏样式
|
||||
* 微软商店版本中点击检查更新将跳转至商店页面
|
||||
* 修复了快退快进的问题
|
||||
|
||||
## v1.5.0
|
||||
|
||||
### Changelog
|
||||
|
||||
33
README.md
33
README.md
@@ -2,7 +2,8 @@
|
||||
|
||||
# IRIS - A lightweight video player
|
||||
|
||||

|
||||
[](https://github.com/nini22P/iris/actions/workflows/ci.yml)
|
||||
<a href="https://apps.microsoft.com/detail/9NML7WNHNRTJ?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/en-us%20dark.svg" height="30"/></a>
|
||||
<a href="https://afdian.com/a/nini22P"><img alt="Afdian" style="height: 30px;" src="https://pic1.afdiancdn.com/static/img/welcome/button-sponsorme.png"></a>
|
||||
[](https://ko-fi.com/nini22p)
|
||||
|
||||
@@ -18,18 +19,14 @@ English | [中文](./README_CN.md)
|
||||
|
||||
## Download
|
||||
|
||||
### Windows
|
||||
|
||||
- [IRIS-windows-installer.exe](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows-installer.exe)
|
||||
- [IRIS-windows.zip](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows.zip)
|
||||
|
||||
### Android
|
||||
|
||||
| Architecture | Download Link |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| arm64-v8a | [IRIS-android-arm64-v8a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-arm64-v8a.apk) |
|
||||
| armeabi-v7a | [IRIS-android-armeabi-v7a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-armeabi-v7a.apk) |
|
||||
| x86_64 | [IRIS-android-x86_64.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-x86_64.apk) |
|
||||
| Platform | Arch/Channel | Download Link | Notes |
|
||||
| :------- | :---------------- | :----------------------------------------------------------------------------------------------------------------- | :--------------------- |
|
||||
| Windows | **Microsoft Store** | [Microsoft Store](https://apps.microsoft.com/detail/9NML7WNHNRTJ) | **Recommended**, auto-updates |
|
||||
| | Installer | [IRIS-windows-installer.exe](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows-installer.exe) | |
|
||||
| | Portable | [IRIS-windows.zip](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows.zip) | Unzip and run |
|
||||
| Android | arm64-v8a | [IRIS-android-arm64-v8a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-arm64-v8a.apk) | For 64-bit devices |
|
||||
| | armeabi-v7a | [IRIS-android-armeabi-v7a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-armeabi-v7a.apk) | For 32-bit devices |
|
||||
| | x86_64 | [IRIS-android-x86_64.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-x86_64.apk) | For emulators/x86 devices |
|
||||
|
||||
## Keyboard and Gesture Controls
|
||||
|
||||
@@ -38,8 +35,8 @@ English | [中文](./README_CN.md)
|
||||
| Key | Description |
|
||||
| ---------------------- | -------------------------------------------------- |
|
||||
| `Space` | Play / Pause / Select file |
|
||||
| `Arrow Left` | Fast backward 10 seconds |
|
||||
| `Arrow Right` | Fast forward 10 seconds |
|
||||
| `Arrow Left` | Fast backward |
|
||||
| `Arrow Right` | Fast forward |
|
||||
| `Arrow Up` | Volume up |
|
||||
| `Arrow Down` | Volume down |
|
||||
| `Ctrl + Arrow Left` | Previous |
|
||||
@@ -70,12 +67,12 @@ English | [中文](./README_CN.md)
|
||||
| --------------------------------- | ----------------------------- |
|
||||
| Tap | Select an item or open a menu |
|
||||
| Double tap center | Play / Pause |
|
||||
| Double tap left side | Fast backward 10 seconds |
|
||||
| Double tap right side | Fast forward 10 seconds |
|
||||
| Double tap left side | Fast backward |
|
||||
| Double tap right side | Fast forward |
|
||||
| Swipe left / right | Adjust playback progress |
|
||||
| Swipe up / down on left side | Adjust screen brightness |
|
||||
| Swipe up / down on right side | Adjust device volume |
|
||||
| Long press | Start speed playback |
|
||||
| Long press | Display Playback Speed Selector |
|
||||
| Long press and swipe left / right | Adjust speed playback speed |
|
||||
|
||||
## Contribution
|
||||
|
||||
35
README_CN.md
35
README_CN.md
@@ -2,7 +2,8 @@
|
||||
|
||||
# IRIS - 轻量级视频播放器
|
||||
|
||||

|
||||
[](https://github.com/nini22P/iris/actions/workflows/ci.yml)
|
||||
<a href="https://apps.microsoft.com/detail/9NML7WNHNRTJ?referrer=appbadge&mode=direct"><img src="https://get.microsoft.com/images/zh-cn%20dark.svg" height="30"/></a>
|
||||
<a href="https://afdian.com/a/nini22P"><img alt="爱发电" style="height: 30px;" src="https://pic1.afdiancdn.com/static/img/welcome/button-sponsorme.png"></a>
|
||||
[](https://ko-fi.com/nini22p)
|
||||
|
||||
@@ -18,18 +19,14 @@
|
||||
|
||||
## 下载
|
||||
|
||||
### Windows
|
||||
|
||||
- [IRIS-windows-installer.exe](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows-installer.exe)
|
||||
- [IRIS-windows.zip](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows.zip)
|
||||
|
||||
### Android
|
||||
|
||||
| 设备架构 | 下载链接 |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| arm64-v8a | [IRIS-android-arm64-v8a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-arm64-v8a.apk) |
|
||||
| armeabi-v7a | [IRIS-android-armeabi-v7a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-armeabi-v7a.apk) |
|
||||
| x86_64 | [IRIS-android-x86_64.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-x86_64.apk) |
|
||||
| 平台 | 架构/渠道 | 下载链接 | 备注 |
|
||||
| :------ | :------------ | :----------------------------------------------------------------------------------------------------------------- | :--------------- |
|
||||
| Windows | **微软商店** | [Microsoft Store](https://apps.microsoft.com/detail/9NML7WNHNRTJ) | **推荐**,自动更新 |
|
||||
| | 安装包 | [IRIS-windows-installer.exe](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows-installer.exe) | |
|
||||
| | 便携版 | [IRIS-windows.zip](https://github.com/nini22P/iris/releases/latest/download/IRIS-windows.zip) | 解压即用 |
|
||||
| Android | arm64-v8a | [IRIS-android-arm64-v8a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-arm64-v8a.apk) | 64位设备 |
|
||||
| | armeabi-v7a | [IRIS-android-armeabi-v7a.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-armeabi-v7a.apk) | 32位设备 |
|
||||
| | x86_64 | [IRIS-android-x86_64.apk](https://github.com/nini22P/iris/releases/latest/download/IRIS-android-x86_64.apk) | 模拟器/x86设备 |
|
||||
|
||||
## 键位和手势
|
||||
|
||||
@@ -38,8 +35,8 @@
|
||||
| 键位 | 描述 |
|
||||
| ---------------------- | ------------------------------------ |
|
||||
| `Space` | 播放 / 暂停 / 选择文件 |
|
||||
| `Arrow Left` | 快退 10 秒 |
|
||||
| `Arrow Right` | 快进 10 秒 |
|
||||
| `Arrow Left` | 快退 |
|
||||
| `Arrow Right` | 快进 |
|
||||
| `Arrow Up` | 提升音量 |
|
||||
| `Arrow Down` | 降低音量 |
|
||||
| `Ctrl + Arrow Left` | 上一个 |
|
||||
@@ -70,13 +67,13 @@
|
||||
| ---------------- | ------------------ |
|
||||
| 单击 | 选择项目或打开菜单 |
|
||||
| 双击屏幕中心 | 播放 / 暂停 |
|
||||
| 双击屏幕左侧 | 快退 10 秒 |
|
||||
| 双击屏幕右侧 | 快进 10 秒 |
|
||||
| 双击屏幕左侧 | 快退 |
|
||||
| 双击屏幕右侧 | 快进 |
|
||||
| 左右滑动 | 调整播放进度 |
|
||||
| 屏幕左侧上下滑动 | 调整屏幕亮度 |
|
||||
| 屏幕右侧上下滑动 | 调整设备音量 |
|
||||
| 长按 | 启动倍速播放 |
|
||||
| 长按后左右滑动 | 调整倍速播放的速度 |
|
||||
| 长按 | 显示播放速度选择器 |
|
||||
| 长按后左右滑动 | 调整播放速度 |
|
||||
|
||||
## 贡献
|
||||
|
||||
|
||||
2
inno.iss
2
inno.iss
@@ -2,7 +2,7 @@
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppName "IRIS"
|
||||
#define MyAppVersion "1.5.0"
|
||||
#define MyAppVersion "1.5.1"
|
||||
#define MyAppPublisher "nini22P"
|
||||
#define MyAppURL "https://github.com/nini22P/iris"
|
||||
#define MyAppExeName "iris.exe"
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:provider/provider.dart';
|
||||
|
||||
void useAppLifecycle() {
|
||||
final context = useContext();
|
||||
final saveProgress = context.read<MediaPlayer>().saveProgress;
|
||||
|
||||
AppLifecycleState? appLifecycleState = useAppLifecycleState();
|
||||
|
||||
@@ -14,7 +13,7 @@ void useAppLifecycle() {
|
||||
try {
|
||||
if (appLifecycleState == AppLifecycleState.paused) {
|
||||
logger('App lifecycle state: paused');
|
||||
saveProgress();
|
||||
context.read<MediaPlayer>().saveProgress();
|
||||
}
|
||||
} catch (e) {
|
||||
logger('App lifecycle state error: $e');
|
||||
|
||||
@@ -15,15 +15,11 @@ FileItem? useCover() {
|
||||
final currentIndex =
|
||||
usePlayQueueStore().select(context, (state) => state.currentIndex);
|
||||
|
||||
final int currentPlayIndex = useMemoized(
|
||||
() => playQueue.indexWhere((element) => element.index == currentIndex),
|
||||
[playQueue, currentIndex]);
|
||||
|
||||
final FileItem? file = useMemoized(
|
||||
() => playQueue.isEmpty || currentPlayIndex < 0
|
||||
? null
|
||||
: playQueue[currentPlayIndex].file,
|
||||
[playQueue, currentPlayIndex]);
|
||||
final FileItem? file = useMemoized(() {
|
||||
final index =
|
||||
playQueue.indexWhere((element) => element.index == currentIndex);
|
||||
return playQueue.isEmpty || index < 0 ? null : playQueue[index].file;
|
||||
}, [playQueue, currentIndex]);
|
||||
|
||||
final localStoragesFuture =
|
||||
useMemoized(() async => await getLocalStorages(context), []);
|
||||
@@ -52,9 +48,8 @@ FileItem? useCover() {
|
||||
|
||||
final files = await storage.getFiles(dir);
|
||||
|
||||
final images = files
|
||||
.where((file) => [ContentType.image].contains(file.type))
|
||||
.toList();
|
||||
final images =
|
||||
files.where((file) => file.type == ContentType.image).toList();
|
||||
|
||||
cover.value = images.firstWhereOrNull((image) =>
|
||||
image.name.split('.').first.toLowerCase() == 'cover') ??
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
|
||||
import 'package:iris/globals.dart' show speedStops, speedSelectorItemWidth;
|
||||
import 'package:iris/hooks/use_brightness.dart';
|
||||
import 'package:iris/hooks/use_volume.dart';
|
||||
@@ -31,7 +32,6 @@ class Gesture {
|
||||
final bool isRightGesture;
|
||||
final double? brightness;
|
||||
final double? volume;
|
||||
final MouseCursor cursor;
|
||||
|
||||
Gesture({
|
||||
required this.onTapDown,
|
||||
@@ -51,7 +51,6 @@ class Gesture {
|
||||
required this.isRightGesture,
|
||||
required this.brightness,
|
||||
required this.volume,
|
||||
required this.cursor,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,8 +64,6 @@ Gesture useGesture({
|
||||
}) {
|
||||
final context = useContext();
|
||||
|
||||
final player = context.read<MediaPlayer>();
|
||||
|
||||
final gestureState = useRef({
|
||||
'isTouch': false,
|
||||
'isLongPress': false,
|
||||
@@ -97,20 +94,22 @@ Gesture useGesture({
|
||||
}
|
||||
|
||||
void onDoubleTapDown(TapDownDetails details) {
|
||||
final player = context.read<MediaPlayer>();
|
||||
|
||||
if (details.kind == PointerDeviceKind.touch) {
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
final tapDx = details.globalPosition.dx;
|
||||
|
||||
if (tapDx > screenWidth * 0.7) {
|
||||
// 右侧 30%
|
||||
if (tapDx > screenWidth * 0.75) {
|
||||
// 右侧 25%
|
||||
showProgress();
|
||||
player.forward(10);
|
||||
} else if (tapDx < screenWidth * 0.3) {
|
||||
// 左侧 30%
|
||||
} else if (tapDx < screenWidth * 0.25) {
|
||||
// 左侧 25%
|
||||
showProgress();
|
||||
player.backward(10);
|
||||
} else {
|
||||
// 中间 40%
|
||||
// 中间 50%
|
||||
if (player.isPlaying) {
|
||||
useAppStore().updateAutoPlay(false);
|
||||
player.pause();
|
||||
@@ -128,7 +127,8 @@ Gesture useGesture({
|
||||
}
|
||||
|
||||
void onLongPressStart(LongPressStartDetails details) {
|
||||
if (gestureState.value['isTouch'] as bool && player.isPlaying) {
|
||||
if (gestureState.value['isTouch'] as bool &&
|
||||
context.read<MediaPlayer>().isPlaying) {
|
||||
gestureState.value['isLongPress'] = true;
|
||||
gestureState.value['startPanOffset'] = details.globalPosition;
|
||||
|
||||
@@ -206,7 +206,8 @@ Gesture useGesture({
|
||||
gestureState.value['isTouch'] = true;
|
||||
gestureState.value['isDragging'] = true;
|
||||
gestureState.value['startPanOffset'] = details.globalPosition;
|
||||
gestureState.value['startSeekPosition'] = player.position;
|
||||
gestureState.value['startSeekPosition'] =
|
||||
context.read<MediaPlayer>().position;
|
||||
gestureState.value['panDirection'] = null;
|
||||
isLeftGesture.value = false;
|
||||
isRightGesture.value = false;
|
||||
@@ -246,9 +247,10 @@ Gesture useGesture({
|
||||
int targetSeconds = (startSeconds + seekSecondsOffset).round();
|
||||
|
||||
// 边界检查
|
||||
targetSeconds = targetSeconds.clamp(0, player.duration.inSeconds);
|
||||
targetSeconds = targetSeconds.clamp(
|
||||
0, context.read<MediaPlayer>().duration.inSeconds);
|
||||
|
||||
player.seek(Duration(seconds: targetSeconds));
|
||||
context.read<MediaPlayer>().seek(Duration(seconds: targetSeconds));
|
||||
showProgress();
|
||||
}
|
||||
|
||||
@@ -259,6 +261,10 @@ Gesture useGesture({
|
||||
isLeftGesture.value =
|
||||
startOffset.dx < MediaQuery.sizeOf(context).width / 2;
|
||||
isRightGesture.value = !isLeftGesture.value;
|
||||
|
||||
if (isRightGesture.value) {
|
||||
FlutterVolumeController.updateShowSystemUI(false);
|
||||
}
|
||||
}
|
||||
|
||||
final double dy = details.delta.dy;
|
||||
@@ -287,6 +293,8 @@ Gesture useGesture({
|
||||
};
|
||||
isLeftGesture.value = false;
|
||||
isRightGesture.value = false;
|
||||
|
||||
FlutterVolumeController.updateShowSystemUI(true);
|
||||
}
|
||||
|
||||
void onPanEnd(DragEndDetails details) => _resetPanState();
|
||||
@@ -299,12 +307,6 @@ Gesture useGesture({
|
||||
}
|
||||
}
|
||||
|
||||
final cursor = useMemoized(() {
|
||||
return player.isPlaying == false
|
||||
? SystemMouseCursors.basic
|
||||
: SystemMouseCursors.none;
|
||||
}, [player.isPlaying]);
|
||||
|
||||
return Gesture(
|
||||
onTapDown: onTapDown,
|
||||
onTap: onTap,
|
||||
@@ -323,6 +325,5 @@ Gesture useGesture({
|
||||
isRightGesture: isRightGesture.value,
|
||||
brightness: brightness.value,
|
||||
volume: volume.value,
|
||||
cursor: cursor,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:iris/globals.dart';
|
||||
import 'package:iris/models/player.dart';
|
||||
import 'package:iris/models/storages/local.dart';
|
||||
import 'package:iris/pages/player/overlays/history.dart';
|
||||
import 'package:iris/pages/player/overlays/play_queue.dart';
|
||||
import 'package:iris/pages/player/overlays/track/subtitle_and_audio_track.dart';
|
||||
import 'package:iris/pages/settings/settings.dart';
|
||||
import 'package:iris/pages/storages/storages.dart';
|
||||
import 'package:iris/widgets/popups/history.dart';
|
||||
import 'package:iris/widgets/popups/play_queue.dart';
|
||||
import 'package:iris/widgets/popups/track/subtitle_and_audio_track.dart';
|
||||
import 'package:iris/widgets/popups/settings/settings.dart';
|
||||
import 'package:iris/widgets/popups/storages/storages.dart';
|
||||
import 'package:iris/store/use_app_store.dart';
|
||||
import 'package:iris/store/use_play_queue_store.dart';
|
||||
import 'package:iris/store/use_player_ui_store.dart';
|
||||
@@ -28,9 +28,9 @@ KeyboardEvent useKeyboard({
|
||||
}) {
|
||||
final context = useContext();
|
||||
|
||||
final player = context.read<MediaPlayer>();
|
||||
|
||||
void onKeyEvent(KeyEvent event) async {
|
||||
final player = context.read<MediaPlayer>();
|
||||
|
||||
if (event.runtimeType == KeyDownEvent) {
|
||||
if (HardwareKeyboard.instance.isAltPressed) {
|
||||
switch (event.logicalKey) {
|
||||
@@ -138,7 +138,7 @@ KeyboardEvent useKeyboard({
|
||||
case LogicalKeyboardKey.space:
|
||||
case LogicalKeyboardKey.mediaPlayPause:
|
||||
showControl();
|
||||
if (context.read<MediaPlayer>().isPlaying) {
|
||||
if (player.isPlaying) {
|
||||
useAppStore().updateAutoPlay(false);
|
||||
player.pause();
|
||||
} else {
|
||||
|
||||
@@ -10,7 +10,6 @@ ValueNotifier<double?> useVolume(bool isGesture) {
|
||||
try {
|
||||
() async {
|
||||
if (!isGesture) return;
|
||||
await FlutterVolumeController.updateShowSystemUI(false);
|
||||
volume.value = await FlutterVolumeController.getVolume();
|
||||
}();
|
||||
} catch (e) {
|
||||
@@ -18,7 +17,6 @@ ValueNotifier<double?> useVolume(bool isGesture) {
|
||||
}
|
||||
return () {
|
||||
volume.value = null;
|
||||
FlutterVolumeController.updateShowSystemUI(true);
|
||||
};
|
||||
}, [isGesture]);
|
||||
|
||||
|
||||
@@ -3,4 +3,5 @@ class INFO {
|
||||
static const String author = '22';
|
||||
static const String authorUrl = 'https://github.com/nini22P';
|
||||
static const String githubUrl = 'https://github.com/nini22P/iris';
|
||||
static const String msStoreId = '9NML7WNHNRTJ';
|
||||
}
|
||||
|
||||
@@ -49,28 +49,25 @@ Future<List<FileItem>> getFTPFiles(
|
||||
List<FileItem> fileItems = [];
|
||||
|
||||
for (final file in files) {
|
||||
if (file.isDirectory || isMediaFile(file.name)) {
|
||||
final basename = p.basenameWithoutExtension(file.name).split('.').first;
|
||||
fileItems.add(
|
||||
FileItem(
|
||||
storageId: storage.id,
|
||||
storageType: StorageType.ftp,
|
||||
name: file.name,
|
||||
uri: getUri(file.name),
|
||||
path: [...path, file.name],
|
||||
isDir: file.isDirectory,
|
||||
size: file.isDirectory ? 0 : file.info?.size ?? 0,
|
||||
lastModified: file.info?.modifyTime != null
|
||||
? DateTime.tryParse(file.info!.modifyTime!)
|
||||
: null,
|
||||
type: file.isDirectory
|
||||
? ContentType.other
|
||||
: checkContentType(file.name),
|
||||
subtitles:
|
||||
isVideoFile(file.name) ? subtitleMap[basename] ?? [] : [],
|
||||
),
|
||||
);
|
||||
}
|
||||
final basename = p.basenameWithoutExtension(file.name).split('.').first;
|
||||
fileItems.add(
|
||||
FileItem(
|
||||
storageId: storage.id,
|
||||
storageType: StorageType.ftp,
|
||||
name: file.name,
|
||||
uri: getUri(file.name),
|
||||
path: [...path, file.name],
|
||||
isDir: file.isDirectory,
|
||||
size: file.isDirectory ? 0 : file.info?.size ?? 0,
|
||||
lastModified: file.info?.modifyTime != null
|
||||
? DateTime.tryParse(file.info!.modifyTime!)
|
||||
: null,
|
||||
type: file.isDirectory
|
||||
? ContentType.other
|
||||
: checkContentType(file.name),
|
||||
subtitles: isVideoFile(file.name) ? subtitleMap[basename] ?? [] : [],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return fileItems;
|
||||
|
||||
@@ -310,19 +310,17 @@ Future<List<FileItem>> getContentFiles(String uri) async {
|
||||
List<FileItem> fileItems = [];
|
||||
|
||||
for (final file in files) {
|
||||
if (file.isDir || isMediaFile(file.name)) {
|
||||
final basename = p.basenameWithoutExtension(file.name).split('.').first;
|
||||
fileItems.add(FileItem(
|
||||
name: file.name,
|
||||
uri: file.uri,
|
||||
path: [uri, file.name],
|
||||
isDir: file.isDir,
|
||||
size: file.isDir ? 0 : file.length,
|
||||
lastModified: DateTime.fromMillisecondsSinceEpoch(file.lastModified),
|
||||
type: file.isDir ? ContentType.other : checkContentType(file.name),
|
||||
subtitles: isVideoFile(file.name) ? subtitleMap[basename] ?? [] : [],
|
||||
));
|
||||
}
|
||||
final basename = p.basenameWithoutExtension(file.name).split('.').first;
|
||||
fileItems.add(FileItem(
|
||||
name: file.name,
|
||||
uri: file.uri,
|
||||
path: [uri, file.name],
|
||||
isDir: file.isDir,
|
||||
size: file.isDir ? 0 : file.length,
|
||||
lastModified: DateTime.fromMillisecondsSinceEpoch(file.lastModified),
|
||||
type: file.isDir ? ContentType.other : checkContentType(file.name),
|
||||
subtitles: isVideoFile(file.name) ? subtitleMap[basename] ?? [] : [],
|
||||
));
|
||||
}
|
||||
|
||||
return fileItems;
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:iris/models/storages/local.dart';
|
||||
import 'package:iris/models/storages/webdav.dart';
|
||||
import 'package:iris/utils/platform.dart';
|
||||
import 'package:iris/widgets/popup.dart';
|
||||
import 'package:iris/pages/storages/storages.dart';
|
||||
import 'package:iris/widgets/popups/storages/storages.dart';
|
||||
import 'package:iris/store/use_storage_store.dart';
|
||||
|
||||
part 'storage.freezed.dart';
|
||||
|
||||
@@ -96,21 +96,19 @@ Future<List<FileItem>> getWebDAVFiles(
|
||||
if (fileName == null) continue;
|
||||
|
||||
final isDir = file.isDir;
|
||||
if (isDir == true || isMediaFile(fileName)) {
|
||||
final basename = p.basenameWithoutExtension(fileName).split('.').first;
|
||||
fileItems.add(FileItem(
|
||||
storageId: id,
|
||||
storageType: StorageType.webdav,
|
||||
name: fileName,
|
||||
uri: getUri(fileName),
|
||||
path: [...path, fileName],
|
||||
isDir: isDir ?? false,
|
||||
size: file.size ?? 0,
|
||||
lastModified: file.mTime,
|
||||
type: isDir ?? false ? ContentType.other : checkContentType(fileName),
|
||||
subtitles: isVideoFile(fileName) ? subtitleMap[basename] ?? [] : [],
|
||||
));
|
||||
}
|
||||
final basename = p.basenameWithoutExtension(fileName).split('.').first;
|
||||
fileItems.add(FileItem(
|
||||
storageId: id,
|
||||
storageType: StorageType.webdav,
|
||||
name: fileName,
|
||||
uri: getUri(fileName),
|
||||
path: [...path, fileName],
|
||||
isDir: isDir ?? false,
|
||||
size: file.size ?? 0,
|
||||
lastModified: file.mTime,
|
||||
type: isDir ?? false ? ContentType.other : checkContentType(fileName),
|
||||
subtitles: isVideoFile(fileName) ? subtitleMap[basename] ?? [] : [],
|
||||
));
|
||||
}
|
||||
|
||||
return fileItems;
|
||||
|
||||
@@ -58,6 +58,11 @@ class Audio extends HookWidget {
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
if (cover != null)
|
||||
_CoverImage(cover: cover!, auth: auth, fit: BoxFit.cover),
|
||||
BackdropFilter(
|
||||
@@ -110,7 +115,7 @@ class Audio extends HookWidget {
|
||||
) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(48, 56, 48, 96),
|
||||
padding: const EdgeInsets.fromLTRB(48, 56, 48, 144),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 400.0,
|
||||
@@ -148,7 +153,11 @@ class Audio extends HookWidget {
|
||||
48,
|
||||
56,
|
||||
24,
|
||||
constraints.maxWidth > 1024 ? 64 : 96,
|
||||
constraints.maxWidth > 1024
|
||||
? 64
|
||||
: constraints.maxWidth > 640
|
||||
? 96
|
||||
: 144,
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,12 @@ import 'package:provider/provider.dart';
|
||||
class ControlBarSlider extends HookWidget {
|
||||
const ControlBarSlider({
|
||||
super.key,
|
||||
required this.showControl,
|
||||
this.showControl,
|
||||
this.disabled = false,
|
||||
this.color,
|
||||
});
|
||||
|
||||
final void Function() showControl;
|
||||
final void Function()? showControl;
|
||||
final bool disabled;
|
||||
final Color? color;
|
||||
|
||||
@@ -92,7 +92,7 @@ class ControlBarSlider extends HookWidget {
|
||||
onChanged: disabled
|
||||
? null
|
||||
: (value) {
|
||||
showControl();
|
||||
showControl?.call();
|
||||
seek(Duration(milliseconds: value.toInt()));
|
||||
},
|
||||
onChangeStart: disabled
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_zustand/flutter_zustand.dart';
|
||||
import 'package:iris/globals.dart' show speedStops, speedSelectorItemWidth;
|
||||
import 'package:iris/hooks/use_gesture.dart';
|
||||
import 'package:iris/models/file.dart';
|
||||
import 'package:iris/models/player.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:iris/pages/player/control_bar/control_bar.dart';
|
||||
import 'package:iris/pages/player/control_bar/control_bar_slider.dart';
|
||||
import 'package:iris/pages/player/overlays/speed_selector.dart';
|
||||
import 'package:iris/store/use_app_store.dart';
|
||||
import 'package:iris/store/use_player_ui_store.dart';
|
||||
import 'package:iris/utils/format_duration_to_minutes.dart';
|
||||
import 'package:iris/widgets/drag_area.dart';
|
||||
import 'package:iris/widgets/title_bar.dart';
|
||||
import 'package:iris/pages/player/title_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ControlsOverlay extends HookWidget {
|
||||
@@ -36,65 +32,10 @@ class ControlsOverlay extends HookWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPlaying =
|
||||
context.select<MediaPlayer, bool>((player) => player.isPlaying);
|
||||
|
||||
final progress =
|
||||
context.select<MediaPlayer, ({Duration position, Duration duration})>(
|
||||
(player) => (position: player.position, duration: player.duration),
|
||||
);
|
||||
|
||||
final saveProgress = context.read<MediaPlayer>().saveProgress;
|
||||
|
||||
final isShowControl =
|
||||
usePlayerUiStore().select(context, (state) => state.isShowControl);
|
||||
final isShowProgress =
|
||||
usePlayerUiStore().select(context, (state) => state.isShowProgress);
|
||||
|
||||
final isSpeedSelectorVisible = useState(false);
|
||||
final selectedSpeed = useState(1.0);
|
||||
final speedSelectorPosition = useState(Offset.zero);
|
||||
final visualOffset = useState(0.0);
|
||||
final initialSpeed = useRef(1.0);
|
||||
|
||||
void showSpeedSelectorCallback(Offset position) {
|
||||
isSpeedSelectorVisible.value = true;
|
||||
speedSelectorPosition.value = position;
|
||||
visualOffset.value = 0.0;
|
||||
initialSpeed.value = useAppStore().state.rate;
|
||||
}
|
||||
|
||||
void hideSpeedSelectorCallback(double finalSpeed) {
|
||||
final initialIndex = speedStops.indexOf(initialSpeed.value);
|
||||
final finalIndex = speedStops.indexOf(finalSpeed);
|
||||
|
||||
if (initialIndex == -1 || finalIndex == -1) return;
|
||||
|
||||
visualOffset.value = (initialIndex - finalIndex) * speedSelectorItemWidth;
|
||||
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 200),
|
||||
() {
|
||||
if (context.mounted) {
|
||||
isSpeedSelectorVisible.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void updateSelectedSpeedCallback(double speed, double newVisualOffset) {
|
||||
selectedSpeed.value = speed;
|
||||
visualOffset.value = newVisualOffset;
|
||||
}
|
||||
|
||||
final gesture = useGesture(
|
||||
showControl: showControl,
|
||||
hideControl: hideControl,
|
||||
showProgress: showProgress,
|
||||
showSpeedSelector: showSpeedSelectorCallback,
|
||||
hideSpeedSelector: hideSpeedSelectorCallback,
|
||||
updateSelectedSpeed: updateSelectedSpeedCallback,
|
||||
);
|
||||
|
||||
final contentColor = useMemoized(
|
||||
() => Theme.of(context).brightness == Brightness.dark
|
||||
@@ -114,189 +55,15 @@ class ControlsOverlay extends HookWidget {
|
||||
}),
|
||||
[contentColor]);
|
||||
|
||||
void onHover(PointerHoverEvent event) {
|
||||
if (event.kind != PointerDeviceKind.touch) {
|
||||
usePlayerUiStore().updateIsHovering(true);
|
||||
showControl();
|
||||
}
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: MouseRegion(
|
||||
cursor: isShowControl || isPlaying == false
|
||||
? SystemMouseCursors.basic
|
||||
: SystemMouseCursors.none,
|
||||
onHover: gesture.onHover,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: gesture.onTap,
|
||||
onTapDown: gesture.onTapDown,
|
||||
onDoubleTapDown: gesture.onDoubleTapDown,
|
||||
onLongPressStart: gesture.onLongPressStart,
|
||||
onLongPressMoveUpdate: gesture.onLongPressMoveUpdate,
|
||||
onLongPressEnd: gesture.onLongPressEnd,
|
||||
onLongPressCancel: gesture.onLongPressCancel,
|
||||
onPanStart: gesture.onPanStart,
|
||||
onPanUpdate: gesture.onPanUpdate,
|
||||
onPanEnd: gesture.onPanEnd,
|
||||
onPanCancel: gesture.onPanCancel,
|
||||
child: Stack(
|
||||
children: [
|
||||
// 播放速度
|
||||
if (isSpeedSelectorVisible.value)
|
||||
Positioned.fill(
|
||||
child: SpeedSelector(
|
||||
selectedSpeed: selectedSpeed.value,
|
||||
visualOffset: visualOffset.value,
|
||||
initialSpeed: initialSpeed.value,
|
||||
),
|
||||
),
|
||||
// 屏幕亮度
|
||||
if (gesture.isLeftGesture && gesture.brightness != null)
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 18, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
gesture.brightness == 0
|
||||
? Icons.brightness_low_rounded
|
||||
: gesture.brightness! < 1
|
||||
? Icons.brightness_medium_rounded
|
||||
: Icons.brightness_high_rounded,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: LinearProgressIndicator(
|
||||
value: gesture.brightness,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
backgroundColor: Colors.grey,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 音量
|
||||
if (gesture.isRightGesture && gesture.volume != null)
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 18, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
gesture.volume == 0
|
||||
? Icons.volume_mute_rounded
|
||||
: gesture.volume! < 0.5
|
||||
? Icons.volume_down_rounded
|
||||
: Icons.volume_up_rounded,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: LinearProgressIndicator(
|
||||
value: gesture.volume,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
backgroundColor: Colors.grey,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isShowProgress &&
|
||||
!isShowControl &&
|
||||
file?.type == ContentType.video)
|
||||
Positioned(
|
||||
left: -28,
|
||||
right: -28,
|
||||
bottom: -16,
|
||||
height: 32,
|
||||
child: ControlBarSlider(
|
||||
showControl: showControl,
|
||||
disabled: true,
|
||||
),
|
||||
),
|
||||
if (isShowProgress &&
|
||||
!isShowControl &&
|
||||
file?.type == ContentType.video)
|
||||
Positioned(
|
||||
left: 12,
|
||||
top: 12,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
file != null ? title : '',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
shadows: const [
|
||||
Shadow(
|
||||
color: Colors.black,
|
||||
offset: Offset(0, 0),
|
||||
blurRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isShowProgress &&
|
||||
!isShowControl &&
|
||||
file?.type == ContentType.video)
|
||||
Positioned(
|
||||
left: 12,
|
||||
bottom: 6,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${formatDurationToMinutes(progress.position)} / ${formatDurationToMinutes(progress.duration)}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
height: 2,
|
||||
decoration: TextDecoration.none,
|
||||
shadows: const [
|
||||
Shadow(
|
||||
color: Colors.black,
|
||||
offset: Offset(0, 0),
|
||||
blurRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 标题栏
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -305,7 +72,7 @@ class ControlsOverlay extends HookWidget {
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: MouseRegion(
|
||||
onHover: gesture.onHover,
|
||||
onHover: onHover,
|
||||
child: GestureDetector(
|
||||
onTap: () => showControl(),
|
||||
child: DragArea(
|
||||
@@ -330,7 +97,7 @@ class ControlsOverlay extends HookWidget {
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: MouseRegion(
|
||||
onHover: gesture.onHover,
|
||||
onHover: onHover,
|
||||
child: GestureDetector(
|
||||
onTap: () => showControl(),
|
||||
child: ControlBar(
|
||||
|
||||
193
lib/pages/player/overlays/gesture_overlay.dart
Normal file
193
lib/pages/player/overlays/gesture_overlay.dart
Normal file
@@ -0,0 +1,193 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_zustand/flutter_zustand.dart';
|
||||
import 'package:iris/globals.dart';
|
||||
import 'package:iris/hooks/use_gesture.dart';
|
||||
import 'package:iris/models/player.dart';
|
||||
import 'package:iris/pages/player/overlays/speed_selector.dart';
|
||||
import 'package:iris/store/use_app_store.dart';
|
||||
import 'package:iris/store/use_player_ui_store.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class GestureOverlay extends HookWidget {
|
||||
const GestureOverlay({
|
||||
super.key,
|
||||
required this.showControl,
|
||||
required this.hideControl,
|
||||
required this.showProgress,
|
||||
});
|
||||
|
||||
final Function() showControl;
|
||||
final Function() hideControl;
|
||||
final Function() showProgress;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPlaying =
|
||||
context.select<MediaPlayer, bool>((player) => player.isPlaying);
|
||||
|
||||
final isShowControl =
|
||||
usePlayerUiStore().select(context, (state) => state.isShowControl);
|
||||
|
||||
final cursor = useMemoized(
|
||||
() => isShowControl || !isPlaying
|
||||
? SystemMouseCursors.basic
|
||||
: SystemMouseCursors.none,
|
||||
[isShowControl, isPlaying]);
|
||||
|
||||
final isSpeedSelectorVisible = useState(false);
|
||||
final selectedSpeed = useState(1.0);
|
||||
final speedSelectorPosition = useState(Offset.zero);
|
||||
final visualOffset = useState(0.0);
|
||||
final initialSpeed = useRef(1.0);
|
||||
|
||||
void showSpeedSelectorCallback(Offset position) {
|
||||
isSpeedSelectorVisible.value = true;
|
||||
speedSelectorPosition.value = position;
|
||||
visualOffset.value = 0.0;
|
||||
initialSpeed.value = useAppStore().state.rate;
|
||||
}
|
||||
|
||||
void hideSpeedSelectorCallback(double finalSpeed) {
|
||||
final initialIndex = speedStops.indexOf(initialSpeed.value);
|
||||
final finalIndex = speedStops.indexOf(finalSpeed);
|
||||
|
||||
if (initialIndex == -1 || finalIndex == -1) return;
|
||||
|
||||
visualOffset.value = (initialIndex - finalIndex) * speedSelectorItemWidth;
|
||||
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 200),
|
||||
() {
|
||||
if (context.mounted) {
|
||||
isSpeedSelectorVisible.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void updateSelectedSpeedCallback(double speed, double newVisualOffset) {
|
||||
selectedSpeed.value = speed;
|
||||
visualOffset.value = newVisualOffset;
|
||||
}
|
||||
|
||||
final gesture = useGesture(
|
||||
showControl: showControl,
|
||||
hideControl: hideControl,
|
||||
showProgress: showProgress,
|
||||
showSpeedSelector: showSpeedSelectorCallback,
|
||||
hideSpeedSelector: hideSpeedSelectorCallback,
|
||||
updateSelectedSpeed: updateSelectedSpeedCallback,
|
||||
);
|
||||
|
||||
return MouseRegion(
|
||||
cursor: cursor,
|
||||
onHover: gesture.onHover,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: gesture.onTap,
|
||||
onTapDown: gesture.onTapDown,
|
||||
onDoubleTapDown: gesture.onDoubleTapDown,
|
||||
onLongPressStart: gesture.onLongPressStart,
|
||||
onLongPressMoveUpdate: gesture.onLongPressMoveUpdate,
|
||||
onLongPressEnd: gesture.onLongPressEnd,
|
||||
onLongPressCancel: gesture.onLongPressCancel,
|
||||
onPanStart: gesture.onPanStart,
|
||||
onPanUpdate: gesture.onPanUpdate,
|
||||
onPanEnd: gesture.onPanEnd,
|
||||
onPanCancel: gesture.onPanCancel,
|
||||
child: Stack(
|
||||
children: [
|
||||
// 播放速度
|
||||
if (isSpeedSelectorVisible.value)
|
||||
Positioned.fill(
|
||||
child: SpeedSelector(
|
||||
selectedSpeed: selectedSpeed.value,
|
||||
visualOffset: visualOffset.value,
|
||||
initialSpeed: initialSpeed.value,
|
||||
),
|
||||
),
|
||||
|
||||
// 屏幕亮度
|
||||
if (gesture.isLeftGesture && gesture.brightness != null)
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 18, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
gesture.brightness == 0
|
||||
? Icons.brightness_low_rounded
|
||||
: gesture.brightness! < 1
|
||||
? Icons.brightness_medium_rounded
|
||||
: Icons.brightness_high_rounded,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: LinearProgressIndicator(
|
||||
value: gesture.brightness,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
backgroundColor: Colors.grey,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||
Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 音量
|
||||
if (gesture.isRightGesture && gesture.volume != null)
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 18, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
gesture.volume == 0
|
||||
? Icons.volume_mute_rounded
|
||||
: gesture.volume! < 0.5
|
||||
? Icons.volume_down_rounded
|
||||
: Icons.volume_up_rounded,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: LinearProgressIndicator(
|
||||
value: gesture.volume,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
backgroundColor: Colors.grey,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||
Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
78
lib/pages/player/overlays/minimal_progress_overlay.dart
Normal file
78
lib/pages/player/overlays/minimal_progress_overlay.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_zustand/flutter_zustand.dart';
|
||||
import 'package:iris/models/file.dart';
|
||||
import 'package:iris/models/player.dart';
|
||||
import 'package:iris/pages/player/control_bar/control_bar_slider.dart';
|
||||
import 'package:iris/store/use_player_ui_store.dart';
|
||||
import 'package:iris/utils/format_duration_to_minutes.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MinimalProgressOverlay extends StatelessWidget {
|
||||
const MinimalProgressOverlay({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.file,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final FileItem? file;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final progress =
|
||||
context.select<MediaPlayer, ({Duration position, Duration duration})>(
|
||||
(player) => (position: player.position, duration: player.duration),
|
||||
);
|
||||
|
||||
const overlayTextStyle = TextStyle(
|
||||
color: Colors.white,
|
||||
decoration: TextDecoration.none,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black,
|
||||
offset: Offset(0, 0),
|
||||
blurRadius: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final isShowControl =
|
||||
usePlayerUiStore().select(context, (state) => state.isShowControl);
|
||||
final isShowProgress =
|
||||
usePlayerUiStore().select(context, (state) => state.isShowProgress);
|
||||
|
||||
if (isShowProgress && !isShowControl && file?.type == ContentType.video) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 12,
|
||||
top: 12,
|
||||
child: Text(
|
||||
title,
|
||||
style: overlayTextStyle.copyWith(fontSize: 20, height: 1),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: -28,
|
||||
right: -28,
|
||||
bottom: -16,
|
||||
height: 32,
|
||||
child: ControlBarSlider(
|
||||
disabled: true,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 12,
|
||||
bottom: 6,
|
||||
child: Text(
|
||||
'${formatDurationToMinutes(progress.position)} / ${formatDurationToMinutes(progress.duration)}',
|
||||
style: overlayTextStyle.copyWith(fontSize: 16, height: 2),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import 'package:iris/models/player.dart';
|
||||
import 'package:iris/models/storages/local.dart';
|
||||
import 'package:iris/pages/player/audio.dart';
|
||||
import 'package:iris/pages/player/overlays/controls_overlay.dart';
|
||||
import 'package:iris/pages/player/overlays/gesture_overlay.dart';
|
||||
import 'package:iris/pages/player/overlays/minimal_progress_overlay.dart';
|
||||
import 'package:iris/pages/player/video_view.dart';
|
||||
import 'package:iris/store/use_player_ui_store.dart';
|
||||
import 'package:iris/utils/check_content_type.dart';
|
||||
@@ -33,8 +35,6 @@ class Player extends HookWidget {
|
||||
final height =
|
||||
context.select<MediaPlayer, double>((player) => player.height);
|
||||
|
||||
final saveProgress = context.read<MediaPlayer>().saveProgress;
|
||||
|
||||
useAppLifecycle();
|
||||
|
||||
final cover = useCover();
|
||||
@@ -121,7 +121,7 @@ class Player extends HookWidget {
|
||||
|
||||
Future<void> showControlForHover(Future<void> callback) async {
|
||||
try {
|
||||
saveProgress();
|
||||
context.read<MediaPlayer>().saveProgress();
|
||||
showControl();
|
||||
usePlayerUiStore().updateIsHovering(true);
|
||||
await callback;
|
||||
@@ -214,7 +214,7 @@ class Player extends HookWidget {
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (bool didPop, Object? result) async {
|
||||
if (!didPop) {
|
||||
await saveProgress();
|
||||
await context.read<MediaPlayer>().saveProgress();
|
||||
if (isDesktop) {
|
||||
windowManager.close();
|
||||
} else {
|
||||
@@ -239,11 +239,24 @@ class Player extends HookWidget {
|
||||
fit: fit,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: MinimalProgressOverlay(
|
||||
title: title,
|
||||
file: file,
|
||||
),
|
||||
),
|
||||
// Audio
|
||||
if (file?.type == ContentType.audio)
|
||||
Positioned.fill(
|
||||
child: Audio(cover: cover),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: GestureOverlay(
|
||||
showControl: showControl,
|
||||
hideControl: hideControl,
|
||||
showProgress: showProgress,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: ControlsOverlay(
|
||||
file: file,
|
||||
|
||||
@@ -10,6 +10,13 @@ import 'package:iris/utils/get_latest_release.dart';
|
||||
import 'package:iris/utils/get_localizations.dart';
|
||||
import 'package:iris/utils/url.dart';
|
||||
|
||||
bool isPortable() {
|
||||
String resolvedExecutablePath = Platform.resolvedExecutable;
|
||||
String path = p.dirname(resolvedExecutablePath);
|
||||
String batFilePath = p.join(path, 'iris-updater.bat');
|
||||
return File(batFilePath).existsSync();
|
||||
}
|
||||
|
||||
Future<void> showReleaseDialog(BuildContext context,
|
||||
{required Release release}) async =>
|
||||
await showDialog<void>(
|
||||
@@ -32,10 +39,7 @@ class ReleaseDialog extends HookWidget {
|
||||
|
||||
useEffect(() {
|
||||
if (isWindows) {
|
||||
String resolvedExecutablePath = Platform.resolvedExecutable;
|
||||
String path = p.dirname(resolvedExecutablePath);
|
||||
String batFilePath = p.join(path, 'iris-updater.bat');
|
||||
updateScriptIsExists.value = File(batFilePath).existsSync();
|
||||
updateScriptIsExists.value = isPortable();
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
@@ -69,55 +69,55 @@ class Popup<T> extends PopupRoute<T> {
|
||||
alignment: direction == PopupDirection.left
|
||||
? Alignment.bottomLeft
|
||||
: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 8,
|
||||
left: direction == PopupDirection.left ? 8 : 0,
|
||||
right: direction == PopupDirection.right ? 8 : 0,
|
||||
),
|
||||
child: AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: direction == PopupDirection.left
|
||||
? const Offset(-1.0, 0.0)
|
||||
: const Offset(1.0, 0.0),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeInOutCubicEmphasized,
|
||||
),
|
||||
child: AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: direction == PopupDirection.left
|
||||
? const Offset(-1.0, 0.0)
|
||||
: const Offset(1.0, 0.0),
|
||||
end: Offset.zero,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeInOutCubicEmphasized,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Dismissible(
|
||||
key: UniqueKey(),
|
||||
direction: direction == PopupDirection.left
|
||||
? DismissDirection.endToStart
|
||||
: DismissDirection.startToEnd,
|
||||
onUpdate: (details) {
|
||||
if (details.previousReached) {
|
||||
_popOnce(context);
|
||||
}
|
||||
},
|
||||
child: Dismissible(
|
||||
key: UniqueKey(),
|
||||
direction: direction == PopupDirection.left
|
||||
? DismissDirection.endToStart
|
||||
: DismissDirection.startToEnd,
|
||||
onUpdate: (details) {
|
||||
if (details.previousReached) {
|
||||
_popOnce(context);
|
||||
}
|
||||
},
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final screenWidth = constraints.maxWidth;
|
||||
final screenHeight = constraints.maxHeight;
|
||||
final int size = screenWidth > 1200
|
||||
? 3
|
||||
: screenWidth > 720
|
||||
? 2
|
||||
: 1;
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final screenWidth = constraints.maxWidth;
|
||||
final screenHeight = constraints.maxHeight;
|
||||
final int size = screenWidth > 1200
|
||||
? 3
|
||||
: screenWidth > 720
|
||||
? 2
|
||||
: 1;
|
||||
|
||||
return UnconstrainedBox(
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 8,
|
||||
left: direction == PopupDirection.left ? 8 : 0,
|
||||
right: direction == PopupDirection.right ? 8 : 0,
|
||||
),
|
||||
child: UnconstrainedBox(
|
||||
child: LimitedBox(
|
||||
maxWidth: screenWidth / size - 16,
|
||||
maxHeight:
|
||||
isDesktop ? screenHeight - 48 : screenHeight - 16,
|
||||
isDesktop ? screenHeight - 56 : screenHeight - 16,
|
||||
child: Card(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
@@ -130,9 +130,9 @@ class Popup<T> extends PopupRoute<T> {
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'package:iris/utils/platform.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:iris/info.dart';
|
||||
@@ -7,6 +10,16 @@ import 'package:iris/utils/get_localizations.dart';
|
||||
import 'package:iris/utils/url.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
bool isMsix() {
|
||||
if (!isWindows) {
|
||||
return false;
|
||||
}
|
||||
String resolvedExecutablePath = Platform.resolvedExecutable;
|
||||
String path = p.dirname(resolvedExecutablePath);
|
||||
String manifestPath = p.join(path, 'AppxManifest.xml');
|
||||
return File(manifestPath).existsSync();
|
||||
}
|
||||
|
||||
class About extends HookWidget {
|
||||
const About({super.key});
|
||||
|
||||
@@ -48,6 +61,11 @@ class About extends HookWidget {
|
||||
title: Text(t.check_update),
|
||||
subtitle: noNewVersion.value ? Text(t.no_new_version) : null,
|
||||
onTap: () async {
|
||||
if (isMsix()) {
|
||||
launchURL(
|
||||
'ms-windows-store://pdp/?ProductId=${INFO.msStoreId}');
|
||||
return;
|
||||
}
|
||||
noNewVersion.value = false;
|
||||
final release = await getLatestRelease();
|
||||
if (release != null && context.mounted) {
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:iris/pages/settings/about.dart';
|
||||
import 'package:iris/pages/settings/general.dart';
|
||||
import 'package:iris/pages/settings/dependencies.dart';
|
||||
import 'package:iris/pages/settings/play.dart';
|
||||
import 'package:iris/widgets/popups/settings/about.dart';
|
||||
import 'package:iris/widgets/popups/settings/general.dart';
|
||||
import 'package:iris/widgets/popups/settings/dependencies.dart';
|
||||
import 'package:iris/widgets/popups/settings/play.dart';
|
||||
import 'package:iris/utils/get_localizations.dart';
|
||||
|
||||
class ITab {
|
||||
@@ -3,15 +3,15 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_zustand/flutter_zustand.dart';
|
||||
import 'package:iris/models/storages/storage.dart';
|
||||
import 'package:iris/pages/storages/favorites.dart';
|
||||
import 'package:iris/pages/storages/files.dart';
|
||||
import 'package:iris/widgets/popups/storages/favorites.dart';
|
||||
import 'package:iris/widgets/popups/storages/files.dart';
|
||||
import 'package:iris/store/use_storage_store.dart';
|
||||
import 'package:iris/utils/get_localizations.dart';
|
||||
import 'package:iris/utils/path_conv.dart';
|
||||
import 'package:iris/widgets/dialogs/show_folder_dialog.dart';
|
||||
import 'package:iris/widgets/dialogs/show_ftp_dialog.dart';
|
||||
import 'package:iris/widgets/dialogs/show_webdav_dialog.dart';
|
||||
import 'package:iris/pages/storages/storages_list.dart';
|
||||
import 'package:iris/widgets/popups/storages/storages_list.dart';
|
||||
import 'package:iris/utils/platform.dart';
|
||||
import 'package:saf_util/saf_util.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:iris/pages/player/overlays/track/audio_track_list.dart';
|
||||
import 'package:iris/pages/player/overlays/track/subtitle_list.dart';
|
||||
import 'package:iris/widgets/popups/track/audio_track_list.dart';
|
||||
import 'package:iris/widgets/popups/track/subtitle_list.dart';
|
||||
import 'package:iris/utils/get_localizations.dart';
|
||||
|
||||
class ITab {
|
||||
@@ -1,7 +1,7 @@
|
||||
name: iris
|
||||
description: "A lightweight video player"
|
||||
publish_to: 'none'
|
||||
version: 1.5.0+3
|
||||
version: 1.5.1+3
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@@ -84,7 +84,7 @@ msix_config:
|
||||
identity_name: 22P.IRISplayer
|
||||
publisher_display_name: 22P
|
||||
publisher: CN=9740B6B2-E777-4F52-8ECD-C4A577A73010
|
||||
msix_version: 1.5.0.0
|
||||
msix_version: 1.5.1.0
|
||||
logo_path: assets/images/logo.png
|
||||
trim_logo: false
|
||||
languages: en-us, zh-cn
|
||||
|
||||
Reference in New Issue
Block a user