misc changes

- remove cronet_http & cupertino_http
- use rhttp package as default http client
- fix #198 #200 crashes on multiple downloads
- fix #162 #102 unable to download with forbidden characters in the name (as it is fixed this can cause reading problems concerning chapters downloaded before this version)
- now supports all features on all platforms such as VPNs and HTTP proxies thanks to rhttp package
This commit is contained in:
kodjomoustapha 2024-08-21 13:30:13 +01:00
parent d177900394
commit b10c3f3a22
53 changed files with 370 additions and 1224 deletions

11
.gitignore vendored
View file

@ -32,6 +32,11 @@ migrate_working_dir/
.pub/
/build/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
.vscode/
# Symbolication related
app.*.symbols
@ -42,3 +47,9 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# FVM Version Cache
.fvm/
# FVM Version
.fvmrc

23
.vscode/settings.json vendored
View file

@ -1,13 +1,14 @@
{
"java.configuration.updateBuildConfiguration": "automatic",
"githubPullRequests.ignoredPullRequestBranches": [
"main"
],
"rust-analyzer.linkedProjects": [
".\\rust\\Cargo.toml"
],
"files.associations": {
"*.html.erb": "erb",
"cstring": "cpp"
}
"java.configuration.updateBuildConfiguration": "automatic",
"githubPullRequests.ignoredPullRequestBranches": [
"main"
],
"rust-analyzer.linkedProjects": [
".\\rust\\Cargo.toml"
],
"files.associations": {
"*.html.erb": "erb",
"cstring": "cpp"
},
"dart.flutterSdkPath": ".fvm/versions/3.22.3"
}

View file

@ -20,6 +20,7 @@ import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:mangayomi/src/rust/frb_generated.dart';
import 'package:media_kit/media_kit.dart';
import 'package:rhttp/rhttp.dart';
import 'package:window_manager/window_manager.dart';
late Isar isar;
@ -33,6 +34,7 @@ void main(List<String> args) async {
}
MediaKit.ensureInitialized();
await RustLib.init();
await Rhttp.init();
if (!(Platform.isAndroid || Platform.isIOS)) {
await windowManager.ensureInitialized();
}

View file

@ -188,8 +188,6 @@ class Settings {
bool? useLibass;
bool? useNativeHttpClient;
Settings(
{this.id = 227,
this.displayType = DisplayType.compactGrid,
@ -272,8 +270,7 @@ class Settings {
this.mangaGridSize,
this.animeGridSize,
this.disableSectionType = SectionType.all,
this.useLibass = true,
this.useNativeHttpClient = false});
this.useLibass = true});
Settings.fromJson(Map<String, dynamic> json) {
animatePageTransitions = json['animatePageTransitions'];
@ -423,7 +420,6 @@ class Settings {
disableSectionType =
SectionType.values[json['disableSectionType'] ?? SectionType.all];
useLibass = json['useLibass'];
useNativeHttpClient = json['useNativeHttpClient'];
}
Map<String, dynamic> toJson() => {
@ -532,8 +528,7 @@ class Settings {
'mangaGridSize': mangaGridSize,
'animeGridSize': animeGridSize,
'disableSectionType': disableSectionType.index,
'useLibass': useLibass,
'useNativeHttpClient': useNativeHttpClient
'useLibass': useLibass
};
}

View file

@ -456,18 +456,13 @@ const SettingsSchema = CollectionSchema(
name: r'useLibass',
type: IsarType.bool,
),
r'useNativeHttpClient': PropertySchema(
id: 83,
name: r'useNativeHttpClient',
type: IsarType.bool,
),
r'usePageTapZones': PropertySchema(
id: 84,
id: 83,
name: r'usePageTapZones',
type: IsarType.bool,
),
r'userAgent': PropertySchema(
id: 85,
id: 84,
name: r'userAgent',
type: IsarType.string,
)
@ -922,9 +917,8 @@ void _settingsSerialize(
writer.writeBool(offsets[80], object.themeIsDark);
writer.writeBool(offsets[81], object.updateProgressAfterReading);
writer.writeBool(offsets[82], object.useLibass);
writer.writeBool(offsets[83], object.useNativeHttpClient);
writer.writeBool(offsets[84], object.usePageTapZones);
writer.writeString(offsets[85], object.userAgent);
writer.writeBool(offsets[83], object.usePageTapZones);
writer.writeString(offsets[84], object.userAgent);
}
Settings _settingsDeserialize(
@ -1086,9 +1080,8 @@ Settings _settingsDeserialize(
themeIsDark: reader.readBoolOrNull(offsets[80]),
updateProgressAfterReading: reader.readBoolOrNull(offsets[81]),
useLibass: reader.readBoolOrNull(offsets[82]),
useNativeHttpClient: reader.readBoolOrNull(offsets[83]),
usePageTapZones: reader.readBoolOrNull(offsets[84]),
userAgent: reader.readStringOrNull(offsets[85]),
usePageTapZones: reader.readBoolOrNull(offsets[83]),
userAgent: reader.readStringOrNull(offsets[84]),
);
object.chapterFilterBookmarkedList =
reader.readObjectList<ChapterFilterBookmarked>(
@ -1382,8 +1375,6 @@ P _settingsDeserializeProp<P>(
case 83:
return (reader.readBoolOrNull(offset)) as P;
case 84:
return (reader.readBoolOrNull(offset)) as P;
case 85:
return (reader.readStringOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@ -6849,34 +6840,6 @@ extension SettingsQueryFilter
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition>
useNativeHttpClientIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'useNativeHttpClient',
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition>
useNativeHttpClientIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'useNativeHttpClient',
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition>
useNativeHttpClientEqualTo(bool? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'useNativeHttpClient',
value: value,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition>
usePageTapZonesIsNull() {
return QueryBuilder.apply(this, (query) {
@ -8092,19 +8055,6 @@ extension SettingsQuerySortBy on QueryBuilder<Settings, Settings, QSortBy> {
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByUseNativeHttpClient() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'useNativeHttpClient', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy>
sortByUseNativeHttpClientDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'useNativeHttpClient', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByUsePageTapZones() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'usePageTapZones', Sort.asc);
@ -9005,19 +8955,6 @@ extension SettingsQuerySortThenBy
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByUseNativeHttpClient() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'useNativeHttpClient', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy>
thenByUseNativeHttpClientDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'useNativeHttpClient', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByUsePageTapZones() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'usePageTapZones', Sort.asc);
@ -9486,12 +9423,6 @@ extension SettingsQueryWhereDistinct
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctByUseNativeHttpClient() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'useNativeHttpClient');
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctByUsePageTapZones() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'usePageTapZones');
@ -10068,13 +9999,6 @@ extension SettingsQueryProperty
});
}
QueryBuilder<Settings, bool?, QQueryOperations>
useNativeHttpClientProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'useNativeHttpClient');
});
}
QueryBuilder<Settings, bool?, QQueryOperations> usePageTapZonesProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'usePageTapZones');

View file

