mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
@@ -318,7 +318,10 @@
|
|||||||
"Deselect All": "取消全选",
|
"Deselect All": "取消全选",
|
||||||
"Add keyword": "添加关键词",
|
"Add keyword": "添加关键词",
|
||||||
"Keyword": "关键词",
|
"Keyword": "关键词",
|
||||||
"Manage": "管理"
|
"Manage": "管理",
|
||||||
|
"Verify": "验证",
|
||||||
|
"Cloudflare verification required": "需要Cloudflare验证",
|
||||||
|
"Success": "成功"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -639,6 +642,9 @@
|
|||||||
"Deselect All": "取消全選",
|
"Deselect All": "取消全選",
|
||||||
"Add keyword": "添加關鍵詞",
|
"Add keyword": "添加關鍵詞",
|
||||||
"Keyword": "關鍵詞",
|
"Keyword": "關鍵詞",
|
||||||
"Manage": "管理"
|
"Manage": "管理",
|
||||||
|
"Verify": "驗證",
|
||||||
|
"Cloudflare verification required": "需要Cloudflare驗證",
|
||||||
|
"Success": "成功"
|
||||||
}
|
}
|
||||||
}
|
}
|
1
debian/gui/venera.desktop
vendored
1
debian/gui/venera.desktop
vendored
@@ -6,3 +6,4 @@ Terminal=false
|
|||||||
Type=Application
|
Type=Application
|
||||||
Categories=Utility
|
Categories=Utility
|
||||||
Keywords=Flutter;comic;images;
|
Keywords=Flutter;comic;images;
|
||||||
|
Icon=venera
|
@@ -57,7 +57,9 @@ class NetworkError extends StatelessWidget {
|
|||||||
if (cfe != null)
|
if (cfe != null)
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => passCloudflare(
|
onPressed: () => passCloudflare(
|
||||||
CloudflareException.fromString(message)!, retry!),
|
CloudflareException.fromString(message)!,
|
||||||
|
retry!,
|
||||||
|
),
|
||||||
child: Text('Verify'.tl),
|
child: Text('Verify'.tl),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@@ -130,7 +132,7 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
if (res.success) {
|
if (res.success) {
|
||||||
return res;
|
return res;
|
||||||
} else {
|
} else {
|
||||||
if(!mounted) return res;
|
if (!mounted) return res;
|
||||||
if (retry >= 3) {
|
if (retry >= 3) {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -188,7 +190,7 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
loadDataWithRetry().then((value) async {
|
loadDataWithRetry().then((value) async {
|
||||||
if(!mounted) return;
|
if (!mounted) return;
|
||||||
if (value.success) {
|
if (value.success) {
|
||||||
data = value.data;
|
data = value.data;
|
||||||
await onDataLoaded();
|
await onDataLoaded();
|
||||||
@@ -321,21 +323,11 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildError(BuildContext context, String error) {
|
Widget buildError(BuildContext context, String error) {
|
||||||
return Center(
|
return NetworkError(
|
||||||
child: Column(
|
withAppbar: false,
|
||||||
mainAxisSize: MainAxisSize.min,
|
message: error,
|
||||||
children: [
|
retry: reset,
|
||||||
Text(error, maxLines: 3),
|
);
|
||||||
const SizedBox(height: 12),
|
|
||||||
Button.outlined(
|
|
||||||
onPressed: () {
|
|
||||||
reset();
|
|
||||||
},
|
|
||||||
child: const Text("Retry"),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
).paddingHorizontal(16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@@ -10,7 +10,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.2.2";
|
final version = "1.2.3";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import 'dart:io' as io;
|
import 'dart:io' as io;
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/consts.dart';
|
import 'package:venera/foundation/consts.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/pages/webview.dart';
|
import 'package:venera/pages/webview.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ class CloudflareException implements DioException {
|
|||||||
class CloudflareInterceptor extends Interceptor {
|
class CloudflareInterceptor extends Interceptor {
|
||||||
@override
|
@override
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
if(options.headers['cookie'].toString().contains('cf_clearance')) {
|
if (options.headers['cookie'].toString().contains('cf_clearance')) {
|
||||||
options.headers['user-agent'] = appdata.implicitData['ua'] ?? webUA;
|
options.headers['user-agent'] = appdata.implicitData['ua'] ?? webUA;
|
||||||
}
|
}
|
||||||
handler.next(options);
|
handler.next(options);
|
||||||
@@ -120,16 +122,25 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
|
|||||||
var webview = DesktopWebview(
|
var webview = DesktopWebview(
|
||||||
initialUrl: url,
|
initialUrl: url,
|
||||||
onTitleChange: (title, controller) async {
|
onTitleChange: (title, controller) async {
|
||||||
var res = await controller.evaluateJavascript(
|
var head =
|
||||||
"document.head.innerHTML.includes('#challenge-success-text')");
|
await controller.evaluateJavascript("document.head.innerHTML") ??
|
||||||
if (res == 'false') {
|
"";
|
||||||
|
Log.info("Cloudflare", "Checking head: $head");
|
||||||
|
var isChallenging = head.contains('#challenge-success-text') ||
|
||||||
|
head.contains("#challenge-error-text") ||
|
||||||
|
head.contains("#challenge-form");
|
||||||
|
if (!isChallenging) {
|
||||||
|
Log.info(
|
||||||
|
"Cloudflare",
|
||||||
|
"Cloudflare is passed due to there is no challenge css",
|
||||||
|
);
|
||||||
var ua = controller.userAgent;
|
var ua = controller.userAgent;
|
||||||
if (ua != null) {
|
if (ua != null) {
|
||||||
appdata.implicitData['ua'] = ua;
|
appdata.implicitData['ua'] = ua;
|
||||||
appdata.writeImplicitData();
|
appdata.writeImplicitData();
|
||||||
}
|
}
|
||||||
var cookiesMap = await controller.getCookies(url);
|
var cookiesMap = await controller.getCookies(url);
|
||||||
if(cookiesMap['cf_clearance'] == null) {
|
if (cookiesMap['cf_clearance'] == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
saveCookies(cookiesMap);
|
saveCookies(cookiesMap);
|
||||||
@@ -137,30 +148,47 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
|
|||||||
onFinished();
|
onFinished();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onClose: onFinished,
|
||||||
);
|
);
|
||||||
webview.open();
|
webview.open();
|
||||||
} else {
|
} else {
|
||||||
|
void check(InAppWebViewController controller) async {
|
||||||
|
var head = await controller.evaluateJavascript(
|
||||||
|
source: "document.head.innerHTML") as String;
|
||||||
|
Log.info("Cloudflare", "Checking head: $head");
|
||||||
|
var isChallenging = head.contains('#challenge-success-text') ||
|
||||||
|
head.contains("#challenge-error-text") ||
|
||||||
|
head.contains("#challenge-form");
|
||||||
|
if (!isChallenging) {
|
||||||
|
Log.info(
|
||||||
|
"Cloudflare",
|
||||||
|
"Cloudflare is passed due to there is no challenge css",
|
||||||
|
);
|
||||||
|
var ua = await controller.getUA();
|
||||||
|
if (ua != null) {
|
||||||
|
appdata.implicitData['ua'] = ua;
|
||||||
|
appdata.writeImplicitData();
|
||||||
|
}
|
||||||
|
var cookies = await controller.getCookies(url) ?? [];
|
||||||
|
if (cookies.firstWhereOrNull(
|
||||||
|
(element) => element.name == 'cf_clearance') ==
|
||||||
|
null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies);
|
||||||
|
App.rootPop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await App.rootContext.to(
|
await App.rootContext.to(
|
||||||
() => AppWebview(
|
() => AppWebview(
|
||||||
initialUrl: url,
|
initialUrl: url,
|
||||||
singlePage: true,
|
singlePage: true,
|
||||||
|
onTitleChange: (title, controller) async {
|
||||||
|
check(controller);
|
||||||
|
},
|
||||||
onLoadStop: (controller) async {
|
onLoadStop: (controller) async {
|
||||||
var res = await controller.platform.evaluateJavascript(
|
check(controller);
|
||||||
source:
|
|
||||||
"document.head.innerHTML.includes('#challenge-success-text')");
|
|
||||||
if (res == false) {
|
|
||||||
var ua = await controller.getUA();
|
|
||||||
if (ua != null) {
|
|
||||||
appdata.implicitData['ua'] = ua;
|
|
||||||
appdata.writeImplicitData();
|
|
||||||
}
|
|
||||||
var cookies = await controller.getCookies(url) ?? [];
|
|
||||||
if(cookies.firstWhereOrNull((element) => element.name == 'cf_clearance') == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies);
|
|
||||||
App.rootPop();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onStarted: (controller) async {
|
onStarted: (controller) async {
|
||||||
var ua = await controller.getUA();
|
var ua = await controller.getUA();
|
||||||
|
@@ -59,6 +59,16 @@ abstract class DownloadTask with ChangeNotifier {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is DownloadTask &&
|
||||||
|
other.id == id &&
|
||||||
|
other.comicType == comicType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(id, comicType);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
||||||
@@ -220,7 +230,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
runRecorder();
|
runRecorder();
|
||||||
|
|
||||||
if (comic == null) {
|
if (comic == null) {
|
||||||
var res = await runWithRetry(() async {
|
_message = "Fetching comic info...";
|
||||||
|
notifyListeners();
|
||||||
|
var res = await _runWithRetry(() async {
|
||||||
var r = await source.loadComicInfo!(comicId);
|
var r = await source.loadComicInfo!(comicId);
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
throw r.errorMessage!;
|
throw r.errorMessage!;
|
||||||
@@ -260,7 +272,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
await LocalManager().saveCurrentDownloadingTasks();
|
await LocalManager().saveCurrentDownloadingTasks();
|
||||||
|
|
||||||
if (_cover == null) {
|
if (_cover == null) {
|
||||||
var res = await runWithRetry(() async {
|
_message = "Downloading cover...";
|
||||||
|
notifyListeners();
|
||||||
|
var res = await _runWithRetry(() async {
|
||||||
Uint8List? data;
|
Uint8List? data;
|
||||||
await for (var progress
|
await for (var progress
|
||||||
in ImageDownloader.loadThumbnail(comic!.cover, source.key)) {
|
in ImageDownloader.loadThumbnail(comic!.cover, source.key)) {
|
||||||
@@ -272,8 +286,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
throw "Failed to download cover";
|
throw "Failed to download cover";
|
||||||
}
|
}
|
||||||
var fileType = detectFileType(data);
|
var fileType = detectFileType(data);
|
||||||
var file =
|
var file = File(FilePath.join(path!, "cover${fileType.ext}"));
|
||||||
File(FilePath.join(path!, "cover${fileType.ext}"));
|
|
||||||
file.writeAsBytesSync(data);
|
file.writeAsBytesSync(data);
|
||||||
return "file://${file.path}";
|
return "file://${file.path}";
|
||||||
});
|
});
|
||||||
@@ -290,7 +303,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
|
|
||||||
if (_images == null) {
|
if (_images == null) {
|
||||||
if (comic!.chapters == null) {
|
if (comic!.chapters == null) {
|
||||||
var res = await runWithRetry(() async {
|
_message = "Fetching image list...";
|
||||||
|
notifyListeners();
|
||||||
|
var res = await _runWithRetry(() async {
|
||||||
var r = await source.loadComicPages!(comicId, null);
|
var r = await source.loadComicPages!(comicId, null);
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
throw r.errorMessage!;
|
throw r.errorMessage!;
|
||||||
@@ -312,6 +327,8 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
} else {
|
} else {
|
||||||
_images = {};
|
_images = {};
|
||||||
_totalCount = 0;
|
_totalCount = 0;
|
||||||
|
int cpCount = 0;
|
||||||
|
int totalCpCount = chapters?.length ?? comic!.chapters!.length;
|
||||||
for (var i in comic!.chapters!.keys) {
|
for (var i in comic!.chapters!.keys) {
|
||||||
if (chapters != null && !chapters!.contains(i)) {
|
if (chapters != null && !chapters!.contains(i)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -320,7 +337,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
_totalCount += _images![i]!.length;
|
_totalCount += _images![i]!.length;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var res = await runWithRetry(() async {
|
_message = "Fetching image list ($cpCount/$totalCpCount)...";
|
||||||
|
notifyListeners();
|
||||||
|
var res = await _runWithRetry(() async {
|
||||||
var r = await source.loadComicPages!(comicId, i);
|
var r = await source.loadComicPages!(comicId, i);
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
throw r.errorMessage!;
|
throw r.errorMessage!;
|
||||||
@@ -458,8 +477,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
}).toList(),
|
}).toList(),
|
||||||
directory: Directory(path!).name,
|
directory: Directory(path!).name,
|
||||||
chapters: comic!.chapters,
|
chapters: comic!.chapters,
|
||||||
cover:
|
cover: File(_cover!.split("file://").last).name,
|
||||||
File(_cover!.split("file://").last).name,
|
|
||||||
comicType: ComicType(source.key.hashCode),
|
comicType: ComicType(source.key.hashCode),
|
||||||
downloadedChapters: chapters ?? [],
|
downloadedChapters: chapters ?? [],
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
@@ -478,7 +496,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
int get hashCode => Object.hash(comicId, source.key);
|
int get hashCode => Object.hash(comicId, source.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<T>> runWithRetry<T>(Future<T> Function() task,
|
Future<Res<T>> _runWithRetry<T>(Future<T> Function() task,
|
||||||
{int retry = 3}) async {
|
{int retry = 3}) async {
|
||||||
for (var i = 0; i < retry; i++) {
|
for (var i = 0; i < retry; i++) {
|
||||||
try {
|
try {
|
||||||
|
@@ -1,349 +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';
|
|
||||||
|
|
||||||
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: 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -46,6 +46,7 @@ class _DownloadingPageState extends State<DownloadingPage> {
|
|||||||
i--;
|
i--;
|
||||||
|
|
||||||
return _DownloadTaskTile(
|
return _DownloadTaskTile(
|
||||||
|
key: ValueKey(LocalManager().downloadingTasks[i]),
|
||||||
task: LocalManager().downloadingTasks[i],
|
task: LocalManager().downloadingTasks[i],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -120,7 +121,7 @@ class _DownloadingPageState extends State<DownloadingPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DownloadTaskTile extends StatefulWidget {
|
class _DownloadTaskTile extends StatefulWidget {
|
||||||
const _DownloadTaskTile({required this.task});
|
const _DownloadTaskTile({required this.task, super.key});
|
||||||
|
|
||||||
final DownloadTask task;
|
final DownloadTask task;
|
||||||
|
|
||||||
@@ -129,20 +130,33 @@ class _DownloadTaskTile extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DownloadTaskTileState extends State<_DownloadTaskTile> {
|
class _DownloadTaskTileState extends State<_DownloadTaskTile> {
|
||||||
|
late DownloadTask task;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
widget.task.addListener(update);
|
task = widget.task;
|
||||||
|
task.addListener(update);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.task.removeListener(update);
|
task.removeListener(update);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant _DownloadTaskTile oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.task != widget.task) {
|
||||||
|
task.removeListener(update);
|
||||||
|
task = widget.task;
|
||||||
|
task.addListener(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
context.findAncestorStateOfType<_DownloadingPageState>()?.update();
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@@ -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();
|
||||||
|
|
||||||
|
@@ -111,9 +111,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
late _ReaderState reader;
|
late _ReaderState reader;
|
||||||
|
|
||||||
int get totalPages => ((reader.images!.length + reader.imagesPerPage - 1) /
|
int get totalPages => (reader.images!.length / reader.imagesPerPage).ceil();
|
||||||
reader.imagesPerPage)
|
|
||||||
.ceil();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -228,6 +226,8 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
? Axis.vertical
|
? Axis.vertical
|
||||||
: Axis.horizontal;
|
: Axis.horizontal;
|
||||||
|
|
||||||
|
bool reverse = reader.mode == ReaderMode.galleryRightToLeft;
|
||||||
|
|
||||||
List<Widget> imageWidgets = images.map((imageKey) {
|
List<Widget> imageWidgets = images.map((imageKey) {
|
||||||
ImageProvider imageProvider =
|
ImageProvider imageProvider =
|
||||||
_createImageProviderFromKey(imageKey, context);
|
_createImageProviderFromKey(imageKey, context);
|
||||||
@@ -239,6 +239,10 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
if (reverse) {
|
||||||
|
imageWidgets = imageWidgets.reversed.toList();
|
||||||
|
}
|
||||||
|
|
||||||
return axis == Axis.vertical
|
return axis == Axis.vertical
|
||||||
? Column(children: imageWidgets)
|
? Column(children: imageWidgets)
|
||||||
: Row(children: imageWidgets);
|
: Row(children: imageWidgets);
|
||||||
|
@@ -98,8 +98,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get maxPage =>
|
int get maxPage => ((images?.length ?? 1) / imagesPerPage).ceil();
|
||||||
((images?.length ?? 1) + imagesPerPage - 1) ~/ imagesPerPage;
|
|
||||||
|
|
||||||
ComicType get type => widget.type;
|
ComicType get type => widget.type;
|
||||||
|
|
||||||
|
@@ -206,37 +206,41 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Stack(
|
return LayoutBuilder(
|
||||||
children: [
|
builder: (context, constrains) {
|
||||||
Positioned.fill(child: buildLeft()),
|
return Stack(
|
||||||
Positioned(
|
children: [
|
||||||
left: offset,
|
Positioned.fill(child: buildLeft()),
|
||||||
width: MediaQuery.of(context).size.width,
|
Positioned(
|
||||||
top: 0,
|
left: offset,
|
||||||
bottom: 0,
|
width: constrains.maxWidth,
|
||||||
child: Listener(
|
top: 0,
|
||||||
onPointerDown: handlePointerDown,
|
bottom: 0,
|
||||||
child: AnimatedSwitcher(
|
child: Listener(
|
||||||
duration: const Duration(milliseconds: 200),
|
onPointerDown: handlePointerDown,
|
||||||
switchInCurve: Curves.fastOutSlowIn,
|
child: AnimatedSwitcher(
|
||||||
switchOutCurve: Curves.fastOutSlowIn,
|
duration: const Duration(milliseconds: 200),
|
||||||
transitionBuilder: (child, animation) {
|
switchInCurve: Curves.fastOutSlowIn,
|
||||||
var tween = Tween<Offset>(
|
switchOutCurve: Curves.fastOutSlowIn,
|
||||||
begin: const Offset(1, 0), end: const Offset(0, 0));
|
transitionBuilder: (child, animation) {
|
||||||
|
var tween = Tween<Offset>(
|
||||||
|
begin: const Offset(1, 0), end: const Offset(0, 0));
|
||||||
|
|
||||||
return SlideTransition(
|
return SlideTransition(
|
||||||
position: tween.animate(animation),
|
position: tween.animate(animation),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Material(
|
child: Material(
|
||||||
key: ValueKey(currentPage),
|
key: ValueKey(currentPage),
|
||||||
child: buildRight(),
|
child: buildRight(),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
),
|
],
|
||||||
)
|
);
|
||||||
],
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -303,7 +303,10 @@ class DesktopWebview {
|
|||||||
proxy: AppDio.proxy,
|
proxy: AppDio.proxy,
|
||||||
));
|
));
|
||||||
_webview!.addOnWebMessageReceivedCallback(onMessage);
|
_webview!.addOnWebMessageReceivedCallback(onMessage);
|
||||||
_webview!.setOnNavigation((s) => onNavigation?.call(s, this));
|
_webview!.setOnNavigation((s) {
|
||||||
|
s = s.substring(1, s.length - 1);
|
||||||
|
return onNavigation?.call(s, this);
|
||||||
|
});
|
||||||
_webview!.launch(initialUrl, triggerOnUrlRequestEvent: false);
|
_webview!.launch(initialUrl, triggerOnUrlRequestEvent: false);
|
||||||
_runTimer();
|
_runTimer();
|
||||||
_webview!.onClose.then((value) {
|
_webview!.onClose.then((value) {
|
||||||
|
10
pubspec.lock
10
pubspec.lock
@@ -1083,21 +1083,21 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "6.5.0"
|
version: "6.5.0"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: yaml
|
name: yaml
|
||||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.3"
|
||||||
zip_flutter:
|
zip_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: zip_flutter
|
name: zip_flutter
|
||||||
sha256: fe63ef9098bb2426b001adba2e28029820d71ce80cce957a36676bd6b3227245
|
sha256: bbf3160062610a43901b7ebbc6f6dd46519540f03a84027dc7b1fff399dda1ac
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.9"
|
version: "0.0.10"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.0 <4.0.0"
|
dart: ">=3.6.0 <4.0.0"
|
||||||
flutter: ">=3.27.3"
|
flutter: ">=3.27.3"
|
||||||
|
@@ -2,7 +2,7 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.2.2+122
|
version: 1.2.3+123
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.6.0 <4.0.0'
|
sdk: '>=3.6.0 <4.0.0'
|
||||||
@@ -48,7 +48,7 @@ dependencies:
|
|||||||
sliver_tools: ^0.2.12
|
sliver_tools: ^0.2.12
|
||||||
flutter_file_dialog: ^3.0.2
|
flutter_file_dialog: ^3.0.2
|
||||||
file_selector: ^1.0.3
|
file_selector: ^1.0.3
|
||||||
zip_flutter: ^0.0.9
|
zip_flutter: ^0.0.10
|
||||||
lodepng_flutter:
|
lodepng_flutter:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/venera-app/lodepng_flutter
|
url: https://github.com/venera-app/lodepng_flutter
|
||||||
@@ -75,6 +75,7 @@ dependencies:
|
|||||||
flex_seed_scheme: ^3.5.0
|
flex_seed_scheme: ^3.5.0
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
yaml: ^3.1.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user