Merge pull request #60 from nini22P/dev

v1.5.1
This commit is contained in:
22
2025-10-05 10:17:58 +08:00
committed by GitHub
39 changed files with 1072 additions and 1039 deletions

View File

@@ -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

View File

@@ -2,7 +2,8 @@
# IRIS - A lightweight video player
![ci](https://github.com/nini22P/iris/actions/workflows/ci.yml/badge.svg)
[![Build Status](https://github.com/nini22P/iris/actions/workflows/ci.yml/badge.svg)](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>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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

View File

@@ -2,7 +2,8 @@
# IRIS - 轻量级视频播放器
![ci](https://github.com/nini22P/iris/actions/workflows/ci.yml/badge.svg)
[![Build Status](https://github.com/nini22P/iris/actions/workflows/ci.yml/badge.svg)](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>
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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 秒 |
| 双击屏幕左侧 | 快退 |
| 双击屏幕右侧 | 快进 |
| 左右滑动 | 调整播放进度 |
| 屏幕左侧上下滑动 | 调整屏幕亮度 |
| 屏幕右侧上下滑动 | 调整设备音量 |
| 长按 | 启动倍速播放 |
| 长按后左右滑动 | 调整倍速播放速度 |
| 长按 | 显示播放速度选择器 |
| 长按后左右滑动 | 调整播放速度 |
## 贡献

View File

@@ -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"

View File

@@ -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');

View File

@@ -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') ??

View File

@@ -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,
);
}

View File

@@ -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 {

View File

@@ -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]);

View File

@@ -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';
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View 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),
),
),
],
),
),
),
),
],
),
),
);
}
}

View 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();
}
}
}

View File

@@ -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,

View 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;
}, []);

View File

@@ -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> {
),
),
),
);
},
),
),
);
},
),
),
),

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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