From 94191594632af85cf0ce2e4fdd6826c8f9fc3f62 Mon Sep 17 00:00:00 2001 From: 22 <60903333+nini22P@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:16:41 +0800 Subject: [PATCH] feat: open video in other app on android --- android/app/src/main/AndroidManifest.xml | 9 +- lib/globals.dart | 4 + lib/hooks/use_player_controller.dart | 10 +- lib/hooks/use_player_core.dart | 9 +- lib/main.dart | 24 ++++- lib/models/file.dart | 2 +- lib/models/storages/local.dart | 12 ++- lib/pages/dialog/show_open_link_dialog.dart | 25 +++-- lib/pages/history.dart | 2 +- lib/pages/home_page.dart | 37 +++++++ lib/pages/player/control_bar.dart | 6 +- lib/pages/player/iris_player.dart | 18 ++-- lib/pages/show_open_link_bottom_sheet.dart | 102 ++++++++++++++++++ lib/pages/storage/files.dart | 15 +-- lib/pages/storage/storages.dart | 6 +- lib/store/use_app_store.dart | 17 +-- lib/store/use_history_store.dart | 1 + lib/store/use_play_queue_store.dart | 70 +++++++++--- lib/store/use_storage_store.dart | 1 + lib/utils/decode_uri.dart | 26 +++++ lib/utils/path_conv.dart | 26 +++++ lib/utils/path_converter.dart | 7 -- lib/utils/request_storage_permission.dart | 18 ++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 40 +++++++ pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 30 files changed, 414 insertions(+), 85 deletions(-) create mode 100644 lib/pages/show_open_link_bottom_sheet.dart create mode 100644 lib/utils/decode_uri.dart create mode 100644 lib/utils/path_conv.dart delete mode 100644 lib/utils/path_converter.dart create mode 100644 lib/utils/request_storage_permission.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 55fb651..be6f6df 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ - - + diff --git a/lib/globals.dart b/lib/globals.dart index 44a85c3..d445158 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -1,4 +1,8 @@ // ignore: unnecessary_library_name library my_app.globals; +import 'package:permission_handler/permission_handler.dart'; + List arguments = []; +Uri? initUri; +PermissionStatus? storagePermissionStatus; diff --git a/lib/hooks/use_player_controller.dart b/lib/hooks/use_player_controller.dart index 7646412..2e68b07 100644 --- a/lib/hooks/use_player_controller.dart +++ b/lib/hooks/use_player_controller.dart @@ -83,11 +83,15 @@ PlayerController usePlayerController( Future updateRate(double value) async => playerCore.rate == value ? null : await playerCore.player.setRate(value); - Future shufflePlayQueue() async => usePlayQueueStore() - .update(getShufflePlayQueue(playQueue, currentIndex), currentIndex); + Future shufflePlayQueue() async => usePlayQueueStore().update( + playQueue: getShufflePlayQueue(playQueue, currentIndex), + index: currentIndex, + ); Future sortPlayQueue() async => usePlayQueueStore().update( - [...playQueue]..sort((a, b) => a.index.compareTo(b.index)), currentIndex); + playQueue: [...playQueue]..sort((a, b) => a.index.compareTo(b.index)), + index: currentIndex, + ); return PlayerController( play, diff --git a/lib/hooks/use_player_core.dart b/lib/hooks/use_player_core.dart index 9077dfb..07d972d 100644 --- a/lib/hooks/use_player_core.dart +++ b/lib/hooks/use_player_core.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_zustand/flutter_zustand.dart'; @@ -166,7 +167,7 @@ PlayerCore usePlayerCore(BuildContext context, Player player) { if (currentFile == null || playQueue.isEmpty) { player.stop(); } else { - log('Now playing: ${currentFile.name}, auto play: $autoPlay'); + log('Now playing: ${currentFile.uri}, auto play: $autoPlay'); player.open( Media(currentFile.uri, httpHeaders: currentFile.auth != null @@ -177,6 +178,9 @@ PlayerCore usePlayerCore(BuildContext context, Player player) { } return () { if (currentFile != null && player.state.duration == Duration.zero) { + if (Platform.isAndroid && currentFile.uri.startsWith('content://')) { + return; + } log('Save progress: ${currentFile.name}'); useHistoryStore().add(Progress( dateTime: DateTime.now().toUtc(), @@ -261,6 +265,9 @@ PlayerCore usePlayerCore(BuildContext context, Player player) { Future saveProgress() async { if (currentFile != null && player.state.duration != Duration.zero) { + if (Platform.isAndroid && currentFile.uri.startsWith('content://')) { + return; + } log('Save progress: ${currentFile.name}'); useHistoryStore().add(Progress( dateTime: DateTime.now().toUtc(), diff --git a/lib/main.dart b/lib/main.dart index c919e53..1443244 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:developer'; - +import 'dart:io'; +import 'package:app_links/app_links.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_zustand/flutter_zustand.dart'; @@ -10,18 +11,26 @@ import 'package:iris/store/use_app_store.dart'; import 'package:iris/theme.dart'; import 'package:iris/utils/is_desktop.dart'; import 'package:media_kit/media_kit.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:window_manager/window_manager.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'globals.dart' as globals; void main(List arguments) async { + log('arguments: $arguments'); globals.arguments = arguments; - log('Arguments: $arguments'); - WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); + final appLinks = AppLinks(); + final initUri = await appLinks.getInitialLink(); + + if (initUri != null) { + log('initUri: $initUri'); + globals.initUri = initUri; + } + if (isDesktop) { await windowManager.ensureInitialized(); @@ -49,6 +58,15 @@ class MyApp extends HookWidget { @override Widget build(BuildContext context) { + useEffect(() { + () async { + globals.storagePermissionStatus = Platform.isAndroid + ? await Permission.storage.status + : PermissionStatus.granted; + }(); + return null; + }, []); + ThemeMode themeMode = useAppStore().select(context, (state) => state.themeMode); String language = useAppStore().select(context, (state) => state.language); diff --git a/lib/models/file.dart b/lib/models/file.dart index 9a06360..2d560a4 100644 --- a/lib/models/file.dart +++ b/lib/models/file.dart @@ -30,7 +30,7 @@ abstract class FileItem implements _$FileItem { @Default([]) List path, @Default(false) bool isDir, @Default(0) int size, - required ContentType type, + @Default(ContentType.video) ContentType type, String? auth, @Default([]) List subtitles, }) = _FileItem; diff --git a/lib/models/storages/local.dart b/lib/models/storages/local.dart index d954ab2..c1b97cf 100644 --- a/lib/models/storages/local.dart +++ b/lib/models/storages/local.dart @@ -14,7 +14,7 @@ import 'package:iris/utils/files_sort.dart'; import 'package:iris/utils/find_subtitle.dart'; import 'package:iris/utils/get_localizations.dart'; import 'package:iris/utils/is_desktop.dart'; -import 'package:iris/utils/path_converter.dart'; +import 'package:iris/utils/path_conv.dart'; import 'package:path/path.dart' as p; import 'package:iris/models/file.dart'; import 'package:iris/utils/check_content_type.dart'; @@ -53,7 +53,7 @@ Future> getLocalFiles( storageId: storage.id, storageType: storage.type, name: p.basename(entity.path), - uri: pathConverter(entity.path).join('/'), + uri: pathConv(entity.path).join('/'), path: [...path, p.basename(entity.path)], isDir: isDir, size: size, @@ -164,13 +164,15 @@ Future pickLocalFile() async { allowedExtensions: [...Formats.video, ...Formats.audio]); if (result != null) { - final filePath = pathConverter(result.files.first.path!); + final filePath = pathConv(result.files.first.path!); final playQueue = await getLocalPlayQueue(filePath); if (playQueue == null || playQueue.playQueue.isEmpty) return; await useAppStore().updateAutoPlay(true); - await usePlayQueueStore() - .update(playQueue.playQueue, playQueue.currentIndex); + await usePlayQueueStore().update( + playQueue: playQueue.playQueue, + index: playQueue.currentIndex, + ); } } diff --git a/lib/pages/dialog/show_open_link_dialog.dart b/lib/pages/dialog/show_open_link_dialog.dart index b6e0a9b..c4829f2 100644 --- a/lib/pages/dialog/show_open_link_dialog.dart +++ b/lib/pages/dialog/show_open_link_dialog.dart @@ -18,25 +18,28 @@ class OpenLinkDialog extends HookWidget { final t = getLocalizations(context); final url = useState(''); - // 播放逻辑函数 void play() { if (url.value.isNotEmpty && RegExp(r'^(http://|https://)').hasMatch(url.value)) { - usePlayQueueStore().update([ - PlayQueueItem( - file: FileItem( - name: url.value, - uri: url.value, - type: ContentType.video, - ), - index: 0, - ) - ], 0); + usePlayQueueStore().update( + playQueue: [ + PlayQueueItem( + file: FileItem( + name: url.value, + uri: url.value, + type: ContentType.video, + ), + index: 0, + ) + ], + index: 0, + ); Navigator.pop(context, 'OK'); } } return AlertDialog( + // actionsPadding: const EdgeInsets.symmetric(horizontal: 16), title: Text(t.open_link, style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), content: ConstrainedBox( diff --git a/lib/pages/history.dart b/lib/pages/history.dart index aabda5c..75bd72e 100644 --- a/lib/pages/history.dart +++ b/lib/pages/history.dart @@ -34,7 +34,7 @@ class History extends HookWidget { .values .toList(); - usePlayQueueStore().update(playQueue, index); + usePlayQueueStore().update(playQueue: playQueue, index: index); } return Column( diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 4b58c04..fc7ef76 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,9 +1,17 @@ +import 'dart:developer'; +import 'dart:io'; +import 'package:app_links/app_links.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:iris/models/file.dart'; import 'package:iris/pages/player/iris_player.dart'; +import 'package:iris/store/use_app_store.dart'; +import 'package:iris/store/use_play_queue_store.dart'; import 'package:iris/theme.dart'; +import 'package:iris/utils/decode_uri.dart'; +import 'package:iris/globals.dart' as globals; class HomePage extends HookWidget { const HomePage({super.key}); @@ -19,6 +27,35 @@ class HomePage extends HookWidget { return null; }, []); + final appLinks = useMemoized(() => AppLinks()); + final Uri? uri = useStream(appLinks.uriLinkStream).data; + + useEffect(() { + () async { + if (uri != null && globals.initUri?.path != uri.path) { + log('Uri: $uri'); + if (Platform.isAndroid) { + final decodedPath = decodePath(uri.path); + final fileName = Uri.decodeComponent(decodedPath.last); + await useAppStore().updateAutoPlay(true); + await usePlayQueueStore().update( + playQueue: [ + PlayQueueItem( + file: FileItem( + name: fileName, + uri: uri.toString(), + ), + index: 0, + ), + ], + index: 0, + ); + } + } + }(); + return null; + }, [uri]); + return Scaffold( body: Theme( data: ThemeData.dark(useMaterial3: true).copyWith( diff --git a/lib/pages/player/control_bar.dart b/lib/pages/player/control_bar.dart index 234b848..5ca8d01 100644 --- a/lib/pages/player/control_bar.dart +++ b/lib/pages/player/control_bar.dart @@ -8,6 +8,7 @@ import 'package:iris/models/store/app_state.dart'; import 'package:iris/pages/dialog/show_open_link_dialog.dart'; import 'package:iris/pages/player/control_bar_slider.dart'; import 'package:iris/pages/history.dart'; +import 'package:iris/pages/show_open_link_bottom_sheet.dart'; import 'package:iris/pages/subtitle_and_audio_track.dart'; import 'package:iris/pages/settings/settings.dart'; import 'package:iris/store/use_app_store.dart'; @@ -364,8 +365,9 @@ class ControlBar extends HookWidget { ), ), onTap: () async { - showControl(); - await showOpenLinkDialog(context); + isDesktop + ? await showOpenLinkDialog(context) + : await showOpenLinkBottomSheet(context); showControl(); }, ), diff --git a/lib/pages/player/iris_player.dart b/lib/pages/player/iris_player.dart index 59ef05b..ca05608 100644 --- a/lib/pages/player/iris_player.dart +++ b/lib/pages/player/iris_player.dart @@ -17,10 +17,11 @@ import 'package:iris/pages/player/audio.dart'; import 'package:iris/pages/player/control_bar_slider.dart'; import 'package:iris/pages/history.dart'; import 'package:iris/pages/play_queue.dart'; +import 'package:iris/pages/show_open_link_bottom_sheet.dart'; import 'package:iris/pages/subtitle_and_audio_track.dart'; import 'package:iris/pages/settings/settings.dart'; import 'package:iris/utils/check_content_type.dart'; -import 'package:iris/utils/path_converter.dart'; +import 'package:iris/utils/path_conv.dart'; import 'package:iris/widgets/popup.dart'; import 'package:iris/pages/storage/storages.dart'; import 'package:iris/store/use_app_store.dart'; @@ -309,7 +310,9 @@ class IrisPlayer extends HookWidget { // 打开链接 case LogicalKeyboardKey.keyL: showControl(); - await showOpenLinkDialog(context); + isDesktop + ? await showOpenLinkDialog(context) + : await showOpenLinkBottomSheet(context); showControl(); break; default: @@ -444,7 +447,7 @@ class IrisPlayer extends HookWidget { final files = details.files .map((file) => checkContentType(file.path) == ContentType.video || checkContentType(file.path) == ContentType.audio - ? pathConverter(file.path) + ? pathConv(file.path) : null) .where((element) => element != null) .toList(); @@ -465,7 +468,8 @@ class IrisPlayer extends HookWidget { } if (filteredPlayQueue.isEmpty) return; useAppStore().updateAutoPlay(true); - usePlayQueueStore().update(filteredPlayQueue, playQueue.currentIndex); + usePlayQueueStore().update( + playQueue: filteredPlayQueue, index: playQueue.currentIndex); } }, child: PopScope( @@ -772,8 +776,7 @@ class IrisPlayer extends HookWidget { duration: const Duration(milliseconds: 200), curve: Curves.easeInOutCubicEmphasized, top: isShowControl.value || - playerCore.mediaType != MediaType.video || - !playerCore.playing + playerCore.mediaType != MediaType.video ? 0 : -72, left: 0, @@ -815,8 +818,7 @@ class IrisPlayer extends HookWidget { duration: const Duration(milliseconds: 200), curve: Curves.easeInOutCubicEmphasized, bottom: isShowControl.value || - playerCore.mediaType != MediaType.video || - !playerCore.playing + playerCore.mediaType != MediaType.video ? 0 : -96, left: 0, diff --git a/lib/pages/show_open_link_bottom_sheet.dart b/lib/pages/show_open_link_bottom_sheet.dart new file mode 100644 index 0000000..c06dc1e --- /dev/null +++ b/lib/pages/show_open_link_bottom_sheet.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:iris/models/file.dart'; +import 'package:iris/store/use_play_queue_store.dart'; +import 'package:iris/utils/get_localizations.dart'; + +Future showOpenLinkBottomSheet(BuildContext context) async => + await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => SingleChildScrollView( + child: Padding( + padding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: const OpenLinkBottomSheet(), + ), + ), + ); + +class OpenLinkBottomSheet extends HookWidget { + const OpenLinkBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + final t = getLocalizations(context); + final url = useState(''); + + void play() { + if (url.value.isNotEmpty && + RegExp(r'^(http://|https://)').hasMatch(url.value)) { + usePlayQueueStore().update( + playQueue: [ + PlayQueueItem( + file: FileItem( + name: url.value, + uri: url.value, + type: ContentType.video, + ), + index: 0, + ) + ], + index: 0, + ); + Navigator.pop(context, 'OK'); + } + } + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + t.open_link, + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + TextFormField( + autofocus: true, + initialValue: '', + onChanged: (value) => url.value = value, + keyboardType: TextInputType.url, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant + .withValues(alpha: 0.87)), + decoration: InputDecoration( + hintText: 'https://example.com/xxx.mp4', + hintStyle: TextStyle(color: Theme.of(context).disabledColor), + border: OutlineInputBorder(), + ), + onFieldSubmitted: (value) { + if (value.isNotEmpty && + RegExp(r'^(http://|https://)').hasMatch(value)) { + play(); + } + }, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () => Navigator.pop(context, 'Cancel'), + child: Text(t.cancel), + ), + TextButton( + onPressed: url.value.isNotEmpty && + RegExp(r'^(http://|https://)').hasMatch(url.value) + ? play + : null, + child: Text(t.play), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/pages/storage/files.dart b/lib/pages/storage/files.dart index d54108d..8dc2645 100644 --- a/lib/pages/storage/files.dart +++ b/lib/pages/storage/files.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_zustand/flutter_zustand.dart'; +import 'package:iris/globals.dart' as globals; import 'package:iris/models/file.dart'; import 'package:iris/models/progress.dart'; import 'package:iris/models/storages/storage.dart'; @@ -31,14 +32,6 @@ class Files extends HookWidget { final refreshState = useState(false); void refresh() => refreshState.value = !refreshState.value; - final storageStatusFuture = useMemoized( - () async => !Platform.isAndroid - ? PermissionStatus.granted - : await Permission.storage.status, - [storage, refreshState.value]); - - final storageStatus = useFuture(storageStatusFuture).data; - final basePath = storage.basePath; final favorites = @@ -93,7 +86,7 @@ class Files extends HookWidget { await useAppStore().updateAutoPlay(true); await useAppStore().updateShuffle(false); - await usePlayQueueStore().update(playQueue, newIndex); + await usePlayQueueStore().update(playQueue: playQueue, index: newIndex); } void back() { @@ -111,12 +104,14 @@ class Files extends HookWidget { children: [ Expanded( child: Platform.isAndroid && - storageStatus != PermissionStatus.granted && + globals.storagePermissionStatus != PermissionStatus.granted && storage is LocalStorage ? Center( child: ElevatedButton( onPressed: () async { await Permission.storage.request(); + globals.storagePermissionStatus = + await Permission.storage.status; refresh(); }, child: Text(t.grant_storage_permission)), diff --git a/lib/pages/storage/storages.dart b/lib/pages/storage/storages.dart index 29e340d..3fd2716 100644 --- a/lib/pages/storage/storages.dart +++ b/lib/pages/storage/storages.dart @@ -8,7 +8,7 @@ import 'package:iris/pages/storage/favorites.dart'; import 'package:iris/pages/storage/files.dart'; import 'package:iris/store/use_storage_store.dart'; import 'package:iris/utils/get_localizations.dart'; -import 'package:iris/utils/path_converter.dart'; +import 'package:iris/utils/path_conv.dart'; import 'package:iris/pages/dialog/show_local_dialog.dart'; import 'package:iris/pages/dialog/show_webdav_dialog.dart'; import 'package:iris/pages/storage/storages_list.dart'; @@ -108,8 +108,8 @@ class Storages extends HookWidget { context, storage: LocalStorage( type: value, - name: pathConverter(selectedDirectory).last, - basePath: pathConverter(selectedDirectory), + name: pathConv(selectedDirectory).last, + basePath: pathConv(selectedDirectory), ), ); } diff --git a/lib/store/use_app_store.dart b/lib/store/use_app_store.dart index aaa0e6b..c36734d 100644 --- a/lib/store/use_app_store.dart +++ b/lib/store/use_app_store.dart @@ -3,11 +3,9 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_zustand/flutter_zustand.dart'; -import 'package:iris/models/file.dart'; import 'package:iris/models/store/app_state.dart'; import 'package:iris/store/persistent_store.dart'; import 'package:iris/globals.dart' as globals; -import 'package:iris/utils/check_content_type.dart'; class AppStore extends PersistentStore { AppStore() : super(AppState()); @@ -82,27 +80,18 @@ class AppStore extends PersistentStore { @override Future load() async { + log('Loading AppState'); try { - bool autoPlay = false; - - if (globals.arguments.isNotEmpty && globals.arguments[0].isNotEmpty) { - final uri = globals.arguments[0]; - if (RegExp(r'^(http://|https://)').hasMatch(uri) || - checkContentType(uri) == ContentType.video || - checkContentType(uri) == ContentType.audio) { - autoPlay = true; - } - } - AndroidOptions getAndroidOptions() => const AndroidOptions( encryptedSharedPreferences: true, ); final storage = FlutterSecureStorage(aOptions: getAndroidOptions()); String? appState = await storage.read(key: 'app_state'); + if (appState != null) { return AppState.fromJson(json.decode(appState)).copyWith( - autoPlay: autoPlay, + autoPlay: globals.initUri == null ? false : true, ); } } catch (e) { diff --git a/lib/store/use_history_store.dart b/lib/store/use_history_store.dart index 01bdf63..78c36d9 100644 --- a/lib/store/use_history_store.dart +++ b/lib/store/use_history_store.dart @@ -34,6 +34,7 @@ class HistoryStore extends PersistentStore { @override Future load() async { + log('Loading HistoryState'); try { AndroidOptions getAndroidOptions() => const AndroidOptions( encryptedSharedPreferences: true, diff --git a/lib/store/use_play_queue_store.dart b/lib/store/use_play_queue_store.dart index c93f251..55bafd1 100644 --- a/lib/store/use_play_queue_store.dart +++ b/lib/store/use_play_queue_store.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:developer'; +import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_zustand/flutter_zustand.dart'; @@ -8,22 +9,35 @@ import 'package:iris/models/storages/local.dart'; import 'package:iris/models/store/play_queue_state.dart'; import 'package:iris/store/persistent_store.dart'; import 'package:iris/globals.dart' as globals; -import 'package:iris/utils/check_content_type.dart'; -import 'package:iris/utils/path_converter.dart'; +import 'package:iris/store/use_app_store.dart'; +import 'package:iris/utils/decode_uri.dart'; +import 'package:iris/utils/is_desktop.dart'; +import 'package:iris/utils/path_conv.dart'; class PlayQueueStore extends PersistentStore { PlayQueueStore() : super(PlayQueueState()); - Future update(List playQueue, int? index) async { + Future update({ + required List playQueue, + int? index, + }) async { set(state.copyWith( playQueue: playQueue, currentIndex: index ?? state.currentIndex, )); + if (Platform.isAndroid && + state.playQueue.any((e) => e.file.uri.startsWith('content://'))) { + return; + } await save(state); } Future updateCurrentIndex(int index) async { set(state.copyWith(currentIndex: index)); + if (Platform.isAndroid && + state.playQueue.any((e) => e.file.uri.startsWith('content://'))) { + return; + } await save(state); } @@ -44,6 +58,10 @@ class PlayQueueStore extends PersistentStore { .toList(); set(state.copyWith(playQueue: [...state.playQueue, ...playQueue])); + if (Platform.isAndroid && + state.playQueue.any((e) => e.file.uri.startsWith('content://'))) { + return; + } await save(state); } @@ -70,14 +88,20 @@ class PlayQueueStore extends PersistentStore { )); } } + if (Platform.isAndroid && + state.playQueue.any((e) => e.file.uri.startsWith('content://'))) { + return; + } await save(state); } @override Future load() async { + log('Loading PlayQueueState'); try { - if (globals.arguments.isNotEmpty && globals.arguments[0].isNotEmpty) { - final uri = globals.arguments[0]; + if (isDesktop && globals.arguments.isNotEmpty) { + String uri = globals.arguments[0]; + // 在线播放 if (RegExp(r'^(http://|https://)').hasMatch(uri)) { final state = PlayQueueState( playQueue: [ @@ -85,7 +109,6 @@ class PlayQueueStore extends PersistentStore { file: FileItem( name: uri, uri: uri, - type: ContentType.video, ), index: 0, ) @@ -96,17 +119,36 @@ class PlayQueueStore extends PersistentStore { return state; } - final filePath = pathConverter(uri); - if (checkContentType(filePath.last) == ContentType.video || - checkContentType(filePath.last) == ContentType.audio) { - final state = await getLocalPlayQueue(filePath); - if (state != null && state.playQueue.isNotEmpty) { - save(state); - return state; - } + // 本地播放 + final filePath = pathConv(uri); + final state = await getLocalPlayQueue(filePath); + if (state != null && state.playQueue.isNotEmpty) { + save(state); + return state; } } + final uri = globals.initUri; + + // Android + if (uri != null && Platform.isAndroid) { + final decodedPath = decodePath(uri.path); + final fileName = Uri.decodeComponent(decodedPath.last); + await useAppStore().updateAutoPlay(true); + return PlayQueueState( + playQueue: [ + PlayQueueItem( + file: FileItem( + name: fileName, + uri: uri.toString(), + ), + index: 0, + ), + ], + currentIndex: 0, + ); + } + AndroidOptions getAndroidOptions() => const AndroidOptions( encryptedSharedPreferences: true, ); diff --git a/lib/store/use_storage_store.dart b/lib/store/use_storage_store.dart index b90a454..b2e5c7b 100644 --- a/lib/store/use_storage_store.dart +++ b/lib/store/use_storage_store.dart @@ -63,6 +63,7 @@ class StorageStore extends PersistentStore { @override Future load() async { + log('Loading StorageState'); try { AndroidOptions getAndroidOptions() => const AndroidOptions( encryptedSharedPreferences: true, diff --git a/lib/utils/decode_uri.dart b/lib/utils/decode_uri.dart new file mode 100644 index 0000000..04e709c --- /dev/null +++ b/lib/utils/decode_uri.dart @@ -0,0 +1,26 @@ +import 'package:iris/utils/path_conv.dart'; +import 'package:path/path.dart' as path; + +List decodePath(String uri) { + String processedUri = uri; + + if (processedUri.startsWith('/')) { + processedUri = processedUri.replaceFirst('/', ''); + } + + if (processedUri.contains('%2F') || + processedUri.contains('%252F') || + processedUri.contains('%3A')) { + processedUri = Uri.decodeComponent(processedUri); + } + + processedUri = path.normalize(processedUri); + + if (processedUri.contains('%2F') || + processedUri.contains('%252F') || + processedUri.contains('%3A')) { + return decodePath(processedUri); + } + + return pathConv(processedUri); +} diff --git a/lib/utils/path_conv.dart b/lib/utils/path_conv.dart new file mode 100644 index 0000000..ae04129 --- /dev/null +++ b/lib/utils/path_conv.dart @@ -0,0 +1,26 @@ +import 'dart:developer'; + +import 'package:path/path.dart' as p; + +List pathConv(String path) { + try { + String normalizedPath = p.normalize(path); + + if (normalizedPath.isEmpty || normalizedPath == '.') { + return []; + } + + if (normalizedPath == '/' || normalizedPath == '\\') { + return ['/']; + } + + return normalizedPath + .replaceAll('\\', '/') + .split('/') + .where((element) => element.isNotEmpty) + .toList(); + } on FormatException catch (e) { + log("Error decoding: $e"); + return []; + } +} diff --git a/lib/utils/path_converter.dart b/lib/utils/path_converter.dart deleted file mode 100644 index 1081b85..0000000 --- a/lib/utils/path_converter.dart +++ /dev/null @@ -1,7 +0,0 @@ -List pathConverter(String path) { - return path - .replaceAll('\\', '/') - .split('/') - .where((e) => e.isNotEmpty) - .toList(); -} diff --git a/lib/utils/request_storage_permission.dart b/lib/utils/request_storage_permission.dart new file mode 100644 index 0000000..9860897 --- /dev/null +++ b/lib/utils/request_storage_permission.dart @@ -0,0 +1,18 @@ +import 'dart:io'; + +import 'package:iris/globals.dart' as globals; +import 'package:permission_handler/permission_handler.dart'; + +Future requestStoragePermission() async { + if (!Platform.isAndroid) { + return; + } + if (globals.storagePermissionStatus != PermissionStatus.granted) { + globals.storagePermissionStatus = await Permission.storage.request(); + if (globals.storagePermissionStatus != PermissionStatus.granted) { + return await requestStoragePermission(); + } else { + return; + } + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index ccad4ac..5a79f8e 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index d3327b4..610455b 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST desktop_drop dynamic_color flutter_secure_storage_linux + gtk media_kit_libs_linux media_kit_video screen_retriever_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 1ee8dd2..9f97334 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import app_links import desktop_drop import disks_desktop import dynamic_color @@ -21,6 +22,7 @@ import window_manager import window_size func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DisksDesktopPlugin.register(with: registry.registrar(forPlugin: "DisksDesktopPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) diff --git a/pubspec.lock b/pubspec.lock index ab60aaa..ac139a8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -30,6 +30,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" archive: dependency: transitive description: @@ -482,6 +514,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" http: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 752c906..acb5bf6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: android_x_storage: ^1.0.2 permission_handler: ^11.3.1 desktop_drop: ^0.5.0 + app_links: ^6.3.3 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 18aae82..ddf1cf6 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -20,6 +21,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); DesktopDropPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopDropPlugin")); DisksDesktopPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index d5d683f..e138ecc 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links desktop_drop disks_desktop dynamic_color