From dff71b58828ce4c9b1760b1db9b1f9def1f680aa Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:57:29 +0800 Subject: [PATCH 1/8] fix: not find audio cover --- lib/hooks/use_cover.dart | 5 ++-- lib/models/storages/ftp.dart | 41 +++++++++++++++------------------ lib/models/storages/local.dart | 24 +++++++++---------- lib/models/storages/webdav.dart | 28 +++++++++++----------- lib/pages/player/audio.dart | 5 ++++ 5 files changed, 50 insertions(+), 53 deletions(-) diff --git a/lib/hooks/use_cover.dart b/lib/hooks/use_cover.dart index 942ffe8..678a9aa 100644 --- a/lib/hooks/use_cover.dart +++ b/lib/hooks/use_cover.dart @@ -52,9 +52,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/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/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..a9b9582 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( From 559f0458215cc2340a527770861e87d602ad22db Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Wed, 24 Sep 2025 21:02:47 +0800 Subject: [PATCH 2/8] fix: popup padding --- lib/widgets/popup.dart | 90 +++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/lib/widgets/popup.dart b/lib/widgets/popup.dart index 8c0f11b..4b08ade 100644 --- a/lib/widgets/popup.dart +++ b/lib/widgets/popup.dart @@ -69,51 +69,51 @@ 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: @@ -130,9 +130,9 @@ class Popup extends PopupRoute { ), ), ), - ); - }, - ), + ), + ); + }, ), ), ), From 31b45c31a3fed7958b8efba4ef09cdb7d5d01f3c Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Thu, 25 Sep 2025 21:35:31 +0800 Subject: [PATCH 3/8] feat: improve control bar layout --- lib/hooks/use_cover.dart | 14 +- lib/pages/player/audio.dart | 8 +- lib/pages/player/control_bar/control_bar.dart | 1120 ++++++++--------- lib/widgets/popup.dart | 2 +- 4 files changed, 551 insertions(+), 593 deletions(-) diff --git a/lib/hooks/use_cover.dart b/lib/hooks/use_cover.dart index 678a9aa..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), []); diff --git a/lib/pages/player/audio.dart b/lib/pages/player/audio.dart index a9b9582..e3b4e63 100644 --- a/lib/pages/player/audio.dart +++ b/lib/pages/player/audio.dart @@ -115,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, @@ -153,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..8a366f0 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'; @@ -73,6 +74,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 +92,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); + player.pause(); + } else { + useAppStore().updateAutoPlay(true); + player.play(); + } + }, + style: ButtonStyle(overlayColor: overlayColor), ), + ], + ); + + final stopButton = IconButton( + tooltip: '${t.stop} ( Ctrl + C )', + icon: Icon( + Icons.stop_rounded, + size: 26, + color: color, ), - child: Column( - children: [ - Visibility( - visible: width < 1024 || !isDesktop, - child: ControlBarSlider( - showControl: showControl, + onPressed: () { + showControl(); + useAppStore().updateAutoPlay(false); + player.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), + ), + ); + + 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( + 'Ctirl + 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( + '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, ), - const SizedBox(width: 2), + ), + ), + onTap: () async { + await player.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/widgets/popup.dart b/lib/widgets/popup.dart index 4b08ade..300359b 100644 --- a/lib/widgets/popup.dart +++ b/lib/widgets/popup.dart @@ -117,7 +117,7 @@ class Popup extends PopupRoute { child: LimitedBox( maxWidth: screenWidth / size - 16, maxHeight: - isDesktop ? screenHeight - 48 : screenHeight - 16, + isDesktop ? screenHeight - 56 : screenHeight - 16, child: Card( child: Material( color: Colors.transparent, From 44e9eb4d65a6d331300707e6a19164bd3f1541d3 Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Thu, 25 Sep 2025 23:02:04 +0800 Subject: [PATCH 4/8] refactor: split gesture overlay --- lib/hooks/use_gesture.dart | 17 +- lib/hooks/use_keyboard.dart | 10 +- lib/hooks/use_volume.dart | 2 - lib/models/storages/storage.dart | 2 +- lib/pages/player/control_bar/control_bar.dart | 10 +- .../control_bar/control_bar_slider.dart | 6 +- .../player/overlays/controls_overlay.dart | 257 +----------------- .../player/overlays/gesture_overlay.dart | 189 +++++++++++++ .../overlays/minimal_progress_overlay.dart | 78 ++++++ lib/pages/player/player.dart | 15 + lib/{widgets => pages/player}/title_bar.dart | 0 .../overlays => widgets/popups}/history.dart | 0 .../popups}/play_queue.dart | 0 .../popups}/settings/about.dart | 0 .../popups}/settings/dependencies.dart | 0 .../popups}/settings/general.dart | 0 .../popups}/settings/play.dart | 0 .../popups}/settings/settings.dart | 8 +- .../popups}/storages/favorites.dart | 0 .../popups}/storages/files.dart | 0 .../popups}/storages/storages.dart | 6 +- .../popups}/storages/storages_list.dart | 0 .../popups}/track/audio_track_list.dart | 0 .../track/subtitle_and_audio_track.dart | 4 +- .../popups}/track/subtitle_list.dart | 0 25 files changed, 329 insertions(+), 275 deletions(-) create mode 100644 lib/pages/player/overlays/gesture_overlay.dart create mode 100644 lib/pages/player/overlays/minimal_progress_overlay.dart rename lib/{widgets => pages/player}/title_bar.dart (100%) rename lib/{pages/player/overlays => widgets/popups}/history.dart (100%) rename lib/{pages/player/overlays => widgets/popups}/play_queue.dart (100%) rename lib/{pages => widgets/popups}/settings/about.dart (100%) rename lib/{pages => widgets/popups}/settings/dependencies.dart (100%) rename lib/{pages => widgets/popups}/settings/general.dart (100%) rename lib/{pages => widgets/popups}/settings/play.dart (100%) rename lib/{pages => widgets/popups}/settings/settings.dart (90%) rename lib/{pages => widgets/popups}/storages/favorites.dart (100%) rename lib/{pages => widgets/popups}/storages/files.dart (100%) rename lib/{pages => widgets/popups}/storages/storages.dart (97%) rename lib/{pages => widgets/popups}/storages/storages_list.dart (100%) rename lib/{pages/player/overlays => widgets/popups}/track/audio_track_list.dart (100%) rename lib/{pages/player/overlays => widgets/popups}/track/subtitle_and_audio_track.dart (94%) rename lib/{pages/player/overlays => widgets/popups}/track/subtitle_list.dart (100%) diff --git a/lib/hooks/use_gesture.dart b/lib/hooks/use_gesture.dart index 817a6a2..1216669 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'; @@ -101,16 +102,16 @@ Gesture useGesture({ 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(); @@ -259,6 +260,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 +292,8 @@ Gesture useGesture({ }; isLeftGesture.value = false; isRightGesture.value = false; + + FlutterVolumeController.updateShowSystemUI(true); } void onPanEnd(DragEndDetails details) => _resetPanState(); diff --git a/lib/hooks/use_keyboard.dart b/lib/hooks/use_keyboard.dart index 25213f2..c300689 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'; 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/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/pages/player/control_bar/control_bar.dart b/lib/pages/player/control_bar/control_bar.dart index 8a366f0..3cde9ff 100644 --- a/lib/pages/player/control_bar/control_bar.dart +++ b/lib/pages/player/control_bar/control_bar.dart @@ -12,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'; 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..861ef03 --- /dev/null +++ b/lib/pages/player/overlays/gesture_overlay.dart @@ -0,0 +1,189 @@ +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 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: 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: 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..b890e7b 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'; @@ -239,11 +241,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/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 100% rename from lib/pages/settings/about.dart rename to lib/widgets/popups/settings/about.dart 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 From b7876bf10b1c0d643e6ba41bdf52eeb4419fe9d5 Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:46:46 +0800 Subject: [PATCH 5/8] feat: add msix check --- lib/info.dart | 1 + lib/widgets/dialogs/show_release_dialog.dart | 12 ++++++++---- lib/widgets/popups/settings/about.dart | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) 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/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/popups/settings/about.dart b/lib/widgets/popups/settings/about.dart index 93cf20f..82a86ea 100644 --- a/lib/widgets/popups/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 batFilePath = p.join(path, 'AppxManifest.xml'); + return File(batFilePath).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) { From a6414701e82d62f8d1a1c23c2923300e2586cf99 Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Fri, 26 Sep 2025 21:49:36 +0800 Subject: [PATCH 6/8] update readme and change log --- CHANGELOG.md | 12 ++++++++++++ README.md | 33 +++++++++++++++------------------ README_CN.md | 35 ++++++++++++++++------------------- inno.iss | 2 +- pubspec.yaml | 4 ++-- 5 files changed, 46 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cb85ff..fee13e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 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. + +### 更新日志 + +* 调整了紧凑布局下的播放控制栏样式 +* 微软商店版本中点击检查更新将跳转至商店页面 + ## 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 -![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) + Afdian [![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 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 - 轻量级视频播放器 -![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) + 爱发电 [![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 秒 | +| 双击屏幕左侧 | 快退 | +| 双击屏幕右侧 | 快进 | | 左右滑动 | 调整播放进度 | | 屏幕左侧上下滑动 | 调整屏幕亮度 | | 屏幕右侧上下滑动 | 调整设备音量 | -| 长按 | 启动倍速播放 | -| 长按后左右滑动 | 调整倍速播放的速度 | +| 长按 | 显示播放速度选择器 | +| 长按后左右滑动 | 调整播放速度 | ## 贡献 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/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 From 4c170f6dd57aa0acf596e53cfdf9f9236b9a3fd5 Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Sun, 5 Oct 2025 09:28:42 +0800 Subject: [PATCH 7/8] fix: forward and backward issues --- CHANGELOG.md | 4 +++- lib/hooks/use_app_lifecycle.dart | 3 +-- lib/hooks/use_gesture.dart | 24 +++++++------------ lib/hooks/use_keyboard.dart | 6 ++--- lib/pages/player/control_bar/control_bar.dart | 10 ++++---- .../player/overlays/gesture_overlay.dart | 10 +++++--- lib/pages/player/player.dart | 6 ++--- 7 files changed, 29 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fee13e0..e494a43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,14 @@ ### Changelog * Adjusted the playback control bar style in compact layout -* Clicking “Check update” in the Microsoft Store version redirects to the Store page. +* Clicking “Check update” in the Microsoft Store version redirects to the Store page +* Fixed forward and backward issues ### 更新日志 * 调整了紧凑布局下的播放控制栏样式 * 微软商店版本中点击检查更新将跳转至商店页面 +* 修复了快退快进的问题 ## v1.5.0 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_gesture.dart b/lib/hooks/use_gesture.dart index 1216669..6751b53 100644 --- a/lib/hooks/use_gesture.dart +++ b/lib/hooks/use_gesture.dart @@ -32,7 +32,6 @@ class Gesture { final bool isRightGesture; final double? brightness; final double? volume; - final MouseCursor cursor; Gesture({ required this.onTapDown, @@ -52,7 +51,6 @@ class Gesture { required this.isRightGesture, required this.brightness, required this.volume, - required this.cursor, }); } @@ -66,8 +64,6 @@ Gesture useGesture({ }) { final context = useContext(); - final player = context.read(); - final gestureState = useRef({ 'isTouch': false, 'isLongPress': false, @@ -98,6 +94,8 @@ 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; @@ -129,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; @@ -207,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; @@ -247,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(); } @@ -306,12 +307,6 @@ Gesture useGesture({ } } - final cursor = useMemoized(() { - return player.isPlaying == false - ? SystemMouseCursors.basic - : SystemMouseCursors.none; - }, [player.isPlaying]); - return Gesture( onTapDown: onTapDown, onTap: onTap, @@ -330,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 c300689..2b2979d 100644 --- a/lib/hooks/use_keyboard.dart +++ b/lib/hooks/use_keyboard.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/pages/player/control_bar/control_bar.dart b/lib/pages/player/control_bar/control_bar.dart index 3cde9ff..71b5bb4 100644 --- a/lib/pages/player/control_bar/control_bar.dart +++ b/lib/pages/player/control_bar/control_bar.dart @@ -52,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); @@ -117,10 +115,10 @@ class ControlBar extends HookWidget { showControl(); if (isPlaying == true) { useAppStore().updateAutoPlay(false); - player.pause(); + context.read().pause(); } else { useAppStore().updateAutoPlay(true); - player.play(); + context.read().play(); } }, style: ButtonStyle(overlayColor: overlayColor), @@ -138,7 +136,7 @@ class ControlBar extends HookWidget { onPressed: () { showControl(); useAppStore().updateAutoPlay(false); - player.pause(); + context.read().pause(); usePlayQueueStore().updateCurrentIndex(-1); }, style: ButtonStyle(overlayColor: overlayColor), @@ -521,7 +519,7 @@ class ControlBar extends HookWidget { ), ), onTap: () async { - await player.saveProgress(); + await context.read().saveProgress(); if (isDesktop) { windowManager.close(); } else { diff --git a/lib/pages/player/overlays/gesture_overlay.dart b/lib/pages/player/overlays/gesture_overlay.dart index 861ef03..d201d00 100644 --- a/lib/pages/player/overlays/gesture_overlay.dart +++ b/lib/pages/player/overlays/gesture_overlay.dart @@ -29,6 +29,12 @@ class GestureOverlay extends HookWidget { 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); @@ -75,9 +81,7 @@ class GestureOverlay extends HookWidget { ); return MouseRegion( - cursor: isShowControl || isPlaying == false - ? SystemMouseCursors.basic - : SystemMouseCursors.none, + cursor: cursor, onHover: gesture.onHover, child: GestureDetector( behavior: HitTestBehavior.opaque, diff --git a/lib/pages/player/player.dart b/lib/pages/player/player.dart index b890e7b..82e303b 100644 --- a/lib/pages/player/player.dart +++ b/lib/pages/player/player.dart @@ -35,8 +35,6 @@ class Player extends HookWidget { final height = context.select((player) => player.height); - final saveProgress = context.read().saveProgress; - useAppLifecycle(); final cover = useCover(); @@ -123,7 +121,7 @@ class Player extends HookWidget { Future showControlForHover(Future callback) async { try { - saveProgress(); + context.read().saveProgress(); showControl(); usePlayerUiStore().updateIsHovering(true); await callback; @@ -216,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 { From 02440b659d84c0447d93dd8ac613a1272775b481 Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Sun, 5 Oct 2025 09:54:59 +0800 Subject: [PATCH 8/8] improve code clarity and maintainability --- lib/pages/player/control_bar/control_bar.dart | 4 ++-- lib/widgets/popups/settings/about.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/player/control_bar/control_bar.dart b/lib/pages/player/control_bar/control_bar.dart index 71b5bb4..ee13b23 100644 --- a/lib/pages/player/control_bar/control_bar.dart +++ b/lib/pages/player/control_bar/control_bar.dart @@ -463,7 +463,7 @@ class ControlBar extends HookWidget { ), title: Text(t.history), trailing: Text( - 'Ctirl + H', + 'Ctrl + H', style: TextStyle( fontSize: 12, color: Theme.of(context).dividerColor, @@ -487,7 +487,7 @@ class ControlBar extends HookWidget { ), title: Text(t.settings), trailing: Text( - 'Ctirl + P', + 'Ctrl + P', style: TextStyle( fontSize: 12, color: Theme.of(context).dividerColor, diff --git a/lib/widgets/popups/settings/about.dart b/lib/widgets/popups/settings/about.dart index 82a86ea..34a1542 100644 --- a/lib/widgets/popups/settings/about.dart +++ b/lib/widgets/popups/settings/about.dart @@ -16,8 +16,8 @@ bool isMsix() { } String resolvedExecutablePath = Platform.resolvedExecutable; String path = p.dirname(resolvedExecutablePath); - String batFilePath = p.join(path, 'AppxManifest.xml'); - return File(batFilePath).existsSync(); + String manifestPath = p.join(path, 'AppxManifest.xml'); + return File(manifestPath).existsSync(); } class About extends HookWidget {