enhanced repo manager

- added custom DNS setting
This commit is contained in:
Schnitzel5 2025-08-23 03:33:48 +02:00
parent 7eba7bdcf2
commit 74c5eab379
31 changed files with 1172 additions and 558 deletions

View file

@ -443,6 +443,7 @@
"manga_extensions_repo": "Manga extensions repo",
"anime_extensions_repo": "Anime extensions repo",
"novel_extensions_repo": "Novel extensions repo",
"custom_dns": "Custom DNS (leave blank to use system DNS)",
"android_proxy_server": "Android Proxy Server (ApkBridge)",
"undefined": "undefined",
"empty_extensions_repo": "You don't have any repository urls here. Click on the plus button to add one!",

View file

@ -2733,6 +2733,12 @@ abstract class AppLocalizations {
/// **'Novel extensions repo'**
String get novel_extensions_repo;
/// No description provided for @custom_dns.
///
/// In en, this message translates to:
/// **'Custom DNS (leave blank to use system DNS)'**
String get custom_dns;
/// No description provided for @android_proxy_server.
///
/// In en, this message translates to:

View file

@ -1402,6 +1402,9 @@ class AppLocalizationsAr extends AppLocalizations {
@override
String get novel_extensions_repo => 'مستودع إضافات الروايات';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1404,6 +1404,9 @@ class AppLocalizationsAs extends AppLocalizations {
@override
String get novel_extensions_repo => 'Novel extensions repo';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1413,6 +1413,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get novel_extensions_repo => 'Roman-Erweiterungs-Repository';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1403,6 +1403,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get novel_extensions_repo => 'Novel extensions repo';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1417,6 +1417,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get novel_extensions_repo => 'Repositorio de extensiones de novelas';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1420,6 +1420,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get novel_extensions_repo => 'Dépôt d\'extensions de romans';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1405,6 +1405,9 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get novel_extensions_repo => 'Novel extensions repo';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1409,6 +1409,9 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get novel_extensions_repo => 'Repositori ekstensi novel';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1417,6 +1417,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get novel_extensions_repo => 'Repository delle estensioni romanzi';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1414,6 +1414,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get novel_extensions_repo => 'Repositório de extensões de romances';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1416,6 +1416,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get novel_extensions_repo => 'Репозиторий расширений новелл';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1403,6 +1403,9 @@ class AppLocalizationsTh extends AppLocalizations {
@override
String get novel_extensions_repo => 'ที่เก็บส่วนขยายโนเวล';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1409,6 +1409,9 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get novel_extensions_repo => 'Roman uzantıları deposu';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -1377,6 +1377,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get novel_extensions_repo => '小说扩展库';
@override
String get custom_dns => 'Custom DNS (leave blank to use system DNS)';
@override
String get android_proxy_server => 'Android Proxy Server (ApkBridge)';

View file

@ -19,6 +19,7 @@ import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/models/track_search.dart';
import 'package:mangayomi/modules/more/data_and_storage/providers/storage_usage.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/modules/more/settings/general/providers/general_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/router/router.dart';
@ -38,6 +39,7 @@ import 'package:path/path.dart' as p;
late Isar isar;
DiscordRPC? discordRpc;
WebViewEnvironment? webViewEnvironment;
String? customDns;
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
if (Platform.isLinux && runWebViewTitleBarWidget(args)) return;
@ -97,6 +99,7 @@ class _MyAppState extends ConsumerState<MyApp> {
unawaited(ref.read(scanLocalLibraryProvider.future));
WidgetsBinding.instance.addPostFrameCallback((_) {
customDns = ref.read(customDnsStateProvider);
if (ref.read(clearChapterCacheOnAppLaunchStateProvider)) {
ref
.read(totalChapterCacheSizeStateProvider.notifier)

View file

@ -169,6 +169,8 @@ class Settings {
int? aniSkipTimeoutLength;
String? customDns;
String? btServerAddress;
int? btServerPort;
@ -261,9 +263,8 @@ class Settings {
bool? rpcShowCoverImage;
bool? downloadedOnlyMode;
late AlgorithmWeights? algorithmWeights;
late AlgorithmWeights? algorithmWeights;
Settings({
this.id = 227,
@ -339,6 +340,7 @@ class Settings {
this.enableAniSkip,
this.enableAutoSkip,
this.aniSkipTimeoutLength,
this.customDns = "",
this.btServerAddress = "127.0.0.1",
this.btServerPort,
this.fullScreenReader = true,
@ -526,6 +528,7 @@ class Settings {
enableAniSkip = json['enableAniSkip'];
enableAutoSkip = json['enableAutoSkip'];
aniSkipTimeoutLength = json['aniSkipTimeoutLength'];
customDns = json['customDns'];
btServerAddress = json['btServerAddress'];
btServerPort = json['btServerPort'];
customColorFilter = json['customColorFilter'] != null
@ -703,6 +706,7 @@ class Settings {
'enableAniSkip': enableAniSkip,
'enableAutoSkip': enableAutoSkip,
'aniSkipTimeoutLength': aniSkipTimeoutLength,
'customDns': customDns,
'btServerAddress': btServerAddress,
'btServerPort': btServerPort,
'fullScreenReader': fullScreenReader,
@ -937,20 +941,34 @@ class Repo {
String? name;
String? website;
String? jsonUrl;
bool? hidden;
Repo({this.name, this.website, this.jsonUrl});
Repo({this.name, this.website, this.jsonUrl, this.hidden});
Repo.fromJson(Map<String, dynamic> json) {
name = json['name'];
website = json['website'];
name = json['meta']?['name'] ?? json['name'];
website = json['meta']?['website'] ?? json['website'];
jsonUrl = json['jsonUrl'];
hidden = json['hidden'];
}
Map<String, dynamic> toJson() => {
'name': name,
'website': website,
'jsonUrl': jsonUrl,
'hidden': hidden,
};
@override
bool operator ==(Object other) {
return other is Repo &&
name == other.name &&
website == other.website &&
jsonUrl == other.jsonUrl;
}
@override
int get hashCode => Object.hash(name, website, jsonUrl);
}
@embedded

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_qjs/quickjs/ffi.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/modules/widgets/custom_sliver_grouped_list_view.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/source.dart';
@ -66,6 +68,9 @@ class _ExtensionScreenState extends ConsumerState<ExtensionScreen> {
final streamExtensions = ref.watch(
getExtensionsStreamProvider(widget.itemType),
);
final repositories = ref.watch(
extensionsRepoStateProvider(widget.itemType),
);
final l10n = l10nLocalizations(context)!;
@ -92,6 +97,12 @@ class _ExtensionScreenState extends ConsumerState<ExtensionScreen> {
final notInstalledEntries = <Source>[];
for (var element in filteredData) {
if (repositories
.firstWhereOrNull((e) => e == element.repo)
?.hidden ??
false) {
continue;
}
final isLatestVersion = element.version == element.versionLast;
if (compareVersions(

View file

@ -1,6 +1,7 @@
import 'package:isar/isar.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/source.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -12,6 +13,7 @@ Stream<List<Source>> getExtensionsStream(Ref ref, ItemType itemType) async* {
.filter()
.idIsNotNull()
.and()
.repo((q) => q.hiddenIsNull().or().hiddenEqualTo(false))
.isActiveEqualTo(true)
.itemTypeEqualTo(itemType)
.watch(fireImmediately: true);

View file

@ -7,7 +7,7 @@ part of 'extensions_provider.dart';
// **************************************************************************
String _$getExtensionsStreamHash() =>
r'3c5d6625c40c222f25fc8141df078dd46bcc762f';
r'af34092ebf31c784010110af746e3ee2731297bd';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -6,7 +6,7 @@ part of 'update_manga_detail_providers.dart';
// RiverpodGenerator
// **************************************************************************
String _$updateMangaDetailHash() => r'30185777f73eaf9eac4cce554a7ecbc9e1c0b613';
String _$updateMangaDetailHash() => r'44a7da7d5ef698b030c98fd5439cf9e7525350ef';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -76,6 +76,16 @@ class ExtensionsRepoState extends _$ExtensionsRepoState {
[];
}
void setVisibility(Repo repo, bool hidden) {
final value = state.map((e) {
if (e == repo) {
e.hidden = hidden;
}
return e;
}).toList();
set(value);
}
void set(List<Repo> value) {
final settings = isar.settings.getSync(227)!;
state = value;

View file

@ -192,7 +192,7 @@ final onlyIncludePinnedSourceStateProvider =
typedef _$OnlyIncludePinnedSourceState = AutoDisposeNotifier<bool>;
String _$extensionsRepoStateHash() =>
r'5c23b8b7ecf83b253b76a2663a71c0c752e53a40';
r'86edc9a3f78d72acda4b20a058031c345ee406eb';
abstract class _$ExtensionsRepoState
extends BuildlessAutoDisposeNotifier<List<Repo>> {

View file

@ -7,6 +7,8 @@ import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/services/fetch_item_sources.dart';
import 'package:mangayomi/utils/cached_network.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
import 'package:url_launcher/url_launcher.dart';
@ -19,8 +21,13 @@ class SourceRepositories extends ConsumerStatefulWidget {
}
class _SourceRepositoriesState extends ConsumerState<SourceRepositories> {
final urlRegex = RegExp(
r'^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$',
);
List<Repo> _entries = [];
String urlInput = "";
bool isRefreshing = false;
Future<void> _launchInBrowser(Uri url) async {
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
throw 'Could not launch $url';
@ -41,6 +48,43 @@ class _SourceRepositoriesState extends ConsumerState<SourceRepositories> {
ItemType.anime => Text(l10n.manage_anime_repo_urls),
_ => Text(l10n.manage_novel_repo_urls),
},
actions: [
isRefreshing
? const Padding(
padding: EdgeInsets.all(20.0),
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 3),
),
)
: Padding(
padding: EdgeInsets.all(8.0),
child: IconButton(
splashRadius: 20,
onPressed: () async {
setState(() {
isRefreshing = true;
});
final result = await ref.refresh(
fetchItemSourcesListProvider(
id: null,
reFresh: true,
itemType: widget.itemType,
).future,
);
setState(() {
isRefreshing = false;
});
return result;
},
icon: Icon(
Icons.refresh,
color: Theme.of(context).hintColor,
),
),
),
],
),
body: data.when(
data: (data) {
@ -62,6 +106,12 @@ class _SourceRepositoriesState extends ConsumerState<SourceRepositories> {
itemCount: _entries.length,
itemBuilder: (context, index) {
final repo = _entries[index];
final isHidden = repo.hidden ?? false;
final repoAvatar = urlRegex
.firstMatch(repo.jsonUrl ?? "")
?.group(4)
?.split("/")
.elementAtOrNull(1);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Card(
@ -69,25 +119,53 @@ class _SourceRepositoriesState extends ConsumerState<SourceRepositories> {
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 15,
Opacity(
opacity: isHidden ? 0.3 : 1,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (repoAvatar != null)
Padding(
padding: EdgeInsets.all(8.0),
child: cachedNetworkImage(
imageUrl:
"https://github.com/$repoAvatar.png?size=64",
fit: BoxFit.contain,
width: 64,
height: 64,
errorWidget: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 15,
),
child: Icon(Icons.label_outline_rounded),
),
useCustomNetworkImage: false,
),
),
if (repoAvatar == null)
const Padding(
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 15,
),
child: Icon(Icons.label_outline_rounded),
),
const SizedBox(width: 10),
Expanded(
child: Text(
repo.name ??
repo.jsonUrl ??
"Invalid source - remove it",
style: TextStyle(
decoration: isHidden
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
),
child: const Icon(Icons.label_outline_rounded),
),
const SizedBox(width: 10),
Expanded(
child: Text(
repo.name ??
repo.jsonUrl ??
"Invalid source - remove it",
),
),
],
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
@ -112,72 +190,40 @@ class _SourceRepositoriesState extends ConsumerState<SourceRepositories> {
),
SizedBox(width: 10),
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Text(
l10n.remove_extensions_repo,
),
content: Text(
l10n.remove_extensions_repo,
),
actions: [
Row(
mainAxisAlignment:
MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
TextButton(
onPressed: () {
final mangaRepos = ref
.read(
extensionsRepoStateProvider(
widget.itemType,
),
)
.toList();
mangaRepos.removeWhere(
(url) =>
url ==
_entries[index],
);
ref
.read(
extensionsRepoStateProvider(
widget.itemType,
).notifier,
)
.set(mangaRepos);
ref.watch(
extensionsRepoStateProvider(
widget.itemType,
),
);
if (context.mounted) {
Navigator.pop(context);
}
},
child: Text(l10n.ok),
),
],
onPressed: () => ref
.read(
extensionsRepoStateProvider(
widget.itemType,
).notifier,
)
.setVisibility(repo, !isHidden),
icon: Stack(
children: [
const Icon(Icons.remove_red_eye_outlined),
if (!isHidden)
Positioned(
right: 8,
child: Transform.scale(
scaleX: 2.5,
child: const Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(
'\\',
style: TextStyle(fontSize: 17),
),
],
);
},
);
},
);
},
),
),
),
],
),
),
SizedBox(width: 10),
IconButton(
onPressed: () =>
_showRemoveRepoDialog(context, index),
icon: const Icon(Icons.delete_outlined),
),
],
@ -207,144 +253,7 @@ class _SourceRepositoriesState extends ConsumerState<SourceRepositories> {
},
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
bool isLoading = false;
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) {
return SizedBox(
child: StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Text(l10n.add_extensions_repo),
content: TextFormField(
controller: controller,
autofocus: true,
keyboardType: TextInputType.url,
onChanged: (value) => setState(() {}),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.url_cannot_be_empty;
}
if (!value.endsWith('.json')) {
return l10n.url_must_end_with_dot_json;
}
try {
final uri = Uri.parse(value);
if (!uri.isAbsolute) {
return l10n.invalid_url_format;
}
return null;
} catch (e) {
return l10n.invalid_url_format;
}
},
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: InputDecoration(
hintText: l10n.url_must_end_with_dot_json,
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(),
),
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
StatefulBuilder(
builder: (context, setState) {
return TextButton(
onPressed:
controller.text.isEmpty ||
!controller.text.endsWith(".json")
? null
: () async {
setState(() => isLoading = true);
try {
final mangaRepos = ref
.read(
extensionsRepoStateProvider(
widget.itemType,
),
)
.toList();
final repo = await ref.read(
getRepoInfosProvider(
jsonUrl: controller.text,
).future,
);
if (repo == null) {
botToast(l10n.unsupported_repo);
return;
}
mangaRepos.add(repo);
ref
.read(
extensionsRepoStateProvider(
widget.itemType,
).notifier,
)
.set(mangaRepos);
} catch (e, s) {
setState(() => isLoading = false);
botToast('$e\n$s');
}
if (context.mounted) {
Navigator.pop(context);
}
},
child: isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(),
)
: Text(
l10n.add,
style: TextStyle(
color:
controller.text.isEmpty ||
!controller.text.endsWith(
".json",
)
? Theme.of(context).primaryColor
.withValues(alpha: 0.2)
: null,
),
),
);
},
),
],
),
],
);
},
),
);
},
);
},
onPressed: () => _showAddRepoDialog(context),
label: Row(
children: [
const Icon(Icons.add),
@ -355,4 +264,193 @@ class _SourceRepositoriesState extends ConsumerState<SourceRepositories> {
),
);
}
_showRemoveRepoDialog(BuildContext context, int index) {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
final l10n = context.l10n;
return AlertDialog(
title: Text(l10n.remove_extensions_repo),
content: Text(l10n.remove_extensions_repo),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
TextButton(
onPressed: () {
final mangaRepos = ref
.read(extensionsRepoStateProvider(widget.itemType))
.toList();
mangaRepos.removeWhere((url) => url == _entries[index]);
ref
.read(
extensionsRepoStateProvider(
widget.itemType,
).notifier,
)
.set(mangaRepos);
ref.watch(extensionsRepoStateProvider(widget.itemType));
if (context.mounted) {
Navigator.pop(context);
}
},
child: Text(l10n.ok),
),
],
),
],
);
},
);
},
);
}
_showAddRepoDialog(BuildContext context) {
bool isLoading = false;
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) {
return SizedBox(
child: StatefulBuilder(
builder: (context, setState) {
final l10n = context.l10n;
return AlertDialog(
title: Text(l10n.add_extensions_repo),
content: TextFormField(
controller: controller,
autofocus: true,
keyboardType: TextInputType.url,
onChanged: (value) => setState(() {}),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.url_cannot_be_empty;
}
if (!value.endsWith('.json')) {
return l10n.url_must_end_with_dot_json;
}
try {
final uri = Uri.parse(value);
if (!uri.isAbsolute) {
return l10n.invalid_url_format;
}
return null;
} catch (e) {
return l10n.invalid_url_format;
}
},
autovalidateMode: AutovalidateMode.onUserInteraction,
decoration: InputDecoration(
hintText: l10n.url_must_end_with_dot_json,
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(),
),
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
StatefulBuilder(
builder: (context, setState) {
return TextButton(
onPressed:
controller.text.isEmpty ||
!controller.text.endsWith(".json")
? null
: () async {
setState(() => isLoading = true);
try {
final mangaRepos = ref
.read(
extensionsRepoStateProvider(
widget.itemType,
),
)
.toList();
final repo = await ref.read(
getRepoInfosProvider(
jsonUrl: controller.text,
).future,
);
if (repo == null) {
botToast(l10n.unsupported_repo);
return;
}
mangaRepos.add(repo);
ref
.read(
extensionsRepoStateProvider(
widget.itemType,
).notifier,
)
.set(mangaRepos);
} catch (e, s) {
setState(() => isLoading = false);
botToast('$e\n$s');
}
if (context.mounted) {
Navigator.pop(context);
}
},
child: isLoading
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(),
)
: Text(
l10n.add,
style: TextStyle(
color:
controller.text.isEmpty ||
!controller.text.endsWith(".json")
? Theme.of(context).primaryColor
.withValues(alpha: 0.2)
: null,
),
),
);
},
),
],
),
],
);
},
),
);
},
);
}
}

