mirror of
https://github.com/venera-app/venera.git
synced 2025-09-28 00:07:24 +00:00
Compare commits
46 Commits
v1.3.1
...
linux-wind
Author | SHA1 | Date | |
---|---|---|---|
d9b23dadf0 | |||
d05eaf8c7e | |||
![]() |
9dae28e366 | ||
![]() |
11e66328c4 | ||
![]() |
73d4e28ed0 | ||
![]() |
169676fd9e | ||
332497cf90 | |||
5f15c08eef | |||
3f6b3152b2 | |||
f5b3b36acb | |||
fd8607777e | |||
fa951cac95 | |||
55ad652191 | |||
533497ead1 | |||
![]() |
00cdc18ddd | ||
![]() |
474d9aa6f1 | ||
ffa0c8f887 | |||
0f3f3ea270 | |||
![]() |
b752caa079 | ||
309df2143b | |||
8e964468ea | |||
ca8f09807b | |||
68b214e295 | |||
00c0a64de0 | |||
![]() |
dbc2c27db0 | ||
fffb3dc973 | |||
0ca8a28639 | |||
6426ebaf16 | |||
316f61394d | |||
04ab75cf92 | |||
4828a57e1a | |||
d089163220 | |||
7b5c13200d | |||
0f6874f8d7 | |||
4af15b9139 | |||
9fe49217dc | |||
76c56964a5 | |||
e8afbca7b2 | |||
5843d7c919 | |||
![]() |
de98dfaa1b | ||
![]() |
30cbfb54ef | ||
c633021963 | |||
![]() |
4640831e69 | ||
af7a7c220e | |||
fd19f6bf7d | |||
96b4125613 |
24
.github/ISSUE_TEMPLATE/bug.yaml
vendored
24
.github/ISSUE_TEMPLATE/bug.yaml
vendored
@@ -8,9 +8,31 @@ body:
|
||||
value: |
|
||||
Thank you for reporting a problem, please complete the title and fill in the following information.
|
||||
|
||||
感谢您的反馈,请填写完整标题并填写以下信息。
|
||||
|
||||
**Please do not report any issues related to config files.**
|
||||
|
||||
To report a bug related to the config file, please send it to the [config repository](https://github.com/venera-app/venera-configs).
|
||||
**请不要报告与配置文件相关的任何问题。**
|
||||
|
||||
This project is a comic reader that allows users write their own config files. And there is no built-in comic source.
|
||||
|
||||
本项目是一个漫画阅读器,允许用户编写自己的配置文件,并且没有内置漫画源。
|
||||
- type: dropdown
|
||||
id: bugType
|
||||
attributes:
|
||||
label: Bug type
|
||||
description: What type of bug are you reporting?
|
||||
options:
|
||||
- Crash
|
||||
- UI
|
||||
- Performance
|
||||
- Security
|
||||
- Reader
|
||||
- JS Engine
|
||||
- Comic Source
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
10
.github/ISSUE_TEMPLATE/enhancement.yaml
vendored
10
.github/ISSUE_TEMPLATE/enhancement.yaml
vendored
@@ -7,6 +7,16 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Welcome to make a feature request, please fill in the following information after completing the title.
|
||||
|
||||
欢迎提出功能建议,请填写完整标题后填写以下信息。
|
||||
|
||||
**Please do not report any issues related to config files.**
|
||||
|
||||
**请不要报告与配置文件相关的任何问题。**
|
||||
|
||||
This project is a comic reader that allows users write their own config files. And there is no built-in comic source.
|
||||
|
||||
本项目是一个漫画阅读器,允许用户编写自己的配置文件,并且没有内置漫画源。
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
|
9
.github/ISSUE_TEMPLATE/other.yaml
vendored
9
.github/ISSUE_TEMPLATE/other.yaml
vendored
@@ -1,9 +0,0 @@
|
||||
name: other
|
||||
description: Other contents
|
||||
body:
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: Content
|
||||
validations:
|
||||
required: true
|
9
.github/workflows/main.yml
vendored
9
.github/workflows/main.yml
vendored
@@ -26,6 +26,9 @@ jobs:
|
||||
echo "$CERTIFICATE" | base64 --decode > signing_certificate.p12
|
||||
security import signing_certificate.p12 -k ~/Library/Keychains/login.keychain -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
|
||||
- name: Check rust-toolchain.toml
|
||||
run: rustup show
|
||||
|
||||
# Step 2: Build the Flutter macOS app
|
||||
- name: Build Flutter macOS App
|
||||
run: flutter build macos --release
|
||||
@@ -97,10 +100,8 @@ jobs:
|
||||
with:
|
||||
distribution: 'oracle'
|
||||
java-version: '17'
|
||||
- name: Setup Rust
|
||||
run: |
|
||||
rustup update
|
||||
rustup default stable
|
||||
- name: Check rust-toolchain.toml
|
||||
run: rustup show
|
||||
- run: flutter pub get
|
||||
- run: flutter build apk --release
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
@@ -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 {
|
||||
|
@@ -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 -->
|
||||
|
@@ -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 {
|
||||
|
4
android/app/src/main/res/values-zh-rCN/strings.xml
Normal file
4
android/app/src/main/res/values-zh-rCN/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="share_text">搜索</string>
|
||||
</resources>
|
4
android/app/src/main/res/values-zh/strings.xml
Normal file
4
android/app/src/main/res/values-zh/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="share_text">搜尋</string>
|
||||
</resources>
|
4
android/app/src/main/res/values/strings.xml
Normal file
4
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="share_text">Search</string>
|
||||
</resources>
|
@@ -358,11 +358,22 @@
|
||||
"Once the operation is successful, app will automatically sync data with the server.": "操作成功后, APP将自动与服务器同步数据",
|
||||
"Cache cleared": "缓存已清除",
|
||||
"Disabled": "已禁用",
|
||||
"WebDAV Auto Sync": "WebDAV 自动同步",
|
||||
"Auto Sync Data": "自动同步数据",
|
||||
"Mark all as read": "全部标记为已读",
|
||||
"Do you want to mark all as read?" : "您要全部标记为已读吗?",
|
||||
"Swipe down for previous chapter": "向下滑动查看上一章",
|
||||
"Swipe up for next chapter": "向上滑动查看下一章"
|
||||
"Swipe up for next chapter": "向上滑动查看下一章",
|
||||
"Initial Page": "初始页面",
|
||||
"Home Page": "主页",
|
||||
"Favorites Page": "收藏页面",
|
||||
"Explore Page": "探索页面",
|
||||
"Categories Page": "分类页面",
|
||||
"Convert to local": "转换为本地",
|
||||
"Refresh": "刷新",
|
||||
"Paging": "分页",
|
||||
"Continuous": "连续",
|
||||
"Display mode of comic list": "漫画列表的显示模式",
|
||||
"A valid WebDav directory URL": "有效的WebDav目录URL"
|
||||
},
|
||||
"zh_TW": {
|
||||
"Home": "首頁",
|
||||
@@ -720,13 +731,24 @@
|
||||
"All Comics": "全部漫畫",
|
||||
"The comic will be marked as no updates as soon as you read it.": "漫畫將在您閱讀後立即標記為無更新",
|
||||
"Disable": "停用",
|
||||
"Once the operation is successful, app will automatically sync data with the server.": "操作成功後, APP將自動與服務器同步數據",
|
||||
"Cache cleared": "緩存已清除",
|
||||
"Disabled": "已禁用",
|
||||
"WebDAV Auto Sync": "WebDAV 自動同步",
|
||||
"Once the operation is successful, app will automatically sync data with the server.": "操作成功後, APP將自動與伺服器同步資料",
|
||||
"Cache cleared": "快取已清除",
|
||||
"Disabled": "已停用",
|
||||
"Auto Sync Data": "自動同步資料",
|
||||
"Mark all as read": "全部標記為已讀",
|
||||
"Do you want to mark all as read?" : "您要全部標記為已讀嗎?",
|
||||
"Swipe down for previous chapter": "向下滑動查看上一章",
|
||||
"Swipe up for next chapter": "向上滑動查看下一章"
|
||||
"Swipe up for next chapter": "向上滑動查看下一章",
|
||||
"Initial Page": "初始頁面",
|
||||
"Home Page": "首頁",
|
||||
"Favorites Page": "收藏頁面",
|
||||
"Explore Page": "探索頁面",
|
||||
"Categories Page": "分類頁面",
|
||||
"Convert to local": "轉換為本地",
|
||||
"Refresh": "刷新",
|
||||
"Paging": "分頁",
|
||||
"Continuous": "連續",
|
||||
"Display mode of comic list": "漫畫列表的顯示模式",
|
||||
"A valid WebDav directory URL": "有效的WebDav目錄URL"
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
),
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -125,11 +125,11 @@ class OverlayWidgetState extends State<OverlayWidget> {
|
||||
void showDialogMessage(BuildContext context, String title, String message) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
builder: (context) => ContentDialog(
|
||||
title: title,
|
||||
content: Text(message).paddingHorizontal(16),
|
||||
actions: [
|
||||
TextButton(
|
||||
FilledButton(
|
||||
onPressed: context.pop,
|
||||
child: Text("OK".tl),
|
||||
)
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,13 @@
|
||||
part of 'components.dart';
|
||||
|
||||
class SideBarRoute<T> extends PopupRoute<T> {
|
||||
SideBarRoute(this.title, this.widget,
|
||||
SideBarRoute(this.widget,
|
||||
{this.showBarrier = true,
|
||||
this.useSurfaceTintColor = false,
|
||||
required this.width,
|
||||
this.addBottomPadding = true,
|
||||
this.addTopPadding = true});
|
||||
|
||||
final String? title;
|
||||
|
||||
final Widget widget;
|
||||
|
||||
final bool showBarrier;
|
||||
@@ -36,11 +34,7 @@ class SideBarRoute<T> extends PopupRoute<T> {
|
||||
Animation<double> secondaryAnimation) {
|
||||
bool showSideBar = MediaQuery.of(context).size.width > width;
|
||||
|
||||
Widget body = SidebarBody(
|
||||
title: title,
|
||||
widget: widget,
|
||||
autoChangeTitleBarColor: !useSurfaceTintColor,
|
||||
);
|
||||
Widget body = widget;
|
||||
|
||||
if (addTopPadding) {
|
||||
body = Padding(
|
||||
@@ -129,97 +123,13 @@ class SideBarRoute<T> extends PopupRoute<T> {
|
||||
}
|
||||
}
|
||||
|
||||
class SidebarBody extends StatefulWidget {
|
||||
const SidebarBody(
|
||||
{required this.title,
|
||||
required this.widget,
|
||||
required this.autoChangeTitleBarColor,
|
||||
super.key});
|
||||
|
||||
final String? title;
|
||||
final Widget widget;
|
||||
final bool autoChangeTitleBarColor;
|
||||
|
||||
@override
|
||||
State<SidebarBody> createState() => _SidebarBodyState();
|
||||
}
|
||||
|
||||
class _SidebarBodyState extends State<SidebarBody> {
|
||||
bool top = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body = Expanded(child: widget.widget);
|
||||
|
||||
if (widget.autoChangeTitleBarColor) {
|
||||
body = NotificationListener<ScrollNotification>(
|
||||
onNotification: (notifications) {
|
||||
if (notifications.metrics.pixels ==
|
||||
notifications.metrics.minScrollExtent &&
|
||||
!top) {
|
||||
setState(() {
|
||||
top = true;
|
||||
});
|
||||
} else if (notifications.metrics.pixels !=
|
||||
notifications.metrics.minScrollExtent &&
|
||||
top) {
|
||||
setState(() {
|
||||
top = false;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: body,
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (widget.title != null)
|
||||
Container(
|
||||
height: 60 + MediaQuery.of(context).padding.top,
|
||||
color: top
|
||||
? null
|
||||
: Theme.of(context).colorScheme.surfaceTint.withAlpha(20),
|
||||
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Tooltip(
|
||||
message: "Back".tl,
|
||||
child: IconButton(
|
||||
iconSize: 25,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
widget.title!,
|
||||
style: const TextStyle(fontSize: 22),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
body
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showSideBar(BuildContext context, Widget widget,
|
||||
{String? title,
|
||||
bool showBarrier = true,
|
||||
{bool showBarrier = true,
|
||||
bool useSurfaceTintColor = false,
|
||||
double width = 500,
|
||||
bool addTopPadding = false}) {
|
||||
return Navigator.of(context).push(
|
||||
SideBarRoute(
|
||||
title,
|
||||
widget,
|
||||
showBarrier: showBarrier,
|
||||
useSurfaceTintColor: useSurfaceTintColor,
|
||||
|
@@ -10,6 +10,34 @@ import 'package:window_manager/window_manager.dart';
|
||||
|
||||
const _kTitleBarHeight = 36.0;
|
||||
|
||||
class WindowFrameController extends InheritedWidget {
|
||||
/// Whether the window frame is hidden.
|
||||
final bool isWindowFrameHidden;
|
||||
|
||||
/// Sets the visibility of the window frame.
|
||||
final void Function(bool) setWindowFrame;
|
||||
|
||||
/// Adds a listener that will be called when close button is clicked.
|
||||
/// The listener should return `true` to allow the window to be closed.
|
||||
final void Function(WindowCloseListener listener) addCloseListener;
|
||||
|
||||
/// Removes a close listener.
|
||||
final void Function(WindowCloseListener listener) removeCloseListener;
|
||||
|
||||
const WindowFrameController._create({
|
||||
required this.isWindowFrameHidden,
|
||||
required this.setWindowFrame,
|
||||
required this.addCloseListener,
|
||||
required this.removeCloseListener,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class WindowFrame extends StatefulWidget {
|
||||
const WindowFrame(this.child, {super.key});
|
||||
|
||||
@@ -17,98 +45,146 @@ class WindowFrame extends StatefulWidget {
|
||||
|
||||
@override
|
||||
State<WindowFrame> createState() => _WindowFrameState();
|
||||
|
||||
static WindowFrameController of(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<WindowFrameController>()!;
|
||||
}
|
||||
}
|
||||
|
||||
typedef WindowCloseListener = bool Function();
|
||||
|
||||
class _WindowFrameState extends State<WindowFrame> {
|
||||
bool isHideWindowFrame = false;
|
||||
bool isWindowFrameHidden = false;
|
||||
bool useDarkTheme = false;
|
||||
var closeListeners = <WindowCloseListener>[];
|
||||
|
||||
/// Sets the visibility of the window frame.
|
||||
void setWindowFrame(bool show) {
|
||||
setState(() {
|
||||
isWindowFrameHidden = !show;
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds a listener that will be called when close button is clicked.
|
||||
/// The listener should return `true` to allow the window to be closed.
|
||||
void addCloseListener(WindowCloseListener listener) {
|
||||
closeListeners.add(listener);
|
||||
}
|
||||
|
||||
/// Removes a close listener.
|
||||
void removeCloseListener(WindowCloseListener listener) {
|
||||
closeListeners.remove(listener);
|
||||
}
|
||||
|
||||
void _onClose() {
|
||||
for (var listener in closeListeners) {
|
||||
if (!listener()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
windowManager.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (App.isMobile) return widget.child;
|
||||
if (isHideWindowFrame) return widget.child;
|
||||
|
||||
var body = Stack(
|
||||
Widget body = Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
padding: const EdgeInsets.only(top: _kTitleBarHeight)),
|
||||
padding: isWindowFrameHidden
|
||||
? null
|
||||
: const EdgeInsets.only(top: _kTitleBarHeight),
|
||||
),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
brightness: useDarkTheme ? Brightness.dark : null,
|
||||
),
|
||||
child: Builder(builder: (context) {
|
||||
return SizedBox(
|
||||
height: _kTitleBarHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
if (App.isMacOS)
|
||||
const DragToMoveArea(
|
||||
child: SizedBox(
|
||||
height: double.infinity,
|
||||
width: 16,
|
||||
),
|
||||
).paddingRight(52)
|
||||
else
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: DragToMoveArea(
|
||||
child: Text(
|
||||
'Venera',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: (useDarkTheme ||
|
||||
context.brightness == Brightness.dark)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
if (!isWindowFrameHidden)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
brightness: useDarkTheme ? Brightness.dark : null,
|
||||
),
|
||||
child: Builder(builder: (context) {
|
||||
return SizedBox(
|
||||
height: _kTitleBarHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
if (App.isMacOS)
|
||||
const DragToMoveArea(
|
||||
child: SizedBox(
|
||||
height: double.infinity,
|
||||
width: 16,
|
||||
),
|
||||
).paddingRight(52)
|
||||
else
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: DragToMoveArea(
|
||||
child: Text(
|
||||
'Venera',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: (useDarkTheme ||
|
||||
context.brightness == Brightness.dark)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
)
|
||||
.toAlign(Alignment.centerLeft)
|
||||
.paddingLeft(4 + (App.isMacOS ? 25 : 0)),
|
||||
),
|
||||
),
|
||||
if (kDebugMode)
|
||||
const TextButton(
|
||||
onPressed: debug,
|
||||
child: Text('Debug'),
|
||||
),
|
||||
if (!App.isMacOS)
|
||||
_WindowButtons(
|
||||
onClose: _onClose,
|
||||
)
|
||||
.toAlign(Alignment.centerLeft)
|
||||
.paddingLeft(4 + (App.isMacOS ? 25 : 0)),
|
||||
),
|
||||
),
|
||||
if (kDebugMode)
|
||||
const TextButton(
|
||||
onPressed: debug,
|
||||
child: Text('Debug'),
|
||||
),
|
||||
if (!App.isMacOS) const WindowButtons()
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
if (App.isLinux) {
|
||||
return VirtualWindowFrame(child: body);
|
||||
} else {
|
||||
return body;
|
||||
body = VirtualWindowFrame(child: body);
|
||||
}
|
||||
|
||||
return WindowFrameController._create(
|
||||
isWindowFrameHidden: isWindowFrameHidden,
|
||||
setWindowFrame: setWindowFrame,
|
||||
addCloseListener: addCloseListener,
|
||||
removeCloseListener: removeCloseListener,
|
||||
child: body,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WindowButtons extends StatefulWidget {
|
||||
const WindowButtons({super.key});
|
||||
class _WindowButtons extends StatefulWidget {
|
||||
const _WindowButtons({required this.onClose});
|
||||
|
||||
final void Function() onClose;
|
||||
|
||||
@override
|
||||
State<WindowButtons> createState() => _WindowButtonsState();
|
||||
State<_WindowButtons> createState() => _WindowButtonsState();
|
||||
}
|
||||
|
||||
class _WindowButtonsState extends State<WindowButtons> with WindowListener {
|
||||
class _WindowButtonsState extends State<_WindowButtons> with WindowListener {
|
||||
bool isMaximized = false;
|
||||
|
||||
@override
|
||||
@@ -197,9 +273,7 @@ class _WindowButtonsState extends State<WindowButtons> with WindowListener {
|
||||
color: !dark ? Colors.white : Colors.black,
|
||||
),
|
||||
hoverColor: Colors.red,
|
||||
onPressed: () {
|
||||
windowManager.close();
|
||||
},
|
||||
onPressed: widget.onClose,
|
||||
)
|
||||
],
|
||||
),
|
||||
@@ -486,31 +560,31 @@ class _VirtualWindowFrameState extends State<VirtualWindowFrame>
|
||||
}
|
||||
|
||||
Widget _buildVirtualWindowFrame(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: (_isMaximized || _isFullScreen) ? 0 : 1,
|
||||
),
|
||||
boxShadow: <BoxShadow>[
|
||||
if (!_isMaximized && !_isFullScreen)
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_isMaximized ? 0 : 8),
|
||||
color: Colors.transparent,
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.toOpacity(0.1),
|
||||
offset: Offset(0.0, _isFocused ? 4 : 2),
|
||||
blurRadius: 6,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
color: Colors.black.toOpacity(_isFocused ? 0.4 : 0.2),
|
||||
offset: Offset(0.0, 2),
|
||||
blurRadius: 4,
|
||||
)
|
||||
],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DragToResizeArea(
|
||||
enableResizeEdges: (_isMaximized || _isFullScreen) ? [] : null,
|
||||
child: _buildVirtualWindowFrame(context),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(_isMaximized ? 0 : 4),
|
||||
child: _buildVirtualWindowFrame(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -567,5 +641,5 @@ TransitionBuilder VirtualWindowFrameInit() {
|
||||
}
|
||||
|
||||
void debug() {
|
||||
ComicSource.reload();
|
||||
ComicSourceManager().reload();
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
||||
export "context.dart";
|
||||
|
||||
class _App {
|
||||
final version = "1.3.1";
|
||||
final version = "1.3.3";
|
||||
|
||||
bool get isAndroid => Platform.isAndroid;
|
||||
|
||||
@@ -77,10 +77,15 @@ class _App {
|
||||
Future<void> init() async {
|
||||
cachePath = (await getApplicationCacheDirectory()).path;
|
||||
dataPath = (await getApplicationSupportDirectory()).path;
|
||||
await data.init();
|
||||
await history.init();
|
||||
await favorites.init();
|
||||
await local.init();
|
||||
}
|
||||
|
||||
Future<void> initComponents() async {
|
||||
await Future.wait([
|
||||
data.init(),
|
||||
history.init(),
|
||||
favorites.init(),
|
||||
local.init(),
|
||||
]);
|
||||
}
|
||||
|
||||
Function? _forceRebuildHandler;
|
||||
|
@@ -4,10 +4,13 @@ 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 {
|
||||
final Settings settings = Settings();
|
||||
class Appdata with Init {
|
||||
Appdata._create();
|
||||
|
||||
final Settings settings = Settings._create();
|
||||
|
||||
var searchHistory = <String>[];
|
||||
|
||||
@@ -51,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,
|
||||
@@ -108,12 +89,35 @@ 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();
|
||||
final appdata = Appdata._create();
|
||||
|
||||
class Settings with ChangeNotifier {
|
||||
Settings();
|
||||
Settings._create();
|
||||
|
||||
final _data = <String, dynamic>{
|
||||
'comicDisplayMode': 'detailed', // detailed, brief
|
||||
@@ -158,9 +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) {
|
||||
|
@@ -145,7 +145,7 @@ class RandomCategoryPartWithRuntimeData extends BaseCategoryPart {
|
||||
}
|
||||
|
||||
CategoryData getCategoryDataWithKey(String key) {
|
||||
for (var source in ComicSource._sources) {
|
||||
for (var source in ComicSource.all()) {
|
||||
if (source.categoryData?.key == key) {
|
||||
return source.categoryData!;
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ import 'package:venera/foundation/history.dart';
|
||||
import 'package:venera/foundation/res.dart';
|
||||
import 'package:venera/utils/data_sync.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
import 'package:venera/utils/init.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
|
||||
@@ -27,81 +28,29 @@ part 'parser.dart';
|
||||
|
||||
part 'models.dart';
|
||||
|
||||
/// build comic list, [Res.subData] should be maxPage or null if there is no limit.
|
||||
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
|
||||
part 'types.dart';
|
||||
|
||||
/// build comic list with next param, [Res.subData] should be next page param or null if there is no next page.
|
||||
typedef ComicListBuilderWithNext = Future<Res<List<Comic>>> Function(
|
||||
String? next);
|
||||
class ComicSourceManager with ChangeNotifier, Init {
|
||||
final List<ComicSource> _sources = [];
|
||||
|
||||
typedef LoginFunction = Future<Res<bool>> Function(String, String);
|
||||
static ComicSourceManager? _instance;
|
||||
|
||||
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
|
||||
ComicSourceManager._create();
|
||||
|
||||
typedef LoadComicPagesFunc = Future<Res<List<String>>> Function(
|
||||
String id, String? ep);
|
||||
factory ComicSourceManager() => _instance ??= ComicSourceManager._create();
|
||||
|
||||
typedef CommentsLoader = Future<Res<List<Comment>>> Function(
|
||||
String id, String? subId, int page, String? replyTo);
|
||||
List<ComicSource> all() => List.from(_sources);
|
||||
|
||||
typedef SendCommentFunc = Future<Res<bool>> Function(
|
||||
String id, String? subId, String content, String? replyTo);
|
||||
|
||||
typedef GetImageLoadingConfigFunc = Future<Map<String, dynamic>> Function(
|
||||
String imageKey, String comicId, String epId)?;
|
||||
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
|
||||
String imageKey)?;
|
||||
|
||||
typedef ComicThumbnailLoader = Future<Res<List<String>>> Function(
|
||||
String comicId, String? next);
|
||||
|
||||
typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(
|
||||
String comicId, bool isLiking);
|
||||
|
||||
/// [isLiking] is true if the user is liking the comment, false if unliking.
|
||||
/// return the new likes count or null.
|
||||
typedef LikeCommentFunc = Future<Res<int?>> Function(
|
||||
String comicId, String? subId, String commentId, bool isLiking);
|
||||
|
||||
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
|
||||
/// return the new vote count or null.
|
||||
typedef VoteCommentFunc = Future<Res<int?>> Function(
|
||||
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
|
||||
|
||||
typedef HandleClickTagEvent = Map<String, String> Function(
|
||||
String namespace, String tag);
|
||||
|
||||
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
|
||||
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);
|
||||
|
||||
class ComicSource {
|
||||
static final List<ComicSource> _sources = [];
|
||||
|
||||
static final List<Function> _listeners = [];
|
||||
|
||||
static void addListener(Function listener) {
|
||||
_listeners.add(listener);
|
||||
}
|
||||
|
||||
static void removeListener(Function listener) {
|
||||
_listeners.remove(listener);
|
||||
}
|
||||
|
||||
static void notifyListeners() {
|
||||
for (var listener in _listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
static List<ComicSource> all() => List.from(_sources);
|
||||
|
||||
static ComicSource? find(String key) =>
|
||||
ComicSource? find(String key) =>
|
||||
_sources.firstWhereOrNull((element) => element.key == key);
|
||||
|
||||
static ComicSource? fromIntKey(int key) =>
|
||||
ComicSource? fromIntKey(int key) =>
|
||||
_sources.firstWhereOrNull((element) => element.key.hashCode == key);
|
||||
|
||||
static Future<void> init() async {
|
||||
@override
|
||||
@protected
|
||||
Future<void> doInit() async {
|
||||
await JsEngine().ensureInit();
|
||||
final path = "${App.dataPath}/comic_source";
|
||||
if (!(await Directory(path).exists())) {
|
||||
Directory(path).create();
|
||||
@@ -120,26 +69,49 @@ class ComicSource {
|
||||
}
|
||||
}
|
||||
|
||||
static Future reload() async {
|
||||
Future reload() async {
|
||||
_sources.clear();
|
||||
JsEngine().runCode("ComicSource.sources = {};");
|
||||
await init();
|
||||
await doInit();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
static void add(ComicSource source) {
|
||||
void add(ComicSource source) {
|
||||
_sources.add(source);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
static void remove(String key) {
|
||||
void remove(String key) {
|
||||
_sources.removeWhere((element) => element.key == key);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
static final availableUpdates = <String, String>{};
|
||||
bool get isEmpty => _sources.isEmpty;
|
||||
|
||||
static bool get isEmpty => _sources.isEmpty;
|
||||
/// Key is the source key, value is the version.
|
||||
final _availableUpdates = <String, String>{};
|
||||
|
||||
void updateAvailableUpdates(Map<String, String> updates) {
|
||||
_availableUpdates.addAll(updates);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Map<String, String> get availableUpdates => Map.from(_availableUpdates);
|
||||
|
||||
void notifyStateChange() {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class ComicSource {
|
||||
static List<ComicSource> all() => ComicSourceManager().all();
|
||||
|
||||
static ComicSource? find(String key) => ComicSourceManager().find(key);
|
||||
|
||||
static ComicSource? fromIntKey(int key) =>
|
||||
ComicSourceManager().fromIntKey(key);
|
||||
|
||||
static bool get isEmpty => ComicSourceManager().isEmpty;
|
||||
|
||||
/// Name of this source.
|
||||
final String name;
|
||||
@@ -321,7 +293,7 @@ class AccountConfig {
|
||||
this.onLoginWithWebviewSuccess,
|
||||
this.cookieFields,
|
||||
this.validateCookies,
|
||||
) : infoItems = const [];
|
||||
) : infoItems = const [];
|
||||
}
|
||||
|
||||
class AccountInfoItem {
|
||||
@@ -478,4 +450,4 @@ class ArchiveDownloader {
|
||||
final Future<Res<String>> Function(String cid, String aid) getDownloadUrl;
|
||||
|
||||
const ArchiveDownloader(this.getArchives, this.getDownloadUrl);
|
||||
}
|
||||
}
|
||||
|
@@ -111,6 +111,9 @@ class Comic {
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ sourceKey.hashCode;
|
||||
|
||||
@override
|
||||
toString() => "$sourceKey@$id";
|
||||
}
|
||||
|
||||
class ComicDetails with HistoryMixin {
|
||||
@@ -336,8 +339,10 @@ class ComicChapters {
|
||||
}
|
||||
if (chapters.isNotEmpty) {
|
||||
return ComicChapters(chapters);
|
||||
} else {
|
||||
} else if (groupedChapters.isNotEmpty) {
|
||||
return ComicChapters.grouped(groupedChapters);
|
||||
} else {
|
||||
throw ArgumentError("Empty chapter list");
|
||||
}
|
||||
}
|
||||
|
||||
|
48
lib/foundation/comic_source/types.dart
Normal file
48
lib/foundation/comic_source/types.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
part of 'comic_source.dart';
|
||||
|
||||
/// build comic list, [Res.subData] should be maxPage or null if there is no limit.
|
||||
typedef ComicListBuilder = Future<Res<List<Comic>>> Function(int page);
|
||||
|
||||
/// build comic list with next param, [Res.subData] should be next page param or null if there is no next page.
|
||||
typedef ComicListBuilderWithNext = Future<Res<List<Comic>>> Function(
|
||||
String? next);
|
||||
|
||||
typedef LoginFunction = Future<Res<bool>> Function(String, String);
|
||||
|
||||
typedef LoadComicFunc = Future<Res<ComicDetails>> Function(String id);
|
||||
|
||||
typedef LoadComicPagesFunc = Future<Res<List<String>>> Function(
|
||||
String id, String? ep);
|
||||
|
||||
typedef CommentsLoader = Future<Res<List<Comment>>> Function(
|
||||
String id, String? subId, int page, String? replyTo);
|
||||
|
||||
typedef SendCommentFunc = Future<Res<bool>> Function(
|
||||
String id, String? subId, String content, String? replyTo);
|
||||
|
||||
typedef GetImageLoadingConfigFunc = Future<Map<String, dynamic>> Function(
|
||||
String imageKey, String comicId, String epId)?;
|
||||
typedef GetThumbnailLoadingConfigFunc = Map<String, dynamic> Function(
|
||||
String imageKey)?;
|
||||
|
||||
typedef ComicThumbnailLoader = Future<Res<List<String>>> Function(
|
||||
String comicId, String? next);
|
||||
|
||||
typedef LikeOrUnlikeComicFunc = Future<Res<bool>> Function(
|
||||
String comicId, bool isLiking);
|
||||
|
||||
/// [isLiking] is true if the user is liking the comment, false if unliking.
|
||||
/// return the new likes count or null.
|
||||
typedef LikeCommentFunc = Future<Res<int?>> Function(
|
||||
String comicId, String? subId, String commentId, bool isLiking);
|
||||
|
||||
/// [isUp] is true if the user is upvoting the comment, false if downvoting.
|
||||
/// return the new vote count or null.
|
||||
typedef VoteCommentFunc = Future<Res<int?>> Function(
|
||||
String comicId, String? subId, String commentId, bool isUp, bool isCancel);
|
||||
|
||||
typedef HandleClickTagEvent = Map<String, String> Function(
|
||||
String namespace, String tag);
|
||||
|
||||
/// [rating] is the rating value, 0-10. 1 represents 0.5 star.
|
||||
typedef StarRatingFunc = Future<Res<bool>> Function(String comicId, int rating);
|
@@ -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;
|
||||
""");
|
||||
}
|
||||
_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"
|
||||
|
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter/foundation.dart' show protected;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:html/parser.dart' as html;
|
||||
import 'package:html/dom.dart' as dom;
|
||||
@@ -24,6 +25,7 @@ import 'package:venera/components/js_ui.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/network/app_dio.dart';
|
||||
import 'package:venera/network/cookie_jar.dart';
|
||||
import 'package:venera/utils/init.dart';
|
||||
|
||||
import 'comic_source/comic_source.dart';
|
||||
import 'consts.dart';
|
||||
@@ -40,7 +42,7 @@ class JavaScriptRuntimeException implements Exception {
|
||||
}
|
||||
}
|
||||
|
||||
class JsEngine with _JSEngineApi, JsUiApi {
|
||||
class JsEngine with _JSEngineApi, JsUiApi, Init {
|
||||
factory JsEngine() => _cache ?? (_cache = JsEngine._create());
|
||||
|
||||
static JsEngine? _cache;
|
||||
@@ -64,7 +66,9 @@ class JsEngine with _JSEngineApi, JsUiApi {
|
||||
responseType: ResponseType.plain, validateStatus: (status) => true));
|
||||
}
|
||||
|
||||
Future<void> init() async {
|
||||
@override
|
||||
@protected
|
||||
Future<void> doInit() async {
|
||||
if (!_closed) {
|
||||
return;
|
||||
}
|
||||
|
@@ -265,6 +265,7 @@ class LocalManager with ChangeNotifier {
|
||||
}
|
||||
_checkPathValidation();
|
||||
_checkNoMedia();
|
||||
await ComicSourceManager().ensureInit();
|
||||
restoreDownloadingTasks();
|
||||
}
|
||||
|
||||
@@ -421,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 = [];
|
||||
|
@@ -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';
|
||||
@@ -30,19 +31,22 @@ extension _FutureInit<T> on Future<T> {
|
||||
|
||||
Future<void> init() async {
|
||||
await App.init().wait();
|
||||
SingleInstanceCookieJar("${App.dataPath}/cookie.db");
|
||||
await SingleInstanceCookieJar.createInstance();
|
||||
var futures = [
|
||||
Rhttp.init(),
|
||||
App.initComponents(),
|
||||
SAFTaskWorker().init().wait(),
|
||||
AppTranslation.init().wait(),
|
||||
TagsTranslation.readData().wait(),
|
||||
JsEngine().init().then((_) => ComicSource.init()).wait(),
|
||||
JsEngine().init().wait(),
|
||||
ComicSourceManager().init().wait(),
|
||||
];
|
||||
await Future.wait(futures);
|
||||
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
||||
_checkOldConfigs();
|
||||
if (App.isAndroid) {
|
||||
handleLinks();
|
||||
handleTextShare();
|
||||
}
|
||||
FlutterError.onError = (details) {
|
||||
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||
|
@@ -34,13 +34,10 @@ void main(List<String> args) {
|
||||
await windowManager.setBackgroundColor(Colors.transparent);
|
||||
}
|
||||
await windowManager.setMinimumSize(const Size(500, 600));
|
||||
if (!App.isLinux) {
|
||||
// https://github.com/leanflutter/window_manager/issues/460
|
||||
var placement = await WindowPlacement.loadFromFile();
|
||||
await placement.applyToWindow();
|
||||
await windowManager.show();
|
||||
WindowPlacement.loop();
|
||||
}
|
||||
var placement = await WindowPlacement.loadFromFile();
|
||||
await placement.applyToWindow();
|
||||
await windowManager.show();
|
||||
WindowPlacement.loop();
|
||||
});
|
||||
}
|
||||
}, (error, stack) {
|
||||
@@ -141,13 +138,15 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
) {
|
||||
String? font;
|
||||
List<String>? fallback;
|
||||
if (App.isWindows) {
|
||||
font = 'Segoe UI';
|
||||
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'
|
||||
];
|
||||
@@ -199,6 +198,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
'dark' => ThemeMode.dark,
|
||||
_ => ThemeMode.system
|
||||
},
|
||||
color: Colors.transparent,
|
||||
localizationsDelegates: [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
@@ -246,6 +246,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
);
|
||||
}
|
||||
return _SystemUiProvider(Material(
|
||||
color: App.isLinux ? Colors.transparent : null,
|
||||
child: widget,
|
||||
));
|
||||
}
|
||||
|
@@ -257,18 +257,7 @@ class RHttpAdapter implements HttpClientAdapter {
|
||||
Future<void>? cancelFuture,
|
||||
) async {
|
||||
var res = await rhttp.Rhttp.request(
|
||||
method: switch (options.method) {
|
||||
'GET' => rhttp.HttpMethod.get,
|
||||
'POST' => rhttp.HttpMethod.post,
|
||||
'PUT' => rhttp.HttpMethod.put,
|
||||
'PATCH' => rhttp.HttpMethod.patch,
|
||||
'DELETE' => rhttp.HttpMethod.delete,
|
||||
'HEAD' => rhttp.HttpMethod.head,
|
||||
'OPTIONS' => rhttp.HttpMethod.options,
|
||||
'TRACE' => rhttp.HttpMethod.trace,
|
||||
'CONNECT' => rhttp.HttpMethod.connect,
|
||||
_ => throw ArgumentError('Unsupported method: ${options.method}'),
|
||||
},
|
||||
method: rhttp.HttpMethod(options.method),
|
||||
url: options.uri.toString(),
|
||||
settings: settings,
|
||||
expectBody: rhttp.HttpExpectBody.stream,
|
||||
@@ -293,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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
import 'package:venera/foundation/log.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
@@ -200,6 +201,11 @@ class SingleInstanceCookieJar extends CookieJarSql {
|
||||
SingleInstanceCookieJar._create(super.path);
|
||||
|
||||
static SingleInstanceCookieJar? instance;
|
||||
|
||||
static Future<void> createInstance() async {
|
||||
var dataPath = (await getApplicationSupportDirectory()).path;
|
||||
instance = SingleInstanceCookieJar("$dataPath/cookie.db");
|
||||
}
|
||||
}
|
||||
|
||||
class CookieManagerSql extends Interceptor {
|
||||
|
@@ -2,6 +2,8 @@ import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:flutter/widgets.dart' show ChangeNotifier;
|
||||
import 'package:flutter_saf/flutter_saf.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||
import 'package:venera/foundation/comic_type.dart';
|
||||
@@ -739,11 +741,12 @@ class ArchiveDownloadTask extends DownloadTask {
|
||||
path = dir.path;
|
||||
}
|
||||
|
||||
var resultFile = File(FilePath.join(path!, "archive.zip"));
|
||||
var archiveFile =
|
||||
File(FilePath.join(App.dataPath, "archive_downloading.zip"));
|
||||
|
||||
Log.info("Download", "Downloading $archiveUrl");
|
||||
|
||||
_downloader = FileDownloader(archiveUrl, resultFile.path);
|
||||
_downloader = FileDownloader(archiveUrl, archiveFile.path);
|
||||
|
||||
bool isDownloaded = false;
|
||||
|
||||
@@ -772,22 +775,33 @@ class ArchiveDownloadTask extends DownloadTask {
|
||||
}
|
||||
|
||||
try {
|
||||
await extractArchive(path!);
|
||||
await _extractArchive(archiveFile.path, path!);
|
||||
} catch (e) {
|
||||
_setError("Failed to extract archive: $e");
|
||||
return;
|
||||
}
|
||||
|
||||
await resultFile.deleteIgnoreError();
|
||||
await archiveFile.deleteIgnoreError();
|
||||
|
||||
LocalManager().completeTask(this);
|
||||
}
|
||||
|
||||
static Future<void> extractArchive(String path) async {
|
||||
var resultFile = FilePath.join(path, "archive.zip");
|
||||
await Isolate.run(() {
|
||||
ZipFile.openAndExtract(resultFile, path);
|
||||
});
|
||||
static Future<void> _extractArchive(String archive, String outDir) async {
|
||||
var out = Directory(outDir);
|
||||
if (out is AndroidDirectory) {
|
||||
// Saf directory can't be accessed by native code.
|
||||
var cacheDir = FilePath.join(App.cachePath, "archive_downloading");
|
||||
Directory(cacheDir).forceCreateSync();
|
||||
await Isolate.run(() {
|
||||
ZipFile.openAndExtract(archive, cacheDir);
|
||||
});
|
||||
await copyDirectoryIsolate(Directory(cacheDir), Directory(outDir));
|
||||
await Directory(cacheDir).deleteIgnoreError(recursive: true);
|
||||
} else {
|
||||
await Isolate.run(() {
|
||||
ZipFile.openAndExtract(archive, outDir);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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,
|
||||
);
|
||||
|
@@ -651,10 +651,16 @@ class _CommentImage {
|
||||
}
|
||||
|
||||
class RichCommentContent extends StatefulWidget {
|
||||
const RichCommentContent({super.key, required this.text});
|
||||
const RichCommentContent({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.showImages = true,
|
||||
});
|
||||
|
||||
final String text;
|
||||
|
||||
final bool showImages;
|
||||
|
||||
@override
|
||||
State<RichCommentContent> createState() => _RichCommentContentState();
|
||||
}
|
||||
@@ -808,7 +814,7 @@ class _RichCommentContentState extends State<RichCommentContent> {
|
||||
children: textSpan,
|
||||
),
|
||||
);
|
||||
if (images.isNotEmpty) {
|
||||
if (images.isNotEmpty && widget.showImages) {
|
||||
content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
@@ -138,7 +138,10 @@ class _CommentWidget extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Expanded(
|
||||
child: RichCommentContent(text: comment.content).fixWidth(324),
|
||||
child: RichCommentContent(
|
||||
text: comment.content,
|
||||
showImages: false,
|
||||
).fixWidth(324),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (comment.time != null)
|
||||
@@ -147,4 +150,4 @@ class _CommentWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -40,10 +40,11 @@ class ComicSourcePage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
if (shouldUpdate.isNotEmpty) {
|
||||
var updates = <String, String>{};
|
||||
for (var key in shouldUpdate) {
|
||||
ComicSource.availableUpdates[key] = versions[key]!;
|
||||
updates[key] = versions[key]!;
|
||||
}
|
||||
ComicSource.notifyListeners();
|
||||
ComicSourceManager().updateAvailableUpdates(updates);
|
||||
}
|
||||
return shouldUpdate.length;
|
||||
}
|
||||
@@ -73,13 +74,13 @@ class _BodyState extends State<_Body> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ComicSource.addListener(updateUI);
|
||||
ComicSourceManager().addListener(updateUI);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
ComicSource.removeListener(updateUI);
|
||||
ComicSourceManager().removeListener(updateUI);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -115,7 +116,7 @@ class _BodyState extends State<_Body> {
|
||||
onConfirm: () {
|
||||
var file = File(source.filePath);
|
||||
file.delete();
|
||||
ComicSource.remove(source.key);
|
||||
ComicSourceManager().remove(source.key);
|
||||
_validatePages();
|
||||
App.forceRebuild();
|
||||
},
|
||||
@@ -136,7 +137,7 @@ class _BodyState extends State<_Body> {
|
||||
child: const Text("cancel")),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await ComicSource.reload();
|
||||
await ComicSourceManager().reload();
|
||||
App.forceRebuild();
|
||||
},
|
||||
child: const Text("continue")),
|
||||
@@ -150,7 +151,7 @@ class _BodyState extends State<_Body> {
|
||||
}
|
||||
context.to(
|
||||
() => _EditFilePage(source.filePath, () async {
|
||||
await ComicSource.reload();
|
||||
await ComicSourceManager().reload();
|
||||
setState(() {});
|
||||
}),
|
||||
);
|
||||
@@ -162,7 +163,7 @@ class _BodyState extends State<_Body> {
|
||||
App.rootContext.showMessage(message: "Invalid url config");
|
||||
return;
|
||||
}
|
||||
ComicSource.remove(source.key);
|
||||
ComicSourceManager().remove(source.key);
|
||||
bool cancel = false;
|
||||
LoadingDialogController? controller;
|
||||
if (showLoading) {
|
||||
@@ -179,14 +180,14 @@ class _BodyState extends State<_Body> {
|
||||
controller?.close();
|
||||
await ComicSourceParser().parse(res.data!, source.filePath);
|
||||
await File(source.filePath).writeAsString(res.data!);
|
||||
if (ComicSource.availableUpdates.containsKey(source.key)) {
|
||||
ComicSource.availableUpdates.remove(source.key);
|
||||
if (ComicSourceManager().availableUpdates.containsKey(source.key)) {
|
||||
ComicSourceManager().availableUpdates.remove(source.key);
|
||||
}
|
||||
} catch (e) {
|
||||
if (cancel) return;
|
||||
App.rootContext.showMessage(message: e.toString());
|
||||
}
|
||||
await ComicSource.reload();
|
||||
await ComicSourceManager().reload();
|
||||
App.forceRebuild();
|
||||
}
|
||||
|
||||
@@ -304,7 +305,7 @@ class _BodyState extends State<_Body> {
|
||||
|
||||
Future<void> addSource(String js, String fileName) async {
|
||||
var comicSource = await ComicSourceParser().createAndParse(js, fileName);
|
||||
ComicSource.add(comicSource);
|
||||
ComicSourceManager().add(comicSource);
|
||||
_addAllPagesWithComicSource(comicSource);
|
||||
appdata.saveData();
|
||||
App.forceRebuild();
|
||||
@@ -563,7 +564,7 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
|
||||
}
|
||||
|
||||
void showUpdateDialog() async {
|
||||
var text = ComicSource.availableUpdates.entries.map((e) {
|
||||
var text = ComicSourceManager().availableUpdates.entries.map((e) {
|
||||
return "${ComicSource.find(e.key)!.name}: ${e.value}";
|
||||
}).join("\n");
|
||||
bool doUpdate = false;
|
||||
@@ -592,9 +593,9 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
|
||||
withProgress: true,
|
||||
);
|
||||
int current = 0;
|
||||
int total = ComicSource.availableUpdates.length;
|
||||
int total = ComicSourceManager().availableUpdates.length;
|
||||
try {
|
||||
var shouldUpdate = ComicSource.availableUpdates.keys.toList();
|
||||
var shouldUpdate = ComicSourceManager().availableUpdates.keys.toList();
|
||||
for (var key in shouldUpdate) {
|
||||
var source = ComicSource.find(key)!;
|
||||
await _BodyState.update(source, false);
|
||||
@@ -692,7 +693,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var newVersion = ComicSource.availableUpdates[source.key];
|
||||
var newVersion = ComicSourceManager().availableUpdates[source.key];
|
||||
bool hasUpdate =
|
||||
newVersion != null && compareSemVer(newVersion, source.version);
|
||||
|
||||
@@ -960,7 +961,7 @@ class _SliverComicSourceState extends State<_SliverComicSource> {
|
||||
source.data["account"] = null;
|
||||
source.account?.logout();
|
||||
source.saveData();
|
||||
ComicSource.notifyListeners();
|
||||
ComicSourceManager().notifyStateChange();
|
||||
setState(() {});
|
||||
},
|
||||
trailing: const Icon(Icons.logout),
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -703,8 +703,9 @@ abstract class FollowUpdatesService {
|
||||
if (_isInitialized) return;
|
||||
_isInitialized = true;
|
||||
_check();
|
||||
DataSync().addListener(updateFollowUpdatesUI);
|
||||
// A short interval will not affect the performance since every comic has a check time.
|
||||
Timer.periodic(const Duration(minutes: 5), (timer) {
|
||||
Timer.periodic(const Duration(minutes: 10), (timer) {
|
||||
_check();
|
||||
});
|
||||
}
|
||||
|
@@ -162,16 +162,50 @@ class _SyncDataWidgetState extends State<_SyncDataWidget>
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (DataSync().lastError != null)
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () {
|
||||
showDialogMessage(
|
||||
App.rootContext,
|
||||
"Error".tl,
|
||||
DataSync().lastError!,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: Colors.red,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text('Error'.tl, style: ts.s12),
|
||||
],
|
||||
),
|
||||
),
|
||||
).paddingRight(4),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.cloud_upload_outlined),
|
||||
onPressed: () async {
|
||||
DataSync().uploadData();
|
||||
}),
|
||||
icon: const Icon(Icons.cloud_upload_outlined),
|
||||
onPressed: () async {
|
||||
DataSync().uploadData();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.cloud_download_outlined),
|
||||
onPressed: () async {
|
||||
DataSync().downloadData();
|
||||
}),
|
||||
icon: const Icon(Icons.cloud_download_outlined),
|
||||
onPressed: () async {
|
||||
DataSync().downloadData();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -263,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,
|
||||
@@ -366,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),
|
||||
@@ -538,7 +573,8 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
launchUrlString("https://github.com/venera-app/venera/blob/master/doc/import_comic.md");
|
||||
launchUrlString(
|
||||
"https://github.com/venera-app/venera/blob/master/doc/import_comic.md");
|
||||
},
|
||||
).fixWidth(90).paddingRight(8),
|
||||
Button.filled(
|
||||
@@ -595,19 +631,19 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
comicSources = ComicSource.all().map((e) => e.name).toList();
|
||||
ComicSource.addListener(onComicSourceChange);
|
||||
ComicSourceManager().addListener(onComicSourceChange);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ComicSource.removeListener(onComicSourceChange);
|
||||
ComicSourceManager().removeListener(onComicSourceChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
int get _availableUpdates {
|
||||
int c = 0;
|
||||
ComicSource.availableUpdates.forEach((key, version) {
|
||||
ComicSourceManager().availableUpdates.forEach((key, version) {
|
||||
var source = ComicSource.find(key);
|
||||
if (source != null) {
|
||||
if (compareSemVer(version, source.version)) {
|
||||
@@ -697,14 +733,24 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.update, color: context.colorScheme.primary, size: 20,),
|
||||
Icon(
|
||||
Icons.update,
|
||||
color: context.colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text("@c updates".tlParams({
|
||||
'c': _availableUpdates,
|
||||
}), style: ts.withColor(context.colorScheme.primary),),
|
||||
Text(
|
||||
"@c updates".tlParams({
|
||||
'c': _availableUpdates,
|
||||
}),
|
||||
style: ts.withColor(context.colorScheme.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
).toAlign(Alignment.centerLeft).paddingHorizontal(16).paddingBottom(8),
|
||||
)
|
||||
.toAlign(Alignment.centerLeft)
|
||||
.paddingHorizontal(16)
|
||||
.paddingBottom(8),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -844,7 +890,8 @@ class _ImageFavoritesState extends State<ImageFavorites> {
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
|
@@ -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) {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
import 'package:venera/pages/categories_page.dart';
|
||||
import 'package:venera/pages/search_page.dart';
|
||||
import 'package:venera/pages/settings/settings_page.dart';
|
||||
@@ -39,6 +40,7 @@ class _MainPageState extends State<MainPage> {
|
||||
_observer = NaviObserver();
|
||||
_navigatorKey = GlobalKey();
|
||||
App.mainNavigatorKey = _navigatorKey;
|
||||
index = int.tryParse(appdata.settings['initialPage'].toString()) ?? 0;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -60,6 +62,7 @@ class _MainPageState extends State<MainPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NaviPane(
|
||||
initialPage: index,
|
||||
observer: _observer,
|
||||
navigatorKey: _navigatorKey!,
|
||||
paneItems: [
|
||||
|
@@ -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);
|
||||
|
@@ -15,6 +15,7 @@ import 'package:photo_view/photo_view_gallery.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:venera/components/components.dart';
|
||||
import 'package:venera/components/custom_slider.dart';
|
||||
import 'package:venera/components/window_frame.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
import 'package:venera/foundation/cache_manager.dart';
|
||||
@@ -114,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;
|
||||
|
||||
@@ -169,6 +170,7 @@ class _ReaderState extends State<Reader>
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
initImagesPerPage(widget.initialPage ?? 1);
|
||||
initReaderWindow();
|
||||
}
|
||||
|
||||
void setImageCacheSize() async {
|
||||
@@ -191,6 +193,9 @@ class _ReaderState extends State<Reader>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (isFullscreen) {
|
||||
fullscreen();
|
||||
}
|
||||
autoPageTurningTimer?.cancel();
|
||||
focusNode.dispose();
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
@@ -199,6 +204,7 @@ class _ReaderState extends State<Reader>
|
||||
DataSync().onDataChanged();
|
||||
});
|
||||
PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20;
|
||||
disposeReaderWindow();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -218,6 +224,9 @@ class _ReaderState extends State<Reader>
|
||||
}
|
||||
|
||||
void onKeyEvent(KeyEvent event) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.f12 && event is KeyUpEvent) {
|
||||
fullscreen();
|
||||
}
|
||||
_imageViewController?.handleKeyEvent(event);
|
||||
}
|
||||
|
||||
@@ -429,11 +438,8 @@ abstract mixin class _ReaderLocation {
|
||||
|
||||
bool toPage(int page) {
|
||||
if (_validatePage(page)) {
|
||||
if (page == this.page) {
|
||||
if (!(chapter == 1 && page == 1) &&
|
||||
!(chapter == maxChapter && page == maxPage)) {
|
||||
return false;
|
||||
}
|
||||
if (page == this.page && page != 1 && page != maxPage) {
|
||||
return false;
|
||||
}
|
||||
this.page = page;
|
||||
update();
|
||||
@@ -495,9 +501,38 @@ abstract mixin class _ReaderLocation {
|
||||
mixin class _ReaderWindow {
|
||||
bool isFullscreen = false;
|
||||
|
||||
void fullscreen() {
|
||||
windowManager.setFullScreen(!isFullscreen);
|
||||
late WindowFrameController windowFrame;
|
||||
|
||||
bool _isInit = false;
|
||||
|
||||
void initReaderWindow() {
|
||||
if (!App.isDesktop || _isInit) return;
|
||||
windowFrame = WindowFrame.of(App.rootContext);
|
||||
windowFrame.addCloseListener(onWindowClose);
|
||||
_isInit = true;
|
||||
}
|
||||
|
||||
void fullscreen() async {
|
||||
if (!App.isDesktop) return;
|
||||
await windowManager.hide();
|
||||
await windowManager.setFullScreen(!isFullscreen);
|
||||
await windowManager.show();
|
||||
isFullscreen = !isFullscreen;
|
||||
WindowFrame.of(App.rootContext).setWindowFrame(!isFullscreen);
|
||||
}
|
||||
|
||||
bool onWindowClose() {
|
||||
if (Navigator.of(App.rootContext).canPop()) {
|
||||
Navigator.of(App.rootContext).pop();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void disposeReaderWindow() {
|
||||
if (!App.isDesktop) return;
|
||||
windowFrame.removeCloseListener(onWindowClose);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -330,11 +330,10 @@ class _WebdavSettingState extends State<_WebdavSetting> {
|
||||
String url = "";
|
||||
String user = "";
|
||||
String pass = "";
|
||||
bool autoSync = false;
|
||||
bool autoSync = true;
|
||||
|
||||
bool isTesting = false;
|
||||
bool upload = true;
|
||||
bool isEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -349,8 +348,7 @@ class _WebdavSettingState extends State<_WebdavSetting> {
|
||||
url = configs[0];
|
||||
user = configs[1];
|
||||
pass = configs[2];
|
||||
isEnabled = true;
|
||||
autoSync = appdata.implicitData['webdavAutoSync'] ?? false;
|
||||
autoSync = appdata.implicitData['webdavAutoSync'] ?? true;
|
||||
}
|
||||
|
||||
void onAutoSyncChanged(bool value) {
|
||||
@@ -368,16 +366,11 @@ class _WebdavSettingState extends State<_WebdavSetting> {
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
SwitchListTile(
|
||||
title: Text("WebDAV Auto Sync".tl),
|
||||
value: autoSync,
|
||||
onChanged: onAutoSyncChanged,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
labelText: "URL",
|
||||
hintText: "A valid WebDav directory URL".tl,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
controller: TextEditingController(text: url),
|
||||
@@ -402,6 +395,16 @@ class _WebdavSettingState extends State<_WebdavSetting> {
|
||||
onChanged: (value) => pass = value,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ListTile(
|
||||
leading: Icon(Icons.sync),
|
||||
title: Text("Auto Sync Data".tl),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
trailing: Switch(
|
||||
value: autoSync,
|
||||
onChanged: onAutoSyncChanged,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Text("Operation".tl),
|
||||
@@ -428,21 +431,28 @@ class _WebdavSettingState extends State<_WebdavSetting> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text("Once the operation is successful, app will automatically sync data with the server.".tl),
|
||||
),
|
||||
],
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: autoSync
|
||||
? Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Once the operation is successful, app will automatically sync data with the server."
|
||||
.tl),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Center(
|
||||
|
@@ -80,6 +80,24 @@ class _ExploreSettingsState extends State<ExploreSettings> {
|
||||
'japanese': "Japanese",
|
||||
},
|
||||
).toSliver(),
|
||||
SelectSetting(
|
||||
title: "Initial Page".tl,
|
||||
settingKey: "initialPage",
|
||||
optionTranslation: {
|
||||
'0': "Home Page".tl,
|
||||
'1': "Favorites Page".tl,
|
||||
'2': "Explore Page".tl,
|
||||
'3': "Categories Page".tl,
|
||||
},
|
||||
).toSliver(),
|
||||
SelectSetting(
|
||||
title: "Display mode of comic list".tl,
|
||||
settingKey: "comicListDisplayMode",
|
||||
optionTranslation: {
|
||||
"paging": "Paging".tl,
|
||||
"Continuous": "Continuous".tl,
|
||||
},
|
||||
).toSliver(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@@ -177,7 +177,7 @@ abstract class CBZ {
|
||||
tags: metaData.tags,
|
||||
comicType: ComicType.local,
|
||||
directory: dest.name,
|
||||
chapters: ComicChapters.fromJson(cpMap),
|
||||
chapters: ComicChapters.fromJsonOrNull(cpMap),
|
||||
downloadedChapters: cpMap?.keys.toList() ?? [],
|
||||
cover: 'cover.${coverFile.extension}',
|
||||
createdAt: DateTime.now(),
|
||||
|
@@ -95,15 +95,17 @@ 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);
|
||||
}
|
||||
}
|
||||
await ComicSource.reload();
|
||||
await ComicSourceManager().reload();
|
||||
}
|
||||
} finally {
|
||||
cacheDir.deleteIgnoreError(recursive: true);
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
@@ -10,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';
|
||||
|
||||
@@ -19,7 +19,7 @@ class DataSync with ChangeNotifier {
|
||||
downloadData();
|
||||
}
|
||||
LocalFavoritesManager().addListener(onDataChanged);
|
||||
ComicSource.addListener(onDataChanged);
|
||||
ComicSourceManager().addListener(onDataChanged);
|
||||
}
|
||||
|
||||
void onDataChanged() {
|
||||
@@ -40,7 +40,11 @@ class DataSync with ChangeNotifier {
|
||||
|
||||
bool get isUploading => _isUploading;
|
||||
|
||||
bool haveWaitingTask = false;
|
||||
bool _haveWaitingTask = false;
|
||||
|
||||
String? _lastError;
|
||||
|
||||
String? get lastError => _lastError;
|
||||
|
||||
bool get isEnabled {
|
||||
var config = appdata.settings['webdav'];
|
||||
@@ -64,17 +68,19 @@ class DataSync with ChangeNotifier {
|
||||
|
||||
Future<Res<bool>> uploadData() async {
|
||||
if (isDownloading) return const Res(true);
|
||||
if (haveWaitingTask) return const Res(true);
|
||||
if (_haveWaitingTask) return const Res(true);
|
||||
while (isUploading) {
|
||||
haveWaitingTask = true;
|
||||
_haveWaitingTask = true;
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
haveWaitingTask = false;
|
||||
_haveWaitingTask = false;
|
||||
_isUploading = true;
|
||||
_lastError = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
var config = _validateConfig();
|
||||
if (config == null) {
|
||||
_lastError = 'Invalid WebDAV configuration';
|
||||
return const Res.error('Invalid WebDAV configuration');
|
||||
}
|
||||
if (config.isEmpty) {
|
||||
@@ -90,21 +96,14 @@ class DataSync with ChangeNotifier {
|
||||
url,
|
||||
user: user,
|
||||
password: pass,
|
||||
adapter: IOHttpClientAdapter(
|
||||
createHttpClient: () {
|
||||
return HttpClient()
|
||||
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
|
||||
},
|
||||
adapter: RHttpAdapter(
|
||||
rhttp.ClientSettings(
|
||||
proxySettings:
|
||||
proxy == null ? null : rhttp.ProxySettings.proxy(proxy),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await client.ping();
|
||||
} catch (e) {
|
||||
Log.error("Upload Data", 'Failed to connect to WebDAV server');
|
||||
return const Res.error('Failed to connect to WebDAV server');
|
||||
}
|
||||
|
||||
try {
|
||||
appdata.settings['dataVersion']++;
|
||||
await appdata.saveData(false);
|
||||
@@ -131,6 +130,7 @@ class DataSync with ChangeNotifier {
|
||||
return const Res(true);
|
||||
} catch (e, s) {
|
||||
Log.error("Upload Data", e, s);
|
||||
_lastError = e.toString();
|
||||
return Res.error(e.toString());
|
||||
}
|
||||
} finally {
|
||||
@@ -140,17 +140,19 @@ class DataSync with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<Res<bool>> downloadData() async {
|
||||
if (haveWaitingTask) return const Res(true);
|
||||
if (_haveWaitingTask) return const Res(true);
|
||||
while (isDownloading || isUploading) {
|
||||
haveWaitingTask = true;
|
||||
_haveWaitingTask = true;
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
haveWaitingTask = false;
|
||||
_haveWaitingTask = false;
|
||||
_isDownloading = true;
|
||||
_lastError = null;
|
||||
notifyListeners();
|
||||
try {
|
||||
var config = _validateConfig();
|
||||
if (config == null) {
|
||||
_lastError = 'Invalid WebDAV configuration';
|
||||
return const Res.error('Invalid WebDAV configuration');
|
||||
}
|
||||
if (config.isEmpty) {
|
||||
@@ -166,21 +168,14 @@ class DataSync with ChangeNotifier {
|
||||
url,
|
||||
user: user,
|
||||
password: pass,
|
||||
adapter: IOHttpClientAdapter(
|
||||
createHttpClient: () {
|
||||
return HttpClient()
|
||||
..findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
|
||||
},
|
||||
adapter: RHttpAdapter(
|
||||
rhttp.ClientSettings(
|
||||
proxySettings:
|
||||
proxy == null ? null : rhttp.ProxySettings.proxy(proxy),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await client.ping();
|
||||
} catch (e) {
|
||||
Log.error("Data Sync", 'Failed to connect to WebDAV server');
|
||||
return const Res.error('Failed to connect to WebDAV server');
|
||||
}
|
||||
|
||||
try {
|
||||
var files = await client.readDir('/');
|
||||
files.sort((a, b) => b.name!.compareTo(a.name!));
|
||||
@@ -206,6 +201,7 @@ class DataSync with ChangeNotifier {
|
||||
return const Res(true);
|
||||
} catch (e, s) {
|
||||
Log.error("Data Sync", e, s);
|
||||
_lastError = e.toString();
|
||||
return Res.error(e.toString());
|
||||
}
|
||||
} finally {
|
||||
|
22
lib/utils/handle_text_share.dart
Normal file
22
lib/utils/handle_text_share.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
40
lib/utils/init.dart
Normal file
40
lib/utils/init.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// A mixin class that provides a way to ensure the class is initialized.
|
||||
abstract mixin class Init {
|
||||
bool _isInit = false;
|
||||
|
||||
final _initCompleter = <Completer<void>>[];
|
||||
|
||||
/// Ensure the class is initialized.
|
||||
Future<void> ensureInit() async {
|
||||
if (_isInit) {
|
||||
return;
|
||||
}
|
||||
var completer = Completer<void>();
|
||||
_initCompleter.add(completer);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<void> _markInit() async {
|
||||
_isInit = true;
|
||||
for (var completer in _initCompleter) {
|
||||
completer.complete();
|
||||
}
|
||||
_initCompleter.clear();
|
||||
}
|
||||
|
||||
@protected
|
||||
Future<void> doInit();
|
||||
|
||||
/// Initialize the class.
|
||||
Future<void> init() async {
|
||||
if (_isInit) {
|
||||
return;
|
||||
}
|
||||
await doInit();
|
||||
await _markInit();
|
||||
}
|
||||
}
|
@@ -40,6 +40,7 @@ extension FileSystemEntityExt on FileSystemEntity {
|
||||
return p.basename(path);
|
||||
}
|
||||
|
||||
/// Delete the file or directory and ignore errors.
|
||||
Future<void> deleteIgnoreError({bool recursive = false}) async {
|
||||
try {
|
||||
await delete(recursive: recursive);
|
||||
@@ -48,12 +49,14 @@ extension FileSystemEntityExt on FileSystemEntity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the file or directory if it exists.
|
||||
Future<void> deleteIfExists({bool recursive = false}) async {
|
||||
if (existsSync()) {
|
||||
await delete(recursive: recursive);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the file or directory if it exists.
|
||||
void deleteIfExistsSync({bool recursive = false}) {
|
||||
if (existsSync()) {
|
||||
deleteSync(recursive: recursive);
|
||||
@@ -74,12 +77,14 @@ extension FileExtension on File {
|
||||
await newFile.writeAsBytes(await readAsBytes());
|
||||
}
|
||||
|
||||
/// Get the base name of the file without the extension.
|
||||
String get basenameWithoutExt {
|
||||
return p.basenameWithoutExtension(path);
|
||||
}
|
||||
}
|
||||
|
||||
extension DirectoryExtension on Directory {
|
||||
/// Calculate the size of the directory.
|
||||
Future<int> get size async {
|
||||
if (!existsSync()) return 0;
|
||||
int total = 0;
|
||||
@@ -91,6 +96,7 @@ extension DirectoryExtension on Directory {
|
||||
return total;
|
||||
}
|
||||
|
||||
/// Change the base name of the directory.
|
||||
Directory renameX(String newName) {
|
||||
newName = sanitizeFileName(newName);
|
||||
return renameSync(path.replaceLast(name, newName));
|
||||
@@ -100,6 +106,7 @@ extension DirectoryExtension on Directory {
|
||||
return File(FilePath.join(path, name));
|
||||
}
|
||||
|
||||
/// Delete the contents of the directory.
|
||||
void deleteContentsSync({recursive = true}) {
|
||||
if (!existsSync()) return;
|
||||
for (var f in listSync()) {
|
||||
@@ -107,14 +114,24 @@ extension DirectoryExtension on Directory {
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the contents of the directory.
|
||||
Future<void> deleteContents({recursive = true}) async {
|
||||
if (!existsSync()) return;
|
||||
for (var f in listSync()) {
|
||||
await f.deleteIfExists(recursive: recursive);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the directory. If the directory already exists, delete it first.
|
||||
void forceCreateSync() {
|
||||
if (existsSync()) {
|
||||
deleteSync(recursive: true);
|
||||
}
|
||||
createSync(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitize the file name. Remove invalid characters and trim the file name.
|
||||
String sanitizeFileName(String fileName) {
|
||||
if (fileName.endsWith('.')) {
|
||||
fileName = fileName.substring(0, fileName.length - 1);
|
||||
@@ -157,6 +174,8 @@ Future<void> copyDirectory(Directory source, Directory destination) async {
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy the **contents** of the source directory to the destination directory.
|
||||
/// This function is executed in an isolate to prevent the UI from freezing.
|
||||
Future<void> copyDirectoryIsolate(
|
||||
Directory source, Directory destination) async {
|
||||
await Isolate.run(() => overrideIO(() => copyDirectory(source, destination)));
|
||||
|
@@ -48,6 +48,12 @@ static void my_application_activate(GApplication* application) {
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
GdkVisual* visual;
|
||||
gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE);
|
||||
visual = gdk_screen_get_rgba_visual(screen);
|
||||
if (visual != NULL && gdk_screen_is_composited(screen)) {
|
||||
gtk_widget_set_visual(GTK_WIDGET(window), visual);
|
||||
}
|
||||
gtk_widget_show(GTK_WIDGET(window));
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
@@ -58,6 +64,7 @@ static void my_application_activate(GApplication* application) {
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
gtk_widget_hide(GTK_WIDGET(window));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
32
pubspec.lock
32
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"
|
||||
@@ -758,10 +758,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: rhttp
|
||||
sha256: "3deabc6c3384b4efa252dfb4a5059acc6530117fdc1b10f5f67ff9768c9af75a"
|
||||
sha256: "037e9b59a68bb4ba664db1cbb4601e878cf5a2fc1cb3d0a9c58e3776609dec4d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.0"
|
||||
version: "0.11.0"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1044,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"
|
||||
@@ -1093,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"
|
||||
|
18
pubspec.yaml
18
pubspec.yaml
@@ -2,11 +2,11 @@ name: venera
|
||||
description: "A comic app."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.3.1+131
|
||||
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,22 +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: 0.10.0
|
||||
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
|
||||
|
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "1.85.1"
|
||||
targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"]
|
Reference in New Issue
Block a user