mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 17:25:32 +00:00
Merge pull request #517 from NBA2K1/Correct-directory
Standardize Folder Structure on Windows, Linux, iOS & macOS
This commit is contained in:
commit
24849cc000
6 changed files with 194 additions and 100 deletions
|
|
@ -73,22 +73,70 @@ void main(List<String> args) async {
|
|||
);
|
||||
}
|
||||
}
|
||||
isar = await StorageProvider().initDB(null, inspector: kDebugMode);
|
||||
await Hive.initFlutter();
|
||||
final storage = StorageProvider();
|
||||
await storage.requestPermission();
|
||||
await _migrateOldLayout();
|
||||
isar = await storage.initDB(null, inspector: kDebugMode);
|
||||
runApp(ProviderScope(child: MyApp(), retry: (retryCount, error) => null));
|
||||
unawaited(_postLaunchInit(storage)); // Defer non-essential async operations
|
||||
}
|
||||
|
||||
Future<void> _postLaunchInit(StorageProvider storage) async {
|
||||
await AppLogger.init();
|
||||
final hivePath = (Platform.isIOS || Platform.isMacOS)
|
||||
? "databases"
|
||||
: p.join("Mangayomi", "databases");
|
||||
await Hive.initFlutter(Platform.isAndroid ? "" : hivePath);
|
||||
Hive.registerAdapter(TrackSearchAdapter());
|
||||
if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) {
|
||||
discordRpc = DiscordRPC(applicationId: "1395040506677039157");
|
||||
await discordRpc?.initialize();
|
||||
}
|
||||
|
||||
runApp(ProviderScope(child: MyApp(), retry: (retryCount, error) => null));
|
||||
unawaited(_postLaunchInit()); // Defer non-essential async operations
|
||||
await storage.deleteBtDirectory();
|
||||
}
|
||||
|
||||
Future<void> _postLaunchInit() async {
|
||||
await StorageProvider().requestPermission();
|
||||
await StorageProvider().deleteBtDirectory();
|
||||
await AppLogger.init();
|
||||
/// This can be removed after next release (v0.6.40?)
|
||||
/// It is a one-time thing to migrate the database and folders to the new
|
||||
/// iOS and macOS location (PR #517)
|
||||
Future<void> _migrateOldLayout() async {
|
||||
if (!(Platform.isIOS || Platform.isMacOS)) return;
|
||||
final root = await getApplicationDocumentsDirectory();
|
||||
final oldRoot = Directory(p.join(root.path, 'Mangayomi'));
|
||||
if (!await oldRoot.exists()) return;
|
||||
final newDbDir = Directory(p.join(root.path, 'databases'));
|
||||
await newDbDir.create(recursive: true);
|
||||
// Move database files to new directory
|
||||
for (final filename in [
|
||||
'mangayomiDb.isar',
|
||||
'mangayomiDb.isar.lock',
|
||||
'tracker_library.hive',
|
||||
'tracker_library.lock',
|
||||
]) {
|
||||
final oldFile = File(p.join(root.path, filename));
|
||||
if (await oldFile.exists()) {
|
||||
final newFile = File(p.join(newDbDir.path, filename));
|
||||
await oldFile.rename(newFile.path);
|
||||
}
|
||||
}
|
||||
// Move subfolders up a level
|
||||
for (final sub in ['backup', 'downloads', 'Pictures', 'local']) {
|
||||
final oldSubDir = Directory(p.join(oldRoot.path, sub));
|
||||
final newSubDir = Directory(p.join(root.path, sub));
|
||||
if (!await oldSubDir.exists()) continue;
|
||||
// If by chance newSubDir is empty, safe to rename; otherwise, move contents
|
||||
if (!(await newSubDir.exists())) {
|
||||
await oldSubDir.rename(newSubDir.path);
|
||||
} else {
|
||||
// merge contents
|
||||
await for (final entity in oldSubDir.list()) {
|
||||
await entity.rename(p.join(newSubDir.path, p.basename(entity.path)));
|
||||
}
|
||||
}
|
||||
// remove subfolder if empty
|
||||
if (await oldSubDir.list().isEmpty) await oldSubDir.delete();
|
||||
}
|
||||
// Clean up old empty folder
|
||||
if (await oldRoot.list().isEmpty) await oldRoot.delete();
|
||||
}
|
||||
|
||||
class MyApp extends ConsumerStatefulWidget {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ Future<void> downloadChapter(
|
|||
chapter,
|
||||
mangaMainDirectory: mangaMainDirectory,
|
||||
))!;
|
||||
await Directory(chapterDirectory.path).create(recursive: true);
|
||||
await storageProvider.createDirectorySafely(chapterDirectory.path);
|
||||
Map<String, String> videoHeader = {};
|
||||
Map<String, String> htmlHeader = {
|
||||
"Priority": "u=0, i",
|
||||
|
|
@ -265,6 +265,7 @@ Future<void> downloadChapter(
|
|||
!mp4FileExist && itemType == ItemType.anime ||
|
||||
!htmlFileExist && itemType == ItemType.novel) {
|
||||
final mainDirectory = (await storageProvider.getDirectory())!;
|
||||
storageProvider.createDirectorySafely(mainDirectory.path);
|
||||
for (var index = 0; index < pageUrls.length; index++) {
|
||||
if (Platform.isAndroid) {
|
||||
if (!(await File(
|
||||
|
|
|
|||
|
|
@ -80,42 +80,35 @@ class AutoBackupLocationState extends _$AutoBackupLocationState {
|
|||
@riverpod
|
||||
Future<void> checkAndBackup(Ref ref) async {
|
||||
final settings = isar.settings.getSync(227);
|
||||
if (settings!.backupFrequency != null) {
|
||||
final backupFrequency = _duration(settings.backupFrequency);
|
||||
if (backupFrequency != null) {
|
||||
if (settings.startDatebackup != null) {
|
||||
final startDatebackup = DateTime.fromMillisecondsSinceEpoch(
|
||||
settings.startDatebackup!,
|
||||
);
|
||||
if (DateTime.now().isAfter(startDatebackup)) {
|
||||
_setBackupFrequency(settings.backupFrequency!);
|
||||
final storageProvider = StorageProvider();
|
||||
await storageProvider.requestPermission();
|
||||
final defaulteDirectory = await storageProvider.getDefaultDirectory();
|
||||
final backupLocation = ref.watch(autoBackupLocationStateProvider).$2;
|
||||
Directory? backupDirectory;
|
||||
backupDirectory = Directory(
|
||||
backupLocation.isEmpty
|
||||
? p.join(defaulteDirectory!.path, "backup")
|
||||
: backupLocation,
|
||||
);
|
||||
if (Platform.isIOS) {
|
||||
backupDirectory = await (storageProvider.getIosBackupDirectory());
|
||||
}
|
||||
if (!(await backupDirectory!.exists())) {
|
||||
backupDirectory.create();
|
||||
}
|
||||
ref.watch(
|
||||
doBackUpProvider(
|
||||
list: ref.watch(backupFrequencyOptionsStateProvider),
|
||||
path: backupDirectory.path,
|
||||
context: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
final backupFrequency = _duration(settings!.backupFrequency);
|
||||
if (backupFrequency == null || settings.startDatebackup == null) return;
|
||||
|
||||
final startDatebackup = DateTime.fromMillisecondsSinceEpoch(
|
||||
settings.startDatebackup!,
|
||||
);
|
||||
if (!DateTime.now().isAfter(startDatebackup)) return;
|
||||
_setBackupFrequency(settings.backupFrequency!);
|
||||
final storageProvider = StorageProvider();
|
||||
final backupLocation = ref.read(autoBackupLocationStateProvider).$2;
|
||||
Directory? backupDirectory;
|
||||
if (Platform.isIOS) {
|
||||
backupDirectory = await (storageProvider.getIosBackupDirectory());
|
||||
} else {
|
||||
final defaultDirectory = await storageProvider.getDefaultDirectory();
|
||||
backupDirectory = Directory(
|
||||
backupLocation.isEmpty
|
||||
? p.join(defaultDirectory!.path, "backup")
|
||||
: backupLocation,
|
||||
);
|
||||
}
|
||||
await storageProvider.createDirectorySafely(backupDirectory!.path);
|
||||
ref.read(
|
||||
doBackUpProvider(
|
||||
list: ref.read(backupFrequencyOptionsStateProvider),
|
||||
path: backupDirectory.path,
|
||||
context: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Duration? _duration(int? backupFrequency) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:extended_image_library/src/platform.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:http_client_helper/http_client_helper.dart';
|
||||
import 'package:mangayomi/providers/storage_provider.dart';
|
||||
import 'package:mangayomi/services/http/m_client.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
|
@ -324,6 +325,7 @@ class CustomExtendedNetworkImageProvider
|
|||
),
|
||||
);
|
||||
Uint8List? data;
|
||||
await StorageProvider().createDirectorySafely(cacheImagesDirectory.path);
|
||||
final File cacheFile = File(join(cacheImagesDirectory.path, md5Key));
|
||||
|
||||
// exist, try to find cache image file
|
||||
|
|
@ -343,8 +345,6 @@ class CustomExtendedNetworkImageProvider
|
|||
// Store in memory cache
|
||||
_memoryCache.put(md5Key, data);
|
||||
}
|
||||
} else if (!cacheImagesDirectory.existsSync()) {
|
||||
await cacheImagesDirectory.create(recursive: true);
|
||||
}
|
||||
|
||||
// load from network
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// ignore_for_file: depend_on_referenced_packages
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'package:mangayomi/eval/model/source_preference.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
|
|
@ -22,33 +23,28 @@ import 'package:permission_handler/permission_handler.dart';
|
|||
import 'package:path/path.dart' as path;
|
||||
|
||||
class StorageProvider {
|
||||
static bool _hasPermission = false;
|
||||
static final StorageProvider _instance = StorageProvider._internal();
|
||||
StorageProvider._internal();
|
||||
factory StorageProvider() => _instance;
|
||||
|
||||
Future<bool> requestPermission() async {
|
||||
if (_hasPermission) return true;
|
||||
if (Platform.isAndroid) {
|
||||
Permission permission = Permission.manageExternalStorage;
|
||||
if (await permission.isGranted) {
|
||||
return true;
|
||||
} else {
|
||||
final result = await permission.request();
|
||||
if (result == PermissionStatus.granted) {
|
||||
_hasPermission = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!Platform.isAndroid) return true;
|
||||
Permission permission = Permission.manageExternalStorage;
|
||||
if (await permission.isGranted) return true;
|
||||
if (await permission.request().isGranted) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<void> deleteBtDirectory() async {
|
||||
final d = await getBtDirectory();
|
||||
await Directory(d!.path).delete(recursive: true);
|
||||
final btDir = Directory(await _btDirectoryPath());
|
||||
if (await btDir.exists()) await btDir.delete(recursive: true);
|
||||
}
|
||||
|
||||
Future<void> deleteTmpDirectory() async {
|
||||
final d = await getTmpDirectory();
|
||||
await Directory(d!.path).delete(recursive: true);
|
||||
final tmpDir = Directory(await _tempDirectoryPath());
|
||||
if (await tmpDir.exists()) await tmpDir.delete(recursive: true);
|
||||
}
|
||||
|
||||
Future<Directory?> getDefaultDirectory() async {
|
||||
|
|
@ -57,6 +53,10 @@ class StorageProvider {
|
|||
directory = Directory("/storage/emulated/0/Mangayomi/");
|
||||
} else {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
// The documents dir in iOS and macOS is already named "Mangayomi".
|
||||
// Appending "Mangayomi" to the documents dir would create
|
||||
// unnecessarily nested Mangayomi/Mangayomi/ folder.
|
||||
if (Platform.isIOS || Platform.isMacOS) return dir;
|
||||
directory = Directory(path.join(dir.path, 'Mangayomi'));
|
||||
}
|
||||
return directory;
|
||||
|
|
@ -70,29 +70,43 @@ class StorageProvider {
|
|||
}
|
||||
|
||||
Future<Directory?> getBtDirectory() async {
|
||||
final gefaultDirectory = await getDefaultDirectory();
|
||||
String dbDir = path.join(gefaultDirectory!.path, 'torrents');
|
||||
await Directory(dbDir).create(recursive: true);
|
||||
final dbDir = await _btDirectoryPath();
|
||||
await createDirectorySafely(dbDir);
|
||||
return Directory(dbDir);
|
||||
}
|
||||
|
||||
Future<String> _btDirectoryPath() async {
|
||||
final defaultDirectory = await getDefaultDirectory();
|
||||
return path.join(defaultDirectory!.path, 'torrents');
|
||||
}
|
||||
|
||||
Future<Directory?> getTmpDirectory() async {
|
||||
final gefaultDirectory = await getDirectory();
|
||||
String dbDir = path.join(gefaultDirectory!.path, 'tmp');
|
||||
await Directory(dbDir).create(recursive: true);
|
||||
return Directory(dbDir);
|
||||
final tmpPath = await _tempDirectoryPath();
|
||||
await createDirectorySafely(tmpPath);
|
||||
return Directory(tmpPath);
|
||||
}
|
||||
|
||||
Future<String> _tempDirectoryPath() async {
|
||||
final defaultDirectory = await getDirectory();
|
||||
return path.join(defaultDirectory!.path, 'tmp');
|
||||
}
|
||||
|
||||
Future<Directory?> getIosBackupDirectory() async {
|
||||
final gefaultDirectory = await getDefaultDirectory();
|
||||
String dbDir = path.join(gefaultDirectory!.path, 'backup');
|
||||
await Directory(dbDir).create(recursive: true);
|
||||
final defaultDirectory = await getDefaultDirectory();
|
||||
String dbDir = path.join(defaultDirectory!.path, 'backup');
|
||||
await createDirectorySafely(dbDir);
|
||||
return Directory(dbDir);
|
||||
}
|
||||
|
||||
Future<Directory?> getDirectory() async {
|
||||
Directory? directory;
|
||||
String dPath = isar.settings.getSync(227)!.downloadLocation ?? "";
|
||||
String dPath = "";
|
||||
try {
|
||||
final setting = isar.settings.getSync(227);
|
||||
dPath = setting?.downloadLocation ?? "";
|
||||
} catch (e) {
|
||||
debugPrint("Could not get downloadLocation from Isar settings: $e");
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
directory = Directory(
|
||||
dPath.isEmpty ? "/storage/emulated/0/Mangayomi/" : "$dPath/",
|
||||
|
|
@ -100,6 +114,10 @@ class StorageProvider {
|
|||
} else {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final p = dPath.isEmpty ? dir.path : dPath;
|
||||
// The documents dir in iOS and macOS is already named "Mangayomi".
|
||||
// Appending "Mangayomi" to the documents dir would create
|
||||
// unnecessarily nested Mangayomi/Mangayomi/ folder.
|
||||
if (Platform.isIOS || Platform.isMacOS) return Directory(p);
|
||||
directory = Directory(path.join(p, 'Mangayomi'));
|
||||
}
|
||||
return directory;
|
||||
|
|
@ -143,27 +161,48 @@ class StorageProvider {
|
|||
|
||||
Future<Directory?> getDatabaseDirectory() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
if (Platform.isAndroid || Platform.isIOS || Platform.isMacOS) {
|
||||
return dir;
|
||||
String dbDir;
|
||||
if (Platform.isAndroid) return dir;
|
||||
if (Platform.isIOS || Platform.isMacOS) {
|
||||
// Put the database files inside /databases like on Windows, Linux
|
||||
// So they are not just in the app folders root dir
|
||||
dbDir = path.join(dir.path, 'databases');
|
||||
} else {
|
||||
String dbDir = path.join(dir.path, 'Mangayomi', 'databases');
|
||||
await Directory(dbDir).create(recursive: true);
|
||||
return Directory(dbDir);
|
||||
dbDir = path.join(dir.path, 'Mangayomi', 'databases');
|
||||
}
|
||||
await createDirectorySafely(dbDir);
|
||||
return Directory(dbDir);
|
||||
}
|
||||
|
||||
Future<Directory?> getGalleryDirectory() async {
|
||||
String gPath = (await getDirectory())!.path;
|
||||
String gPath;
|
||||
if (Platform.isAndroid) {
|
||||
gPath = "/storage/emulated/0/Pictures/Mangayomi/";
|
||||
} else {
|
||||
gPath = path.join(gPath, 'Pictures');
|
||||
gPath = path.join((await getDirectory())!.path, 'Pictures');
|
||||
}
|
||||
await Directory(gPath).create(recursive: true);
|
||||
await createDirectorySafely(gPath);
|
||||
return Directory(gPath);
|
||||
}
|
||||
|
||||
Future<Isar> initDB(String? path, {bool? inspector = false}) async {
|
||||
Future<void> createDirectorySafely(String dirPath) async {
|
||||
final dir = Directory(dirPath);
|
||||
try {
|
||||
await dir.create(recursive: true);
|
||||
} catch (_) {
|
||||
if (await requestPermission()) {
|
||||
try {
|
||||
await dir.create(recursive: true);
|
||||
} catch (e) {
|
||||
debugPrint('Initial directory creation failed for $dirPath: $e');
|
||||
}
|
||||
} else {
|
||||
debugPrint('Permission denied. Cannot create: $dirPath');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Isar> initDB(String? path, {bool inspector = false}) async {
|
||||
Directory? dir;
|
||||
if (path == null) {
|
||||
dir = await getDatabaseDirectory();
|
||||
|
|
@ -191,14 +230,29 @@ class StorageProvider {
|
|||
],
|
||||
directory: dir!.path,
|
||||
name: "mangayomiDb",
|
||||
inspector: inspector!,
|
||||
inspector: inspector,
|
||||
);
|
||||
|
||||
final settings = await isar.settings.filter().idEqualTo(227).findFirst();
|
||||
if (settings == null) {
|
||||
await isar.writeTxn(() async {
|
||||
isar.settings.put(Settings());
|
||||
});
|
||||
try {
|
||||
final settings = await isar.settings.filter().idEqualTo(227).findFirst();
|
||||
if (settings == null) {
|
||||
await isar.writeTxn(() async => isar.settings.put(Settings()));
|
||||
}
|
||||
} catch (_) {
|
||||
if (await requestPermission()) {
|
||||
try {
|
||||
final settings = await isar.settings
|
||||
.filter()
|
||||
.idEqualTo(227)
|
||||
.findFirst();
|
||||
if (settings == null) {
|
||||
await isar.writeTxn(() async => isar.settings.put(Settings()));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Failed after retry with permission: $e");
|
||||
}
|
||||
} else {
|
||||
debugPrint("Permission denied during Database init fallback.");
|
||||
}
|
||||
}
|
||||
|
||||
final prefs = await isar.trackPreferences
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:http/http.dart';
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/video.dart';
|
||||
import 'package:mangayomi/providers/storage_provider.dart';
|
||||
import 'package:mangayomi/services/http/m_client.dart';
|
||||
import 'package:mangayomi/services/http/rhttp/src/model/settings.dart';
|
||||
import 'package:mangayomi/services/download_manager/m3u8/models/download.dart';
|
||||
|
|
@ -124,21 +125,18 @@ class M3u8Downloader {
|
|||
}
|
||||
|
||||
Future<void> download(void Function(DownloadProgress) onProgress) async {
|
||||
final tempDir = Directory(path.join(downloadDir, 'temp'));
|
||||
final tempDir = path.join(downloadDir, 'temp');
|
||||
await StorageProvider().createDirectorySafely(tempDir);
|
||||
|
||||
try {
|
||||
await tempDir.create(recursive: true);
|
||||
final (tsList, key, iv, mediaSequence) = await _getTsList();
|
||||
|
||||
final tsListToDownload = await _filterExistingSegments(
|
||||
tsList,
|
||||
tempDir.path,
|
||||
);
|
||||
final tsListToDownload = await _filterExistingSegments(tsList, tempDir);
|
||||
_log('Downloading ${tsListToDownload.length} segments...');
|
||||
|
||||
await _downloadSegmentsWithProgress(
|
||||
tsListToDownload,
|
||||
tempDir.path,
|
||||
tempDir,
|
||||
key,
|
||||
iv,
|
||||
mediaSequence,
|
||||
|
|
|
|||
Loading…
Reference in a new issue