diff --git a/android/app/build.gradle b/android/app/build.gradle index e29f83e..9a8ad45 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -32,7 +32,7 @@ keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) android { namespace = "com.github.wgh136.venera" compileSdk = flutter.compileSdkVersion - ndkVersion "25.1.8937393" + ndkVersion "28.0.13004108" splits{ abi { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f6483fe..2789298 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -47,6 +47,11 @@ + + + + + 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 65bae8b..9e0b9ec 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 @@ -7,6 +7,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build +import android.os.Bundle import android.os.Environment import android.provider.Settings import android.util.Log @@ -40,6 +41,41 @@ class MainActivity : FlutterFragmentActivity() { private val nextLocalRequestCode = AtomicInteger() + private val sharedTexts = ArrayList() + + private var textShareHandler: ((String) -> Unit)? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (intent?.action == Intent.ACTION_SEND) { + if (intent.type == "text/plain") { + val text = intent.getStringExtra(Intent.EXTRA_TEXT) + if (text != null) + handleSharedText(text) + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + if (intent.action == Intent.ACTION_SEND) { + if (intent.type == "text/plain") { + val text = intent.getStringExtra(Intent.EXTRA_TEXT) + if (text != null) + handleSharedText(text) + } + } + } + + private fun handleSharedText(text: String) { + if (textShareHandler != null) { + textShareHandler?.invoke(text) + } else { + sharedTexts.add(text) + } + } + private fun startContractForResult( contract: ActivityResultContract, input: I, @@ -134,6 +170,26 @@ class MainActivity : FlutterFragmentActivity() { val mimeType = req.arguments() openFile(res, mimeType!!) } + + val shareTextChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/text_share") + shareTextChannel.setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink) { + textShareHandler = {text -> + events.success(text) + } + if (sharedTexts.isNotEmpty()) { + for (text in sharedTexts) { + events.success(text) + } + sharedTexts.clear() + } + } + + override fun onCancel(arguments: Any?) { + textShareHandler = null + } + }) } private fun getProxy(): String { diff --git a/android/app/src/main/res/values-zh-rCN/strings.xml b/android/app/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000..daadbbd --- /dev/null +++ b/android/app/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,4 @@ + + + 搜索 + \ No newline at end of file diff --git a/android/app/src/main/res/values-zh/strings.xml b/android/app/src/main/res/values-zh/strings.xml new file mode 100644 index 0000000..b24f947 --- /dev/null +++ b/android/app/src/main/res/values-zh/strings.xml @@ -0,0 +1,4 @@ + + + 搜尋 + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..edd1aca --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Search + \ No newline at end of file diff --git a/assets/translation.json b/assets/translation.json index 030f946..fb4cf08 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -367,7 +367,12 @@ "Home Page": "主页", "Favorites Page": "收藏页面", "Explore Page": "探索页面", - "Categories Page": "分类页面" + "Categories Page": "分类页面", + "Convert to local": "转换为本地", + "Refresh": "刷新", + "Paging": "分页", + "Continuous": "连续", + "Display mode of comic list": "漫画列表的显示模式" }, "zh_TW": { "Home": "首頁", @@ -737,6 +742,11 @@ "Home Page": "首頁", "Favorites Page": "收藏頁面", "Explore Page": "探索頁面", - "Categories Page": "分類頁面" + "Categories Page": "分類頁面", + "Convert to local": "轉換為本地", + "Refresh": "刷新", + "Paging": "分頁", + "Continuous": "連續", + "Display mode of comic list": "漫畫列表的顯示模式" } } diff --git a/lib/components/comic.dart b/lib/components/comic.dart index aaad1ab..76aaebd 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -770,7 +770,7 @@ class _SliverGridComicsState extends State { @override void didUpdateWidget(covariant SliverGridComics oldWidget) { - if (!oldWidget.comics.isEqualTo(widget.comics)) { + if (!comics.isEqualTo(widget.comics)) { comics.clear(); for (var comic in widget.comics) { if (isBlocked(comic) == null) { @@ -879,6 +879,7 @@ class _SliverGridComics extends StatelessWidget { return comic; } return AnimatedContainer( + key: ValueKey(comics[index].id), duration: const Duration(milliseconds: 150), decoration: BoxDecoration( color: isSelected @@ -1140,7 +1141,7 @@ class ComicListState extends State { setState(() {}); }); } - if (_loading[page] == true) { + if (_data[page] != null || _loading[page] == true) { return; } _loading[page] = true; @@ -1150,8 +1151,8 @@ class ComicListState extends State { if (!mounted) return; if (res.success) { if (res.data.isEmpty) { - _data[page] = const []; setState(() { + _data[page] = const []; _maxPage = page; }); } else { @@ -1201,6 +1202,11 @@ class ComicListState extends State { @override Widget build(BuildContext context) { + var type = appdata.settings['comicListDisplayMode']; + return type == 'paging' ? buildPagingMode() : buildContinuousMode(); + } + + Widget buildPagingMode() { if (_error != null) { return Column( children: [ @@ -1249,6 +1255,85 @@ class ComicListState extends State { ], ); } + + Widget buildContinuousMode() { + if (_error != null && _data.isEmpty) { + return Column( + children: [ + if (widget.errorLeading != null) widget.errorLeading!, + _buildPageSelector(), + Expanded( + child: NetworkError( + withAppbar: false, + message: _error!, + retry: () { + setState(() { + _error = null; + }); + }, + ), + ), + ], + ); + } + if (_data[_page] == null) { + _loadPage(_page); + return Column( + children: [ + if (widget.errorLeading != null) widget.errorLeading!, + const Expanded( + child: Center( + child: CircularProgressIndicator(), + ), + ), + ], + ); + } + return SmoothCustomScrollView( + key: enablePageStorage ? PageStorageKey('scroll$_page') : null, + controller: widget.controller, + slivers: [ + if (widget.leadingSliver != null) widget.leadingSliver!, + SliverGridComics( + comics: _data.values.expand((element) => element).toList(), + menuBuilder: widget.menuBuilder, + onLastItemBuild: () { + if (_error == null && (_maxPage == null || _page < _maxPage!)) { + _loadPage(_data.length + 1); + } + }, + ), + if (_error != null) + SliverToBoxAdapter( + child: Column( + children: [ + Row( + children: [ + const Icon(Icons.error_outline), + const SizedBox(width: 8), + Expanded(child: Text(_error!, maxLines: 3)), + ], + ), + const SizedBox(height: 8), + Center( + child: OutlinedButton( + onPressed: () { + setState(() { + _error = null; + }); + }, + child: Text("Retry".tl), + ), + ), + ], + ).paddingHorizontal(16).paddingVertical(8), + ) + else if (_maxPage == null || _page < _maxPage!) + const SliverListLoadingIndicator(), + if (widget.trailingSliver != null) widget.trailingSliver!, + ], + ); + } } class StarRating extends StatelessWidget { @@ -1535,17 +1620,20 @@ class _SMClipper extends CustomClipper { } class SimpleComicTile extends StatelessWidget { - const SimpleComicTile({super.key, required this.comic, this.onTap}); + const SimpleComicTile( + {super.key, required this.comic, this.onTap, this.withTitle = false}); final Comic comic; final void Function()? onTap; + final bool withTitle; + @override Widget build(BuildContext context) { var image = _findImageProvider(comic); - var child = image == null + Widget child = image == null ? const SizedBox() : AnimatedImage( image: image, @@ -1555,7 +1643,18 @@ class SimpleComicTile extends StatelessWidget { filterQuality: FilterQuality.medium, ); - return AnimatedTapRegion( + child = Container( + width: 98, + height: 136, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.secondaryContainer, + ), + clipBehavior: Clip.antiAlias, + child: child, + ); + + child = AnimatedTapRegion( borderRadius: 8, onTap: onTap ?? () { @@ -1566,16 +1665,29 @@ class SimpleComicTile extends StatelessWidget { ), ); }, - child: Container( - width: 92, - height: 114, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context).colorScheme.secondaryContainer, - ), - clipBehavior: Clip.antiAlias, - child: child, - ), + child: child, ); + + if (withTitle) { + child = Column( + mainAxisSize: MainAxisSize.min, + children: [ + child, + const SizedBox(height: 4), + SizedBox( + width: 92, + child: Center( + child: Text( + comic.title.replaceAll('\n', ''), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ); + } + + return child; } } diff --git a/lib/components/scroll.dart b/lib/components/scroll.dart index dd0df47..3988630 100644 --- a/lib/components/scroll.dart +++ b/lib/components/scroll.dart @@ -99,11 +99,13 @@ class _SmoothScrollProviderState extends State { ); if (_futurePosition == old) return; var target = _futurePosition!; - _controller.animateTo( + _controller + .animateTo( _futurePosition!, duration: _fastAnimationDuration, curve: Curves.linear, - ).then((_) { + ) + .then((_) { var current = _controller.position.pixels; if (current == target && current == _futurePosition) { _futurePosition = null; @@ -144,3 +146,169 @@ class ScrollControllerProvider extends InheritedWidget { return oldWidget.controller != controller; } } + +class AppScrollBar extends StatefulWidget { + const AppScrollBar({ + super.key, + required this.controller, + required this.child, + this.topPadding = 0, + }); + + final ScrollController controller; + + final Widget child; + + final double topPadding; + + @override + State createState() => _AppScrollBarState(); +} + +class _AppScrollBarState extends State { + late final ScrollController _scrollController; + + double minExtent = 0; + double maxExtent = 0; + double position = 0; + + double viewHeight = 0; + + final _scrollIndicatorSize = App.isDesktop ? 42.0 : 64.0; + + late final VerticalDragGestureRecognizer _dragGestureRecognizer; + + @override + void initState() { + super.initState(); + _scrollController = widget.controller; + _scrollController.addListener(onChanged); + Future.microtask(onChanged); + _dragGestureRecognizer = VerticalDragGestureRecognizer() + ..onUpdate = onUpdate; + } + + void onUpdate(DragUpdateDetails details) { + if (maxExtent - minExtent <= 0 || + viewHeight == 0 || + details.primaryDelta == null) { + return; + } + var offset = details.primaryDelta!; + var positionOffset = + offset / (viewHeight - _scrollIndicatorSize) * (maxExtent - minExtent); + _scrollController.jumpTo((position + positionOffset).clamp( + minExtent, + maxExtent, + )); + } + + void onChanged() { + if (_scrollController.positions.isEmpty) return; + var position = _scrollController.position; + if (position.minScrollExtent != minExtent || + position.maxScrollExtent != maxExtent || + position.pixels != this.position) { + setState(() { + minExtent = position.minScrollExtent; + maxExtent = position.maxScrollExtent; + this.position = position.pixels; + }); + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constrains) { + var scrollHeight = (maxExtent - minExtent); + var height = constrains.maxHeight - widget.topPadding; + viewHeight = height; + var top = scrollHeight == 0 + ? 0.0 + : (position - minExtent) / + scrollHeight * + (height - _scrollIndicatorSize); + return Stack( + children: [ + Positioned.fill( + child: widget.child, + ), + Positioned( + top: top + widget.topPadding, + right: 0, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: (event) { + _dragGestureRecognizer.addPointer(event); + }, + child: SizedBox( + width: _scrollIndicatorSize/2, + height: _scrollIndicatorSize, + child: CustomPaint( + painter: _ScrollIndicatorPainter( + backgroundColor: context.colorScheme.surface, + shadowColor: context.colorScheme.shadow, + ), + child: Column( + children: [ + const Spacer(), + Icon(Icons.arrow_drop_up, size: 18), + Icon(Icons.arrow_drop_down, size: 18), + const Spacer(), + ], + ).paddingLeft(4), + ), + ), + ), + ), + ), + ], + ); + }, + ); + } +} + +class _ScrollIndicatorPainter extends CustomPainter { + final Color backgroundColor; + + final Color shadowColor; + + const _ScrollIndicatorPainter({ + required this.backgroundColor, + required this.shadowColor, + }); + + @override + void paint(Canvas canvas, Size size) { + var path = Path() + ..moveTo(size.width, 0) + ..lineTo(size.width, size.height) + ..arcToPoint( + Offset(size.width, 0), + radius: Radius.circular(size.width), + ); + canvas.drawShadow(path, shadowColor, 4, true); + var backgroundPaint = Paint() + ..color = backgroundColor + ..style = PaintingStyle.fill; + path = Path() + ..moveTo(size.width, 0) + ..lineTo(size.width, size.height) + ..arcToPoint( + Offset(size.width, 0), + radius: Radius.circular(size.width), + ); + canvas.drawPath(path, backgroundPaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return oldDelegate is! _ScrollIndicatorPainter || + oldDelegate.backgroundColor != backgroundColor || + oldDelegate.shadowColor != shadowColor; + } +} diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 12e1fa7..9c472b7 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -13,7 +13,7 @@ export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.3.2"; + final version = "1.3.3"; bool get isAndroid => Platform.isAndroid; diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 93c1405..6959be4 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -4,9 +4,10 @@ import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/utils/data_sync.dart'; +import 'package:venera/utils/init.dart'; import 'package:venera/utils/io.dart'; -class Appdata { +class Appdata with Init { Appdata._create(); final Settings settings = Settings._create(); @@ -53,28 +54,6 @@ class Appdata { saveData(); } - Future init() async { - var dataPath = (await getApplicationSupportDirectory()).path; - var file = File(FilePath.join( - dataPath, - 'appdata.json', - )); - if (!await file.exists()) { - return; - } - var json = jsonDecode(await file.readAsString()); - for (var key in (json['settings'] as Map).keys) { - if (json['settings'][key] != null) { - settings[key] = json['settings'][key]; - } - } - searchHistory = List.from(json['searchHistory']); - var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json')); - if (await implicitDataFile.exists()) { - implicitData = jsonDecode(await implicitDataFile.readAsString()); - } - } - Map toJson() { return { 'settings': settings._data, @@ -110,6 +89,29 @@ class Appdata { var file = File(FilePath.join(App.dataPath, 'implicitData.json')); file.writeAsString(jsonEncode(implicitData)); } + + @override + Future doInit() async { + var dataPath = (await getApplicationSupportDirectory()).path; + var file = File(FilePath.join( + dataPath, + 'appdata.json', + )); + if (!await file.exists()) { + return; + } + var json = jsonDecode(await file.readAsString()); + for (var key in (json['settings'] as Map).keys) { + if (json['settings'][key] != null) { + settings[key] = json['settings'][key]; + } + } + searchHistory = List.from(json['searchHistory']); + var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json')); + if (await implicitDataFile.exists()) { + implicitData = jsonDecode(await implicitDataFile.readAsString()); + } + } } final appdata = Appdata._create(); @@ -160,10 +162,12 @@ class Settings with ChangeNotifier { 'customImageProcessing': defaultCustomImageProcessing, 'sni': true, 'autoAddLanguageFilter': 'none', // none, chinese, english, japanese - 'comicSourceListUrl': "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json", + 'comicSourceListUrl': + "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json", 'preloadImageCount': 4, 'followUpdatesFolder': null, 'initialPage': '0', + 'comicListDisplayMode': 'paging', // paging, continuous }; operator [](String key) { diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 2ecc171..f17c76d 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -111,6 +111,9 @@ class Comic { @override int get hashCode => id.hashCode ^ sourceKey.hashCode; + + @override + toString() => "$sourceKey@$id"; } class ComicDetails with HistoryMixin { diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index f7ac7a4..0b9f353 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -224,7 +224,8 @@ class LocalFavoritesManager with ChangeNotifier { source_folder text ); """); - for (var folder in _getFolderNamesWithDB()) { + var folderNames = _getFolderNamesWithDB(); + for (var folder in folderNames) { var columns = _db.select(""" pragma table_info("$folder"); """); @@ -246,6 +247,15 @@ class LocalFavoritesManager with ChangeNotifier { break; } } + await appdata.ensureInit(); + // Make sure the follow updates folder is ready + var followUpdateFolder = appdata.settings['followUpdatesFolder']; + if (followUpdateFolder is String && + folderNames.contains(followUpdateFolder)) { + prepareTableForFollowUpdates(followUpdateFolder, false); + } else { + appdata.settings['followUpdatesFolder'] = null; + } } List find(String id, ComicType type) { @@ -849,7 +859,7 @@ class LocalFavoritesManager with ChangeNotifier { } } - void prepareTableForFollowUpdates(String table) { + void prepareTableForFollowUpdates(String table, [bool clearData = true]) { // check if the table has the column "last_update_time" "has_new_update" "last_check_time" var columns = _db.select(""" pragma table_info("$table"); @@ -866,10 +876,12 @@ class LocalFavoritesManager with ChangeNotifier { add column has_new_update int; """); } - _db.execute(""" + if (clearData) { + _db.execute(""" update "$table" set has_new_update = 0; """); + } if (!columns.any((element) => element["name"] == "last_check_time")) { _db.execute(""" alter table "$table" diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index be0efe0..c5f00b6 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -422,12 +422,30 @@ class LocalManager with ChangeNotifier { return files.map((e) => "file://${e.path}").toList(); } - bool isDownloaded(String id, ComicType type, [int? ep]) { + bool isDownloaded(String id, ComicType type, + [int? ep, ComicChapters? chapters]) { var comic = find(id, type); if (comic == null) return false; if (comic.chapters == null || ep == null) return true; + if (chapters != null) { + if (comic.chapters?.length != chapters.length) { + // update + add(LocalComic( + id: comic.id, + title: comic.title, + subtitle: comic.subtitle, + tags: comic.tags, + directory: comic.directory, + chapters: chapters, + cover: comic.cover, + comicType: comic.comicType, + downloadedChapters: comic.downloadedChapters, + createdAt: comic.createdAt, + )); + } + } return comic.downloadedChapters - .contains(comic.chapters!.ids.elementAt(ep - 1)); + .contains((chapters ?? comic.chapters)!.ids.elementAtOrNull(ep - 1)); } List downloadingTasks = []; diff --git a/lib/init.dart b/lib/init.dart index 939ea70..2841a03 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -11,6 +11,7 @@ import 'package:venera/pages/comic_source_page.dart'; import 'package:venera/pages/follow_updates_page.dart'; import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/utils/app_links.dart'; +import 'package:venera/utils/handle_text_share.dart'; import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; import 'foundation/appdata.dart'; @@ -45,6 +46,7 @@ Future init() async { _checkOldConfigs(); if (App.isAndroid) { handleLinks(); + handleTextShare(); } FlutterError.onError = (details) { Log.error("Unhandled Exception", "${details.exception}\n${details.stack}"); diff --git a/lib/main.dart b/lib/main.dart index 1d0a880..0ab3875 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -141,24 +141,15 @@ class _MyAppState extends State with WidgetsBindingObserver { ) { String? font; List? fallback; - if (App.isWindows) { - font = 'Segoe UI'; - fallback = [ - 'Segoe UI', - 'Microsoft YaHei', - 'PingFang SC', - 'Noto Sans CJK', - 'Arial', - 'sans-serif' - ]; - } - if (App.isLinux) { + if (App.isLinux || App.isWindows) { font = 'Noto Sans CJK'; fallback = [ 'Segoe UI', + 'Noto Sans SC', + 'Noto Sans TC', + 'Noto Sans', 'Microsoft YaHei', 'PingFang SC', - 'Noto Sans CJK', 'Arial', 'sans-serif' ]; diff --git a/lib/network/app_dio.dart b/lib/network/app_dio.dart index 7689410..4dc4393 100644 --- a/lib/network/app_dio.dart +++ b/lib/network/app_dio.dart @@ -282,9 +282,27 @@ class RHttpAdapter implements HttpClientAdapter { return ResponseBody( res.body, res.statusCode, - statusMessage: null, + statusMessage: _getStatusMessage(res.statusCode), isRedirect: false, headers: headers, ); } + + static String _getStatusMessage(int statusCode) { + return switch(statusCode) { + 200 => "OK", + 201 => "Created", + 202 => "Accepted", + 204 => "No Content", + 206 => "Partial Content", + 301 => "Moved Permanently", + 302 => "Found", + 400 => "Invalid Status Code 400: The Request is invalid.", + 401 => "Invalid Status Code 401: The Request is unauthorized.", + 403 => "Invalid Status Code 403: No permission to access the resource. Check your account or network.", + 404 => "Invalid Status Code 404: Not found.", + 429 => "Invalid Status Code 429: Too many requests. Please try again later.", + _ => "Invalid Status Code $statusCode", + }; + } } diff --git a/lib/pages/aggregated_search_page.dart b/lib/pages/aggregated_search_page.dart index a84c471..75f66e2 100644 --- a/lib/pages/aggregated_search_page.dart +++ b/lib/pages/aggregated_search_page.dart @@ -90,7 +90,7 @@ class _SliverSearchResultState extends State<_SliverSearchResult> with AutomaticKeepAliveClientMixin { bool isLoading = true; - static const _kComicHeight = 132.0; + static const _kComicHeight = 162.0; get _comicWidth => _kComicHeight * 0.7; @@ -152,7 +152,7 @@ class _SliverSearchResultState extends State<_SliverSearchResult> } Widget buildComic(Comic c) { - return SimpleComicTile(comic: c) + return SimpleComicTile(comic: c, withTitle: true) .paddingLeft(_kLeftPadding) .paddingBottom(2); } diff --git a/lib/pages/comic_details_page/chapters.dart b/lib/pages/comic_details_page/chapters.dart index 0a9586f..10ece3d 100644 --- a/lib/pages/comic_details_page/chapters.dart +++ b/lib/pages/comic_details_page/chapters.dart @@ -186,12 +186,17 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> late TabController tabController; - int index = 0; + late int index; @override void initState() { super.initState(); history = widget.history; + if (history?.group != null) { + index = history!.group! - 1; + } else { + index = 0; + } } @override @@ -199,6 +204,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> state = context.findAncestorStateOfType<_ComicPageState>()!; chapters = state.comic.chapters!; tabController = TabController( + initialIndex: index, length: chapters.ids.length, vsync: this, ); diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index d714be9..eea258a 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -518,11 +518,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { ), ], ); - body = Scrollbar( + body = AppScrollBar( + topPadding: 48, controller: scrollController, - thickness: App.isDesktop ? 8 : 12, - radius: const Radius.circular(8), - interactive: true, child: ScrollConfiguration( behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), child: body, diff --git a/lib/pages/favorites/network_favorites_page.dart b/lib/pages/favorites/network_favorites_page.dart index 5baef91..57582e7 100644 --- a/lib/pages/favorites/network_favorites_page.dart +++ b/lib/pages/favorites/network_favorites_page.dart @@ -110,6 +110,15 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> { child: Text(widget.data.title), ), actions: [ + Tooltip( + message: "Refresh".tl, + child: IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + comicListKey.currentState!.refresh(); + }, + ), + ), MenuButton(entries: [ MenuEntry( icon: Icons.sync, diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 94a281f..579e162 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -297,7 +297,7 @@ class _HistoryState extends State<_History> { ).paddingHorizontal(16), if (history.isNotEmpty) SizedBox( - height: 128, + height: 136, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: history.length, @@ -400,13 +400,14 @@ class _LocalState extends State<_Local> { ).paddingHorizontal(16), if (local.isNotEmpty) SizedBox( - height: 128, + height: 136, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: local.length, itemBuilder: (context, index) { return SimpleComicTile(comic: local[index]) - .paddingHorizontal(8); + .paddingHorizontal(8) + .paddingVertical(2); }, ), ).paddingHorizontal(8), diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index 409ed1f..2b45c63 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; +import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/pages/comic_details_page/comic_page.dart'; @@ -304,7 +305,9 @@ class _LocalComicsPageState extends State { } }); } else { - (c as LocalComic).read(); + // prevent dirty data + var comic = LocalManager().find(c.id, ComicType(c.sourceKey.hashCode))!; + comic.read(); } }, menuBuilder: (c) { diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 43932c4..00018f1 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -26,7 +26,7 @@ class _ReaderImagesState extends State<_ReaderImages> { inProgress = true; if (reader.type == ComicType.local || (LocalManager() - .isDownloaded(reader.cid, reader.type, reader.chapter))) { + .isDownloaded(reader.cid, reader.type, reader.chapter, reader.widget.chapters))) { try { var images = await LocalManager() .getImages(reader.cid, reader.type, reader.chapter); diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 057e78b..cb75c04 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -115,7 +115,7 @@ class _ReaderState extends State String get cid => widget.cid; - String get eid => widget.chapters?.ids.elementAt(chapter - 1) ?? '0'; + String get eid => widget.chapters?.ids.elementAtOrNull(chapter - 1) ?? '0'; List? images; diff --git a/lib/pages/settings/explore_settings.dart b/lib/pages/settings/explore_settings.dart index 6721a5e..da8e2bf 100644 --- a/lib/pages/settings/explore_settings.dart +++ b/lib/pages/settings/explore_settings.dart @@ -90,6 +90,14 @@ class _ExploreSettingsState extends State { '3': "Categories Page".tl, }, ).toSliver(), + SelectSetting( + title: "Display mode of comic list".tl, + settingKey: "comicListDisplayMode", + optionTranslation: { + "paging": "Paging".tl, + "Continuous": "Continuous".tl, + }, + ).toSliver(), ], ); } diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 56f14c9..bd5195b 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -95,11 +95,13 @@ Future importAppData(File file, [bool checkVersion = false]) async { } var comicSourceDir = FilePath.join(cacheDirPath, "comic_source"); if (Directory(comicSourceDir).existsSync()) { + Directory(FilePath.join(App.dataPath, "comic_source")) + .deleteIfExistsSync(recursive: true); + Directory(FilePath.join(App.dataPath, "comic_source")).createSync(); for (var file in Directory(comicSourceDir).listSync()) { if (file is File) { var targetFile = FilePath.join(App.dataPath, "comic_source", file.name); - File(targetFile).deleteIfExistsSync(); await file.copy(targetFile); } } diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index 763685e..04cf28b 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -9,6 +9,7 @@ import 'package:venera/network/app_dio.dart'; import 'package:venera/utils/data.dart'; import 'package:venera/utils/ext.dart'; import 'package:webdav_client/webdav_client.dart' hide File; +import 'package:rhttp/rhttp.dart' as rhttp; import 'io.dart'; @@ -89,11 +90,18 @@ class DataSync with ChangeNotifier { String user = config[1]; String pass = config[2]; + var proxy = await AppDio.getProxy(); + var client = newClient( url, user: user, password: pass, - adapter: RHttpAdapter(), + adapter: RHttpAdapter( + rhttp.ClientSettings( + proxySettings: + proxy == null ? null : rhttp.ProxySettings.proxy(proxy), + ), + ), ); try { @@ -154,11 +162,18 @@ class DataSync with ChangeNotifier { String user = config[1]; String pass = config[2]; + var proxy = await AppDio.getProxy(); + var client = newClient( url, user: user, password: pass, - adapter: RHttpAdapter(), + adapter: RHttpAdapter( + rhttp.ClientSettings( + proxySettings: + proxy == null ? null : rhttp.ProxySettings.proxy(proxy), + ), + ), ); try { diff --git a/lib/utils/handle_text_share.dart b/lib/utils/handle_text_share.dart new file mode 100644 index 0000000..b40d669 --- /dev/null +++ b/lib/utils/handle_text_share.dart @@ -0,0 +1,22 @@ +import 'package:flutter/services.dart'; +import 'package:venera/foundation/app.dart'; +import 'package:venera/pages/aggregated_search_page.dart'; + +bool _isHandling = false; + +/// Handle text share event. +/// App will navigate to [AggregatedSearchPage] with the shared text as keyword. +void handleTextShare() async { + if (_isHandling) return; + _isHandling = true; + + var channel = EventChannel('venera/text_share'); + await for (var event in channel.receiveBroadcastStream()) { + if (App.mainNavigatorKey == null) { + await Future.delayed(const Duration(milliseconds: 200)); + } + if (event is String) { + App.rootContext.to(() => AggregatedSearchPage(keyword: event)); + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 84c2b4b..620d6c1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -408,8 +408,8 @@ packages: dependency: "direct main" description: path: "." - ref: "5978d0c7784fbbefcacc573547f0ab01ba59b7b3" - resolved-ref: "5978d0c7784fbbefcacc573547f0ab01ba59b7b3" + ref: "8feae95df7fb00455df129ad7a0dfec1d0e8d8e4" + resolved-ref: "8feae95df7fb00455df129ad7a0dfec1d0e8d8e4" url: "https://github.com/wgh136/flutter_qjs" source: git version: "0.3.7" @@ -425,17 +425,17 @@ packages: dependency: transitive description: name: flutter_rust_bridge - sha256: "3292ad6085552987b8b3b9a7e5805567f4013372d302736b702801acb001ee00" + sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611" url: "https://pub.dev" source: hosted - version: "2.7.1" + version: "2.9.0" flutter_saf: dependency: "direct main" description: path: "." - ref: "7637b8b67d0a831f3cd7e702b8173e300880d32e" - resolved-ref: "7637b8b67d0a831f3cd7e702b8173e300880d32e" - url: "https://github.com/pkuislm/flutter_saf.git" + ref: "690a03a954f1603e0149cfd479c8961b88f21336" + resolved-ref: "690a03a954f1603e0149cfd479c8961b88f21336" + url: "https://github.com/venera-app/flutter_saf" source: git version: "0.0.1" flutter_test: @@ -612,8 +612,8 @@ packages: dependency: "direct main" description: path: "." - ref: "9a784b193af5d55b2a35e58fa390bda3e4f35d00" - resolved-ref: "9a784b193af5d55b2a35e58fa390bda3e4f35d00" + ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1 + resolved-ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1 url: "https://github.com/venera-app/lodepng_flutter" source: git version: "0.0.1" @@ -757,12 +757,11 @@ packages: rhttp: dependency: "direct main" description: - path: rhttp - ref: HEAD - resolved-ref: "18d430cc45fd4f0114885c5235090abf65106257" - url: "https://github.com/wgh136/rhttp" - source: git - version: "0.10.0" + name: rhttp + sha256: "037e9b59a68bb4ba664db1cbb4601e878cf5a2fc1cb3d0a9c58e3776609dec4d" + url: "https://pub.dev" + source: hosted + version: "0.11.0" screen_retriever: dependency: transitive description: @@ -1045,8 +1044,8 @@ packages: dependency: "direct main" description: path: "." - ref: "285f87f15bccd2d5d5ff443761348c6ee47b98d1" - resolved-ref: "285f87f15bccd2d5d5ff443761348c6ee47b98d1" + ref: "2f669c98fb81cff1c64fee93466a1475c77e4273" + resolved-ref: "2f669c98fb81cff1c64fee93466a1475c77e4273" url: "https://github.com/wgh136/webdav_client" source: git version: "1.2.2" @@ -1094,10 +1093,10 @@ packages: dependency: "direct main" description: name: zip_flutter - sha256: bbf3160062610a43901b7ebbc6f6dd46519540f03a84027dc7b1fff399dda1ac + sha256: c4d5a34c5803def866bc550926bb16fe89717c9b7304695d5b2ede30964eb8a8 url: "https://pub.dev" source: hosted - version: "0.0.10" + version: "0.0.12" sdks: dart: ">=3.7.0 <4.0.0" - flutter: ">=3.29.0" + flutter: ">=3.29.2" diff --git a/pubspec.yaml b/pubspec.yaml index 37a2bad..ae37b56 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,11 +2,11 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.3.2+132 +version: 1.3.3+133 environment: sdk: '>=3.6.0 <4.0.0' - flutter: 3.29.0 + flutter: 3.29.2 dependencies: flutter: @@ -19,7 +19,7 @@ dependencies: flutter_qjs: git: url: https://github.com/wgh136/flutter_qjs - ref: 5978d0c7784fbbefcacc573547f0ab01ba59b7b3 + ref: 8feae95df7fb00455df129ad7a0dfec1d0e8d8e4 crypto: ^3.0.6 dio: ^5.8.0+1 html: ^0.15.5 @@ -52,25 +52,22 @@ dependencies: sliver_tools: ^0.2.12 flutter_file_dialog: ^3.0.2 file_selector: ^1.0.3 - zip_flutter: ^0.0.10 + zip_flutter: ^0.0.12 lodepng_flutter: git: url: https://github.com/venera-app/lodepng_flutter - ref: 9a784b193af5d55b2a35e58fa390bda3e4f35d00 - rhttp: - git: - url: https://github.com/wgh136/rhttp - path: rhttp + ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1 + rhttp: ^0.11.0 webdav_client: git: url: https://github.com/wgh136/webdav_client - ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1 + ref: 2f669c98fb81cff1c64fee93466a1475c77e4273 battery_plus: ^6.2.1 local_auth: ^2.3.0 flutter_saf: git: - url: https://github.com/pkuislm/flutter_saf.git - ref: 7637b8b67d0a831f3cd7e702b8173e300880d32e + url: https://github.com/venera-app/flutter_saf + ref: 690a03a954f1603e0149cfd479c8961b88f21336 dynamic_color: ^1.7.0 shimmer_animation: ^2.1.0 flutter_memory_info: ^0.0.1