@ -1,7 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'package:mangayomi/models/page.dart';
import 'package:mangayomi/modules/more/settings/advanced/providers/native_http_client.dart';
import 'package:mangayomi/services/background_downloader/background_downloader.dart';
import 'package:isar/isar.dart';
import 'package:mangayomi/main.dart';
@ -38,14 +37,16 @@ Future<List<PageUrl>> downloadChapter(
bool isOk = false;
final manga = chapter.manga.value!;
final path1 = await storageProvider.getDirectory();
final regExp = RegExp(r'[^a-zA-Z0-9 .()\-\s]');
final forbiddenCharacters =
RegExp(r'[\\/:*?"<>|\0]|(^CON$|^PRN$|^AUX$|^NUL$|^COM[1-9]$|^LPT[1-9]$)');
String scanlator = chapter.scanlator!.isNotEmpty
? "${chapter.scanlator!.replaceAll(regExp, '_')}_"
? "${chapter.scanlator!.replaceAll(forbiddenCharacters, '_')}_"
: "";
final chapterName = chapter.name!.replaceAll(forbiddenCharacters, ' ');
final isManga = chapter.manga.value!.isManga!;
final finalPath =
"downloads/${isManga ? "Manga" : "Anime"}/${manga.source} (${manga.lang!.toUpperCase()})/${manga.name!.replaceAll(regExp, '_')}${isManga ? "/$scanlator${chapter.name!.replaceAll(regExp, '_')}" : ""}";
"downloads/${isManga ? "Manga" : "Anime"}/${manga.source} (${manga.lang!.toUpperCase()})/${manga.name!.replaceAll(forbiddenCharacters, '_')}${isManga ? "/$scanlator${chapter.name!.replaceAll(forbiddenCharacters, '_')}" : ""}";
path = Directory("${path1!.path}$finalPath/");
Map<String, String> videoHeader = {};
@ -110,8 +111,7 @@ Future<List<PageUrl>> downloadChapter(
bool cbzFileExist =
await File("${mangaDir!.path}${chapter.name}.cbz").exists() &&
ref.watch(saveAsCBZArchiveStateProvider);
bool mp4FileExist =
await File("${mangaDir.path}${chapter.name}.mp4").exists();
bool mp4FileExist = await File("${mangaDir.path}$chapterName.mp4").exists();
if (!cbzFileExist && isManga || !mp4FileExist && !isManga) {
for (var index = 0; index < pageUrls.length; index++) {
final path2 = Directory("${path1.path}downloads/");
@ -119,8 +119,8 @@ Future<List<PageUrl>> downloadChapter(
Directory("${path1.path}downloads/${isManga ? "Manga" : "Anime"}/");
final path4 = Directory(
"${path5.path}${manga.source} (${manga.lang!.toUpperCase()})/");
final path3 =
Directory("${path4.path}${manga.name!.replaceAll(regExp, '_')}/");
final path3 = Directory(
"${path4.path}${manga.name!.replaceAll(forbiddenCharacters, '_')}/");
if (!(await path1.exists())) {
await path1.create();
@ -153,80 +153,89 @@ Future<List<PageUrl>> downloadChapter(
headers.addAll(cookie);
headers[HttpHeaders.userAgentHeader] = userAgent;
}
Map<String, String> pageHeaders = headers;
pageHeaders.addAll(page.headers ?? {});
if (isManga) {
if ((await path.exists())) {
if (await File("${path.path}" "${padIndex(index + 1)}.jpg")
.exists()) {
} else {
Map<String, String> pageHeaders = headers;
pageHeaders.addAll(page.headers ?? {});
tasks.add(DownloadTask(
taskId: page.url,
headers: pageHeaders,
url: page.url.trim().trimLeft().trimRight(),
filename: "${padIndex(index + 1)}.jpg",
baseDirectory: BaseDirectory.temporary,
directory: 'Mangayomi/$finalPath',
updates: Updates.statusAndProgress,
retries: 3,
allowPause: true,
requiresWiFi: onlyOnWifi));
}
final file = File(
"${tempDir.path}/Mangayomi/$finalPath/${padIndex(index + 1)}.jpg");
if (file.existsSync()) {
await file.copy("${path.path}${padIndex(index + 1)}.jpg");
await file.delete();
} else {
await path.create();
if (await File("${path.path}" "${padIndex(index + 1)}.jpg")
.exists()) {
if ((await path.exists())) {
if (await File("${path.path}${padIndex(index + 1)}.jpg")
.exists()) {
} else {
tasks.add(DownloadTask(
taskId: page.url,
headers: pageHeaders,
url: page.url.trim().trimLeft().trimRight(),
filename: "${padIndex(index + 1)}.jpg",
baseDirectory: BaseDirectory.temporary,
directory: 'Mangayomi/$finalPath',
updates: Updates.statusAndProgress,
retries: 3,
allowPause: true,
requiresWiFi: onlyOnWifi));
}
} else {
Map<String, String> pageHeaders = headers;
pageHeaders.addAll(page.headers ?? {});
tasks.add(DownloadTask(
taskId: page.url,
headers: pageHeaders,
url: page.url.trim().trimLeft().trimRight(),
filename: "${padIndex(index + 1)}.jpg",
baseDirectory: BaseDirectory.temporary,
directory: 'Mangayomi/$finalPath',
updates: Updates.statusAndProgress,
allowPause: true,
retries: 3,
requiresWiFi: onlyOnWifi));
await path.create();
if (await File("${path.path}" "${padIndex(index + 1)}.jpg")
.exists()) {
} else {
tasks.add(DownloadTask(
taskId: page.url,
headers: pageHeaders,
url: page.url.trim().trimLeft().trimRight(),
filename: "${padIndex(index + 1)}.jpg",
baseDirectory: BaseDirectory.temporary,
directory: 'Mangayomi/$finalPath',
updates: Updates.statusAndProgress,
allowPause: true,
retries: 3,
requiresWiFi: onlyOnWifi));
}
}
}
} else {
if ((await path.exists())) {
if (await File("${path.path}${chapter.name}.mp4").exists()) {
} else {
Map<String, String> pageHeaders = headers;
pageHeaders.addAll(page.headers ?? {});
tasks.add(DownloadTask(
taskId: page.url,
headers: pageHeaders,
url: page.url.trim().trimLeft().trimRight(),
filename: "${chapter.name}.mp4",
baseDirectory: BaseDirectory.temporary,
directory: 'Mangayomi/$finalPath',
updates: Updates.statusAndProgress,
allowPause: true,
retries: 3,
requiresWiFi: onlyOnWifi));
}
final file =
File("${tempDir.path}/Mangayomi/$finalPath/$chapterName.mp4");
if (file.existsSync()) {
await file.copy("${path.path}$chapterName.mp4");
await file.delete();
} else {
await path.create();
if (await File("${path.path}${chapter.name}.mp4").exists()) {
if ((await path.exists())) {
if (await File("${path.path}$chapterName.mp4").exists()) {
} else {
tasks.add(DownloadTask(
taskId: page.url,
headers: pageHeaders,
url: page.url.trim().trimLeft().trimRight(),
filename: "$chapterName.mp4",
baseDirectory: BaseDirectory.temporary,
directory: 'Mangayomi/$finalPath',
updates: Updates.statusAndProgress,
allowPause: true,
retries: 3,
requiresWiFi: onlyOnWifi));
}
} else {
Map<String, String> pageHeaders = headers;
pageHeaders.addAll(page.headers ?? {});
tasks.add(DownloadTask(
taskId: page.url,
headers: pageHeaders,
url: page.url.trim().trimLeft().trimRight(),
filename: "${chapter.name}.mp4",
baseDirectory: BaseDirectory.temporary,
directory: 'Mangayomi/$finalPath',
updates: Updates.statusAndProgress,
allowPause: true,
retries: 3,
requiresWiFi: onlyOnWifi));
await path.create();
if (await File("${path.path}$chapterName.mp4").exists()) {
} else {
tasks.add(DownloadTask(
taskId: page.url,
headers: pageHeaders,
url: page.url.trim().trimLeft().trimRight(),
filename: "$chapterName.mp4",
baseDirectory: BaseDirectory.temporary,
directory: 'Mangayomi/$finalPath',
updates: Updates.statusAndProgress,
allowPause: true,
retries: 3,
requiresWiFi: onlyOnWifi));
}
}
}
}
@ -249,24 +258,16 @@ Future<List<PageUrl>> downloadChapter(
isar.downloads.putSync(download..chapter.value = chapter);
});
} else {
if (isManga) {
await FileDownloader(
useNativeHttpClient:
ref.watch(useNativeHttpClientStateProvider))
.downloadBatch(
tasks,
batchProgressCallback: (succeeded, failed) async {
await FileDownloader().downloadBatch(
tasks,
batchProgressCallback: (succeeded, failed) async {
if (isManga) {
if (succeeded == tasks.length) {
if (isManga) {
savePageUrls();
if (ref.watch(saveAsCBZArchiveStateProvider)) {
await ref.watch(convertToCBZProvider(
path!.path,
mangaDir.path,
chapter.name!,
pageUrls.map((e) => e.url).toList())
.future);
}
savePageUrls();
if (ref.watch(saveAsCBZArchiveStateProvider)) {
await ref.watch(convertToCBZProvider(path!.path, mangaDir.path,
chapter.name!, pageUrls.map((e) => e.url).toList())
.future);
}
}
bool isEmpty = isar.downloads
@ -298,20 +299,11 @@ Future<List<PageUrl>> downloadChapter(
..isDownload = (succeeded == tasks.length));
});
}
},
taskProgressCallback: (taskProgress) async {
if (taskProgress.progress == 1.0) {
final file = File(
"${tempDir.path}/${taskProgress.task.directory}/${taskProgress.task.filename}");
await file.copy("${path!.path}/${taskProgress.task.filename}");
await file.delete();
}
},
);
} else {
await FileDownloader(useNativeHttpClient: false).download(
tasks.first,
onProgress: (progress) async {
}
},
taskProgressCallback: (taskProgress) async {
final progress = taskProgress.progress;
if (!isManga) {
bool isEmpty = isar.downloads
.filter()
.chapterIdEqualTo(chapter.id!)
@ -341,17 +333,15 @@ Future<List<PageUrl>> downloadChapter(
..isDownload = (progress == 1.0));
});
}
},
onStatus: (status) async {
if (status == TaskStatus.complete) {
final file = File(
"${tempDir.path}/${tasks.first.directory}/${tasks.first.filename}");
await file.copy("${path!.path}/${tasks.first.filename}");
await file.delete();
}
},
);
}
}
if (progress == 1.0) {
final file = File(
"${tempDir.path}/${taskProgress.task.directory}/${taskProgress.task.filename}");
await file.copy("${path!.path}${taskProgress.task.filename}");
await file.delete();
}
},
);
}
}
return pageUrls;

View file

@ -6,7 +6,7 @@ part of 'download_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$downloadChapterHash() => r'a2ba0ce07800518f35f47fa2272049357141a854';
String _$downloadChapterHash() => r'eca8ccbe5f93f07c3471af81355fe9b3a8ec11e8';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -149,8 +149,8 @@ class DownloadQueueScreen extends ConsumerWidget {
});
} else if (value.toString() == 'CancelAll') {
final chapterIds = entries
.where((element) =>
element.chapter.value!.name ==
.where((e) =>
e.chapter.value!.name ==
element.chapter.value!.name)
.map((e) => e.chapterId)
.toList();

View file

@ -1,43 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/more/settings/advanced/providers/native_http_client.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
class AdvancedScreen extends ConsumerWidget {
const AdvancedScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = l10nLocalizations(context)!;
final useNativeHttpClient = ref.watch(useNativeHttpClientStateProvider);
return Scaffold(
appBar: AppBar(
title: Text(l10n.advanced),
),
body: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: SwitchListTile(
title: Text(l10n.use_native_http_client),
subtitle: Text(
l10n.use_native_http_client_info,
style:
TextStyle(fontSize: 11, color: context.secondaryColor),
),
value: useNativeHttpClient,
onChanged: (value) {
ref
.read(useNativeHttpClientStateProvider.notifier)
.set(value);
}),
),
],
),
),
);
}
}

View file

@ -1,19 +0,0 @@
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'native_http_client.g.dart';
@riverpod
class UseNativeHttpClientState extends _$UseNativeHttpClientState {
@override
bool build() {
return isar.settings.getSync(227)!.useNativeHttpClient ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(settings!..useNativeHttpClient = value));
}
}

View file

