Remove the config file repository url from app.

This commit is contained in:
2025-06-18 16:34:49 +08:00
parent a46ceebf19
commit b2a164e066
4 changed files with 170 additions and 140 deletions

View File

@@ -389,7 +389,7 @@
"Do not report any issues related to sources to App repo.": "请不要向App仓库报告任何与源相关的问题",
"Show single image on first page": "在首页显示单张图片",
"Click to select an image": "点击选择一张图片",
"Source URL": "地址",
"Repo URL": "仓库地址",
"The URL should point to a 'index.json' file": "该URL应指向一个'index.json'文件",
"Double tap to zoom": "双击缩放",
"Clear Unfavorited": "清除未收藏",
@@ -786,7 +786,7 @@
"Do not report any issues related to sources to App repo.": "請不要向App倉庫報告任何與源相關的問題",
"Show single image on first page": "在首頁顯示單張圖片",
"Click to select an image": "點擊選擇一張圖片",
"Source URL": "地址",
"Repo URL": "倉庫地址",
"The URL should point to a 'index.json' file": "該URL應指向一個'index.json'文件",
"Double tap to zoom": "雙擊縮放",
"Clear Unfavorited": "清除未收藏",

View File

@@ -9,13 +9,45 @@ Venera uses [flutter_qjs](https://github.com/wgh136/flutter_qjs) as js engine wh
This document will describe how to write a comic source for Venera.
## Preparation
## Comic Source List
Venera can display a list of comic sources in the app.
You should provide a repository url to let the app load the comic source list.
The url should point to a JSON file that contains the list of comic sources.
The JSON file should have the following format:
```json
[
{
"name": "Source Name",
"url": "https://example.com/source.js",
"filename": "Relative path to the source file",
"version": "1.0.0",
"description": "A brief description of the source"
}
]
```
Only one of `url` and `filename` should be provided.
The description field is optional.
Currently, you can use the following repo url:
```
https://cdn.jsdelivr.net/gh/venera-app/venera-configs@main/index.json
```
The repo is maintained by the Venera team, and you can submit a pull request to add your comic source.
## Create a Comic Source
### Preparation
- Install Venera. Using flutter to run the project is recommended since it's easier to debug.
- An editor that supports javascript.
- Download template and venera javascript api from [here](https://github.com/venera-app/venera-configs).
## Start Writing
### Start Writing
The template contains detailed comments and examples. You can refer to it when writing your own comic source.
@@ -23,7 +55,7 @@ Here is a brief introduction to the template:
> Note: Javascript api document is [here](js_api.md).
### Write basic information
#### Write basic information
```javascript
class NewComicSource extends ComicSource {
@@ -49,7 +81,7 @@ In this part, you need to do the following:
- Change the class name to your source name.
- Fill in the name, key, version, minAppVersion, and url fields.
### init function
#### init function
```javascript
/**
@@ -64,7 +96,7 @@ The function will be called when the source is initialized. You can do some init
Remove this function if not used.
### Account
#### Account
```javascript
// [Optional] account related
@@ -140,7 +172,7 @@ In this part, you can implement login, logout, and register functions.
Remove this part if not used.
### Explore page
#### Explore page
```javascript
// explore page list
@@ -185,7 +217,7 @@ There are three types of explore pages:
- multiPageComicList: An explore page contains multiple comics, the comics are loaded page by page.
- mixed: An explore page contains multiple parts, each part can be a list of comics or a block of comics which have a title and a view more button.
### Category Page
#### Category Page
```javascript
// categories
@@ -227,7 +259,7 @@ Category page is a static page that contains multiple parts, each part contains
A comic source can only have one category page.
### Category Comics Page
#### Category Comics Page
```javascript
/// category comic loading related
@@ -280,7 +312,7 @@ When user clicks on a category, the category comics page will be displayed.
This part is used to load comics of a category.
### Search
#### Search
```javascript
/// search related
@@ -339,7 +371,7 @@ This part is used to load search results.
`load` and `loadNext` functions are used to load search results.
If `load` function is implemented, `loadNext` function will be ignored.
### Favorites
#### Favorites
```javascript
// favorite related
@@ -411,7 +443,7 @@ This part is used to manage network favorites of the source.
`load` and `loadNext` functions are used to load search results.
If `load` function is implemented, `loadNext` function will be ignored.
### Comic Details
#### Comic Details
```javascript
/// single comic related
@@ -576,7 +608,7 @@ If `load` function is implemented, `loadNext` function will be ignored.
This part is used to load comic details.
### Settings
#### Settings
```javascript
/*
@@ -635,7 +667,7 @@ This part is used to load comic details.
This part is used to provide settings for the source.
### Translations
#### Translations
```javascript
// [Optional] translations for the strings in this config

View File

@@ -189,7 +189,7 @@ class Settings with ChangeNotifier {
'customImageProcessing': defaultCustomImageProcessing,
'sni': true,
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
'comicSourceListUrl': defaultComicSourceUrl,
'comicSourceListUrl': '',
'preloadImageCount': 4,
'followUpdatesFolder': null,
'initialPage': '0',
@@ -233,5 +233,3 @@ function processImage(image, cid, eid, page, sourceKey) {
return futureImage;
}
''';
const defaultComicSourceUrl = "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json";

View File

@@ -51,9 +51,7 @@ class ComicSourcePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: const _Body(),
);
return Scaffold(body: const _Body());
}
}
@@ -87,10 +85,7 @@ class _BodyState extends State<_Body> {
Widget build(BuildContext context) {
return SmoothCustomScrollView(
slivers: [
SliverAppbar(
title: Text('Comic Source'.tl),
style: AppbarStyle.shadow,
),
SliverAppbar(title: Text('Comic Source'.tl), style: AppbarStyle.shadow),
buildCard(context),
for (var source in ComicSource.all())
_SliverComicSource(
@@ -109,9 +104,7 @@ class _BodyState extends State<_Body> {
showConfirmDialog(
context: App.rootContext,
title: "Delete".tl,
content: "Delete comic source '@n' ?".tlParams({
"n": source.name,
}),
content: "Delete comic source '@n' ?".tlParams({"n": source.name}),
btnColor: context.colorScheme.error,
onConfirm: () {
var file = File(source.filePath);
@@ -134,13 +127,15 @@ class _BodyState extends State<_Body> {
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("cancel")),
child: const Text("cancel"),
),
TextButton(
onPressed: () async {
await ComicSourceManager().reload();
App.forceRebuild();
},
child: const Text("continue")),
child: const Text("continue"),
),
],
),
);
@@ -157,8 +152,10 @@ class _BodyState extends State<_Body> {
);
}
static Future<void> update(ComicSource source,
[bool showLoading = true]) async {
static Future<void> update(
ComicSource source, [
bool showLoading = true,
]) async {
if (!source.url.isURL) {
App.rootContext.showMessage(message: "Invalid url config");
return;
@@ -174,8 +171,10 @@ class _BodyState extends State<_Body> {
);
}
try {
var res = await AppDio().get<String>(source.url,
options: Options(responseType: ResponseType.plain));
var res = await AppDio().get<String>(
source.url,
options: Options(responseType: ResponseType.plain),
);
if (cancel) return;
controller?.close();
await ComicSourceParser().parse(res.data!, source.filePath);
@@ -192,12 +191,11 @@ class _BodyState extends State<_Body> {
}
Widget buildCard(BuildContext context) {
Widget buildButton(
{required Widget child, required VoidCallback onPressed}) {
return Button.normal(
onPressed: onPressed,
child: child,
).fixHeight(32);
Widget buildButton({
required Widget child,
required VoidCallback onPressed,
}) {
return Button.normal(onPressed: onPressed, child: child).fixHeight(32);
}
return SliverToBoxAdapter(
@@ -218,7 +216,9 @@ class _BodyState extends State<_Body> {
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
suffix: IconButton(
onPressed: () => handleAddSource(url),
icon: const Icon(Icons.check))),
icon: const Icon(Icons.check),
),
),
onChanged: (value) {
url = value;
},
@@ -245,10 +245,7 @@ class _BodyState extends State<_Body> {
),
ListTile(
title: Text("Help".tl),
trailing: buildButton(
onPressed: help,
child: Text("Open".tl),
),
trailing: buildButton(onPressed: help, child: Text("Open".tl)),
),
ListTile(
title: Text("Check updates".tl),
@@ -277,7 +274,8 @@ class _BodyState extends State<_Body> {
void help() {
launchUrlString(
"https://github.com/venera-app/venera/blob/master/doc/comic_source.md");
"https://github.com/venera-app/venera/blob/master/doc/comic_source.md",
);
}
Future<void> handleAddSource(String url) async {
@@ -288,11 +286,16 @@ class _BodyState extends State<_Body> {
splits.removeWhere((element) => element == "");
var fileName = splits.last;
bool cancel = false;
var controller = showLoadingDialog(App.rootContext,
onCancel: () => cancel = true, barrierDismissible: false);
var controller = showLoadingDialog(
App.rootContext,
onCancel: () => cancel = true,
barrierDismissible: false,
);
try {
var res = await AppDio()
.get<String>(url, options: Options(responseType: ResponseType.plain));
var res = await AppDio().get<String>(
url,
options: Options(responseType: ResponseType.plain),
);
if (cancel) return;
controller.close();
await addSource(res.data!, fileName);
@@ -332,6 +335,12 @@ class _ComicSourceListState extends State<_ComicSourceList> {
json = null;
});
}
if (controller.text.isEmpty) {
setState(() {
json = [];
});
return;
}
var dio = AppDio();
try {
var res = await dio.get<String>(controller.text);
@@ -343,8 +352,7 @@ class _ComicSourceListState extends State<_ComicSourceList> {
json = jsonDecode(res.data!);
});
}
}
catch(e) {
} catch (e) {
context.showMessage(message: "Network error".tl);
if (mounted) {
setState(() {
@@ -372,10 +380,7 @@ class _ComicSourceListState extends State<_ComicSourceList> {
@override
Widget build(BuildContext context) {
return PopUpWidgetScaffold(
title: "Comic Source".tl,
body: buildBody(),
);
return PopUpWidgetScaffold(title: "Comic Source".tl, body: buildBody());
}
Widget buildBody() {
@@ -399,32 +404,36 @@ class _ComicSourceListState extends State<_ComicSourceList> {
children: [
ListTile(
leading: Icon(Icons.source_outlined),
title: Text("Source URL".tl),
title: Text("Repo URL".tl),
),
TextField(
controller: controller,
decoration: InputDecoration(
hintText: "URL",
border: const UnderlineInputBorder(),
contentPadding:
const EdgeInsets.symmetric(horizontal: 12),
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
),
onChanged: (value) {
changed = true;
},
).paddingHorizontal(16).paddingBottom(8),
Text("The URL should point to a 'index.json' file".tl).paddingLeft(16),
Text("Do not report any issues related to sources to App repo.".tl).paddingLeft(16),
Text(
"The URL should point to a 'index.json' file".tl,
).paddingLeft(16),
Text(
"Do not report any issues related to sources to App repo.".tl,
).paddingLeft(16),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
controller.text = defaultComicSourceUrl;
changed = true;
launchUrlString(
"https://github.com/venera-app/venera/blob/master/doc/comic_source.md",
);
},
child: Text("Reset".tl),
child: Text("Help".tl),
),
FilledButton.tonal(
onPressed: load,
@@ -440,7 +449,11 @@ class _ComicSourceListState extends State<_ComicSourceList> {
}
if (index == 1 && json == null) {
return Center(child: CircularProgressIndicator());
return Center(
child: CircularProgressIndicator(
strokeWidth: 2,
).fixWidth(24).fixHeight(24),
);
}
index--;
@@ -551,8 +564,7 @@ void _addAllPagesWithComicSource(ComicSource source) {
!networkFavorites.contains(source.favoriteData!.key)) {
networkFavorites.add(source.favoriteData!.key);
}
if (source.searchPageData != null &&
!searchPages.contains(source.key)) {
if (source.searchPageData != null && !searchPages.contains(source.key)) {
searchPages.add(source.key);
}
@@ -594,15 +606,10 @@ class __EditFilePageState extends State<_EditFilePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: Appbar(
title: Text("Edit".tl),
),
appBar: Appbar(title: Text("Edit".tl)),
body: Column(
children: [
Container(
height: 0.6,
color: context.colorScheme.outlineVariant,
),
Container(height: 0.6, color: context.colorScheme.outlineVariant),
Expanded(
child: CodeEditor(
initialValue: current,
@@ -643,9 +650,11 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
}
void showUpdateDialog() async {
var text = ComicSourceManager().availableUpdates.entries.map((e) {
var text = ComicSourceManager().availableUpdates.entries
.map((e) {
return "${ComicSource.find(e.key)!.name}: ${e.value}";
}).join("\n");
})
.join("\n");
bool doUpdate = false;
await showDialog(
context: App.rootContext,
@@ -783,10 +792,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
child: ListTile(
title: Row(
children: [
Text(
source.name,
style: ts.s18,
),
Text(source.name, style: ts.s18),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
@@ -819,7 +825,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
style: const TextStyle(fontSize: 13),
),
),
).paddingLeft(4)
).paddingLeft(4),
],
),
trailing: Row(
@@ -864,15 +870,9 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
),
),
SliverToBoxAdapter(
child: Column(
children: buildSourceSettings().toList(),
),
),
SliverToBoxAdapter(
child: Column(
children: _buildAccount().toList(),
),
child: Column(children: buildSourceSettings().toList()),
),
SliverToBoxAdapter(child: Column(children: _buildAccount().toList())),
],
);
}
@@ -898,8 +898,10 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
}
}
} else {
current = item.value['options']
.firstWhere((e) => e['value'] == current)['text'] ??
current =
item.value['options'].firstWhere(
(e) => e['value'] == current,
)['text'] ??
current;
}
yield ListTile(
@@ -907,8 +909,9 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
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))
.map<String>(
(e) => ((e['text'] ?? e['value']) as String).ts(source.key),
)
.toList(),
onTap: (i) {
source.data['settings'][key] =
@@ -936,8 +939,11 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
source.data['settings'][key] ?? item.value['default'] ?? '';
yield ListTile(
title: Text((item.value['title'] as String).ts(source.key)),
subtitle:
Text(current, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(
current,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
@@ -978,10 +984,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
trailing: const Icon(Icons.arrow_right),
onTap: () async {
await context.to(
() => _LoginPage(
config: source.account!,
source: source,
),
() => _LoginPage(config: source.account!, source: source),
);
source.saveData();
setState(() {});
@@ -1027,9 +1030,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
trailing: loading
? const SizedBox.square(
dimension: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
);
@@ -1070,9 +1071,7 @@ class _LoginPageState extends State<_LoginPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const Appbar(
title: Text(''),
),
appBar: const Appbar(title: Text('')),
body: Center(
child: Container(
padding: const EdgeInsets.all(16),
@@ -1200,8 +1199,9 @@ class _LoginPageState extends State<_LoginPage> {
setState(() {
loading = true;
});
var cookies =
widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList();
var cookies = widget.config.cookieFields!
.map((e) => _cookies[e] ?? '')
.toList();
widget.config.validateCookies!(cookies).then((value) {
if (value) {
widget.source.data['account'] = 'ok';