mirror of
https://github.com/venera-app/venera.git
synced 2025-09-28 00:07:24 +00:00
add more js api & improve ui
This commit is contained in:
@@ -133,7 +133,7 @@ class _CategoryPage extends StatelessWidget {
|
||||
children: [
|
||||
if (data.enableRankingPage)
|
||||
buildTag("Ranking".tl, (p0, p1) {
|
||||
context.to(() => RankingPage(sourceKey: findComicSourceKey()));
|
||||
context.to(() => RankingPage(categoryKey: data.key));
|
||||
}),
|
||||
for (var buttonData in data.buttons)
|
||||
buildTag(buttonData.label.tl, (p0, p1) => buttonData.onTap())
|
||||
|
@@ -26,6 +26,7 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
||||
late final CategoryComicsData data;
|
||||
late final List<CategoryComicsOptions> options;
|
||||
late List<String> optionsValue;
|
||||
late String sourceKey;
|
||||
|
||||
void findData() {
|
||||
for (final source in ComicSource.all()) {
|
||||
@@ -40,6 +41,7 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
||||
return true;
|
||||
}).toList();
|
||||
optionsValue = options.map((e) => e.options.keys.first).toList();
|
||||
sourceKey = source.key;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -60,7 +62,7 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
||||
),
|
||||
body: ComicList(
|
||||
key: Key(widget.category + optionsValue.toString()),
|
||||
leadingSliver: buildOptions(),
|
||||
leadingSliver: buildOptions().toSliver(),
|
||||
loadPage: (i) => data.load(
|
||||
widget.category,
|
||||
widget.param,
|
||||
@@ -74,7 +76,7 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
||||
Widget buildOptionItem(
|
||||
String text, String value, int group, BuildContext context) {
|
||||
return OptionChip(
|
||||
text: text,
|
||||
text: text.ts(sourceKey),
|
||||
isSelected: value == optionsValue[group],
|
||||
onTap: () {
|
||||
if (value == optionsValue[group]) return;
|
||||
@@ -105,12 +107,10 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
||||
children.add(const SizedBox(height: 8));
|
||||
}
|
||||
}
|
||||
return SliverToBoxAdapter(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [...children, const Divider()],
|
||||
).paddingLeft(8).paddingRight(8),
|
||||
);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [...children, const Divider()],
|
||||
).paddingLeft(8).paddingRight(8);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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';
|
||||
@@ -10,8 +12,10 @@ import 'package:venera/foundation/image_provider/cached_image.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/foundation/res.dart';
|
||||
import 'package:venera/network/download.dart';
|
||||
import 'package:venera/pages/category_comics_page.dart';
|
||||
import 'package:venera/pages/favorites/favorites_page.dart';
|
||||
import 'package:venera/pages/reader/reader.dart';
|
||||
import 'package:venera/pages/search_result_page.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
import 'dart:math' as math;
|
||||
@@ -149,8 +153,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(comic.title, style: ts.s18),
|
||||
if (comic.subTitle != null) Text(comic.subTitle!, style: ts.s14),
|
||||
SelectableText(comic.title, style: ts.s18),
|
||||
if (comic.subTitle != null)
|
||||
SelectableText(comic.subTitle!, style: ts.s14),
|
||||
Text(
|
||||
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
||||
style: ts.s12,
|
||||
@@ -345,7 +350,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
for (var e in comic.tags.entries)
|
||||
buildWrap(
|
||||
children: [
|
||||
buildTag(text: e.key, isTitle: true),
|
||||
buildTag(text: e.key.ts(comicSource.key), isTitle: true),
|
||||
for (var tag in e.value)
|
||||
buildTag(text: tag, onTap: () => onTapTag(tag, e.key)),
|
||||
],
|
||||
@@ -407,7 +412,6 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement the _ComicPageActions mixin
|
||||
abstract mixin class _ComicPageActions {
|
||||
void update();
|
||||
|
||||
@@ -546,9 +550,74 @@ abstract mixin class _ComicPageActions {
|
||||
update();
|
||||
}
|
||||
|
||||
void onTapTag(String tag, String namespace) {}
|
||||
void onTapTag(String tag, String namespace) {
|
||||
var config = comicSource.handleClickTagEvent?.call(namespace, tag) ??
|
||||
{
|
||||
'action': 'search',
|
||||
'keyword': tag,
|
||||
};
|
||||
var context = App.mainNavigatorKey!.currentContext!;
|
||||
if (config['action'] == 'search') {
|
||||
context.to(() => SearchResultPage(
|
||||
text: config['keyword'] ?? '',
|
||||
sourceKey: comicSource.key,
|
||||
options: const [],
|
||||
));
|
||||
} else if (config['action'] == 'category') {
|
||||
context.to(
|
||||
() => CategoryComicsPage(
|
||||
category: config['keyword'] ?? '',
|
||||
categoryKey: comicSource.categoryData!.key,
|
||||
param: config['param'],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showMoreActions() {}
|
||||
void showMoreActions() {
|
||||
var context = App.rootContext;
|
||||
showMenuX(
|
||||
context,
|
||||
Offset(
|
||||
context.width - 16,
|
||||
context.padding.top,
|
||||
),
|
||||
[
|
||||
MenuEntry(
|
||||
icon: Icons.copy,
|
||||
text: "Copy Title".tl,
|
||||
onClick: () {
|
||||
Clipboard.setData(ClipboardData(text: comic.title));
|
||||
context.showMessage(message: "Copied".tl);
|
||||
},
|
||||
),
|
||||
MenuEntry(
|
||||
icon: Icons.copy_rounded,
|
||||
text: "Copy ID".tl,
|
||||
onClick: () {
|
||||
Clipboard.setData(ClipboardData(text: comic.id));
|
||||
context.showMessage(message: "Copied".tl);
|
||||
},
|
||||
),
|
||||
if (comic.url != null)
|
||||
MenuEntry(
|
||||
icon: Icons.link,
|
||||
text: "Copy URL".tl,
|
||||
onClick: () {
|
||||
Clipboard.setData(ClipboardData(text: comic.url!));
|
||||
context.showMessage(message: "Copied".tl);
|
||||
},
|
||||
),
|
||||
if (comic.url != null)
|
||||
MenuEntry(
|
||||
icon: Icons.open_in_browser,
|
||||
text: "Open in Browser".tl,
|
||||
onClick: () {
|
||||
launchUrlString(comic.url!);
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
void showComments() {
|
||||
showSideBar(
|
||||
@@ -1217,7 +1286,10 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: Appbar(title: Text("Download".tl), backgroundColor: context.colorScheme.surfaceContainerLow,),
|
||||
appBar: Appbar(
|
||||
title: Text("Download".tl),
|
||||
backgroundColor: context.colorScheme.surfaceContainerLow,
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -1233,14 +1305,14 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
|
||||
onChanged: widget.downloadedEps.contains(i)
|
||||
? null
|
||||
: (v) {
|
||||
setState(() {
|
||||
if (selected.contains(i)) {
|
||||
selected.remove(i);
|
||||
} else {
|
||||
selected.add(i);
|
||||
}
|
||||
});
|
||||
});
|
||||
setState(() {
|
||||
if (selected.contains(i)) {
|
||||
selected.remove(i);
|
||||
} else {
|
||||
selected.add(i);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@@ -146,12 +146,90 @@ class _BodyState extends State<_Body> {
|
||||
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'];
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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)),
|
||||
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;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void delete(ComicSource source) {
|
||||
showConfirmDialog(
|
||||
context: App.rootContext,
|
||||
@@ -280,11 +358,12 @@ class _BodyState extends State<_Body> {
|
||||
if (file == null) return;
|
||||
try {
|
||||
var fileName = file.name;
|
||||
var bytes = file.bytes!;
|
||||
var bytes = await File(file.path!).readAsBytes();
|
||||
var content = utf8.decode(bytes);
|
||||
await addSource(content, fileName);
|
||||
} catch (e) {
|
||||
} catch (e, s) {
|
||||
App.rootContext.showMessage(message: e.toString());
|
||||
Log.error("Add comic source", "$e\n$s");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -88,6 +88,9 @@ class _CommentsPageState extends State<CommentsPage> {
|
||||
withAppbar: false,
|
||||
);
|
||||
} else {
|
||||
var showAvatar = _comments!.any((e) {
|
||||
return e.avatar != null;
|
||||
});
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -109,6 +112,7 @@ class _CommentsPageState extends State<CommentsPage> {
|
||||
comment: _comments![index],
|
||||
source: widget.source,
|
||||
comic: widget.data,
|
||||
showAvatar: showAvatar,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -204,6 +208,7 @@ class _CommentTile extends StatefulWidget {
|
||||
required this.comment,
|
||||
required this.source,
|
||||
required this.comic,
|
||||
required this.showAvatar,
|
||||
});
|
||||
|
||||
final Comment comment;
|
||||
@@ -212,6 +217,8 @@ class _CommentTile extends StatefulWidget {
|
||||
|
||||
final ComicDetails comic;
|
||||
|
||||
final bool showAvatar;
|
||||
|
||||
@override
|
||||
State<_CommentTile> createState() => _CommentTileState();
|
||||
}
|
||||
@@ -239,7 +246,7 @@ class _CommentTileState extends State<_CommentTile> {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.comment.avatar != null)
|
||||
if (widget.showAvatar)
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
@@ -247,12 +254,14 @@ class _CommentTileState extends State<_CommentTile> {
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer),
|
||||
child: AnimatedImage(
|
||||
image: CachedImageProvider(
|
||||
widget.comment.avatar!,
|
||||
sourceKey: widget.source.key,
|
||||
),
|
||||
),
|
||||
child: widget.comment.avatar == null
|
||||
? null
|
||||
: AnimatedImage(
|
||||
image: CachedImageProvider(
|
||||
widget.comment.avatar!,
|
||||
sourceKey: widget.source.key,
|
||||
),
|
||||
),
|
||||
).paddingRight(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -313,6 +322,7 @@ class _CommentTileState extends State<_CommentTile> {
|
||||
source: widget.source,
|
||||
replyId: widget.comment.id,
|
||||
),
|
||||
showBarrier: false,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
@@ -376,7 +386,11 @@ class _CommentTileState extends State<_CommentTile> {
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
else if (isLiked)
|
||||
const Icon(Icons.favorite, size: 16)
|
||||
Icon(
|
||||
Icons.favorite,
|
||||
size: 16,
|
||||
color: context.useTextColor(Colors.red),
|
||||
)
|
||||
else
|
||||
const Icon(Icons.favorite_border, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
|
@@ -27,7 +27,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
||||
favPage.folderList = this;
|
||||
folders = LocalFavoritesManager().folderNames;
|
||||
networkFolders = ComicSource.all()
|
||||
.where((e) => e.favoriteData != null)
|
||||
.where((e) => e.favoriteData != null && e.isLogged)
|
||||
.map((e) => e.favoriteData!.key)
|
||||
.toList();
|
||||
super.initState();
|
||||
|
@@ -316,7 +316,7 @@ class _LocalState extends State<_Local> {
|
||||
Button.outlined(
|
||||
child: Row(
|
||||
children: [
|
||||
if(LocalManager().downloadingTasks.first.isPaused)
|
||||
if (LocalManager().downloadingTasks.first.isPaused)
|
||||
const Icon(Icons.pause_circle_outline, size: 18)
|
||||
else
|
||||
const _AnimatedDownloadingIcon(),
|
||||
@@ -813,11 +813,14 @@ class _AccountsWidgetState extends State<_AccountsWidget> {
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
runSpacing: 8,
|
||||
spacing: 8,
|
||||
children: accounts.map((e) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -825,7 +828,7 @@ class _AccountsWidgetState extends State<_AccountsWidget> {
|
||||
child: Text(e),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
).paddingHorizontal(16).paddingBottom(16),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@@ -5,9 +5,9 @@ import "package:venera/foundation/comic_source/comic_source.dart";
|
||||
import "package:venera/utils/translations.dart";
|
||||
|
||||
class RankingPage extends StatefulWidget {
|
||||
const RankingPage({required this.sourceKey, super.key});
|
||||
const RankingPage({required this.categoryKey, super.key});
|
||||
|
||||
final String sourceKey;
|
||||
final String categoryKey;
|
||||
|
||||
@override
|
||||
State<RankingPage> createState() => _RankingPageState();
|
||||
@@ -20,14 +20,14 @@ class _RankingPageState extends State<RankingPage> {
|
||||
|
||||
void findData() {
|
||||
for (final source in ComicSource.all()) {
|
||||
if (source.categoryData?.key == widget.sourceKey) {
|
||||
if (source.categoryData?.key == widget.categoryKey) {
|
||||
data = source.categoryComicsData!;
|
||||
options = data.rankingData!.options;
|
||||
optionValue = options.keys.first;
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw "${widget.sourceKey} Not found";
|
||||
throw "${widget.categoryKey} Not found";
|
||||
}
|
||||
|
||||
@override
|
||||
|
@@ -317,6 +317,14 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
||||
height = constrains.maxHeight;
|
||||
width = height * cacheSize.width / cacheSize.height;
|
||||
}
|
||||
} else {
|
||||
if(width == double.infinity) {
|
||||
width = constrains.maxWidth;
|
||||
height = 300;
|
||||
} else if(height == double.infinity) {
|
||||
height = constrains.maxHeight;
|
||||
width = 300;
|
||||
}
|
||||
}
|
||||
|
||||
if(_imageInfo != null){
|
||||
@@ -371,6 +379,7 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
backgroundColor: context.colorScheme.surfaceContainerLow,
|
||||
value: (_loadingProgress != null &&
|
||||
_loadingProgress!.expectedTotalBytes!=null &&
|
||||
_loadingProgress!.expectedTotalBytes! != 0)
|
||||
|
@@ -112,6 +112,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
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(
|
||||
@@ -119,7 +120,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
spacing: 8,
|
||||
children: option.options.entries.map((e) {
|
||||
return OptionChip(
|
||||
text: e.value.tl,
|
||||
text: e.value.ts(searchTarget),
|
||||
isSelected: options[i] == e.key,
|
||||
onTap: () {
|
||||
options[i] = e.key;
|
||||
@@ -127,7 +128,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
).paddingHorizontal(16));
|
||||
));
|
||||
}
|
||||
|
||||
return SliverToBoxAdapter(
|
||||
@@ -136,13 +137,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text("Search Options".tl),
|
||||
),
|
||||
...children,
|
||||
],
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
Reference in New Issue
Block a user