diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1cb85ff..e494a43 100644
--- a/CHANGELOG.md
+++ b/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
diff --git a/README.md b/README.md
index cfffbeb..e5b745a 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,8 @@
# IRIS - A lightweight video player
-
+[](https://github.com/nini22P/iris/actions/workflows/ci.yml)
+
[](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
diff --git a/README_CN.md b/README_CN.md
index de90e2d..190bc3a 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -2,7 +2,8 @@
# IRIS - 轻量级视频播放器
-
+[](https://github.com/nini22P/iris/actions/workflows/ci.yml)
+
[](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 秒 |
+| 双击屏幕左侧 | 快退 |
+| 双击屏幕右侧 | 快进 |
| 左右滑动 | 调整播放进度 |
| 屏幕左侧上下滑动 | 调整屏幕亮度 |
| 屏幕右侧上下滑动 | 调整设备音量 |
-| 长按 | 启动倍速播放 |
-| 长按后左右滑动 | 调整倍速播放的速度 |
+| 长按 | 显示播放速度选择器 |
+| 长按后左右滑动 | 调整播放速度 |
## 贡献
diff --git a/inno.iss b/inno.iss
index f0be132..91ac487 100644
--- a/inno.iss
+++ b/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"
diff --git a/lib/hooks/use_app_lifecycle.dart b/lib/hooks/use_app_lifecycle.dart
index 6d31494..1f977ad 100644
--- a/lib/hooks/use_app_lifecycle.dart
+++ b/lib/hooks/use_app_lifecycle.dart
@@ -6,7 +6,6 @@ import 'package:provider/provider.dart';
void useAppLifecycle() {
final context = useContext();
- final saveProgress = context.read().saveProgress;
AppLifecycleState? appLifecycleState = useAppLifecycleState();
@@ -14,7 +13,7 @@ void useAppLifecycle() {
try {
if (appLifecycleState == AppLifecycleState.paused) {
logger('App lifecycle state: paused');
- saveProgress();
+ context.read().saveProgress();
}
} catch (e) {
logger('App lifecycle state error: $e');
diff --git a/lib/hooks/use_cover.dart b/lib/hooks/use_cover.dart
index 942ffe8..723666d 100644
--- a/lib/hooks/use_cover.dart
+++ b/lib/hooks/use_cover.dart
@@ -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') ??
diff --git a/lib/hooks/use_gesture.dart b/lib/hooks/use_gesture.dart
index 817a6a2..6751b53 100644
--- a/lib/hooks/use_gesture.dart
+++ b/lib/hooks/use_gesture.dart
@@ -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();
-
final gestureState = useRef({
'isTouch': false,
'isLongPress': false,
@@ -97,20 +94,22 @@ Gesture useGesture({
}
void onDoubleTapDown(TapDownDetails details) {
+ final player = context.read();
+
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().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().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().duration.inSeconds);
- player.seek(Duration(seconds: targetSeconds));
+ context.read().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,
);
}
diff --git a/lib/hooks/use_keyboard.dart b/lib/hooks/use_keyboard.dart
index 25213f2..2b2979d 100644
--- a/lib/hooks/use_keyboard.dart
+++ b/lib/hooks/use_keyboard.dart
@@ -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();
-
void onKeyEvent(KeyEvent event) async {
+ final player = context.read();
+
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().isPlaying) {
+ if (player.isPlaying) {
useAppStore().updateAutoPlay(false);
player.pause();
} else {
diff --git a/lib/hooks/use_volume.dart b/lib/hooks/use_volume.dart
index e6788c3..4221b8e 100644
--- a/lib/hooks/use_volume.dart
+++ b/lib/hooks/use_volume.dart
@@ -10,7 +10,6 @@ ValueNotifier useVolume(bool isGesture) {
try {
() async {
if (!isGesture) return;
- await FlutterVolumeController.updateShowSystemUI(false);
volume.value = await FlutterVolumeController.getVolume();
}();
} catch (e) {
@@ -18,7 +17,6 @@ ValueNotifier useVolume(bool isGesture) {
}
return () {
volume.value = null;
- FlutterVolumeController.updateShowSystemUI(true);
};
}, [isGesture]);
diff --git a/lib/info.dart b/lib/info.dart
index 609a88c..48a0bac 100644
--- a/lib/info.dart
+++ b/lib/info.dart
@@ -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';
}
diff --git a/lib/models/storages/ftp.dart b/lib/models/storages/ftp.dart
index 7b28d6e..1a85995 100644
--- a/lib/models/storages/ftp.dart
+++ b/lib/models/storages/ftp.dart
@@ -49,28 +49,25 @@ Future> getFTPFiles(
List 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;
diff --git a/lib/models/storages/local.dart b/lib/models/storages/local.dart
index 931a5bf..ed1bf48 100644
--- a/lib/models/storages/local.dart
+++ b/lib/models/storages/local.dart
@@ -310,19 +310,17 @@ Future> getContentFiles(String uri) async {
List 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;
diff --git a/lib/models/storages/storage.dart b/lib/models/storages/storage.dart
index 5511981..be3b9c8 100644
--- a/lib/models/storages/storage.dart
+++ b/lib/models/storages/storage.dart
@@ -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';
diff --git a/lib/models/storages/webdav.dart b/lib/models/storages/webdav.dart
index 5bab88f..a28406a 100644
--- a/lib/models/storages/webdav.dart
+++ b/lib/models/storages/webdav.dart
@@ -96,21 +96,19 @@ Future> 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;
diff --git a/lib/pages/player/audio.dart b/lib/pages/player/audio.dart
index 264ce8a..e3b4e63 100644
--- a/lib/pages/player/audio.dart
+++ b/lib/pages/player/audio.dart
@@ -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(
diff --git a/lib/pages/player/control_bar/control_bar.dart b/lib/pages/player/control_bar/control_bar.dart
index a9f5ada..ee13b23 100644
--- a/lib/pages/player/control_bar/control_bar.dart
+++ b/lib/pages/player/control_bar/control_bar.dart
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_zustand/flutter_zustand.dart';
import 'package:iris/globals.dart' show rateMenuKey, speedStops, moreMenuKey;
+import 'package:iris/models/file.dart';
import 'package:iris/models/player.dart';
import 'package:iris/models/storages/local.dart';
import 'package:iris/models/store/app_state.dart';
@@ -11,18 +12,18 @@ import 'package:iris/store/use_player_ui_store.dart';
import 'package:iris/widgets/dialogs/show_open_link_dialog.dart';
import 'package:iris/widgets/dialogs/show_rate_dialog.dart';
import 'package:iris/pages/player/control_bar/control_bar_slider.dart';
-import 'package:iris/pages/player/overlays/history.dart';
+import 'package:iris/widgets/popups/history.dart';
import 'package:iris/widgets/bottom_sheets/show_open_link_bottom_sheet.dart';
-import 'package:iris/pages/settings/settings.dart';
+import 'package:iris/widgets/popups/settings/settings.dart';
import 'package:iris/pages/player/control_bar/volume_control.dart';
-import 'package:iris/pages/player/overlays/track/subtitle_and_audio_track.dart';
+import 'package:iris/widgets/popups/track/subtitle_and_audio_track.dart';
import 'package:iris/store/use_app_store.dart';
import 'package:iris/store/use_play_queue_store.dart';
import 'package:iris/utils/get_localizations.dart';
-import 'package:iris/pages/player/overlays/play_queue.dart';
+import 'package:iris/widgets/popups/play_queue.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:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
@@ -51,8 +52,6 @@ class ControlBar extends HookWidget {
final isInitializing =
context.select((player) => player.isInitializing);
- final player = context.read();
-
final rate = useAppStore().select(context, (state) => state.rate);
final volume = useAppStore().select(context, (state) => state.volume);
final isMuted = useAppStore().select(context, (state) => state.isMuted);
@@ -73,6 +72,17 @@ class ControlBar extends HookWidget {
final displayIsPlaying = useState(isPlaying);
+ final playQueue =
+ usePlayQueueStore().select(context, (state) => state.playQueue);
+ final currentIndex =
+ usePlayQueueStore().select(context, (state) => state.currentIndex);
+
+ final FileItem? file = useMemoized(() {
+ final index =
+ playQueue.indexWhere((element) => element.index == currentIndex);
+ return playQueue.isEmpty || index < 0 ? null : playQueue[index].file;
+ }, [playQueue, currentIndex]);
+
useEffect(() {
if (!isSeeking) {
displayIsPlaying.value = isPlaying;
@@ -80,603 +90,549 @@ class ControlBar extends HookWidget {
return null;
}, [isPlaying]);
- return Container(
- padding: const EdgeInsets.all(8),
- decoration: BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.topCenter,
- end: Alignment.bottomCenter,
- colors: [
- Colors.black.withValues(alpha: 0),
- Colors.black.withValues(alpha: 0.25),
- Colors.black.withValues(alpha: 0.65),
- ],
+ final playPauseButton = Stack(
+ alignment: Alignment.center,
+ children: [
+ if (isInitializing)
+ SizedBox(
+ width: 32,
+ height: 32,
+ child: CircularProgressIndicator(
+ strokeWidth: 4,
+ color: Theme.of(context).colorScheme.surface,
+ ),
+ ),
+ IconButton(
+ tooltip: '${displayIsPlaying.value ? t.pause : t.play} ( Space )',
+ icon: Icon(
+ displayIsPlaying.value
+ ? Icons.pause_rounded
+ : Icons.play_arrow_rounded,
+ size: 32,
+ color: color,
+ ),
+ onPressed: () {
+ showControl();
+ if (isPlaying == true) {
+ useAppStore().updateAutoPlay(false);
+ context.read().pause();
+ } else {
+ useAppStore().updateAutoPlay(true);
+ context.read().play();
+ }
+ },
+ style: ButtonStyle(overlayColor: overlayColor),
+ ),
+ ],
+ );
+
+ final stopButton = IconButton(
+ tooltip: '${t.stop} ( Ctrl + C )',
+ icon: Icon(
+ Icons.stop_rounded,
+ size: 26,
+ color: color,
+ ),
+ onPressed: () {
+ showControl();
+ useAppStore().updateAutoPlay(false);
+ context.read().pause();
+ usePlayQueueStore().updateCurrentIndex(-1);
+ },
+ style: ButtonStyle(overlayColor: overlayColor),
+ );
+
+ final prevButton = playQueueLength > 1
+ ? IconButton(
+ tooltip: '${t.previous} ( Ctrl + ← )',
+ icon: Icon(
+ Icons.skip_previous_rounded,
+ size: 26,
+ color: color,
+ ),
+ onPressed: () {
+ showControl();
+ usePlayQueueStore().previous();
+ },
+ style: ButtonStyle(overlayColor: overlayColor),
+ )
+ : const SizedBox.shrink();
+
+ final nextButton = playQueueLength > 1
+ ? IconButton(
+ tooltip: '${t.next} ( Ctrl + → )',
+ icon: Icon(
+ Icons.skip_next_rounded,
+ size: 26,
+ color: color,
+ ),
+ onPressed: () {
+ showControl();
+ usePlayQueueStore().next();
+ },
+ style: ButtonStyle(overlayColor: overlayColor),
+ )
+ : const SizedBox.shrink();
+
+ final shuffleButton = Builder(
+ builder: (context) => IconButton(
+ tooltip: '${t.shuffle}: ${shuffle ? t.on : t.off} ( Ctrl + X )',
+ icon: Icon(
+ Icons.shuffle_rounded,
+ size: 20,
+ color: !shuffle ? color?.withAlpha(153) : color,
),
+ onPressed: () {
+ showControl();
+ shuffle ? usePlayQueueStore().sort() : usePlayQueueStore().shuffle();
+ useAppStore().updateShuffle(!shuffle);
+ },
+ style: ButtonStyle(overlayColor: overlayColor),
),
- child: Column(
- children: [
- Visibility(
- visible: width < 1024 || !isDesktop,
- child: ControlBarSlider(
- showControl: showControl,
+ );
+
+ final repeatButton = Builder(
+ builder: (context) => IconButton(
+ tooltip:
+ '${repeat == Repeat.one ? t.repeat_one : repeat == Repeat.all ? t.repeat_all : t.repeat_none} ( Ctrl + R )',
+ icon: Icon(
+ repeat == Repeat.one
+ ? Icons.repeat_one_rounded
+ : Icons.repeat_rounded,
+ size: 20,
+ color: repeat == Repeat.none ? color?.withAlpha(153) : color,
+ ),
+ onPressed: () {
+ showControl();
+ useAppStore().toggleRepeat();
+ },
+ style: ButtonStyle(overlayColor: overlayColor),
+ ),
+ );
+
+ final fitButton = IconButton(
+ tooltip:
+ '${t.video_zoom}: ${fit == BoxFit.contain ? t.fit : fit == BoxFit.fill ? t.stretch : fit == BoxFit.cover ? t.crop : '100%'} ( Ctrl + V )',
+ icon: Icon(
+ fit == BoxFit.contain
+ ? Icons.fit_screen_rounded
+ : fit == BoxFit.fill
+ ? Icons.aspect_ratio_rounded
+ : fit == BoxFit.cover
+ ? Icons.crop_landscape_rounded
+ : Icons.crop_free_rounded,
+ size: 20,
+ color: color,
+ ),
+ onPressed: () {
+ showControl();
+ useAppStore().toggleFit();
+ },
+ style: ButtonStyle(overlayColor: overlayColor),
+ );
+
+ final rateButton = PopupMenuButton(
+ key: rateMenuKey,
+ clipBehavior: Clip.hardEdge,
+ constraints: const BoxConstraints(minWidth: 0),
+ itemBuilder: (BuildContext context) => speedStops
+ .map(
+ (item) => PopupMenuItem(
+ child: Text(
+ '${item}X',
+ style: TextStyle(
+ color: item == rate
+ ? Theme.of(context).colorScheme.primary
+ : null,
+ fontWeight: item == rate ? FontWeight.bold : FontWeight.w100,
+ height: 1,
+ ),
+ ),
+ onTap: () async {
+ showControl();
+ useAppStore().updateRate(item);
+ },
+ ),
+ )
+ .toList(),
+ child: Tooltip(
+ message: t.playback_speed,
+ child: TextButton(
+ onPressed: () => rateMenuKey.currentState?.showButtonMenu(),
+ style: ButtonStyle(overlayColor: overlayColor),
+ child: Text(
+ '${rate}X',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
color: color,
),
),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- const SizedBox(width: 2),
- Stack(
- alignment: Alignment.center,
- children: [
- IconButton(
- tooltip:
- '${displayIsPlaying.value ? t.pause : t.play} ( Space )',
- icon: Icon(
- displayIsPlaying.value
- ? Icons.pause_rounded
- : Icons.play_arrow_rounded,
- size: 32,
- color: color,
- ),
- onPressed: () {
- showControl();
- if (isPlaying == true) {
- useAppStore().updateAutoPlay(false);
- player.pause();
- } else {
- useAppStore().updateAutoPlay(true);
- player.play();
- }
- },
- style: ButtonStyle(overlayColor: overlayColor),
- ),
- if (isInitializing)
- SizedBox(
- width: 32,
- height: 32,
- child: CircularProgressIndicator(
- strokeWidth: 4,
- color: Theme.of(context).colorScheme.surface,
- ),
- ),
- ],
+ ),
+ ),
+ );
+
+ final volumeWidget = width < 768
+ ? Builder(
+ builder: (context) => IconButton(
+ tooltip: '${t.volume}: $volume',
+ icon: Icon(
+ isMuted || volume == 0
+ ? Icons.volume_off_rounded
+ : volume < 50
+ ? Icons.volume_down_rounded
+ : Icons.volume_up_rounded,
+ size: 20,
+ color: color,
),
- IconButton(
- tooltip: '${t.stop} ( Ctrl + C )',
- icon: Icon(
- Icons.stop_rounded,
- size: 26,
- color: color,
- ),
- onPressed: () {
- showControl();
- useAppStore().updateAutoPlay(false);
- player.pause();
- usePlayQueueStore().updateCurrentIndex(-1);
- },
- style: ButtonStyle(overlayColor: overlayColor),
+ onPressed: () => showControlForHover(
+ showVolumePopover(context, showControl),
),
- if (playQueueLength > 1)
- IconButton(
- tooltip: '${t.previous} ( Ctrl + ← )',
- icon: Icon(
- Icons.skip_previous_rounded,
- size: 26,
- color: color,
- ),
- onPressed: () {
- showControl();
- usePlayQueueStore().previous();
- },
- style: ButtonStyle(overlayColor: overlayColor),
- ),
- if (playQueueLength > 1)
- IconButton(
- tooltip: '${t.next} ( Ctrl + → )',
- icon: Icon(
- Icons.skip_next_rounded,
- size: 26,
- color: color,
- ),
- onPressed: () {
- showControl();
- usePlayQueueStore().next();
- },
- style: ButtonStyle(overlayColor: overlayColor),
- ),
- if (width >= 768)
- Builder(
- builder: (context) => IconButton(
- tooltip:
- '${t.shuffle}: ${shuffle ? t.on : t.off} ( Ctrl + X )',
- icon: Icon(
- Icons.shuffle_rounded,
- size: 20,
- color: !shuffle ? color?.withAlpha(153) : color,
- ),
- onPressed: () {
- showControl();
- shuffle
- ? usePlayQueueStore().sort()
- : usePlayQueueStore().shuffle();
- useAppStore().updateShuffle(!shuffle);
- },
- style: ButtonStyle(overlayColor: overlayColor),
- ),
- ),
- if (width >= 768)
- Builder(
- builder: (context) => IconButton(
- tooltip:
- '${repeat == Repeat.one ? t.repeat_one : repeat == Repeat.all ? t.repeat_all : t.repeat_none} ( Ctrl + R )',
- icon: Icon(
- repeat == Repeat.one
- ? Icons.repeat_one_rounded
- : Icons.repeat_rounded,
- size: 20,
- color:
- repeat == Repeat.none ? color?.withAlpha(153) : color,
- ),
- onPressed: () {
- showControl();
- useAppStore().toggleRepeat();
- },
- style: ButtonStyle(overlayColor: overlayColor),
- ),
- ),
- if (width >= 768)
- IconButton(
- tooltip:
- '${t.video_zoom}: ${fit == BoxFit.contain ? t.fit : fit == BoxFit.fill ? t.stretch : fit == BoxFit.cover ? t.crop : '100%'} ( Ctrl + V )',
- icon: Icon(
- fit == BoxFit.contain
- ? Icons.fit_screen_rounded
- : fit == BoxFit.fill
- ? Icons.aspect_ratio_rounded
- : fit == BoxFit.cover
- ? Icons.crop_landscape_rounded
- : Icons.crop_free_rounded,
- size: 20,
- color: color,
- ),
- onPressed: () {
- showControl();
- useAppStore().toggleFit();
- },
- style: ButtonStyle(overlayColor: overlayColor),
- ),
- if (width > 600)
- PopupMenuButton(
- key: rateMenuKey,
- clipBehavior: Clip.hardEdge,
- constraints: const BoxConstraints(minWidth: 0),
- itemBuilder: (BuildContext context) => speedStops
- .map(
- (item) => PopupMenuItem(
- child: Text(
- '${item}X',
- style: TextStyle(
- color: item == rate
- ? Theme.of(context).colorScheme.primary
- : null,
- fontWeight: item == rate
- ? FontWeight.bold
- : FontWeight.w100,
- height: 1,
- ),
- ),
- onTap: () async {
- showControl();
- useAppStore().updateRate(item);
- },
- ),
- )
- .toList(),
- child: Tooltip(
- message: t.playback_speed,
- child: TextButton(
- onPressed: () =>
- rateMenuKey.currentState?.showButtonMenu(),
- style: ButtonStyle(overlayColor: overlayColor),
- child: Text(
- '${rate}X',
- style: TextStyle(
- fontWeight: FontWeight.bold,
- color: color,
- ),
- ),
- ),
- ),
- ),
- if (width < 640)
- Builder(
- builder: (context) => IconButton(
- tooltip: '${t.volume}: $volume',
- icon: Icon(
- isMuted || volume == 0
- ? Icons.volume_off_rounded
- : volume < 50
- ? Icons.volume_down_rounded
- : Icons.volume_up_rounded,
- size: 20,
- color: color,
- ),
- onPressed: () => showControlForHover(
- showVolumePopover(context, showControl),
- ),
- style: ButtonStyle(overlayColor: overlayColor),
- ),
- ),
- if (width >= 640)
- SizedBox(
- width: 160,
- child: VolumeControl(
- showControl: showControl,
- showVolumeText: false,
- color: color,
- overlayColor: overlayColor,
- ),
- ),
- Expanded(
- child: Visibility(
- visible:
- width >= 1024 && isDesktop,
- child: ControlBarSlider(
- showControl: showControl,
- color: color,
- ),
- ),
+ style: ButtonStyle(overlayColor: overlayColor),
+ ),
+ )
+ : SizedBox(
+ width: 160,
+ child: VolumeControl(
+ showControl: showControl,
+ showVolumeText: false,
+ color: color,
+ overlayColor: overlayColor,
+ ),
+ );
+
+ final sliderWidget =
+ ControlBarSlider(showControl: showControl, color: color);
+
+ final subtitleButton = IconButton(
+ tooltip: '${t.subtitle_and_audio_track} ( S )',
+ icon: Icon(
+ Icons.subtitles_rounded,
+ size: 20,
+ color: color,
+ ),
+ onPressed: () async {
+ showControlForHover(
+ showPopup(
+ context: context,
+ child: Provider.value(
+ value: context.read(),
+ child: const SubtitleAndAudioTrack(),
+ ),
+ direction: PopupDirection.right,
+ ),
+ );
+ },
+ style: ButtonStyle(overlayColor: overlayColor),
+ );
+
+ final playQueueButton = IconButton(
+ tooltip: '${t.play_queue} ( P )',
+ icon: Transform.translate(
+ offset: const Offset(1, 1.5),
+ child: Icon(
+ Icons.playlist_play_rounded,
+ size: 28,
+ color: color,
+ ),
+ ),
+ onPressed: () async {
+ showControlForHover(
+ showPopup(
+ context: context,
+ child: const PlayQueue(),
+ direction: PopupDirection.right,
+ ),
+ );
+ },
+ style: ButtonStyle(overlayColor: overlayColor),
+ );
+
+ final storageButton = IconButton(
+ tooltip: '${t.storage} ( F )',
+ icon: Icon(
+ Icons.storage_rounded,
+ size: 18,
+ color: color,
+ ),
+ onPressed: () => showControlForHover(
+ showPopup(
+ context: context,
+ child: const Storages(),
+ direction: PopupDirection.right,
+ ),
+ ),
+ style: ButtonStyle(overlayColor: overlayColor),
+ );
+
+ final fullscreenButton = IconButton(
+ tooltip: isFullScreen
+ ? '${t.exit_fullscreen} ( Escape, F11, Enter )'
+ : '${t.enter_fullscreen} ( F11, Enter )',
+ icon: Icon(
+ isFullScreen
+ ? Icons.close_fullscreen_rounded
+ : Icons.open_in_full_rounded,
+ size: 19,
+ color: color,
+ ),
+ onPressed: () async {
+ showControl();
+ usePlayerUiStore().updateFullScreen(!isFullScreen);
+ },
+ style: ButtonStyle(overlayColor: overlayColor),
+ );
+
+ final moreMenuButton = PopupMenuButton(
+ key: moreMenuKey,
+ icon: Icon(
+ Icons.more_vert_rounded,
+ size: 20,
+ color: color,
+ ),
+ style: ButtonStyle(overlayColor: overlayColor),
+ clipBehavior: Clip.hardEdge,
+ constraints: const BoxConstraints(minWidth: 200),
+ itemBuilder: (BuildContext context) => [
+ PopupMenuItem(
+ child: ListTile(
+ mouseCursor: SystemMouseCursors.click,
+ leading: const Icon(
+ Icons.file_open_rounded,
+ size: 16.5,
+ ),
+ title: Text(t.open_file),
+ trailing: Text(
+ 'Ctrl + O',
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context).dividerColor,
),
- if (width >= 420)
- IconButton(
- tooltip: '${t.subtitle_and_audio_track} ( S )',
- icon: Icon(
- Icons.subtitles_rounded,
- size: 20,
- color: color,
- ),
- onPressed: () async {
- showControlForHover(
- showPopup(
- context: context,
- child: Provider.value(
- value: context.read(),
- child: const SubtitleAndAudioTrack(),
- ),
- direction: PopupDirection.right,
- ),
- );
- },
- style: ButtonStyle(overlayColor: overlayColor),
- ),
- IconButton(
- tooltip: '${t.play_queue} ( P )',
- icon: Transform.translate(
- offset: const Offset(0, 1.5),
- child: Icon(
- Icons.playlist_play_rounded,
- size: 28,
- color: color,
- ),
- ),
- onPressed: () async {
- showControlForHover(
- showPopup(
- context: context,
- child: const PlayQueue(),
- direction: PopupDirection.right,
- ),
- );
- },
- style: ButtonStyle(overlayColor: overlayColor),
+ ),
+ ),
+ onTap: () async {
+ showControl();
+ if (Platform.isAndroid) {
+ await pickContentFile();
+ } else {
+ await pickLocalFile();
+ }
+ showControl();
+ },
+ ),
+ PopupMenuItem(
+ child: ListTile(
+ mouseCursor: SystemMouseCursors.click,
+ leading: const Icon(
+ Icons.file_present_rounded,
+ size: 16.5,
+ ),
+ title: Text(t.open_link),
+ trailing: Text(
+ 'Ctrl + L',
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context).dividerColor,
),
- IconButton(
- tooltip: '${t.storage} ( F )',
- icon: Icon(
- Icons.storage_rounded,
- size: 18,
- color: color,
- ),
- onPressed: () => showControlForHover(
- showPopup(
- context: context,
- child: const Storages(),
- direction: PopupDirection.right,
- ),
- ),
- style: ButtonStyle(overlayColor: overlayColor),
+ ),
+ ),
+ onTap: () async {
+ isDesktop
+ ? await showOpenLinkDialog(context)
+ : await showOpenLinkBottomSheet(context);
+ showControl();
+ },
+ ),
+ if (width < 600)
+ PopupMenuItem(
+ child: ListTile(
+ mouseCursor: SystemMouseCursors.click,
+ leading: const Icon(
+ Icons.speed_rounded,
+ size: 20,
),
- Visibility(
- visible: isDesktop,
- child: IconButton(
- tooltip: isFullScreen
- ? '${t.exit_fullscreen} ( Escape, F11, Enter )'
- : '${t.enter_fullscreen} ( F11, Enter )',
- icon: Icon(
- isFullScreen
- ? Icons.close_fullscreen_rounded
- : Icons.open_in_full_rounded,
- size: 19,
- color: color,
- ),
- onPressed: () async {
- showControl();
- usePlayerUiStore().updateFullScreen(!isFullScreen);
- },
- style: ButtonStyle(overlayColor: overlayColor),
- ),
+ title: Text('${t.playback_speed}: ${rate}X'),
+ ),
+ onTap: () => showControlForHover(showRateDialog(context)),
+ ),
+ PopupMenuItem(
+ child: ListTile(
+ mouseCursor: SystemMouseCursors.click,
+ leading: const Icon(
+ Icons.history_rounded,
+ size: 20,
+ ),
+ title: Text(t.history),
+ trailing: Text(
+ 'Ctrl + H',
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context).dividerColor,
),
- PopupMenuButton(
- key: moreMenuKey,
- icon: Icon(
- Icons.more_vert_rounded,
- size: 20,
- color: color,
- ),
- style: ButtonStyle(overlayColor: overlayColor),
- clipBehavior: Clip.hardEdge,
- constraints: const BoxConstraints(minWidth: 200),
- itemBuilder: (BuildContext context) => [
- PopupMenuItem(
- child: ListTile(
- mouseCursor: SystemMouseCursors.click,
- leading: const Icon(
- Icons.file_open_rounded,
- size: 16.5,
- ),
- title: Text(t.open_file),
- trailing: Text(
- 'Ctrl + O',
- style: TextStyle(
- fontSize: 12,
- color: Theme.of(context).dividerColor,
- ),
- ),
- ),
- onTap: () async {
- showControl();
- if (Platform.isAndroid) {
- await pickContentFile();
- } else {
- await pickLocalFile();
- }
- showControl();
- },
- ),
- PopupMenuItem(
- child: ListTile(
- mouseCursor: SystemMouseCursors.click,
- leading: const Icon(
- Icons.file_present_rounded,
- size: 16.5,
- ),
- title: Text(t.open_link),
- trailing: Text(
- 'Ctrl + L',
- style: TextStyle(
- fontSize: 12,
- color: Theme.of(context).dividerColor,
- ),
- ),
- ),
- onTap: () async {
- isDesktop
- ? await showOpenLinkDialog(context)
- : await showOpenLinkBottomSheet(context);
- showControl();
- },
- ),
- if (width < 768)
- PopupMenuItem(
- child: ListTile(
- mouseCursor: SystemMouseCursors.click,
- leading: Icon(
- Icons.shuffle_rounded,
- size: 20,
- color: !shuffle
- ? Theme.of(context).disabledColor
- : Theme.of(context).colorScheme.onSurfaceVariant,
- ),
- title: Text('${t.shuffle}: ${shuffle ? t.on : t.off}'),
- trailing: Text(
- 'Ctrl + X',
- style: TextStyle(
- fontSize: 12,
- color: Theme.of(context).dividerColor,
- ),
- ),
- ),
- onTap: () {
- showControl();
- shuffle
- ? usePlayQueueStore().sort()
- : usePlayQueueStore().shuffle();
- useAppStore().updateShuffle(!shuffle);
- },
- ),
- if (width < 768)
- PopupMenuItem(
- child: ListTile(
- mouseCursor: SystemMouseCursors.click,
- leading: Icon(
- repeat == Repeat.one
- ? Icons.repeat_one_rounded
- : Icons.repeat_rounded,
- size: 20,
- color: repeat == Repeat.none
- ? Theme.of(context).disabledColor
- : Theme.of(context).colorScheme.onSurfaceVariant,
- ),
- title: Text(repeat == Repeat.one
- ? t.repeat_one
- : repeat == Repeat.all
- ? t.repeat_all
- : t.repeat_none),
- trailing: Text(
- 'Ctrl + R',
- style: TextStyle(
- fontSize: 12,
- color: Theme.of(context).dividerColor,
- ),
- ),
- ),
- onTap: () {
- showControl();
- useAppStore().toggleRepeat();
- },
- ),
- if (width < 768)
- PopupMenuItem(
- child: ListTile(
- mouseCursor: SystemMouseCursors.click,
- leading: Icon(
- fit == BoxFit.contain
- ? Icons.fit_screen_rounded
- : fit == BoxFit.fill
- ? Icons.aspect_ratio_rounded
- : fit == BoxFit.cover
- ? Icons.crop_landscape_rounded
- : Icons.crop_free_rounded,
- size: 20,
- ),
- title: Text(
- '${t.video_zoom}: ${fit == BoxFit.contain ? t.fit : fit == BoxFit.fill ? t.stretch : fit == BoxFit.cover ? t.crop : '100%'}'),
- trailing: Text(
- 'Ctrl + V',
- style: TextStyle(
- fontSize: 12,
- color: Theme.of(context).dividerColor,
- ),
- ),
- ),
- onTap: () {
- showControl();
- useAppStore().toggleFit();
- },
- ),
- if (width <= 460)
- PopupMenuItem(
- child: ListTile(
- mouseCursor: SystemMouseCursors.click,
- leading: const Icon(
- Icons.speed_rounded,
- size: 20,
- ),
- title: Text('${t.playback_speed}: ${rate}X'),
- ),
- onTap: () => showControlForHover(showRateDialog(context)),
- ),
- if (width < 420)
- PopupMenuItem(
- child: ListTile(
- mouseCursor: SystemMouseCursors.click,
- leading: const Icon(
- Icons.subtitles_rounded,
- size: 20,
- ),
- title: Text(t.subtitle_and_audio_track),
- trailing: Text(
- 'S',
- style: TextStyle(
- fontSize: 12,
- color: Theme.of(context).dividerColor,
- ),
- ),
- ),
- onTap: () => showControlForHover(
- showPopup(
- context: context,
- child: Provider.value(
- value: context.read(),
- child: const SubtitleAndAudioTrack(),
- ),
- direction: PopupDirection.right,
- ),
- ),
- ),
- PopupMenuItem(
- child: ListTile(
- mouseCursor: SystemMouseCursors.click,
- leading: const Icon(
- Icons.history_rounded,
- size: 20,
- ),
- title: Text(t.history),
- trailing: Text(
- 'Ctirl + H',
- style: TextStyle(
- fontSize: 12,
- color: Theme.of(context).dividerColor,
- ),
- ),
- ),
- onTap: () => showControlForHover(
- showPopup(
- context: context,
- child: const History(),
- direction: PopupDirection.right,
- ),
- ),
- ),
- PopupMenuItem(
- child: ListTile(
- mouseCursor: SystemMouseCursors.click,
- leading: const Icon(
- Icons.settings_rounded,
- size: 20,
- ),
- title: Text(t.settings),
- trailing: Text(
- 'Ctirl + P',
- style: TextStyle(
- fontSize: 12,
- color: Theme.of(context).dividerColor,
- ),
- ),
- ),
- onTap: () => showControlForHover(
- showPopup(
- context: context,
- child: const Settings(),
- direction: PopupDirection.right,
- ),
- ),
- ),
- PopupMenuItem(
- child: ListTile(
- mouseCursor: SystemMouseCursors.click,
- leading: const Icon(
- Icons.exit_to_app_rounded,
- size: 20,
- ),
- title: Text(t.exit),
- trailing: Text(
- 'Alt + X',
- style: TextStyle(
- fontSize: 12,
- color: Theme.of(context).dividerColor,
- ),
- ),
- ),
- onTap: () async {
- await player.saveProgress();
- if (isDesktop) {
- windowManager.close();
- } else {
- SystemNavigator.pop();
- exit(0);
- }
- },
- ),
- ],
+ ),
+ ),
+ onTap: () => showControlForHover(
+ showPopup(
+ context: context,
+ child: const History(),
+ direction: PopupDirection.right,
+ ),
+ ),
+ ),
+ PopupMenuItem(
+ child: ListTile(
+ mouseCursor: SystemMouseCursors.click,
+ leading: const Icon(
+ Icons.settings_rounded,
+ size: 20,
+ ),
+ title: Text(t.settings),
+ trailing: Text(
+ 'Ctrl + P',
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context).dividerColor,
+ ),
+ ),
+ ),
+ onTap: () => showControlForHover(
+ showPopup(
+ context: context,
+ child: const Settings(),
+ direction: PopupDirection.right,
+ ),
+ ),
+ ),
+ PopupMenuItem(
+ child: ListTile(
+ mouseCursor: SystemMouseCursors.click,
+ leading: const Icon(
+ Icons.exit_to_app_rounded,
+ size: 20,
+ ),
+ title: Text(t.exit),
+ trailing: Text(
+ 'Alt + X',
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context).dividerColor,
),
- const SizedBox(width: 2),
+ ),
+ ),
+ onTap: () async {
+ await context.read().saveProgress();
+ if (isDesktop) {
+ windowManager.close();
+ } else {
+ SystemNavigator.pop();
+ exit(0);
+ }
+ },
+ ),
+ ],
+ );
+
+ const double mobileBreakpoint = 640.0;
+ const double tabletBreakpoint = 1024.0;
+
+ final Widget controlLayout;
+
+ if (width < mobileBreakpoint) {
+ controlLayout = Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ sliderWidget,
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ shuffleButton,
+ prevButton,
+ playPauseButton,
+ stopButton,
+ nextButton,
+ repeatButton,
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ if (file?.type != ContentType.audio) fitButton,
+ volumeWidget,
+ subtitleButton,
+ playQueueButton,
+ storageButton,
+ if (isDesktop) fullscreenButton,
+ moreMenuButton,
+ ],
+ )
+ ],
+ );
+ } else if (width < tabletBreakpoint) {
+ controlLayout = Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ sliderWidget,
+ const SizedBox(height: 4),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ playPauseButton,
+ stopButton,
+ prevButton,
+ nextButton,
+ shuffleButton,
+ repeatButton,
+ if (file?.type != ContentType.audio) fitButton,
+ rateButton,
+ volumeWidget,
+ const Spacer(),
+ subtitleButton,
+ playQueueButton,
+ storageButton,
+ if (isDesktop) fullscreenButton,
+ moreMenuButton,
],
),
],
+ );
+ } else {
+ controlLayout = Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ playPauseButton,
+ stopButton,
+ prevButton,
+ nextButton,
+ shuffleButton,
+ repeatButton,
+ if (file?.type != ContentType.audio) fitButton,
+ rateButton,
+ volumeWidget,
+ Expanded(child: sliderWidget),
+ subtitleButton,
+ playQueueButton,
+ storageButton,
+ if (isDesktop) fullscreenButton,
+ moreMenuButton,
+ ],
+ );
+ }
+
+ return Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topCenter,
+ end: Alignment.bottomCenter,
+ colors: [
+ Colors.black.withValues(alpha: 0),
+ Colors.black.withValues(alpha: 0.25),
+ Colors.black.withValues(alpha: 0.65),
+ ],
+ ),
),
+ child: controlLayout,
);
}
}
diff --git a/lib/pages/player/control_bar/control_bar_slider.dart b/lib/pages/player/control_bar/control_bar_slider.dart
index d068f20..b497333 100644
--- a/lib/pages/player/control_bar/control_bar_slider.dart
+++ b/lib/pages/player/control_bar/control_bar_slider.dart
@@ -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
diff --git a/lib/pages/player/overlays/controls_overlay.dart b/lib/pages/player/overlays/controls_overlay.dart
index b6c49d4..a9b6a82 100644
--- a/lib/pages/player/overlays/controls_overlay.dart
+++ b/lib/pages/player/overlays/controls_overlay.dart
@@ -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((player) => player.isPlaying);
-
- final progress =
- context.select(
- (player) => (position: player.position, duration: player.duration),
- );
-
final saveProgress = context.read().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(
- 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(
- 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(
diff --git a/lib/pages/player/overlays/gesture_overlay.dart b/lib/pages/player/overlays/gesture_overlay.dart
new file mode 100644
index 0000000..d201d00
--- /dev/null
+++ b/lib/pages/player/overlays/gesture_overlay.dart
@@ -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((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(
+ 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(
+ Colors.white),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/player/overlays/minimal_progress_overlay.dart b/lib/pages/player/overlays/minimal_progress_overlay.dart
new file mode 100644
index 0000000..d491d33
--- /dev/null
+++ b/lib/pages/player/overlays/minimal_progress_overlay.dart
@@ -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(
+ (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();
+ }
+ }
+}
diff --git a/lib/pages/player/player.dart b/lib/pages/player/player.dart
index f6966e9..82e303b 100644
--- a/lib/pages/player/player.dart
+++ b/lib/pages/player/player.dart
@@ -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((player) => player.height);
- final saveProgress = context.read().saveProgress;
-
useAppLifecycle();
final cover = useCover();
@@ -121,7 +121,7 @@ class Player extends HookWidget {
Future showControlForHover(Future callback) async {
try {
- saveProgress();
+ context.read().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().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,
diff --git a/lib/widgets/title_bar.dart b/lib/pages/player/title_bar.dart
similarity index 100%
rename from lib/widgets/title_bar.dart
rename to lib/pages/player/title_bar.dart
diff --git a/lib/widgets/dialogs/show_release_dialog.dart b/lib/widgets/dialogs/show_release_dialog.dart
index c8f5712..86188d6 100644
--- a/lib/widgets/dialogs/show_release_dialog.dart
+++ b/lib/widgets/dialogs/show_release_dialog.dart
@@ -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 showReleaseDialog(BuildContext context,
{required Release release}) async =>
await showDialog(
@@ -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;
}, []);
diff --git a/lib/widgets/popup.dart b/lib/widgets/popup.dart
index 8c0f11b..300359b 100644
--- a/lib/widgets/popup.dart
+++ b/lib/widgets/popup.dart
@@ -69,55 +69,55 @@ class Popup extends PopupRoute {
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(
- 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(
+ 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 extends PopupRoute {
),
),
),
- );
- },
- ),
+ ),
+ );
+ },
),
),
),
diff --git a/lib/pages/player/overlays/history.dart b/lib/widgets/popups/history.dart
similarity index 100%
rename from lib/pages/player/overlays/history.dart
rename to lib/widgets/popups/history.dart
diff --git a/lib/pages/player/overlays/play_queue.dart b/lib/widgets/popups/play_queue.dart
similarity index 100%
rename from lib/pages/player/overlays/play_queue.dart
rename to lib/widgets/popups/play_queue.dart
diff --git a/lib/pages/settings/about.dart b/lib/widgets/popups/settings/about.dart
similarity index 81%
rename from lib/pages/settings/about.dart
rename to lib/widgets/popups/settings/about.dart
index 93cf20f..34a1542 100644
--- a/lib/pages/settings/about.dart
+++ b/lib/widgets/popups/settings/about.dart
@@ -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) {
diff --git a/lib/pages/settings/dependencies.dart b/lib/widgets/popups/settings/dependencies.dart
similarity index 100%
rename from lib/pages/settings/dependencies.dart
rename to lib/widgets/popups/settings/dependencies.dart
diff --git a/lib/pages/settings/general.dart b/lib/widgets/popups/settings/general.dart
similarity index 100%
rename from lib/pages/settings/general.dart
rename to lib/widgets/popups/settings/general.dart
diff --git a/lib/pages/settings/play.dart b/lib/widgets/popups/settings/play.dart
similarity index 100%
rename from lib/pages/settings/play.dart
rename to lib/widgets/popups/settings/play.dart
diff --git a/lib/pages/settings/settings.dart b/lib/widgets/popups/settings/settings.dart
similarity index 90%
rename from lib/pages/settings/settings.dart
rename to lib/widgets/popups/settings/settings.dart
index 8066735..4cf2cc0 100644
--- a/lib/pages/settings/settings.dart
+++ b/lib/widgets/popups/settings/settings.dart
@@ -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 {
diff --git a/lib/pages/storages/favorites.dart b/lib/widgets/popups/storages/favorites.dart
similarity index 100%
rename from lib/pages/storages/favorites.dart
rename to lib/widgets/popups/storages/favorites.dart
diff --git a/lib/pages/storages/files.dart b/lib/widgets/popups/storages/files.dart
similarity index 100%
rename from lib/pages/storages/files.dart
rename to lib/widgets/popups/storages/files.dart
diff --git a/lib/pages/storages/storages.dart b/lib/widgets/popups/storages/storages.dart
similarity index 97%
rename from lib/pages/storages/storages.dart
rename to lib/widgets/popups/storages/storages.dart
index 7e5b3f5..62f80ae 100644
--- a/lib/pages/storages/storages.dart
+++ b/lib/widgets/popups/storages/storages.dart
@@ -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';
diff --git a/lib/pages/storages/storages_list.dart b/lib/widgets/popups/storages/storages_list.dart
similarity index 100%
rename from lib/pages/storages/storages_list.dart
rename to lib/widgets/popups/storages/storages_list.dart
diff --git a/lib/pages/player/overlays/track/audio_track_list.dart b/lib/widgets/popups/track/audio_track_list.dart
similarity index 100%
rename from lib/pages/player/overlays/track/audio_track_list.dart
rename to lib/widgets/popups/track/audio_track_list.dart
diff --git a/lib/pages/player/overlays/track/subtitle_and_audio_track.dart b/lib/widgets/popups/track/subtitle_and_audio_track.dart
similarity index 94%
rename from lib/pages/player/overlays/track/subtitle_and_audio_track.dart
rename to lib/widgets/popups/track/subtitle_and_audio_track.dart
index dc5ea12..2e29027 100644
--- a/lib/pages/player/overlays/track/subtitle_and_audio_track.dart
+++ b/lib/widgets/popups/track/subtitle_and_audio_track.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 {
diff --git a/lib/pages/player/overlays/track/subtitle_list.dart b/lib/widgets/popups/track/subtitle_list.dart
similarity index 100%
rename from lib/pages/player/overlays/track/subtitle_list.dart
rename to lib/widgets/popups/track/subtitle_list.dart
diff --git a/pubspec.yaml b/pubspec.yaml
index 63e4064..7dffbf4 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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