Merge pull request #274 from venera-app/v1.3.3-dev

V1.3.3
This commit is contained in:
nyne
2025-03-18 17:48:21 +08:00
committed by GitHub
31 changed files with 584 additions and 113 deletions

View File

@@ -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 {

View File

@@ -47,6 +47,11 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="exhentai.org" android:pathPrefix="/g" />
</intent-filter>
<intent-filter android:label="@string/share_text">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View File

@@ -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<String>()
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 <I, O> startContractForResult(
contract: ActivityResultContract<I, O>,
input: I,
@@ -134,6 +170,26 @@ class MainActivity : FlutterFragmentActivity() {
val mimeType = req.arguments<String>()
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 {

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="share_text">搜索</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="share_text">搜尋</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="share_text">Search</string>
</resources>

View File

@@ -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": "漫畫列表的顯示模式"
}
}

View File

@@ -770,7 +770,7 @@ class _SliverGridComicsState extends State<SliverGridComics> {
@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<ComicList> {
setState(() {});
});
}
if (_loading[page] == true) {
if (_data[page] != null || _loading[page] == true) {
return;
}
_loading[page] = true;
@@ -1150,8 +1151,8 @@ class ComicListState extends State<ComicList> {
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<ComicList> {
@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<ComicList> {
],
);
}
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<Rect> {
}
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,
);
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;
}
}

View File

@@ -99,11 +99,13 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
);
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<AppScrollBar> createState() => _AppScrollBarState();
}
class _AppScrollBarState extends State<AppScrollBar> {
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;
}
}

View File

@@ -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;

View File

@@ -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<void> 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<String, dynamic>).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<String, dynamic> 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<void> 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<String, dynamic>).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) {

View File

@@ -111,6 +111,9 @@ class Comic {
@override
int get hashCode => id.hashCode ^ sourceKey.hashCode;
@override
toString() => "$sourceKey@$id";
}
class ComicDetails with HistoryMixin {

View File

@@ -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<String> 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;
""");
}
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"

View File

@@ -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<DownloadTask> downloadingTasks = [];

View File

@@ -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<void> init() async {
_checkOldConfigs();
if (App.isAndroid) {
handleLinks();
handleTextShare();
}
FlutterError.onError = (details) {
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");

View File

@@ -141,24 +141,15 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
) {
String? font;
List<String>? 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'
];

View File

@@ -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",
};
}
}

View File

@@ -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);
}

View File

@@ -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,
);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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),

View File

@@ -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<LocalComicsPage> {
}
});
} else {
(c as LocalComic).read();
// prevent dirty data
var comic = LocalManager().find(c.id, ComicType(c.sourceKey.hashCode))!;
comic.read();
}
},
menuBuilder: (c) {

View File

@@ -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);

View File

@@ -115,7 +115,7 @@ class _ReaderState extends State<Reader>
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<String>? images;

View File

@@ -90,6 +90,14 @@ class _ExploreSettingsState extends State<ExploreSettings> {
'3': "Categories Page".tl,
},
).toSliver(),
SelectSetting(
title: "Display mode of comic list".tl,
settingKey: "comicListDisplayMode",
optionTranslation: {
"paging": "Paging".tl,
"Continuous": "Continuous".tl,
},
).toSliver(),
],
);
}

View File

@@ -95,11 +95,13 @@ Future<void> 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);
}
}

View File

@@ -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 {

View File

@@ -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));
}
}
}

View File

@@ -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"

View File

@@ -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