@ -1,27 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'native_http_client.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$useNativeHttpClientStateHash() =>
r'477bc54ed73067db40dc291532818108a2a37536';
/// See also [UseNativeHttpClientState].
@ProviderFor(UseNativeHttpClientState)
final useNativeHttpClientStateProvider =
AutoDisposeNotifierProvider<UseNativeHttpClientState, bool>.internal(
UseNativeHttpClientState.new,
name: r'useNativeHttpClientStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$useNativeHttpClientStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$UseNativeHttpClientState = AutoDisposeNotifier<bool>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:mangayomi/modules/more/widgets/list_tile_widget.dart';
@ -48,12 +46,6 @@ class SettingsScreen extends StatelessWidget {
subtitle: l10n.browse_subtitle,
icon: Icons.explore_rounded,
onTap: () => context.push('/browseS')),
if (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)
ListTileWidget(
title: l10n.advanced,
subtitle: l10n.browse_subtitle,
icon: Icons.code_rounded,
onTap: () => context.push('/advancedScreen')),
ListTileWidget(
onTap: () {
context.push('/about');

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:draggable_menu/draggable_menu.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -119,7 +121,7 @@ Future<void> customDraggableTabBar(
}
: 0;
return Scaffold(
backgroundColor: Colors.transparent,
backgroundColor: Platform.isLinux ? null : Colors.transparent,
body: Container(
width: context.width(1) - width,
decoration: BoxDecoration(

View file

@ -17,7 +17,9 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:path/path.dart' as path;
class StorageProvider {
final RegExp _regExpChar = RegExp(r'[^a-zA-Z0-9 .()\-\s]');
final _forbiddenCharacters =
RegExp(r'[\\/:*?"<>|\0]|(^CON$|^PRN$|^AUX$|^NUL$|^COM[1-9]$|^LPT[1-9]$)');
Future<bool> requestPermission() async {
Permission permission = Permission.manageExternalStorage;
if (Platform.isAndroid) {
@ -78,12 +80,12 @@ class StorageProvider {
) async {
final manga = chapter.manga.value!;
String scanlator = chapter.scanlator?.isNotEmpty ?? false
? "${chapter.scanlator!.replaceAll(_regExpChar, '_')}_"
? "${chapter.scanlator!.replaceAll(_forbiddenCharacters, '_')}_"
: "";
final isManga = chapter.manga.value!.isManga!;
final dir = await getDirectory();
return Directory(
"${dir!.path}/downloads/${isManga ? "Manga" : "Anime"}/${manga.source} (${manga.lang!.toUpperCase()})/${manga.name!.replaceAll(_regExpChar, '_')}/$scanlator${chapter.name!.replaceAll(_regExpChar, '_')}/");
"${dir!.path}/downloads/${isManga ? "Manga" : "Anime"}/${manga.source} (${manga.lang!.toUpperCase()})/${manga.name!.replaceAll(_forbiddenCharacters, '_')}/$scanlator${chapter.name!.replaceAll(_forbiddenCharacters, '_')}/");
}
Future<Directory?> getMangaMainDirectory(Chapter chapter) async {
@ -91,7 +93,7 @@ class StorageProvider {
final isManga = chapter.manga.value!.isManga!;
final dir = await getDirectory();
return Directory(
"${dir!.path}/downloads/${isManga ? "Manga" : "Anime"}/${manga.source} (${manga.lang!.toUpperCase()})/${manga.name!.replaceAll(_regExpChar, '_')}/");
"${dir!.path}/downloads/${isManga ? "Manga" : "Anime"}/${manga.source} (${manga.lang!.toUpperCase()})/${manga.name!.replaceAll(_forbiddenCharacters, '_')}/");
}
Future<Directory?> getDatabaseDirectory() async {

View file

@ -12,7 +12,6 @@ import 'package:mangayomi/modules/browse/extension/widgets/create_extension.dart
import 'package:mangayomi/modules/browse/sources/sources_filter_screen.dart';
import 'package:mangayomi/modules/more/backup_and_restore/backup_and_restore.dart';
import 'package:mangayomi/modules/more/categories/categories_screen.dart';
import 'package:mangayomi/modules/more/settings/advanced/advanced.dart';
import 'package:mangayomi/modules/more/settings/downloads/downloads_screen.dart';
import 'package:mangayomi/modules/more/settings/player/player_screen.dart';
import 'package:mangayomi/modules/more/settings/track/track.dart';
@ -534,19 +533,6 @@ class RouterNotifier extends ChangeNotifier {
);
},
),
GoRoute(
path: "/advancedScreen",
name: "advancedScreen",
builder: (context, state) {
return const AdvancedScreen();
},
pageBuilder: (context, state) {
return transitionPage(
key: state.pageKey,
child: const AdvancedScreen(),
);
},
),
];
}

View file

@ -10,7 +10,8 @@ class DoodExtractor {
String? quality,
bool redirect = true,
}) async {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
final newQuality = quality ?? ('Doodstream ${redirect ? ' mirror' : ''}');
try {

View file

@ -7,7 +7,8 @@ import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/utils/xpath_selector.dart';
class FilemoonExtractor {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
Future<List<Video>> videosFromUrl(
String url, String prefix, String suffix) async {

View file

@ -8,7 +8,8 @@ import 'package:mangayomi/services/http/m_client.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
class GogoCdnExtractor {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
final JsonCodec json = const JsonCodec();
Future<List<Video>> videosFromUrl(String serverUrl) async {

View file

@ -8,7 +8,8 @@ import 'package:mangayomi/utils/xpath_selector.dart';
class Mp4uploadExtractor {
static final RegExp qualityRegex = RegExp(r'\WHEIGHT=(\d+)');
static const String referer = "https://mp4upload.com/";
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
Future<List<Video>> videosFromUrl(String url, Map<String, String> headers,
{String prefix = '', String suffix = ''}) async {
final newHeaders = Map<String, String>.from(headers)

View file

@ -9,7 +9,8 @@ class MyStreamExtractor {
String url, Map<String, String> headers) async {
final host = url.substringBefore("/watch");
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
try {
final response = await client.get(Uri.parse(url), headers: headers);

View file

@ -5,7 +5,8 @@ import 'package:mangayomi/services/http/m_client.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
class MytvExtractor {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
Future<List<Video>> videosFromUrl(String url) async {
try {

View file

@ -6,7 +6,8 @@ import 'package:mangayomi/utils/extensions/dom_extensions.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
class OkruExtractor {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
Future<List<Video>> videosFromUrl(String url,
{String prefix = "", bool fixQualities = true}) async {

View file

@ -5,7 +5,8 @@ import 'package:mangayomi/services/http/m_client.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
class SendvidExtractor {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
final Map<String, String> headers;
SendvidExtractor(this.headers);

View file

@ -4,7 +4,8 @@ import 'package:mangayomi/services/http/m_client.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
class SibnetExtractor {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
Future<List<Video>> videosFromUrl(String url, {String prefix = ""}) async {
List<Video> videoList = [];

View file

@ -5,7 +5,8 @@ import 'package:mangayomi/utils/extensions/others.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
class StreamlareExtractor {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
Future<List<Video>> videosFromUrl(String url,
{String prefix = "", String suffix = ""}) async {

View file

@ -7,7 +7,8 @@ import 'package:mangayomi/utils/extensions/string_extensions.dart';
class StreamTapeExtractor {
Future<List<Video>> videosFromUrl(String url,
{String quality = "StreamTape"}) async {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
try {
const baseUrl = "https://streamtape.com/e/";
final newUrl =

View file

@ -7,7 +7,8 @@ import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/utils/xpath_selector.dart';
class StreamWishExtractor {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
final Map<String, String> headers = {};
Future<List<Video>> videosFromUrl(String url, String prefix) async {

View file

@ -5,7 +5,8 @@ import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/utils/xpath_selector.dart';
class VidBomExtractor {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
Future<List<Video>> videosFromUrl(String url) async {
try {

View file

@ -6,7 +6,8 @@ import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/utils/xpath_selector.dart';
class VoeExtractor {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
final linkRegex = RegExp(
r'(http|https)://([\w_-]+(?:\.[\w_-]+)+)([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])');

View file

@ -5,7 +5,8 @@ import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/utils/xpath_selector.dart';
class YourUploadExtractor {
final InterceptedClient client = MClient.init();
final InterceptedClient client =
MClient.init(reqcopyWith: {'useDartHttpClient': true});
Future<List<Video>> videosFromUrl(String url, Map<String, String> headers,
{String name = "YourUpload", String prefix = ""}) async {

View file

@ -6,8 +6,7 @@ import 'dart:math';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_http_client.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_native_http_client.dart.dart';
import 'package:mangayomi/services/background_downloader/src/downloader/downloader_http_client.dart';
import 'database.dart';
import 'exceptions.dart';
import 'models.dart';
@ -100,11 +99,9 @@ abstract base class BaseDownloader {
BaseDownloader();
factory BaseDownloader.instance(PersistentStorage persistentStorage,
Database database, bool useNativeHttpClient) {
final instance = !useNativeHttpClient
? DesktopDownloaderHttpClient()
: DesktopDownloaderNativeHttpClient();
factory BaseDownloader.instance(
PersistentStorage persistentStorage, Database database) {
final instance = DownloaderHttpClient();
instance._storage = persistentStorage;
instance.database = database;
unawaited(instance.initialize());

View file

@ -1,604 +0,0 @@
// ignore_for_file: depend_on_referenced_packages
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:isolate';
import 'package:async/async.dart';
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
import 'package:http/io_client.dart';
import 'package:logging/logging.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import '../base_downloader.dart';
import '../chunk.dart';
import '../exceptions.dart';
import '../file_downloader.dart';
import '../models.dart';
import '../task.dart';
import '../utils.dart';
import 'isolate.dart';
const okResponses = [200, 201, 202, 203, 204, 205, 206];
/// Implementation of download functionality for desktop platforms
///
/// On desktop (MacOS, Linux, Windows) the download and upload are implemented
/// in Dart, as there is no native platform equivalent of URLSession or
/// WorkManager as there is on iOS and Android
final class DesktopDownloaderNativeHttpClient extends BaseDownloader {
static final _log = Logger('DesktopDownloaderNativeHttpClient');
static const unlimited = 1 << 20;
var maxConcurrent = 10;
var maxConcurrentByHost = unlimited;
var maxConcurrentByGroup = unlimited;
static final DesktopDownloaderNativeHttpClient _singleton =
DesktopDownloaderNativeHttpClient._internal();
final _queue = PriorityQueue<Task>();
final _running = Queue<Task>(); // subset that is running
final _resume = <Task>{};
final _isolateSendPorts =
<Task, SendPort?>{}; // isolate SendPort for running task
static var httpClient = MClient.nativeHttpClient();
static Duration? _requestTimeout;
static var _proxy = <String, dynamic>{}; // 'address' and 'port'
static var _bypassTLSCertificateValidation = false;
factory DesktopDownloaderNativeHttpClient() => _singleton;
DesktopDownloaderNativeHttpClient._internal();
@override
Future<bool> enqueue(Task task) async {
try {
Uri.decodeFull(task.url);
} catch (e) {
_log.fine('Invalid url: ${task.url} error: $e');
return false;
}
super.enqueue(task);
_queue.add(task);
processStatusUpdate(TaskStatusUpdate(task, TaskStatus.enqueued));
_advanceQueue();
return true;
}
/// Advance the queue if it's not empty and there is room in the run queue
void _advanceQueue() {
while (_running.length < maxConcurrent && _queue.isNotEmpty) {
final task = _getNextTask();
if (task != null) {
_running.add(task);
_executeTask(task).then((_) {
_remove(task);
_advanceQueue();
});
} else {
return; // if no suitable task, done
}
}
}
/// Returns a [Task] to run, or null if no suitable task is available
Task? _getNextTask() {
final tasksThatHaveToWait = <Task>[];
while (_queue.isNotEmpty) {
final task = _queue.removeFirst();
if (_numActiveWithHostname(task.hostName) < maxConcurrentByHost &&
_numActiveWithGroup(task.group) < maxConcurrentByGroup) {
_queue.addAll(tasksThatHaveToWait); // put back in queue
return task;
}
tasksThatHaveToWait.add(task);
}
_queue.addAll(tasksThatHaveToWait); // put back in queue
return null;
}
/// Returns number of tasks active with this [hostname]
int _numActiveWithHostname(String hostname) => _running.fold(
0,
(previousValue, task) =>
task.hostName == hostname ? previousValue + 1 : previousValue);
/// Returns number of tasks active with this [group]
int _numActiveWithGroup(String group) => _running.fold(
0,
(previousValue, task) =>
task.group == group ? previousValue + 1 : previousValue);
/// Execute this task
///
/// The task runs on an Isolate, which is sent the task information and
/// which will emit status and progress updates. These updates will be
/// 'forwarded' to the [backgroundChannel] and processed by the
/// [FileDownloader]
Future<void> _executeTask(Task task) async {
final resumeData = await getResumeData(task.taskId);
if (resumeData != null) {
await removeResumeData(task.taskId);
}
final isResume = _resume.remove(task) && resumeData != null;
final filePath = await task.filePath(); // "" for MultiUploadTask
// spawn an isolate to do the task
final receivePort = ReceivePort();
final errorPort = ReceivePort();
errorPort.listen((message) {
final exceptionDescription = (message as List).first as String;
final stackTrace = message.last;
logError(task, exceptionDescription);
log.fine('Stack trace: $stackTrace');
processStatusUpdate(TaskStatusUpdate(
task, TaskStatus.failed, TaskException(exceptionDescription)));
receivePort.close(); // also ends listener at the end
});
RootIsolateToken? rootIsolateToken = RootIsolateToken.instance;
if (rootIsolateToken == null) {
processStatusUpdate(TaskStatusUpdate(task, TaskStatus.failed,
TaskException('Could not obtain rootIsolateToken')));
return;
}
log.finer('${isResume ? "Resuming" : "Starting"} taskId ${task.taskId}');
await Isolate.spawn(doTask, (rootIsolateToken, receivePort.sendPort, true),
onError: errorPort.sendPort);
final messagesFromIsolate = StreamQueue<dynamic>(receivePort);
final sendPort = await messagesFromIsolate.next as SendPort;
sendPort.send((
task,
filePath,
resumeData,
isResume,
requestTimeout,
proxy,
bypassTLSCertificateValidation
));
if (_isolateSendPorts.keys.contains(task)) {
// if already registered with null value, cancel immediately
sendPort.send('cancel');
}
// store the isolate's sendPort so we can send it messages for
// cancellation, and for managing parallel downloads
_isolateSendPorts[task] = sendPort;
// listen for messages sent back from the isolate, until 'done'
// note that the task sent by the isolate may have changed. Therefore, we
// use updatedTask instead of task from here on
while (await messagesFromIsolate.hasNext) {
final message = await messagesFromIsolate.next;
switch (message) {
case 'done':
receivePort.close();
case (
'statusUpdate',
Task updatedTask,
TaskStatus status,
TaskException? exception,
String? responseBody,
Map<String, String>? responseHeaders,
int? responseCode,
String? mimeType,
String? charSet
):
final taskStatusUpdate = TaskStatusUpdate(
updatedTask,
status,
exception,
responseBody,
responseHeaders,
responseCode,
mimeType,
charSet);
if (updatedTask.group != BaseDownloader.chunkGroup) {
if (status.isFinalState) {
_remove(updatedTask);
}
processStatusUpdate(taskStatusUpdate);
} else {
_parallelTaskSendPort(Chunk.getParentTaskId(updatedTask))
?.send(taskStatusUpdate);
}
case (
'progressUpdate',
Task updatedTask,
double progress,
int expectedFileSize,
double downloadSpeed,
Duration timeRemaining
):
final taskProgressUpdate = TaskProgressUpdate(updatedTask, progress,
expectedFileSize, downloadSpeed, timeRemaining);
if (updatedTask.group != BaseDownloader.chunkGroup) {
processProgressUpdate(taskProgressUpdate);
} else {
_parallelTaskSendPort(Chunk.getParentTaskId(updatedTask))
?.send(taskProgressUpdate);
}
case ('taskCanResume', bool taskCanResume):
setCanResume(task, taskCanResume);
case ('resumeData', String data, int requiredStartByte, String? eTag):
setResumeData(ResumeData(task, data, requiredStartByte, eTag));
// from [ParallelDownloadTask]
case ('enqueueChild', DownloadTask childTask):
await FileDownloader().enqueue(childTask);
// from [ParallelDownloadTask]
case ('cancelTasksWithId', List<String> taskIds):
await FileDownloader().cancelTasksWithIds(taskIds);
// from [ParallelDownloadTask]
case ('pauseTasks', List<DownloadTask> tasks):
for (final chunkTask in tasks) {
await FileDownloader().pause(chunkTask);
}
case ('log', String logMessage):
_log.finest(logMessage);
default:
_log.warning('Received message with unknown type '
'$message from Isolate');
}
}
errorPort.close();
_isolateSendPorts.remove(task);
}
// intercept the status and progress updates for tasks that are 'chunks', i.e.
// part of a [ParallelDownloadTask]. Updates for these tasks are sent to the
// isolate running the [ParallelDownloadTask] instead
@override
void processStatusUpdate(TaskStatusUpdate update) {
// Regular update if task's group is not chunkGroup
if (update.task.group != FileDownloader.chunkGroup) {
return super.processStatusUpdate(update);
}
// If chunkGroup, send update to task's parent isolate.
// The task's metadata contains taskId of parent
_parallelTaskSendPort(Chunk.getParentTaskId(update.task))?.send(update);
}
@override
void processProgressUpdate(TaskProgressUpdate update) {
// Regular update if task's group is not chunkGroup
if (update.task.group != FileDownloader.chunkGroup) {
return super.processProgressUpdate(update);
}
// If chunkGroup, send update to task's parent isolate.
// The task's metadata contains taskId of parent
_parallelTaskSendPort(Chunk.getParentTaskId(update.task))?.send(update);
}
/// Return the [SendPort] for the [ParallelDownloadTask] represented by [taskId]
/// or null if not a [ParallelDownloadTask] or not found
SendPort? _parallelTaskSendPort(String taskId) => _isolateSendPorts.entries
.firstWhereOrNull((entry) =>
entry.key is ParallelDownloadTask && entry.key.taskId == taskId)
?.value;
@override
Future<int> reset(String group) async {
final retryAndPausedTaskCount = await super.reset(group);
final inQueueIds = _queue.unorderedElements
.where((task) => task.group == group)
.map((task) => task.taskId);
final runningIds = _running
.where((task) => task.group == group)
.map((task) => task.taskId);
final taskIds = [...inQueueIds, ...runningIds];
if (taskIds.isNotEmpty) {
await cancelTasksWithIds(taskIds);
}
return retryAndPausedTaskCount + taskIds.length;
}
@override
Future<List<Task>> allTasks(
String group, bool includeTasksWaitingToRetry) async {
final retryAndPausedTasks =
await super.allTasks(group, includeTasksWaitingToRetry);
final inQueue =
_queue.unorderedElements.where((task) => task.group == group);
final running = _running.where((task) => task.group == group);
return [...retryAndPausedTasks, ...inQueue, ...running];
}
/// Cancels ongoing platform tasks whose taskId is in the list provided
///
/// Returns true if all cancellations were successful
@override
Future<bool> cancelPlatformTasksWithIds(List<String> taskIds) async {
final inQueue = _queue.unorderedElements
.where((task) => taskIds.contains(task.taskId))
.toList(growable: false);
for (final task in inQueue) {
processStatusUpdate(TaskStatusUpdate(task, TaskStatus.canceled));
_remove(task);
}
final running = _running.where((task) => taskIds.contains(task.taskId));
for (final task in running) {
final sendPort = _isolateSendPorts[task];
if (sendPort != null) {
sendPort.send('cancel');
_isolateSendPorts.remove(task);
} else {
// register task for cancellation even if sendPort does not yet exist:
// this will lead to immediate cancellation when the Isolate starts
_isolateSendPorts[task] = null;
}
}
return true;
}
@override
Future<Task?> taskForId(String taskId) async {
var task = await super.taskForId(taskId);
if (task != null) {
return task;
}
try {
return _running.where((task) => task.taskId == taskId).first;
} on StateError {
try {
return _queue.unorderedElements
.where((task) => task.taskId == taskId)
.first;
} on StateError {
return null;
}
}
}
@override
Future<bool> pause(Task task) async {
final sendPort = _isolateSendPorts[task];
if (sendPort != null) {
sendPort.send('pause');
return true;
}
return false;
}
@override
Future<bool> resume(Task task) async {
if (await super.resume(task)) {
task = awaitTasks.containsKey(task)
? awaitTasks.keys
.firstWhere((awaitTask) => awaitTask.taskId == task.taskId)
: task;
_resume.add(task);
if (await enqueue(task)) {
if (task is ParallelDownloadTask) {
final resumeData = await getResumeData(task.taskId);
if (resumeData == null) {
return false;
}
return resumeChunkTasks(task, resumeData);
}
return true;
}
}
return false;
}
@override
Future<Map<String, String>> popUndeliveredData(Undelivered dataType) =>
Future.value({});
@override
Future<String?> moveToSharedStorage(String filePath,
SharedStorage destination, String directory, String? mimeType) async {
final destDirectoryPath =
await getDestinationDirectoryPath(destination, directory);
if (destDirectoryPath == null) {
return null;
}
if (!await Directory(destDirectoryPath).exists()) {
await Directory(destDirectoryPath).create(recursive: true);
}
final fileName = path.basename(filePath);
final destFilePath = path.join(destDirectoryPath, fileName);
try {
await File(filePath).rename(destFilePath);
} on FileSystemException catch (e) {
_log.warning('Error moving $filePath to shared storage: $e');
return null;
}
return destFilePath;
}
@override
Future<String?> pathInSharedStorage(
String filePath, SharedStorage destination, String directory) async {
final destDirectoryPath =
await getDestinationDirectoryPath(destination, directory);
if (destDirectoryPath == null) {
return null;
}
final fileName = path.basename(filePath);
return path.join(destDirectoryPath, fileName);
}
/// Returns the path of the destination directory in shared storage, or null
///
/// Only the .Downloads directory is supported on desktop.
/// The [directory] is appended to the base Downloads directory.
/// The directory at the returned path is not guaranteed to exist.
Future<String?> getDestinationDirectoryPath(
SharedStorage destination, String directory) async {
if (destination != SharedStorage.downloads) {
_log.finer('Desktop only supports .downloads destination');
return null;
}
final downloadsDirectory = await getDownloadsDirectory();
if (downloadsDirectory == null) {
_log.warning('Could not obtain downloads directory');
return null;
}
// remove leading and trailing slashes from [directory]
var cleanDirectory = directory.replaceAll(RegExp(r'^/+'), '');
cleanDirectory = cleanDirectory.replaceAll(RegExp(r'/$'), '');
return cleanDirectory.isEmpty
? downloadsDirectory.path
: path.join(downloadsDirectory.path, cleanDirectory);
}
@override
Future<bool> openFile(Task? task, String? filePath, String? mimeType) async {
final executable = Platform.isLinux
? 'xdg-open'
: Platform.isMacOS
? 'open'
: 'start';
filePath ??= await task!.filePath();
if (!await File(filePath).exists()) {
_log.fine('File to open does not exist: $filePath');
return false;
}
final result = await Process.run(executable, [filePath], runInShell: true);
if (result.exitCode != 0) {
_log.fine(
'openFile command $executable returned exit code ${result.exitCode}');
}
return result.exitCode == 0;
}
@override
Future<Duration> getTaskTimeout() => Future.value(const Duration(days: 1));
@override
Future<void> setForceFailPostOnBackgroundChannel(bool value) {
throw UnimplementedError();
}
@override
Future<String> testSuggestedFilename(
DownloadTask task, String contentDisposition) async {
final h = contentDisposition.isNotEmpty
? {'Content-disposition': contentDisposition}
: <String, String>{};
final t = await taskWithSuggestedFilename(task, h, false);
return t.filename;
}
@override
dynamic platformConfig(
{dynamic globalConfig,
dynamic androidConfig,
dynamic iOSConfig,
dynamic desktopConfig}) =>
desktopConfig;
@override
Future<(String, String)> configureItem((String, dynamic) configItem) async {
switch (configItem) {
case (Config.requestTimeout, Duration? duration):
requestTimeout = duration;
case (Config.proxy, (String address, int port)):
proxy = {'address': address, 'port': port};
case (Config.proxy, false):
proxy = {};
case (Config.bypassTLSCertificateValidation, bool bypass):
bypassTLSCertificateValidation = bypass;
case (
Config.holdingQueue,
(
int? maxConcurrentParam,
int? maxConcurrentByHostParam,
int? maxConcurrentByGroupParam
)
):
maxConcurrent = maxConcurrentParam ?? 10;
maxConcurrentByHost = maxConcurrentByHostParam ?? unlimited;
maxConcurrentByGroup = maxConcurrentByGroupParam ?? unlimited;
default:
return (
configItem.$1,
'not implemented'
); // this method did not process this configItem
}
return (configItem.$1, ''); // normal result
}
/// Sets requestTimeout and recreates HttpClient
static set requestTimeout(Duration? value) {
_requestTimeout = value;
_recreateClient();
}
static Duration? get requestTimeout => _requestTimeout;
/// Sets proxy and recreates HttpClient
///
/// Value must be dict containing 'address' and 'port'
/// or empty for no proxy
static set proxy(Map<String, dynamic> value) {
_proxy = value;
_recreateClient();
}
static Map<String, dynamic> get proxy => _proxy;
/// Set or resets bypass for TLS certificate validation
static set bypassTLSCertificateValidation(bool value) {
_bypassTLSCertificateValidation = value;
_recreateClient();
}
static bool get bypassTLSCertificateValidation =>
_bypassTLSCertificateValidation;
/// Set the HTTP Client to use, with the given parameters
///
/// This is a convenience method, bundling the [requestTimeout],
/// [proxy] and [bypassTLSCertificateValidation]
static void setHttpClient(Duration? requestTimeout,
Map<String, dynamic> proxy, bool bypassTLSCertificateValidation) {
_requestTimeout = requestTimeout;
_proxy = proxy;
_bypassTLSCertificateValidation = bypassTLSCertificateValidation;
_recreateClient();
}
/// Recreates the [httpClient] used for Requests and isolate downloads/uploads
static _recreateClient() async {
if (Platform.isWindows || Platform.isLinux) {
final client = HttpClient();
client.connectionTimeout = requestTimeout;
client.findProxy = proxy.isNotEmpty
? (_) => 'PROXY ${_proxy['address']}:${_proxy['port']}'
: null;
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
httpClient = IOClient(client);
} else {
httpClient = MClient.nativeHttpClient();
}
}
@override
void destroy() {
super.destroy();
_queue.clear();
_running.clear();
_isolateSendPorts.clear();
}
/// Remove all references to [task]
void _remove(Task task) {
_queue.remove(task);
_running.remove(task);
_isolateSendPorts.remove(task);
}
}

View file

@ -2,9 +2,7 @@ import 'dart:async';
import 'dart:isolate';
import 'package:http/http.dart' as http;
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_http_client.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_native_http_client.dart.dart'
as desktop_downloader_native;
import 'package:mangayomi/services/background_downloader/src/downloader/downloader_http_client.dart';
import 'package:mangayomi/services/background_downloader/background_downloader.dart';
import 'download_isolate.dart';
import 'isolate.dart';
@ -13,11 +11,8 @@ import 'isolate.dart';
///
/// Sends updates via the [sendPort] and can be commanded to cancel via
/// the [messagesToIsolate] queue
Future<void> doDataTask(
DataTask task, SendPort sendPort, bool useNativeHttpClient) async {
final client = useNativeHttpClient
? desktop_downloader_native.DesktopDownloaderNativeHttpClient.httpClient
: DesktopDownloaderHttpClient.httpClient;
Future<void> doDataTask(DataTask task, SendPort sendPort) async {
final client = DownloaderHttpClient.httpClient;
var request = http.Request(task.httpRequestMethod, Uri.parse(task.url));
request.headers.addAll(task.headers);
if (task.post is String) {

View file

@ -6,9 +6,7 @@ import 'dart:isolate';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_http_client.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_native_http_client.dart.dart'
as desktop_downloader_native;
import 'package:mangayomi/services/background_downloader/src/downloader/downloader_http_client.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
@ -32,8 +30,7 @@ Future<void> doDownloadTask(
ResumeData? resumeData,
bool isResume,
Duration requestTimeout,
SendPort sendPort,
bool useNativeHttpClient) async {
SendPort sendPort) async {
// use downloadTask from here on as a 'global' variable in this isolate,
// as we may change the filename of the task
downloadTask = task;
@ -48,9 +45,7 @@ Future<void> doDownloadTask(
final eTag = resumeData?.eTag;
isResume = isResume &&
await determineIfResumeIsPossible(tempFilePath, requiredStartByte);
final client = useNativeHttpClient
? desktop_downloader_native.DesktopDownloaderNativeHttpClient.httpClient
: DesktopDownloaderHttpClient.httpClient;
final client = DownloaderHttpClient.httpClient;
var request =
http.Request(downloadTask.httpRequestMethod, Uri.parse(downloadTask.url));
request.headers.addAll(downloadTask.headers);

View file

@ -7,10 +7,11 @@ import 'dart:isolate';
import 'package:async/async.dart';
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
import 'package:http/io_client.dart';
import 'package:logging/logging.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:rhttp/rhttp.dart';
import '../base_downloader.dart';
import '../chunk.dart';
import '../exceptions.dart';
@ -27,29 +28,30 @@ const okResponses = [200, 201, 202, 203, 204, 205, 206];
/// On desktop (MacOS, Linux, Windows) the download and upload are implemented
/// in Dart, as there is no native platform equivalent of URLSession or
/// WorkManager as there is on iOS and Android
final class DesktopDownloaderHttpClient extends BaseDownloader {
static final _log = Logger('DesktopDownloaderHttpClient');
final class DownloaderHttpClient extends BaseDownloader {
static final _log = Logger('DownloaderHttpClient');
static const unlimited = 1 << 20;
var maxConcurrent = 10;
var maxConcurrentByHost = unlimited;
var maxConcurrentByGroup = unlimited;
static final DesktopDownloaderHttpClient _singleton =
DesktopDownloaderHttpClient._internal();
static final DownloaderHttpClient _singleton =
DownloaderHttpClient._internal();
final _queue = PriorityQueue<Task>();
final _running = Queue<Task>(); // subset that is running
final _resume = <Task>{};
final _isolateSendPorts =
<Task, SendPort?>{}; // isolate SendPort for running task
static var httpClient = IOClient(HttpClient()
..badCertificateCallback =
(X509Certificate cert, String host, int port) => true);
static var httpClient = MClient.httpClient(
settings: const ClientSettings(
throwOnStatusCode: false,
tlsSettings: TlsSettings(verifyCertificates: false)));
static Duration? _requestTimeout;
static var _proxy = <String, dynamic>{}; // 'address' and 'port'
static var _bypassTLSCertificateValidation = false;
factory DesktopDownloaderHttpClient() => _singleton;
factory DownloaderHttpClient() => _singleton;
DesktopDownloaderHttpClient._internal();
DownloaderHttpClient._internal();
@override
Future<bool> enqueue(Task task) async {
@ -142,7 +144,7 @@ final class DesktopDownloaderHttpClient extends BaseDownloader {
return;
}
log.finer('${isResume ? "Resuming" : "Starting"} taskId ${task.taskId}');
await Isolate.spawn(doTask, (rootIsolateToken, receivePort.sendPort, false),
await Isolate.spawn(doTask, (rootIsolateToken, receivePort.sendPort),
onError: errorPort.sendPort);
final messagesFromIsolate = StreamQueue<dynamic>(receivePort);
final sendPort = await messagesFromIsolate.next as SendPort;
@ -574,14 +576,11 @@ final class DesktopDownloaderHttpClient extends BaseDownloader {
/// Recreates the [httpClient] used for Requests and isolate downloads/uploads
static _recreateClient() async {
final client = HttpClient();
client.connectionTimeout = requestTimeout;
client.findProxy = proxy.isNotEmpty
? (_) => 'PROXY ${_proxy['address']}:${_proxy['port']}'
: null;
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
httpClient = IOClient(client);
await Rhttp.init();
httpClient = MClient.httpClient(
settings: const ClientSettings(
throwOnStatusCode: false,
tlsSettings: TlsSettings(verifyCertificates: false)));
}
@override

View file

@ -7,8 +7,7 @@ import 'dart:isolate';
import 'dart:math';
import 'package:async/async.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_http_client.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_native_http_client.dart.dart';
import 'package:mangayomi/services/background_downloader/src/downloader/downloader_http_client.dart';
import 'package:mangayomi/services/background_downloader/src/exceptions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
@ -51,8 +50,8 @@ final log = Logger('FileDownloader');
///
/// The first message sent back is a [ReceivePort] that is the command port
/// for the isolate. The first command must be the arguments: task and filePath.
Future<void> doTask((RootIsolateToken, SendPort, bool) isolateArguments) async {
final (rootIsolateToken, sendPort, useNativeHttpClient) = isolateArguments;
Future<void> doTask((RootIsolateToken, SendPort) isolateArguments) async {
final (rootIsolateToken, sendPort) = isolateArguments;
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
final receivePort = ReceivePort();
// send the receive port back to the main Isolate
@ -68,11 +67,8 @@ Future<void> doTask((RootIsolateToken, SendPort, bool) isolateArguments) async {
Map<String, dynamic> proxy,
bool bypassTLSCertificateValidation
) = await messagesToIsolate.next;
useNativeHttpClient
? DesktopDownloaderNativeHttpClient.setHttpClient(
requestTimeout, proxy, bypassTLSCertificateValidation)
: DesktopDownloaderHttpClient.setHttpClient(
requestTimeout, proxy, bypassTLSCertificateValidation);
DownloaderHttpClient.setHttpClient(
requestTimeout, proxy, bypassTLSCertificateValidation);
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((LogRecord rec) {
if (kDebugMode) {
@ -99,18 +95,11 @@ Future<void> doTask((RootIsolateToken, SendPort, bool) isolateArguments) async {
resumeData,
isResume,
requestTimeout ?? const Duration(seconds: 60),
sendPort,
useNativeHttpClient),
DownloadTask() => doDownloadTask(
task,
filePath,
resumeData,
isResume,
requestTimeout ?? const Duration(seconds: 60),
sendPort,
useNativeHttpClient),
UploadTask() => doUploadTask(task, filePath, sendPort,useNativeHttpClient),
DataTask() => doDataTask(task, sendPort, useNativeHttpClient)
sendPort),
DownloadTask() => doDownloadTask(task, filePath, resumeData, isResume,
requestTimeout ?? const Duration(seconds: 60), sendPort),
UploadTask() => doUploadTask(task, filePath, sendPort),
DataTask() => doDataTask(task, sendPort)
};
}
receivePort.close();

View file

@ -7,9 +7,7 @@ import 'dart:isolate';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_http_client.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_native_http_client.dart.dart'
as desktop_downloader_native;
import 'package:mangayomi/services/background_downloader/src/downloader/downloader_http_client.dart';
import '../chunk.dart';
import '../exceptions.dart';
import '../models.dart';
@ -65,15 +63,11 @@ Future<void> doParallelDownloadTask(
ResumeData? resumeData,
bool isResume,
Duration requestTimeout,
SendPort sendPort,
bool useNativeHttpClient) async {
SendPort sendPort) async {
parentTask = task;
if (!isResume) {
// start the download by creating [Chunk]s and enqueuing chunk tasks
final response = await (useNativeHttpClient
? desktop_downloader_native
.DesktopDownloaderNativeHttpClient.httpClient
: DesktopDownloaderHttpClient.httpClient)
final response = await (DownloaderHttpClient.httpClient)
.head(Uri.parse(task.url), headers: task.headers);
responseHeaders = response.headers;
responseStatusCode = response.statusCode;

View file

@ -11,9 +11,7 @@ import 'package:path/path.dart' as p;
import '../models.dart';
import '../task.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_http_client.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_native_http_client.dart.dart'
as desktop_downloader_native;
import 'package:mangayomi/services/background_downloader/src/downloader/downloader_http_client.dart';
import 'isolate.dart';
const boundary = '-----background_downloader-akjhfw281onqciyhnIk';
@ -23,11 +21,11 @@ const lineFeed = '\r\n';
///
/// Sends updates via the [sendPort] and can be commanded to cancel via
/// the [messagesToIsolate] queue
Future<void> doUploadTask(UploadTask task, String filePath, SendPort sendPort,
bool useNativeHttpClient) async {
Future<void> doUploadTask(
UploadTask task, String filePath, SendPort sendPort) async {
final resultStatus = task.post == 'binary'
? await binaryUpload(task, filePath, sendPort, useNativeHttpClient)
: await multipartUpload(task, filePath, sendPort, useNativeHttpClient);
? await binaryUpload(task, filePath, sendPort)
: await multipartUpload(task, filePath, sendPort);
processStatusUpdateInIsolate(task, resultStatus, sendPort);
}
@ -35,8 +33,8 @@ Future<void> doUploadTask(UploadTask task, String filePath, SendPort sendPort,
///
/// Sends updates via the [sendPort] and can be commanded to cancel via
/// the [messagesToIsolate] queue
Future<TaskStatus> binaryUpload(UploadTask task, String filePath,
SendPort sendPort, bool useNativeHttpClient) async {
Future<TaskStatus> binaryUpload(
UploadTask task, String filePath, SendPort sendPort) async {
final inFile = File(filePath);
if (!inFile.existsSync()) {
logError(task, 'file to upload does not exist: $filePath');
@ -47,9 +45,7 @@ Future<TaskStatus> binaryUpload(UploadTask task, String filePath,
final fileSize = inFile.lengthSync();
var resultStatus = TaskStatus.failed;
try {
final client = useNativeHttpClient
? desktop_downloader_native.DesktopDownloaderNativeHttpClient.httpClient
: DesktopDownloaderHttpClient.httpClient;
final client = DownloaderHttpClient.httpClient;
final request =
http.StreamedRequest(task.httpRequestMethod, Uri.parse(task.url));
request.headers.addAll(task.headers);
@ -100,8 +96,8 @@ Future<TaskStatus> binaryUpload(UploadTask task, String filePath,
///
/// Sends updates via the [sendPort] and can be commanded to cancel via
/// the [messagesToIsolate] queue
Future<TaskStatus> multipartUpload(UploadTask task, String filePath,
SendPort sendPort, bool useNativeHttpClient) async {
Future<TaskStatus> multipartUpload(
UploadTask task, String filePath, SendPort sendPort) async {
// field portion of the multipart, all in one string
// multiple values should be encoded as '"value1", "value2", ...'
final multiValueRegEx = RegExp(r'^(?:"[^"]+"\s*,\s*)+"[^"]+"$');
@ -156,9 +152,7 @@ Future<TaskStatus> multipartUpload(UploadTask task, String filePath,
var resultStatus = TaskStatus.failed;
try {
// setup the connection
final client = useNativeHttpClient
? desktop_downloader_native.DesktopDownloaderNativeHttpClient.httpClient
: DesktopDownloaderHttpClient.httpClient;
final client = DownloaderHttpClient.httpClient;
final request =
http.StreamedRequest(task.httpRequestMethod, Uri.parse(task.url));
request.contentLength = contentLength;

View file

@ -6,9 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'base_downloader.dart';
import 'localstore/localstore.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_http_client.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_native_http_client.dart.dart'
as desktop_downloader_native;
import 'package:mangayomi/services/background_downloader/src/downloader/downloader_http_client.dart';
/// Provides access to all functions of the plugin in a single place.
interface class FileDownloader {
@ -33,24 +31,20 @@ interface class FileDownloader {
@visibleForTesting
BaseDownloader get downloaderForTesting => _downloader;
factory FileDownloader(
{PersistentStorage? persistentStorage,
bool useNativeHttpClient = false}) {
factory FileDownloader({PersistentStorage? persistentStorage}) {
assert(
_singleton == null || persistentStorage == null,
'You can only supply a persistentStorage on the very first call to '
'FileDownloader()');
_singleton ??= FileDownloader._internal(
persistentStorage ?? LocalStorePersistentStorage(),
useNativeHttpClient);
persistentStorage ?? LocalStorePersistentStorage(),
);
return _singleton!;
}
FileDownloader._internal(
PersistentStorage persistentStorage, bool useNativeHttpClient) {
FileDownloader._internal(PersistentStorage persistentStorage) {
database = Database(persistentStorage);
_downloader = BaseDownloader.instance(
persistentStorage, database, useNativeHttpClient);
_downloader = BaseDownloader.instance(persistentStorage, database);
}
/// True when initialization is complete and downloader ready for use
@ -779,25 +773,12 @@ interface class FileDownloader {
/// the downloader. If not set, the default [http.Client] will be used.
/// The request is executed on an Isolate, to ensure minimal interference
/// with the main Isolate
Future<http.Response> request(Request request, bool useNativeHttpClient) {
if (useNativeHttpClient) {
return compute(_doRequest, (
request,
desktop_downloader_native
.DesktopDownloaderNativeHttpClient.requestTimeout,
desktop_downloader_native.DesktopDownloaderNativeHttpClient.proxy,
desktop_downloader_native
.DesktopDownloaderNativeHttpClient.bypassTLSCertificateValidation,
true
));
}
Future<http.Response> request(Request request) {
return compute(_doRequest, (
request,
DesktopDownloaderHttpClient.requestTimeout,
DesktopDownloaderHttpClient.proxy,
DesktopDownloaderHttpClient.bypassTLSCertificateValidation,
false
DownloaderHttpClient.requestTimeout,
DownloaderHttpClient.proxy,
DownloaderHttpClient.bypassTLSCertificateValidation
));
}
@ -910,26 +891,14 @@ interface class FileDownloader {
/// This function is run on an Isolate to ensure performance on the main
/// Isolate is not affected
Future<http.Response> _doRequest(
(Request, Duration?, Map<String, dynamic>, bool, bool) params) async {
final (
request,
requestTimeout,
proxy,
bypassTLSCertificateValidation,
useNativeHttpClient
) = params;
(Request, Duration?, Map<String, dynamic>, bool) params) async {
final (request, requestTimeout, proxy, bypassTLSCertificateValidation) =
params;
if (useNativeHttpClient) {
desktop_downloader_native.DesktopDownloaderNativeHttpClient.setHttpClient(
requestTimeout, proxy, bypassTLSCertificateValidation);
} else {
DesktopDownloaderHttpClient.setHttpClient(
requestTimeout, proxy, bypassTLSCertificateValidation);
}
DownloaderHttpClient.setHttpClient(
requestTimeout, proxy, bypassTLSCertificateValidation);
final client = useNativeHttpClient
? desktop_downloader_native.DesktopDownloaderNativeHttpClient.httpClient
: DesktopDownloaderHttpClient.httpClient;
final client = DownloaderHttpClient.httpClient;
var response = http.Response('', 499,
reasonPhrase: 'Not attempted'); // dummy to start with
while (request.retriesRemaining >= 0) {

View file

@ -20,7 +20,10 @@ Future<(List<Video>, bool, String?)> getVideoList(
final mangaDirectory = await storageProvider.getMangaMainDirectory(episode);
final isLocalArchive = episode.manga.value!.isLocalArchive! &&
episode.manga.value!.source != "torrent";
final mp4animePath = "${mangaDirectory!.path}${episode.name}.mp4";
final forbiddenCharacters =
RegExp(r'[\\/:*?"<>|\0]|(^CON$|^PRN$|^AUX$|^NUL$|^COM[1-9]$|^LPT[1-9]$)');
final mp4animePath =
"${mangaDirectory!.path}${episode.name!.replaceAll(forbiddenCharacters, ' ')}.mp4";
if (await File(mp4animePath).exists() || isLocalArchive) {
final path = isLocalArchive ? episode.archivePath : mp4animePath;

View file

@ -1,4 +1,3 @@
import 'package:cupertino_http/cupertino_http.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:mangayomi/eval/dart/model/m_bridge.dart';
import 'dart:async';
@ -8,33 +7,32 @@ import 'package:mangayomi/main.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'
as flutter_inappwebview;
import 'package:mangayomi/models/settings.dart';
import 'package:cronet_http/cronet_http.dart';
import 'package:http/io_client.dart';
import 'package:mangayomi/utils/log/log.dart';
import 'package:rhttp/rhttp.dart' as rhttp;
class MClient {
MClient();
static Client httpClient() {
if (isar.settings.getSync(227)?.useNativeHttpClient ?? false) {
return nativeHttpClient();
}
return IOClient(HttpClient());
}
static Client nativeHttpClient() {
if (Platform.isAndroid) {
final engine = CronetEngine.build(
enablePublicKeyPinningBypassForLocalTrustAnchors: true,
enableHttp2: true,
enableBrotli: true,
cacheMode: CacheMode.memory,
cacheMaxSize: 5 * 1024 * 1024);
return CronetClient.fromCronetEngine(engine, closeEngine: true);
}
if (Platform.isIOS || Platform.isMacOS) {
final config = URLSessionConfiguration.ephemeralSessionConfiguration()
..cache = URLCache.withCapacity(memoryCapacity: 5 * 1024 * 1024);
return CupertinoClient.fromSessionConfiguration(config);
static Client httpClient(
{Map<String, dynamic>? reqcopyWith, rhttp.ClientSettings? settings}) {
if (!(reqcopyWith?["useDartHttpClient"] ?? false)) {
try {
settings ??= rhttp.ClientSettings(
throwOnStatusCode: false,
proxySettings: reqcopyWith?["noProxy"] ?? false
? const rhttp.ProxySettings.noProxy()
: null,
timeout: reqcopyWith?["timeout"] != null
? Duration(seconds: reqcopyWith?["timeout"])
: null,
connectTimeout: reqcopyWith?["connectTimeout"] != null
? Duration(seconds: reqcopyWith?["connectTimeout"])
: null,
tlsSettings: rhttp.TlsSettings(
verifyCertificates:
reqcopyWith?["verifyCertificates"] ?? false));
return rhttp.RhttpCompatibleClient.createSync(settings: settings);
} catch (_) {}
}
return IOClient(HttpClient());
}
@ -42,7 +40,7 @@ class MClient {
static InterceptedClient init(
{MSource? source, Map<String, dynamic>? reqcopyWith}) {
return InterceptedClient.build(
client: httpClient(),
client: httpClient(reqcopyWith: reqcopyWith),
interceptors: [MCookieManager(reqcopyWith), LoggerInterceptor()]);
}

View file

@ -1,5 +1,5 @@
// This file is automatically generated, so please do not edit it.
// Generated by `flutter_rust_bridge`@ 2.2.0.
// Generated by `flutter_rust_bridge`@ 2.3.0.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import

View file

@ -1,5 +1,5 @@
// This file is automatically generated, so please do not edit it.
// Generated by `flutter_rust_bridge`@ 2.2.0.
// Generated by `flutter_rust_bridge`@ 2.3.0.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field
@ -31,6 +31,16 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
);
}
/// Initialize flutter_rust_bridge in mock mode.
/// No libraries for FFI are loaded.
static void initMock({
required RustLibApi api,
}) {
instance.initMockImpl(
api: api,
);
}
/// Dispose flutter_rust_bridge
///
/// The call to this function is optional, since flutter_rust_bridge (and everything else)
@ -53,7 +63,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
kDefaultExternalLibraryLoaderConfig;
@override
String get codegenVersion => '2.2.0';
String get codegenVersion => '2.3.0';
@override
int get rustContentHash => -287789500;

View file

@ -1,5 +1,5 @@
// This file is automatically generated, so please do not edit it.
// Generated by `flutter_rust_bridge`@ 2.2.0.
// Generated by `flutter_rust_bridge`@ 2.3.0.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field

View file

@ -1,5 +1,5 @@
// This file is automatically generated, so please do not edit it.
// Generated by `flutter_rust_bridge`@ 2.2.0.
// Generated by `flutter_rust_bridge`@ 2.3.0.
// ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field

View file

@ -6,7 +6,7 @@ part of 'headers.dart';
// RiverpodGenerator
// **************************************************************************
String _$headersHash() => r'c01fac2f30fa852565e9fca36f3a921f9bf3c112';
String _$headersHash() => r'9ec5e31ef5ee097be7de6bed89a65b0afa5a51da';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -14,8 +14,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
media_kit_native_event_loop
rhttp
rust_lib_mangayomi
)

View file

@ -5,23 +5,23 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3"
url: "https://pub.dev"
source: hosted
version: "72.0.0"
version: "68.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.2"
version: "0.1.0"
analyzer:
dependency: "direct overridden"
description:
name: analyzer
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808"
url: "https://pub.dev"
source: hosted
version: "6.7.0"
version: "6.5.0"
analyzer_plugin:
dependency: transitive
description:
@ -138,18 +138,18 @@ packages:
dependency: "direct dev"
description:
name: build_runner
sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04
sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7"
url: "https://pub.dev"
source: hosted
version: "2.4.12"
version: "2.4.11"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe
url: "https://pub.dev"
source: hosted
version: "7.3.2"
version: "7.3.1"
built_collection:
dependency: transitive
description:
@ -230,14 +230,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.1"
cronet_http:
dependency: "direct main"
description:
name: cronet_http
sha256: "3af9c4d57bf07ef4b307e77b22be4ad61bea19ee6ff65e62184863f3a09f1415"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
cross_file:
dependency: transitive
description:
@ -250,10 +242,10 @@ packages:
dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.5"
csslib:
dependency: transitive
description:
@ -262,14 +254,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
cupertino_http:
dependency: "direct main"
description:
name: cupertino_http
sha256: "7e75c45a27cc13a886ab0a1e4d8570078397057bd612de9d24fe5df0d9387717"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
cupertino_icons:
dependency: "direct main"
description:
@ -282,10 +266,10 @@ packages:
dependency: transitive
description:
name: custom_lint_core
sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6
sha256: "4ddbbdaa774265de44c97054dcec058a83d9081d071785ece601e348c18c267d"
url: "https://pub.dev"
source: hosted
version: "0.6.3"
version: "0.6.5"
dart_eval:
dependency: "direct main"
description:
@ -395,10 +379,10 @@ packages:
dependency: transitive
description:
name: extended_image_library
sha256: c9caee8fe9b6547bd41c960c4f2d1ef8e34321804de6a1777f1d614a24247ad6
sha256: "9a94ec9314aa206cfa35f16145c3cd6e2c924badcc670eaaca8a3a8063a68cd7"
url: "https://pub.dev"
source: hosted
version: "4.0.4"
version: "4.0.5"
fake_async:
dependency: transitive
description:
@ -411,10 +395,10 @@ packages:
dependency: "direct main"
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
ffigen:
dependency: "direct main"
description:
@ -542,10 +526,10 @@ packages:
dependency: "direct main"
description:
name: flutter_rust_bridge
sha256: "7beb9cb4690916a6c4fd151d91dba53555ea258dbc029dd5f1bfba2e7bd32b86"
sha256: b0271cc147d5afccf9774809e4eef52b7357babe1a1a31db649df6f02dd27580
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.3.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -617,10 +601,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: ddc16d34b0d74cb313986918c0f0885a7ba2fc24d8fb8419de75f0015144ccfe
sha256: "48d03a1e4887b00fe622695139246e3c778ac814eeb32421467b56d23fa64034"
url: "https://pub.dev"
source: hosted
version: "14.2.3"
version: "14.2.6"
google_fonts:
dependency: "direct main"
description:
@ -709,14 +693,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.2"
http_profile:
dependency: transitive
description:
name: http_profile
sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
image:
dependency: transitive
description:
@ -781,14 +757,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
jni:
dependency: transitive
description:
name: jni
sha256: f377c585ea9c08d48b427dc2e03780af2889d1bb094440da853c6883c1acba4b
url: "https://pub.dev"
source: hosted
version: "0.10.1"
js:
dependency: transitive
description:
@ -841,18 +809,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
url: "https://pub.dev"
source: hosted
version: "10.0.5"
version: "10.0.4"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.3"
leak_tracker_testing:
dependency: transitive
description:
@ -889,10 +857,10 @@ packages:
dependency: transitive
description:
name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
sha256: "12e8a9842b5a7390de7a781ec63d793527582398d16ea26c60fed58833c9ae79"
url: "https://pub.dev"
source: hosted
version: "0.1.2-main.4"
version: "0.1.0-main.0"
matcher:
dependency: transitive
description:
@ -905,10 +873,10 @@ packages:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.8.0"
maybe_just_nothing:
dependency: transitive
description:
@ -1036,10 +1004,10 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860"
sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918
url: "https://pub.dev"
source: hosted
version: "8.0.1"
version: "8.0.2"
package_info_plus_platform_interface:
dependency: transitive
description:
@ -1068,10 +1036,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb"
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
url: "https://pub.dev"
source: hosted
version: "2.2.9"
version: "2.2.10"
path_provider_foundation:
dependency: transitive
description:
@ -1116,10 +1084,10 @@ packages:
dependency: transitive
description:
name: permission_handler_android
sha256: eaf2a1ec4472775451e88ca6a7b86559ef2f1d1ed903942ed135e38ea0097dca
sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa"
url: "https://pub.dev"
source: hosted
version: "12.0.8"
version: "12.0.12"
permission_handler_apple:
dependency: transitive
description:
@ -1132,10 +1100,10 @@ packages:
dependency: transitive
description:
name: permission_handler_html
sha256: "6cac773d389e045a8d4f85418d07ad58ef9e42a56e063629ce14c4c26344de24"
sha256: d220eb8476b466d58b161e10b3001d93999010a26228a3fb89c4280db1249546
url: "https://pub.dev"
source: hosted
version: "0.1.2"
version: "0.1.3+1"
permission_handler_platform_interface:
dependency: transitive
description:
@ -1248,6 +1216,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.0"
rhttp:
dependency: "direct main"
description:
name: rhttp
sha256: "0390216e898cf2ebd353594715724ace0101e26f6fb84dac68293d4883432fb8"
url: "https://pub.dev"
source: hosted
version: "0.5.4"
riverpod:
dependency: transitive
description:
@ -1260,10 +1236,10 @@ packages:
dependency: transitive
description:
name: riverpod_analyzer_utils
sha256: ee72770090078e6841d51355292335f1bc254907c6694283389dcb8156d99a4d
sha256: ac28d7bc678471ec986b42d88e5a0893513382ff7542c7ac9634463b044ac72c
url: "https://pub.dev"
source: hosted
version: "0.5.3"
version: "0.5.4"
riverpod_annotation:
dependency: "direct main"
description:
@ -1276,10 +1252,10 @@ packages:
dependency: "direct dev"
description:
name: riverpod_generator
sha256: "1ad626afbd8b01d168870b13c0b036f8a5bdb57c14cd426dc5b4595466bd6e2f"
sha256: "63311e361ffc578d655dfc31b48dfa4ed3bc76fd06f9be845e9bf97c5c11a429"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.3"
rust_lib_mangayomi:
dependency: "direct main"
description:
@ -1496,10 +1472,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
version: "0.7.0"
time:
dependency: transitive
description:
@ -1560,10 +1536,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9"
sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79
url: "https://pub.dev"
source: hosted
version: "6.3.8"
version: "6.3.9"
url_launcher_ios:
dependency: transitive
description:
@ -1576,10 +1552,10 @@ packages:
dependency: transitive
description:
name: url_launcher_linux
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.2.0"
url_launcher_macos:
dependency: transitive
description:
@ -1632,26 +1608,26 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
url: "https://pub.dev"
source: hosted
version: "14.2.4"
version: "14.2.1"
volume_controller:
dependency: transitive
description:
name: volume_controller
sha256: "189bdc7a554f476b412e4c8b2f474562b09d74bc458c23667356bce3ca1d48c9"
sha256: c71d4c62631305df63b72da79089e078af2659649301807fa746088f365cb48e
url: "https://pub.dev"
source: hosted
version: "2.0.7"
version: "2.0.8"
wakelock_plus:
dependency: transitive
description:
name: wakelock_plus
sha256: "4fa83a128b4127619e385f686b4f080a5d2de46cff8e8c94eccac5fcf76550e5"
sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484
url: "https://pub.dev"
source: hosted
version: "1.2.7"
version: "1.2.8"
wakelock_plus_platform_interface:
dependency: transitive
description:
@ -1696,10 +1672,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9"
sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a"
url: "https://pub.dev"
source: hosted
version: "5.5.3"
version: "5.5.4"
window_manager:
dependency: "direct main"
description:
@ -1773,5 +1749,5 @@ packages:
source: hosted
version: "2.2.1"
sdks:
dart: ">=3.5.0 <4.0.0"
dart: ">=3.4.4 <4.0.0"
flutter: ">=3.22.0"

View file

@ -4,7 +4,7 @@ publish_to: "none"
version: 0.2.9+65
environment:
sdk: ">=3.5.0 <4.0.0"
sdk: ">=3.4.4 <4.0.0"
dependencies:
flutter:
@ -64,18 +64,17 @@ dependencies:
git:
url: https://github.com/kodjodevf/flutter_qjs.git
ref: main
cronet_http: ^1.3.1
cupertino_http: ^1.5.0
http: ^1.2.1
flutter_code_editor: ^0.3.1
flutter_highlight: ^0.7.0
highlight: ^0.7.0
json_view: ^0.4.2
super_sliver_list: ^0.4.1
flutter_rust_bridge: ^2.2.0
flutter_rust_bridge: ^2.3.0
rust_lib_mangayomi:
path: rust_builder
pseudom: ^1.0.1
rhttp: ^0.5.4
dependency_overrides:
http: ^1.2.1

9
rust/Cargo.lock generated
View file

@ -403,9 +403,9 @@ dependencies = [
[[package]]
name = "flutter_rust_bridge"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5db18c05aac3922abfe24282af9128947d2b27856f3d6a4f30888bffa976855a"
checksum = "f13c1e57b460f7adbd6cbf8b4cd0a1d14238ed64f5cc2a6c2ccb7a605ac01354"
dependencies = [
"allo-isolate",
"android_logger",
@ -420,6 +420,7 @@ dependencies = [
"futures",
"js-sys",
"lazy_static",
"log",
"oslog",
"threadpool",
"tokio",
@ -430,9 +431,9 @@ dependencies = [
[[package]]
name = "flutter_rust_bridge_macros"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83de8013ddf893c420794e8a722f941eeca7f4939b46434282d66b676a0997c"
checksum = "16c85e62d6d34f5c1590af004ccd3dc45b1c726dba6721b632b164c19894fab4"
dependencies = [
"hex",
"md-5",

View file

@ -7,5 +7,5 @@ edition = "2021"
crate-type = ["cdylib", "staticlib"]
[dependencies]
flutter_rust_bridge = "=2.2.0"
flutter_rust_bridge = "=2.3.0"
image = "0.25.0"

View file

@ -1,5 +1,5 @@
// This file is automatically generated, so please do not edit it.
// Generated by `flutter_rust_bridge`@ 2.2.0.
// Generated by `flutter_rust_bridge`@ 2.3.0.
#![allow(
non_camel_case_types,
@ -36,7 +36,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_opaque = RustOpaqueMoi,
default_rust_auto_opaque = RustAutoOpaqueMoi,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.2.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.3.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -287789500;
// Section: executor
@ -184,7 +184,7 @@ impl SseEncode for bool {
#[cfg(not(target_family = "wasm"))]
mod io {
// This file is automatically generated, so please do not edit it.
// Generated by `flutter_rust_bridge`@ 2.2.0.
// Generated by `flutter_rust_bridge`@ 2.3.0.
// Section: imports
@ -206,7 +206,7 @@ pub use io::*;
#[cfg(target_family = "wasm")]
mod web {
// This file is automatically generated, so please do not edit it.
// Generated by `flutter_rust_bridge`@ 2.2.0.
// Generated by `flutter_rust_bridge`@ 2.3.0.
// Section: imports

View file

@ -18,8 +18,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
jni
media_kit_native_event_loop
rhttp
rust_lib_mangayomi
)