mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
169676fd9e | ||
332497cf90 | |||
5f15c08eef | |||
3f6b3152b2 | |||
f5b3b36acb | |||
fd8607777e | |||
fa951cac95 | |||
55ad652191 | |||
533497ead1 | |||
![]() |
00cdc18ddd | ||
![]() |
474d9aa6f1 | ||
ffa0c8f887 | |||
0f3f3ea270 | |||
![]() |
b752caa079 | ||
309df2143b | |||
8e964468ea | |||
ca8f09807b | |||
68b214e295 | |||
00c0a64de0 |
@@ -32,7 +32,7 @@ keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
|||||||
android {
|
android {
|
||||||
namespace = "com.github.wgh136.venera"
|
namespace = "com.github.wgh136.venera"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion "25.1.8937393"
|
ndkVersion "28.0.13004108"
|
||||||
|
|
||||||
splits{
|
splits{
|
||||||
abi {
|
abi {
|
||||||
|
@@ -47,6 +47,11 @@
|
|||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="https" android:host="exhentai.org" android:pathPrefix="/g" />
|
<data android:scheme="https" android:host="exhentai.org" android:pathPrefix="/g" />
|
||||||
</intent-filter>
|
</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>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
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.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -40,6 +41,41 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
|
|
||||||
private val nextLocalRequestCode = AtomicInteger()
|
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(
|
private fun <I, O> startContractForResult(
|
||||||
contract: ActivityResultContract<I, O>,
|
contract: ActivityResultContract<I, O>,
|
||||||
input: I,
|
input: I,
|
||||||
@@ -134,6 +170,26 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
val mimeType = req.arguments<String>()
|
val mimeType = req.arguments<String>()
|
||||||
openFile(res, mimeType!!)
|
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 {
|
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>
|
@@ -367,7 +367,12 @@
|
|||||||
"Home Page": "主页",
|
"Home Page": "主页",
|
||||||
"Favorites Page": "收藏页面",
|
"Favorites Page": "收藏页面",
|
||||||
"Explore Page": "探索页面",
|
"Explore Page": "探索页面",
|
||||||
"Categories Page": "分类页面"
|
"Categories Page": "分类页面",
|
||||||
|
"Convert to local": "转换为本地",
|
||||||
|
"Refresh": "刷新",
|
||||||
|
"Paging": "分页",
|
||||||
|
"Continuous": "连续",
|
||||||
|
"Display mode of comic list": "漫画列表的显示模式"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -725,18 +730,23 @@
|
|||||||
"All Comics": "全部漫畫",
|
"All Comics": "全部漫畫",
|
||||||
"The comic will be marked as no updates as soon as you read it.": "漫畫將在您閱讀後立即標記為無更新",
|
"The comic will be marked as no updates as soon as you read it.": "漫畫將在您閱讀後立即標記為無更新",
|
||||||
"Disable": "停用",
|
"Disable": "停用",
|
||||||
"Once the operation is successful, app will automatically sync data with the server.": "操作成功後, APP將自動與服務器同步數據",
|
"Once the operation is successful, app will automatically sync data with the server.": "操作成功後, APP將自動與伺服器同步資料",
|
||||||
"Cache cleared": "緩存已清除",
|
"Cache cleared": "快取已清除",
|
||||||
"Disabled": "已禁用",
|
"Disabled": "已停用",
|
||||||
"WebDAV Auto Sync": "WebDAV 自動同步",
|
"WebDAV Auto Sync": "WebDAV 自動同步",
|
||||||
"Mark all as read": "全部標記為已讀",
|
"Mark all as read": "全部標記為已讀",
|
||||||
"Do you want to mark all as read?" : "您要全部標記為已讀嗎?",
|
"Do you want to mark all as read?" : "您要全部標記為已讀嗎?",
|
||||||
"Swipe down for previous chapter": "向下滑動查看上一章",
|
"Swipe down for previous chapter": "向下滑動查看上一章",
|
||||||
"Swipe up for next chapter": "向上滑動查看下一章",
|
"Swipe up for next chapter": "向上滑動查看下一章",
|
||||||
"Initial Page": "初始頁面",
|
"Initial Page": "初始頁面",
|
||||||
"Home Page": "主頁",
|
"Home Page": "首頁",
|
||||||
"Favorites Page": "收藏頁面",
|
"Favorites Page": "收藏頁面",
|
||||||
"Explore Page": "探索頁面",
|
"Explore Page": "探索頁面",
|
||||||
"Categories Page": "分類頁面"
|
"Categories Page": "分類頁面",
|
||||||
|
"Convert to local": "轉換為本地",
|
||||||
|
"Refresh": "刷新",
|
||||||
|
"Paging": "分頁",
|
||||||
|
"Continuous": "連續",
|
||||||
|
"Display mode of comic list": "漫畫列表的顯示模式"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -770,7 +770,7 @@ class _SliverGridComicsState extends State<SliverGridComics> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant SliverGridComics oldWidget) {
|
void didUpdateWidget(covariant SliverGridComics oldWidget) {
|
||||||
if (!oldWidget.comics.isEqualTo(widget.comics)) {
|
if (!comics.isEqualTo(widget.comics)) {
|
||||||
comics.clear();
|
comics.clear();
|
||||||
for (var comic in widget.comics) {
|
for (var comic in widget.comics) {
|
||||||
if (isBlocked(comic) == null) {
|
if (isBlocked(comic) == null) {
|
||||||
@@ -879,6 +879,7 @@ class _SliverGridComics extends StatelessWidget {
|
|||||||
return comic;
|
return comic;
|
||||||
}
|
}
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
|
key: ValueKey(comics[index].id),
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
@@ -1140,7 +1141,7 @@ class ComicListState extends State<ComicList> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (_loading[page] == true) {
|
if (_data[page] != null || _loading[page] == true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_loading[page] = true;
|
_loading[page] = true;
|
||||||
@@ -1150,8 +1151,8 @@ class ComicListState extends State<ComicList> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
if (res.data.isEmpty) {
|
if (res.data.isEmpty) {
|
||||||
_data[page] = const [];
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_data[page] = const [];
|
||||||
_maxPage = page;
|
_maxPage = page;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -1201,6 +1202,11 @@ class ComicListState extends State<ComicList> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var type = appdata.settings['comicListDisplayMode'];
|
||||||
|
return type == 'paging' ? buildPagingMode() : buildContinuousMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildPagingMode() {
|
||||||
if (_error != null) {
|
if (_error != null) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
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 {
|
class StarRating extends StatelessWidget {
|
||||||
@@ -1535,17 +1620,20 @@ class _SMClipper extends CustomClipper<Rect> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SimpleComicTile extends StatelessWidget {
|
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 Comic comic;
|
||||||
|
|
||||||
final void Function()? onTap;
|
final void Function()? onTap;
|
||||||
|
|
||||||
|
final bool withTitle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var image = _findImageProvider(comic);
|
var image = _findImageProvider(comic);
|
||||||
|
|
||||||
var child = image == null
|
Widget child = image == null
|
||||||
? const SizedBox()
|
? const SizedBox()
|
||||||
: AnimatedImage(
|
: AnimatedImage(
|
||||||
image: image,
|
image: image,
|
||||||
@@ -1555,7 +1643,18 @@ class SimpleComicTile extends StatelessWidget {
|
|||||||
filterQuality: FilterQuality.medium,
|
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,
|
borderRadius: 8,
|
||||||
onTap: onTap ??
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -99,11 +99,13 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
|||||||
);
|
);
|
||||||
if (_futurePosition == old) return;
|
if (_futurePosition == old) return;
|
||||||
var target = _futurePosition!;
|
var target = _futurePosition!;
|
||||||
_controller.animateTo(
|
_controller
|
||||||
|
.animateTo(
|
||||||
_futurePosition!,
|
_futurePosition!,
|
||||||
duration: _fastAnimationDuration,
|
duration: _fastAnimationDuration,
|
||||||
curve: Curves.linear,
|
curve: Curves.linear,
|
||||||
).then((_) {
|
)
|
||||||
|
.then((_) {
|
||||||
var current = _controller.position.pixels;
|
var current = _controller.position.pixels;
|
||||||
if (current == target && current == _futurePosition) {
|
if (current == target && current == _futurePosition) {
|
||||||
_futurePosition = null;
|
_futurePosition = null;
|
||||||
@@ -144,3 +146,169 @@ class ScrollControllerProvider extends InheritedWidget {
|
|||||||
return oldWidget.controller != controller;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.3.2";
|
final version = "1.3.3";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
|
@@ -4,9 +4,10 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/utils/data_sync.dart';
|
import 'package:venera/utils/data_sync.dart';
|
||||||
|
import 'package:venera/utils/init.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
|
|
||||||
class Appdata {
|
class Appdata with Init {
|
||||||
Appdata._create();
|
Appdata._create();
|
||||||
|
|
||||||
final Settings settings = Settings._create();
|
final Settings settings = Settings._create();
|
||||||
@@ -53,28 +54,6 @@ class Appdata {
|
|||||||
saveData();
|
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() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
'settings': settings._data,
|
'settings': settings._data,
|
||||||
@@ -110,6 +89,29 @@ class Appdata {
|
|||||||
var file = File(FilePath.join(App.dataPath, 'implicitData.json'));
|
var file = File(FilePath.join(App.dataPath, 'implicitData.json'));
|
||||||
file.writeAsString(jsonEncode(implicitData));
|
file.writeAsString(jsonEncode(implicitData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> doInit() async {
|
||||||
|
var dataPath = (await getApplicationSupportDirectory()).path;
|
||||||
|
var file = File(FilePath.join(
|
||||||
|
dataPath,
|
||||||
|
'appdata.json',
|
||||||
|
));
|
||||||
|
if (!await file.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var json = jsonDecode(await file.readAsString());
|
||||||
|
for (var key in (json['settings'] as Map<String, dynamic>).keys) {
|
||||||
|
if (json['settings'][key] != null) {
|
||||||
|
settings[key] = json['settings'][key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searchHistory = List.from(json['searchHistory']);
|
||||||
|
var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json'));
|
||||||
|
if (await implicitDataFile.exists()) {
|
||||||
|
implicitData = jsonDecode(await implicitDataFile.readAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final appdata = Appdata._create();
|
final appdata = Appdata._create();
|
||||||
@@ -160,10 +162,12 @@ class Settings with ChangeNotifier {
|
|||||||
'customImageProcessing': defaultCustomImageProcessing,
|
'customImageProcessing': defaultCustomImageProcessing,
|
||||||
'sni': true,
|
'sni': true,
|
||||||
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
'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,
|
'preloadImageCount': 4,
|
||||||
'followUpdatesFolder': null,
|
'followUpdatesFolder': null,
|
||||||
'initialPage': '0',
|
'initialPage': '0',
|
||||||
|
'comicListDisplayMode': 'paging', // paging, continuous
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
|
@@ -111,6 +111,9 @@ class Comic {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => id.hashCode ^ sourceKey.hashCode;
|
int get hashCode => id.hashCode ^ sourceKey.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() => "$sourceKey@$id";
|
||||||
}
|
}
|
||||||
|
|
||||||
class ComicDetails with HistoryMixin {
|
class ComicDetails with HistoryMixin {
|
||||||
|
@@ -224,7 +224,8 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
source_folder text
|
source_folder text
|
||||||
);
|
);
|
||||||
""");
|
""");
|
||||||
for (var folder in _getFolderNamesWithDB()) {
|
var folderNames = _getFolderNamesWithDB();
|
||||||
|
for (var folder in folderNames) {
|
||||||
var columns = _db.select("""
|
var columns = _db.select("""
|
||||||
pragma table_info("$folder");
|
pragma table_info("$folder");
|
||||||
""");
|
""");
|
||||||
@@ -246,6 +247,15 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
break;
|
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) {
|
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"
|
// check if the table has the column "last_update_time" "has_new_update" "last_check_time"
|
||||||
var columns = _db.select("""
|
var columns = _db.select("""
|
||||||
pragma table_info("$table");
|
pragma table_info("$table");
|
||||||
@@ -866,10 +876,12 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
add column has_new_update int;
|
add column has_new_update int;
|
||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
|
if (clearData) {
|
||||||
_db.execute("""
|
_db.execute("""
|
||||||
update "$table"
|
update "$table"
|
||||||
set has_new_update = 0;
|
set has_new_update = 0;
|
||||||
""");
|
""");
|
||||||
|
}
|
||||||
if (!columns.any((element) => element["name"] == "last_check_time")) {
|
if (!columns.any((element) => element["name"] == "last_check_time")) {
|
||||||
_db.execute("""
|
_db.execute("""
|
||||||
alter table "$table"
|
alter table "$table"
|
||||||
|
@@ -422,12 +422,30 @@ class LocalManager with ChangeNotifier {
|
|||||||
return files.map((e) => "file://${e.path}").toList();
|
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);
|
var comic = find(id, type);
|
||||||
if (comic == null) return false;
|
if (comic == null) return false;
|
||||||
if (comic.chapters == null || ep == null) return true;
|
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
|
return comic.downloadedChapters
|
||||||
.contains(comic.chapters!.ids.elementAt(ep - 1));
|
.contains((chapters ?? comic.chapters)!.ids.elementAtOrNull(ep - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DownloadTask> downloadingTasks = [];
|
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/follow_updates_page.dart';
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
import 'package:venera/utils/app_links.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/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
import 'foundation/appdata.dart';
|
import 'foundation/appdata.dart';
|
||||||
@@ -45,6 +46,7 @@ Future<void> init() async {
|
|||||||
_checkOldConfigs();
|
_checkOldConfigs();
|
||||||
if (App.isAndroid) {
|
if (App.isAndroid) {
|
||||||
handleLinks();
|
handleLinks();
|
||||||
|
handleTextShare();
|
||||||
}
|
}
|
||||||
FlutterError.onError = (details) {
|
FlutterError.onError = (details) {
|
||||||
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
|
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||||
|
@@ -141,24 +141,15 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
) {
|
) {
|
||||||
String? font;
|
String? font;
|
||||||
List<String>? fallback;
|
List<String>? fallback;
|
||||||
if (App.isWindows) {
|
if (App.isLinux || App.isWindows) {
|
||||||
font = 'Segoe UI';
|
|
||||||
fallback = [
|
|
||||||
'Segoe UI',
|
|
||||||
'Microsoft YaHei',
|
|
||||||
'PingFang SC',
|
|
||||||
'Noto Sans CJK',
|
|
||||||
'Arial',
|
|
||||||
'sans-serif'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (App.isLinux) {
|
|
||||||
font = 'Noto Sans CJK';
|
font = 'Noto Sans CJK';
|
||||||
fallback = [
|
fallback = [
|
||||||
'Segoe UI',
|
'Segoe UI',
|
||||||
|
'Noto Sans SC',
|
||||||
|
'Noto Sans TC',
|
||||||
|
'Noto Sans',
|
||||||
'Microsoft YaHei',
|
'Microsoft YaHei',
|
||||||
'PingFang SC',
|
'PingFang SC',
|
||||||
'Noto Sans CJK',
|
|
||||||
'Arial',
|
'Arial',
|
||||||
'sans-serif'
|
'sans-serif'
|
||||||
];
|
];
|
||||||
|
@@ -282,9 +282,27 @@ class RHttpAdapter implements HttpClientAdapter {
|
|||||||
return ResponseBody(
|
return ResponseBody(
|
||||||
res.body,
|
res.body,
|
||||||
res.statusCode,
|
res.statusCode,
|
||||||
statusMessage: null,
|
statusMessage: _getStatusMessage(res.statusCode),
|
||||||
isRedirect: false,
|
isRedirect: false,
|
||||||
headers: headers,
|
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",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -90,7 +90,7 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
|||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
|
|
||||||
static const _kComicHeight = 132.0;
|
static const _kComicHeight = 162.0;
|
||||||
|
|
||||||
get _comicWidth => _kComicHeight * 0.7;
|
get _comicWidth => _kComicHeight * 0.7;
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildComic(Comic c) {
|
Widget buildComic(Comic c) {
|
||||||
return SimpleComicTile(comic: c)
|
return SimpleComicTile(comic: c, withTitle: true)
|
||||||
.paddingLeft(_kLeftPadding)
|
.paddingLeft(_kLeftPadding)
|
||||||
.paddingBottom(2);
|
.paddingBottom(2);
|
||||||
}
|
}
|
||||||
|
@@ -186,12 +186,17 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters>
|
|||||||
|
|
||||||
late TabController tabController;
|
late TabController tabController;
|
||||||
|
|
||||||
int index = 0;
|
late int index;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
history = widget.history;
|
history = widget.history;
|
||||||
|
if (history?.group != null) {
|
||||||
|
index = history!.group! - 1;
|
||||||
|
} else {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -199,6 +204,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters>
|
|||||||
state = context.findAncestorStateOfType<_ComicPageState>()!;
|
state = context.findAncestorStateOfType<_ComicPageState>()!;
|
||||||
chapters = state.comic.chapters!;
|
chapters = state.comic.chapters!;
|
||||||
tabController = TabController(
|
tabController = TabController(
|
||||||
|
initialIndex: index,
|
||||||
length: chapters.ids.length,
|
length: chapters.ids.length,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
);
|
);
|
||||||
|
@@ -518,11 +518,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
body = Scrollbar(
|
body = AppScrollBar(
|
||||||
|
topPadding: 48,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
thickness: App.isDesktop ? 8 : 12,
|
|
||||||
radius: const Radius.circular(8),
|
|
||||||
interactive: true,
|
|
||||||
child: ScrollConfiguration(
|
child: ScrollConfiguration(
|
||||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||||
child: body,
|
child: body,
|
||||||
|
@@ -110,6 +110,15 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> {
|
|||||||
child: Text(widget.data.title),
|
child: Text(widget.data.title),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
Tooltip(
|
||||||
|
message: "Refresh".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
onPressed: () {
|
||||||
|
comicListKey.currentState!.refresh();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
MenuButton(entries: [
|
MenuButton(entries: [
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon: Icons.sync,
|
icon: Icons.sync,
|
||||||
|
@@ -297,7 +297,7 @@ class _HistoryState extends State<_History> {
|
|||||||
).paddingHorizontal(16),
|
).paddingHorizontal(16),
|
||||||
if (history.isNotEmpty)
|
if (history.isNotEmpty)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 128,
|
height: 136,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: history.length,
|
itemCount: history.length,
|
||||||
@@ -400,13 +400,14 @@ class _LocalState extends State<_Local> {
|
|||||||
).paddingHorizontal(16),
|
).paddingHorizontal(16),
|
||||||
if (local.isNotEmpty)
|
if (local.isNotEmpty)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 128,
|
height: 136,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: local.length,
|
itemCount: local.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return SimpleComicTile(comic: local[index])
|
return SimpleComicTile(comic: local[index])
|
||||||
.paddingHorizontal(8);
|
.paddingHorizontal(8)
|
||||||
|
.paddingVertical(2);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
).paddingHorizontal(8),
|
).paddingHorizontal(8),
|
||||||
|
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/pages/comic_details_page/comic_page.dart';
|
import 'package:venera/pages/comic_details_page/comic_page.dart';
|
||||||
@@ -304,7 +305,9 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
(c as LocalComic).read();
|
// prevent dirty data
|
||||||
|
var comic = LocalManager().find(c.id, ComicType(c.sourceKey.hashCode))!;
|
||||||
|
comic.read();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
menuBuilder: (c) {
|
menuBuilder: (c) {
|
||||||
|
@@ -26,7 +26,7 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
inProgress = true;
|
inProgress = true;
|
||||||
if (reader.type == ComicType.local ||
|
if (reader.type == ComicType.local ||
|
||||||
(LocalManager()
|
(LocalManager()
|
||||||
.isDownloaded(reader.cid, reader.type, reader.chapter))) {
|
.isDownloaded(reader.cid, reader.type, reader.chapter, reader.widget.chapters))) {
|
||||||
try {
|
try {
|
||||||
var images = await LocalManager()
|
var images = await LocalManager()
|
||||||
.getImages(reader.cid, reader.type, reader.chapter);
|
.getImages(reader.cid, reader.type, reader.chapter);
|
||||||
|
@@ -115,7 +115,7 @@ class _ReaderState extends State<Reader>
|
|||||||
|
|
||||||
String get cid => widget.cid;
|
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;
|
List<String>? images;
|
||||||
|
|
||||||
|
@@ -90,6 +90,14 @@ class _ExploreSettingsState extends State<ExploreSettings> {
|
|||||||
'3': "Categories Page".tl,
|
'3': "Categories Page".tl,
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
|
SelectSetting(
|
||||||
|
title: "Display mode of comic list".tl,
|
||||||
|
settingKey: "comicListDisplayMode",
|
||||||
|
optionTranslation: {
|
||||||
|
"paging": "Paging".tl,
|
||||||
|
"Continuous": "Continuous".tl,
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -95,11 +95,13 @@ Future<void> importAppData(File file, [bool checkVersion = false]) async {
|
|||||||
}
|
}
|
||||||
var comicSourceDir = FilePath.join(cacheDirPath, "comic_source");
|
var comicSourceDir = FilePath.join(cacheDirPath, "comic_source");
|
||||||
if (Directory(comicSourceDir).existsSync()) {
|
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()) {
|
for (var file in Directory(comicSourceDir).listSync()) {
|
||||||
if (file is File) {
|
if (file is File) {
|
||||||
var targetFile =
|
var targetFile =
|
||||||
FilePath.join(App.dataPath, "comic_source", file.name);
|
FilePath.join(App.dataPath, "comic_source", file.name);
|
||||||
File(targetFile).deleteIfExistsSync();
|
|
||||||
await file.copy(targetFile);
|
await file.copy(targetFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import 'package:venera/network/app_dio.dart';
|
|||||||
import 'package:venera/utils/data.dart';
|
import 'package:venera/utils/data.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:webdav_client/webdav_client.dart' hide File;
|
import 'package:webdav_client/webdav_client.dart' hide File;
|
||||||
|
import 'package:rhttp/rhttp.dart' as rhttp;
|
||||||
|
|
||||||
import 'io.dart';
|
import 'io.dart';
|
||||||
|
|
||||||
@@ -89,11 +90,18 @@ class DataSync with ChangeNotifier {
|
|||||||
String user = config[1];
|
String user = config[1];
|
||||||
String pass = config[2];
|
String pass = config[2];
|
||||||
|
|
||||||
|
var proxy = await AppDio.getProxy();
|
||||||
|
|
||||||
var client = newClient(
|
var client = newClient(
|
||||||
url,
|
url,
|
||||||
user: user,
|
user: user,
|
||||||
password: pass,
|
password: pass,
|
||||||
adapter: RHttpAdapter(),
|
adapter: RHttpAdapter(
|
||||||
|
rhttp.ClientSettings(
|
||||||
|
proxySettings:
|
||||||
|
proxy == null ? null : rhttp.ProxySettings.proxy(proxy),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -154,11 +162,18 @@ class DataSync with ChangeNotifier {
|
|||||||
String user = config[1];
|
String user = config[1];
|
||||||
String pass = config[2];
|
String pass = config[2];
|
||||||
|
|
||||||
|
var proxy = await AppDio.getProxy();
|
||||||
|
|
||||||
var client = newClient(
|
var client = newClient(
|
||||||
url,
|
url,
|
||||||
user: user,
|
user: user,
|
||||||
password: pass,
|
password: pass,
|
||||||
adapter: RHttpAdapter(),
|
adapter: RHttpAdapter(
|
||||||
|
rhttp.ClientSettings(
|
||||||
|
proxySettings:
|
||||||
|
proxy == null ? null : rhttp.ProxySettings.proxy(proxy),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
pubspec.lock
39
pubspec.lock
@@ -408,8 +408,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "5978d0c7784fbbefcacc573547f0ab01ba59b7b3"
|
ref: "8feae95df7fb00455df129ad7a0dfec1d0e8d8e4"
|
||||||
resolved-ref: "5978d0c7784fbbefcacc573547f0ab01ba59b7b3"
|
resolved-ref: "8feae95df7fb00455df129ad7a0dfec1d0e8d8e4"
|
||||||
url: "https://github.com/wgh136/flutter_qjs"
|
url: "https://github.com/wgh136/flutter_qjs"
|
||||||
source: git
|
source: git
|
||||||
version: "0.3.7"
|
version: "0.3.7"
|
||||||
@@ -425,17 +425,17 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_rust_bridge
|
name: flutter_rust_bridge
|
||||||
sha256: "3292ad6085552987b8b3b9a7e5805567f4013372d302736b702801acb001ee00"
|
sha256: "5a5c7a5deeef2cc2ffe6076a33b0429f4a20ceac22a397297aed2b1eb067e611"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.1"
|
version: "2.9.0"
|
||||||
flutter_saf:
|
flutter_saf:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "7637b8b67d0a831f3cd7e702b8173e300880d32e"
|
ref: "690a03a954f1603e0149cfd479c8961b88f21336"
|
||||||
resolved-ref: "7637b8b67d0a831f3cd7e702b8173e300880d32e"
|
resolved-ref: "690a03a954f1603e0149cfd479c8961b88f21336"
|
||||||
url: "https://github.com/pkuislm/flutter_saf.git"
|
url: "https://github.com/venera-app/flutter_saf"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -612,8 +612,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "9a784b193af5d55b2a35e58fa390bda3e4f35d00"
|
ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1
|
||||||
resolved-ref: "9a784b193af5d55b2a35e58fa390bda3e4f35d00"
|
resolved-ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1
|
||||||
url: "https://github.com/venera-app/lodepng_flutter"
|
url: "https://github.com/venera-app/lodepng_flutter"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -757,12 +757,11 @@ packages:
|
|||||||
rhttp:
|
rhttp:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: rhttp
|
name: rhttp
|
||||||
ref: HEAD
|
sha256: "037e9b59a68bb4ba664db1cbb4601e878cf5a2fc1cb3d0a9c58e3776609dec4d"
|
||||||
resolved-ref: "18d430cc45fd4f0114885c5235090abf65106257"
|
url: "https://pub.dev"
|
||||||
url: "https://github.com/wgh136/rhttp"
|
source: hosted
|
||||||
source: git
|
version: "0.11.0"
|
||||||
version: "0.10.0"
|
|
||||||
screen_retriever:
|
screen_retriever:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1045,8 +1044,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "285f87f15bccd2d5d5ff443761348c6ee47b98d1"
|
ref: "2f669c98fb81cff1c64fee93466a1475c77e4273"
|
||||||
resolved-ref: "285f87f15bccd2d5d5ff443761348c6ee47b98d1"
|
resolved-ref: "2f669c98fb81cff1c64fee93466a1475c77e4273"
|
||||||
url: "https://github.com/wgh136/webdav_client"
|
url: "https://github.com/wgh136/webdav_client"
|
||||||
source: git
|
source: git
|
||||||
version: "1.2.2"
|
version: "1.2.2"
|
||||||
@@ -1094,10 +1093,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: zip_flutter
|
name: zip_flutter
|
||||||
sha256: bbf3160062610a43901b7ebbc6f6dd46519540f03a84027dc7b1fff399dda1ac
|
sha256: c4d5a34c5803def866bc550926bb16fe89717c9b7304695d5b2ede30964eb8a8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.10"
|
version: "0.0.12"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.0 <4.0.0"
|
dart: ">=3.7.0 <4.0.0"
|
||||||
flutter: ">=3.29.0"
|
flutter: ">=3.29.2"
|
||||||
|
21
pubspec.yaml
21
pubspec.yaml
@@ -2,11 +2,11 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.3.2+132
|
version: 1.3.3+133
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.6.0 <4.0.0'
|
sdk: '>=3.6.0 <4.0.0'
|
||||||
flutter: 3.29.0
|
flutter: 3.29.2
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@@ -19,7 +19,7 @@ dependencies:
|
|||||||
flutter_qjs:
|
flutter_qjs:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/wgh136/flutter_qjs
|
url: https://github.com/wgh136/flutter_qjs
|
||||||
ref: 5978d0c7784fbbefcacc573547f0ab01ba59b7b3
|
ref: 8feae95df7fb00455df129ad7a0dfec1d0e8d8e4
|
||||||
crypto: ^3.0.6
|
crypto: ^3.0.6
|
||||||
dio: ^5.8.0+1
|
dio: ^5.8.0+1
|
||||||
html: ^0.15.5
|
html: ^0.15.5
|
||||||
@@ -52,25 +52,22 @@ dependencies:
|
|||||||
sliver_tools: ^0.2.12
|
sliver_tools: ^0.2.12
|
||||||
flutter_file_dialog: ^3.0.2
|
flutter_file_dialog: ^3.0.2
|
||||||
file_selector: ^1.0.3
|
file_selector: ^1.0.3
|
||||||
zip_flutter: ^0.0.10
|
zip_flutter: ^0.0.12
|
||||||
lodepng_flutter:
|
lodepng_flutter:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/venera-app/lodepng_flutter
|
url: https://github.com/venera-app/lodepng_flutter
|
||||||
ref: 9a784b193af5d55b2a35e58fa390bda3e4f35d00
|
ref: ac7d05dde32e8d728102a9ff66e6b55f05d94ba1
|
||||||
rhttp:
|
rhttp: ^0.11.0
|
||||||
git:
|
|
||||||
url: https://github.com/wgh136/rhttp
|
|
||||||
path: rhttp
|
|
||||||
webdav_client:
|
webdav_client:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/wgh136/webdav_client
|
url: https://github.com/wgh136/webdav_client
|
||||||
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
|
ref: 2f669c98fb81cff1c64fee93466a1475c77e4273
|
||||||
battery_plus: ^6.2.1
|
battery_plus: ^6.2.1
|
||||||
local_auth: ^2.3.0
|
local_auth: ^2.3.0
|
||||||
flutter_saf:
|
flutter_saf:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/pkuislm/flutter_saf.git
|
url: https://github.com/venera-app/flutter_saf
|
||||||
ref: 7637b8b67d0a831f3cd7e702b8173e300880d32e
|
ref: 690a03a954f1603e0149cfd479c8961b88f21336
|
||||||
dynamic_color: ^1.7.0
|
dynamic_color: ^1.7.0
|
||||||
shimmer_animation: ^2.1.0
|
shimmer_animation: ^2.1.0
|
||||||
flutter_memory_info: ^0.0.1
|
flutter_memory_info: ^0.0.1
|
||||||
|
Reference in New Issue
Block a user