diff --git a/assets/translation.json b/assets/translation.json index f9f3955..999477e 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -320,7 +320,8 @@ "Keyword": "关键词", "Manage": "管理", "Verify": "验证", - "Cloudflare verification required": "需要Cloudflare验证" + "Cloudflare verification required": "需要Cloudflare验证", + "Success": "成功" }, "zh_TW": { "Home": "首頁", @@ -643,6 +644,7 @@ "Keyword": "關鍵詞", "Manage": "管理", "Verify": "驗證", - "Cloudflare verification required": "需要Cloudflare驗證" + "Cloudflare verification required": "需要Cloudflare驗證", + "Success": "成功" } } \ No newline at end of file diff --git a/lib/pages/accounts_page.dart b/lib/pages/accounts_page.dart deleted file mode 100644 index b569b19..0000000 --- a/lib/pages/accounts_page.dart +++ /dev/null @@ -1,409 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.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/comic_source/comic_source.dart'; -import 'package:venera/foundation/state_controller.dart'; -import 'package:venera/network/cookie_jar.dart'; -import 'package:venera/pages/webview.dart'; -import 'package:venera/utils/translations.dart'; -import 'dart:io' as io; - -class AccountsPageLogic extends StateController { - final _reLogin = {}; -} - -class AccountsPage extends StatelessWidget { - const AccountsPage({super.key}); - - AccountsPageLogic get logic => StateController.find(); - - @override - Widget build(BuildContext context) { - var body = StateBuilder( - init: AccountsPageLogic(), - builder: (logic) { - return CustomScrollView( - slivers: [ - SliverAppbar(title: Text("Accounts".tl)), - SliverList( - delegate: SliverChildListDelegate( - buildContent(context).toList(), - ), - ), - SliverPadding( - padding: EdgeInsets.only(bottom: context.padding.bottom), - ) - ], - ); - }, - ); - - return Scaffold( - body: body, - ); - } - - Iterable buildContent(BuildContext context) sync* { - var sources = ComicSource.all().where((element) => element.account != null); - if (sources.isEmpty) return; - - for (var element in sources) { - final bool logged = element.isLogged; - yield Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text( - element.name, - style: const TextStyle(fontSize: 16), - ), - ); - if (!logged) { - yield ListTile( - title: Text("Log in".tl), - trailing: const Icon(Icons.arrow_right), - onTap: () async { - await context.to( - () => _LoginPage( - config: element.account!, - source: element, - ), - ); - element.saveData(); - ComicSource.notifyListeners(); - logic.update(); - }, - ); - } - if (logged) { - for (var item in element.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 (element.data["account"] is List) { - bool loading = logic._reLogin[element.key] == true; - yield ListTile( - title: Text("Re-login".tl), - subtitle: Text("Click if login expired".tl), - onTap: () async { - if (element.data["account"] == null) { - context.showMessage(message: "No data".tl); - return; - } - logic._reLogin[element.key] = true; - logic.update(); - final List account = element.data["account"]; - var res = await element.account!.login!(account[0], account[1]); - if (res.error) { - context.showMessage(message: res.errorMessage!); - } else { - context.showMessage(message: "Success".tl); - } - logic._reLogin[element.key] = false; - logic.update(); - }, - trailing: loading - ? const SizedBox.square( - dimension: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Icon(Icons.refresh), - ); - } - yield ListTile( - title: Text("Log out".tl), - onTap: () { - element.data["account"] = null; - element.account?.logout(); - element.saveData(); - ComicSource.notifyListeners(); - logic.update(); - }, - trailing: const Icon(Icons.logout), - ); - } - yield const Divider(thickness: 0.6); - } - } - - void setClipboard(String text) { - Clipboard.setData(ClipboardData(text: text)); - showToast( - message: "Copied".tl, - icon: const Icon(Icons.check), - context: App.rootContext, - ); - } -} - -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.isWindows || 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 windows and 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(); - } -} diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index bdbb94c..55298d0 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -1,5 +1,7 @@ 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'; @@ -7,11 +9,13 @@ 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 StatefulWidget { +class ComicSourcePage extends StatelessWidget { const ComicSourcePage({super.key}); static Future checkComicSourceUpdate() async { @@ -44,11 +48,6 @@ class ComicSourcePage extends StatefulWidget { return shouldUpdate.length; } - @override - State createState() => _ComicSourcePageState(); -} - -class _ComicSourcePageState extends State { @override Widget build(BuildContext context) { return Scaffold( @@ -92,167 +91,19 @@ class _BodyState extends State<_Body> { style: AppbarStyle.shadow, ), buildCard(context), - for (var source in ComicSource.all()) buildSource(context, source), + 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)), ], ); } - Widget buildSource(BuildContext context, ComicSource source) { - var newVersion = ComicSource.availableUpdates[source.key]; - bool hasUpdate = - newVersion != null && compareSemVer(newVersion, source.version); - return SliverToBoxAdapter( - child: Column( - children: [ - const Divider(), - ListTile( - title: Row( - children: [ - Text(source.name), - const SizedBox(width: 6), - 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), - ), - ), - ) - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Tooltip( - message: "Edit".tl, - child: IconButton( - onPressed: () => edit(source), - icon: const Icon(Icons.edit_note)), - ), - Tooltip( - message: "Update".tl, - child: IconButton( - onPressed: () => update(source), - icon: const Icon(Icons.update)), - ), - Tooltip( - message: "Delete".tl, - child: IconButton( - onPressed: () => delete(source), - icon: const Icon(Icons.delete)), - ), - ], - ), - ), - ListTile( - title: const Text("Version"), - subtitle: Text(source.version), - ), - ...buildSourceSettings(source), - ], - ), - ); - } - - Iterable buildSourceSettings(ComicSource source) 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"); - } - } - } - void delete(ComicSource source) { showConfirmDialog( context: App.rootContext, @@ -297,10 +148,12 @@ class _BodyState extends State<_Body> { // } } - context.to(() => _EditFilePage(source.filePath, () async { - await ComicSource.reload(); - setState(() {}); - })); + context.to( + () => _EditFilePage(source.filePath, () async { + await ComicSource.reload(); + setState(() {}); + }), + ); } static Future update(ComicSource source) async { @@ -764,3 +617,566 @@ class _CallbackSettingState extends State<_CallbackSetting> { ); } } + +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 = ComicSource.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(); + ComicSource.notifyListeners(); + 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.isWindows || 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 windows and 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(); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index ffd8318..a550dbf 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -9,7 +9,6 @@ import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/log.dart'; -import 'package:venera/pages/accounts_page.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/comic_source_page.dart'; import 'package:venera/pages/downloading_page.dart'; @@ -36,7 +35,6 @@ class HomePage extends StatelessWidget { const _History(), const _Local(), const _ComicSourceWidget(), - const _AccountsWidget(), const ImageFavorites(), SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)), ], @@ -698,115 +696,6 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> { } } -class _AccountsWidget extends StatefulWidget { - const _AccountsWidget(); - - @override - State<_AccountsWidget> createState() => _AccountsWidgetState(); -} - -class _AccountsWidgetState extends State<_AccountsWidget> { - late List accounts; - - void onComicSourceChange() { - setState(() { - accounts.clear(); - for (var c in ComicSource.all()) { - if (c.isLogged) { - accounts.add(c.name); - } - } - }); - } - - @override - void initState() { - accounts = []; - for (var c in ComicSource.all()) { - if (c.isLogged) { - accounts.add(c.name); - } - } - ComicSource.addListener(onComicSourceChange); - super.initState(); - } - - @override - void dispose() { - ComicSource.removeListener(onComicSourceChange); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SliverToBoxAdapter( - child: 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: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () { - context.to(() => const AccountsPage()); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 56, - child: Row( - children: [ - Center( - child: Text('Accounts'.tl, style: ts.s18), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text(accounts.length.toString(), style: ts.s12), - ), - const Spacer(), - const Icon(Icons.arrow_right), - ], - ), - ).paddingHorizontal(16), - SizedBox( - width: double.infinity, - child: Wrap( - runSpacing: 8, - spacing: 8, - children: accounts.map((e) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text(e), - ); - }).toList(), - ).paddingHorizontal(16).paddingBottom(16), - ), - ], - ), - ), - ), - ); - } -} - class _AnimatedDownloadingIcon extends StatefulWidget { const _AnimatedDownloadingIcon();