feat: open video in other app on android

This commit is contained in:
22
2025-01-23 17:16:41 +08:00
parent ebdd171dee
commit 9419159463
30 changed files with 414 additions and 85 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,7 @@ class History extends HookWidget {
.values
.toList();
usePlayQueueStore().update(playQueue, index);
usePlayQueueStore().update(playQueue: playQueue, index: index);
}
return Column(

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ class HistoryStore extends PersistentStore<HistoryState> {
@override
Future<HistoryState?> load() async {
log('Loading HistoryState');
try {
AndroidOptions getAndroidOptions() => const AndroidOptions(
encryptedSharedPreferences: true,

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
List<String> pathConverter(String path) {
return path
.replaceAll('\\', '/')
.split('/')
.where((e) => e.isNotEmpty)
.toList();
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
app_links
desktop_drop
disks_desktop
dynamic_color