import 'dart:convert'; import 'dart:io' as io; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/network/app_dio.dart'; import 'package:venera/network/cookie_jar.dart'; import 'package:venera/pages/webview.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; class ComicSourcePage extends StatelessWidget { const ComicSourcePage({super.key}); static Future update( ComicSource source, [ bool showLoading = true, ]) async { if (!source.url.isURL) { if (showLoading) { App.rootContext.showMessage(message: "Invalid url config"); return; } else { throw Exception("Invalid url config"); } } ComicSourceManager().remove(source.key); bool cancel = false; LoadingDialogController? controller; if (showLoading) { controller = showLoadingDialog( App.rootContext, onCancel: () => cancel = true, barrierDismissible: false, ); } try { var res = await AppDio().get( source.url, options: Options( responseType: ResponseType.plain, headers: {"cache-time": "no"}, ), ); if (cancel) return; controller?.close(); await ComicSourceParser().parse(res.data!, source.filePath); await io.File(source.filePath).writeAsString(res.data!); if (ComicSourceManager().availableUpdates.containsKey(source.key)) { ComicSourceManager().availableUpdates.remove(source.key); } } catch (e) { if (cancel) return; if (showLoading) { App.rootContext.showMessage(message: e.toString()); } else { rethrow; } } await ComicSourceManager().reload(); if (showLoading) { App.forceRebuild(); } } static Future checkComicSourceUpdate() async { if (ComicSource.all().isEmpty) { return 0; } var dio = AppDio(); var res = await dio.get(appdata.settings['comicSourceListUrl']); if (res.statusCode != 200) { return -1; } var list = jsonDecode(res.data!) as List; var versions = {}; for (var source in list) { versions[source['key']] = source['version']; } var shouldUpdate = []; for (var source in ComicSource.all()) { if (versions.containsKey(source.key) && compareSemVer(versions[source.key]!, source.version)) { shouldUpdate.add(source.key); } } if (shouldUpdate.isNotEmpty) { var updates = {}; for (var key in shouldUpdate) { updates[key] = versions[key]!; } ComicSourceManager().updateAvailableUpdates(updates); } return shouldUpdate.length; } @override Widget build(BuildContext context) { return Scaffold(body: const _Body()); } } class _Body extends StatefulWidget { const _Body(); @override State<_Body> createState() => _BodyState(); } class _BodyState extends State<_Body> { var url = ""; void updateUI() { setState(() {}); } @override void initState() { super.initState(); ComicSourceManager().addListener(updateUI); } @override void dispose() { super.dispose(); ComicSourceManager().removeListener(updateUI); } @override Widget build(BuildContext context) { return SmoothCustomScrollView( slivers: [ SliverAppbar(title: Text('Comic Source'.tl), style: AppbarStyle.shadow), buildCard(context), for (var source in ComicSource.all()) _SliverComicSource( key: ValueKey(source.key), source: source, edit: edit, update: update, delete: delete, ), SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)), ], ); } void delete(ComicSource source) { showConfirmDialog( context: App.rootContext, title: "Delete".tl, content: "Delete comic source '@n' ?".tlParams({"n": source.name}), btnColor: context.colorScheme.error, onConfirm: () { var file = File(source.filePath); file.delete(); ComicSourceManager().remove(source.key); _validatePages(); App.forceRebuild(); }, ); } void edit(ComicSource source) async { if (App.isDesktop) { try { await Process.run("code", [source.filePath], runInShell: true); await showDialog( context: App.rootContext, builder: (context) => AlertDialog( title: const Text("Reload Configs"), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text("cancel"), ), TextButton( onPressed: () async { await ComicSourceManager().reload(); App.forceRebuild(); }, child: const Text("continue"), ), ], ), ); return; } catch (e) { // } } context.to( () => _EditFilePage(source.filePath, () async { await ComicSourceManager().reload(); setState(() {}); }), ); } void update(ComicSource source, [bool showLoading = true]) { ComicSourcePage.update(source, showLoading); } Widget buildCard(BuildContext context) { return SliverToBoxAdapter( child: SizedBox( width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ ListTile( title: Text("Add comic source".tl), leading: const Icon(Icons.dashboard_customize), ), TextField( decoration: InputDecoration( hintText: "URL", border: const UnderlineInputBorder(), contentPadding: const EdgeInsets.symmetric(horizontal: 12), suffix: IconButton( onPressed: () => handleAddSource(url), icon: const Icon(Icons.check), ), ), onChanged: (value) { url = value; }, onSubmitted: handleAddSource, ).paddingHorizontal(16).paddingBottom(8), Wrap( spacing: 8, runSpacing: 8, children: [ FilledButton.tonalIcon( icon: Icon(Icons.article_outlined), label: Text("Comic Source list".tl), onPressed: () { showPopUpWidget( App.rootContext, _ComicSourceList(handleAddSource), ); }, ), FilledButton.tonalIcon( icon: Icon(Icons.file_open_outlined), label: Text("Use a config file".tl), onPressed: _selectFile, ), FilledButton.tonalIcon( icon: Icon(Icons.help_outline), label: Text("Help".tl), onPressed: help, ), _CheckUpdatesButton(), ], ).paddingHorizontal(12).paddingVertical(8), const SizedBox(height: 8), ], ), ), ); } void _selectFile() async { final file = await selectFile(ext: ["js"]); if (file == null) return; try { var fileName = file.name; var bytes = await file.readAsBytes(); var content = utf8.decode(bytes); await addSource(content, fileName); } catch (e, s) { App.rootContext.showMessage(message: e.toString()); Log.error("Add comic source", "$e\n$s"); } } void help() { launchUrlString( "https://github.com/venera-app/venera/blob/master/doc/comic_source.md", ); } Future handleAddSource(String url) async { if (url.isEmpty) { return; } var splits = url.split("/"); splits.removeWhere((element) => element == ""); var fileName = splits.last; bool cancel = false; var controller = showLoadingDialog( App.rootContext, onCancel: () => cancel = true, barrierDismissible: false, ); try { var res = await AppDio().get( url, options: Options( responseType: ResponseType.plain, headers: {"cache-time": "no"}, ), ); if (cancel) return; controller.close(); await addSource(res.data!, fileName); } catch (e, s) { if (cancel) return; context.showMessage(message: e.toString()); Log.error("Add comic source", "$e\n$s"); } } Future addSource(String js, String fileName) async { var comicSource = await ComicSourceParser().createAndParse(js, fileName); ComicSourceManager().add(comicSource); _addAllPagesWithComicSource(comicSource); appdata.saveData(); App.forceRebuild(); } } class _ComicSourceList extends StatefulWidget { const _ComicSourceList(this.onAdd); final Future Function(String) onAdd; @override State<_ComicSourceList> createState() => _ComicSourceListState(); } class _ComicSourceListState extends State<_ComicSourceList> { List? json; bool changed = false; var controller = TextEditingController(); void load() async { if (json != null) { setState(() { json = null; }); } if (controller.text.isEmpty) { setState(() { json = []; }); return; } var dio = AppDio(); try { var res = await dio.get(controller.text); if (res.statusCode != 200) { throw "error"; } if (mounted) { setState(() { json = jsonDecode(res.data!); }); } } catch (e) { context.showMessage(message: "Network error".tl); if (mounted) { setState(() { json = []; }); } } } @override void initState() { super.initState(); controller.text = appdata.settings['comicSourceListUrl']; load(); } @override void dispose() { super.dispose(); if (changed) { appdata.settings['comicSourceListUrl'] = controller.text; appdata.saveData(); } } @override Widget build(BuildContext context) { return PopUpWidgetScaffold(title: "Comic Source".tl, body: buildBody()); } Widget buildBody() { var currentKey = ComicSource.all().map((e) => e.key).toList(); return ListView.builder( itemCount: (json?.length ?? 1) + 1, itemBuilder: (context, index) { if (index == 0) { return Container( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), decoration: BoxDecoration( border: Border.all( color: Theme.of(context).colorScheme.outlineVariant, width: 0.6, ), borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( leading: Icon(Icons.source_outlined), title: Text("Repo URL".tl), ), TextField( controller: controller, decoration: InputDecoration( hintText: "URL", border: const UnderlineInputBorder(), contentPadding: const EdgeInsets.symmetric(horizontal: 12), ), onChanged: (value) { changed = true; }, ).paddingHorizontal(16).paddingBottom(8), Text( "The URL should point to a 'index.json' file".tl, ).paddingLeft(16), Text( "Do not report any issues related to sources to App repo.".tl, ).paddingLeft(16), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () { launchUrlString( "https://github.com/venera-app/venera/blob/master/doc/comic_source.md", ); }, child: Text("Help".tl), ), FilledButton.tonal( onPressed: load, child: Text("Refresh".tl), ), const SizedBox(width: 16), ], ), const SizedBox(height: 16), ], ), ); } if (index == 1 && json == null) { return Center( child: CircularProgressIndicator( strokeWidth: 2, ).fixWidth(24).fixHeight(24), ); } index--; var key = json![index]["key"]; var action = currentKey.contains(key) ? const Icon(Icons.check, size: 20).paddingRight(8) : Button.filled( child: Text("Add".tl), onPressed: () async { var fileName = json![index]["fileName"]; var url = json![index]["url"]; if (url == null || !(url.toString()).isURL) { var listUrl = appdata.settings['comicSourceListUrl'] as String; if (listUrl .replaceFirst("https://", "") .replaceFirst("http://", "") .contains("/")) { url = listUrl.substring(0, listUrl.lastIndexOf("/") + 1) + fileName; } else { url = '$listUrl/$fileName'; } } await widget.onAdd(url); setState(() {}); }, ).fixHeight(32); var description = json![index]["version"]; if (json![index]["description"] != null) { description = "$description\n${json![index]["description"]}"; } return ListTile( title: Text(json![index]["name"]), subtitle: Text(description), trailing: action, ); }, ); } } void _validatePages() { List explorePages = appdata.settings['explore_pages']; List categoryPages = appdata.settings['categories']; List networkFavorites = appdata.settings['favorites']; var totalExplorePages = ComicSource.all() .map((e) => e.explorePages.map((e) => e.title)) .expand((element) => element) .toList(); var totalCategoryPages = ComicSource.all() .map((e) => e.categoryData?.key) .where((element) => element != null) .map((e) => e!) .toList(); var totalNetworkFavorites = ComicSource.all() .map((e) => e.favoriteData?.key) .where((element) => element != null) .map((e) => e!) .toList(); for (var page in List.from(explorePages)) { if (!totalExplorePages.contains(page)) { explorePages.remove(page); } } for (var page in List.from(categoryPages)) { if (!totalCategoryPages.contains(page)) { categoryPages.remove(page); } } for (var page in List.from(networkFavorites)) { if (!totalNetworkFavorites.contains(page)) { networkFavorites.remove(page); } } appdata.settings['explore_pages'] = explorePages.toSet().toList(); appdata.settings['categories'] = categoryPages.toSet().toList(); appdata.settings['favorites'] = networkFavorites.toSet().toList(); appdata.saveData(); } void _addAllPagesWithComicSource(ComicSource source) { var explorePages = appdata.settings['explore_pages']; var categoryPages = appdata.settings['categories']; var networkFavorites = appdata.settings['favorites']; var searchPages = appdata.settings['searchSources']; if (source.explorePages.isNotEmpty) { for (var page in source.explorePages) { if (!explorePages.contains(page.title)) { explorePages.add(page.title); } } } if (source.categoryData != null && !categoryPages.contains(source.categoryData!.key)) { categoryPages.add(source.categoryData!.key); } if (source.favoriteData != null && !networkFavorites.contains(source.favoriteData!.key)) { networkFavorites.add(source.favoriteData!.key); } if (source.searchPageData != null && !searchPages.contains(source.key)) { searchPages.add(source.key); } appdata.settings['explore_pages'] = explorePages.toSet().toList(); appdata.settings['categories'] = categoryPages.toSet().toList(); appdata.settings['favorites'] = networkFavorites.toSet().toList(); appdata.settings['searchSources'] = searchPages.toSet().toList(); appdata.saveData(); } class _EditFilePage extends StatefulWidget { const _EditFilePage(this.path, this.onExit); final String path; final void Function() onExit; @override State<_EditFilePage> createState() => __EditFilePageState(); } class __EditFilePageState extends State<_EditFilePage> { var current = ''; @override void initState() { super.initState(); current = File(widget.path).readAsStringSync(); } @override void dispose() { File(widget.path).writeAsStringSync(current); widget.onExit(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: Appbar(title: Text("Edit".tl)), body: Column( children: [ Container(height: 0.6, color: context.colorScheme.outlineVariant), Expanded( child: CodeEditor( initialValue: current, onChanged: (value) => current = value, ), ), ], ), ); } } class _CheckUpdatesButton extends StatefulWidget { const _CheckUpdatesButton(); @override State<_CheckUpdatesButton> createState() => _CheckUpdatesButtonState(); } class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> { bool isLoading = false; void check() async { setState(() { isLoading = true; }); var count = await ComicSourcePage.checkComicSourceUpdate(); if (count == -1) { context.showMessage(message: "Network error".tl); } else if (count == 0) { context.showMessage(message: "No updates".tl); } else { showUpdateDialog(); } setState(() { isLoading = false; }); } void showUpdateDialog() async { var text = ComicSourceManager().availableUpdates.entries .map((e) { return "${ComicSource.find(e.key)!.name}: ${e.value}"; }) .join("\n"); bool doUpdate = false; await showDialog( context: App.rootContext, builder: (context) { return ContentDialog( title: "Updates".tl, content: Text(text).paddingHorizontal(16), actions: [ FilledButton( onPressed: () { doUpdate = true; context.pop(); }, child: Text("Update".tl), ), ], ); }, ); if (doUpdate) { var loadingController = showLoadingDialog( context, message: "Updating".tl, withProgress: true, ); int current = 0; int total = ComicSourceManager().availableUpdates.length; try { var shouldUpdate = ComicSourceManager().availableUpdates.keys.toList(); for (var key in shouldUpdate) { var source = ComicSource.find(key)!; await ComicSourcePage.update(source, false); current++; loadingController.setProgress(current / total); } } catch (e) { context.showMessage(message: e.toString()); } loadingController.close(); } } @override Widget build(BuildContext context) { return FilledButton.tonalIcon( icon: isLoading ? SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : Icon(Icons.update), label: Text("Check updates".tl), onPressed: check, ); } } class _CallbackSetting extends StatefulWidget { const _CallbackSetting({required this.setting, required this.sourceKey}); final MapEntry> setting; final String sourceKey; @override State<_CallbackSetting> createState() => _CallbackSettingState(); } class _CallbackSettingState extends State<_CallbackSetting> { String get key => widget.setting.key; String get buttonText => widget.setting.value['buttonText'] ?? "Click"; String get title => widget.setting.value['title'] ?? key; bool isLoading = false; Future onClick() async { var func = widget.setting.value['callback']; var result = func([]); if (result is Future) { setState(() { isLoading = true; }); try { await result; } finally { setState(() { isLoading = false; }); } } } @override Widget build(BuildContext context) { return ListTile( title: Text(title.ts(widget.sourceKey)), trailing: Button.normal( onPressed: onClick, isLoading: isLoading, child: Text(buttonText.ts(widget.sourceKey)), ).fixHeight(32), ); } } class _SliverComicSource extends StatefulWidget { const _SliverComicSource({ super.key, required this.source, required this.edit, required this.update, required this.delete, }); final ComicSource source; final void Function(ComicSource source) edit; final void Function(ComicSource source) update; final void Function(ComicSource source) delete; @override State<_SliverComicSource> createState() => _SliverComicSourceState(); } class _SliverComicSourceState extends State<_SliverComicSource> { ComicSource get source => widget.source; @override Widget build(BuildContext context) { var newVersion = ComicSourceManager().availableUpdates[source.key]; bool hasUpdate = newVersion != null && compareSemVer(newVersion, source.version); return SliverMainAxisGroup( slivers: [ SliverPadding(padding: const EdgeInsets.only(top: 16)), SliverToBoxAdapter( child: ListTile( title: Row( children: [ Text(source.name, style: ts.s18), const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: context.colorScheme.surfaceContainer, borderRadius: BorderRadius.circular(8), ), child: Text( source.version, style: const TextStyle(fontSize: 13), ), ), if (hasUpdate) Tooltip( message: newVersion, child: Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2, ), decoration: BoxDecoration( color: context.colorScheme.primaryContainer, borderRadius: BorderRadius.circular(8), ), child: Text( "New Version".tl, style: const TextStyle(fontSize: 13), ), ), ).paddingLeft(4), ], ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Tooltip( message: "Edit".tl, child: IconButton( onPressed: () => widget.edit(source), icon: const Icon(Icons.edit_note), ), ), Tooltip( message: "Update".tl, child: IconButton( onPressed: () => widget.update(source), icon: const Icon(Icons.update), ), ), Tooltip( message: "Delete".tl, child: IconButton( onPressed: () => widget.delete(source), icon: const Icon(Icons.delete), ), ), ], ), ), ), SliverToBoxAdapter( child: Container( margin: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: context.colorScheme.outlineVariant, width: 0.6, ), ), ), ), ), SliverToBoxAdapter( child: Column(children: buildSourceSettings().toList()), ), SliverToBoxAdapter(child: Column(children: _buildAccount().toList())), ], ); } Iterable buildSourceSettings() sync* { if (source.settings == null) { return; } else if (source.data['settings'] == null) { source.data['settings'] = {}; } for (var item in source.settings!.entries) { var key = item.key; String type = item.value['type']; try { if (type == "select") { var current = source.data['settings'][key]; if (current == null) { var d = item.value['default']; for (var option in item.value['options']) { if (option['value'] == d) { current = option['text'] ?? option['value']; break; } } } else { current = item.value['options'].firstWhere( (e) => e['value'] == current, )['text'] ?? current; } yield ListTile( title: Text((item.value['title'] as String).ts(source.key)), trailing: Select( current: (current as String).ts(source.key), values: (item.value['options'] as List) .map( (e) => ((e['text'] ?? e['value']) as String).ts(source.key), ) .toList(), onTap: (i) { source.data['settings'][key] = item.value['options'][i]['value']; source.saveData(); setState(() {}); }, ), ); } else if (type == "switch") { var current = source.data['settings'][key] ?? item.value['default']; yield ListTile( title: Text((item.value['title'] as String).ts(source.key)), trailing: Switch( value: current, onChanged: (v) { source.data['settings'][key] = v; source.saveData(); setState(() {}); }, ), ); } else if (type == "input") { var current = source.data['settings'][key] ?? item.value['default'] ?? ''; yield ListTile( title: Text((item.value['title'] as String).ts(source.key)), subtitle: Text( current, maxLines: 1, overflow: TextOverflow.ellipsis, ), trailing: IconButton( icon: const Icon(Icons.edit), onPressed: () { showInputDialog( context: context, title: (item.value['title'] as String).ts(source.key), initialValue: current, inputValidator: item.value['validator'] == null ? null : RegExp(item.value['validator']), onConfirm: (value) { source.data['settings'][key] = value; source.saveData(); setState(() {}); return null; }, ); }, ), ); } else if (type == "callback") { yield _CallbackSetting(setting: item, sourceKey: source.key); } } catch (e, s) { Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s"); } } } final _reLogin = {}; Iterable _buildAccount() sync* { if (source.account == null) return; final bool logged = source.isLogged; if (!logged) { yield ListTile( title: Text("Log in".tl), trailing: const Icon(Icons.arrow_right), onTap: () async { await context.to( () => _LoginPage(config: source.account!, source: source), ); source.saveData(); setState(() {}); }, ); } if (logged) { for (var item in source.account!.infoItems) { if (item.builder != null) { yield item.builder!(context); } else { yield ListTile( title: Text(item.title.tl), subtitle: item.data == null ? null : Text(item.data!()), onTap: item.onTap, ); } } if (source.data["account"] is List) { bool loading = _reLogin[source.key] == true; yield ListTile( title: Text("Re-login".tl), subtitle: Text("Click if login expired".tl), onTap: () async { if (source.data["account"] == null) { context.showMessage(message: "No data".tl); return; } setState(() { _reLogin[source.key] = true; }); final List account = source.data["account"]; var res = await source.account!.login!(account[0], account[1]); if (res.error) { context.showMessage(message: res.errorMessage!); } else { context.showMessage(message: "Success".tl); } setState(() { _reLogin[source.key] = false; }); }, trailing: loading ? const SizedBox.square( dimension: 24, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.refresh), ); } yield ListTile( title: Text("Log out".tl), onTap: () { source.data["account"] = null; source.account?.logout(); source.saveData(); ComicSourceManager().notifyStateChange(); setState(() {}); }, trailing: const Icon(Icons.logout), ); } } } class _LoginPage extends StatefulWidget { const _LoginPage({required this.config, required this.source}); final AccountConfig config; final ComicSource source; @override State<_LoginPage> createState() => _LoginPageState(); } class _LoginPageState extends State<_LoginPage> { String username = ""; String password = ""; bool loading = false; final Map _cookies = {}; @override Widget build(BuildContext context) { return Scaffold( appBar: const Appbar(title: Text('')), body: Center( child: Container( padding: const EdgeInsets.all(16), constraints: const BoxConstraints(maxWidth: 400), child: AutofillGroup( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text("Login".tl, style: const TextStyle(fontSize: 24)), const SizedBox(height: 32), if (widget.config.cookieFields == null) TextField( decoration: InputDecoration( labelText: "Username".tl, border: const OutlineInputBorder(), ), enabled: widget.config.login != null, onChanged: (s) { username = s; }, autofillHints: const [AutofillHints.username], ).paddingBottom(16), if (widget.config.cookieFields == null) TextField( decoration: InputDecoration( labelText: "Password".tl, border: const OutlineInputBorder(), ), obscureText: true, enabled: widget.config.login != null, onChanged: (s) { password = s; }, onSubmitted: (s) => login(), autofillHints: const [AutofillHints.password], ).paddingBottom(16), for (var field in widget.config.cookieFields ?? []) TextField( decoration: InputDecoration( labelText: field, border: const OutlineInputBorder(), ), obscureText: true, enabled: widget.config.validateCookies != null, onChanged: (s) { _cookies[field] = s; }, ).paddingBottom(16), if (widget.config.login == null && widget.config.cookieFields == null) Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline), const SizedBox(width: 8), Text("Login with password is disabled".tl), ], ) else Button.filled( isLoading: loading, onPressed: login, child: Text("Continue".tl), ), const SizedBox(height: 24), if (widget.config.loginWebsite != null) TextButton( onPressed: () { if (App.isLinux) { loginWithWebview2(); } else { loginWithWebview(); } }, child: Text("Login with webview".tl), ), const SizedBox(height: 8), if (widget.config.registerWebsite != null) TextButton( onPressed: () => launchUrlString(widget.config.registerWebsite!), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.link), const SizedBox(width: 8), Text("Create Account".tl), ], ), ), ], ), ), ), ), ); } void login() { if (widget.config.login != null) { if (username.isEmpty || password.isEmpty) { showToast( message: "Cannot be empty".tl, icon: const Icon(Icons.error_outline), context: context, ); return; } setState(() { loading = true; }); widget.config.login!(username, password).then((value) { if (value.error) { context.showMessage(message: value.errorMessage!); setState(() { loading = false; }); } else { if (mounted) { context.pop(); } } }); } else if (widget.config.validateCookies != null) { setState(() { loading = true; }); var cookies = widget.config.cookieFields! .map((e) => _cookies[e] ?? '') .toList(); widget.config.validateCookies!(cookies).then((value) { if (value) { widget.source.data['account'] = 'ok'; widget.source.saveData(); context.pop(); } else { context.showMessage(message: "Invalid cookies".tl); setState(() { loading = false; }); } }); } } void loginWithWebview() async { var url = widget.config.loginWebsite!; var title = ''; bool success = false; void validate(InAppWebViewController c) async { if (widget.config.checkLoginStatus != null && widget.config.checkLoginStatus!(url, title)) { var cookies = (await c.getCookies(url)) ?? []; SingleInstanceCookieJar.instance?.saveFromResponse( Uri.parse(url), cookies, ); success = true; widget.config.onLoginWithWebviewSuccess?.call(); App.mainNavigatorKey?.currentContext?.pop(); } } await context.to( () => AppWebview( initialUrl: widget.config.loginWebsite!, onNavigation: (u, c) { url = u; validate(c); return false; }, onTitleChange: (t, c) { title = t; validate(c); }, ), ); if (success) { widget.source.data['account'] = 'ok'; widget.source.saveData(); context.pop(); } } // for linux void loginWithWebview2() async { if (!await DesktopWebview.isAvailable()) { context.showMessage(message: "Webview is not available".tl); } var url = widget.config.loginWebsite!; var title = ''; bool success = false; void onClose() { if (success) { widget.source.data['account'] = 'ok'; widget.source.saveData(); context.pop(); } } void validate(DesktopWebview webview) async { if (widget.config.checkLoginStatus != null && widget.config.checkLoginStatus!(url, title)) { var cookiesMap = await webview.getCookies(url); var cookies = []; cookiesMap.forEach((key, value) { cookies.add(io.Cookie(key, value)); }); SingleInstanceCookieJar.instance?.saveFromResponse( Uri.parse(url), cookies, ); success = true; widget.config.onLoginWithWebviewSuccess?.call(); webview.close(); onClose(); } } var webview = DesktopWebview( initialUrl: widget.config.loginWebsite!, onTitleChange: (t, webview) { title = t; validate(webview); }, onNavigation: (u, webview) { url = u; validate(webview); }, onClose: onClose, ); webview.open(); } }