feat(filesystem): rewrite filesystem interface for better compatability

This commit is contained in:
Jagandeep Brar
2022-03-16 23:23:06 -04:00
parent 13c08ddf57
commit 4c60df5523
22 changed files with 248 additions and 213 deletions

View File

@@ -264,6 +264,7 @@
"lunasea.HoursAgo": "{} Hours Ago",
"lunasea.IncorrectEncryptionKey": "Incorrect encryption key",
"lunasea.Internal": "Internal",
"lunasea.InvalidFileTypeSelected": "Invalid File Type Selected",
"lunasea.JustNow": "Just Now",
"lunasea.Module": "Module",
"lunasea.ModuleIsNotEnabled": "{} Is Not Enabled",
@@ -274,6 +275,7 @@
"lunasea.Options": "Options",
"lunasea.Page": "Page",
"lunasea.PlatformSpecific": "Platform-Specific",
"lunasea.PleaseTryAgain": "Please Try Again",
"lunasea.Production": "Production",
"lunasea.Refresh": "Refresh",
"lunasea.Refreshing": "Refreshing…",

View File

@@ -1,6 +1,5 @@
// Packages
export 'package:cached_network_image/cached_network_image.dart';
export 'package:easy_localization/easy_localization.dart';
export 'package:expandable/expandable.dart';
export 'package:fading_edge_scrollview/fading_edge_scrollview.dart';
export 'package:flash/flash.dart';

View File

@@ -1,127 +1,2 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lunasea/core.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
class LunaFileSystem {
/// Export a given byte array to the filesystem.
///
/// Depending on the platform, uses different methods to export the data:
/// - iOS/Android: Utilizes system-level sharesheet
/// - macOS: Uses standard OS save dialog
Future<bool> export(BuildContext context, String name, List<int> data) async {
if (Platform.isAndroid || Platform.isIOS) {
return _exportMobile(context, name, data);
} else if (Platform.isMacOS) {
return _exportDesktop(name, data);
} else {
throw MissingPluginException(
'No plugin to handle exporting data is available for this platform.');
}
}
/// Import a file from the filesystem.
///
/// Depending on the platform, uses different methods to import the data:
/// - iOS/Android: Utilizes `file_picker` to use mobile-OS file picker
/// - macOS: Uses standard OS file picker
Future<File?> import(
BuildContext context, List<String> acceptedExtensions) async {
if (Platform.isAndroid || Platform.isIOS) {
return _importMobile(context, acceptedExtensions);
} else if (Platform.isMacOS) {
return _importDesktop(acceptedExtensions);
} else {
throw MissingPluginException(
'No plugin to handle importing data is available for this platform.');
}
}
/// Import a file from the filesystem (mobile).
///
/// Allows selection of any filetype, but will check the extension of the selected file against [acceptedExtensions].
/// If not found in the array, or if any error occurs, will return null.
Future<File?> _importMobile(
BuildContext context, List<String> acceptedExtensions) async {
try {
FilePickerResult? _file = await FilePicker.platform.pickFiles(
type: FileType.any,
allowMultiple: false,
allowCompression: false,
withData: false,
);
if (_file != null &&
acceptedExtensions.contains(_file.files[0].extension ?? '')) {
return File(_file.files[0].path!);
}
} catch (error, stack) {
LunaLogger().error('Failed to import data from filesystem', error, stack);
}
return null;
}
/// Export a given byte array to the OS-level share sheet with the given name.
///
/// Temporarily writes the byte array as a [File] to the temporary storage directory on the OS.
///
/// Temporary storage is eventually cleared by the OS, but is more than enough time to save/send via the share sheet.
Future<bool> _exportMobile(
BuildContext context, String name, List<int> data) async {
try {
final RenderBox box = context.findRenderObject() as RenderBox;
Directory tempDirectory = await getTemporaryDirectory();
String path = '${tempDirectory.path}/$name';
File file = File(path);
await file.writeAsBytes(data);
await Share.shareFiles(
[path],
sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size,
);
return true;
} catch (error, stack) {
LunaLogger().error('Failed to export data to sharesheet', error, stack);
}
return false;
}
/// Export the given data to the OS-level filesystem save dialog with the given suggested name.
///
/// Prompts the user with a save dialog prompt with the supplied name as the recommended name.
Future<bool> _exportDesktop(String name, List<int> data) async {
try {
String? path =
await FileSelectorPlatform.instance.getSavePath(suggestedName: name);
if (path?.isNotEmpty ?? false) {
File file = File(path!);
await file.writeAsBytes(data);
return true;
}
} catch (error, stack) {
LunaLogger().error('Failed to export data to filesystem', error, stack);
}
return false;
}
/// Import a file from the filesystem (Desktop).
///
/// Locks selection to the given [acceptedExtensions].
/// If any error occurs, will return null.
Future<File?> _importDesktop(List<String> acceptedExtensions) async {
try {
final typeGroup = XTypeGroup(
label: 'types',
extensions: acceptedExtensions,
);
XFile? file = await FileSelectorPlatform.instance
.openFile(acceptedTypeGroups: [typeGroup]);
return File(file!.path);
} catch (error, stack) {
LunaLogger().error('Failed to import data from filesystem', error, stack);
}
return null;
}
}
export 'filesystem/file.dart';
export 'filesystem/filesystem.dart';