View file

@ -34,6 +34,7 @@ class _GeneralStateScreen extends ConsumerState<GeneralScreen> {
@override
Widget build(BuildContext context) {
final l10n = l10nLocalizations(context);
final customDns = ref.watch(customDnsStateProvider);
final enableDiscordRpc = ref.watch(enableDiscordRpcStateProvider);
final hideDiscordRpcInIncognito = ref.watch(
hideDiscordRpcInIncognitoStateProvider,
@ -48,6 +49,14 @@ class _GeneralStateScreen extends ConsumerState<GeneralScreen> {
body: SingleChildScrollView(
child: Column(
children: [
ListTile(
onTap: () => _showCustomDnsDialog(context, ref, customDns),
title: Text(l10n.custom_dns),
subtitle: Text(
customDns,
style: TextStyle(fontSize: 11, color: context.secondaryColor),
),
),
Container(
margin: const EdgeInsets.all(20.0),
padding: const EdgeInsets.all(10.0),
@ -290,4 +299,77 @@ class _GeneralStateScreen extends ConsumerState<GeneralScreen> {
),
);
}
void _showCustomDnsDialog(
BuildContext context,
WidgetRef ref,
String customDns,
) {
final dnsController = TextEditingController(text: customDns);
String dns = customDns;
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Text(
context.l10n.custom_dns,
style: const TextStyle(fontSize: 30),
),
content: SizedBox(
width: context.width(0.8),
height: context.height(0.3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: dnsController,
autofocus: true,
onChanged: (value) => setState(() {
dns = value;
}),
decoration: InputDecoration(
hintText: "8.8.8.8",
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide(),
),
),
),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: SizedBox(
width: context.width(1),
child: ElevatedButton(
onPressed: () {
ref.read(customDnsStateProvider.notifier).set(dns);
Navigator.pop(context);
},
child: Text(context.l10n.dialog_confirm),
),
),
),
],
),
),
);
},
),
);
}
}

