mirror of
https://github.com/venera-app/venera.git
synced 2025-09-28 08:17:25 +00:00
add loginWithWebview, mixed explore page, app links, html node api;
improve ui
This commit is contained in:
@@ -5,6 +5,8 @@ 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 {
|
||||
@@ -60,18 +62,13 @@ class AccountsPage extends StatelessWidget {
|
||||
title: Text("Log in".tl),
|
||||
trailing: const Icon(Icons.arrow_right),
|
||||
onTap: () async {
|
||||
if (element.account!.onLogin != null) {
|
||||
await element.account!.onLogin!(context);
|
||||
}
|
||||
if (element.account!.login != null && context.mounted) {
|
||||
await context.to(
|
||||
() => _LoginPage(
|
||||
login: element.account!.login!,
|
||||
registerWebsite: element.account!.registerWebsite,
|
||||
),
|
||||
);
|
||||
element.saveData();
|
||||
}
|
||||
await context.to(
|
||||
() => _LoginPage(
|
||||
config: element.account!,
|
||||
source: element,
|
||||
),
|
||||
);
|
||||
element.saveData();
|
||||
logic.update();
|
||||
},
|
||||
);
|
||||
@@ -121,7 +118,7 @@ class AccountsPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
yield ListTile(
|
||||
title: Text("Exit".tl),
|
||||
title: Text("Log out".tl),
|
||||
onTap: () {
|
||||
element.data["account"] = null;
|
||||
element.account?.logout();
|
||||
@@ -146,11 +143,11 @@ class AccountsPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _LoginPage extends StatefulWidget {
|
||||
const _LoginPage({required this.login, this.registerWebsite});
|
||||
const _LoginPage({required this.config, required this.source});
|
||||
|
||||
final LoginFunction login;
|
||||
final AccountConfig config;
|
||||
|
||||
final String? registerWebsite;
|
||||
final ComicSource source;
|
||||
|
||||
@override
|
||||
State<_LoginPage> createState() => _LoginPageState();
|
||||
@@ -181,6 +178,7 @@ class _LoginPageState extends State<_LoginPage> {
|
||||
labelText: "Username".tl,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
enabled: widget.config.login != null,
|
||||
onChanged: (s) {
|
||||
username = s;
|
||||
},
|
||||
@@ -192,21 +190,39 @@ class _LoginPageState extends State<_LoginPage> {
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
obscureText: true,
|
||||
enabled: widget.config.login != null,
|
||||
onChanged: (s) {
|
||||
password = s;
|
||||
},
|
||||
onSubmitted: (s) => login(),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Button.filled(
|
||||
isLoading: loading,
|
||||
onPressed: login,
|
||||
child: Text("Continue".tl),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
if (widget.registerWebsite != null)
|
||||
if (widget.config.login == 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)
|
||||
FilledButton(
|
||||
onPressed: loginWithWebview,
|
||||
child: Text("Login with webview".tl),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (widget.config.registerWebsite != null)
|
||||
TextButton(
|
||||
onPressed: () => launchUrlString(widget.registerWebsite!),
|
||||
onPressed: () =>
|
||||
launchUrlString(widget.config.registerWebsite!),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -235,7 +251,7 @@ class _LoginPageState extends State<_LoginPage> {
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
widget.login(username, password).then((value) {
|
||||
widget.config.login!(username, password).then((value) {
|
||||
if (value.error) {
|
||||
context.showMessage(message: value.errorMessage!);
|
||||
setState(() {
|
||||
@@ -248,4 +264,54 @@ class _LoginPageState extends State<_LoginPage> {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void loginWithWebview() async {
|
||||
var url = widget.config.loginWebsite!;
|
||||
var title = '';
|
||||
bool success = false;
|
||||
await context.to(
|
||||
() => AppWebview(
|
||||
initialUrl: widget.config.loginWebsite!,
|
||||
onNavigation: (u, c) {
|
||||
url = u;
|
||||
print(url);
|
||||
() async {
|
||||
if (widget.config.checkLoginStatus != null) {
|
||||
if (widget.config.checkLoginStatus!(url, title)) {
|
||||
var cookies = (await c.getCookies(url)) ?? [];
|
||||
SingleInstanceCookieJar.instance?.saveFromResponse(
|
||||
Uri.parse(url),
|
||||
cookies,
|
||||
);
|
||||
success = true;
|
||||
App.mainNavigatorKey?.currentContext?.pop();
|
||||
}
|
||||
}
|
||||
}();
|
||||
return false;
|
||||
},
|
||||
onTitleChange: (t, c) {
|
||||
() async {
|
||||
if (widget.config.checkLoginStatus != null) {
|
||||
if (widget.config.checkLoginStatus!(url, title)) {
|
||||
var cookies = (await c.getCookies(url)) ?? [];
|
||||
SingleInstanceCookieJar.instance?.saveFromResponse(
|
||||
Uri.parse(url),
|
||||
cookies,
|
||||
);
|
||||
success = true;
|
||||
App.mainNavigatorKey?.currentContext?.pop();
|
||||
}
|
||||
}
|
||||
}();
|
||||
title = t;
|
||||
},
|
||||
),
|
||||
);
|
||||
if (success) {
|
||||
widget.source.data['account'] = 'ok';
|
||||
widget.source.saveData();
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -134,6 +134,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
const SizedBox(width: 16),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
height: 144,
|
||||
@@ -369,11 +370,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
buildTag(text: comic.uploadTime!),
|
||||
],
|
||||
),
|
||||
if (comic.uploadTime != null)
|
||||
if (comic.updateTime != null)
|
||||
buildWrap(
|
||||
children: [
|
||||
buildTag(text: 'Update Time'.tl, isTitle: true),
|
||||
buildTag(text: comicSource.name),
|
||||
buildTag(text: comic.updateTime!),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -1120,13 +1121,18 @@ class _FavoritePanelState extends State<_FavoritePanel> {
|
||||
cid: widget.cid,
|
||||
comicSource: comicSource,
|
||||
isFavorite: widget.isFavorite,
|
||||
onFavorite: widget.onFavorite,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NetworkFavorites extends StatefulWidget {
|
||||
const _NetworkFavorites(
|
||||
{required this.cid, required this.comicSource, required this.isFavorite});
|
||||
const _NetworkFavorites({
|
||||
required this.cid,
|
||||
required this.comicSource,
|
||||
required this.isFavorite,
|
||||
required this.onFavorite,
|
||||
});
|
||||
|
||||
final String cid;
|
||||
|
||||
@@ -1134,6 +1140,8 @@ class _NetworkFavorites extends StatefulWidget {
|
||||
|
||||
final bool? isFavorite;
|
||||
|
||||
final void Function(bool) onFavorite;
|
||||
|
||||
@override
|
||||
State<_NetworkFavorites> createState() => _NetworkFavoritesState();
|
||||
}
|
||||
@@ -1167,7 +1175,10 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> {
|
||||
var res = await widget.comicSource.favoriteData!
|
||||
.addOrDelFavorite!(widget.cid, '', !isFavorite);
|
||||
if (res.success) {
|
||||
widget.onFavorite(!isFavorite);
|
||||
context.pop();
|
||||
App.rootContext.showMessage(
|
||||
message: isFavorite ? "Removed".tl : "Added".tl);
|
||||
} else {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
|
@@ -187,13 +187,6 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> {
|
||||
comicSourceKey,
|
||||
key: ValueKey(key),
|
||||
);
|
||||
} else if (data.overridePageBuilder != null) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
return data.overridePageBuilder!(context);
|
||||
},
|
||||
key: ValueKey(key),
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: Text("Empty Page"),
|
||||
|
@@ -121,6 +121,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
||||
"${e.time} | ${comicSource?.name ?? "Unknown"}",
|
||||
comicSource?.key ?? "Unknown",
|
||||
null,
|
||||
null,
|
||||
);
|
||||
}).toList(),
|
||||
menuBuilder: (c) {
|
||||
@@ -202,6 +203,7 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
|
||||
"${e.time} | ${comicSource?.name ?? "Unknown"}",
|
||||
comicSource?.key ?? "Unknown",
|
||||
null,
|
||||
null,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@@ -87,6 +87,7 @@ class _HistoryPageState extends State<HistoryPage> {
|
||||
getDescription(e),
|
||||
e.type.comicSource?.key ?? "Invalid",
|
||||
null,
|
||||
null,
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
|
@@ -94,7 +94,12 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
const BackButton(),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(context.reader.widget.name, style: ts.s18),
|
||||
child: Text(
|
||||
context.reader.widget.name,
|
||||
style: ts.s18,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Tooltip(
|
||||
@@ -356,7 +361,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
context,
|
||||
ReaderSettings(
|
||||
onChanged: (key) {
|
||||
if(key == "readerMode") {
|
||||
if (key == "readerMode") {
|
||||
context.reader.mode = ReaderMode.fromKey(appdata.settings[key]);
|
||||
App.rootContext.pop();
|
||||
}
|
||||
|
@@ -27,7 +27,15 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
|
||||
late List<String> options;
|
||||
|
||||
void search([String? text]) {}
|
||||
late String text;
|
||||
|
||||
void search([String? text]) {
|
||||
if (text != null) {
|
||||
setState(() {
|
||||
this.text = text;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -37,12 +45,14 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
);
|
||||
sourceKey = widget.sourceKey;
|
||||
options = widget.options;
|
||||
text = widget.text;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ComicList(
|
||||
key: Key(text + options.toString()),
|
||||
errorLeading: AppSearchBar(
|
||||
controller: controller,
|
||||
),
|
||||
@@ -52,7 +62,7 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
loadPage: (i) {
|
||||
var source = ComicSource.find(sourceKey);
|
||||
return source!.searchPageData!.loadPage!(
|
||||
controller.initialText,
|
||||
text,
|
||||
i,
|
||||
options,
|
||||
);
|
||||
|
@@ -11,11 +11,13 @@ import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/network/app_dio.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
import 'dart:io' as io;
|
||||
|
||||
export 'package:flutter_inappwebview/flutter_inappwebview.dart' show WebUri, URLRequest;
|
||||
export 'package:flutter_inappwebview/flutter_inappwebview.dart'
|
||||
show WebUri, URLRequest;
|
||||
|
||||
extension WebviewExtension on InAppWebViewController{
|
||||
Future<Map<String, String>?> getCookies(String url) async{
|
||||
extension WebviewExtension on InAppWebViewController {
|
||||
Future<List<io.Cookie>?> getCookies(String url) async {
|
||||
if(url.contains("https://")){
|
||||
url.replaceAll("https://", "");
|
||||
}
|
||||
@@ -24,18 +26,20 @@ extension WebviewExtension on InAppWebViewController{
|
||||
}
|
||||
CookieManager cookieManager = CookieManager.instance();
|
||||
final cookies = await cookieManager.getCookies(url: WebUri(url));
|
||||
Map<String, String> res = {};
|
||||
for(var cookie in cookies){
|
||||
res[cookie.name] = cookie.value;
|
||||
var res = <io.Cookie>[];
|
||||
for (var cookie in cookies) {
|
||||
var c = io.Cookie(cookie.name, cookie.value);
|
||||
c.domain = cookie.domain;
|
||||
res.add(c);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Future<String?> getUA() async{
|
||||
Future<String?> getUA() async {
|
||||
var res = await evaluateJavascript(source: "navigator.userAgent");
|
||||
if(res is String){
|
||||
if(res[0] == "'" || res[0] == "\"") {
|
||||
res = res.substring(1, res.length-1);
|
||||
if (res is String) {
|
||||
if (res[0] == "'" || res[0] == "\"") {
|
||||
res = res.substring(1, res.length - 1);
|
||||
}
|
||||
}
|
||||
return res is String ? res : null;
|
||||
@@ -43,17 +47,27 @@ extension WebviewExtension on InAppWebViewController{
|
||||
}
|
||||
|
||||
class AppWebview extends StatefulWidget {
|
||||
const AppWebview({required this.initialUrl, this.onTitleChange,
|
||||
this.onNavigation, this.singlePage = false, this.onStarted, super.key});
|
||||
const AppWebview(
|
||||
{required this.initialUrl,
|
||||
this.onTitleChange,
|
||||
this.onNavigation,
|
||||
this.singlePage = false,
|
||||
this.onStarted,
|
||||
this.onLoadStop,
|
||||
super.key});
|
||||
|
||||
final String initialUrl;
|
||||
|
||||
final void Function(String title, InAppWebViewController controller)? onTitleChange;
|
||||
final void Function(String title, InAppWebViewController controller)?
|
||||
onTitleChange;
|
||||
|
||||
final bool Function(String url)? onNavigation;
|
||||
final bool Function(String url, InAppWebViewController controller)?
|
||||
onNavigation;
|
||||
|
||||
final void Function(InAppWebViewController controller)? onStarted;
|
||||
|
||||
final void Function(InAppWebViewController controller)? onLoadStop;
|
||||
|
||||
final bool singlePage;
|
||||
|
||||
@override
|
||||
@@ -74,35 +88,42 @@ class _AppWebviewState extends State<AppWebview> {
|
||||
message: "More",
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
onPressed: (){
|
||||
showMenu(context: context, position: RelativeRect.fromLTRB(
|
||||
MediaQuery.of(context).size.width,
|
||||
0,
|
||||
MediaQuery.of(context).size.width,
|
||||
0
|
||||
), items: [
|
||||
PopupMenuItem(
|
||||
child: Text("Open in browser".tl),
|
||||
onTap: () async => launchUrlString((await controller?.getUrl())!.path),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text("Copy link".tl),
|
||||
onTap: () async => Clipboard.setData(ClipboardData(text: (await controller?.getUrl())!.path)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text("Reload".tl),
|
||||
onTap: () => controller?.reload(),
|
||||
),
|
||||
]);
|
||||
onPressed: () {
|
||||
showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(
|
||||
MediaQuery.of(context).size.width,
|
||||
0,
|
||||
MediaQuery.of(context).size.width,
|
||||
0),
|
||||
items: [
|
||||
PopupMenuItem(
|
||||
child: Text("Open in browser".tl),
|
||||
onTap: () async =>
|
||||
launchUrlString((await controller?.getUrl())!.path),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text("Copy link".tl),
|
||||
onTap: () async => Clipboard.setData(ClipboardData(
|
||||
text: (await controller?.getUrl())!.path)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text("Reload".tl),
|
||||
onTap: () => controller?.reload(),
|
||||
),
|
||||
]);
|
||||
},
|
||||
),
|
||||
)
|
||||
];
|
||||
|
||||
Widget body = InAppWebView(
|
||||
initialSettings: InAppWebViewSettings(
|
||||
isInspectable: true,
|
||||
),
|
||||
initialUrlRequest: URLRequest(url: WebUri(widget.initialUrl)),
|
||||
onTitleChanged: (c, t){
|
||||
if(mounted){
|
||||
onTitleChanged: (c, t) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
title = t ?? "Webview";
|
||||
});
|
||||
@@ -110,19 +131,24 @@ class _AppWebviewState extends State<AppWebview> {
|
||||
widget.onTitleChange?.call(title, controller!);
|
||||
},
|
||||
shouldOverrideUrlLoading: (c, r) async {
|
||||
var res = widget.onNavigation?.call(r.request.url?.toString() ?? "") ?? false;
|
||||
if(res) {
|
||||
var res =
|
||||
widget.onNavigation?.call(r.request.url?.toString() ?? "", c) ??
|
||||
false;
|
||||
if (res) {
|
||||
return NavigationActionPolicy.CANCEL;
|
||||
} else {
|
||||
return NavigationActionPolicy.ALLOW;
|
||||
}
|
||||
},
|
||||
onWebViewCreated: (c){
|
||||
onWebViewCreated: (c) {
|
||||
controller = c;
|
||||
widget.onStarted?.call(c);
|
||||
},
|
||||
onProgressChanged: (c, p){
|
||||
if(mounted){
|
||||
onLoadStop: (c, r) {
|
||||
widget.onLoadStop?.call(c);
|
||||
},
|
||||
onProgressChanged: (c, p) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_progress = p / 100;
|
||||
});
|
||||
@@ -133,19 +159,22 @@ class _AppWebviewState extends State<AppWebview> {
|
||||
body = Stack(
|
||||
children: [
|
||||
Positioned.fill(child: body),
|
||||
if(_progress < 1.0)
|
||||
const Positioned.fill(child: Center(
|
||||
child: CircularProgressIndicator()))
|
||||
if (_progress < 1.0)
|
||||
const Positioned.fill(
|
||||
child: Center(child: CircularProgressIndicator()))
|
||||
],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: Appbar(
|
||||
title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis,),
|
||||
actions: actions,
|
||||
),
|
||||
body: body
|
||||
);
|
||||
appBar: Appbar(
|
||||
title: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
actions: actions,
|
||||
),
|
||||
body: body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,13 +191,12 @@ class DesktopWebview {
|
||||
|
||||
final void Function()? onClose;
|
||||
|
||||
DesktopWebview({
|
||||
required this.initialUrl,
|
||||
this.onTitleChange,
|
||||
this.onNavigation,
|
||||
this.onStarted,
|
||||
this.onClose
|
||||
});
|
||||
DesktopWebview(
|
||||
{required this.initialUrl,
|
||||
this.onTitleChange,
|
||||
this.onNavigation,
|
||||
this.onStarted,
|
||||
this.onClose});
|
||||
|
||||
Webview? _webview;
|
||||
|
||||
@@ -178,8 +206,8 @@ class DesktopWebview {
|
||||
|
||||
void onMessage(String message) {
|
||||
var json = jsonDecode(message);
|
||||
if(json is Map){
|
||||
if(json["id"] == "document_created"){
|
||||
if (json is Map) {
|
||||
if (json["id"] == "document_created") {
|
||||
title = json["data"]["title"];
|
||||
_ua = json["data"]["ua"];
|
||||
onTitleChange?.call(title!, this);
|
||||
@@ -210,14 +238,15 @@ class DesktopWebview {
|
||||
}
|
||||
collect();
|
||||
''';
|
||||
if(_webview != null) {
|
||||
if (_webview != null) {
|
||||
onMessage(await evaluateJavascript(js) ?? '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void open() async {
|
||||
_webview = await WebviewWindow.create(configuration: CreateConfiguration(
|
||||
_webview = await WebviewWindow.create(
|
||||
configuration: CreateConfiguration(
|
||||
useWindowPositionAndSize: true,
|
||||
userDataFolderWindows: "${App.dataPath}\\webview",
|
||||
title: "webview",
|
||||
@@ -242,11 +271,11 @@ class DesktopWebview {
|
||||
return _webview!.evaluateJavaScript(source);
|
||||
}
|
||||
|
||||
Future<Map<String, String>> getCookies(String url) async{
|
||||
Future<Map<String, String>> getCookies(String url) async {
|
||||
var allCookies = await _webview!.getAllCookies();
|
||||
var res = <String, String>{};
|
||||
for(var c in allCookies) {
|
||||
if(_cookieMatch(url, c.domain)){
|
||||
for (var c in allCookies) {
|
||||
if (_cookieMatch(url, c.domain)) {
|
||||
res[_removeCode0(c.name)] = _removeCode0(c.value);
|
||||
}
|
||||
}
|
||||
@@ -279,4 +308,4 @@ class DesktopWebview {
|
||||
_webview?.close();
|
||||
_webview = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user