View File

@@ -0,0 +1,9 @@
class LunaFile {
String path;
List<int> data;
LunaFile({
required this.path,
required this.data,
});
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'file.dart';
import './platform/filesystem_stub.dart'
if (dart.library.io) './platform/filesystem_io.dart'
if (dart.library.html) './platform/filesystem_web.dart';
abstract class LunaFileSystem {
factory LunaFileSystem() => getFileSystem();
Future<bool> save(BuildContext context, String name, List<int> data) async {
throw UnsupportedError('LunaFileSystem unsupported');
}
Future<LunaFile?> read(BuildContext context, List<String> extensions) async {
throw UnsupportedError('LunaFileSystem unsupported');
}
static bool isValidExtension(List<String> extensions, String? extension) {
String _ext = extension ?? '';
return extensions.contains(_ext);
}
}

View File

@@ -0,0 +1,4 @@
import '../filesystem.dart';
import './platform_web.dart';
LunaFileSystem getFileSystem() => Web();

View File

@@ -0,0 +1,21 @@
import 'package:flutter/foundation.dart';
import '../filesystem.dart';
import './platform_desktop.dart';
import './platform_mobile.dart';
LunaFileSystem getFileSystem() {
switch (defaultTargetPlatform) {
// Mobile
case TargetPlatform.android:
case TargetPlatform.iOS:
return Mobile();
// Desktop
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return Desktop();
default:
throw UnsupportedError('LunaFileSystem unsupported');
}
}

View File

@@ -0,0 +1,4 @@
import '../filesystem.dart';
LunaFileSystem getFileSystem() =>
throw UnsupportedError('LunaFileSystem unsupported');

View File

@@ -0,0 +1,56 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import '../../../ui.dart';
import '../../../utilities/logger.dart';
import '../filesystem.dart';
import '../file.dart';
class Desktop implements LunaFileSystem {
@override
Future<bool> save(BuildContext context, String name, List<int> data) async {
try {
String? path = await FilePicker.platform.saveFile(
fileName: name,
lockParentWindow: true,
);
if (path != null) {
File file = File(path);
file.writeAsBytesSync(data);
return true;
}
return false;
} catch (error, stack) {
LunaLogger().error('Failed to save to filesystem', error, stack);
rethrow;
}
}
@override
Future<LunaFile?> read(BuildContext context, List<String> extensions) async {
try {
final result = await FilePicker.platform.pickFiles(withData: true);
if (result?.files.isNotEmpty ?? false) {
String? _ext = result!.files[0].extension;
if (LunaFileSystem.isValidExtension(extensions, _ext)) {
return LunaFile(
path: result.files[0].path!,
data: result.files[0].bytes!,
);
} else {
showLunaInfoSnackBar(
title: 'lunasea.InvalidFileTypeSelected'.tr(),
message: 'lunasea.PleaseTryAgain'.tr(),
);
}
}
return null;
} catch (error, stack) {
LunaLogger().error('Failed to read from filesystem', error, stack);
rethrow;
}
}
}

View File

@@ -0,0 +1,69 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import '../../../ui.dart';
import '../../../utilities/logger.dart';
import '../filesystem.dart';
import '../file.dart';
class Mobile implements LunaFileSystem {
@override
Future<bool> save(BuildContext context, String name, List<int> data) async {
try {
Directory directory = await getTemporaryDirectory();
String path = '${directory.path}/$name';
File file = File(path);
file.writeAsBytesSync(data);
// Determine share window position
RenderBox? box = context.findRenderObject() as RenderBox?;
Rect? rect;
if (box != null) rect = box.localToGlobal(Offset.zero) & box.size;
ShareResult result = await Share.shareFilesWithResult(
[path],
sharePositionOrigin: rect,
);
switch (result.status) {
case ShareResultStatus.success:
return true;
case ShareResultStatus.unavailable:
case ShareResultStatus.dismissed:
return false;
}
} catch (error, stack) {
LunaLogger().error('Failed to save to filesystem', error, stack);
rethrow;
}
}
@override
Future<LunaFile?> read(BuildContext context, List<String> extensions) async {
try {
final result = await FilePicker.platform.pickFiles(withData: true);
if (result?.files.isNotEmpty ?? false) {
String? _ext = result!.files[0].extension;
if (LunaFileSystem.isValidExtension(extensions, _ext)) {
return LunaFile(
path: result.files[0].path!,
data: result.files[0].bytes!,
);
} else {
showLunaInfoSnackBar(
title: 'lunasea.InvalidFileTypeSelected'.tr(),
message: 'lunasea.PleaseTryAgain'.tr(),
);
}
}
return null;
} catch (error, stack) {
LunaLogger().error('Failed to read from filesystem', error, stack);
rethrow;
}
}
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import '../filesystem.dart';
import '../file.dart';
class Web implements LunaFileSystem {
@override
Future<bool> save(BuildContext context, String name, List<int> data) async {
throw UnsupportedError('LunaFileSystem unsupported');
}
@override
Future<LunaFile?> read(BuildContext context, List<String> extensions) async {
throw UnsupportedError('LunaFileSystem unsupported');
}
}

View File

@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
export 'package:easy_localization/easy_localization.dart';
export 'ui/appbar.dart';
export 'ui/banner.dart';
export 'ui/block.dart';

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/nzbget.dart';
@@ -158,23 +157,25 @@ class _State extends State<NZBGet> {
Future<void> _addByFile() async {
try {
File? _file = await LunaFileSystem().import(context, ['nzb']);
LunaFile? _file = await LunaFileSystem().read(context, [
'nzb',
]);
if (_file != null) {
List<int> _data = _file.readAsBytesSync();
String _name = _file.path.substring(_file.path.lastIndexOf('/') + 1);
if (_data.isNotEmpty)
await _api.uploadFile(_data, _name).then((value) {
if (_file.data.isNotEmpty) {
await _api.uploadFile(_file.data, _name).then((value) {
_refreshKeys[0]?.currentState?.show();
showLunaSuccessSnackBar(
title: 'Uploaded NZB (File)',
message: _name,
);
});
} else {
showLunaErrorSnackBar(
title: 'Failed to Upload NZB',
message: 'Please select a valid file type',
);
} else {
showLunaErrorSnackBar(
title: 'Failed to Upload NZB',
message: 'Please select a valid file',
);
}
}
} catch (error, stack) {
LunaLogger().error('Failed to add NZB by file', error, stack);

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/sabnzbd.dart';
@@ -211,24 +210,28 @@ class _State extends State<SABnzbd> {
Future<void> _addByFile() async {
try {
File? _file =
await LunaFileSystem().import(context, ['nzb', 'zip', 'rar', 'gz']);
LunaFile? _file = await LunaFileSystem().read(context, [
'nzb',
'zip',
'rar',
'gz',
]);
if (_file != null) {
List<int> _data = _file.readAsBytesSync();
String _name = _file.path.substring(_file.path.lastIndexOf('/') + 1);
if (_data.isNotEmpty)
await _api.uploadFile(_data, _name).then((value) {
if (_file.data.isNotEmpty) {
await _api.uploadFile(_file.data, _name).then((value) {
_refreshKeys[0]?.currentState?.show();
showLunaSuccessSnackBar(
title: 'Uploaded NZB (File)',
message: _name,
);
});
} else {
showLunaErrorSnackBar(
title: 'Failed to Upload NZB',
message: 'Please select a valid file type',
);
} else {
showLunaErrorSnackBar(
title: 'Failed to Upload NZB',
message: 'Please select a valid file type',
);
}
}
} catch (error, stack) {
LunaLogger().error('Failed to add NZB by file', error, stack);

View File

@@ -95,8 +95,11 @@ extension SearchDownloadTypeExtension on SearchDownloadType {
.api
.downloadRelease(data)
.then((download) async {
bool result = await LunaFileSystem()
.export(context, '$cleanTitle.nzb', utf8.encode(download!));
bool result = await LunaFileSystem().save(
context,
'$cleanTitle.nzb',
utf8.encode(download!),
);
if (result)
showLunaSuccessSnackBar(
title: 'Saved NZB', message: 'NZB has been successfully saved');

View File

@@ -26,7 +26,7 @@ class SettingsSystemBackupRestoreBackupTile extends StatelessWidget {
String encrypted = LunaEncryption().encrypt(_values.item2, data);
String name = DateFormat('y-MM-dd kk-mm-ss').format(DateTime.now());
if (encrypted != LunaEncryption.ENCRYPTION_FAILURE) {
bool result = await LunaFileSystem().export(
bool result = await LunaFileSystem().save(
context,
'$name.lunasea',
utf8.encode(encrypted),

View File

@@ -1,4 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:lunasea/core.dart';
import 'package:lunasea/modules/settings.dart';
@@ -20,13 +19,15 @@ class SettingsSystemBackupRestoreRestoreTile extends StatelessWidget {
Future<void> _restore(BuildContext context) async {
try {
File? file = await LunaFileSystem().import(context, ['lunasea']);
LunaFile? file = await LunaFileSystem().read(context, ['lunasea']);
if (file != null) {
String _data = file.readAsStringSync();
Tuple2<bool, String> _key =
await SettingsDialogs().decryptBackup(context);
if (_key.item1) {
String _decrypted = LunaEncryption().decrypt(_key.item2, _data);
String _decrypted = LunaEncryption().decrypt(
_key.item2,
String.fromCharCodes(file.data),
);
if (_decrypted != LunaEncryption.ENCRYPTION_FAILURE) {
LunaConfiguration().import(context, _decrypted).then(
(_) => showLunaSuccessSnackBar(
@@ -41,11 +42,6 @@ class SettingsSystemBackupRestoreRestoreTile extends StatelessWidget {
);
}
}
} else {
showLunaErrorSnackBar(
title: 'Failed to Restore',
message: 'Please select a valid file type',
);
}
} catch (error, stack) {
LunaLogger().error('Restore Failed', error, stack);

View File

@@ -105,7 +105,7 @@ class _State extends State<_Widget> with LunaScrollControllerMixin {
);
String data = await LunaLogger().exportLogs();
bool result = await LunaFileSystem()
.export(context, 'logs.json', utf8.encode(data));
.save(context, 'logs.json', utf8.encode(data));
if (result)
showLunaSuccessSnackBar(
title: 'Saved Logs',

View File

@@ -34,6 +34,7 @@
"lunasea.HoursAgo": "{} Hours Ago",
"lunasea.IncorrectEncryptionKey": "Incorrect encryption key",
"lunasea.Internal": "Internal",
"lunasea.InvalidFileTypeSelected": "Invalid File Type Selected",
"lunasea.JustNow": "Just Now",
"lunasea.Module": "Module",
"lunasea.ModuleIsNotEnabled": "{} Is Not Enabled",
@@ -44,6 +45,7 @@
"lunasea.Options": "Options",
"lunasea.Page": "Page",
"lunasea.PlatformSpecific": "Platform-Specific",
"lunasea.PleaseTryAgain": "Please Try Again",
"lunasea.Production": "Production",
"lunasea.Refresh": "Refresh",
"lunasea.Refreshing": "Refreshing…",

View File

@@ -6,7 +6,6 @@ import FlutterMacOS
import Foundation
import cloud_firestore
import file_selector_macos
import firebase_auth
import firebase_core
import firebase_messaging
@@ -21,7 +20,6 @@ import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))

View File

@@ -50,13 +50,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
build:
dependency: transitive
description:
@@ -204,13 +197,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
cross_file:
dependency: transitive
description:
name: cross_file
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.2"
crypto:
dependency: transitive
description:
@@ -309,13 +295,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
ffi:
dependency: transitive
description:
@@ -337,20 +316,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.5.1"
file_selector_macos:
dependency: "direct main"
description:
name: file_selector_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.2"
file_selector_platform_interface:
dependency: "direct main"
description:
name: file_selector_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.4"
firebase_auth:
dependency: "direct main"
description:
@@ -522,11 +487,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.0"
flutter_test:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
@@ -1258,13 +1218,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.8"
timing:
dependency: transitive
description:

View File

@@ -21,8 +21,6 @@ dependencies:
expandable: ^5.0.1
fading_edge_scrollview: ^2.0.1
file_picker: ^4.5.1
file_selector_macos: ^0.8.2
file_selector_platform_interface: ^2.0.4
firebase_auth: ^3.3.11
firebase_core: ^1.13.1
firebase_messaging: ^11.2.11