mirror of
https://github.com/nini22P/iris.git
synced 2026-06-20 08:52:19 +08:00
feat: open video in other app on android
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<application
|
||||
android:label="Iris"
|
||||
android:name="${applicationName}"
|
||||
@@ -10,7 +9,7 @@
|
||||
android:fullBackupContent="false"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
>
|
||||
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
||||
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleInstance" android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
@@ -20,6 +19,12 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
// ignore: unnecessary_library_name
|
||||
library my_app.globals;
|
||||
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
List<String> arguments = [];
|
||||
Uri? initUri;
|
||||
PermissionStatus? storagePermissionStatus;
|
||||
|
||||
@@ -83,11 +83,15 @@ PlayerController usePlayerController(
|
||||
Future<void> updateRate(double value) async =>
|
||||
playerCore.rate == value ? null : await playerCore.player.setRate(value);
|
||||
|
||||
Future<void> shufflePlayQueue() async => usePlayQueueStore()
|
||||
.update(getShufflePlayQueue(playQueue, currentIndex), currentIndex);
|
||||
Future<void> shufflePlayQueue() async => usePlayQueueStore().update(
|
||||
playQueue: getShufflePlayQueue(playQueue, currentIndex),
|
||||
index: currentIndex,
|
||||
);
|
||||
|
||||
Future<void> 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,
|
||||
|
||||
@@ -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<void> 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(),
|
||||
|
||||
@@ -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<String> 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);
|
||||
|
||||
@@ -30,7 +30,7 @@ abstract class FileItem implements _$FileItem {
|
||||
@Default([]) List<String> path,
|
||||
@Default(false) bool isDir,
|
||||
@Default(0) int size,
|
||||
required ContentType type,
|
||||
@Default(ContentType.video) ContentType type,
|
||||
String? auth,
|
||||
@Default([]) List<Subtitle> subtitles,
|
||||
}) = _FileItem;
|
||||
|
||||
@@ -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<List<FileItem>> 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<void> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -34,7 +34,7 @@ class History extends HookWidget {
|
||||
.values
|
||||
.toList();
|
||||
|
||||
usePlayQueueStore().update(playQueue, index);
|
||||
usePlayQueueStore().update(playQueue: playQueue, index: index);
|
||||
}
|
||||
|
||||
return Column(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
102
lib/pages/show_open_link_bottom_sheet.dart
Normal file
102
lib/pages/show_open_link_bottom_sheet.dart
Normal file
@@ -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<void> showOpenLinkBottomSheet(BuildContext context) async =>
|
||||
await showModalBottomSheet<void>(
|
||||
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: <Widget>[
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<AppState> {
|
||||
AppStore() : super(AppState());
|
||||
@@ -82,27 +80,18 @@ class AppStore extends PersistentStore<AppState> {
|
||||
|
||||
@override
|
||||
Future<AppState?> 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) {
|
||||
|
||||
@@ -34,6 +34,7 @@ class HistoryStore extends PersistentStore<HistoryState> {
|
||||
|
||||
@override
|
||||
Future<HistoryState?> load() async {
|
||||
log('Loading HistoryState');
|
||||
try {
|
||||
AndroidOptions getAndroidOptions() => const AndroidOptions(
|
||||
encryptedSharedPreferences: true,
|
||||
|
||||
@@ -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<PlayQueueState> {
|
||||
PlayQueueStore() : super(PlayQueueState());
|
||||
|
||||
Future<void> update(List<PlayQueueItem> playQueue, int? index) async {
|
||||
Future<void> update({
|
||||
required List<PlayQueueItem> 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<void> 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<PlayQueueState> {
|
||||
.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<PlayQueueState> {
|
||||
));
|
||||
}
|
||||
}
|
||||
if (Platform.isAndroid &&
|
||||
state.playQueue.any((e) => e.file.uri.startsWith('content://'))) {
|
||||
return;
|
||||
}
|
||||
await save(state);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PlayQueueState?> 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<PlayQueueState> {
|
||||
file: FileItem(
|
||||
name: uri,
|
||||
uri: uri,
|
||||
type: ContentType.video,
|
||||
),
|
||||
index: 0,
|
||||
)
|
||||
@@ -96,17 +119,36 @@ class PlayQueueStore extends PersistentStore<PlayQueueState> {
|
||||
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,
|
||||
);
|
||||
|
||||
@@ -63,6 +63,7 @@ class StorageStore extends PersistentStore<StorageState> {
|
||||
|
||||
@override
|
||||
Future<StorageState?> load() async {
|
||||
log('Loading StorageState');
|
||||
try {
|
||||
AndroidOptions getAndroidOptions() => const AndroidOptions(
|
||||
encryptedSharedPreferences: true,
|
||||
|
||||
26
lib/utils/decode_uri.dart
Normal file
26
lib/utils/decode_uri.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:iris/utils/path_conv.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
List<String> 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);
|
||||
}
|
||||
26
lib/utils/path_conv.dart
Normal file
26
lib/utils/path_conv.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
List<String> 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 [];
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
List<String> pathConverter(String path) {
|
||||
return path
|
||||
.replaceAll('\\', '/')
|
||||
.split('/')
|
||||
.where((e) => e.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
18
lib/utils/request_storage_permission.dart
Normal file
18
lib/utils/request_storage_permission.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:iris/globals.dart' as globals;
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
Future<void> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <dynamic_color/dynamic_color_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
#include <media_kit_video/media_kit_video_plugin.h>
|
||||
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
40
pubspec.lock
40
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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <desktop_drop/desktop_drop_plugin.h>
|
||||
#include <disks_desktop/disks_desktop_plugin.h>
|
||||
#include <dynamic_color/dynamic_color_plugin_c_api.h>
|
||||
@@ -20,6 +21,8 @@
|
||||
#include <window_size/window_size_plugin.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AppLinksPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||
DesktopDropPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
||||
DisksDesktopPluginRegisterWithRegistrar(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
desktop_drop
|
||||
disks_desktop
|
||||
dynamic_color
|
||||
|
||||
Reference in New Issue
Block a user