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