From 0ac9ee70614dc1e99306fbb454994b4c8ba1fae1 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 14 Nov 2024 15:28:57 +0800 Subject: [PATCH 01/25] fix #37 --- lib/pages/comic_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index c12e67c..fac9c64 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -1037,6 +1037,7 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> { if (!isInitialLoading && next == null) { return; } + if(isLoading) return; Future.microtask(() { setState(() { isLoading = true; From 47eb597d965d149008ce8fa9c23300b735208a03 Mon Sep 17 00:00:00 2001 From: Pacalini <141402887+Pacalini@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:10:47 +0800 Subject: [PATCH 02/25] reader: fix start/end flipping --- lib/pages/reader/reader.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index df36314..e3938e4 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -244,7 +244,9 @@ abstract mixin class _ReaderLocation { bool toPage(int page) { if (_validatePage(page)) { if (page == this.page) { - return false; + if(!(chapter == 1 && page == 1) && !(chapter == maxChapter && page == maxPage)) { + return false; + } } this.page = page; update(); From f912e57bfd312fc04d400191414725977c6c100b Mon Sep 17 00:00:00 2001 From: AnxuNA <41771421+axlmly@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:29:46 +0800 Subject: [PATCH 03/25] Change the style of _buildBriefMode. (#44) Change the style of _buildBriefMode --- lib/components/comic.dart | 247 ++++++++++++++++--------------------- lib/components/layout.dart | 20 +-- 2 files changed, 112 insertions(+), 155 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 70c9e3e..11d994b 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -25,8 +25,7 @@ class ComicTile extends StatelessWidget { onTap!(); return; } - App.mainNavigatorKey?.currentContext - ?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey)); + App.mainNavigatorKey?.currentContext?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey)); } void onLongPress(BuildContext context) { @@ -51,8 +50,7 @@ class ComicTile extends StatelessWidget { icon: Icons.chrome_reader_mode_outlined, text: 'Details'.tl, onClick: () { - App.mainNavigatorKey?.currentContext - ?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey)); + App.mainNavigatorKey?.currentContext?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey)); }, ), MenuEntry( @@ -84,17 +82,13 @@ class ComicTile extends StatelessWidget { Widget build(BuildContext context) { var type = appdata.settings['comicDisplayMode']; - Widget child = type == 'detailed' - ? _buildDetailedMode(context) - : _buildBriefMode(context); + Widget child = type == 'detailed' ? _buildDetailedMode(context) : _buildBriefMode(context); var isFavorite = appdata.settings['showFavoriteStatusOnTile'] - ? LocalFavoritesManager() - .isExist(comic.id, ComicType(comic.sourceKey.hashCode)) + ? LocalFavoritesManager().isExist(comic.id, ComicType(comic.sourceKey.hashCode)) : false; var history = appdata.settings['showHistoryStatusOnTile'] - ? HistoryManager() - .findSync(comic.id, ComicType(comic.sourceKey.hashCode)) + ? HistoryManager().findSync(comic.id, ComicType(comic.sourceKey.hashCode)) : null; if (history?.page == 0) { history!.page = 1; @@ -138,8 +132,7 @@ class ComicTile extends StatelessWidget { constraints: const BoxConstraints(minWidth: 24), padding: const EdgeInsets.symmetric(horizontal: 4), child: CustomPaint( - painter: - _ReadingHistoryPainter(history.page, history.maxPage), + painter: _ReadingHistoryPainter(history.page, history.maxPage), ), ) ], @@ -212,9 +205,7 @@ class ComicTile extends StatelessWidget { badge: badge ?? comic.language, tags: comic.tags, maxLines: 2, - enableTranslate: ComicSource.find(comic.sourceKey) - ?.enableTagsTranslate ?? - false, + enableTranslate: ComicSource.find(comic.sourceKey)?.enableTagsTranslate ?? false, rating: comic.stars, ), ), @@ -225,75 +216,82 @@ class ComicTile extends StatelessWidget { } Widget _buildBriefMode(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), - child: Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(8), - elevation: 1, - child: Stack( - children: [ - Positioned.fill( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), + return LayoutBuilder( + builder: (context, constraints) { + return InkWell( + borderRadius: BorderRadius.circular(12), + onTap: _onTap, + onLongPress: enableLongPressed ? () => onLongPress(context) : null, + onSecondaryTapDown: (detail) => onSecondaryTap(detail, context), + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), + child: Column( + children: [ + Expanded( + // Wrap the Container with Expanded + child: SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + child: Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + clipBehavior: Clip.antiAlias, + child: buildImage(context), + ), + ), + Positioned( + bottom: 0, + right: 0, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + child: Container( + color: Colors.black.withOpacity(0.5), // 半透明黑色背景 + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), + child: Text( + comic.description.replaceAll("\n", ""), + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 12, + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + ), + ], + ), + ), ), - clipBehavior: Clip.antiAlias, - child: buildImage(context), - ), + Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 8, 0), + child: Text( + comic.title.replaceAll("\n", ""), + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14.0, + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - width: double.infinity, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.transparent, - Colors.black.withOpacity(0.3), - Colors.black.withOpacity(0.5), - ]), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(8), - bottomRight: Radius.circular(8), - ), - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), - child: Text( - comic.title.replaceAll("\n", ""), - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14.0, - color: Colors.white, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - )), - Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: _onTap, - onLongPress: - enableLongPressed ? () => onLongPress(context) : null, - onSecondaryTapDown: (detail) => - onSecondaryTap(detail, context), - borderRadius: BorderRadius.circular(8), - child: const SizedBox.expand(), - ), - ), - ) - ], - ), - ), + ), + ); + }, ); } @@ -340,9 +338,7 @@ class ComicTile extends StatelessWidget { } appdata.saveData(); context.showMessage(message: 'Blocked'.tl); - comicTileContext - .findAncestorStateOfType<_SliverGridComicsState>()! - .update(); + comicTileContext.findAncestorStateOfType<_SliverGridComicsState>()!.update(); }, child: Text('Block'.tl), ), @@ -383,8 +379,7 @@ class _ComicDescription extends StatelessWidget { s = s.replaceAll("\n", " "); } } - var enableTranslate = - App.locale.languageCode == 'zh' && this.enableTranslate; + var enableTranslate = App.locale.languageCode == 'zh' && this.enableTranslate; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -401,9 +396,7 @@ class _ComicDescription extends StatelessWidget { if (subtitle != "") Text( subtitle, - style: TextStyle( - fontSize: 10.0, - color: context.colorScheme.onSurface.withOpacity(0.7)), + style: TextStyle(fontSize: 10.0, color: context.colorScheme.onSurface.withOpacity(0.7)), maxLines: 1, softWrap: true, overflow: TextOverflow.ellipsis, @@ -440,18 +433,13 @@ class _ComicDescription extends StatelessWidget { decoration: BoxDecoration( color: s == "Unavailable" ? Theme.of(context).colorScheme.errorContainer - : Theme.of(context) - .colorScheme - .secondaryContainer, - borderRadius: - const BorderRadius.all(Radius.circular(8)), + : Theme.of(context).colorScheme.secondaryContainer, + borderRadius: const BorderRadius.all(Radius.circular(8)), ), child: Center( widthFactor: 1, child: Text( - enableTranslate - ? TagsTranslation.translateTag(s) - : s.split(':').last, + enableTranslate ? TagsTranslation.translateTag(s) : s.split(':').last, style: const TextStyle(fontSize: 12), softWrap: true, overflow: TextOverflow.ellipsis, @@ -522,20 +510,17 @@ class _ReadingHistoryPainter extends CustomPainter { textDirection: TextDirection.ltr, ); textPainter.layout(); - textPainter.paint( - canvas, - Offset((size.width - textPainter.width) / 2, - (size.height - textPainter.height) / 2)); + textPainter.paint(canvas, Offset((size.width - textPainter.width) / 2, (size.height - textPainter.height) / 2)); } else if (page == maxPage) { // 在中央绘制勾 final paint = Paint() ..color = Colors.white ..strokeWidth = 2 ..style = PaintingStyle.stroke; - canvas.drawLine(Offset(size.width * 0.2, size.height * 0.5), - Offset(size.width * 0.45, size.height * 0.75), paint); - canvas.drawLine(Offset(size.width * 0.45, size.height * 0.75), - Offset(size.width * 0.85, size.height * 0.3), paint); + canvas.drawLine( + Offset(size.width * 0.2, size.height * 0.5), Offset(size.width * 0.45, size.height * 0.75), paint); + canvas.drawLine( + Offset(size.width * 0.45, size.height * 0.75), Offset(size.width * 0.85, size.height * 0.3), paint); } else { // 在左上角绘制page, 在右下角绘制maxPage final textPainter = TextPainter( @@ -561,18 +546,13 @@ class _ReadingHistoryPainter extends CustomPainter { textDirection: TextDirection.ltr, ); textPainter2.layout(); - textPainter2.paint( - canvas, - Offset(size.width - textPainter2.width, - size.height - textPainter2.height)); + textPainter2.paint(canvas, Offset(size.width - textPainter2.width, size.height - textPainter2.height)); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { - return oldDelegate is! _ReadingHistoryPainter || - oldDelegate.page != page || - oldDelegate.maxPage != maxPage; + return oldDelegate is! _ReadingHistoryPainter || oldDelegate.page != page || oldDelegate.maxPage != maxPage; } } @@ -683,8 +663,7 @@ class _SliverGridComics extends StatelessWidget { onLastItemBuild?.call(); } var badge = badgeBuilder?.call(comics[index]); - var isSelected = - selection == null ? false : selection![comics[index]] ?? false; + var isSelected = selection == null ? false : selection![comics[index]] ?? false; var comic = ComicTile( comic: comics[index], badge: badge, @@ -693,9 +672,7 @@ class _SliverGridComics extends StatelessWidget { ); return Container( decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).colorScheme.surfaceContainer - : null, + color: isSelected ? Theme.of(context).colorScheme.surfaceContainer : null, borderRadius: BorderRadius.circular(12), ), margin: const EdgeInsets.all(4), @@ -820,9 +797,7 @@ class ComicListState extends State { decoration: InputDecoration( labelText: "Page".tl, ), - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly - ], + inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (v) { value = v; }, @@ -835,15 +810,13 @@ class ComicListState extends State { if (page == null) { context.showMessage(message: "Invalid page".tl); } else { - if (page > 0 && - (_maxPage == null || page <= _maxPage!)) { + if (page > 0 && (_maxPage == null || page <= _maxPage!)) { setState(() { _error = null; _page = page; }); } else { - context.showMessage( - message: "Invalid page".tl); + context.showMessage(message: "Invalid page".tl); } } }, @@ -855,8 +828,7 @@ class ComicListState extends State { ); }, child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Text("Page $_page / ${_maxPage ?? '?'}"), ), ), @@ -987,8 +959,7 @@ class ComicListState extends State { comics: _data[_page] ?? const [], menuBuilder: widget.menuBuilder, ), - if (_data[_page]!.length > 6 && _maxPage != 1) - _buildSliverPageSelector(), + if (_data[_page]!.length > 6 && _maxPage != 1) _buildSliverPageSelector(), if (widget.trailingSliver != null) widget.trailingSliver!, ], ); @@ -1149,20 +1120,15 @@ class _RatingWidgetState extends State { if (!widget.selectable) { return; } - if (dx >= - widget.size * widget.count + widget.padding * (widget.count - 1)) { + if (dx >= widget.size * widget.count + widget.padding * (widget.count - 1)) { value = widget.maxRating; } else { for (double i = 1; i < widget.count + 1; i++) { - if (dx > widget.size * i + widget.padding * (i - 1) && - dx < widget.size * i + widget.padding * i) { + if (dx > widget.size * i + widget.padding * (i - 1) && dx < widget.size * i + widget.padding * i) { value = i * (widget.maxRating / widget.count); break; - } else if (dx > widget.size * (i - 1) + widget.padding * (i - 1) && - dx < widget.size * i + widget.padding * i) { - value = (dx - widget.padding * (i - 1)) / - (widget.size * widget.count) * - widget.maxRating; + } else if (dx > widget.size * (i - 1) + widget.padding * (i - 1) && dx < widget.size * i + widget.padding * i) { + value = (dx - widget.padding * (i - 1)) / (widget.size * widget.count) * widget.maxRating; break; } } @@ -1190,8 +1156,7 @@ class _RatingWidgetState extends State { if (widget.count / fullStars() == widget.maxRating / value) { return 0; } - return (value % (widget.maxRating / widget.count)) / - (widget.maxRating / widget.count); + return (value % (widget.maxRating / widget.count)) / (widget.maxRating / widget.count); } List buildRow() { diff --git a/lib/components/layout.dart b/lib/components/layout.dart index fa65475..1d098a2 100644 --- a/lib/components/layout.dart +++ b/lib/components/layout.dart @@ -2,10 +2,7 @@ part of 'components.dart'; class SliverGridViewWithFixedItemHeight extends StatelessWidget { const SliverGridViewWithFixedItemHeight( - {required this.delegate, - required this.maxCrossAxisExtent, - required this.itemHeight, - super.key}); + {required this.delegate, required this.maxCrossAxisExtent, required this.itemHeight, super.key}); final SliverChildDelegate delegate; @@ -65,8 +62,7 @@ class SliverGridDelegateWithFixedHeight extends SliverGridDelegate { @override bool shouldRelayout(covariant SliverGridDelegate oldDelegate) { if (oldDelegate is! SliverGridDelegateWithFixedHeight) return true; - if (oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent || - oldDelegate.itemHeight != itemHeight) { + if (oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent || oldDelegate.itemHeight != itemHeight) { return true; } return false; @@ -95,8 +91,7 @@ class SliverGridDelegateWithComics extends SliverGridDelegate { } } - SliverGridLayout getDetailedModeLayout( - SliverConstraints constraints, double scale) { + SliverGridLayout getDetailedModeLayout(SliverConstraints constraints, double scale) { const minCrossAxisExtent = 360; final itemHeight = 152 * scale; final width = constraints.crossAxisExtent; @@ -111,14 +106,11 @@ class SliverGridDelegateWithComics extends SliverGridDelegate { reverseCrossAxis: false); } - SliverGridLayout getBriefModeLayout( - SliverConstraints constraints, double scale) { + SliverGridLayout getBriefModeLayout(SliverConstraints constraints, double scale) { final maxCrossAxisExtent = 192.0 * scale; - const childAspectRatio = 0.72; + const childAspectRatio = 0.68; const crossAxisSpacing = 0.0; - int crossAxisCount = - (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)) - .ceil(); + int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil(); // Ensure a minimum count of 1, can be zero and result in an infinite extent // below when the window size is 0. crossAxisCount = math.max(1, crossAxisCount); From 65b41b287323f09f4c2ad9f08ec2ba0ddd4fe383 Mon Sep 17 00:00:00 2001 From: boa <42885162+boa-z@users.noreply.github.com> Date: Thu, 14 Nov 2024 20:40:28 +0800 Subject: [PATCH 04/25] add option to ignore certificate errors (#46) add option to ignore certificate errors --- assets/translation.json | 6 ++++-- lib/foundation/appdata.dart | 1 + lib/network/app_dio.dart | 19 +++++++++++++------ lib/pages/settings/network.dart | 17 +++++++++++++---- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 9917cd2..14604bb 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -209,7 +209,8 @@ "Update Comics Info": "更新漫画信息", "Create Folder": "新建文件夹", "Select an image on screen": "选择屏幕上的图片", - "Added @count comics to download queue.": "已添加 @count 本漫画到下载队列" + "Added @count comics to download queue.": "已添加 @count 本漫画到下载队列", + "Ignore Certificate Errors": "忽略证书错误" }, "zh_TW": { "Home": "首頁", @@ -421,6 +422,7 @@ "Update Comics Info": "更新漫畫信息", "Create Folder": "新建文件夾", "Select an image on screen": "選擇屏幕上的圖片", - "Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列" + "Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列", + "Ignore Certificate Errors": "忽略證書錯誤" } } \ No newline at end of file diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index afd1522..1cc7ed3 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -119,6 +119,7 @@ class _Settings with ChangeNotifier { 'quickFavorite': null, 'enableTurnPageByVolumeKey': true, 'enableClockAndBatteryInfoInReader': true, + 'ignoreCertificateErrors': false, }; operator [](String key) { diff --git a/lib/network/app_dio.dart b/lib/network/app_dio.dart index 4608952..26db4da 100644 --- a/lib/network/app_dio.dart +++ b/lib/network/app_dio.dart @@ -106,6 +106,7 @@ class MyLogInterceptor implements Interceptor { class AppDio with DioMixin { String? _proxy = proxy; + static bool get ignoreCertificateErrors => appdata.settings['ignoreCertificateErrors'] == true; AppDio([BaseOptions? options]) { this.options = options ?? BaseOptions(); @@ -123,6 +124,7 @@ class AppDio with DioMixin { client.idleTimeout = const Duration(seconds: 100); client.badCertificateCallback = (X509Certificate cert, String host, int port) { + if (ignoreCertificateErrors) return true; if (host.contains("cdn")) return true; 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})$'); @@ -189,8 +191,8 @@ class AppDio with DioMixin { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { - if(options?.headers?['prevent-parallel'] == 'true') { - while(_requests.containsKey(path)) { + if (options?.headers?['prevent-parallel'] == 'true') { + while (_requests.containsKey(path)) { await Future.delayed(const Duration(milliseconds: 20)); } _requests[path] = true; @@ -204,6 +206,9 @@ class AppDio with DioMixin { proxySettings: proxy == null ? const rhttp.ProxySettings.noProxy() : rhttp.ProxySettings.proxy(proxy!), + tlsSettings: rhttp.TlsSettings( + verifyCertificates: !ignoreCertificateErrors, + ), )); } try { @@ -216,9 +221,8 @@ class AppDio with DioMixin { onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, ); - } - finally { - if(_requests.containsKey(path)) { + } finally { + if (_requests.containsKey(path)) { _requests.remove(path); } } @@ -237,6 +241,9 @@ class RHttpAdapter implements HttpClientAdapter { keepAlivePing: Duration(seconds: 30), ), throwOnStatusCode: false, + tlsSettings: rhttp.TlsSettings( + verifyCertificates: !AppDio.ignoreCertificateErrors, + ), ); } @@ -284,7 +291,7 @@ class RHttpAdapter implements HttpClientAdapter { headers[key]!.add(entry.$2); } var data = res.body; - if(headers['content-encoding']?.contains('gzip') ?? false) { + if (headers['content-encoding']?.contains('gzip') ?? false) { // rhttp does not support gzip decoding data = gzip.decoder.bind(data).map((data) => Uint8List.fromList(data)); } diff --git a/lib/pages/settings/network.dart b/lib/pages/settings/network.dart index 21042ea..9396b7e 100644 --- a/lib/pages/settings/network.dart +++ b/lib/pages/settings/network.dart @@ -38,14 +38,11 @@ class _ProxySettingView extends StatefulWidget { class _ProxySettingViewState extends State<_ProxySettingView> { String type = ''; - String host = ''; - String port = ''; - String username = ''; - String password = ''; + bool ignoreCertificateErrors = false; // USERNAME:PASSWORD@HOST:PORT String toProxyStr() { @@ -103,6 +100,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> { void initState() { var proxy = appdata.settings['proxy']; parseProxyString(proxy); + ignoreCertificateErrors = appdata.settings['ignoreCertificateErrors'] ?? false; super.initState(); } @@ -148,6 +146,17 @@ class _ProxySettingViewState extends State<_ProxySettingView> { }, ), if (type == 'manual') buildManualProxy(), + SwitchListTile( + title: Text("Ignore Certificate Errors".tl), + value: ignoreCertificateErrors, + onChanged: (v) { + setState(() { + ignoreCertificateErrors = v; + }); + appdata.settings['ignoreCertificateErrors'] = ignoreCertificateErrors; + appdata.saveData(); + }, + ), ], ), ), From edff9c7a0cb4988bd172552e8e48940731c79c57 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 15 Nov 2024 17:03:28 +0800 Subject: [PATCH 05/25] fix config update issue --- lib/foundation/comic_source/parser.dart | 8 +++++--- lib/pages/comic_source_page.dart | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index b02c54f..23b8d37 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -157,9 +157,11 @@ class ComicSourceParser { await source.loadData(); - Future.delayed(const Duration(milliseconds: 50), () { - JsEngine().runCode("ComicSource.sources.$_key.init()"); - }); + if(_checkExists("init")) { + Future.delayed(const Duration(milliseconds: 50), () { + JsEngine().runCode("ComicSource.sources.$_key.init()"); + }); + } return source; } diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 95c5f67..2657339 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -55,10 +55,10 @@ class ComicSourcePage extends StatefulWidget { title: "Updates Available".tl, content: msg, confirmText: "Update", - onConfirm: () { + onConfirm: () async { for (var key in shouldUpdate) { var source = ComicSource.find(key); - _BodyState.update(source!); + await _BodyState.update(source!); } }, ); @@ -277,7 +277,7 @@ class _BodyState extends State<_Body> { } } - static void update(ComicSource source) async { + static Future update(ComicSource source) async { if (!source.url.isURL) { App.rootContext.showMessage(message: "Invalid url config"); return; From 165e5f28504b0f60f6923b52a0290c6ffb24dae4 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 15 Nov 2024 18:27:59 +0800 Subject: [PATCH 06/25] add authorization --- android/app/src/main/AndroidManifest.xml | 1 + .../com/github/wgh136/venera/MainActivity.kt | 165 +++++++++--------- assets/translation.json | 6 +- ios/Runner/Info.plist | 2 + lib/foundation/appdata.dart | 1 + lib/main.dart | 66 ++++++- lib/pages/auth_page.dart | 60 +++++++ lib/pages/settings/app.dart | 27 ++- lib/pages/settings/setting_components.dart | 5 +- lib/pages/settings/settings_page.dart | 1 + pubspec.lock | 48 +++++ pubspec.yaml | 1 + windows/build.iss | 1 + 13 files changed, 287 insertions(+), 97 deletions(-) create mode 100644 lib/pages/auth_page.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4319b96..2d833ce 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + Unit)? = null - private val selectFileCode = 0x11 - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == pickDirectoryCode) { - if(resultCode != Activity.RESULT_OK) { - result.success(null) - return - } - val pickedDirectoryUri = data?.data - if (pickedDirectoryUri == null) { - result.success(null) - return - } - Thread { - try { - result.success(onPickedDirectory(pickedDirectoryUri)) - } - catch (e: Exception) { - result.error("Failed to Copy Files", e.toString(), null) - } - }.start() - } else if (requestCode == storageRequestCode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - storagePermissionRequest?.invoke(Environment.isExternalStorageManager()) - } - storagePermissionRequest = null - } else if (requestCode == selectFileCode) { - if (resultCode != Activity.RESULT_OK) { - result.success(null) - return - } - val uri = data?.data - if (uri == null) { - result.success(null) - return - } - val contentResolver = context.contentResolver - val file = DocumentFile.fromSingleUri(context, uri) - if (file == null) { - result.success(null) - return - } - val fileName = file.name - if (fileName == null) { - result.success(null) - return - } - // copy file to cache directory - val cacheDir = context.cacheDir - val newFile = File(cacheDir, fileName) - val inputStream = contentResolver.openInputStream(uri) - if (inputStream == null) { - result.success(null) - return - } - val outputStream = FileOutputStream(newFile) - inputStream.copyTo(outputStream) - inputStream.close() - outputStream.close() - // send file path to flutter - result.success(newFile.absolutePath) - } - } - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) MethodChannel( @@ -115,12 +49,30 @@ class MainActivity : FlutterActivity() { } res.success(null) } + "getDirectoryPath" -> { - this.result = res val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - startActivityForResult(intent, pickDirectoryCode) + val launcher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> + if (activityResult.resultCode != Activity.RESULT_OK) { + result.success(null) + } + val pickedDirectoryUri = activityResult.data?.data + if (pickedDirectoryUri == null) + result.success(null) + else + Thread { + try { + result.success(onPickedDirectory(pickedDirectoryUri)) + } catch (e: Exception) { + result.error("Failed to Copy Files", e.toString(), null) + } + }.start() + } + launcher.launch(intent) } + else -> res.notImplemented() } } @@ -137,6 +89,7 @@ class MainActivity : FlutterActivity() { events.success(2) } } + override fun onCancel(arguments: Any?) { listening = false } @@ -144,7 +97,7 @@ class MainActivity : FlutterActivity() { val storageChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/storage") storageChannel.setMethodCallHandler { _, res -> - requestStoragePermission {result -> + requestStoragePermission { result -> res.success(result) } } @@ -167,12 +120,13 @@ class MainActivity : FlutterActivity() { } override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { - if(listening){ + if (listening) { when (keyCode) { KeyEvent.KEYCODE_VOLUME_DOWN -> { volumeListen.down() return true } + KeyEvent.KEYCODE_VOLUME_UP -> { volumeListen.up() return true @@ -184,8 +138,8 @@ class MainActivity : FlutterActivity() { /// copy the directory to tmp directory, return copied directory private fun onPickedDirectory(uri: Uri): String { - val contentResolver = context.contentResolver - var tmp = context.cacheDir + val contentResolver = contentResolver + var tmp = cacheDir tmp = File(tmp, "getDirectoryPathTemp") tmp.mkdir() copyDirectory(contentResolver, uri, tmp) @@ -194,9 +148,9 @@ class MainActivity : FlutterActivity() { } private fun copyDirectory(resolver: ContentResolver, srcUri: Uri, destDir: File) { - val src = DocumentFile.fromTreeUri(context, srcUri) ?: return + val src = DocumentFile.fromTreeUri(this, srcUri) ?: return for (file in src.listFiles()) { - if(file.isDirectory) { + if (file.isDirectory) { val newDir = File(destDir, file.name!!) newDir.mkdir() copyDirectory(resolver, file.uri, newDir) @@ -212,7 +166,7 @@ class MainActivity : FlutterActivity() { } private fun requestStoragePermission(result: (Boolean) -> Unit) { - if(Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { val readPermission = ContextCompat.checkSelfPermission( this, Manifest.permission.READ_EXTERNAL_STORAGE @@ -241,8 +195,11 @@ class MainActivity : FlutterActivity() { try { val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) intent.addCategory("android.intent.category.DEFAULT") - intent.data = Uri.parse("package:" + context.packageName) - startActivityForResult(intent, storageRequestCode) + intent.data = Uri.parse("package:$packageName") + val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ -> + result(Environment.isExternalStorageManager()) + } + launcher.launch(intent) } catch (e: Exception) { result(false) } @@ -258,7 +215,7 @@ class MainActivity : FlutterActivity() { grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if(requestCode == storageRequestCode) { + if (requestCode == storageRequestCode) { storagePermissionRequest?.invoke(grantResults.all { it == PackageManager.PERMISSION_GRANTED }) @@ -266,21 +223,57 @@ class MainActivity : FlutterActivity() { } } - fun openFile() { + private fun openFile() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.type = "*/*" - startActivityForResult(intent, selectFileCode) + val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> + if (activityResult.resultCode != Activity.RESULT_OK) { + result.success(null) + } + val uri = activityResult.data?.data + if (uri == null) { + result.success(null) + return@registerForActivityResult + } + val contentResolver = contentResolver + val file = DocumentFile.fromSingleUri(this, uri) + if (file == null) { + result.success(null) + return@registerForActivityResult + } + val fileName = file.name + if (fileName == null) { + result.success(null) + return@registerForActivityResult + } + // copy file to cache directory + val cacheDir = cacheDir + val newFile = File(cacheDir, fileName) + val inputStream = contentResolver.openInputStream(uri) + if (inputStream == null) { + result.success(null) + return@registerForActivityResult + } + val outputStream = FileOutputStream(newFile) + inputStream.copyTo(outputStream) + inputStream.close() + outputStream.close() + // send file path to flutter + result.success(newFile.absolutePath) + } + launcher.launch(intent) } } -class VolumeListen{ +class VolumeListen { var onUp = fun() {} var onDown = fun() {} - fun up(){ + fun up() { onUp() } - fun down(){ + + fun down() { onDown() } } diff --git a/assets/translation.json b/assets/translation.json index 14604bb..96579de 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -210,7 +210,8 @@ "Create Folder": "新建文件夹", "Select an image on screen": "选择屏幕上的图片", "Added @count comics to download queue.": "已添加 @count 本漫画到下载队列", - "Ignore Certificate Errors": "忽略证书错误" + "Ignore Certificate Errors": "忽略证书错误", + "Authorization Required": "需要身份验证" }, "zh_TW": { "Home": "首頁", @@ -423,6 +424,7 @@ "Create Folder": "新建文件夾", "Select an image on screen": "選擇屏幕上的圖片", "Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列", - "Ignore Certificate Errors": "忽略證書錯誤" + "Ignore Certificate Errors": "忽略證書錯誤", + "Authorization Required": "需要身份驗證" } } \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 93a0af2..be8d5d6 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -51,5 +51,7 @@ LSSupportsOpeningDocumentsInPlace + NSFaceIDUsageDescription + Ensure that the operation is being performed by the user themselves. diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 1cc7ed3..8c5d347 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -120,6 +120,7 @@ class _Settings with ChangeNotifier { 'enableTurnPageByVolumeKey': true, 'enableClockAndBatteryInfoInReader': true, 'ignoreCertificateErrors': false, + 'authorizationRequired': false, }; operator [](String key) { diff --git a/lib/main.dart b/lib/main.dart index 7351392..2000267 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:rhttp/rhttp.dart'; import 'package:venera/foundation/log.dart'; -import 'package:venera/network/app_dio.dart'; +import 'package:venera/pages/auth_page.dart'; import 'package:venera/pages/comic_source_page.dart'; import 'package:venera/pages/main_page.dart'; import 'package:venera/pages/settings/settings_page.dart'; @@ -65,15 +65,59 @@ class MyApp extends StatefulWidget { State createState() => _MyAppState(); } -class _MyAppState extends State { +class _MyAppState extends State with WidgetsBindingObserver { @override void initState() { checkUpdates(); App.registerForceRebuild(forceRebuild); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + WidgetsBinding.instance.addObserver(this); super.initState(); } + bool isAuthPageActive = false; + + OverlayEntry? hideContentOverlay; + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if(!App.isMobile) { + return; + } + if (state == AppLifecycleState.inactive && hideContentOverlay == null) { + hideContentOverlay = OverlayEntry( + builder: (context) { + return Positioned.fill( + child: Container( + width: double.infinity, + height: double.infinity, + color: App.rootContext.colorScheme.surface, + ), + ); + }, + ); + Overlay.of(App.rootContext).insert(hideContentOverlay!); + } else if (hideContentOverlay != null && + state == AppLifecycleState.resumed) { + hideContentOverlay!.remove(); + hideContentOverlay = null; + } + if (state == AppLifecycleState.hidden && + appdata.settings['authorizationRequired'] && + !isAuthPageActive) { + isAuthPageActive = true; + App.rootContext.to( + () => AuthPage( + onSuccessfulAuth: () { + App.rootContext.pop(); + isAuthPageActive = false; + }, + ), + ); + } + super.didChangeAppLifecycleState(state); + } + void forceRebuild() { void rebuild(Element el) { el.markNeedsBuild(); @@ -86,14 +130,25 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { + Widget home; + if (appdata.settings['authorizationRequired']) { + home = AuthPage( + onSuccessfulAuth: () { + App.rootContext.toReplacement(() => const MainPage()); + }, + ); + } else { + home = const MainPage(); + } return MaterialApp( - home: const MainPage(), + home: home, debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: App.mainColor, surface: Colors.white, primary: App.mainColor.shade600, + // ignore: deprecated_member_use background: Colors.white, ), fontFamily: App.isWindows ? "Microsoft YaHei" : null, @@ -105,6 +160,7 @@ class _MyAppState extends State { brightness: Brightness.dark, surface: Colors.black, primary: App.mainColor.shade400, + // ignore: deprecated_member_use background: Colors.black, ), fontFamily: App.isWindows ? "Microsoft YaHei" : null, @@ -171,12 +227,12 @@ class _MyAppState extends State { } void checkUpdates() async { - if(!appdata.settings['checkUpdateOnStart']) { + if (!appdata.settings['checkUpdateOnStart']) { return; } var lastCheck = appdata.implicitData['lastCheckUpdate'] ?? 0; var now = DateTime.now().millisecondsSinceEpoch; - if(now - lastCheck < 24 * 60 * 60 * 1000) { + if (now - lastCheck < 24 * 60 * 60 * 1000) { return; } appdata.implicitData['lastCheckUpdate'] = now; diff --git a/lib/pages/auth_page.dart b/lib/pages/auth_page.dart new file mode 100644 index 0000000..67b0d29 --- /dev/null +++ b/lib/pages/auth_page.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:venera/utils/translations.dart'; + +class AuthPage extends StatefulWidget { + const AuthPage({super.key, this.onSuccessfulAuth}); + + final void Function()? onSuccessfulAuth; + + @override + State createState() => _AuthPageState(); +} + +class _AuthPageState extends State { + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (!didPop) { + SystemNavigator.pop(); + } + }, + child: Material( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.security, size: 36), + const SizedBox(height: 16), + Text("Authentication Required".tl), + const SizedBox(height: 16), + FilledButton( + onPressed: auth, + child: Text("Continue".tl), + ), + ], + ), + ), + ), + ); + } + + void auth() async { + var localAuth = LocalAuthentication(); + var canCheckBiometrics = await localAuth.canCheckBiometrics; + if (!canCheckBiometrics && !await localAuth.isDeviceSupported()) { + widget.onSuccessfulAuth?.call(); + return; + } + var isAuthorized = await localAuth.authenticate( + localizedReason: "Please authenticate to continue".tl, + ); + if (isAuthorized) { + widget.onSuccessfulAuth?.call(); + } + } +} diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index c491f51..b35301f 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -36,12 +36,12 @@ class _AppSettingsState extends State { if (App.isAndroid) { var channel = const MethodChannel("venera/storage"); var permission = await channel.invokeMethod(''); - if(permission != true) { + if (permission != true) { context.showMessage(message: "Permission denied".tl); return; } var path = await selectDirectory(); - if(path != null) { + if (path != null) { // check if the path is writable var testFile = File(FilePath.join(path, "test")); try { @@ -177,6 +177,29 @@ class _AppSettingsState extends State { App.forceRebuild(); }, ).toSliver(), + if (!App.isLinux) + _SwitchSetting( + title: "Authorization Required".tl, + settingKey: "authorizationRequired", + onChanged: () async { + var current = appdata.settings['authorizationRequired']; + if (current) { + final auth = LocalAuthentication(); + final bool canAuthenticateWithBiometrics = + await auth.canCheckBiometrics; + final bool canAuthenticate = canAuthenticateWithBiometrics || + await auth.isDeviceSupported(); + if (!canAuthenticate) { + context.showMessage(message: "Biometrics not supported".tl); + setState(() { + appdata.settings['authorizationRequired'] = false; + }); + appdata.saveData(); + return; + } + } + }, + ).toSliver(), ], ); } diff --git a/lib/pages/settings/setting_components.dart b/lib/pages/settings/setting_components.dart index ee14ddc..6472ca8 100644 --- a/lib/pages/settings/setting_components.dart +++ b/lib/pages/settings/setting_components.dart @@ -33,9 +33,10 @@ class _SwitchSettingState extends State<_SwitchSetting> { onChanged: (value) { setState(() { appdata.settings[widget.settingKey] = value; - appdata.saveData(); }); - widget.onChanged?.call(); + appdata.saveData().then((_) { + widget.onChanged?.call(); + }); }, ), ); diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index 889a7c6..04a0215 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; +import 'package:local_auth/local_auth.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; diff --git a/pubspec.lock b/pubspec.lock index 88ac6d3..22bde61 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -356,6 +356,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + url: "https://pub.dev" + source: hosted + version: "2.0.23" flutter_qjs: dependency: "direct main" description: @@ -512,6 +520,46 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + local_auth: + dependency: "direct main" + description: + name: local_auth + sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92" + url: "https://pub.dev" + source: hosted + version: "1.0.46" + local_auth_darwin: + dependency: transitive + description: + name: local_auth_darwin + sha256: "6d2950da311d26d492a89aeb247c72b4653ddc93601ea36a84924a396806d49c" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 + url: "https://pub.dev" + source: hosted + version: "1.0.11" lodepng_flutter: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 89e929b..19e08db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: url: https://github.com/wgh136/webdav_client ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1 battery_plus: ^6.2.0 + local_auth: ^2.3.0 dev_dependencies: flutter_test: diff --git a/windows/build.iss b/windows/build.iss index 01d6c2b..1e4ec0d 100644 --- a/windows/build.iss +++ b/windows/build.iss @@ -54,6 +54,7 @@ Source: "{#RootPath}\build\windows\x64\runner\Release\url_launcher_windows_plugi Source: "{#RootPath}\build\windows\x64\runner\Release\battery_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\screen_retriever_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#RootPath}\build\windows\x64\runner\Release\local_auth_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion Source: "{#RootPath}\build\windows\x64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion From d749e7421e8e5f1baa16ba46fce450c86bd71abd Mon Sep 17 00:00:00 2001 From: AnxuNA <41771421+axlmly@users.noreply.github.com> Date: Fri, 15 Nov 2024 21:16:17 +0800 Subject: [PATCH 07/25] Open in Browser Translation (#49) Open in Browser Translation --- assets/translation.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/translation.json b/assets/translation.json index 96579de..0fd80a4 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -47,6 +47,7 @@ "Select file": "选择文件", "View list": "查看列表", "Open help": "打开帮助", + "Open in Browser": "打开网页", "Check updates": "检查更新", "Edit": "编辑", "Update": "更新", @@ -263,6 +264,7 @@ "Select file": "選擇文件", "View list": "查看列表", "Open help": "打開幫助", + "Open in Browser": "打開網頁", "Check updates": "檢查更新", "Edit": "編輯", "Update": "更新", From 8513a739ecde35c01b9923ba511655b8f3602a3a Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 15 Nov 2024 22:14:31 +0800 Subject: [PATCH 08/25] When AppLifecycleState is changed to resumed, check for data updates. --- lib/pages/home_page.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index f73446c..097b9de 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -89,11 +89,13 @@ class _SyncDataWidget extends StatefulWidget { State<_SyncDataWidget> createState() => _SyncDataWidgetState(); } -class _SyncDataWidgetState extends State<_SyncDataWidget> { +class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObserver { @override void initState() { super.initState(); DataSync().addListener(update); + WidgetsBinding.instance.addObserver(this); + lastCheck = DateTime.now(); } void update() { @@ -106,6 +108,20 @@ class _SyncDataWidgetState extends State<_SyncDataWidget> { void dispose() { super.dispose(); DataSync().removeListener(update); + WidgetsBinding.instance.removeObserver(this); + } + + late DateTime lastCheck; + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + if(state == AppLifecycleState.resumed) { + if(DateTime.now().difference(lastCheck) > const Duration(minutes: 10)) { + lastCheck = DateTime.now(); + DataSync().downloadData(); + } + } } @override From 7bc0aeb4affb1956e9fc27b78a7c986e52894b61 Mon Sep 17 00:00:00 2001 From: Pacalini <141402887+Pacalini@users.noreply.github.com> Date: Sat, 16 Nov 2024 16:07:39 +0800 Subject: [PATCH 09/25] tool bar: RtL slider & button swap (#50) --- lib/components/custom_slider.dart | 224 ++++++++++++++++++++++++++++++ lib/pages/reader/reader.dart | 1 + lib/pages/reader/scaffold.dart | 42 +++--- 3 files changed, 244 insertions(+), 23 deletions(-) create mode 100644 lib/components/custom_slider.dart diff --git a/lib/components/custom_slider.dart b/lib/components/custom_slider.dart new file mode 100644 index 0000000..ae9f04f --- /dev/null +++ b/lib/components/custom_slider.dart @@ -0,0 +1,224 @@ +import 'package:flutter/material.dart'; + +/// patched slider.dart with RtL support +class _SliderDefaultsM3 extends SliderThemeData { + _SliderDefaultsM3(this.context) + : super(trackHeight: 4.0); + + final BuildContext context; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + Color? get activeTrackColor => _colors.primary; + + @override + Color? get inactiveTrackColor => _colors.surfaceContainerHighest; + + @override + Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54); + + @override + Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38); + + @override + Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12); + + @override + Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.12); + + @override + Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(0.38); + + @override + Color? get inactiveTickMarkColor => _colors.onSurfaceVariant.withOpacity(0.38); + + @override + Color? get disabledActiveTickMarkColor => _colors.onSurface.withOpacity(0.38); + + @override + Color? get disabledInactiveTickMarkColor => _colors.onSurface.withOpacity(0.38); + + @override + Color? get thumbColor => _colors.primary; + + @override + Color? get disabledThumbColor => Color.alphaBlend(_colors.onSurface.withOpacity(0.38), _colors.surface); + + @override + Color? get overlayColor => WidgetStateColor.resolveWith((Set states) { + if (states.contains(WidgetState.dragged)) { + return _colors.primary.withOpacity(0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.primary.withOpacity(0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.primary.withOpacity(0.1); + } + + return Colors.transparent; + }); + + @override + TextStyle? get valueIndicatorTextStyle => Theme.of(context).textTheme.labelMedium!.copyWith( + color: _colors.onPrimary, + ); + + @override + SliderComponentShape? get valueIndicatorShape => const DropSliderValueIndicatorShape(); +} + +class CustomSlider extends StatefulWidget { + const CustomSlider({required this.min, required this.max, required this.value, required this.divisions, required this.onChanged, required this.focusNode, this.reversed = false, super.key}); + + final double min; + + final double max; + + final double value; + + final int divisions; + + final void Function(double) onChanged; + + final FocusNode? focusNode; + + final bool reversed; + + @override + State createState() => _CustomSliderState(); +} + +class _CustomSliderState extends State { + late double value; + + @override + void initState() { + super.initState(); + value = widget.value; + } + + @override + void didUpdateWidget(CustomSlider oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.value != oldWidget.value) { + setState(() { + value = widget.value; + }); + } + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final theme = _SliderDefaultsM3(context); + return Padding( + padding: const EdgeInsets.fromLTRB(24, 12, 24, 12), + child: widget.max - widget.min > 0 ? LayoutBuilder( + builder: (context, constraints) => MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTapDown: (details){ + var dx = details.localPosition.dx; + if(widget.reversed){ + dx = constraints.maxWidth - dx; + } + var gap = constraints.maxWidth / widget.divisions; + var gapValue = (widget.max - widget.min) / widget.divisions; + widget.onChanged.call((dx / gap).round() * gapValue + widget.min); + }, + onVerticalDragUpdate: (details){ + var dx = details.localPosition.dx; + if(dx > constraints.maxWidth || dx < 0) return; + if(widget.reversed){ + dx = constraints.maxWidth - dx; + } + var gap = constraints.maxWidth / widget.divisions; + var gapValue = (widget.max - widget.min) / widget.divisions; + widget.onChanged.call((dx / gap).round() * gapValue + widget.min); + }, + child: SizedBox( + height: 24, + child: Center( + child: SizedBox( + height: 24, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned.fill( + child: Center( + child: Container( + width: double.infinity, + height: 6, + decoration: BoxDecoration( + color: theme.inactiveTrackColor, + borderRadius: const BorderRadius.all(Radius.circular(10)) + ), + ), + ), + ), + if(constraints.maxWidth / widget.divisions > 10) + Positioned.fill( + child: Row( + children: (){ + var res = []; + for(int i = 0; i { bool get isOpen => _isOpen; + bool get isReversed => context.reader.mode == ReaderMode.galleryRightToLeft || + context.reader.mode == ReaderMode.continuousRightToLeft; + int showFloatingButtonValue = 0; var lastValue = 0; @@ -217,34 +220,26 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { children: [ const SizedBox(width: 8), IconButton.filledTonal( - onPressed: () { - if (!context.reader.toPrevChapter()) { - context.reader.toPage(1); - } else { - if (showFloatingButtonValue != 0) { - setState(() { - showFloatingButtonValue = 0; - }); - } - } - }, + onPressed: () => !isReversed + ? context.reader.chapter > 1 + ? context.reader.toPrevChapter() + : context.reader.toPage(1) + : context.reader.chapter < context.reader.maxChapter + ? context.reader.toNextChapter() + : context.reader.toPage(context.reader.maxPage), icon: const Icon(Icons.first_page), ), Expanded( child: buildSlider(), ), IconButton.filledTonal( - onPressed: () { - if (!context.reader.toNextChapter()) { - context.reader.toPage(context.reader.maxPage); - } else { - if (showFloatingButtonValue != 0) { - setState(() { - showFloatingButtonValue = 0; - }); - } - } - }, + onPressed: () => !isReversed + ? context.reader.chapter < context.reader.maxChapter + ? context.reader.toNextChapter() + : context.reader.toPage(context.reader.maxPage) + : context.reader.chapter > 1 + ? context.reader.toPrevChapter() + : context.reader.toPage(1), icon: const Icon(Icons.last_page)), const SizedBox( width: 8, @@ -379,12 +374,13 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { var sliderFocus = FocusNode(); Widget buildSlider() { - return Slider( + return CustomSlider( focusNode: sliderFocus, value: context.reader.page.toDouble(), min: 1, max: context.reader.maxPage.clamp(context.reader.page, 1 << 16).toDouble(), + reversed: isReversed, divisions: (context.reader.maxPage - 1).clamp(2, 1 << 16), onChanged: (i) { context.reader.toPage(i.toInt()); From 30a1c806cdeb7def095a4bdaf418c96425b45002 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 16 Nov 2024 16:51:56 +0800 Subject: [PATCH 10/25] Convert network folder to local --- assets/translation.json | 10 +- lib/components/flyout.dart | 4 + lib/foundation/favorites.dart | 54 +++++- lib/pages/favorites/favorite_actions.dart | 175 ++++++++++++++++++ lib/pages/favorites/favorites_page.dart | 1 + lib/pages/favorites/local_favorites_page.dart | 51 +++++ .../favorites/network_favorites_page.dart | 22 +++ 7 files changed, 308 insertions(+), 9 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 0fd80a4..7612940 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -212,7 +212,10 @@ "Select an image on screen": "选择屏幕上的图片", "Added @count comics to download queue.": "已添加 @count 本漫画到下载队列", "Ignore Certificate Errors": "忽略证书错误", - "Authorization Required": "需要身份验证" + "Authorization Required": "需要身份验证", + "Sync": "同步", + "The folder is Linked to @source": "文件夹已关联到 @source", + "Source Folder": "源收藏夹" }, "zh_TW": { "Home": "首頁", @@ -427,6 +430,9 @@ "Select an image on screen": "選擇屏幕上的圖片", "Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列", "Ignore Certificate Errors": "忽略證書錯誤", - "Authorization Required": "需要身份驗證" + "Authorization Required": "需要身份驗證", + "Sync": "同步", + "The folder is Linked to @source": "文件夾已關聯到 @source", + "Source Folder": "源收藏夾" } } \ No newline at end of file diff --git a/lib/components/flyout.dart b/lib/components/flyout.dart index 8bd131e..d183dc4 100644 --- a/lib/components/flyout.dart +++ b/lib/components/flyout.dart @@ -51,6 +51,10 @@ class Flyout extends StatefulWidget { @override State createState() => FlyoutState(); + + static FlyoutState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } } class FlyoutState extends State { diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index eef3fa3..d6f245c 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -346,6 +346,32 @@ class LocalFavoritesManager with ChangeNotifier { return name; } + void linkFolderToNetwork(String folder, String source, String networkFolder) { + _db.execute(""" + insert or replace into folder_sync (folder_name, source_key, source_folder) + values (?, ?, ?); + """, [folder, source, networkFolder]); + } + + bool isLinkedToNetworkFolder(String folder, String source, String networkFolder) { + var res = _db.select(""" + select * from folder_sync + where folder_name == ? and source_key == ? and source_folder == ?; + """, [folder, source, networkFolder]); + return res.isNotEmpty; + } + + (String?, String?) findLinked(String folder) { + var res = _db.select(""" + select * from folder_sync + where folder_name == ?; + """, [folder]); + if (res.isEmpty) { + return (null, null); + } + return (res.first["source_key"], res.first["source_folder"]); + } + bool comicExists(String folder, String id, ComicType type) { var res = _db.select(""" select * from "$folder" @@ -365,20 +391,19 @@ class LocalFavoritesManager with ChangeNotifier { return FavoriteItem.fromRow(res.first); } - /// add comic to a folder - /// - /// This method will download cover to local, to avoid problems like changing url - void addComic(String folder, FavoriteItem comic, [int? order]) async { + /// add comic to a folder. + /// return true if success, false if already exists + bool addComic(String folder, FavoriteItem comic, [int? order]) { _modifiedAfterLastCache = true; if (!existsFolder(folder)) { throw Exception("Folder does not exists"); } var res = _db.select(""" select * from "$folder" - where id == '${comic.id}'; - """); + where id == ? and type == ?; + """, [comic.id, comic.type.value]); if (res.isNotEmpty) { - return; + return false; } final params = [ comic.id, @@ -406,6 +431,7 @@ class LocalFavoritesManager with ChangeNotifier { """, [...params, minValue(folder) - 1]); } notifyListeners(); + return true; } /// delete a folder @@ -414,6 +440,10 @@ class LocalFavoritesManager with ChangeNotifier { _db.execute(""" drop table "$name"; """); + _db.execute(""" + delete from folder_order + where folder_name == ?; + """, [name]); notifyListeners(); } @@ -461,6 +491,16 @@ class LocalFavoritesManager with ChangeNotifier { ALTER TABLE "$before" RENAME TO "$after"; """); + _db.execute(""" + update folder_order + set folder_name = ? + where folder_name == ?; + """, [after, before]); + _db.execute(""" + update folder_sync + set folder_name = ? + where folder_name == ?; + """, [after, before]); notifyListeners(); } diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index 1cb8847..e19686f 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -288,3 +288,178 @@ Future sortFolders() async { LocalFavoritesManager().updateOrder(folders); } + +Future importNetworkFolder( + String source, + String? folder, + String? folderID, +) async { + var comicSource = ComicSource.find(source); + if (comicSource == null) { + return; + } + if(folder != null && folder.isEmpty) { + folder = null; + } + var resultName = folder ?? comicSource.name; + var exists = LocalFavoritesManager().existsFolder(resultName); + if (exists) { + if (!LocalFavoritesManager() + .isLinkedToNetworkFolder(resultName, source, folderID ?? "")) { + App.rootContext.showMessage(message: "Folder already exists".tl); + return; + } + } + if(!exists) { + LocalFavoritesManager().createFolder(resultName); + LocalFavoritesManager().linkFolderToNetwork( + resultName, + source, + folderID ?? "", + ); + } + + var current = 0; + var isFinished = false; + String? next; + + Future fetchNext() async { + var retry = 3; + + while (true) { + try { + if (comicSource.favoriteData?.loadComic != null) { + next ??= '1'; + var page = int.parse(next!); + var res = await comicSource.favoriteData!.loadComic!(page, folderID); + var count = 0; + for (var c in res.data) { + var result = LocalFavoritesManager().addComic( + resultName, + FavoriteItem( + id: c.id, + name: c.title, + coverPath: c.cover, + type: ComicType(source.hashCode), + author: c.subtitle ?? '', + tags: c.tags ?? [], + ), + ); + if (result) { + count++; + } + } + current += count; + if (res.data.isEmpty || res.subData == page) { + isFinished = true; + next = null; + } else { + next = (page + 1).toString(); + } + } else if (comicSource.favoriteData?.loadNext != null) { + var res = await comicSource.favoriteData!.loadNext!(next, folderID); + var count = 0; + for (var c in res.data) { + var result = LocalFavoritesManager().addComic( + resultName, + FavoriteItem( + id: c.id, + name: c.title, + coverPath: c.cover, + type: ComicType(source.hashCode), + author: c.subtitle ?? '', + tags: c.tags ?? [], + ), + ); + if (result) { + count++; + } + } + current += count; + if (res.data.isEmpty || res.subData == null) { + isFinished = true; + next = null; + } else { + next = res.subData; + } + } else { + throw "Unsupported source"; + } + return; + } catch (e) { + retry--; + if (retry == 0) { + rethrow; + } + continue; + } + } + } + + bool isCanceled = false; + String? errorMsg; + bool isErrored() => errorMsg != null; + + void Function()? updateDialog; + + showDialog( + context: App.rootContext, + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + updateDialog = () => setState(() {}); + return ContentDialog( + title: isFinished + ? "Finished".tl + : isErrored() + ? "Error".tl + : "Importing".tl, + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + LinearProgressIndicator( + value: isFinished ? 1 : null, + ), + const SizedBox(height: 4), + Text("Imported @c comics".tlParams({ + "c": current, + })), + const SizedBox(height: 4), + if (isErrored()) Text("Error: $errorMsg"), + ], + ).paddingHorizontal(16), + actions: [ + Button.filled( + color: (isFinished || isErrored()) + ? null + : context.colorScheme.error, + onPressed: () { + isCanceled = true; + context.pop(); + }, + child: (isFinished || isErrored()) + ? Text("OK".tl) + : Text("Cancel".tl), + ), + ], + ); + }, + ); + }, + ).then((_) { + isCanceled = true; + }); + + while (!isFinished && !isCanceled) { + try { + await fetchNext(); + updateDialog?.call(); + } catch (e) { + errorMsg = e.toString(); + updateDialog?.call(); + break; + } + } +} diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index c380a9a..f815506 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:math'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; import 'package:venera/components/components.dart'; diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index 4bbef38..ec9e070 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -14,6 +14,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { late List comics; + String? networkSource; + String? networkFolder; + void updateComics() { setState(() { comics = LocalFavoritesManager().getAllComics(widget.folder); @@ -24,6 +27,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { void initState() { favPage = context.findAncestorStateOfType<_FavoritesPageState>()!; comics = LocalFavoritesManager().getAllComics(widget.folder); + var (a, b) = LocalFavoritesManager().findLinked(widget.folder); + networkSource = a; + networkFolder = b; super.initState(); } @@ -49,6 +55,51 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { child: Text(favPage.folder ?? "Unselected".tl), ), actions: [ + if (networkSource != null) + Tooltip( + message: "Sync".tl, + child: Flyout( + flyoutBuilder: (context) { + var sourceName = ComicSource.find(networkSource!)?.name ?? + networkSource!; + var text = "The folder is Linked to @source".tlParams({ + "source": sourceName, + }); + if(networkFolder != null && networkFolder!.isNotEmpty) { + text += "\n${"Source Folder".tl}: $networkFolder"; + } + return FlyoutContent( + title: "Sync".tl, + content: Text(text), + actions: [ + Button.filled( + child: Text("Update".tl), + onPressed: () { + context.pop(); + importNetworkFolder( + networkSource!, + widget.folder, + networkFolder!, + ).then( + (value) { + updateComics(); + }, + ); + }, + ), + ], + ); + }, + child: Builder(builder: (context) { + return IconButton( + icon: const Icon(Icons.sync), + onPressed: () { + Flyout.of(context).show(); + }, + ); + }), + ), + ), MenuButton( entries: [ MenuEntry( diff --git a/lib/pages/favorites/network_favorites_page.dart b/lib/pages/favorites/network_favorites_page.dart index d08d1fc..b842d55 100644 --- a/lib/pages/favorites/network_favorites_page.dart +++ b/lib/pages/favorites/network_favorites_page.dart @@ -108,6 +108,17 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> { onTap: context.width < _kTwoPanelChangeWidth ? showFolders : null, child: Text(widget.data.title), ), + actions: [ + MenuButton(entries: [ + MenuEntry( + icon: Icons.sync, + text: "Convert to local".tl, + onClick: () { + importNetworkFolder(widget.data.key, null, null); + }, + ) + ]), + ], ), errorLeading: Appbar( leading: Tooltip( @@ -533,6 +544,17 @@ class _FavoriteFolder extends StatelessWidget { key: comicListKey, leadingSliver: SliverAppbar( title: Text(title), + actions: [ + MenuButton(entries: [ + MenuEntry( + icon: Icons.sync, + text: "Convert to local".tl, + onClick: () { + importNetworkFolder(data.key, title, folderID); + }, + ) + ]), + ], ), errorLeading: Appbar( title: Text(title), From 0ee99a8760603f623699819dd4a4d9fcdafca24d Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 16 Nov 2024 19:13:19 +0800 Subject: [PATCH 11/25] fix android method channel --- .../com/github/wgh136/venera/MainActivity.kt | 142 +++++++++++++----- 1 file changed, 103 insertions(+), 39 deletions(-) diff --git a/android/app/src/main/kotlin/com/github/wgh136/venera/MainActivity.kt b/android/app/src/main/kotlin/com/github/wgh136/venera/MainActivity.kt index e1365e8..db2781e 100644 --- a/android/app/src/main/kotlin/com/github/wgh136/venera/MainActivity.kt +++ b/android/app/src/main/kotlin/com/github/wgh136/venera/MainActivity.kt @@ -1,19 +1,27 @@ package com.github.wgh136.venera +import android.Manifest import android.app.Activity import android.content.ContentResolver import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build -import android.view.KeyEvent -import android.Manifest import android.os.Environment +import android.provider.DocumentsContract import android.provider.Settings +import android.view.KeyEvent +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import dev.flutter.packages.file_selector_android.FileUtils import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.EventChannel @@ -21,17 +29,43 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugins.GeneratedPluginRegistrant import java.io.File import java.io.FileOutputStream -import java.lang.Exception +import java.util.concurrent.atomic.AtomicInteger class MainActivity : FlutterFragmentActivity() { var volumeListen = VolumeListen() var listening = false - private lateinit var result: MethodChannel.Result - private val storageRequestCode = 0x10 private var storagePermissionRequest: ((Boolean) -> Unit)? = null + private val nextLocalRequestCode = AtomicInteger() + + private fun startContractForResult( + contract: ActivityResultContract, + input: I, + callback: ActivityResultCallback + ) { + val key = "activity_rq_for_result#${nextLocalRequestCode.getAndIncrement()}" + val registry = activityResultRegistry + var launcher: ActivityResultLauncher? = null + val observer = object : LifecycleEventObserver { + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (Lifecycle.Event.ON_DESTROY == event) { + launcher?.unregister() + lifecycle.removeObserver(this) + } + } + } + lifecycle.addObserver(observer) + val newCallback = ActivityResultCallback { + launcher?.unregister() + lifecycle.removeObserver(observer) + callback.onActivityResult(it) + } + launcher = registry.register(key, contract, newCallback) + launcher.launch(input) + } + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) MethodChannel( @@ -53,24 +87,21 @@ class MainActivity : FlutterFragmentActivity() { "getDirectoryPath" -> { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - val launcher = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> - if (activityResult.resultCode != Activity.RESULT_OK) { - result.success(null) - } - val pickedDirectoryUri = activityResult.data?.data - if (pickedDirectoryUri == null) - result.success(null) - else - Thread { - try { - result.success(onPickedDirectory(pickedDirectoryUri)) - } catch (e: Exception) { - result.error("Failed to Copy Files", e.toString(), null) - } - }.start() + startContractForResult(ActivityResultContracts.StartActivityForResult(), intent) { activityResult -> + if (activityResult.resultCode != Activity.RESULT_OK) { + res.success(null) + return@startContractForResult } - launcher.launch(intent) + val pickedDirectoryUri = activityResult.data?.data + if (pickedDirectoryUri == null) + res.success(null) + else + try { + res.success(onPickedDirectory(pickedDirectoryUri)) + } catch (e: Exception) { + res.error("Failed to Copy Files", e.toString(), null) + } + } } else -> res.notImplemented() @@ -104,8 +135,7 @@ class MainActivity : FlutterFragmentActivity() { val selectFileChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/select_file") selectFileChannel.setMethodCallHandler { _, res -> - openFile() - result = res + openFile(res) } } @@ -138,13 +168,24 @@ class MainActivity : FlutterFragmentActivity() { /// copy the directory to tmp directory, return copied directory private fun onPickedDirectory(uri: Uri): String { - val contentResolver = contentResolver - var tmp = cacheDir - tmp = File(tmp, "getDirectoryPathTemp") - tmp.mkdir() - copyDirectory(contentResolver, uri, tmp) + if (!hasStoragePermission()) { + // dart:io cannot access the directory without permission. + // so we need to copy the directory to cache directory + val contentResolver = contentResolver + var tmp = cacheDir + tmp = File(tmp, "getDirectoryPathTemp") + tmp.mkdir() + Thread { + copyDirectory(contentResolver, uri, tmp) + }.start() - return tmp.absolutePath + return tmp.absolutePath + } else { + val docId = DocumentsContract.getTreeDocumentId(uri) + val split: Array = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + return if ((split.size >= 2) && (split[1] != null)) split[1]!! + else File.separator + } } private fun copyDirectory(resolver: ContentResolver, srcUri: Uri, destDir: File) { @@ -165,6 +206,20 @@ class MainActivity : FlutterFragmentActivity() { } } + private fun hasStoragePermission(): Boolean { + return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + ContextCompat.checkSelfPermission( + this, + Manifest.permission.READ_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission( + this, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) == PackageManager.PERMISSION_GRANTED + } else { + Environment.isExternalStorageManager() + } + } + private fun requestStoragePermission(result: (Boolean) -> Unit) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { val readPermission = ContextCompat.checkSelfPermission( @@ -196,10 +251,9 @@ class MainActivity : FlutterFragmentActivity() { val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) intent.addCategory("android.intent.category.DEFAULT") intent.data = Uri.parse("package:$packageName") - val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ -> + startContractForResult(ActivityResultContracts.StartActivityForResult(), intent){ _ -> result(Environment.isExternalStorageManager()) } - launcher.launch(intent) } catch (e: Exception) { result(false) } @@ -223,29 +277,40 @@ class MainActivity : FlutterFragmentActivity() { } } - private fun openFile() { + private fun openFile(result: MethodChannel.Result) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.type = "*/*" - val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> + startContractForResult(ActivityResultContracts.StartActivityForResult(), intent){ activityResult -> if (activityResult.resultCode != Activity.RESULT_OK) { result.success(null) + return@startContractForResult } val uri = activityResult.data?.data if (uri == null) { result.success(null) - return@registerForActivityResult + return@startContractForResult } val contentResolver = contentResolver val file = DocumentFile.fromSingleUri(this, uri) if (file == null) { result.success(null) - return@registerForActivityResult + return@startContractForResult } val fileName = file.name if (fileName == null) { result.success(null) - return@registerForActivityResult + return@startContractForResult + } + if(hasStoragePermission()) { + try { + val filePath = FileUtils.getPathFromUri(this, uri) + result.success(filePath) + return@startContractForResult + } + catch (e: Exception) { + // ignore + } } // copy file to cache directory val cacheDir = cacheDir @@ -253,7 +318,7 @@ class MainActivity : FlutterFragmentActivity() { val inputStream = contentResolver.openInputStream(uri) if (inputStream == null) { result.success(null) - return@registerForActivityResult + return@startContractForResult } val outputStream = FileOutputStream(newFile) inputStream.copyTo(outputStream) @@ -262,7 +327,6 @@ class MainActivity : FlutterFragmentActivity() { // send file path to flutter result.success(newFile.absolutePath) } - launcher.launch(intent) } } From 708cf83a32dd83c1df51eb69721e981fb1c43989 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 17 Nov 2024 17:23:43 +0800 Subject: [PATCH 12/25] improve ui --- assets/translation.json | 44 +++++- lib/components/comic.dart | 113 +++++++++----- lib/components/select.dart | 9 +- lib/foundation/comic_source/comic_source.dart | 2 + lib/network/cloudflare.dart | 1 - lib/pages/categories_page.dart | 34 ++--- lib/pages/comic_page.dart | 61 ++++++-- lib/pages/comic_source_page.dart | 138 +++++++++--------- lib/pages/explore_page.dart | 9 +- lib/pages/favorites/local_favorites_page.dart | 2 +- lib/pages/search_page.dart | 27 +++- lib/pages/settings/setting_components.dart | 52 ++++--- lib/utils/io.dart | 1 - pubspec.yaml | 2 +- 14 files changed, 320 insertions(+), 175 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 7612940..d2dc8bc 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -43,7 +43,7 @@ "Confirm": "确认", "Are you sure you want to delete this comic?": "您确定要删除这部漫画吗?", "Are you sure you want to delete @a selected comics?": "您确定要删除 @a 部漫画吗?", - "Add comic source": "添加漫画来源", + "Add comic source": "添加漫画源", "Select file": "选择文件", "View list": "查看列表", "Open help": "打开帮助", @@ -102,8 +102,8 @@ "Auto page turning interval": "自动翻页间隔", "Theme Mode": "主题模式", "System": "系统", - "Light": "明亮", - "Dark": "黑暗", + "Light": "浅色", + "Dark": "深色", "Theme Color": "主题颜色", "Red": "红色", "Pink": "粉色", @@ -215,7 +215,21 @@ "Authorization Required": "需要身份验证", "Sync": "同步", "The folder is Linked to @source": "文件夹已关联到 @source", - "Source Folder": "源收藏夹" + "Source Folder": "源收藏夹", + "Use a config file": "使用配置文件", + "Comic Source list": "漫画源列表", + "View": "查看", + "Copy": "复制", + "Copied": "已复制", + "Search History": "搜索历史", + "Clear Search History": "清除搜索历史", + "Search in": "搜索于", + "Clear History": "清除历史", + "Are you sure you want to clear your history?": "确定要清除您的历史记录吗?", + "No Explore Pages": "没有探索页面", + "Add a comic source in home page": "在主页添加一个漫画源", + "Please check your settings": "请检查您的设置", + "No Category Pages": "没有分类页面" }, "zh_TW": { "Home": "首頁", @@ -263,7 +277,7 @@ "Confirm": "確認", "Are you sure you want to delete this comic?": "您確定要刪除這部漫畫嗎?", "Are you sure you want to delete @a selected comics?": "您確定要刪除 @a 部漫畫嗎?", - "Add comic source": "添加漫畫來源", + "Add comic source": "添加漫畫源", "Select file": "選擇文件", "View list": "查看列表", "Open help": "打開幫助", @@ -320,8 +334,8 @@ "Auto page turning interval": "自動翻頁間隔", "Theme Mode": "主題模式", "System": "系統", - "Light": "明亮", - "Dark": "黑暗", + "Light": "浅色", + "Dark": "深色", "Theme Color": "主題顏色", "Red": "紅色", "Pink": "粉色", @@ -433,6 +447,20 @@ "Authorization Required": "需要身份驗證", "Sync": "同步", "The folder is Linked to @source": "文件夾已關聯到 @source", - "Source Folder": "源收藏夾" + "Source Folder": "源收藏夾", + "Use a config file": "使用配置文件", + "Comic Source list": "漫畫源列表", + "View": "查看", + "Copy": "複製", + "Copied": "已複製", + "Search History": "搜索歷史", + "Clear Search History": "清除搜索歷史", + "Search in": "搜索於", + "Clear History": "清除歷史", + "Are you sure you want to clear your history?": "確定要清除您的歷史記錄嗎?", + "No Explore Pages": "沒有探索頁面", + "Add a comic source in home page": "在主頁添加一個漫畫源", + "Please check your settings": "請檢查您的設定", + "No Category Pages": "沒有分類頁面" } } \ No newline at end of file diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 11d994b..92258cb 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -25,7 +25,8 @@ class ComicTile extends StatelessWidget { onTap!(); return; } - App.mainNavigatorKey?.currentContext?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey)); + App.mainNavigatorKey?.currentContext + ?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey)); } void onLongPress(BuildContext context) { @@ -50,7 +51,8 @@ class ComicTile extends StatelessWidget { icon: Icons.chrome_reader_mode_outlined, text: 'Details'.tl, onClick: () { - App.mainNavigatorKey?.currentContext?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey)); + App.mainNavigatorKey?.currentContext + ?.to(() => ComicPage(id: comic.id, sourceKey: comic.sourceKey)); }, ), MenuEntry( @@ -82,13 +84,17 @@ class ComicTile extends StatelessWidget { Widget build(BuildContext context) { var type = appdata.settings['comicDisplayMode']; - Widget child = type == 'detailed' ? _buildDetailedMode(context) : _buildBriefMode(context); + Widget child = type == 'detailed' + ? _buildDetailedMode(context) + : _buildBriefMode(context); var isFavorite = appdata.settings['showFavoriteStatusOnTile'] - ? LocalFavoritesManager().isExist(comic.id, ComicType(comic.sourceKey.hashCode)) + ? LocalFavoritesManager() + .isExist(comic.id, ComicType(comic.sourceKey.hashCode)) : false; var history = appdata.settings['showHistoryStatusOnTile'] - ? HistoryManager().findSync(comic.id, ComicType(comic.sourceKey.hashCode)) + ? HistoryManager() + .findSync(comic.id, ComicType(comic.sourceKey.hashCode)) : null; if (history?.page == 0) { history!.page = 1; @@ -132,7 +138,8 @@ class ComicTile extends StatelessWidget { constraints: const BoxConstraints(minWidth: 24), padding: const EdgeInsets.symmetric(horizontal: 4), child: CustomPaint( - painter: _ReadingHistoryPainter(history.page, history.maxPage), + painter: + _ReadingHistoryPainter(history.page, history.maxPage), ), ) ], @@ -205,7 +212,9 @@ class ComicTile extends StatelessWidget { badge: badge ?? comic.language, tags: comic.tags, maxLines: 2, - enableTranslate: ComicSource.find(comic.sourceKey)?.enableTagsTranslate ?? false, + enableTranslate: ComicSource.find(comic.sourceKey) + ?.enableTagsTranslate ?? + false, rating: comic.stars, ), ), @@ -237,7 +246,9 @@ class ComicTile extends StatelessWidget { Positioned.fill( child: Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, + color: Theme.of(context) + .colorScheme + .secondaryContainer, borderRadius: BorderRadius.circular(8), ), clipBehavior: Clip.antiAlias, @@ -338,7 +349,9 @@ class ComicTile extends StatelessWidget { } appdata.saveData(); context.showMessage(message: 'Blocked'.tl); - comicTileContext.findAncestorStateOfType<_SliverGridComicsState>()!.update(); + comicTileContext + .findAncestorStateOfType<_SliverGridComicsState>()! + .update(); }, child: Text('Block'.tl), ), @@ -379,7 +392,8 @@ class _ComicDescription extends StatelessWidget { s = s.replaceAll("\n", " "); } } - var enableTranslate = App.locale.languageCode == 'zh' && this.enableTranslate; + var enableTranslate = + App.locale.languageCode == 'zh' && this.enableTranslate; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -396,7 +410,9 @@ class _ComicDescription extends StatelessWidget { if (subtitle != "") Text( subtitle, - style: TextStyle(fontSize: 10.0, color: context.colorScheme.onSurface.withOpacity(0.7)), + style: TextStyle( + fontSize: 10.0, + color: context.colorScheme.onSurface.withOpacity(0.7)), maxLines: 1, softWrap: true, overflow: TextOverflow.ellipsis, @@ -433,13 +449,18 @@ class _ComicDescription extends StatelessWidget { decoration: BoxDecoration( color: s == "Unavailable" ? Theme.of(context).colorScheme.errorContainer - : Theme.of(context).colorScheme.secondaryContainer, - borderRadius: const BorderRadius.all(Radius.circular(8)), + : Theme.of(context) + .colorScheme + .secondaryContainer, + borderRadius: + const BorderRadius.all(Radius.circular(8)), ), child: Center( widthFactor: 1, child: Text( - enableTranslate ? TagsTranslation.translateTag(s) : s.split(':').last, + enableTranslate + ? TagsTranslation.translateTag(s) + : s.split(':').last, style: const TextStyle(fontSize: 12), softWrap: true, overflow: TextOverflow.ellipsis, @@ -510,17 +531,20 @@ class _ReadingHistoryPainter extends CustomPainter { textDirection: TextDirection.ltr, ); textPainter.layout(); - textPainter.paint(canvas, Offset((size.width - textPainter.width) / 2, (size.height - textPainter.height) / 2)); + textPainter.paint( + canvas, + Offset((size.width - textPainter.width) / 2, + (size.height - textPainter.height) / 2)); } else if (page == maxPage) { // 在中央绘制勾 final paint = Paint() ..color = Colors.white ..strokeWidth = 2 ..style = PaintingStyle.stroke; - canvas.drawLine( - Offset(size.width * 0.2, size.height * 0.5), Offset(size.width * 0.45, size.height * 0.75), paint); - canvas.drawLine( - Offset(size.width * 0.45, size.height * 0.75), Offset(size.width * 0.85, size.height * 0.3), paint); + canvas.drawLine(Offset(size.width * 0.2, size.height * 0.5), + Offset(size.width * 0.45, size.height * 0.75), paint); + canvas.drawLine(Offset(size.width * 0.45, size.height * 0.75), + Offset(size.width * 0.85, size.height * 0.3), paint); } else { // 在左上角绘制page, 在右下角绘制maxPage final textPainter = TextPainter( @@ -546,13 +570,18 @@ class _ReadingHistoryPainter extends CustomPainter { textDirection: TextDirection.ltr, ); textPainter2.layout(); - textPainter2.paint(canvas, Offset(size.width - textPainter2.width, size.height - textPainter2.height)); + textPainter2.paint( + canvas, + Offset(size.width - textPainter2.width, + size.height - textPainter2.height)); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { - return oldDelegate is! _ReadingHistoryPainter || oldDelegate.page != page || oldDelegate.maxPage != maxPage; + return oldDelegate is! _ReadingHistoryPainter || + oldDelegate.page != page || + oldDelegate.maxPage != maxPage; } } @@ -663,16 +692,22 @@ class _SliverGridComics extends StatelessWidget { onLastItemBuild?.call(); } var badge = badgeBuilder?.call(comics[index]); - var isSelected = selection == null ? false : selection![comics[index]] ?? false; + var isSelected = + selection == null ? false : selection![comics[index]] ?? false; var comic = ComicTile( comic: comics[index], badge: badge, menuOptions: menuBuilder?.call(comics[index]), onTap: onTap != null ? () => onTap!(comics[index]) : null, ); + if(selection == null) { + return comic; + } return Container( decoration: BoxDecoration( - color: isSelected ? Theme.of(context).colorScheme.surfaceContainer : null, + color: isSelected + ? Theme.of(context).colorScheme.surfaceContainer + : null, borderRadius: BorderRadius.circular(12), ), margin: const EdgeInsets.all(4), @@ -797,7 +832,9 @@ class ComicListState extends State { decoration: InputDecoration( labelText: "Page".tl, ), - inputFormatters: [FilteringTextInputFormatter.digitsOnly], + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly + ], onChanged: (v) { value = v; }, @@ -810,13 +847,15 @@ class ComicListState extends State { if (page == null) { context.showMessage(message: "Invalid page".tl); } else { - if (page > 0 && (_maxPage == null || page <= _maxPage!)) { + if (page > 0 && + (_maxPage == null || page <= _maxPage!)) { setState(() { _error = null; _page = page; }); } else { - context.showMessage(message: "Invalid page".tl); + context.showMessage( + message: "Invalid page".tl); } } }, @@ -828,7 +867,8 @@ class ComicListState extends State { ); }, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 6), child: Text("Page $_page / ${_maxPage ?? '?'}"), ), ), @@ -959,7 +999,8 @@ class ComicListState extends State { comics: _data[_page] ?? const [], menuBuilder: widget.menuBuilder, ), - if (_data[_page]!.length > 6 && _maxPage != 1) _buildSliverPageSelector(), + if (_data[_page]!.length > 6 && _maxPage != 1) + _buildSliverPageSelector(), if (widget.trailingSliver != null) widget.trailingSliver!, ], ); @@ -1120,15 +1161,20 @@ class _RatingWidgetState extends State { if (!widget.selectable) { return; } - if (dx >= widget.size * widget.count + widget.padding * (widget.count - 1)) { + if (dx >= + widget.size * widget.count + widget.padding * (widget.count - 1)) { value = widget.maxRating; } else { for (double i = 1; i < widget.count + 1; i++) { - if (dx > widget.size * i + widget.padding * (i - 1) && dx < widget.size * i + widget.padding * i) { + if (dx > widget.size * i + widget.padding * (i - 1) && + dx < widget.size * i + widget.padding * i) { value = i * (widget.maxRating / widget.count); break; - } else if (dx > widget.size * (i - 1) + widget.padding * (i - 1) && dx < widget.size * i + widget.padding * i) { - value = (dx - widget.padding * (i - 1)) / (widget.size * widget.count) * widget.maxRating; + } else if (dx > widget.size * (i - 1) + widget.padding * (i - 1) && + dx < widget.size * i + widget.padding * i) { + value = (dx - widget.padding * (i - 1)) / + (widget.size * widget.count) * + widget.maxRating; break; } } @@ -1156,7 +1202,8 @@ class _RatingWidgetState extends State { if (widget.count / fullStars() == widget.maxRating / value) { return 0; } - return (value % (widget.maxRating / widget.count)) / (widget.maxRating / widget.count); + return (value % (widget.maxRating / widget.count)) / + (widget.maxRating / widget.count); } List buildRow() { diff --git a/lib/components/select.dart b/lib/components/select.dart index 66c746e..e2b9e05 100644 --- a/lib/components/select.dart +++ b/lib/components/select.dart @@ -31,8 +31,9 @@ class Select extends StatelessWidget { var size = renderBox.size; showMenu( elevation: 3, - color: context.colorScheme.surface, - surfaceTintColor: Colors.transparent, + color: context.brightness == Brightness.light + ? const Color(0xFFF6F6F6) + : const Color(0xFF1E1E1E), context: context, useRootNavigator: true, constraints: BoxConstraints( @@ -41,8 +42,8 @@ class Select extends StatelessWidget { ), position: RelativeRect.fromLTRB( offset.dx, - offset.dy + size.height, - offset.dx + size.height, + offset.dy + size.height + 2, + offset.dx + size.height + 2, offset.dy, ), items: values diff --git a/lib/foundation/comic_source/comic_source.dart b/lib/foundation/comic_source/comic_source.dart index 758d8f3..a537918 100644 --- a/lib/foundation/comic_source/comic_source.dart +++ b/lib/foundation/comic_source/comic_source.dart @@ -136,6 +136,8 @@ class ComicSource { notifyListeners(); } + static bool get isEmpty => _sources.isEmpty; + /// Name of this source. final String name; diff --git a/lib/network/cloudflare.dart b/lib/network/cloudflare.dart index 7d8c5f2..ff22041 100644 --- a/lib/network/cloudflare.dart +++ b/lib/network/cloudflare.dart @@ -1,7 +1,6 @@ import 'dart:io' as io; import 'package:dio/dio.dart'; -import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/consts.dart'; diff --git a/lib/pages/categories_page.dart b/lib/pages/categories_page.dart index c55d1ac..fe2bb45 100644 --- a/lib/pages/categories_page.dart +++ b/lib/pages/categories_page.dart @@ -30,8 +30,15 @@ class CategoriesPage extends StatelessWidget { .toList(); if(categories.isEmpty) { + var msg = "No Category Pages".tl; + msg += '\n'; + if(ComicSource.isEmpty) { + msg += "Add a comic source in home page".tl; + } else { + msg += "Please check your settings".tl; + } return NetworkError( - message: "No Category Pages".tl, + message: msg, retry: () { controller.update(); }, @@ -248,36 +255,19 @@ class _CategoryPage extends StatelessWidget { Widget buildTag(String tag, ClickTagCallback onClick, [String? namespace, String? param]) { - String translateTag(String tag) { - /* - // TODO: Implement translation - if (enableTranslation) { - if (namespace != null) { - tag = TagsTranslation.translationTagWithNamespace(tag, namespace); - } else { - tag = tag.translateTagsToCN; - } - } - - */ - return tag; - } - return Padding( padding: const EdgeInsets.fromLTRB(8, 6, 8, 6), child: Builder( builder: (context) { return Material( - elevation: 0.6, - borderRadius: const BorderRadius.all(Radius.circular(4)), - color: context.colorScheme.surfaceContainerLow, - surfaceTintColor: Colors.transparent, + borderRadius: const BorderRadius.all(Radius.circular(8)), + color: context.colorScheme.primaryContainer.withOpacity(0.72), child: InkWell( - borderRadius: const BorderRadius.all(Radius.circular(4)), + borderRadius: const BorderRadius.all(Radius.circular(8)), onTap: () => onClick(tag, param), child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text(translateTag(tag)), + child: Text(tag), ), ), ); diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index fac9c64..081ad0f 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -327,7 +327,7 @@ class _ComicPageState extends LoadingState } Widget buildDescription() { - if (comic.description == null) { + if (comic.description == null || comic.description!.trim().isEmpty) { return const SliverPadding(padding: EdgeInsets.zero); } return SliverToBoxAdapter( @@ -392,6 +392,27 @@ class _ComicPageState extends LoadingState child: InkWell( borderRadius: borderRadius, onTap: onTap, + onLongPress: () { + Clipboard.setData(ClipboardData(text: text)); + context.showMessage(message: "Copied".tl); + }, + onSecondaryTapDown: (details) { + showMenuX(context, details.globalPosition, [ + MenuEntry( + icon: Icons.remove_red_eye, + text: "View".tl, + onClick: onTap, + ), + MenuEntry( + icon: Icons.copy, + text: "Copy".tl, + onClick: () { + Clipboard.setData(ClipboardData(text: text)); + context.showMessage(message: "Copied".tl); + }, + ), + ]); + }, child: Text(text).padding(padding), ), ); @@ -406,6 +427,26 @@ class _ComicPageState extends LoadingState } } + String formatTime(String time) { + if (int.tryParse(time) != null) { + var t = int.tryParse(time); + if (t! > 1000000000000) { + return DateTime.fromMillisecondsSinceEpoch(t) + .toString() + .substring(0, 19); + } else { + return DateTime.fromMillisecondsSinceEpoch(t * 1000) + .toString() + .substring(0, 19); + } + } + if (time.contains('T') || time.contains('Z')) { + var t = DateTime.parse(time); + return t.toString().substring(0, 19); + } + return time; + } + Widget buildWrap({required List children}) { return Wrap( runSpacing: 8, @@ -464,14 +505,14 @@ class _ComicPageState extends LoadingState buildWrap( children: [ buildTag(text: 'Upload Time'.tl, isTitle: true), - buildTag(text: comic.uploadTime!), + buildTag(text: formatTime(comic.uploadTime!)), ], ), if (comic.updateTime != null) buildWrap( children: [ buildTag(text: 'Update Time'.tl, isTitle: true), - buildTag(text: comic.updateTime!), + buildTag(text: formatTime(comic.updateTime!)), ], ), const SizedBox(height: 12), @@ -575,7 +616,7 @@ abstract mixin class _ComicPageActions { void quickFavorite() { var folder = appdata.settings['quickFavorite']; - if(folder is! String) { + if (folder is! String) { return; } LocalFavoritesManager().addComic( @@ -1037,7 +1078,7 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> { if (!isInitialLoading && next == null) { return; } - if(isLoading) return; + if (isLoading) return; Future.microtask(() { setState(() { isLoading = true; @@ -1610,10 +1651,12 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> { const SizedBox(width: 16), Expanded( child: FilledButton( - onPressed: selected.isEmpty ? null : () { - widget.finishSelect(selected); - context.pop(); - }, + onPressed: selected.isEmpty + ? null + : () { + widget.finishSelect(selected); + context.pop(); + }, child: Text("Download Selected".tl), ), ), diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index 2657339..2ae6a71 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -40,7 +40,7 @@ class ComicSourcePage extends StatefulWidget { } controller?.close(); if (shouldUpdate.isEmpty) { - if(!implicit) { + if (!implicit) { App.rootContext.showMessage(message: "No Update Available".tl); } return; @@ -95,24 +95,12 @@ class _BodyState extends State<_Body> { return SmoothCustomScrollView( slivers: [ buildCard(context), - buildSettings(), for (var source in ComicSource.all()) buildSource(context, source), SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)), ], ); } - Widget buildSettings() { - return SliverToBoxAdapter( - child: ListTile( - leading: const Icon(Icons.update_outlined), - title: Text("Check updates".tl), - onTap: () => ComicSourcePage.checkComicSourceUpdate(false), - trailing: const Icon(Icons.arrow_right), - ), - ); - } - Widget buildSource(BuildContext context, ComicSource source) { return SliverToBoxAdapter( child: Column( @@ -181,11 +169,12 @@ class _BodyState extends State<_Body> { 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] = item.value['options'][i]['value']; + source.data['settings'][key] = + item.value['options'][i]['value']; source.saveData(); setState(() {}); }, @@ -209,7 +198,8 @@ class _BodyState extends State<_Body> { 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: () { @@ -231,8 +221,7 @@ class _BodyState extends State<_Body> { ), ); } - } - catch(e, s) { + } catch (e, s) { Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s"); } } @@ -305,55 +294,73 @@ 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); + } return SliverToBoxAdapter( - child: Card.outlined( - child: SizedBox( - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text("Add comic source".tl), - leading: const Icon(Icons.dashboard_customize), + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Add comic source".tl), + leading: const Icon(Icons.dashboard_customize), + ), + TextField( + decoration: InputDecoration( + 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; + }, + onSubmitted: handleAddSource, + ).paddingHorizontal(16).paddingBottom(8), + ListTile( + title: Text("Comic Source list".tl), + trailing: buildButton( + child: Text("View".tl), + onPressed: () { + showPopUpWidget( + App.rootContext, + _ComicSourceList(handleAddSource), + ); + }, ), - TextField( - decoration: InputDecoration( - 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; - }, - onSubmitted: handleAddSource) - .paddingHorizontal(16) - .paddingBottom(32), - Row( - children: [ - TextButton( - onPressed: _selectFile, child: Text("Select file".tl)) - .paddingLeft(8), - const Spacer(), - TextButton( - onPressed: () { - showPopUpWidget( - App.rootContext, _ComicSourceList(handleAddSource)); - }, - child: Text("View list".tl)), - const Spacer(), - TextButton(onPressed: help, child: Text("Open help".tl)) - .paddingRight(8), - ], + ), + ListTile( + title: Text("Use a config file".tl), + trailing: buildButton( + onPressed: _selectFile, + child: Text("Select".tl), ), - const SizedBox(height: 8), - ], - ), + ), + ListTile( + title: Text("Help".tl), + trailing: buildButton( + onPressed: help, + child: Text("Open".tl), + ), + ), + ListTile( + title: Text("Check updates".tl), + trailing: buildButton( + onPressed: () => ComicSourcePage.checkComicSourceUpdate(false), + child: Text("Check".tl), + ), + ), + const SizedBox(height: 8), + ], ), - ).paddingHorizontal(12), + ), ); } @@ -372,8 +379,7 @@ class _BodyState extends State<_Body> { } void help() { - launchUrlString( - "https://github.com/venera-app/venera/blob/master/doc/comic_source.md"); + launchUrlString("https://github.com/venera-app/venera-configs"); } Future handleAddSource(String url) async { diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart index d575b7f..125bd7d 100644 --- a/lib/pages/explore_page.dart +++ b/lib/pages/explore_page.dart @@ -93,8 +93,15 @@ class _ExplorePageState extends State Widget buildBody(String i) => _SingleExplorePage(i, key: Key(i)); Widget buildEmpty() { + var msg = "No Explore Pages".tl; + msg += '\n'; + if(ComicSource.isEmpty) { + msg += "Add a comic source in home page".tl; + } else { + msg += "Please check your settings".tl; + } return NetworkError( - message: "No Explore Pages".tl, + message: msg, retry: () { setState(() { pages = ComicSource.all() diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index ec9e070..460aedd 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -187,7 +187,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { }); }), MenuEntry( - icon: Icons.update, + icon: Icons.download, text: "Download All".tl, onClick: () async { int count = 0; diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index 6bfc070..735c7e5 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -305,13 +305,24 @@ class _SearchPageState extends State { ), ); } - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 12), - title: Text(appdata.searchHistory[index - 2]), + return InkWell( onTap: () { search(appdata.searchHistory[index - 2]); }, - ); + child: Container( + decoration: BoxDecoration( + // color: context.colorScheme.surfaceContainer, + border: Border( + left: BorderSide( + color: context.colorScheme.outlineVariant, + width: 2, + ), + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Text(appdata.searchHistory[index - 2], style: ts.s14), + ), + ).paddingBottom(8).paddingHorizontal(4); }, childCount: 2 + appdata.searchHistory.length, ), @@ -490,7 +501,7 @@ class SearchOptionWidget extends StatelessWidget { contentPadding: EdgeInsets.zero, title: Text(option.label.ts(sourceKey)), ), - if(option.type == 'select') + if (option.type == 'select') Wrap( runSpacing: 8, spacing: 8, @@ -504,7 +515,7 @@ class SearchOptionWidget extends StatelessWidget { ); }).toList(), ), - if(option.type == 'multi-select') + if (option.type == 'multi-select') Wrap( runSpacing: 8, spacing: 8, @@ -514,7 +525,7 @@ class SearchOptionWidget extends StatelessWidget { isSelected: (jsonDecode(value) as List).contains(e.key), onTap: () { var list = jsonDecode(value) as List; - if(list.contains(e.key)) { + if (list.contains(e.key)) { list.remove(e.key); } else { list.add(e.key); @@ -524,7 +535,7 @@ class SearchOptionWidget extends StatelessWidget { ); }).toList(), ), - if(option.type == 'dropdown') + if (option.type == 'dropdown') Select( current: option.options[value], values: option.options.values.toList(), diff --git a/lib/pages/settings/setting_components.dart b/lib/pages/settings/setting_components.dart index 6472ca8..6e43be1 100644 --- a/lib/pages/settings/setting_components.dart +++ b/lib/pages/settings/setting_components.dart @@ -134,7 +134,9 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> { builder: (context) { return ContentDialog( title: "Help".tl, - content: Text(widget.help!).paddingHorizontal(16).fixWidth(double.infinity), + content: Text(widget.help!) + .paddingHorizontal(16) + .fixWidth(double.infinity), actions: [ Button.filled( onPressed: context.pop, @@ -159,8 +161,9 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> { var rect = offset & size; showMenu( elevation: 3, - color: context.colorScheme.surface, - surfaceTintColor: Colors.transparent, + color: context.brightness == Brightness.light + ? const Color(0xFFF6F6F6) + : const Color(0xFF1E1E1E), context: context, position: RelativeRect.fromRect( rect, @@ -230,7 +233,9 @@ class _EndSelectorSelectSettingState extends State<_EndSelectorSelectSetting> { builder: (context) { return ContentDialog( title: "Help".tl, - content: Text(widget.help!).paddingHorizontal(16).fixWidth(double.infinity), + content: Text(widget.help!) + .paddingHorizontal(16) + .fixWidth(double.infinity), actions: [ Button.filled( onPressed: context.pop, @@ -459,24 +464,31 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> { } }); showDialog( - context: context, - builder: (context) { - return SimpleDialog( - title: const Text("Add"), + context: context, + builder: (context) { + return ContentDialog( + title: "Add".tl, + content: Column( + mainAxisSize: MainAxisSize.min, children: canAdd.entries - .map((e) => InkWell( - child: ListTile(title: Text(e.value), key: Key(e.key)), - onTap: () { - context.pop(); - setState(() { - keys.add(e.key); - }); - updateSetting(); - }, - )) + .map( + (e) => ListTile( + title: Text(e.value), + key: Key(e.key), + onTap: () { + context.pop(); + setState(() { + keys.add(e.key); + }); + updateSetting(); + }, + ), + ) .toList(), - ); - }); + ), + ); + }, + ); } void updateSetting() { diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 8ddf9c6..5e41265 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -190,7 +190,6 @@ class IOSDirectoryPicker { final String? path = await _channel.invokeMethod('selectDirectory'); return path; } catch (e) { - print("Error selecting directory: $e"); // 返回报错信息 return e.toString(); } diff --git a/pubspec.yaml b/pubspec.yaml index 19e08db..d061be5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.0.5+105 environment: sdk: '>=3.5.0 <4.0.0' - flutter: 3.24.4 + flutter: 3.24.5 dependencies: flutter: From 213179b8c2dee5d9e437efb6a3110a2fd7ba50cf Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 17 Nov 2024 18:25:18 +0800 Subject: [PATCH 13/25] update workflow --- .github/workflows/linux.yml | 33 ---------------- .github/workflows/main.yml | 79 ++++++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 35 deletions(-) delete mode 100644 .github/workflows/linux.yml diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml deleted file mode 100644 index 558ea6f..0000000 --- a/.github/workflows/linux.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Build Linux -run-name: Build Linux -on: - workflow_dispatch: {} -jobs: - Build_Linux: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 - with: - channel: 'stable' - flutter-version-file: pubspec.yaml - architecture: x64 - - run: | - sudo apt-get update -y - sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1 - dart pub global activate flutter_to_debian - - run: python3 debian/build.py - - run: dart run flutter_to_arch - - run: | - sudo rm -rf build/linux/arch/app.tar.gz - sudo rm -rf build/linux/arch/pkg - sudo rm -rf build/linux/arch/src - sudo rm -rf build/linux/arch/PKGBUILD - - uses: actions/upload-artifact@v4 - with: - name: deb_build - path: build/linux/x64/release/debian - - uses: actions/upload-artifact@v4 - with: - name: arch_build - path: build/linux/arch/ \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8254751..8d3f910 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,5 @@ -name: Build IOS -run-name: Build IOS +name: Build ALL +run-name: Build ALL on: workflow_dispatch: {} jobs: @@ -63,3 +63,78 @@ jobs: with: name: app-ios.ipa path: /Users/runner/work/venera/venera/build/ios/iphoneos/venera-ios.ipa + Build_Android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version-file: pubspec.yaml + architecture: x64 + - name: Decode and install certificate + env: + STORE_FILE: ${{ secrets.ANDROID_KEYSTORE }} + PROPERTY_FILE: ${{ secrets.ANDROID_KEY_PROPERTIES }} + run: | + echo "$STORE_FILE" | base64 --decode > android/keystore.jks + echo "$PROPERTY_FILE" | base64 --decode > android/key.properties + - run: flutter pub get + - run: flutter build apk --release + - uses: actions/upload-artifact@v4 + with: + name: apks + path: build/app/outputs/apk/release + Build_Windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version-file: pubspec.yaml + architecture: x64 + - name: install inno setup + run: | + winget install -h --id JRSoftware.InnoSetup -e -s winget + $inno_path = "C:\Users\$env:UserName\AppData\Local\Programs\Inno Setup 6" + wget "https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/refs/heads/main/ChineseSimplified.isl" -OutFile "$inno_path\Languages\ChineseSimplified.isl" + - run: flutter pub get + - name: build + run: | + $inno_path = "C:\Users\$env:UserName\AppData\Local\Programs\Inno Setup 6" + $env:PATH += ";$inno_path" + python windows/build.py + - uses: actions/upload-artifact@v4 + with: + name: windows_build + path: build/windows/Venera-* + Build_Linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: 'stable' + flutter-version-file: pubspec.yaml + architecture: x64 + - run: | + sudo apt-get update -y + sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1 + dart pub global activate flutter_to_debian + - run: python3 debian/build.py + - run: dart run flutter_to_arch + - run: | + sudo rm -rf build/linux/arch/app.tar.gz + sudo rm -rf build/linux/arch/pkg + sudo rm -rf build/linux/arch/src + sudo rm -rf build/linux/arch/PKGBUILD + - uses: actions/upload-artifact@v4 + with: + name: deb_build + path: build/linux/x64/release/debian + - uses: actions/upload-artifact@v4 + with: + name: arch_build + path: build/linux/arch/ + From 9988e7614969ae3d6d55683422ba2381dc37009d Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 17 Nov 2024 18:43:29 +0800 Subject: [PATCH 14/25] update workflow --- .github/workflows/main.yml | 4 +++- pubspec.lock | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8d3f910..4d5e4d4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -78,7 +78,7 @@ jobs: PROPERTY_FILE: ${{ secrets.ANDROID_KEY_PROPERTIES }} run: | echo "$STORE_FILE" | base64 --decode > android/keystore.jks - echo "$PROPERTY_FILE" | base64 --decode > android/key.properties + echo "$PROPERTY_FILE" > android/key.properties - run: flutter pub get - run: flutter build apk --release - uses: actions/upload-artifact@v4 @@ -89,6 +89,8 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v3 + - name: install yq + run: winget install --id MikeFarah.yq - uses: subosito/flutter-action@v2 with: channel: "stable" diff --git a/pubspec.lock b/pubspec.lock index 22bde61..1d9b852 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1036,4 +1036,4 @@ packages: version: "0.0.1" sdks: dart: ">=3.5.4 <4.0.0" - flutter: ">=3.24.4" + flutter: ">=3.24.5" From 00af5f19899e37499aba24629e584fc75d9fd84f Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 17 Nov 2024 20:50:32 +0800 Subject: [PATCH 15/25] update workflow --- .github/workflows/main.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d5e4d4..1c7594f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -79,6 +79,10 @@ jobs: run: | echo "$STORE_FILE" | base64 --decode > android/keystore.jks echo "$PROPERTY_FILE" > android/key.properties + - uses: actions/setup-java@v4 + with: + distribution: 'oracle' + java-version: '17' - run: flutter pub get - run: flutter build apk --release - uses: actions/upload-artifact@v4 @@ -90,7 +94,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: install yq - run: winget install --id MikeFarah.yq + run: choco install yq -y - uses: subosito/flutter-action@v2 with: channel: "stable" @@ -98,13 +102,13 @@ jobs: architecture: x64 - name: install inno setup run: | - winget install -h --id JRSoftware.InnoSetup -e -s winget - $inno_path = "C:\Users\$env:UserName\AppData\Local\Programs\Inno Setup 6" + choco install innosetup -y + $inno_path = "C:\Program Files (x86)\Inno Setup 6" wget "https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/refs/heads/main/ChineseSimplified.isl" -OutFile "$inno_path\Languages\ChineseSimplified.isl" - run: flutter pub get - name: build run: | - $inno_path = "C:\Users\$env:UserName\AppData\Local\Programs\Inno Setup 6" + $inno_path = "C:\Program Files (x86)\Inno Setup 6\" $env:PATH += ";$inno_path" python windows/build.py - uses: actions/upload-artifact@v4 From 458bc261f3daa6045432151938f573869deb88c5 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 17 Nov 2024 20:57:39 +0800 Subject: [PATCH 16/25] update workflow --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1c7594f..e13b497 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -95,6 +95,8 @@ jobs: - uses: actions/checkout@v3 - name: install yq run: choco install yq -y + - name: install wget + run: choco install wget -y - uses: subosito/flutter-action@v2 with: channel: "stable" From a1d1f504bddd33fff55b4d8a69fe055eb35a679c Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 17 Nov 2024 21:22:55 +0800 Subject: [PATCH 17/25] fix windows build --- .github/workflows/main.yml | 17 +++++------------ windows/build.iss | 2 +- windows/build.py | 8 ++++++++ 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e13b497..3aaf354 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -93,25 +93,18 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@v3 - - name: install yq - run: choco install yq -y - - name: install wget - run: choco install wget -y + - name: install dependencies + run: | + choco install yq -y + pip install httpx - uses: subosito/flutter-action@v2 with: channel: "stable" flutter-version-file: pubspec.yaml architecture: x64 - - name: install inno setup - run: | - choco install innosetup -y - $inno_path = "C:\Program Files (x86)\Inno Setup 6" - wget "https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/refs/heads/main/ChineseSimplified.isl" -OutFile "$inno_path\Languages\ChineseSimplified.isl" - - run: flutter pub get - name: build run: | - $inno_path = "C:\Program Files (x86)\Inno Setup 6\" - $env:PATH += ";$inno_path" + flutter pub get python windows/build.py - uses: actions/upload-artifact@v4 with: diff --git a/windows/build.iss b/windows/build.iss index 1e4ec0d..3f9953b 100644 --- a/windows/build.iss +++ b/windows/build.iss @@ -33,7 +33,7 @@ WizardStyle=modern [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" -Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" +Name: "chinesesimplified"; MessagesFile: "{#RootPath}\windows\ChineseSimplified.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked diff --git a/windows/build.py b/windows/build.py index 55bb0c1..c3d1a4f 100644 --- a/windows/build.py +++ b/windows/build.py @@ -1,5 +1,6 @@ import subprocess import os +import httpx file = open('pubspec.yaml', 'r') content = file.read() @@ -26,6 +27,13 @@ file = open('windows/build.iss', 'w') file.write(newContent) file.close() +if not os.path.exists("windows/ChineseSimplified.isl"): + # download ChineseSimplified.isl + url = "https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/refs/heads/main/ChineseSimplified.isl" + response = httpx.get(url) + with open('windows/ChineseSimplified.isl', 'wb') as file: + file.write(response.content) + subprocess.run(["iscc", "windows/build.iss"], shell=True) with open('windows/build.iss', 'w') as file: From 036474a5d2068f8d32e7a0b990684aa4dcf2759c Mon Sep 17 00:00:00 2001 From: AnxuNA <41771421+axlmly@users.noreply.github.com> Date: Sun, 17 Nov 2024 22:55:04 +0800 Subject: [PATCH 18/25] Optimization _buildBriefMode (#51) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更改_buildBriefMode样式 --- lib/components/comic.dart | 156 +++++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 71 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 92258cb..e288119 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -225,85 +225,99 @@ class ComicTile extends StatelessWidget { } Widget _buildBriefMode(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - return InkWell( - borderRadius: BorderRadius.circular(12), - onTap: _onTap, - onLongPress: enableLongPressed ? () => onLongPress(context) : null, - onSecondaryTapDown: (detail) => onSecondaryTap(detail, context), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), - child: Column( - children: [ - Expanded( - // Wrap the Container with Expanded - child: SizedBox( - width: constraints.maxWidth, - height: constraints.maxHeight, - child: Stack( - children: [ - Positioned.fill( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - clipBehavior: Clip.antiAlias, - child: buildImage(context), - ), - ), - Positioned( - bottom: 0, - right: 0, - child: ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0), - ), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), + child: LayoutBuilder( + builder: (context, constraints) { + return InkWell( + borderRadius: BorderRadius.circular(8), + onTap: _onTap, + onLongPress: + enableLongPressed ? () => onLongPress(context) : null, + onSecondaryTapDown: (detail) => onSecondaryTap(detail, context), + child: Column( + children: [ + Expanded( + child: SizedBox( + child: Stack( + children: [ + Positioned.fill( child: Container( - color: Colors.black.withOpacity(0.5), // 半透明黑色背景 - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), - child: Text( - comic.description.replaceAll("\n", ""), - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 12, - color: Colors.white, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + borderRadius: BorderRadius.circular(8), ), + clipBehavior: Clip.antiAlias, + child: buildImage(context), ), ), - ), - ], + Positioned( + bottom: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 4), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + bottomRight: Radius.circular(10.0), + bottomLeft: Radius.circular(10.0), + ), + child: Container( + color: Colors.black.withOpacity(0.5), + child: Padding( + padding: + const EdgeInsets.fromLTRB(8, 6, 8, 6), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constraints.maxWidth * 0.88, + ), + child: Text( + comic.description.isEmpty + ? comic.subtitle + ?.replaceAll('\n', '') ?? + '' + : comic.description + .split('|') + .join('\n'), + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 12, + color: Colors.white, + ), + textAlign: TextAlign.right, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + ), + )), + ], + ), ), ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 8, 0), - child: Text( - comic.title.replaceAll("\n", ""), - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14.0, - color: Colors.white, + Padding( + padding: const EdgeInsets.fromLTRB(8, 4, 8, 0), + child: Text( + comic.title.replaceAll('\n', ''), + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14.0, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - ), - ], - ), - ), - ); - }, - ); + ], + ), + ); + }, + )); } void block(BuildContext comicTileContext) { From 800b67fb2864d2fd36c89c09b8b9cfbb1ee88002 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 18 Nov 2024 10:52:55 +0800 Subject: [PATCH 19/25] fix network issue --- lib/network/app_dio.dart | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/lib/network/app_dio.dart b/lib/network/app_dio.dart index 26db4da..233b932 100644 --- a/lib/network/app_dio.dart +++ b/lib/network/app_dio.dart @@ -110,32 +110,20 @@ class AppDio with DioMixin { AppDio([BaseOptions? options]) { this.options = options ?? BaseOptions(); - httpClientAdapter = RHttpAdapter(const rhttp.ClientSettings()); + httpClientAdapter = RHttpAdapter(rhttp.ClientSettings( + proxySettings: proxy == null + ? const rhttp.ProxySettings.noProxy() + : rhttp.ProxySettings.proxy(proxy!), + tlsSettings: rhttp.TlsSettings( + verifyCertificates: !ignoreCertificateErrors, + ), + )); interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!)); interceptors.add(NetworkCacheManager()); interceptors.add(CloudflareInterceptor()); interceptors.add(MyLogInterceptor()); } - static HttpClient createHttpClient() { - final client = HttpClient(); - client.connectionTimeout = const Duration(seconds: 5); - client.findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy"; - client.idleTimeout = const Duration(seconds: 100); - client.badCertificateCallback = - (X509Certificate cert, String host, int port) { - if (ignoreCertificateErrors) return true; - if (host.contains("cdn")) return true; - 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})$'); - if (ipv4RegExp.hasMatch(host)) { - return true; - } - return false; - }; - return client; - } - static String? proxy; static Future getProxy() async { From b4921c8e14c7122908fafa7ea1c1efa63772e543 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 18 Nov 2024 16:59:54 +0800 Subject: [PATCH 20/25] support rich text comment --- lib/pages/comments_page.dart | 296 ++++++++++++++++++++++++++++++++++- lib/utils/app_links.dart | 6 +- 2 files changed, 297 insertions(+), 5 deletions(-) diff --git a/lib/pages/comments_page.dart b/lib/pages/comments_page.dart index cc345e7..8f19673 100644 --- a/lib/pages/comments_page.dart +++ b/lib/pages/comments_page.dart @@ -1,8 +1,14 @@ +import 'dart:collection'; + +import 'package:flutter/gestures.dart'; import 'package:flutter/material.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'; import 'package:venera/foundation/image_provider/cached_image.dart'; +import 'package:venera/utils/app_links.dart'; +import 'package:venera/utils/ext.dart'; import 'package:venera/utils/translations.dart'; class CommentsPage extends StatefulWidget { @@ -268,7 +274,10 @@ class _CommentTileState extends State<_CommentTile> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(widget.comment.userName, style: ts.bold,), + Text( + widget.comment.userName, + style: ts.bold, + ), if (widget.comment.time != null) Text(widget.comment.time!, style: ts.s12), const SizedBox(height: 4), @@ -426,7 +435,7 @@ class _CommentTileState extends State<_CommentTile> { isCancel, ); if (res.success) { - if(isCancel) { + if (isCancel) { voteStatus = 0; } else { if (isUp) { @@ -498,6 +507,287 @@ class _CommentContent extends StatelessWidget { @override Widget build(BuildContext context) { - return SelectableText(text); + if (!text.contains('<') && !text.contains('http')) { + return SelectableText(text); + } else { + return _RichCommentContent(text: text); + } + } +} + +class _Tag { + final String name; + final Map attributes; + + const _Tag(this.name, this.attributes); + + TextSpan merge(TextSpan s, BuildContext context) { + var style = s.style ?? ts; + style = switch (name) { + 'b' => style.bold, + 'i' => style.italic, + 'u' => style.underline, + 's' => style.lineThrough, + 'a' => style.withColor(context.colorScheme.primary), + 'span' => () { + if (attributes.containsKey('style')) { + var s = attributes['style']!; + var css = s.split(';'); + for (var c in css) { + var kv = c.split(':'); + if (kv.length == 2) { + var key = kv[0].trim(); + var value = kv[1].trim(); + switch (key) { + case 'color': + // Color is not supported, we should make text display well in light and dark mode. + break; + case 'font-weight': + if (value == 'bold') { + style = style.bold; + } else if (value == 'lighter') { + style = style.light; + } + break; + case 'font-style': + if (value == 'italic') { + style = style.italic; + } + break; + case 'text-decoration': + if (value == 'underline') { + style = style.underline; + } else if (value == 'line-through') { + style = style.lineThrough; + } + break; + case 'font-size': + // Font size is not supported. + break; + } + } + } + } + return style; + }(), + _ => style, + }; + if (style.color != null) { + style = style.copyWith(decorationColor: style.color); + } + var recognizer = s.recognizer; + if (name == 'a') { + var link = attributes['href']; + if (link != null && link.isURL) { + recognizer = TapGestureRecognizer() + ..onTap = () { + handleLink(link); + }; + } + } + return TextSpan( + text: s.text, + style: style, + recognizer: recognizer, + ); + } + + static void handleLink(String link) async { + if (link.isURL) { + if (await handleAppLink(Uri.parse(link))) { + App.rootContext.pop(); + } else { + launchUrlString(link); + } + } + } +} + +class _CommentImage { + final String url; + final String? link; + + const _CommentImage(this.url, this.link); +} + +class _RichCommentContent extends StatefulWidget { + const _RichCommentContent({required this.text}); + + final String text; + + @override + State<_RichCommentContent> createState() => _RichCommentContentState(); +} + +class _RichCommentContentState extends State<_RichCommentContent> { + var textSpan = []; + var images = <_CommentImage>[]; + + @override + void didChangeDependencies() { + render(); + super.didChangeDependencies(); + } + + bool isValidUrlChar(String char) { + return RegExp(r'[a-zA-Z0-9%:/.@\-_?&=#*!]').hasMatch(char); + } + + void render() { + var s = Queue<_Tag>(); + + int i = 0; + var buffer = StringBuffer(); + var text = widget.text; + + void writeBuffer() { + if (buffer.isEmpty) return; + var span = TextSpan(text: buffer.toString()); + for (var tag in s) { + span = tag.merge(span, context); + } + textSpan.add(span); + buffer.clear(); + } + + while (i < text.length) { + if (text[i] == '<' && i != text.length - 1) { + if (text[i + 1] != '/') { + // start tag + var j = text.indexOf('>', i); + if (j != -1) { + var tagContent = text.substring(i + 1, j); + var splits = tagContent.split(' '); + splits.removeWhere((element) => element.isEmpty); + var tagName = splits[0]; + var attributes = {}; + for (var k = 1; k < splits.length; k++) { + var attr = splits[k]; + var attrSplits = attr.split('='); + if (attrSplits.length == 2) { + attributes[attrSplits[0]] = attrSplits[1].replaceAll('"', ''); + } + } + const acceptedTags = ['img', 'a', 'b', 'i', 'u', 's', 'br', 'span']; + if (acceptedTags.contains(tagName)) { + writeBuffer(); + if (tagName == 'img') { + var url = attributes['src']; + String? link; + for (var tag in s) { + if (tag.name == 'a') { + link = tag.attributes['href']; + break; + } + } + if (url != null) { + images.add(_CommentImage(url, link)); + } + } else if (tagName == 'br') { + buffer.write('\n'); + } else { + s.add(_Tag(tagName, attributes)); + } + i = j + 1; + continue; + } + } + } else { + // end tag + var j = text.indexOf('>', i); + if (j != -1) { + var tagContent = text.substring(i + 2, j); + var splits = tagContent.split(' '); + splits.removeWhere((element) => element.isEmpty); + var tagName = splits[0]; + if (s.isNotEmpty && s.last.name == tagName) { + writeBuffer(); + s.removeLast(); + i = j + 1; + continue; + } + if (tagName == 'br') { + i = j + 1; + buffer.write('\n'); + continue; + } + } + } + } else if (text.length - i > 8 && + text.substring(i, i + 4) == 'http' && + !s.any((e) => e.name == 'a')) { + // auto link + int j = i; + for (; j < text.length; j++) { + if (!isValidUrlChar(text[j])) { + break; + } + } + var url = text.substring(i, j); + if (url.isURL) { + writeBuffer(); + textSpan.add(TextSpan( + text: url, + style: ts.withColor(context.colorScheme.primary), + recognizer: TapGestureRecognizer() + ..onTap = () { + _Tag.handleLink(url); + }, + )); + i = j; + continue; + } + } + buffer.write(text[i]); + i++; + } + writeBuffer(); + } + + @override + Widget build(BuildContext context) { + Widget content = SelectableText.rich( + TextSpan( + style: DefaultTextStyle.of(context).style, + children: textSpan, + ), + ); + if (images.isNotEmpty) { + content = Column( + mainAxisSize: MainAxisSize.min, + children: [ + content, + Wrap( + runSpacing: 4, + spacing: 4, + children: images.map((e) { + Widget image = Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.surfaceContainerLow, + ), + width: 100, + height: 100, + child: Image( + width: 100, + height: 100, + image: CachedImageProvider(e.url), + ), + ); + if (e.link != null) { + image = InkWell( + onTap: () { + _Tag.handleLink(e.link!); + }, + child: image, + ); + } + return image; + }).toList(), + ) + ], + ); + } + return content; } } diff --git a/lib/utils/app_links.dart b/lib/utils/app_links.dart index 2e36be3..e6947c7 100644 --- a/lib/utils/app_links.dart +++ b/lib/utils/app_links.dart @@ -10,7 +10,7 @@ void handleLinks() { }); } -void handleAppLink(Uri uri) async { +Future handleAppLink(Uri uri) async { for(var source in ComicSource.all()) { if(source.linkHandler != null) { if(source.linkHandler!.domains.contains(uri.host)) { @@ -22,9 +22,11 @@ void handleAppLink(Uri uri) async { App.mainNavigatorKey!.currentContext?.to(() { return ComicPage(id: id, sourceKey: source.key); }); + return true; } - return; + return false; } } } + return false; } \ No newline at end of file From 1489e6c86dd8ca5451000e9480be6fb7479b6fce Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 18 Nov 2024 17:02:07 +0800 Subject: [PATCH 21/25] add appVersion to JsEngine --- lib/foundation/js_engine.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart index 8ae647f..88eae18 100644 --- a/lib/foundation/js_engine.dart +++ b/lib/foundation/js_engine.dart @@ -19,6 +19,7 @@ import 'package:pointycastle/block/modes/cfb.dart'; import 'package:pointycastle/block/modes/ecb.dart'; import 'package:pointycastle/block/modes/ofb.dart'; import 'package:uuid/uuid.dart'; +import 'package:venera/foundation/app.dart'; import 'package:venera/network/app_dio.dart'; import 'package:venera/network/cookie_jar.dart'; @@ -70,6 +71,7 @@ class JsEngine with _JSEngineApi { var setGlobalFunc = _engine!.evaluate("(key, value) => { this[key] = value; }"); (setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]); + setGlobalFunc(["appVersion", App.version]); setGlobalFunc.free(); var jsInit = await rootBundle.load("assets/init.js"); _engine! From 250f4580293b83822825a46c5f2d09813b89cdc7 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 18 Nov 2024 17:22:25 +0800 Subject: [PATCH 22/25] improve word segmentation --- lib/components/comic.dart | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index e288119..268f1b6 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -320,13 +320,41 @@ class ComicTile extends StatelessWidget { )); } + List _splitText(String text) { + // split text by space, comma. text in brackets will be kept together. + var words = []; + var buffer = StringBuffer(); + var inBracket = false; + for (var i = 0; i < text.length; i++) { + var c = text[i]; + if (c == '[' || c == '(') { + inBracket = true; + } else if (c == ']' || c == ')') { + inBracket = false; + } else if (c == ' ' || c == ',') { + if (inBracket) { + buffer.write(c); + } else { + words.add(buffer.toString()); + buffer.clear(); + } + } else { + buffer.write(c); + } + } + if (buffer.isNotEmpty) { + words.add(buffer.toString()); + } + return words; + } + void block(BuildContext comicTileContext) { showDialog( context: App.rootContext, builder: (context) { var words = []; var all = []; - all.addAll(comic.title.split(' ').where((element) => element != '')); + all.addAll(_splitText(comic.title)); if (comic.subtitle != null && comic.subtitle != "") { all.add(comic.subtitle!); } From 689700f52a2212643388dba42be0a00575995573 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 18 Nov 2024 17:42:20 +0800 Subject: [PATCH 23/25] improve tab bar --- lib/components/appbar.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index 6573dea..80c67f7 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -369,10 +369,14 @@ class _FilledTabBarState extends State { final double tabWidth = tabRight - tabLeft; final double tabCenter = tabLeft + tabWidth / 2; final double tabBarWidth = tabBarBox.size.width; - final double scrollOffset = tabCenter - tabBarWidth / 2; + double scrollOffset = tabCenter - tabBarWidth / 2; if (scrollOffset == scrollController.offset) { return; } + scrollOffset = scrollOffset.clamp( + 0.0, + scrollController.position.maxScrollExtent, + ); scrollController.animateTo( scrollOffset, duration: const Duration(milliseconds: 200), From ce6f65f912ce89660d6f996d444ac0687e58b2fc Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 18 Nov 2024 17:56:27 +0800 Subject: [PATCH 24/25] fix auto link --- lib/pages/comments_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/comments_page.dart b/lib/pages/comments_page.dart index 8f19673..aa60868 100644 --- a/lib/pages/comments_page.dart +++ b/lib/pages/comments_page.dart @@ -630,7 +630,7 @@ class _RichCommentContentState extends State<_RichCommentContent> { } bool isValidUrlChar(String char) { - return RegExp(r'[a-zA-Z0-9%:/.@\-_?&=#*!]').hasMatch(char); + return RegExp(r'[a-zA-Z0-9%:/.@\-_?&=#*!+;]').hasMatch(char); } void render() { From 8db52c9db109df7c5031cd83e6c8bed3866875c9 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 18 Nov 2024 17:57:35 +0800 Subject: [PATCH 25/25] update version code --- lib/foundation/app.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 61fcb34..1cd8ab9 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -10,7 +10,7 @@ export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.0.5"; + final version = "1.0.6"; bool get isAndroid => Platform.isAndroid; diff --git a/pubspec.yaml b/pubspec.yaml index d061be5..0c27fe0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.0.5+105 +version: 1.0.6+106 environment: sdk: '>=3.5.0 <4.0.0'