From 430b6eeb3acb5a5ed24ae2bbbc2d35f5a87c120b Mon Sep 17 00:00:00 2001 From: nyne <67669799+wgh136@users.noreply.github.com> Date: Fri, 29 Nov 2024 21:33:28 +0800 Subject: [PATCH] Feat/saf (#81) * [Android] Use SAF to change local path * Use IOOverrides to replace openDirectoryPlatform and openFilePlatform * fix io --- lib/components/components.dart | 1 - .../image_provider/base_image_provider.dart | 3 +- .../image_provider/cached_image.dart | 3 +- lib/foundation/local.dart | 24 ++--- lib/main.dart | 70 ++++++------- lib/network/download.dart | 39 ++++---- lib/pages/favorites/favorites_page.dart | 3 - lib/pages/reader/images.dart | 2 +- lib/pages/reader/scaffold.dart | 4 +- lib/pages/settings/app.dart | 21 +--- lib/utils/cbz.dart | 2 +- lib/utils/import_comic.dart | 8 +- lib/utils/io.dart | 97 ++++++++++++------- pubspec.lock | 4 +- 14 files changed, 147 insertions(+), 134 deletions(-) diff --git a/lib/components/components.dart b/lib/components/components.dart index 90c7d93..b4b2dd8 100644 --- a/lib/components/components.dart +++ b/lib/components/components.dart @@ -25,7 +25,6 @@ import 'package:venera/network/cloudflare.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/favorites/favorites_page.dart'; import 'package:venera/utils/ext.dart'; -import 'package:venera/utils/io.dart'; import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; diff --git a/lib/foundation/image_provider/base_image_provider.dart b/lib/foundation/image_provider/base_image_provider.dart index 57dd120..f7c981b 100644 --- a/lib/foundation/image_provider/base_image_provider.dart +++ b/lib/foundation/image_provider/base_image_provider.dart @@ -1,5 +1,4 @@ import 'dart:async' show Future, StreamController, scheduleMicrotask; -import 'dart:collection'; import 'dart:convert'; import 'dart:ui' as ui show Codec; import 'dart:ui'; @@ -108,7 +107,7 @@ abstract class BaseImageProvider> } } - static final _cache = LinkedHashMap(); + static final _cache = {}; static var _cacheSize = 0; diff --git a/lib/foundation/image_provider/cached_image.dart b/lib/foundation/image_provider/cached_image.dart index a962f3d..701eb76 100644 --- a/lib/foundation/image_provider/cached_image.dart +++ b/lib/foundation/image_provider/cached_image.dart @@ -1,5 +1,4 @@ import 'dart:async' show Future, StreamController; -import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:venera/network/images.dart'; @@ -25,7 +24,7 @@ class CachedImageProvider @override Future load(StreamController chunkEvents) async { if(url.startsWith("file://")) { - var file = openFilePlatform(url.substring(7)); + var file = File(url.substring(7)); return file.readAsBytes(); } await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) { diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 41bc3a5..6558df9 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -71,7 +71,7 @@ class LocalComic with HistoryMixin implements Comic { downloadedChapters = List.from(jsonDecode(row[8] as String)), createdAt = DateTime.fromMillisecondsSinceEpoch(row[9] as int); - File get coverFile => openFilePlatform(FilePath.join( + File get coverFile => File(FilePath.join( baseDir, cover, )); @@ -151,6 +151,8 @@ class LocalManager with ChangeNotifier { /// path to the directory where all the comics are stored late String path; + Directory get directory => Directory(path); + // return error message if failed Future setNewPath(String newPath) async { var newDir = Directory(newPath); @@ -162,7 +164,7 @@ class LocalManager with ChangeNotifier { } try { await copyDirectoryIsolate( - Directory(path), + directory, newDir, ); await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath); @@ -170,7 +172,7 @@ class LocalManager with ChangeNotifier { Log.error("IO", e, s); return e.toString(); } - await Directory(path).deleteIgnoreError(recursive:true); + await directory.deleteContents(recursive: true); path = newPath; return null; } @@ -217,15 +219,15 @@ class LocalManager with ChangeNotifier { '''); if (File(FilePath.join(App.dataPath, 'local_path')).existsSync()) { path = File(FilePath.join(App.dataPath, 'local_path')).readAsStringSync(); - if (!Directory(path).existsSync()) { + if (!directory.existsSync()) { path = await findDefaultPath(); } } else { path = await findDefaultPath(); } try { - if (!Directory(path).existsSync()) { - await Directory(path).create(); + if (!directory.existsSync()) { + await directory.create(); } } catch(e, s) { @@ -354,12 +356,12 @@ class LocalManager with ChangeNotifier { throw "Invalid ep"; } var comic = find(id, type) ?? (throw "Comic Not Found"); - var directory = openDirectoryPlatform(comic.baseDir); + var directory = Directory(comic.baseDir); if (comic.chapters != null) { var cid = ep is int ? comic.chapters!.keys.elementAt(ep - 1) : (ep as String); - directory = openDirectoryPlatform(FilePath.join(directory.path, cid)); + directory = Directory(FilePath.join(directory.path, cid)); } var files = []; await for (var entity in directory.list()) { @@ -406,10 +408,10 @@ class LocalManager with ChangeNotifier { String id, ComicType type, String name) async { var comic = find(id, type); if (comic != null) { - return openDirectoryPlatform(FilePath.join(path, comic.directory)); + return Directory(FilePath.join(path, comic.directory)); } var dir = findValidDirectoryName(path, name); - return openDirectoryPlatform(FilePath.join(path, dir)).create().then((value) => value); + return Directory(FilePath.join(path, dir)).create().then((value) => value); } void completeTask(DownloadTask task) { @@ -468,7 +470,7 @@ class LocalManager with ChangeNotifier { void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) { if(removeFileOnDisk) { - var dir = openDirectoryPlatform(FilePath.join(path, c.directory)); + var dir = Directory(FilePath.join(path, c.directory)); dir.deleteIgnoreError(recursive: true); } //Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted. diff --git a/lib/main.dart b/lib/main.dart index 8d11045..01cd575 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,40 +20,42 @@ void main(List args) { if (runWebViewTitleBarWidget(args)) { return; } - runZonedGuarded(() async { - await Rhttp.init(); - WidgetsFlutterBinding.ensureInitialized(); - await init(); - if (App.isAndroid) { - handleLinks(); - } - FlutterError.onError = (details) { - Log.error( - "Unhandled Exception", "${details.exception}\n${details.stack}"); - }; - runApp(const MyApp()); - if (App.isDesktop) { - await windowManager.ensureInitialized(); - windowManager.waitUntilReadyToShow().then((_) async { - await windowManager.setTitleBarStyle( - TitleBarStyle.hidden, - windowButtonVisibility: App.isMacOS, - ); - if (App.isLinux) { - await windowManager.setBackgroundColor(Colors.transparent); - } - await windowManager.setMinimumSize(const Size(500, 600)); - if (!App.isLinux) { - // https://github.com/leanflutter/window_manager/issues/460 - var placement = await WindowPlacement.loadFromFile(); - await placement.applyToWindow(); - await windowManager.show(); - WindowPlacement.loop(); - } - }); - } - }, (error, stack) { - Log.error("Unhandled Exception", "$error\n$stack"); + overrideIO(() { + runZonedGuarded(() async { + await Rhttp.init(); + WidgetsFlutterBinding.ensureInitialized(); + await init(); + if (App.isAndroid) { + handleLinks(); + } + FlutterError.onError = (details) { + Log.error( + "Unhandled Exception", "${details.exception}\n${details.stack}"); + }; + runApp(const MyApp()); + if (App.isDesktop) { + await windowManager.ensureInitialized(); + windowManager.waitUntilReadyToShow().then((_) async { + await windowManager.setTitleBarStyle( + TitleBarStyle.hidden, + windowButtonVisibility: App.isMacOS, + ); + if (App.isLinux) { + await windowManager.setBackgroundColor(Colors.transparent); + } + await windowManager.setMinimumSize(const Size(500, 600)); + if (!App.isLinux) { + // https://github.com/leanflutter/window_manager/issues/460 + var placement = await WindowPlacement.loadFromFile(); + await placement.applyToWindow(); + await windowManager.show(); + WindowPlacement.loop(); + } + }); + } + }, (error, stack) { + Log.error("Unhandled Exception", "$error\n$stack"); + }); }); } diff --git a/lib/network/download.dart b/lib/network/download.dart index da2b93f..7657549 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -235,20 +235,21 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { } if (path == null) { - var dir = await LocalManager().findValidDirectory( - comicId, - comicType, - comic!.title, - ); - if (!(await dir.exists())) { - try { + try { + var dir = await LocalManager().findValidDirectory( + comicId, + comicType, + comic!.title, + ); + if (!(await dir.exists())) { await dir.create(); - } catch (e) { - _setError("Error: $e"); - return; } + path = dir.path; + } catch (e, s) { + Log.error("Download", e.toString(), s); + _setError("Error: $e"); + return; } - path = dir.path; } await LocalManager().saveCurrentDownloadingTasks(); @@ -266,11 +267,13 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { throw "Failed to download cover"; } var fileType = detectFileType(data); - var file = File(FilePath.join(path!, "cover${fileType.ext}")); + var file = + File(FilePath.join(path!, "cover${fileType.ext}")); file.writeAsBytesSync(data); return "file://${file.path}"; }); if (res.error) { + Log.error("Download", res.errorMessage!); _setError("Error: ${res.errorMessage}"); return; } else { @@ -294,6 +297,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { return; } if (res.error) { + Log.error("Download", res.errorMessage!); _setError("Error: ${res.errorMessage}"); return; } else { @@ -323,6 +327,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { return; } if (res.error) { + Log.error("Download", res.errorMessage!); _setError("Error: ${res.errorMessage}"); return; } else { @@ -347,6 +352,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { return; } if (task.error != null) { + Log.error("Download", task.error.toString()); _setError("Error: ${task.error}"); return; } @@ -375,7 +381,6 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { _message = message; notifyListeners(); stopRecorder(); - Log.error("Download", message); } @override @@ -448,7 +453,8 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { }).toList(), directory: Directory(path!).name, chapters: comic!.chapters, - cover: File(_cover!.split("file://").last).uri.pathSegments.last, + cover: + File(_cover!.split("file://").last).name, comicType: ComicType(source.key.hashCode), downloadedChapters: chapters ?? [], createdAt: DateTime.now(), @@ -721,13 +727,12 @@ class ArchiveDownloadTask extends DownloadTask { _currentBytes = status.downloadedBytes; _expectedBytes = status.totalBytes; _message = - "${bytesToReadableString(_currentBytes)}/${bytesToReadableString(_expectedBytes)}"; + "${bytesToReadableString(_currentBytes)}/${bytesToReadableString(_expectedBytes)}"; _speed = status.bytesPerSecond; isDownloaded = status.isFinished; notifyListeners(); } - } - catch(e) { + } catch (e) { _setError("Error: $e"); return; } diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index f71eb17..76542fd 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:math'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; import 'package:venera/components/components.dart'; @@ -11,9 +10,7 @@ import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/favorites.dart'; -import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/res.dart'; -import 'package:venera/network/download.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index a202591..d273eca 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -604,7 +604,7 @@ ImageProvider _createImageProvider(int page, BuildContext context) { var reader = context.reader; var imageKey = reader.images![page - 1]; if (imageKey.startsWith('file://')) { - return FileImage(openFilePlatform(imageKey.replaceFirst("file://", ''))); + return FileImage(File(imageKey.replaceFirst("file://", ''))); } else { return ReaderImageProvider( imageKey, diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 804f32c..95043c3 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -469,7 +469,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ImageProvider image; var imageKey = images[index]; if (imageKey.startsWith('file://')) { - image = FileImage(openFilePlatform(imageKey.replaceFirst("file://", ''))); + image = FileImage(File(imageKey.replaceFirst("file://", ''))); } else { image = ReaderImageProvider( imageKey, @@ -515,7 +515,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } } if (imageKey.startsWith("file://")) { - return await openFilePlatform(imageKey.substring(7)).readAsBytes(); + return await File(imageKey.substring(7)).readAsBytes(); } else { return (await CacheManager().findCache( "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))! diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index b35301f..234340c 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -34,25 +34,8 @@ class _AppSettingsState extends State { callback: () async { String? result; if (App.isAndroid) { - var channel = const MethodChannel("venera/storage"); - var permission = await channel.invokeMethod(''); - if (permission != true) { - context.showMessage(message: "Permission denied".tl); - return; - } - var path = await selectDirectory(); - if (path != null) { - // check if the path is writable - var testFile = File(FilePath.join(path, "test")); - try { - await testFile.writeAsBytes([1]); - await testFile.delete(); - } catch (e) { - context.showMessage(message: "Permission denied".tl); - return; - } - result = path; - } + var picker = DirectoryPicker(); + result = (await picker.pickDirectory())?.path; } else if (App.isIOS) { result = await selectDirectoryIOS(); } else { diff --git a/lib/utils/cbz.dart b/lib/utils/cbz.dart index 35a9fd6..7bc6ba1 100644 --- a/lib/utils/cbz.dart +++ b/lib/utils/cbz.dart @@ -187,7 +187,7 @@ abstract class CBZ { } int i = 1; for (var image in allImages) { - var src = openFilePlatform(image); + var src = File(image); var width = allImages.length.toString().length; var dstName = '${i.toString().padLeft(width, '0')}.${image.split('.').last}'; diff --git a/lib/utils/import_comic.dart b/lib/utils/import_comic.dart index cbcecbc..21eb5cf 100644 --- a/lib/utils/import_comic.dart +++ b/lib/utils/import_comic.dart @@ -60,7 +60,7 @@ class ImportComic { if (cancelled) { return imported; } - var comicDir = openDirectoryPlatform( + var comicDir = Directory( FilePath.join(comicSrc.path, comic['DIRNAME'] as String)); String titleJP = comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String; @@ -217,7 +217,7 @@ class ImportComic { chapters.sort(); if (hasChapters && coverPath == '') { // use the first image in the first chapter as the cover - var firstChapter = openDirectoryPlatform('${directory.path}/${chapters.first}'); + var firstChapter = Directory('${directory.path}/${chapters.first}'); await for (var entry in firstChapter.list()) { if (entry is File) { coverPath = entry.name; @@ -248,8 +248,8 @@ class ImportComic { var destination = data['destination'] as String; Map result = {}; for (var dir in toBeCopied) { - var source = openDirectoryPlatform(dir); - var dest = openDirectoryPlatform("$destination/${source.name}"); + var source = Directory(dir); + var dest = Directory("$destination/${source.name}"); if (dest.existsSync()) { // The destination directory already exists, and it is not managed by the app. // Rename the old directory to avoid conflicts. diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 9df3d29..2a959ce 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -81,7 +81,7 @@ extension DirectoryExtension on Directory { int total = 0; for (var f in listSync(recursive: true)) { if (FileSystemEntity.typeSync(f.path) == FileSystemEntityType.file) { - total += await openFilePlatform(f.path).length(); + total += await File(f.path).length(); } } return total; @@ -93,7 +93,21 @@ extension DirectoryExtension on Directory { } File joinFile(String name) { - return openFilePlatform(FilePath.join(path, name)); + return File(FilePath.join(path, name)); + } + + void deleteContentsSync({recursive = true}) { + if (!existsSync()) return; + for (var f in listSync()) { + f.deleteIfExistsSync(recursive: recursive); + } + } + + Future deleteContents({recursive = true}) async { + if (!existsSync()) return; + for (var f in listSync()) { + await f.deleteIfExists(recursive: recursive); + } } } @@ -124,14 +138,15 @@ String sanitizeFileName(String fileName) { Future copyDirectory(Directory source, Directory destination) async { List contents = source.listSync(); for (FileSystemEntity content in contents) { - String newPath = destination.path + - Platform.pathSeparator + - content.path.split(Platform.pathSeparator).last; + String newPath = FilePath.join(destination.path, content.name); if (content is File) { - content.copySync(newPath); + var resultFile = File(newPath); + resultFile.createSync(); + var data = content.readAsBytesSync(); + resultFile.writeAsBytesSync(data); } else if (content is Directory) { - Directory newDirectory = openDirectoryPlatform(newPath); + Directory newDirectory = Directory(newPath); newDirectory.createSync(); copyDirectory(content.absolute, newDirectory.absolute); } @@ -140,18 +155,18 @@ Future copyDirectory(Directory source, Directory destination) async { Future copyDirectoryIsolate( Directory source, Directory destination) async { - await Isolate.run(() { - copyDirectory(source, destination); + await Isolate.run(() async { + await copyDirectory(source, destination); }); } String findValidDirectoryName(String path, String directory) { var name = sanitizeFileName(directory); - var dir = openDirectoryPlatform("$path/$name"); + var dir = Directory("$path/$name"); var i = 1; while (dir.existsSync() && dir.listSync().isNotEmpty) { name = sanitizeFileName("$directory($i)"); - dir = openDirectoryPlatform("$path/$name"); + dir = Directory("$path/$name"); i++; } return name; @@ -184,11 +199,12 @@ class DirectoryPicker { directory = (await AndroidDirectory.pickDirectory())?.path; } else { // ios, macos - directory = await _methodChannel.invokeMethod("getDirectoryPath"); + directory = + await _methodChannel.invokeMethod("getDirectoryPath"); } if (directory == null) return null; _finalizer.attach(this, directory); - return openDirectoryPlatform(directory); + return Directory(directory); } finally { Future.delayed(const Duration(milliseconds: 100), () { IO._isSelectingFiles = false; @@ -311,31 +327,42 @@ Future saveFile( } } -Directory openDirectoryPlatform(String path) { - if(App.isAndroid) { - var dir = AndroidDirectory.fromPathSync(path); - if(dir == null) { - return Directory(path); +class _IOOverrides extends IOOverrides { + @override + Directory createDirectory(String path) { + if (App.isAndroid) { + var dir = AndroidDirectory.fromPathSync(path); + if (dir == null) { + return super.createDirectory(path); + } + return dir; + } else { + return super.createDirectory(path); + } + } + + @override + File createFile(String path) { + if (path.startsWith("file://")) { + path = path.substring(7); + } + if (App.isAndroid) { + var f = AndroidFile.fromPathSync(path); + if (f == null) { + return super.createFile(path); + } + return f; + } else { + return super.createFile(path); } - return dir; - } else { - return Directory(path); } } -File openFilePlatform(String path) { - if(path.startsWith("file://")) { - path = path.substring(7); - } - if(App.isAndroid) { - var f = AndroidFile.fromPathSync(path); - if(f == null) { - return File(path); - } - return f; - } else { - return File(path); - } +void overrideIO(void Function() f) { + IOOverrides.runWithIOOverrides( + f, + _IOOverrides(), + ); } class Share { @@ -396,4 +423,4 @@ class FileSelectResult { } String get name => File(path).name; -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index eb64a71..8128abe 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -393,8 +393,8 @@ packages: dependency: "direct main" description: path: "." - ref: "829a566b738a26ea98e523807f49838e21308543" - resolved-ref: "829a566b738a26ea98e523807f49838e21308543" + ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d + resolved-ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d url: "https://github.com/pkuislm/flutter_saf.git" source: git version: "0.0.1"