mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
add more js api & improve ui
This commit is contained in:
189
assets/init.js
189
assets/init.js
@@ -1,5 +1,35 @@
|
|||||||
|
/*
|
||||||
|
Venera JavaScript Library
|
||||||
|
*/
|
||||||
|
|
||||||
/// encode, decode, hash, decrypt
|
/// encode, decode, hash, decrypt
|
||||||
let Convert = {
|
let Convert = {
|
||||||
|
/**
|
||||||
|
* @param str {string}
|
||||||
|
* @returns {ArrayBuffer}
|
||||||
|
*/
|
||||||
|
encodeUtf8: (str) => {
|
||||||
|
return sendMessage({
|
||||||
|
method: "convert",
|
||||||
|
type: "utf8",
|
||||||
|
value: str,
|
||||||
|
isEncode: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param value {ArrayBuffer}
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
decodeUtf8: (value) => {
|
||||||
|
return sendMessage({
|
||||||
|
method: "convert",
|
||||||
|
type: "utf8",
|
||||||
|
value: value,
|
||||||
|
isEncode: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ArrayBuffer} value
|
* @param {ArrayBuffer} value
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
@@ -78,6 +108,41 @@ let Convert = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param key {ArrayBuffer}
|
||||||
|
* @param value {ArrayBuffer}
|
||||||
|
* @param hash {string} - md5, sha1, sha256, sha512
|
||||||
|
* @returns {ArrayBuffer}
|
||||||
|
*/
|
||||||
|
hmac: (key, value, hash) => {
|
||||||
|
return sendMessage({
|
||||||
|
method: "convert",
|
||||||
|
type: "hmac",
|
||||||
|
value: value,
|
||||||
|
key: key,
|
||||||
|
hash: hash,
|
||||||
|
isEncode: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param key {ArrayBuffer}
|
||||||
|
* @param value {ArrayBuffer}
|
||||||
|
* @param hash {string} - md5, sha1, sha256, sha512
|
||||||
|
* @returns {string} - hex string
|
||||||
|
*/
|
||||||
|
hmacString: (key, value, hash) => {
|
||||||
|
return sendMessage({
|
||||||
|
method: "convert",
|
||||||
|
type: "hmac",
|
||||||
|
value: value,
|
||||||
|
key: key,
|
||||||
|
hash: hash,
|
||||||
|
isEncode: true,
|
||||||
|
isString: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ArrayBuffer} value
|
* @param {ArrayBuffer} value
|
||||||
* @param {ArrayBuffer} key
|
* @param {ArrayBuffer} key
|
||||||
@@ -160,6 +225,21 @@ let Convert = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a time-based uuid
|
||||||
|
*
|
||||||
|
* Note: the engine will generate a new uuid every time it is called
|
||||||
|
*
|
||||||
|
* To get the same uuid, please save it to the local storage
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function createUuid() {
|
||||||
|
return sendMessage({
|
||||||
|
method: "uuid"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function randomInt(min, max) {
|
function randomInt(min, max) {
|
||||||
return sendMessage({
|
return sendMessage({
|
||||||
method: 'random',
|
method: 'random',
|
||||||
@@ -520,6 +600,91 @@ let console = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a comic object
|
||||||
|
* @param id {string}
|
||||||
|
* @param title {string}
|
||||||
|
* @param subtitle {string}
|
||||||
|
* @param cover {string}
|
||||||
|
* @param tags {string[]}
|
||||||
|
* @param description {string}
|
||||||
|
* @param maxPage {number | null}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function Comic({id, title, subtitle, cover, tags, description, maxPage}) {
|
||||||
|
this.id = id;
|
||||||
|
this.title = title;
|
||||||
|
this.subtitle = subtitle;
|
||||||
|
this.cover = cover;
|
||||||
|
this.tags = tags;
|
||||||
|
this.description = description;
|
||||||
|
this.maxPage = maxPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a comic details object
|
||||||
|
* @param title {string}
|
||||||
|
* @param cover {string}
|
||||||
|
* @param description {string | null}
|
||||||
|
* @param tags {Map<string, string[]> | {} | null}
|
||||||
|
* @param chapters {Map<string, string> | {} | null} - key: chapter id, value: chapter title
|
||||||
|
* @param isFavorite {boolean | null} - favorite status. If the comic source supports multiple folders, this field should be null
|
||||||
|
* @param subId {string | null} - a param which is passed to comments api
|
||||||
|
* @param thumbnails {string[] | null} - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails
|
||||||
|
* @param recommend {Comic[] | null} - related comics
|
||||||
|
* @param commentCount {number | null}
|
||||||
|
* @param likesCount {number | null}
|
||||||
|
* @param isLiked {boolean | null}
|
||||||
|
* @param uploader {string | null}
|
||||||
|
* @param updateTime {string | null}
|
||||||
|
* @param uploadTime {string | null}
|
||||||
|
* @param url {string | null}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function ComicDetails({title, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url}) {
|
||||||
|
this.title = title;
|
||||||
|
this.cover = cover;
|
||||||
|
this.description = description;
|
||||||
|
this.tags = tags;
|
||||||
|
this.chapters = chapters;
|
||||||
|
this.isFavorite = isFavorite;
|
||||||
|
this.subId = subId;
|
||||||
|
this.thumbnails = thumbnails;
|
||||||
|
this.recommend = recommend;
|
||||||
|
this.commentCount = commentCount;
|
||||||
|
this.likesCount = likesCount;
|
||||||
|
this.isLiked = isLiked;
|
||||||
|
this.uploader = uploader;
|
||||||
|
this.updateTime = updateTime;
|
||||||
|
this.uploadTime = uploadTime;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a comment object
|
||||||
|
* @param userName {string}
|
||||||
|
* @param avatar {string?}
|
||||||
|
* @param content {string}
|
||||||
|
* @param time {string?}
|
||||||
|
* @param replyCount {number?}
|
||||||
|
* @param id {string?}
|
||||||
|
* @param isLiked {boolean?}
|
||||||
|
* @param score {number?}
|
||||||
|
* @param voteStatus {number?} - 1: upvote, -1: downvote, 0: none
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function Comment({userName, avatar, content, time, replyCount, id, isLiked, score, voteStatus}) {
|
||||||
|
this.userName = userName;
|
||||||
|
this.avatar = avatar;
|
||||||
|
this.content = content;
|
||||||
|
this.time = time;
|
||||||
|
this.replyCount = replyCount;
|
||||||
|
this.id = id;
|
||||||
|
this.isLiked = isLiked;
|
||||||
|
this.score = score;
|
||||||
|
this.voteStatus = voteStatus;
|
||||||
|
}
|
||||||
|
|
||||||
class ComicSource {
|
class ComicSource {
|
||||||
name = ""
|
name = ""
|
||||||
|
|
||||||
@@ -544,6 +709,19 @@ class ComicSource {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load a setting with its key
|
||||||
|
* @param key {string}
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
loadSetting(key) {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'load_setting',
|
||||||
|
key: this.key,
|
||||||
|
setting_key: key
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save data
|
* save data
|
||||||
* @param {string} dataKey
|
* @param {string} dataKey
|
||||||
@@ -570,6 +748,17 @@ class ComicSource {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
get isLogged() {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'isLogged',
|
||||||
|
key: this.key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
init() { }
|
init() { }
|
||||||
|
|
||||||
static sources = {}
|
static sources = {}
|
||||||
|
@@ -116,10 +116,10 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
});
|
});
|
||||||
loadData().then((value) async {
|
loadData().then((value) async {
|
||||||
if (value.success) {
|
if (value.success) {
|
||||||
|
data = value.data;
|
||||||
await onDataLoaded();
|
await onDataLoaded();
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
data = value.data;
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -131,22 +131,10 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildError() {
|
Widget buildError() {
|
||||||
return Center(
|
return NetworkError(
|
||||||
child: Column(
|
message: error!,
|
||||||
mainAxisSize: MainAxisSize.min,
|
retry: retry,
|
||||||
children: [
|
);
|
||||||
Text(
|
|
||||||
error!,
|
|
||||||
maxLines: 3,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Button.text(
|
|
||||||
onPressed: retry,
|
|
||||||
child: const Text("Retry"),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
).paddingHorizontal(16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -154,11 +142,12 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
void initState() {
|
void initState() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
loadData().then((value) {
|
loadData().then((value) async {
|
||||||
if (value.success) {
|
if (value.success) {
|
||||||
|
data = value.data;
|
||||||
|
await onDataLoaded();
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
data = value.data;
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@@ -295,20 +295,21 @@ class ContentDialog extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showInputDialog({
|
Future<void> showInputDialog({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required String title,
|
required String title,
|
||||||
required String hintText,
|
String? hintText,
|
||||||
required FutureOr<Object?> Function(String) onConfirm,
|
required FutureOr<Object?> Function(String) onConfirm,
|
||||||
String? initialValue,
|
String? initialValue,
|
||||||
String confirmText = "Confirm",
|
String confirmText = "Confirm",
|
||||||
String cancelText = "Cancel",
|
String cancelText = "Cancel",
|
||||||
|
RegExp? inputValidator,
|
||||||
}) {
|
}) {
|
||||||
var controller = TextEditingController(text: initialValue);
|
var controller = TextEditingController(text: initialValue);
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
String? error;
|
String? error;
|
||||||
|
|
||||||
showDialog(
|
return showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
@@ -327,6 +328,11 @@ void showInputDialog({
|
|||||||
Button.filled(
|
Button.filled(
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
if (inputValidator != null &&
|
||||||
|
!inputValidator.hasMatch(controller.text)) {
|
||||||
|
setState(() => error = "Invalid input");
|
||||||
|
return;
|
||||||
|
}
|
||||||
var futureOr = onConfirm(controller.text);
|
var futureOr = onConfirm(controller.text);
|
||||||
Object? result;
|
Object? result;
|
||||||
if (futureOr is Future) {
|
if (futureOr is Future) {
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
part of 'components.dart';
|
part of 'components.dart';
|
||||||
|
|
||||||
class SmoothCustomScrollView extends StatelessWidget {
|
class SmoothCustomScrollView extends StatelessWidget {
|
||||||
const SmoothCustomScrollView({super.key, required this.slivers, this.controller});
|
const SmoothCustomScrollView(
|
||||||
|
{super.key, required this.slivers, this.controller});
|
||||||
|
|
||||||
final ScrollController? controller;
|
final ScrollController? controller;
|
||||||
|
|
||||||
@@ -22,9 +23,9 @@ class SmoothCustomScrollView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SmoothScrollProvider extends StatefulWidget {
|
class SmoothScrollProvider extends StatefulWidget {
|
||||||
const SmoothScrollProvider({super.key, this.controller, required this.builder});
|
const SmoothScrollProvider(
|
||||||
|
{super.key, this.controller, required this.builder});
|
||||||
|
|
||||||
final ScrollController? controller;
|
final ScrollController? controller;
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if(App.isMacOS) {
|
if (App.isMacOS) {
|
||||||
return widget.builder(
|
return widget.builder(
|
||||||
context,
|
context,
|
||||||
_controller,
|
_controller,
|
||||||
@@ -77,13 +78,15 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
|||||||
}
|
}
|
||||||
if (!_isMouseScroll) return;
|
if (!_isMouseScroll) return;
|
||||||
var currentLocation = _controller.position.pixels;
|
var currentLocation = _controller.position.pixels;
|
||||||
|
var old = _futurePosition;
|
||||||
_futurePosition ??= currentLocation;
|
_futurePosition ??= currentLocation;
|
||||||
double k = (_futurePosition! - currentLocation).abs() / 1600 + 1;
|
double k = (_futurePosition! - currentLocation).abs() / 1600 + 1;
|
||||||
_futurePosition =
|
_futurePosition = _futurePosition! + pointerSignal.scrollDelta.dy * k;
|
||||||
_futurePosition! + pointerSignal.scrollDelta.dy * k;
|
|
||||||
_futurePosition = _futurePosition!.clamp(
|
_futurePosition = _futurePosition!.clamp(
|
||||||
_controller.position.minScrollExtent,
|
_controller.position.minScrollExtent,
|
||||||
_controller.position.maxScrollExtent);
|
_controller.position.maxScrollExtent,
|
||||||
|
);
|
||||||
|
if(_futurePosition == old) return;
|
||||||
_controller.animateTo(_futurePosition!,
|
_controller.animateTo(_futurePosition!,
|
||||||
duration: _fastAnimationDuration, curve: Curves.linear);
|
duration: _fastAnimationDuration, curve: Curves.linear);
|
||||||
}
|
}
|
||||||
|
@@ -17,8 +17,11 @@ import '../js_engine.dart';
|
|||||||
import '../log.dart';
|
import '../log.dart';
|
||||||
|
|
||||||
part 'category.dart';
|
part 'category.dart';
|
||||||
|
|
||||||
part 'favorites.dart';
|
part 'favorites.dart';
|
||||||
|
|
||||||
part 'parser.dart';
|
part 'parser.dart';
|
||||||
|
|
||||||
part 'models.dart';
|
part 'models.dart';
|
||||||
|
|
||||||
/// build comic list, [Res.subData] should be maxPage or null if there is no limit.
|
/// build comic list, [Res.subData] should be maxPage or null if there is no limit.
|
||||||
@@ -50,11 +53,16 @@ typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(
|
|||||||
|
|
||||||
/// [isLiking] is true if the user is liking the comment, false if unliking.
|
/// [isLiking] is true if the user is liking the comment, false if unliking.
|
||||||
/// return the new likes count or null.
|
/// return the new likes count or null.
|
||||||
typedef LikeCommentFunc = Future<Res<int?>> Function(String comicId, String? subId, String commentId, bool isLiking);
|
typedef LikeCommentFunc = Future<Res<int?>> Function(
|
||||||
|
String comicId, String? subId, String commentId, bool isLiking);
|
||||||
|
|
||||||
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
|
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
|
||||||
/// return the new vote count or null.
|
/// return the new vote count or null.
|
||||||
typedef VoteCommentFunc = Future<Res<int?>> Function(String comicId, String? subId, String commentId, bool isUp, bool isCancel);
|
typedef VoteCommentFunc = Future<Res<int?>> Function(
|
||||||
|
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
|
||||||
|
|
||||||
|
typedef HandleClickTagEvent = Map<String, String> Function(
|
||||||
|
String namespace, String tag);
|
||||||
|
|
||||||
class ComicSource {
|
class ComicSource {
|
||||||
static final List<ComicSource> _sources = [];
|
static final List<ComicSource> _sources = [];
|
||||||
@@ -147,9 +155,6 @@ class ComicSource {
|
|||||||
/// Search page.
|
/// Search page.
|
||||||
final SearchPageData? searchPageData;
|
final SearchPageData? searchPageData;
|
||||||
|
|
||||||
/// Settings.
|
|
||||||
final List<SettingItem> settings;
|
|
||||||
|
|
||||||
/// Load comic info.
|
/// Load comic info.
|
||||||
final LoadComicFunc? loadComicInfo;
|
final LoadComicFunc? loadComicInfo;
|
||||||
|
|
||||||
@@ -186,6 +191,12 @@ class ComicSource {
|
|||||||
|
|
||||||
final LikeCommentFunc? likeCommentFunc;
|
final LikeCommentFunc? likeCommentFunc;
|
||||||
|
|
||||||
|
final Map<String, dynamic>? settings;
|
||||||
|
|
||||||
|
final Map<String, Map<String, String>>? translations;
|
||||||
|
|
||||||
|
final HandleClickTagEvent? handleClickTagEvent;
|
||||||
|
|
||||||
Future<void> loadData() async {
|
Future<void> loadData() async {
|
||||||
var file = File("${App.dataPath}/comic_source/$key.data");
|
var file = File("${App.dataPath}/comic_source/$key.data");
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
@@ -246,8 +257,11 @@ class ComicSource {
|
|||||||
this.sendCommentFunc,
|
this.sendCommentFunc,
|
||||||
this.likeOrUnlikeComic,
|
this.likeOrUnlikeComic,
|
||||||
this.voteCommentFunc,
|
this.voteCommentFunc,
|
||||||
this.likeCommentFunc,)
|
this.likeCommentFunc,
|
||||||
: idMatcher = null;
|
this.idMatcher,
|
||||||
|
this.translations,
|
||||||
|
this.handleClickTagEvent,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountConfig {
|
class AccountConfig {
|
||||||
@@ -368,21 +382,6 @@ class SearchOptions {
|
|||||||
String get defaultValue => options.keys.first;
|
String get defaultValue => options.keys.first;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingItem {
|
|
||||||
final String name;
|
|
||||||
final String iconName;
|
|
||||||
final SettingType type;
|
|
||||||
final List<String>? options;
|
|
||||||
|
|
||||||
const SettingItem(this.name, this.iconName, this.type, this.options);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SettingType {
|
|
||||||
switcher,
|
|
||||||
selector,
|
|
||||||
input,
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
||||||
String category, String? param, List<String> options, int page);
|
String category, String? param, List<String> options, int page);
|
||||||
|
|
||||||
@@ -423,4 +422,3 @@ class CategoryComicsOptions {
|
|||||||
|
|
||||||
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen);
|
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -107,6 +107,8 @@ class ComicDetails with HistoryMixin {
|
|||||||
|
|
||||||
final String? updateTime;
|
final String? updateTime;
|
||||||
|
|
||||||
|
final String? url;
|
||||||
|
|
||||||
static Map<String, List<String>> _generateMap(Map<String, dynamic> map) {
|
static Map<String, List<String>> _generateMap(Map<String, dynamic> map) {
|
||||||
var res = <String, List<String>>{};
|
var res = <String, List<String>>{};
|
||||||
map.forEach((key, value) {
|
map.forEach((key, value) {
|
||||||
@@ -137,7 +139,8 @@ class ComicDetails with HistoryMixin {
|
|||||||
commentsCount = json["commentsCount"],
|
commentsCount = json["commentsCount"],
|
||||||
uploader = json["uploader"],
|
uploader = json["uploader"],
|
||||||
uploadTime = json["uploadTime"],
|
uploadTime = json["uploadTime"],
|
||||||
updateTime = json["updateTime"];
|
updateTime = json["updateTime"],
|
||||||
|
url = json["url"];
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
@@ -159,6 +162,7 @@ class ComicDetails with HistoryMixin {
|
|||||||
"uploader": uploader,
|
"uploader": uploader,
|
||||||
"uploadTime": uploadTime,
|
"uploadTime": uploadTime,
|
||||||
"updateTime": updateTime,
|
"updateTime": updateTime,
|
||||||
|
"url": url,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -130,7 +130,7 @@ class ComicSourceParser {
|
|||||||
_loadFavoriteData(),
|
_loadFavoriteData(),
|
||||||
_loadExploreData(),
|
_loadExploreData(),
|
||||||
_loadSearchData(),
|
_loadSearchData(),
|
||||||
[],
|
_parseSettings(),
|
||||||
_parseLoadComicFunc(),
|
_parseLoadComicFunc(),
|
||||||
_parseThumbnailLoader(),
|
_parseThumbnailLoader(),
|
||||||
_parseLoadComicPagesFunc(),
|
_parseLoadComicPagesFunc(),
|
||||||
@@ -144,6 +144,9 @@ class ComicSourceParser {
|
|||||||
_parseLikeFunc(),
|
_parseLikeFunc(),
|
||||||
_parseVoteCommentFunc(),
|
_parseVoteCommentFunc(),
|
||||||
_parseLikeCommentFunc(),
|
_parseLikeCommentFunc(),
|
||||||
|
_parseIdMatch(),
|
||||||
|
_parseTranslation(),
|
||||||
|
_parseClickTagEvent(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await source.loadData();
|
await source.loadData();
|
||||||
@@ -639,13 +642,13 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LikeOrUnlikeComicFunc? _parseLikeFunc() {
|
LikeOrUnlikeComicFunc? _parseLikeFunc() {
|
||||||
if (!_checkExists("comic.likeOrUnlikeComic")) {
|
if (!_checkExists("comic.likeComic")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (id, isLiking) async {
|
return (id, isLiking) async {
|
||||||
try {
|
try {
|
||||||
await JsEngine().runCode("""
|
await JsEngine().runCode("""
|
||||||
ComicSource.sources.$_key.comic.likeOrUnlikeComic(${jsonEncode(id)}, ${jsonEncode(isLiking)})
|
ComicSource.sources.$_key.comic.likeComic(${jsonEncode(id)}, ${jsonEncode(isLiking)})
|
||||||
""");
|
""");
|
||||||
return const Res(true);
|
return const Res(true);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -688,4 +691,41 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _parseSettings() {
|
||||||
|
return _getValue("settings") ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
RegExp? _parseIdMatch() {
|
||||||
|
if (!_checkExists("comic.idMatch")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return RegExp(_getValue("comic.idMatch"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Map<String, String>>? _parseTranslation() {
|
||||||
|
if (!_checkExists("translation")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var data = _getValue("translation");
|
||||||
|
var res = <String, Map<String, String>>{};
|
||||||
|
for (var e in data.entries) {
|
||||||
|
res[e.key] = Map<String, String>.from(e.value);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleClickTagEvent? _parseClickTagEvent() {
|
||||||
|
if (!_checkExists("comic.onClickTag")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (namespace, tag) {
|
||||||
|
var res = JsEngine().runCode("""
|
||||||
|
ComicSource.sources.$_key.comic.onClickTag(${jsonEncode(namespace)}, ${jsonEncode(tag)})
|
||||||
|
""");
|
||||||
|
var r = Map<String, String?>.from(res);
|
||||||
|
r.removeWhere((key, value) => value == null);
|
||||||
|
return Map.from(r);
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,6 +19,7 @@ import 'package:pointycastle/block/modes/cbc.dart';
|
|||||||
import 'package:pointycastle/block/modes/cfb.dart';
|
import 'package:pointycastle/block/modes/cfb.dart';
|
||||||
import 'package:pointycastle/block/modes/ecb.dart';
|
import 'package:pointycastle/block/modes/ecb.dart';
|
||||||
import 'package:pointycastle/block/modes/ofb.dart';
|
import 'package:pointycastle/block/modes/ofb.dart';
|
||||||
|
import 'package:uuid/uuid.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/network/cookie_jar.dart';
|
||||||
|
|
||||||
@@ -112,6 +113,9 @@ class JsEngine with _JSEngineApi{
|
|||||||
{
|
{
|
||||||
String key = message["key"];
|
String key = message["key"];
|
||||||
String dataKey = message["data_key"];
|
String dataKey = message["data_key"];
|
||||||
|
if(dataKey == 'setting'){
|
||||||
|
throw "setting is not allowed to be saved";
|
||||||
|
}
|
||||||
var data = message["data"];
|
var data = message["data"];
|
||||||
var source = ComicSource.find(key)!;
|
var source = ComicSource.find(key)!;
|
||||||
source.data[dataKey] = data;
|
source.data[dataKey] = data;
|
||||||
@@ -145,6 +149,23 @@ class JsEngine with _JSEngineApi{
|
|||||||
{
|
{
|
||||||
return handleCookieCallback(Map.from(message));
|
return handleCookieCallback(Map.from(message));
|
||||||
}
|
}
|
||||||
|
case "uuid":
|
||||||
|
{
|
||||||
|
return const Uuid().v1();
|
||||||
|
}
|
||||||
|
case "load_setting":
|
||||||
|
{
|
||||||
|
String key = message["key"];
|
||||||
|
String settingKey = message["setting_key"];
|
||||||
|
var source = ComicSource.find(key)!;
|
||||||
|
return source.data["setting"]?[settingKey]
|
||||||
|
?? source.settings?[settingKey]['default']
|
||||||
|
?? (throw "Setting not found: $settingKey");
|
||||||
|
}
|
||||||
|
case "isLogged":
|
||||||
|
{
|
||||||
|
return ComicSource.find(message["key"])!.isLogged;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,6 +324,10 @@ mixin class _JSEngineApi{
|
|||||||
bool isEncode = data["isEncode"];
|
bool isEncode = data["isEncode"];
|
||||||
try {
|
try {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case "utf8":
|
||||||
|
return isEncode
|
||||||
|
? utf8.encode(value)
|
||||||
|
: utf8.decode(value);
|
||||||
case "base64":
|
case "base64":
|
||||||
if(value is String){
|
if(value is String){
|
||||||
value = utf8.encode(value);
|
value = utf8.encode(value);
|
||||||
@@ -318,6 +343,21 @@ mixin class _JSEngineApi{
|
|||||||
return Uint8List.fromList(sha256.convert(value).bytes);
|
return Uint8List.fromList(sha256.convert(value).bytes);
|
||||||
case "sha512":
|
case "sha512":
|
||||||
return Uint8List.fromList(sha512.convert(value).bytes);
|
return Uint8List.fromList(sha512.convert(value).bytes);
|
||||||
|
case "hmac":
|
||||||
|
var key = data["key"];
|
||||||
|
var hash = data["hash"];
|
||||||
|
var hmac = Hmac(switch(hash) {
|
||||||
|
"md5" => md5,
|
||||||
|
"sha1" => sha1,
|
||||||
|
"sha256" => sha256,
|
||||||
|
"sha512" => sha512,
|
||||||
|
_ => throw "Unsupported hash: $hash"
|
||||||
|
}, key);
|
||||||
|
if(data['isString'] == true){
|
||||||
|
return hmac.convert(value).toString();
|
||||||
|
} else {
|
||||||
|
return Uint8List.fromList(hmac.convert(value).bytes);
|
||||||
|
}
|
||||||
case "aes-ecb":
|
case "aes-ecb":
|
||||||
if(!isEncode){
|
if(!isEncode){
|
||||||
var key = data["key"];
|
var key = data["key"];
|
||||||
|
@@ -114,7 +114,8 @@ class AppDio with DioMixin {
|
|||||||
client.connectionTimeout = const Duration(seconds: 5);
|
client.connectionTimeout = const Duration(seconds: 5);
|
||||||
client.findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
|
client.findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
|
||||||
client.idleTimeout = const Duration(seconds: 100);
|
client.idleTimeout = const Duration(seconds: 100);
|
||||||
client.badCertificateCallback = (X509Certificate cert, String host, int port) {
|
client.badCertificateCallback =
|
||||||
|
(X509Certificate cert, String host, int port) {
|
||||||
if (host.contains("cdn")) return true;
|
if (host.contains("cdn")) return true;
|
||||||
final ipv4RegExp = RegExp(
|
final ipv4RegExp = RegExp(
|
||||||
r'^((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})$');
|
r'^((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})$');
|
||||||
@@ -129,7 +130,8 @@ class AppDio with DioMixin {
|
|||||||
static String? proxy;
|
static String? proxy;
|
||||||
|
|
||||||
static Future<String?> getProxy() async {
|
static Future<String?> getProxy() async {
|
||||||
if ((appdata.settings['proxy'] as String).removeAllBlank == "direct") return null;
|
if ((appdata.settings['proxy'] as String).removeAllBlank == "direct")
|
||||||
|
return null;
|
||||||
if (appdata.settings['proxy'] != "system") return appdata.settings['proxy'];
|
if (appdata.settings['proxy'] != "system") return appdata.settings['proxy'];
|
||||||
|
|
||||||
String res;
|
String res;
|
||||||
@@ -168,7 +170,7 @@ class AppDio with DioMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Response<T>> request<T> (
|
Future<Response<T>> request<T>(
|
||||||
String path, {
|
String path, {
|
||||||
Object? data,
|
Object? data,
|
||||||
Map<String, dynamic>? queryParameters,
|
Map<String, dynamic>? queryParameters,
|
||||||
@@ -178,11 +180,18 @@ class AppDio with DioMixin {
|
|||||||
ProgressCallback? onReceiveProgress,
|
ProgressCallback? onReceiveProgress,
|
||||||
}) async {
|
}) async {
|
||||||
proxy = await getProxy();
|
proxy = await getProxy();
|
||||||
if(_proxy != proxy) {
|
if (_proxy != proxy) {
|
||||||
_proxy = proxy;
|
_proxy = proxy;
|
||||||
(httpClientAdapter as IOHttpClientAdapter).close();
|
(httpClientAdapter as IOHttpClientAdapter).close();
|
||||||
httpClientAdapter = IOHttpClientAdapter(createHttpClient: createHttpClient);
|
httpClientAdapter =
|
||||||
|
IOHttpClientAdapter(createHttpClient: createHttpClient);
|
||||||
}
|
}
|
||||||
|
Log.info(
|
||||||
|
"Network",
|
||||||
|
"${options?.method ?? 'GET'} $path\n"
|
||||||
|
"Headers: ${options?.headers}\n"
|
||||||
|
"Data: $data\n",
|
||||||
|
);
|
||||||
return super.request(
|
return super.request(
|
||||||
path,
|
path,
|
||||||
data: data,
|
data: data,
|
||||||
|
@@ -133,7 +133,7 @@ class _CategoryPage extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (data.enableRankingPage)
|
if (data.enableRankingPage)
|
||||||
buildTag("Ranking".tl, (p0, p1) {
|
buildTag("Ranking".tl, (p0, p1) {
|
||||||
context.to(() => RankingPage(sourceKey: findComicSourceKey()));
|
context.to(() => RankingPage(categoryKey: data.key));
|
||||||
}),
|
}),
|
||||||
for (var buttonData in data.buttons)
|
for (var buttonData in data.buttons)
|
||||||
buildTag(buttonData.label.tl, (p0, p1) => buttonData.onTap())
|
buildTag(buttonData.label.tl, (p0, p1) => buttonData.onTap())
|
||||||
|
@@ -26,6 +26,7 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
late final CategoryComicsData data;
|
late final CategoryComicsData data;
|
||||||
late final List<CategoryComicsOptions> options;
|
late final List<CategoryComicsOptions> options;
|
||||||
late List<String> optionsValue;
|
late List<String> optionsValue;
|
||||||
|
late String sourceKey;
|
||||||
|
|
||||||
void findData() {
|
void findData() {
|
||||||
for (final source in ComicSource.all()) {
|
for (final source in ComicSource.all()) {
|
||||||
@@ -40,6 +41,7 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
optionsValue = options.map((e) => e.options.keys.first).toList();
|
optionsValue = options.map((e) => e.options.keys.first).toList();
|
||||||
|
sourceKey = source.key;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +62,7 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
),
|
),
|
||||||
body: ComicList(
|
body: ComicList(
|
||||||
key: Key(widget.category + optionsValue.toString()),
|
key: Key(widget.category + optionsValue.toString()),
|
||||||
leadingSliver: buildOptions(),
|
leadingSliver: buildOptions().toSliver(),
|
||||||
loadPage: (i) => data.load(
|
loadPage: (i) => data.load(
|
||||||
widget.category,
|
widget.category,
|
||||||
widget.param,
|
widget.param,
|
||||||
@@ -74,7 +76,7 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
Widget buildOptionItem(
|
Widget buildOptionItem(
|
||||||
String text, String value, int group, BuildContext context) {
|
String text, String value, int group, BuildContext context) {
|
||||||
return OptionChip(
|
return OptionChip(
|
||||||
text: text,
|
text: text.ts(sourceKey),
|
||||||
isSelected: value == optionsValue[group],
|
isSelected: value == optionsValue[group],
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (value == optionsValue[group]) return;
|
if (value == optionsValue[group]) return;
|
||||||
@@ -105,12 +107,10 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
children.add(const SizedBox(height: 8));
|
children.add(const SizedBox(height: 8));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SliverToBoxAdapter(
|
return Column(
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [...children, const Divider()],
|
children: [...children, const Divider()],
|
||||||
).paddingLeft(8).paddingRight(8),
|
).paddingLeft(8).paddingRight(8);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
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/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/comic_source/comic_source.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/local.dart';
|
||||||
import 'package:venera/foundation/res.dart';
|
import 'package:venera/foundation/res.dart';
|
||||||
import 'package:venera/network/download.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/favorites/favorites_page.dart';
|
||||||
import 'package:venera/pages/reader/reader.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/io.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
@@ -149,8 +153,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(comic.title, style: ts.s18),
|
SelectableText(comic.title, style: ts.s18),
|
||||||
if (comic.subTitle != null) Text(comic.subTitle!, style: ts.s14),
|
if (comic.subTitle != null)
|
||||||
|
SelectableText(comic.subTitle!, style: ts.s14),
|
||||||
Text(
|
Text(
|
||||||
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
||||||
style: ts.s12,
|
style: ts.s12,
|
||||||
@@ -345,7 +350,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
for (var e in comic.tags.entries)
|
for (var e in comic.tags.entries)
|
||||||
buildWrap(
|
buildWrap(
|
||||||
children: [
|
children: [
|
||||||
buildTag(text: e.key, isTitle: true),
|
buildTag(text: e.key.ts(comicSource.key), isTitle: true),
|
||||||
for (var tag in e.value)
|
for (var tag in e.value)
|
||||||
buildTag(text: tag, onTap: () => onTapTag(tag, e.key)),
|
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 {
|
abstract mixin class _ComicPageActions {
|
||||||
void update();
|
void update();
|
||||||
|
|
||||||
@@ -546,9 +550,74 @@ abstract mixin class _ComicPageActions {
|
|||||||
update();
|
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() {
|
void showComments() {
|
||||||
showSideBar(
|
showSideBar(
|
||||||
@@ -1217,7 +1286,10 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: Appbar(title: Text("Download".tl), backgroundColor: context.colorScheme.surfaceContainerLow,),
|
appBar: Appbar(
|
||||||
|
title: Text("Download".tl),
|
||||||
|
backgroundColor: context.colorScheme.surfaceContainerLow,
|
||||||
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@@ -146,12 +146,90 @@ class _BodyState extends State<_Body> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: const Text("Version"),
|
title: const Text("Version"),
|
||||||
subtitle: Text(source.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) {
|
void delete(ComicSource source) {
|
||||||
showConfirmDialog(
|
showConfirmDialog(
|
||||||
context: App.rootContext,
|
context: App.rootContext,
|
||||||
@@ -280,11 +358,12 @@ class _BodyState extends State<_Body> {
|
|||||||
if (file == null) return;
|
if (file == null) return;
|
||||||
try {
|
try {
|
||||||
var fileName = file.name;
|
var fileName = file.name;
|
||||||
var bytes = file.bytes!;
|
var bytes = await File(file.path!).readAsBytes();
|
||||||
var content = utf8.decode(bytes);
|
var content = utf8.decode(bytes);
|
||||||
await addSource(content, fileName);
|
await addSource(content, fileName);
|
||||||
} catch (e) {
|
} catch (e, s) {
|
||||||
App.rootContext.showMessage(message: e.toString());
|
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,
|
withAppbar: false,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
var showAvatar = _comments!.any((e) {
|
||||||
|
return e.avatar != null;
|
||||||
|
});
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -109,6 +112,7 @@ class _CommentsPageState extends State<CommentsPage> {
|
|||||||
comment: _comments![index],
|
comment: _comments![index],
|
||||||
source: widget.source,
|
source: widget.source,
|
||||||
comic: widget.data,
|
comic: widget.data,
|
||||||
|
showAvatar: showAvatar,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -204,6 +208,7 @@ class _CommentTile extends StatefulWidget {
|
|||||||
required this.comment,
|
required this.comment,
|
||||||
required this.source,
|
required this.source,
|
||||||
required this.comic,
|
required this.comic,
|
||||||
|
required this.showAvatar,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Comment comment;
|
final Comment comment;
|
||||||
@@ -212,6 +217,8 @@ class _CommentTile extends StatefulWidget {
|
|||||||
|
|
||||||
final ComicDetails comic;
|
final ComicDetails comic;
|
||||||
|
|
||||||
|
final bool showAvatar;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_CommentTile> createState() => _CommentTileState();
|
State<_CommentTile> createState() => _CommentTileState();
|
||||||
}
|
}
|
||||||
@@ -239,7 +246,7 @@ class _CommentTileState extends State<_CommentTile> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.comment.avatar != null)
|
if (widget.showAvatar)
|
||||||
Container(
|
Container(
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
@@ -247,7 +254,9 @@ class _CommentTileState extends State<_CommentTile> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer),
|
color: Theme.of(context).colorScheme.secondaryContainer),
|
||||||
child: AnimatedImage(
|
child: widget.comment.avatar == null
|
||||||
|
? null
|
||||||
|
: AnimatedImage(
|
||||||
image: CachedImageProvider(
|
image: CachedImageProvider(
|
||||||
widget.comment.avatar!,
|
widget.comment.avatar!,
|
||||||
sourceKey: widget.source.key,
|
sourceKey: widget.source.key,
|
||||||
@@ -313,6 +322,7 @@ class _CommentTileState extends State<_CommentTile> {
|
|||||||
source: widget.source,
|
source: widget.source,
|
||||||
replyId: widget.comment.id,
|
replyId: widget.comment.id,
|
||||||
),
|
),
|
||||||
|
showBarrier: false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -376,7 +386,11 @@ class _CommentTileState extends State<_CommentTile> {
|
|||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
)
|
)
|
||||||
else if (isLiked)
|
else if (isLiked)
|
||||||
const Icon(Icons.favorite, size: 16)
|
Icon(
|
||||||
|
Icons.favorite,
|
||||||
|
size: 16,
|
||||||
|
color: context.useTextColor(Colors.red),
|
||||||
|
)
|
||||||
else
|
else
|
||||||
const Icon(Icons.favorite_border, size: 16),
|
const Icon(Icons.favorite_border, size: 16),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
@@ -27,7 +27,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
favPage.folderList = this;
|
favPage.folderList = this;
|
||||||
folders = LocalFavoritesManager().folderNames;
|
folders = LocalFavoritesManager().folderNames;
|
||||||
networkFolders = ComicSource.all()
|
networkFolders = ComicSource.all()
|
||||||
.where((e) => e.favoriteData != null)
|
.where((e) => e.favoriteData != null && e.isLogged)
|
||||||
.map((e) => e.favoriteData!.key)
|
.map((e) => e.favoriteData!.key)
|
||||||
.toList();
|
.toList();
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@@ -316,7 +316,7 @@ class _LocalState extends State<_Local> {
|
|||||||
Button.outlined(
|
Button.outlined(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if(LocalManager().downloadingTasks.first.isPaused)
|
if (LocalManager().downloadingTasks.first.isPaused)
|
||||||
const Icon(Icons.pause_circle_outline, size: 18)
|
const Icon(Icons.pause_circle_outline, size: 18)
|
||||||
else
|
else
|
||||||
const _AnimatedDownloadingIcon(),
|
const _AnimatedDownloadingIcon(),
|
||||||
@@ -813,11 +813,14 @@ class _AccountsWidgetState extends State<_AccountsWidget> {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
|
runSpacing: 8,
|
||||||
|
spacing: 8,
|
||||||
children: accounts.map((e) {
|
children: accounts.map((e) {
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8, vertical: 2),
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -825,7 +828,7 @@ class _AccountsWidgetState extends State<_AccountsWidget> {
|
|||||||
child: Text(e),
|
child: Text(e),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
).paddingHorizontal(16).paddingBottom(16),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@@ -5,9 +5,9 @@ import "package:venera/foundation/comic_source/comic_source.dart";
|
|||||||
import "package:venera/utils/translations.dart";
|
import "package:venera/utils/translations.dart";
|
||||||
|
|
||||||
class RankingPage extends StatefulWidget {
|
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
|
@override
|
||||||
State<RankingPage> createState() => _RankingPageState();
|
State<RankingPage> createState() => _RankingPageState();
|
||||||
@@ -20,14 +20,14 @@ class _RankingPageState extends State<RankingPage> {
|
|||||||
|
|
||||||
void findData() {
|
void findData() {
|
||||||
for (final source in ComicSource.all()) {
|
for (final source in ComicSource.all()) {
|
||||||
if (source.categoryData?.key == widget.sourceKey) {
|
if (source.categoryData?.key == widget.categoryKey) {
|
||||||
data = source.categoryComicsData!;
|
data = source.categoryComicsData!;
|
||||||
options = data.rankingData!.options;
|
options = data.rankingData!.options;
|
||||||
optionValue = options.keys.first;
|
optionValue = options.keys.first;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw "${widget.sourceKey} Not found";
|
throw "${widget.categoryKey} Not found";
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@@ -317,6 +317,14 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
|||||||
height = constrains.maxHeight;
|
height = constrains.maxHeight;
|
||||||
width = height * cacheSize.width / cacheSize.height;
|
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){
|
if(_imageInfo != null){
|
||||||
@@ -371,6 +379,7 @@ class _ComicImageState extends State<ComicImage> with WidgetsBindingObserver {
|
|||||||
height: 24,
|
height: 24,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
|
backgroundColor: context.colorScheme.surfaceContainerLow,
|
||||||
value: (_loadingProgress != null &&
|
value: (_loadingProgress != null &&
|
||||||
_loadingProgress!.expectedTotalBytes!=null &&
|
_loadingProgress!.expectedTotalBytes!=null &&
|
||||||
_loadingProgress!.expectedTotalBytes! != 0)
|
_loadingProgress!.expectedTotalBytes! != 0)
|
||||||
|
@@ -112,6 +112,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
for (int i = 0; i < searchOptions.length; i++) {
|
for (int i = 0; i < searchOptions.length; i++) {
|
||||||
final option = searchOptions[i];
|
final option = searchOptions[i];
|
||||||
children.add(ListTile(
|
children.add(ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(option.label.tl),
|
title: Text(option.label.tl),
|
||||||
));
|
));
|
||||||
children.add(Wrap(
|
children.add(Wrap(
|
||||||
@@ -119,7 +120,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: option.options.entries.map((e) {
|
children: option.options.entries.map((e) {
|
||||||
return OptionChip(
|
return OptionChip(
|
||||||
text: e.value.tl,
|
text: e.value.ts(searchTarget),
|
||||||
isSelected: options[i] == e.key,
|
isSelected: options[i] == e.key,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
options[i] = e.key;
|
options[i] = e.key;
|
||||||
@@ -127,7 +128,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
).paddingHorizontal(16));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
@@ -136,13 +137,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: children,
|
||||||
ListTile(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
title: Text("Search Options".tl),
|
|
||||||
),
|
|
||||||
...children,
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
import '../foundation/app.dart';
|
import '../foundation/app.dart';
|
||||||
|
|
||||||
extension AppTranslation on String {
|
extension AppTranslation on String {
|
||||||
@@ -27,10 +28,27 @@ extension AppTranslation on String {
|
|||||||
|
|
||||||
static late final Map<String, Map<String, String>> translations;
|
static late final Map<String, Map<String, String>> translations;
|
||||||
|
|
||||||
static Future<void> init() async{
|
static Future<void> init() async {
|
||||||
var data = await rootBundle.load("assets/translation.json");
|
var data = await rootBundle.load("assets/translation.json");
|
||||||
var json = jsonDecode(utf8.decode(data.buffer.asUint8List()));
|
var json = jsonDecode(utf8.decode(data.buffer.asUint8List()));
|
||||||
translations = { for (var e in json.entries) e.key : Map<String, String>.from(e.value) };
|
translations = {
|
||||||
|
for (var e in json.entries) e.key: Map<String, String>.from(e.value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Translate a string using specified comic source
|
||||||
|
String ts(String sourceKey) {
|
||||||
|
var comicSource = ComicSource.find(sourceKey);
|
||||||
|
if (comicSource == null || comicSource.translations == null) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
var locale = App.locale;
|
||||||
|
var lc = locale.languageCode;
|
||||||
|
var cc = locale.countryCode;
|
||||||
|
var key = "$lc${cc == null ? "" : "_$cc"}";
|
||||||
|
return (comicSource.translations![key] ??
|
||||||
|
comicSource.translations![lc])?[this] ??
|
||||||
|
this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -550,7 +550,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: uuid
|
name: uuid
|
||||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||||
|
@@ -42,6 +42,7 @@ dependencies:
|
|||||||
path: packages/scrollable_positioned_list
|
path: packages/scrollable_positioned_list
|
||||||
flutter_reorderable_grid_view: 5.0.1
|
flutter_reorderable_grid_view: 5.0.1
|
||||||
yaml: any
|
yaml: any
|
||||||
|
uuid: ^4.5.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user