From b2a164e066e74ee7f1a32cbf36bf488204e48f6b Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 18 Jun 2025 16:34:49 +0800 Subject: [PATCH] Remove the config file repository url from app. --- assets/translation.json | 4 +- doc/comic_source.md | 58 ++++++-- lib/foundation/appdata.dart | 4 +- lib/pages/comic_source_page.dart | 244 +++++++++++++++---------------- 4 files changed, 170 insertions(+), 140 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 45079e2..8f8ef7c 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -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": "清除未收藏", diff --git a/doc/comic_source.md b/doc/comic_source.md index 9d5d830..4ddecd1 100644 --- a/doc/comic_source.md +++ b/doc/comic_source.md @@ -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 diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 5600664..fb9052a 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -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"; diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 81b9622..d79a403 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -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); @@ -133,14 +126,16 @@ class _BodyState extends State<_Body> { title: const Text("Reload Configs"), actions: [ TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("cancel")), + onPressed: () => Navigator.pop(context), + child: const Text("cancel"), + ), TextButton( - onPressed: () async { - await ComicSourceManager().reload(); - App.forceRebuild(); - }, - child: const Text("continue")), + onPressed: () async { + await ComicSourceManager().reload(); + App.forceRebuild(); + }, + child: const Text("continue"), + ), ], ), ); @@ -157,8 +152,10 @@ class _BodyState extends State<_Body> { ); } - static Future update(ComicSource source, - [bool showLoading = true]) async { + static Future 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(source.url, - options: Options(responseType: ResponseType.plain)); + var res = await AppDio().get( + 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( @@ -213,12 +211,14 @@ class _BodyState extends State<_Body> { ), TextField( decoration: InputDecoration( - hintText: "URL", - border: const UnderlineInputBorder(), - contentPadding: const EdgeInsets.symmetric(horizontal: 12), - suffix: IconButton( - onPressed: () => handleAddSource(url), - icon: const Icon(Icons.check))), + hintText: "URL", + border: const UnderlineInputBorder(), + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + suffix: IconButton( + onPressed: () => handleAddSource(url), + 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 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(url, options: Options(responseType: ResponseType.plain)); + var res = await AppDio().get( + 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(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--; @@ -449,28 +462,28 @@ class _ComicSourceListState extends State<_ComicSourceList> { var action = currentKey.contains(key) ? const Icon(Icons.check, size: 20).paddingRight(8) : Button.filled( - child: Text("Add".tl), - onPressed: () async { - var fileName = json![index]["fileName"]; - var url = json![index]["url"]; - if (url == null || !(url.toString()).isURL) { - var listUrl = - appdata.settings['comicSourceListUrl'] as String; - if (listUrl - .replaceFirst("https://", "") - .replaceFirst("http://", "") - .contains("/")) { - url = - listUrl.substring(0, listUrl.lastIndexOf("/") + 1) + - fileName; - } else { - url = '$listUrl/$fileName'; - } - } - await widget.onAdd(url); - setState(() {}); - }, - ).fixHeight(32); + child: Text("Add".tl), + onPressed: () async { + var fileName = json![index]["fileName"]; + var url = json![index]["url"]; + if (url == null || !(url.toString()).isURL) { + var listUrl = + appdata.settings['comicSourceListUrl'] as String; + if (listUrl + .replaceFirst("https://", "") + .replaceFirst("http://", "") + .contains("/")) { + url = + listUrl.substring(0, listUrl.lastIndexOf("/") + 1) + + fileName; + } else { + url = '$listUrl/$fileName'; + } + } + await widget.onAdd(url); + setState(() {}); + }, + ).fixHeight(32); var description = json![index]["version"]; if (json![index]["description"] != null) { @@ -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) { - return "${ComicSource.find(e.key)!.name}: ${e.value}"; - }).join("\n"); + var text = ComicSourceManager().availableUpdates.entries + .map((e) { + return "${ComicSource.find(e.key)!.name}: ${e.value}"; + }) + .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((e) => - ((e['text'] ?? e['value']) as String).ts(source.key)) + .map( + (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';