mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Merge account page and comic source page.
This commit is contained in:
@@ -320,7 +320,8 @@
|
|||||||
"Keyword": "关键词",
|
"Keyword": "关键词",
|
||||||
"Manage": "管理",
|
"Manage": "管理",
|
||||||
"Verify": "验证",
|
"Verify": "验证",
|
||||||
"Cloudflare verification required": "需要Cloudflare验证"
|
"Cloudflare verification required": "需要Cloudflare验证",
|
||||||
|
"Success": "成功"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -643,6 +644,7 @@
|
|||||||
"Keyword": "關鍵詞",
|
"Keyword": "關鍵詞",
|
||||||
"Manage": "管理",
|
"Manage": "管理",
|
||||||
"Verify": "驗證",
|
"Verify": "驗證",
|
||||||
"Cloudflare verification required": "需要Cloudflare驗證"
|
"Cloudflare verification required": "需要Cloudflare驗證",
|
||||||
|
"Success": "成功"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -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 = <String, bool>{};
|
|
||||||
}
|
|
||||||
|
|
||||||
class AccountsPage extends StatelessWidget {
|
|
||||||
const AccountsPage({super.key});
|
|
||||||
|
|
||||||
AccountsPageLogic get logic => StateController.find<AccountsPageLogic>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var body = StateBuilder<AccountsPageLogic>(
|
|
||||||
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<Widget> 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<String, String> _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 ?? <String>[])
|
|
||||||
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 = <io.Cookie>[];
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io' as io;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.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/comic_source/comic_source.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/network/app_dio.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/ext.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
class ComicSourcePage extends StatefulWidget {
|
class ComicSourcePage extends StatelessWidget {
|
||||||
const ComicSourcePage({super.key});
|
const ComicSourcePage({super.key});
|
||||||
|
|
||||||
static Future<int> checkComicSourceUpdate() async {
|
static Future<int> checkComicSourceUpdate() async {
|
||||||
@@ -44,11 +48,6 @@ class ComicSourcePage extends StatefulWidget {
|
|||||||
return shouldUpdate.length;
|
return shouldUpdate.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
State<ComicSourcePage> createState() => _ComicSourcePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ComicSourcePageState extends State<ComicSourcePage> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -92,167 +91,19 @@ class _BodyState extends State<_Body> {
|
|||||||
style: AppbarStyle.shadow,
|
style: AppbarStyle.shadow,
|
||||||
),
|
),
|
||||||
buildCard(context),
|
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)),
|
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<Widget> 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<String>((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) {
|
void delete(ComicSource source) {
|
||||||
showConfirmDialog(
|
showConfirmDialog(
|
||||||
context: App.rootContext,
|
context: App.rootContext,
|
||||||
@@ -297,10 +148,12 @@ class _BodyState extends State<_Body> {
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.to(() => _EditFilePage(source.filePath, () async {
|
context.to(
|
||||||
await ComicSource.reload();
|
() => _EditFilePage(source.filePath, () async {
|
||||||
setState(() {});
|
await ComicSource.reload();
|
||||||
}));
|
setState(() {});
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> update(ComicSource source) async {
|
static Future<void> 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<Widget> 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<String>((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 = <String, bool>{};
|
||||||
|
|
||||||
|
Iterable<Widget> _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<String, String> _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 ?? <String>[])
|
||||||
|
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 = <io.Cookie>[];
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -9,7 +9,6 @@ import 'package:venera/foundation/favorites.dart';
|
|||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.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_page.dart';
|
||||||
import 'package:venera/pages/comic_source_page.dart';
|
import 'package:venera/pages/comic_source_page.dart';
|
||||||
import 'package:venera/pages/downloading_page.dart';
|
import 'package:venera/pages/downloading_page.dart';
|
||||||
@@ -36,7 +35,6 @@ class HomePage extends StatelessWidget {
|
|||||||
const _History(),
|
const _History(),
|
||||||
const _Local(),
|
const _Local(),
|
||||||
const _ComicSourceWidget(),
|
const _ComicSourceWidget(),
|
||||||
const _AccountsWidget(),
|
|
||||||
const ImageFavorites(),
|
const ImageFavorites(),
|
||||||
SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)),
|
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<String> 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 {
|
class _AnimatedDownloadingIcon extends StatefulWidget {
|
||||||
const _AnimatedDownloadingIcon();
|
const _AnimatedDownloadingIcon();
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user