mirror of
https://github.com/venera-app/venera.git
synced 2025-09-28 08:17:25 +00:00
add star rating, network cache, advanced search option, loginWithCookies, loadNext; fix some minor issues
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -158,6 +159,8 @@ class _LoginPageState extends State<_LoginPage> {
|
||||
String password = "";
|
||||
bool loading = false;
|
||||
|
||||
final Map<String, String> _cookies = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -173,31 +176,44 @@ class _LoginPageState extends State<_LoginPage> {
|
||||
children: [
|
||||
Text("Login".tl, style: const TextStyle(fontSize: 24)),
|
||||
const SizedBox(height: 32),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Username".tl,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
enabled: widget.config.login != null,
|
||||
onChanged: (s) {
|
||||
username = s;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Password".tl,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
obscureText: true,
|
||||
enabled: widget.config.login != null,
|
||||
onChanged: (s) {
|
||||
password = s;
|
||||
},
|
||||
onSubmitted: (s) => login(),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
if (widget.config.login == null)
|
||||
if (widget.config.cookieFields == null)
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: "Username".tl,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
enabled: widget.config.login != null,
|
||||
onChanged: (s) {
|
||||
username = s;
|
||||
},
|
||||
).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(),
|
||||
).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: [
|
||||
@@ -214,7 +230,7 @@ class _LoginPageState extends State<_LoginPage> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (widget.config.loginWebsite != null)
|
||||
FilledButton(
|
||||
TextButton(
|
||||
onPressed: loginWithWebview,
|
||||
child: Text("Login with webview".tl),
|
||||
),
|
||||
@@ -240,71 +256,81 @@ class _LoginPageState extends State<_LoginPage> {
|
||||
}
|
||||
|
||||
void login() {
|
||||
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();
|
||||
}
|
||||
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;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}();
|
||||
validate(c);
|
||||
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;
|
||||
validate(c);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@@ -352,6 +352,18 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
ListTile(
|
||||
title: Text("Information".tl),
|
||||
),
|
||||
if (comic.stars != null)
|
||||
Row(
|
||||
children: [
|
||||
StarRating(
|
||||
value: comic.stars!,
|
||||
size: 24,
|
||||
onTap: starRating,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(comic.stars!.toStringAsFixed(2)),
|
||||
],
|
||||
).paddingLeft(16).paddingVertical(8),
|
||||
for (var e in comic.tags.entries)
|
||||
buildWrap(
|
||||
children: [
|
||||
@@ -641,6 +653,72 @@ abstract mixin class _ComicPageActions {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void starRating() {
|
||||
if (!comicSource.isLogged) {
|
||||
return;
|
||||
}
|
||||
var rating = 0.0;
|
||||
var isLoading = false;
|
||||
showDialog(
|
||||
context: App.rootContext,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setState) => SimpleDialog(
|
||||
title: const Text("Rating"),
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 210,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
RatingWidget(
|
||||
padding: 2,
|
||||
onRatingUpdate: (value) => rating = value,
|
||||
value: 1,
|
||||
selectable: true,
|
||||
size: 40,
|
||||
),
|
||||
const Spacer(),
|
||||
Button.filled(
|
||||
isLoading: isLoading,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
comicSource.starRatingFunc!
|
||||
(comic.id, rating.round())
|
||||
.then((value) {
|
||||
if (value.success) {
|
||||
App.rootContext
|
||||
.showMessage(message: "Success".tl);
|
||||
Navigator.of(dialogContext).pop();
|
||||
} else {
|
||||
App.rootContext
|
||||
.showMessage(message: value.errorMessage!);
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Text("Submit".tl),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionButton extends StatelessWidget {
|
||||
@@ -880,62 +958,88 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
|
||||
),
|
||||
),
|
||||
SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(childCount: thumbnails.length,
|
||||
(context, index) {
|
||||
if (index == thumbnails.length - 1 && error == null) {
|
||||
loadNext();
|
||||
}
|
||||
return Padding(
|
||||
padding: context.width < changePoint
|
||||
? const EdgeInsets.all(4)
|
||||
: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => state.read(null, index + 1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
child: AnimatedImage(
|
||||
image: CachedImageProvider(
|
||||
thumbnails[index],
|
||||
sourceKey: state.widget.sourceKey,
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
childCount: thumbnails.length,
|
||||
(context, index) {
|
||||
if (index == thumbnails.length - 1 && error == null) {
|
||||
loadNext();
|
||||
}
|
||||
var url = thumbnails[index];
|
||||
ImagePart? part;
|
||||
if (url.contains('@')) {
|
||||
var params = url.split('@')[1].split('&');
|
||||
url = url.split('@')[0];
|
||||
double? x1, y1, x2, y2;
|
||||
try {
|
||||
for (var p in params) {
|
||||
if (p.startsWith('x')) {
|
||||
var r = p.split('=')[1];
|
||||
x1 = double.parse(r.split('-')[0]);
|
||||
x2 = double.parse(r.split('-')[1]);
|
||||
}
|
||||
if (p.startsWith('y')) {
|
||||
var r = p.split('=')[1];
|
||||
y1 = double.parse(r.split('-')[0]);
|
||||
y2 = double.parse(r.split('-')[1]);
|
||||
}
|
||||
}
|
||||
} finally {}
|
||||
part = ImagePart(x1: x1, y1: y1, x2: x2, y2: y2);
|
||||
}
|
||||
return Padding(
|
||||
padding: context.width < changePoint
|
||||
? const EdgeInsets.all(4)
|
||||
: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => state.read(null, index + 1),
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
child: AnimatedImage(
|
||||
image: CachedImageProvider(
|
||||
url,
|
||||
sourceKey: state.widget.sourceKey,
|
||||
),
|
||||
fit: BoxFit.contain,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
part: part,
|
||||
),
|
||||
fit: BoxFit.contain,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text((index + 1).toString()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Text((index + 1).toString()),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 200,
|
||||
childAspectRatio: 0.65,
|
||||
),
|
||||
),
|
||||
if(error != null)
|
||||
if (error != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -1288,7 +1392,8 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
var res = await widget.comicSource.favoriteData!.addOrDelFavorite!(
|
||||
var res =
|
||||
await widget.comicSource.favoriteData!.addOrDelFavorite!(
|
||||
widget.cid,
|
||||
selected!,
|
||||
!addedFolders.contains(selected!),
|
||||
|
@@ -267,7 +267,7 @@ class _CommentTileState extends State<_CommentTile> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.comment.userName),
|
||||
Text(widget.comment.userName, style: ts.bold,),
|
||||
if (widget.comment.time != null)
|
||||
Text(widget.comment.time!, style: ts.s12),
|
||||
const SizedBox(height: 4),
|
||||
@@ -403,38 +403,45 @@ class _CommentTileState extends State<_CommentTile> {
|
||||
|
||||
int? voteStatus;
|
||||
|
||||
bool isVoteUp = false;
|
||||
bool isVotingUp = false;
|
||||
|
||||
bool isVoteDown = false;
|
||||
bool isVotingDown = false;
|
||||
|
||||
void vote(bool isUp) async {
|
||||
if (isVoteUp || isVoteDown) return;
|
||||
if (isVotingUp || isVotingDown) return;
|
||||
setState(() {
|
||||
if (isUp) {
|
||||
isVoteUp = true;
|
||||
isVotingUp = true;
|
||||
} else {
|
||||
isVoteDown = true;
|
||||
isVotingDown = true;
|
||||
}
|
||||
});
|
||||
var isCancel = (isUp && voteStatus == 1) || (!isUp && voteStatus == -1);
|
||||
var res = await widget.source.voteCommentFunc!(
|
||||
widget.comic.comicId,
|
||||
widget.comic.subId,
|
||||
widget.comment.id!,
|
||||
isUp,
|
||||
(isUp && voteStatus == 1) || (!isUp && voteStatus == -1),
|
||||
isCancel,
|
||||
);
|
||||
if (res.success) {
|
||||
if (isUp) {
|
||||
voteStatus = 1;
|
||||
if(isCancel) {
|
||||
voteStatus = 0;
|
||||
} else {
|
||||
voteStatus = -1;
|
||||
if (isUp) {
|
||||
voteStatus = 1;
|
||||
} else {
|
||||
voteStatus = -1;
|
||||
}
|
||||
}
|
||||
widget.comment.voteStatus = voteStatus;
|
||||
widget.comment.score = res.data ?? widget.comment.score;
|
||||
} else {
|
||||
context.showMessage(message: res.errorMessage ?? "Error");
|
||||
}
|
||||
setState(() {
|
||||
isVoteUp = false;
|
||||
isVoteDown = false;
|
||||
isVotingUp = false;
|
||||
isVotingDown = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -461,7 +468,7 @@ class _CommentTileState extends State<_CommentTile> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Button.icon(
|
||||
isLoading: isVoteUp,
|
||||
isLoading: isVotingUp,
|
||||
icon: const Icon(Icons.arrow_upward),
|
||||
size: 18,
|
||||
color: upColor,
|
||||
@@ -471,7 +478,7 @@ class _CommentTileState extends State<_CommentTile> {
|
||||
Text(widget.comment.score.toString()),
|
||||
const SizedBox(width: 4),
|
||||
Button.icon(
|
||||
isLoading: isVoteDown,
|
||||
isLoading: isVotingDown,
|
||||
icon: const Icon(Icons.arrow_downward),
|
||||
size: 18,
|
||||
color: downColor,
|
||||
|
@@ -179,7 +179,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> {
|
||||
Widget build(BuildContext context) {
|
||||
if (data.loadMultiPart != null) {
|
||||
return buildMultiPart();
|
||||
} else if (data.loadPage != null) {
|
||||
} else if (data.loadPage != null || data.loadNext != null) {
|
||||
return buildComicList();
|
||||
} else if (data.loadMixed != null) {
|
||||
return _MixedExplorePage(
|
||||
@@ -196,7 +196,8 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> {
|
||||
|
||||
Widget buildComicList() {
|
||||
return ComicList(
|
||||
loadPage: data.loadPage!,
|
||||
loadPage: data.loadPage,
|
||||
loadNext: data.loadNext,
|
||||
key: ValueKey(key),
|
||||
);
|
||||
}
|
||||
|
@@ -112,7 +112,8 @@ class _NormalFavoritePage extends StatelessWidget {
|
||||
),
|
||||
title: Text(data.title),
|
||||
),
|
||||
loadPage: (i) => data.loadComic(i),
|
||||
loadPage: data.loadComic == null ? null : (i) => data.loadComic!(i),
|
||||
loadNext: data.loadNext == null ? null : (next) => data.loadNext!(next),
|
||||
menuBuilder: (comic) {
|
||||
return [
|
||||
MenuEntry(
|
||||
@@ -393,7 +394,8 @@ class _FolderTile extends StatelessWidget {
|
||||
return StatefulBuilder(builder: (context, setState) {
|
||||
return ContentDialog(
|
||||
title: "Delete".tl,
|
||||
content: Text("Are you sure you want to delete this folder?".tl).paddingHorizontal(16),
|
||||
content: Text("Are you sure you want to delete this folder?".tl)
|
||||
.paddingHorizontal(16),
|
||||
actions: [
|
||||
Button.filled(
|
||||
isLoading: loading,
|
||||
@@ -516,7 +518,11 @@ class _FavoriteFolder extends StatelessWidget {
|
||||
errorLeading: Appbar(
|
||||
title: Text(title),
|
||||
),
|
||||
loadPage: (i) => data.loadComic(i, folderID),
|
||||
loadPage:
|
||||
data.loadComic == null ? null : (i) => data.loadComic!(i, folderID),
|
||||
loadNext: data.loadNext == null
|
||||
? null
|
||||
: (next) => data.loadNext!(next, folderID),
|
||||
menuBuilder: (comic) {
|
||||
return [
|
||||
MenuEntry(
|
||||
|
@@ -46,7 +46,12 @@ class _RankingPageState extends State<RankingPage> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: ComicList(
|
||||
loadPage: (i) => data.rankingData!.load(optionValue, i),
|
||||
loadPage: data.rankingData!.load == null
|
||||
? null
|
||||
: (i) => data.rankingData!.load!(optionValue, i),
|
||||
loadNext: data.rankingData!.loadWithNext == null
|
||||
? null
|
||||
: (i) => data.rankingData!.loadWithNext!(optionValue, i),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:venera/components/components.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
@@ -24,6 +26,8 @@ class _SearchPageState extends State<SearchPage> {
|
||||
|
||||
String searchTarget = "";
|
||||
|
||||
var focusNode = FocusNode();
|
||||
|
||||
var options = <String>[];
|
||||
|
||||
void update() {
|
||||
@@ -137,6 +141,12 @@ class _SearchPageState extends State<SearchPage> {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -152,6 +162,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
onChanged: (s) {
|
||||
findSuggestions();
|
||||
},
|
||||
focusNode: focusNode,
|
||||
);
|
||||
if (suggestions.isNotEmpty) {
|
||||
yield buildSuggestions(context);
|
||||
@@ -186,6 +197,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
onTap: () {
|
||||
setState(() {
|
||||
searchTarget = e.key;
|
||||
useDefaultOptions();
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -197,6 +209,13 @@ class _SearchPageState extends State<SearchPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void useDefaultOptions() {
|
||||
final searchOptions =
|
||||
ComicSource.find(searchTarget)!.searchPageData!.searchOptions ??
|
||||
<SearchOptions>[];
|
||||
options = searchOptions.map((e) => e.defaultValue).toList();
|
||||
}
|
||||
|
||||
Widget buildSearchOptions() {
|
||||
var children = <Widget>[];
|
||||
|
||||
@@ -204,30 +223,21 @@ class _SearchPageState extends State<SearchPage> {
|
||||
ComicSource.find(searchTarget)!.searchPageData!.searchOptions ??
|
||||
<SearchOptions>[];
|
||||
if (searchOptions.length != options.length) {
|
||||
options = searchOptions.map((e) => e.defaultValue).toList();
|
||||
useDefaultOptions();
|
||||
}
|
||||
if (searchOptions.isEmpty) {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
for (int i = 0; i < searchOptions.length; i++) {
|
||||
final option = searchOptions[i];
|
||||
children.add(ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(option.label.tl),
|
||||
));
|
||||
children.add(Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: option.options.entries.map((e) {
|
||||
return OptionChip(
|
||||
text: e.value.ts(searchTarget),
|
||||
isSelected: options[i] == e.key,
|
||||
onTap: () {
|
||||
options[i] = e.key;
|
||||
update();
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
children.add(SearchOptionWidget(
|
||||
option: option,
|
||||
value: options[i],
|
||||
onChanged: (value) {
|
||||
options[i] = value;
|
||||
update();
|
||||
},
|
||||
sourceKey: searchTarget,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -336,7 +346,9 @@ class _SearchPageState extends State<SearchPage> {
|
||||
} else {
|
||||
controller.text += "$text ";
|
||||
}
|
||||
suggestions.clear();
|
||||
update();
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
|
||||
bool showMethod = MediaQuery.of(context).size.width < 600;
|
||||
@@ -444,3 +456,77 @@ class _SearchPageState extends State<SearchPage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SearchOptionWidget extends StatelessWidget {
|
||||
const SearchOptionWidget({
|
||||
super.key,
|
||||
required this.option,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
required this.sourceKey,
|
||||
});
|
||||
|
||||
final SearchOptions option;
|
||||
|
||||
final String value;
|
||||
|
||||
final void Function(String) onChanged;
|
||||
|
||||
final String sourceKey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(option.label.ts(sourceKey)),
|
||||
),
|
||||
if(option.type == 'select')
|
||||
Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: option.options.entries.map((e) {
|
||||
return OptionChip(
|
||||
text: e.value.ts(sourceKey),
|
||||
isSelected: value == e.key,
|
||||
onTap: () {
|
||||
onChanged(e.key);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
if(option.type == 'multi-select')
|
||||
Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: option.options.entries.map((e) {
|
||||
return OptionChip(
|
||||
text: e.value.ts(sourceKey),
|
||||
isSelected: (jsonDecode(value) as List).contains(e.key),
|
||||
onTap: () {
|
||||
var list = jsonDecode(value) as List;
|
||||
if(list.contains(e.key)) {
|
||||
list.remove(e.key);
|
||||
} else {
|
||||
list.add(e.key);
|
||||
}
|
||||
onChanged(jsonEncode(list));
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
if(option.type == 'dropdown')
|
||||
Select(
|
||||
current: option.options[value],
|
||||
values: option.options.values.toList(),
|
||||
onTap: (index) {
|
||||
onChanged(option.options.keys.elementAt(index));
|
||||
},
|
||||
minWidth: 96,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||
import 'package:venera/foundation/state_controller.dart';
|
||||
import 'package:venera/pages/search_page.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
import 'package:venera/utils/tags_translation.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
@@ -87,12 +88,27 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
);
|
||||
sourceKey = widget.sourceKey;
|
||||
options = widget.options;
|
||||
validateOptions();
|
||||
text = widget.text;
|
||||
appdata.addSearchHistory(text);
|
||||
suggestionsController = _SuggestionsController(controller);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void validateOptions() {
|
||||
var source = ComicSource.find(sourceKey);
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
var searchOptions = source.searchPageData!.searchOptions;
|
||||
if (searchOptions == null) {
|
||||
return;
|
||||
}
|
||||
if (options.length != searchOptions.length) {
|
||||
options = searchOptions.map((e) => e.defaultValue).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ComicList(
|
||||
@@ -422,25 +438,15 @@ class _SearchSettingsDialogState extends State<_SearchSettingsDialog> {
|
||||
}
|
||||
for (int i = 0; i < searchOptions.length; i++) {
|
||||
final option = searchOptions[i];
|
||||
children.add(ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(option.label.tl),
|
||||
));
|
||||
children.add(Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: option.options.entries.map((e) {
|
||||
return OptionChip(
|
||||
text: e.value.ts(searchTarget),
|
||||
isSelected: options[i] == e.key,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
options[i] = e.key;
|
||||
});
|
||||
onChanged();
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
children.add(SearchOptionWidget(
|
||||
option: option,
|
||||
value: options[i],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
options[i] = value;
|
||||
});
|
||||
},
|
||||
sourceKey: searchTarget,
|
||||
));
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user