View file

@ -3,6 +3,26 @@ import 'package:mangayomi/models/settings.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'general_state_provider.g.dart';
@riverpod
class CustomDnsState extends _$CustomDnsState {
@override
String build() {
return isar.settings.getSync(227)!.customDns ?? "";
}
void set(String value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(
settings!
..customDns = value
..updatedAt = DateTime.now().millisecondsSinceEpoch,
),
);
}
}
@riverpod
class EnableDiscordRpcState extends _$EnableDiscordRpcState {
@override

View file

@ -6,6 +6,22 @@ part of 'general_state_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$customDnsStateHash() => r'6061c64d742b3f873e54c1b9ef724b7c0b6350a2';
/// See also [CustomDnsState].
@ProviderFor(CustomDnsState)
final customDnsStateProvider =
AutoDisposeNotifierProvider<CustomDnsState, String>.internal(
CustomDnsState.new,
name: r'customDnsStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$customDnsStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$CustomDnsState = AutoDisposeNotifier<String>;
String _$enableDiscordRpcStateHash() =>
r'ab8ce3b29f5d94aedbc88dcb87c7c834648270f5';

View file

@ -6,7 +6,7 @@ part of 'get_video_list.dart';
// RiverpodGenerator
// **************************************************************************
String _$getVideoListHash() => r'd39e325f21e68830c0692101e6efd95b2e04bcef';
String _$getVideoListHash() => r'a7f0b9549bc2c3b4ddb68850f13da57081d8d002';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -54,8 +54,20 @@ class MClient {
rhttp.ClientSettings? settings,
bool showCloudFlareError = true,
}) {
final clientSettings = customDns == null
? settings
: settings?.copyWith(
dnsSettings: DnsSettings.dynamic(
resolver: (host) async => [customDns!],
),
) ??
ClientSettings(
dnsSettings: DnsSettings.dynamic(
resolver: (host) async => [customDns!],
),
);
return InterceptedClient.build(
client: httpClient(settings: settings, reqcopyWith: reqcopyWith),
client: httpClient(settings: clientSettings, reqcopyWith: reqcopyWith),
retryPolicy: ResolveCloudFlareChallenge(showCloudFlareError),
interceptors: [
MCookieManager(reqcopyWith),