mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b6cccb7749 | ||
![]() |
dac07cfac4 | ||
![]() |
da12b3bcca | ||
![]() |
017f964705 | ||
![]() |
bed0f78e81 | ||
![]() |
092eb59c10 | ||
![]() |
a5d3d160c8 | ||
![]() |
d3c3748ce5 | ||
![]() |
586874de15 | ||
bda2c6c2e1 | |||
e9aa6fcf30 | |||
60c6be08c5 | |||
e4e2d264f5 | |||
c2cfd066f6 | |||
d7b91f6a50 | |||
da025b16ff | |||
08e0082186 | |||
463805f5ed | |||
72b146a9bf | |||
1104d28f14 | |||
cf7be85f29 | |||
cab66619df | |||
bdd0724788 | |||
617c452e07 | |||
c8e6e1311c | |||
0bdb1299ca | |||
af9835eb8f | |||
4801457e0e | |||
0c9f7126a2 | |||
3cf9228e2a | |||
07f8cd2455 | |||
659b211038 | |||
4e121748cd | |||
14fe901144 | |||
835b40860d | |||
ef435dcaa5 | |||
e999652a3e | |||
425cbed8a1 | |||
![]() |
488299bcfb | ||
9b821f1b46 | |||
867b2a4b64 | |||
8f07c8a2bb | |||
![]() |
7aed61a65e | ||
674b5c9636 | |||
153f1a9dfe | |||
6c5df47663 | |||
24188b51c0 | |||
070c803f97 | |||
b425eec561 | |||
![]() |
95c98eeaed | ||
60f7b4d3b0 | |||
2ee2a01550 | |||
a2f628001a | |||
de4503a2de | |||
30b2aa2f99 | |||
2f4927f719 | |||
9fb3482474 | |||
2063eee82b | |||
91b765ffba | |||
bbfe87fff2 | |||
![]() |
430b6eeb3a |
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -183,4 +183,4 @@ jobs:
|
||||
outputs/*.deb
|
||||
outputs/*.zst
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.ACTION_GITHUB_TOKEN }}
|
||||
|
@@ -53,6 +53,7 @@
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false"/>
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
|
@@ -246,7 +246,12 @@
|
||||
"New version available": "有新版本可用",
|
||||
"A new version is available. Do you want to update now?" : "有新版本可用。您要现在更新吗?",
|
||||
"No new version available": "没有新版本可用",
|
||||
"Export as pdf": "导出为pdf"
|
||||
"Export as pdf": "导出为pdf",
|
||||
"Export as epub": "导出为epub",
|
||||
"Aggregated Search": "聚合搜索",
|
||||
"No search results found": "未找到搜索结果",
|
||||
"Added @c comics to download queue." : "已添加 @c 本漫画到下载队列",
|
||||
"Download started": "下载已开始"
|
||||
},
|
||||
"zh_TW": {
|
||||
"Home": "首頁",
|
||||
@@ -495,6 +500,11 @@
|
||||
"New version available": "有新版本可用",
|
||||
"A new version is available. Do you want to update now?" : "有新版本可用。您要現在更新嗎?",
|
||||
"No new version available": "沒有新版本可用",
|
||||
"Export as pdf": "匯出為pdf"
|
||||
"Export as pdf": "匯出為pdf",
|
||||
"Export as epub": "匯出為epub",
|
||||
"Aggregated Search": "聚合搜索",
|
||||
"No search results found": "未找到搜索結果",
|
||||
"Added @c comics to download queue." : "已添加 @c 本漫畫到下載隊列",
|
||||
"Download started": "下載已開始"
|
||||
}
|
||||
}
|
@@ -76,7 +76,7 @@ class _AppbarState extends State<Appbar> {
|
||||
var content = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: widget.backgroundColor ??
|
||||
context.colorScheme.surface.withOpacity(0.72),
|
||||
context.colorScheme.surface.toOpacity(0.72),
|
||||
),
|
||||
height: _kAppBarHeight + context.padding.top,
|
||||
child: Row(
|
||||
@@ -189,20 +189,19 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
leading ??
|
||||
(Navigator.of(context).canPop()
|
||||
? Tooltip(
|
||||
message: "Back".tl,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.maybePop(context),
|
||||
),
|
||||
)
|
||||
message: "Back".tl,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Navigator.maybePop(context),
|
||||
),
|
||||
)
|
||||
: const SizedBox()),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: DefaultTextStyle(
|
||||
style:
|
||||
DefaultTextStyle.of(context).style.copyWith(fontSize: 20),
|
||||
style: DefaultTextStyle.of(context).style.copyWith(fontSize: 20),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: title,
|
||||
@@ -215,12 +214,12 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
],
|
||||
).paddingTop(topPadding);
|
||||
|
||||
if(style == AppbarStyle.blur) {
|
||||
if (style == AppbarStyle.blur) {
|
||||
return SizedBox.expand(
|
||||
child: BlurEffect(
|
||||
blur: 15,
|
||||
child: Material(
|
||||
color: context.colorScheme.surface.withOpacity(0.72),
|
||||
color: context.colorScheme.surface.toOpacity(0.72),
|
||||
elevation: 0,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
child: body,
|
||||
@@ -298,12 +297,21 @@ class _FilledTabBarState extends State<FilledTabBar> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
PageStorageBucket get bucket => PageStorage.of(context);
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_controller = widget.controller ?? DefaultTabController.of(context);
|
||||
_controller.animation!.addListener(onTabChanged);
|
||||
initPainter();
|
||||
super.didChangeDependencies();
|
||||
var prevIndex = bucket.readState(context) as int?;
|
||||
if (prevIndex != null &&
|
||||
prevIndex != _controller.index &&
|
||||
prevIndex >= 0 &&
|
||||
prevIndex < widget.tabs.length) {
|
||||
_controller.index = prevIndex;
|
||||
}
|
||||
_controller.animation!.addListener(onTabChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -347,6 +355,7 @@ class _FilledTabBarState extends State<FilledTabBar> {
|
||||
controller: scrollController,
|
||||
builder: (context, controller, physics) {
|
||||
return SingleChildScrollView(
|
||||
key: const PageStorageKey('scroll'),
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.zero,
|
||||
controller: controller,
|
||||
@@ -387,6 +396,7 @@ class _FilledTabBarState extends State<FilledTabBar> {
|
||||
}
|
||||
updateScrollOffset(i);
|
||||
previousIndex = i;
|
||||
bucket.writeState(context, i);
|
||||
}
|
||||
|
||||
void updateScrollOffset(int i) {
|
||||
@@ -724,6 +734,7 @@ class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
editingController.clear();
|
||||
onChanged?.call("");
|
||||
},
|
||||
);
|
||||
},
|
||||
|
@@ -214,7 +214,7 @@ class _ButtonState extends State<Button> {
|
||||
boxShadow: (isHover && !isLoading && (widget.type == ButtonType.filled || widget.type == ButtonType.normal))
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: Colors.black.toOpacity(0.1),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
)
|
||||
@@ -248,7 +248,7 @@ class _ButtonState extends State<Button> {
|
||||
if (widget.type == ButtonType.filled) {
|
||||
var color = widget.color ?? context.colorScheme.primary;
|
||||
if (isHover) {
|
||||
return color.withOpacity(0.9);
|
||||
return color.toOpacity(0.9);
|
||||
} else {
|
||||
return color;
|
||||
}
|
||||
@@ -256,13 +256,13 @@ class _ButtonState extends State<Button> {
|
||||
if (widget.type == ButtonType.normal) {
|
||||
var color = widget.color ?? context.colorScheme.surfaceContainer;
|
||||
if (isHover) {
|
||||
return color.withOpacity(0.9);
|
||||
return color.toOpacity(0.9);
|
||||
} else {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
if (isHover) {
|
||||
return context.colorScheme.outline.withOpacity(0.2);
|
||||
return context.colorScheme.outline.toOpacity(0.2);
|
||||
}
|
||||
return Colors.transparent;
|
||||
}
|
||||
@@ -345,7 +345,7 @@ class _IconButtonState extends State<_IconButton> {
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.outlineVariant
|
||||
.withOpacity(0.4)
|
||||
.toOpacity(0.4)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular((iconSize + 12) / 2),
|
||||
),
|
||||
|
@@ -43,7 +43,7 @@ class ComicTile extends StatelessWidget {
|
||||
var renderBox = context.findRenderObject() as RenderBox;
|
||||
var size = renderBox.size;
|
||||
var location = renderBox.localToGlobal(
|
||||
Offset(size.width / 2, size.height / 2),
|
||||
Offset((size.width - 242) / 2, size.height / 2),
|
||||
);
|
||||
showMenu(location, context);
|
||||
}
|
||||
@@ -144,7 +144,7 @@ class ComicTile extends StatelessWidget {
|
||||
if (history != null)
|
||||
Container(
|
||||
height: 24,
|
||||
color: Colors.blue.withOpacity(0.9),
|
||||
color: Colors.blue.toOpacity(0.9),
|
||||
constraints: const BoxConstraints(minWidth: 24),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: CustomPaint(
|
||||
@@ -293,7 +293,7 @@ class ComicTile extends StatelessWidget {
|
||||
Radius.circular(10.0),
|
||||
),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color: Colors.black.toOpacity(0.5),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.fromLTRB(8, 6, 8, 6),
|
||||
@@ -475,7 +475,7 @@ class _ComicDescription extends StatelessWidget {
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 10.0,
|
||||
color: context.colorScheme.onSurface.withOpacity(0.7)),
|
||||
color: context.colorScheme.onSurface.toOpacity(0.7)),
|
||||
maxLines: 1,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -780,7 +780,7 @@ class _SliverGridComics extends StatelessWidget {
|
||||
duration: const Duration(milliseconds: 150),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.72)
|
||||
? Theme.of(context).colorScheme.secondaryContainer.toOpacity(0.72)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
@@ -832,6 +832,7 @@ class ComicList extends StatefulWidget {
|
||||
this.errorLeading,
|
||||
this.menuBuilder,
|
||||
this.controller,
|
||||
this.refreshHandlerCallback,
|
||||
});
|
||||
|
||||
final Future<Res<List<Comic>>> Function(int page)? loadPage;
|
||||
@@ -848,6 +849,8 @@ class ComicList extends StatefulWidget {
|
||||
|
||||
final ScrollController? controller;
|
||||
|
||||
final void Function(VoidCallback c)? refreshHandlerCallback;
|
||||
|
||||
@override
|
||||
State<ComicList> createState() => ComicListState();
|
||||
}
|
||||
@@ -865,6 +868,51 @@ class ComicListState extends State<ComicList> {
|
||||
|
||||
String? _nextUrl;
|
||||
|
||||
Map<String, dynamic> get state => {
|
||||
'maxPage': _maxPage,
|
||||
'data': _data,
|
||||
'page': _page,
|
||||
'error': _error,
|
||||
'loading': _loading,
|
||||
'nextUrl': _nextUrl,
|
||||
};
|
||||
|
||||
void restoreState(Map<String, dynamic>? state) {
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
_maxPage = state['maxPage'];
|
||||
_data.clear();
|
||||
_data.addAll(state['data']);
|
||||
_page = state['page'];
|
||||
_error = state['error'];
|
||||
_loading.clear();
|
||||
_loading.addAll(state['loading']);
|
||||
_nextUrl = state['nextUrl'];
|
||||
}
|
||||
|
||||
void storeState() {
|
||||
PageStorage.of(context).writeState(context, state);
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
_data.clear();
|
||||
_page = 1;
|
||||
_maxPage = null;
|
||||
_error = null;
|
||||
_nextUrl = null;
|
||||
_loading.clear();
|
||||
storeState();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
restoreState(PageStorage.of(context).readState(context));
|
||||
widget.refreshHandlerCallback?.call(refresh);
|
||||
}
|
||||
|
||||
void remove(Comic c) {
|
||||
if (_data[_page] == null || !_data[_page]!.remove(c)) {
|
||||
for (var page in _data.values) {
|
||||
@@ -1012,15 +1060,20 @@ class ComicListState extends State<ComicList> {
|
||||
while (_data[page] == null) {
|
||||
await _fetchNext();
|
||||
}
|
||||
setState(() {});
|
||||
if(mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
});
|
||||
if(mounted) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_loading[page] = false;
|
||||
storeState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1069,6 +1122,7 @@ class ComicListState extends State<ComicList> {
|
||||
);
|
||||
}
|
||||
return SmoothCustomScrollView(
|
||||
key: const PageStorageKey('scroll'),
|
||||
controller: widget.controller,
|
||||
slivers: [
|
||||
if (widget.leadingSliver != null) widget.leadingSliver!,
|
||||
|
@@ -1,5 +1,3 @@
|
||||
library components;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
|
||||
/// patched slider.dart with RtL support
|
||||
class _SliderDefaultsM3 extends SliderThemeData {
|
||||
@@ -15,45 +16,45 @@ class _SliderDefaultsM3 extends SliderThemeData {
|
||||
Color? get inactiveTrackColor => _colors.surfaceContainerHighest;
|
||||
|
||||
@override
|
||||
Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);
|
||||
Color? get secondaryActiveTrackColor => _colors.primary.toOpacity(0.54);
|
||||
|
||||
@override
|
||||
Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38);
|
||||
Color? get disabledActiveTrackColor => _colors.onSurface.toOpacity(0.38);
|
||||
|
||||
@override
|
||||
Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12);
|
||||
Color? get disabledInactiveTrackColor => _colors.onSurface.toOpacity(0.12);
|
||||
|
||||
@override
|
||||
Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.12);
|
||||
Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.toOpacity(0.12);
|
||||
|
||||
@override
|
||||
Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(0.38);
|
||||
Color? get activeTickMarkColor => _colors.onPrimary.toOpacity(0.38);
|
||||
|
||||
@override
|
||||
Color? get inactiveTickMarkColor => _colors.onSurfaceVariant.withOpacity(0.38);
|
||||
Color? get inactiveTickMarkColor => _colors.onSurfaceVariant.toOpacity(0.38);
|
||||
|
||||
@override
|
||||
Color? get disabledActiveTickMarkColor => _colors.onSurface.withOpacity(0.38);
|
||||
Color? get disabledActiveTickMarkColor => _colors.onSurface.toOpacity(0.38);
|
||||
|
||||
@override
|
||||
Color? get disabledInactiveTickMarkColor => _colors.onSurface.withOpacity(0.38);
|
||||
Color? get disabledInactiveTickMarkColor => _colors.onSurface.toOpacity(0.38);
|
||||
|
||||
@override
|
||||
Color? get thumbColor => _colors.primary;
|
||||
|
||||
@override
|
||||
Color? get disabledThumbColor => Color.alphaBlend(_colors.onSurface.withOpacity(0.38), _colors.surface);
|
||||
Color? get disabledThumbColor => Color.alphaBlend(_colors.onSurface.toOpacity(0.38), _colors.surface);
|
||||
|
||||
@override
|
||||
Color? get overlayColor => WidgetStateColor.resolveWith((Set<WidgetState> states) {
|
||||
if (states.contains(WidgetState.dragged)) {
|
||||
return _colors.primary.withOpacity(0.1);
|
||||
return _colors.primary.toOpacity(0.1);
|
||||
}
|
||||
if (states.contains(WidgetState.hovered)) {
|
||||
return _colors.primary.withOpacity(0.08);
|
||||
return _colors.primary.toOpacity(0.08);
|
||||
}
|
||||
if (states.contains(WidgetState.focused)) {
|
||||
return _colors.primary.withOpacity(0.1);
|
||||
return _colors.primary.toOpacity(0.1);
|
||||
}
|
||||
|
||||
return Colors.transparent;
|
||||
|
@@ -141,7 +141,7 @@ class FlyoutState extends State<Flyout> {
|
||||
animation: animation,
|
||||
builder: (context, builder) {
|
||||
return ColoredBox(
|
||||
color: Colors.black.withOpacity(0.3 * animation.value),
|
||||
color: Colors.black.toOpacity(0.3 * animation.value),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -185,12 +185,18 @@ class FlyoutContent extends StatelessWidget {
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
type: MaterialType.card,
|
||||
color: context.colorScheme.surface.withOpacity(0.82),
|
||||
color: context.colorScheme.surface.toOpacity(0.82),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: minFlyoutWidth,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: context.brightness == ui.Brightness.dark
|
||||
? Border.all(color: context.colorScheme.outlineVariant)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -215,108 +221,3 @@ class FlyoutContent extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FlyoutTextButton extends StatefulWidget {
|
||||
const FlyoutTextButton(
|
||||
{super.key,
|
||||
required this.child,
|
||||
required this.flyoutBuilder,
|
||||
this.navigator});
|
||||
|
||||
final Widget child;
|
||||
|
||||
final WidgetBuilder flyoutBuilder;
|
||||
|
||||
final NavigatorState? navigator;
|
||||
|
||||
@override
|
||||
State<FlyoutTextButton> createState() => _FlyoutTextButtonState();
|
||||
}
|
||||
|
||||
class _FlyoutTextButtonState extends State<FlyoutTextButton> {
|
||||
final FlyoutController _controller = FlyoutController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Flyout(
|
||||
controller: _controller,
|
||||
flyoutBuilder: widget.flyoutBuilder,
|
||||
navigator: widget.navigator,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
_controller.show();
|
||||
},
|
||||
child: widget.child,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class FlyoutIconButton extends StatefulWidget {
|
||||
const FlyoutIconButton(
|
||||
{super.key,
|
||||
required this.icon,
|
||||
required this.flyoutBuilder,
|
||||
this.navigator});
|
||||
|
||||
final Widget icon;
|
||||
|
||||
final WidgetBuilder flyoutBuilder;
|
||||
|
||||
final NavigatorState? navigator;
|
||||
|
||||
@override
|
||||
State<FlyoutIconButton> createState() => _FlyoutIconButtonState();
|
||||
}
|
||||
|
||||
class _FlyoutIconButtonState extends State<FlyoutIconButton> {
|
||||
final FlyoutController _controller = FlyoutController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Flyout(
|
||||
controller: _controller,
|
||||
flyoutBuilder: widget.flyoutBuilder,
|
||||
navigator: widget.navigator,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
_controller.show();
|
||||
},
|
||||
icon: widget.icon,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class FlyoutFilledButton extends StatefulWidget {
|
||||
const FlyoutFilledButton(
|
||||
{super.key,
|
||||
required this.child,
|
||||
required this.flyoutBuilder,
|
||||
this.navigator});
|
||||
|
||||
final Widget child;
|
||||
|
||||
final WidgetBuilder flyoutBuilder;
|
||||
|
||||
final NavigatorState? navigator;
|
||||
|
||||
@override
|
||||
State<FlyoutFilledButton> createState() => _FlyoutFilledButtonState();
|
||||
}
|
||||
|
||||
class _FlyoutFilledButtonState extends State<FlyoutFilledButton> {
|
||||
final FlyoutController _controller = FlyoutController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Flyout(
|
||||
controller: _controller,
|
||||
flyoutBuilder: widget.flyoutBuilder,
|
||||
navigator: widget.navigator,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
_controller.show();
|
||||
},
|
||||
child: widget.child,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,8 @@
|
||||
part of 'components.dart';
|
||||
|
||||
class MouseBackDetector extends StatelessWidget {
|
||||
const MouseBackDetector({super.key, required this.onTapDown, required this.child});
|
||||
const MouseBackDetector(
|
||||
{super.key, required this.onTapDown, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@@ -20,3 +21,45 @@ class MouseBackDetector extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedTapRegion extends StatefulWidget {
|
||||
const AnimatedTapRegion({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onTap,
|
||||
this.borderRadius = 0,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
|
||||
final void Function() onTap;
|
||||
|
||||
final double borderRadius;
|
||||
|
||||
@override
|
||||
State<AnimatedTapRegion> createState() => _AnimatedTapRegionState();
|
||||
}
|
||||
|
||||
class _AnimatedTapRegionState extends State<AnimatedTapRegion> {
|
||||
bool isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => isHovered = true),
|
||||
onExit: (_) => setState(() => isHovered = false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: AnimatedScale(
|
||||
duration: _fastAnimationDuration,
|
||||
scale: isHovered ? 1.1 : 1,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -96,6 +96,20 @@ class ListLoadingIndicator extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class SliverListLoadingIndicator extends StatelessWidget {
|
||||
const SliverListLoadingIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// SliverToBoxAdapter can not been lazy loaded.
|
||||
// Use SliverList to make sure the animation can be lazy loaded.
|
||||
return SliverList.list(children: const [
|
||||
SizedBox(),
|
||||
ListLoadingIndicator(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class LoadingState<T extends StatefulWidget, S extends Object>
|
||||
extends State<T> {
|
||||
bool isLoading = false;
|
||||
@@ -299,9 +313,7 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
|
||||
|
||||
Widget buildLoading(BuildContext context) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
).fixWidth(32).fixHeight(32),
|
||||
child: const CircularProgressIndicator().fixWidth(32).fixHeight(32),
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -20,6 +20,8 @@ class _MenuRoute<T> extends PopupRoute<T> {
|
||||
@override
|
||||
String? get barrierLabel => "menu";
|
||||
|
||||
double get entryHeight => App.isMobile ? 42 : 36;
|
||||
|
||||
@override
|
||||
Widget buildPage(BuildContext context, Animation<double> animation,
|
||||
Animation<double> secondaryAnimation) {
|
||||
@@ -30,7 +32,7 @@ class _MenuRoute<T> extends PopupRoute<T> {
|
||||
left = size.width - width - 10;
|
||||
}
|
||||
var top = location.dy;
|
||||
var height = 16 + 32 * entries.length;
|
||||
var height = 16 + entryHeight * entries.length;
|
||||
if (top + height > size.height - 15) {
|
||||
top = size.height - height - 15;
|
||||
}
|
||||
@@ -42,9 +44,12 @@ class _MenuRoute<T> extends PopupRoute<T> {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: context.brightness == Brightness.dark
|
||||
? Border.all(color: context.colorScheme.outlineVariant)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: context.colorScheme.shadow.withOpacity(0.2),
|
||||
color: context.colorScheme.shadow.toOpacity(0.2),
|
||||
blurRadius: 8,
|
||||
blurStyle: BlurStyle.outer,
|
||||
),
|
||||
@@ -53,9 +58,7 @@ class _MenuRoute<T> extends PopupRoute<T> {
|
||||
child: BlurEffect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Material(
|
||||
color: context.brightness == Brightness.light
|
||||
? const Color(0xFFFAFAFA).withOpacity(0.82)
|
||||
: const Color(0xFF090909).withOpacity(0.82),
|
||||
color: context.colorScheme.surface.toOpacity(0.78),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
width: width,
|
||||
@@ -83,7 +86,7 @@ class _MenuRoute<T> extends PopupRoute<T> {
|
||||
entry.onClick();
|
||||
},
|
||||
child: SizedBox(
|
||||
height: App.isMobile ? 42 : 36,
|
||||
height: entryHeight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
|
@@ -46,21 +46,28 @@ class _ToastOverlay extends StatelessWidget {
|
||||
child: IconTheme(
|
||||
data: IconThemeData(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) icon!.paddingRight(8),
|
||||
Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w500),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (trailing != null) trailing!.paddingLeft(8)
|
||||
],
|
||||
child: IntrinsicWidth(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: context.width - 32,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) icon!.paddingRight(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w500),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing!.paddingLeft(8)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -220,7 +227,7 @@ LoadingDialogController showLoadingDialog(BuildContext context,
|
||||
);
|
||||
});
|
||||
|
||||
var navigator = Navigator.of(context);
|
||||
var navigator = Navigator.of(context, rootNavigator: true);
|
||||
|
||||
navigator.push(loadingDialogRoute).then((value) => controller.closed = true);
|
||||
|
||||
|
@@ -23,14 +23,15 @@ class PaneActionEntry {
|
||||
}
|
||||
|
||||
class NaviPane extends StatefulWidget {
|
||||
const NaviPane({required this.paneItems,
|
||||
required this.paneActions,
|
||||
required this.pageBuilder,
|
||||
this.initialPage = 0,
|
||||
this.onPageChanged,
|
||||
required this.observer,
|
||||
required this.navigatorKey,
|
||||
super.key});
|
||||
const NaviPane(
|
||||
{required this.paneItems,
|
||||
required this.paneActions,
|
||||
required this.pageBuilder,
|
||||
this.initialPage = 0,
|
||||
this.onPageChanged,
|
||||
required this.observer,
|
||||
required this.navigatorKey,
|
||||
super.key});
|
||||
|
||||
final List<PaneItemEntry> paneItems;
|
||||
|
||||
@@ -84,17 +85,14 @@ class NaviPaneState extends State<NaviPane>
|
||||
|
||||
static const _kBottomBarHeight = 58.0;
|
||||
|
||||
static const _kFoldedSideBarWidth = 80.0;
|
||||
static const _kFoldedSideBarWidth = 72.0;
|
||||
|
||||
static const _kSideBarWidth = 256.0;
|
||||
static const _kSideBarWidth = 224.0;
|
||||
|
||||
static const _kTopBarHeight = 48.0;
|
||||
|
||||
double get bottomBarHeight =>
|
||||
_kBottomBarHeight + MediaQuery
|
||||
.of(context)
|
||||
.padding
|
||||
.bottom;
|
||||
_kBottomBarHeight + MediaQuery.of(context).padding.bottom;
|
||||
|
||||
void onNavigatorStateChange() {
|
||||
onRebuild(context);
|
||||
@@ -136,10 +134,7 @@ class NaviPaneState extends State<NaviPane>
|
||||
}
|
||||
|
||||
double targetFormContext(BuildContext context) {
|
||||
var width = MediaQuery
|
||||
.of(context)
|
||||
.size
|
||||
.width;
|
||||
var width = MediaQuery.of(context).size.width;
|
||||
double target = 0;
|
||||
if (width > changePoint) {
|
||||
target = 2;
|
||||
@@ -208,14 +203,13 @@ class NaviPaneState extends State<NaviPane>
|
||||
return Navigator(
|
||||
observers: [widget.observer],
|
||||
key: widget.navigatorKey,
|
||||
onGenerateRoute: (settings) =>
|
||||
AppPageRoute(
|
||||
preventRebuild: false,
|
||||
isRootRoute: true,
|
||||
builder: (context) {
|
||||
return _NaviMainView(state: this);
|
||||
},
|
||||
),
|
||||
onGenerateRoute: (settings) => AppPageRoute(
|
||||
preventRebuild: false,
|
||||
isRootRoute: true,
|
||||
builder: (context) {
|
||||
return _NaviMainView(state: this);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -252,20 +246,14 @@ class NaviPaneState extends State<NaviPane>
|
||||
|
||||
Widget buildBottom() {
|
||||
return Material(
|
||||
textStyle: Theme
|
||||
.of(context)
|
||||
.textTheme
|
||||
.labelSmall,
|
||||
textStyle: Theme.of(context).textTheme.labelSmall,
|
||||
elevation: 0,
|
||||
child: Container(
|
||||
height: _kBottomBarHeight,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.outlineVariant,
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -273,7 +261,7 @@ class NaviPaneState extends State<NaviPane>
|
||||
child: Row(
|
||||
children: List<Widget>.generate(
|
||||
widget.paneItems.length,
|
||||
(index) {
|
||||
(index) {
|
||||
return Expanded(
|
||||
child: _SingleBottomNaviWidget(
|
||||
enabled: currentPage == index,
|
||||
@@ -293,7 +281,7 @@ class NaviPaneState extends State<NaviPane>
|
||||
|
||||
Widget buildLeft() {
|
||||
final value = controller.value;
|
||||
const paddingHorizontal = 16.0;
|
||||
const paddingHorizontal = 12.0;
|
||||
return Material(
|
||||
child: Container(
|
||||
width: _kFoldedSideBarWidth +
|
||||
@@ -303,57 +291,39 @@ class NaviPaneState extends State<NaviPane>
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.outlineVariant,
|
||||
width: 1,
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: value == 3
|
||||
? (_kSideBarWidth - paddingHorizontal * 2 - 1)
|
||||
: (_kFoldedSideBarWidth - paddingHorizontal * 2 - 1),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: MediaQuery
|
||||
.of(context)
|
||||
.padding
|
||||
.top),
|
||||
...List<Widget>.generate(
|
||||
widget.paneItems.length,
|
||||
(index) =>
|
||||
_SideNaviWidget(
|
||||
enabled: currentPage == index,
|
||||
entry: widget.paneItems[index],
|
||||
showTitle: value == 3,
|
||||
onTap: () {
|
||||
updatePage(index);
|
||||
},
|
||||
key: ValueKey(index),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
...List<Widget>.generate(
|
||||
widget.paneActions.length,
|
||||
(index) =>
|
||||
_PaneActionWidget(
|
||||
entry: widget.paneActions[index],
|
||||
showTitle: value == 3,
|
||||
key: ValueKey(index + widget.paneItems.length),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
)
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: MediaQuery.of(context).padding.top),
|
||||
...List<Widget>.generate(
|
||||
widget.paneItems.length,
|
||||
(index) => _SideNaviWidget(
|
||||
enabled: currentPage == index,
|
||||
entry: widget.paneItems[index],
|
||||
showTitle: value == 3,
|
||||
onTap: () {
|
||||
updatePage(index);
|
||||
},
|
||||
key: ValueKey(index),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
...List<Widget>.generate(
|
||||
widget.paneActions.length,
|
||||
(index) => _PaneActionWidget(
|
||||
entry: widget.paneActions[index],
|
||||
showTitle: value == 3,
|
||||
key: ValueKey(index + widget.paneItems.length),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -361,12 +331,13 @@ class NaviPaneState extends State<NaviPane>
|
||||
}
|
||||
}
|
||||
|
||||
class _SideNaviWidget extends StatefulWidget {
|
||||
const _SideNaviWidget({required this.enabled,
|
||||
required this.entry,
|
||||
required this.onTap,
|
||||
required this.showTitle,
|
||||
super.key});
|
||||
class _SideNaviWidget extends StatelessWidget {
|
||||
const _SideNaviWidget(
|
||||
{required this.enabled,
|
||||
required this.entry,
|
||||
required this.onTap,
|
||||
required this.showTitle,
|
||||
super.key});
|
||||
|
||||
final bool enabled;
|
||||
|
||||
@@ -376,60 +347,37 @@ class _SideNaviWidget extends StatefulWidget {
|
||||
|
||||
final bool showTitle;
|
||||
|
||||
@override
|
||||
State<_SideNaviWidget> createState() => _SideNaviWidgetState();
|
||||
}
|
||||
|
||||
class _SideNaviWidgetState extends State<_SideNaviWidget> {
|
||||
bool isHovering = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme
|
||||
.of(context)
|
||||
.colorScheme;
|
||||
final icon =
|
||||
Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon);
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (details) => setState(() => isHovering = true),
|
||||
onExit: (details) => setState(() => isHovering = false),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: widget.onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
width: double.infinity,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.enabled
|
||||
? colorScheme.primaryContainer
|
||||
: isHovering
|
||||
? colorScheme.surfaceContainerHigh
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: widget.showTitle
|
||||
? Row(
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Text(widget.entry.label)
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: icon,
|
||||
)),
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final icon = Icon(enabled ? entry.activeIcon : entry.icon);
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
color: enabled ? colorScheme.primaryContainer : null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: showTitle ? Row(
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 12),
|
||||
Text(entry.label)
|
||||
],
|
||||
) : Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
);
|
||||
).paddingVertical(4);
|
||||
}
|
||||
}
|
||||
|
||||
class _PaneActionWidget extends StatefulWidget {
|
||||
class _PaneActionWidget extends StatelessWidget {
|
||||
const _PaneActionWidget(
|
||||
{required this.entry, required this.showTitle, super.key});
|
||||
|
||||
@@ -437,58 +385,37 @@ class _PaneActionWidget extends StatefulWidget {
|
||||
|
||||
final bool showTitle;
|
||||
|
||||
@override
|
||||
State<_PaneActionWidget> createState() => _PaneActionWidgetState();
|
||||
}
|
||||
|
||||
class _PaneActionWidgetState extends State<_PaneActionWidget> {
|
||||
bool isHovering = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme
|
||||
.of(context)
|
||||
.colorScheme;
|
||||
final icon = Icon(widget.entry.icon);
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (details) => setState(() => isHovering = true),
|
||||
onExit: (details) => setState(() => isHovering = false),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: widget.entry.onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
width: double.infinity,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: isHovering ? colorScheme.surfaceContainerHigh : null,
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: widget.showTitle
|
||||
? Row(
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
),
|
||||
Text(widget.entry.label)
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: icon,
|
||||
)),
|
||||
final icon = Icon(entry.icon);
|
||||
return InkWell(
|
||||
onTap: entry.onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
height: 38,
|
||||
child: showTitle ? Row(
|
||||
children: [
|
||||
icon,
|
||||
const SizedBox(width: 12),
|
||||
Text(entry.label)
|
||||
],
|
||||
) : Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
);
|
||||
).paddingVertical(4);
|
||||
}
|
||||
}
|
||||
|
||||
class _SingleBottomNaviWidget extends StatefulWidget {
|
||||
const _SingleBottomNaviWidget({required this.enabled,
|
||||
required this.entry,
|
||||
required this.onTap,
|
||||
super.key});
|
||||
const _SingleBottomNaviWidget(
|
||||
{required this.enabled,
|
||||
required this.entry,
|
||||
required this.onTap,
|
||||
super.key});
|
||||
|
||||
final bool enabled;
|
||||
|
||||
@@ -556,11 +483,9 @@ class _SingleBottomNaviWidgetState extends State<_SingleBottomNaviWidget>
|
||||
|
||||
Widget buildContent() {
|
||||
final value = controller.value;
|
||||
final colorScheme = Theme
|
||||
.of(context)
|
||||
.colorScheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final icon =
|
||||
Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon);
|
||||
Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon);
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 64,
|
||||
@@ -661,12 +586,12 @@ class _NaviPopScope extends StatelessWidget {
|
||||
Widget res = App.isIOS
|
||||
? child
|
||||
: PopScope(
|
||||
canPop: App.isAndroid ? false : true,
|
||||
onPopInvokedWithResult: (value, result) {
|
||||
action();
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
canPop: App.isAndroid ? false : true,
|
||||
onPopInvokedWithResult: (value, result) {
|
||||
action();
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
if (popGesture) {
|
||||
res = GestureDetector(
|
||||
onPanStart: (details) {
|
||||
@@ -725,8 +650,8 @@ class _NaviMainViewState extends State<_NaviMainView> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (shouldShowAppBar) state.buildBottom().paddingBottom(
|
||||
context.padding.bottom),
|
||||
if (shouldShowAppBar)
|
||||
state.buildBottom().paddingBottom(context.padding.bottom),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@@ -78,6 +78,9 @@ class _SmoothScrollProviderState extends State<SmoothScrollProvider> {
|
||||
},
|
||||
onPointerSignal: (pointerSignal) {
|
||||
if (pointerSignal is PointerScrollEvent) {
|
||||
if (HardwareKeyboard.instance.isShiftPressed) {
|
||||
return;
|
||||
}
|
||||
if (pointerSignal.kind == PointerDeviceKind.mouse &&
|
||||
!_isMouseScroll) {
|
||||
setState(() {
|
||||
|
@@ -267,13 +267,14 @@ class OptionChip extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
return AnimatedContainer(
|
||||
duration: _fastAnimationDuration,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? context.colorScheme.primaryContainer
|
||||
? context.colorScheme.secondaryContainer
|
||||
: context.colorScheme.surface,
|
||||
border: isSelected
|
||||
? Border.all(color: context.colorScheme.primaryContainer)
|
||||
? Border.all(color: context.colorScheme.secondaryContainer)
|
||||
: Border.all(color: context.colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
|
@@ -563,7 +563,7 @@ class _VirtualWindowFrameState extends State<VirtualWindowFrame>
|
||||
boxShadow: <BoxShadow>[
|
||||
if (!_isMaximized && !_isFullScreen)
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: Colors.black.toOpacity(0.1),
|
||||
offset: Offset(0.0, _isFocused ? 4 : 2),
|
||||
blurRadius: 6,
|
||||
)
|
||||
|
@@ -10,7 +10,7 @@ export "widget_utils.dart";
|
||||
export "context.dart";
|
||||
|
||||
class _App {
|
||||
final version = "1.0.8";
|
||||
final version = "1.1.1";
|
||||
|
||||
bool get isAndroid => Platform.isAndroid;
|
||||
|
||||
@@ -63,22 +63,9 @@ class _App {
|
||||
}
|
||||
}
|
||||
|
||||
var mainColor = Colors.blue;
|
||||
|
||||
Future<void> init() async {
|
||||
cachePath = (await getApplicationCacheDirectory()).path;
|
||||
dataPath = (await getApplicationSupportDirectory()).path;
|
||||
mainColor = switch (appdata.settings['color']) {
|
||||
'red' => Colors.red,
|
||||
'pink' => Colors.pink,
|
||||
'purple' => Colors.purple,
|
||||
'green' => Colors.green,
|
||||
'orange' => Colors.orange,
|
||||
'blue' => Colors.blue,
|
||||
'yellow' => Colors.yellow,
|
||||
'cyan' => Colors.cyan,
|
||||
_ => Colors.blue,
|
||||
};
|
||||
}
|
||||
|
||||
Function? _forceRebuildHandler;
|
||||
|
@@ -92,7 +92,7 @@ class _Settings with ChangeNotifier {
|
||||
final _data = <String, dynamic>{
|
||||
'comicDisplayMode': 'detailed', // detailed, brief
|
||||
'comicTileScale': 1.00, // 0.75-1.25
|
||||
'color': 'blue', // red, pink, purple, green, orange, blue
|
||||
'color': 'system', // red, pink, purple, green, orange, blue
|
||||
'theme_mode': 'system', // light, dark, system
|
||||
'newFavoriteAddTo': 'end', // start, end
|
||||
'moveFavoriteAfterRead': 'none', // none, end, start
|
||||
|
@@ -1,4 +1,4 @@
|
||||
part of comic_source;
|
||||
part of 'comic_source.dart';
|
||||
|
||||
class CategoryData {
|
||||
/// The title is displayed in the tab bar.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
library comic_source;
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
@@ -198,9 +198,7 @@ class ComicDetails with HistoryMixin {
|
||||
maxPage = json["maxPage"],
|
||||
comments = (json["comments"] as List?)
|
||||
?.map((e) => Comment.fromJson(e))
|
||||
.toList(){
|
||||
print(json);
|
||||
}
|
||||
.toList();
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
|
@@ -90,11 +90,10 @@ class ComicSourceParser {
|
||||
var className = line1.split("class")[1].split("extends ComicSource").first;
|
||||
className = className.trim();
|
||||
JsEngine().runCode("""
|
||||
(() => {
|
||||
$js
|
||||
(() => { $js
|
||||
this['temp'] = new $className()
|
||||
}).call()
|
||||
""");
|
||||
""", className);
|
||||
_name = JsEngine().runCode("this['temp'].name") ??
|
||||
(throw ComicSourceParseException('name is required'));
|
||||
var key = JsEngine().runCode("this['temp'].key") ??
|
||||
|
@@ -6,6 +6,7 @@ import 'package:venera/foundation/appdata.dart';
|
||||
import 'package:venera/foundation/image_provider/local_favorite_image.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/foundation/log.dart';
|
||||
import 'package:venera/utils/tags_translation.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'app.dart';
|
||||
@@ -177,6 +178,28 @@ class LocalFavoritesManager with ChangeNotifier {
|
||||
source_folder text
|
||||
);
|
||||
""");
|
||||
for (var folder in _getFolderNamesWithDB()) {
|
||||
var columns = _db.select("""
|
||||
pragma table_info("$folder");
|
||||
""");
|
||||
if (!columns.any((element) => element["name"] == "translated_tags")) {
|
||||
_db.execute("""
|
||||
alter table "$folder"
|
||||
add column translated_tags TEXT;
|
||||
""");
|
||||
var comics = getAllComics(folder);
|
||||
for (var comic in comics) {
|
||||
var translatedTags = _translateTags(comic.tags);
|
||||
_db.execute("""
|
||||
update "$folder"
|
||||
set translated_tags = ?
|
||||
where id == ? and type == ?;
|
||||
""", [translatedTags, comic.id, comic.type.value]);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<String> find(String id, ComicType type) {
|
||||
@@ -338,6 +361,7 @@ class LocalFavoritesManager with ChangeNotifier {
|
||||
cover_path TEXT,
|
||||
time TEXT,
|
||||
display_order int,
|
||||
translated_tags TEXT,
|
||||
primary key (id, type)
|
||||
);
|
||||
""");
|
||||
@@ -391,6 +415,17 @@ class LocalFavoritesManager with ChangeNotifier {
|
||||
return FavoriteItem.fromRow(res.first);
|
||||
}
|
||||
|
||||
String _translateTags(List<String> tags) {
|
||||
var res = <String>[];
|
||||
for (var tag in tags) {
|
||||
var translated = tag.translateTagsToCN;
|
||||
if (translated != tag) {
|
||||
res.add(translated);
|
||||
}
|
||||
}
|
||||
return res.join(",");
|
||||
}
|
||||
|
||||
/// add comic to a folder.
|
||||
/// return true if success, false if already exists
|
||||
bool addComic(String folder, FavoriteItem comic, [int? order]) {
|
||||
@@ -405,6 +440,7 @@ class LocalFavoritesManager with ChangeNotifier {
|
||||
if (res.isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
var translatedTags = _translateTags(comic.tags);
|
||||
final params = [
|
||||
comic.id,
|
||||
comic.name,
|
||||
@@ -412,22 +448,23 @@ class LocalFavoritesManager with ChangeNotifier {
|
||||
comic.type.value,
|
||||
comic.tags.join(","),
|
||||
comic.coverPath,
|
||||
comic.time
|
||||
comic.time,
|
||||
translatedTags
|
||||
];
|
||||
if (order != null) {
|
||||
_db.execute("""
|
||||
insert into "$folder" (id, name, author, type, tags, cover_path, time, display_order)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
insert into "$folder" (id, name, author, type, tags, cover_path, time, translated_tags, display_order)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
""", [...params, order]);
|
||||
} else if (appdata.settings['newFavoriteAddTo'] == "end") {
|
||||
_db.execute("""
|
||||
insert into "$folder" (id, name, author, type, tags, cover_path, time, display_order)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
insert into "$folder" (id, name, author, type, tags, cover_path, time, translated_tags, display_order)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
""", [...params, maxValue(folder) + 1]);
|
||||
} else {
|
||||
_db.execute("""
|
||||
insert into "$folder" (id, name, author, type, tags, cover_path, time, display_order)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
insert into "$folder" (id, name, author, type, tags, cover_path, time, translated_tags, display_order)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
""", [...params, minValue(folder) - 1]);
|
||||
}
|
||||
notifyListeners();
|
||||
@@ -501,10 +538,11 @@ class LocalFavoritesManager with ChangeNotifier {
|
||||
int count = 0;
|
||||
await Future.microtask(() {
|
||||
var all = allComics();
|
||||
for(var c in all) {
|
||||
for (var c in all) {
|
||||
var comicSource = c.type.comicSource;
|
||||
if ((c.type == ComicType.local && LocalManager().find(c.id, c.type) == null)
|
||||
|| (c.type != ComicType.local && comicSource == null)) {
|
||||
if ((c.type == ComicType.local &&
|
||||
LocalManager().find(c.id, c.type) == null) ||
|
||||
(c.type != ComicType.local && comicSource == null)) {
|
||||
deleteComicWithId(c.folder, c.id, c.type);
|
||||
count++;
|
||||
}
|
||||
@@ -593,6 +631,33 @@ class LocalFavoritesManager with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<FavoriteItem> searchInFolder(String folder, String keyword) {
|
||||
var keywordList = keyword.split(" ");
|
||||
keyword = keywordList.first;
|
||||
keyword = "%$keyword%";
|
||||
var res = _db.select("""
|
||||
SELECT * FROM "$folder"
|
||||
WHERE name LIKE ? OR author LIKE ? OR tags LIKE ? OR translated_tags LIKE ?;
|
||||
""", [keyword, keyword, keyword, keyword]);
|
||||
var comics = res.map((e) => FavoriteItem.fromRow(e)).toList();
|
||||
bool test(FavoriteItem comic, String keyword) {
|
||||
if (comic.name.contains(keyword)) {
|
||||
return true;
|
||||
} else if (comic.author.contains(keyword)) {
|
||||
return true;
|
||||
} else if (comic.tags.any((element) => element.contains(keyword))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 1; i < keywordList.length; i++) {
|
||||
comics =
|
||||
comics.where((element) => test(element, keywordList[i])).toList();
|
||||
}
|
||||
return comics;
|
||||
}
|
||||
|
||||
List<FavoriteItemWithFolderInfo> search(String keyword) {
|
||||
var keywordList = keyword.split(" ");
|
||||
keyword = keywordList.first;
|
||||
@@ -601,8 +666,8 @@ class LocalFavoritesManager with ChangeNotifier {
|
||||
keyword = "%$keyword%";
|
||||
var res = _db.select("""
|
||||
SELECT * FROM "$table"
|
||||
WHERE name LIKE ? OR author LIKE ? OR tags LIKE ?;
|
||||
""", [keyword, keyword, keyword]);
|
||||
WHERE name LIKE ? OR author LIKE ? OR tags LIKE ? OR translated_tags LIKE ?;
|
||||
""", [keyword, keyword, keyword, keyword]);
|
||||
for (var comic in res) {
|
||||
comics.add(
|
||||
FavoriteItemWithFolderInfo(FavoriteItem.fromRow(comic), table));
|
||||
|
@@ -42,6 +42,7 @@ class History implements Comic {
|
||||
|
||||
int page;
|
||||
|
||||
@override
|
||||
String id;
|
||||
|
||||
/// readEpisode is a set of episode numbers that have been read.
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'dart:async' show Future, StreamController, scheduleMicrotask;
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui show Codec;
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -10,6 +11,39 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
||||
extends ImageProvider<T> {
|
||||
const BaseImageProvider();
|
||||
|
||||
static double? _effectiveScreenWidth;
|
||||
|
||||
static const double _normalComicImageRatio = 0.72;
|
||||
|
||||
static const double _minComicImageWidth = 1920 * _normalComicImageRatio;
|
||||
|
||||
static TargetImageSize _getTargetSize(width, height) {
|
||||
if (_effectiveScreenWidth == null) {
|
||||
final screens = PlatformDispatcher.instance.displays;
|
||||
for (var screen in screens) {
|
||||
if (screen.size.width > screen.size.height) {
|
||||
_effectiveScreenWidth = max(
|
||||
_effectiveScreenWidth ?? 0,
|
||||
screen.size.height * _normalComicImageRatio,
|
||||
);
|
||||
} else {
|
||||
_effectiveScreenWidth = max(
|
||||
_effectiveScreenWidth ?? 0,
|
||||
screen.size.width
|
||||
);
|
||||
}
|
||||
}
|
||||
if (_effectiveScreenWidth! < _minComicImageWidth) {
|
||||
_effectiveScreenWidth = _minComicImageWidth;
|
||||
}
|
||||
}
|
||||
if (width > _effectiveScreenWidth!) {
|
||||
height = (height * _effectiveScreenWidth! / width).round();
|
||||
width = _effectiveScreenWidth!.round();
|
||||
}
|
||||
return TargetImageSize(width: width, height: height);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) {
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
@@ -45,19 +79,12 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
||||
|
||||
while (data == null && !stop) {
|
||||
try {
|
||||
if(_cache.containsKey(key.key)){
|
||||
data = _cache[key.key];
|
||||
} else {
|
||||
data = await load(chunkEvents);
|
||||
_checkCacheSize();
|
||||
_cache[key.key] = data;
|
||||
_cacheSize += data.length;
|
||||
}
|
||||
data = await load(chunkEvents);
|
||||
} catch (e) {
|
||||
if(e.toString().contains("Invalid Status Code: 404")) {
|
||||
if (e.toString().contains("Invalid Status Code: 404")) {
|
||||
rethrow;
|
||||
}
|
||||
if(e.toString().contains("Invalid Status Code: 403")) {
|
||||
if (e.toString().contains("Invalid Status Code: 403")) {
|
||||
rethrow;
|
||||
}
|
||||
if (e.toString().contains("handshake")) {
|
||||
@@ -73,23 +100,24 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
||||
}
|
||||
}
|
||||
|
||||
if(stop) {
|
||||
if (stop) {
|
||||
throw Exception("Image loading is stopped");
|
||||
}
|
||||
|
||||
if(data!.isEmpty) {
|
||||
if (data!.isEmpty) {
|
||||
throw Exception("Empty image data");
|
||||
}
|
||||
|
||||
try {
|
||||
final buffer = await ImmutableBuffer.fromUint8List(data);
|
||||
return await decode(buffer);
|
||||
return await decode(buffer, getTargetSize: _getTargetSize);
|
||||
} catch (e) {
|
||||
await CacheManager().delete(this.key);
|
||||
if (data.length < 2 * 1024) {
|
||||
// data is too short, it's likely that the data is text, not image
|
||||
try {
|
||||
var text = const Utf8Codec(allowMalformed: false).decoder.convert(data);
|
||||
var text =
|
||||
const Utf8Codec(allowMalformed: false).decoder.convert(data);
|
||||
throw Exception("Expected image data, but got text: $text");
|
||||
} catch (e) {
|
||||
// ignore
|
||||
@@ -107,30 +135,6 @@ abstract class BaseImageProvider<T extends BaseImageProvider<T>>
|
||||
}
|
||||
}
|
||||
|
||||
static final _cache = <String, Uint8List>{};
|
||||
|
||||
static var _cacheSize = 0;
|
||||
|
||||
static var _cacheSizeLimit = 50 * 1024 * 1024;
|
||||
|
||||
static void _checkCacheSize(){
|
||||
while (_cacheSize > _cacheSizeLimit){
|
||||
var firstKey = _cache.keys.first;
|
||||
_cacheSize -= _cache[firstKey]!.length;
|
||||
_cache.remove(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
static void clearCache(){
|
||||
_cache.clear();
|
||||
_cacheSize = 0;
|
||||
}
|
||||
|
||||
static void setCacheSizeLimit(int size){
|
||||
_cacheSizeLimit = size;
|
||||
_checkCacheSize();
|
||||
}
|
||||
|
||||
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents);
|
||||
|
||||
String get key;
|
||||
|
@@ -2,6 +2,7 @@ import 'dart:async' show Future, StreamController;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:venera/network/images.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'base_image_provider.dart';
|
||||
import 'reader_image.dart' as image_provider;
|
||||
|
||||
@@ -20,6 +21,14 @@ class ReaderImageProvider
|
||||
|
||||
@override
|
||||
Future<Uint8List> load(StreamController<ImageChunkEvent> chunkEvents) async {
|
||||
if (imageKey.startsWith('file://')) {
|
||||
var file = File(imageKey);
|
||||
if (await file.exists()) {
|
||||
return file.readAsBytes();
|
||||
}
|
||||
throw "Error: File not found.";
|
||||
}
|
||||
|
||||
await for (var event
|
||||
in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) {
|
||||
chunkEvents.add(ImageChunkEvent(
|
||||
|
@@ -389,7 +389,7 @@ class LocalManager with ChangeNotifier {
|
||||
return files.map((e) => "file://${e.path}").toList();
|
||||
}
|
||||
|
||||
Future<bool> isDownloaded(String id, ComicType type, [int? ep]) async {
|
||||
bool isDownloaded(String id, ComicType type, [int? ep]) {
|
||||
var comic = find(id, type);
|
||||
if (comic == null) return false;
|
||||
if (comic.chapters == null || ep == null) return true;
|
||||
|
@@ -111,4 +111,10 @@ extension StyledText on TextStyle {
|
||||
TextStyle get s40 => copyWith(fontSize: 40);
|
||||
|
||||
TextStyle withColor(Color? color) => copyWith(color: color);
|
||||
}
|
||||
|
||||
extension ColorExt on Color {
|
||||
Color toOpacity(double opacity) {
|
||||
return withValues(alpha: opacity);
|
||||
}
|
||||
}
|
@@ -18,11 +18,11 @@ Future<void> init() async {
|
||||
await appdata.init();
|
||||
await App.init();
|
||||
await HistoryManager().init();
|
||||
await TagsTranslation.readData();
|
||||
await LocalFavoritesManager().init();
|
||||
SingleInstanceCookieJar("${App.dataPath}/cookie.db");
|
||||
await JsEngine().init();
|
||||
await ComicSource.init();
|
||||
await LocalManager().init();
|
||||
await TagsTranslation.readData();
|
||||
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
||||
}
|
176
lib/main.dart
176
lib/main.dart
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'package:desktop_webview_window/desktop_webview_window.dart';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
@@ -128,6 +129,20 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Color translateColorSetting() {
|
||||
return switch (appdata.settings['color']) {
|
||||
'red' => Colors.red,
|
||||
'pink' => Colors.pink,
|
||||
'purple' => Colors.purple,
|
||||
'green' => Colors.green,
|
||||
'orange' => Colors.orange,
|
||||
'blue' => Colors.blue,
|
||||
'yellow' => Colors.yellow,
|
||||
'cyan' => Colors.cyan,
|
||||
_ => Colors.blue,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget home;
|
||||
@@ -140,90 +155,93 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
} else {
|
||||
home = const MainPage();
|
||||
}
|
||||
return MaterialApp(
|
||||
home: home,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: App.mainColor,
|
||||
surface: Colors.white,
|
||||
primary: App.mainColor.shade600,
|
||||
// ignore: deprecated_member_use
|
||||
background: Colors.white,
|
||||
),
|
||||
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
||||
),
|
||||
navigatorKey: App.rootNavigatorKey,
|
||||
darkTheme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: App.mainColor,
|
||||
return DynamicColorBuilder(builder: (light, dark) {
|
||||
if (appdata.settings['color'] != 'system' || light == null || dark == null) {
|
||||
var color = translateColorSetting();
|
||||
light = ColorScheme.fromSeed(
|
||||
seedColor: color,
|
||||
);
|
||||
dark = ColorScheme.fromSeed(
|
||||
seedColor: color,
|
||||
brightness: Brightness.dark,
|
||||
surface: Colors.black,
|
||||
primary: App.mainColor.shade400,
|
||||
// ignore: deprecated_member_use
|
||||
background: Colors.black,
|
||||
);
|
||||
}
|
||||
return MaterialApp(
|
||||
home: home,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: light.copyWith(
|
||||
surface: Colors.white,
|
||||
),
|
||||
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
||||
),
|
||||
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
||||
),
|
||||
themeMode: switch (appdata.settings['theme_mode']) {
|
||||
'light' => ThemeMode.light,
|
||||
'dark' => ThemeMode.dark,
|
||||
_ => ThemeMode.system
|
||||
},
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
locale: () {
|
||||
var lang = appdata.settings['language'];
|
||||
if (lang == 'system') {
|
||||
return null;
|
||||
}
|
||||
return switch (lang) {
|
||||
'zh-CN' => const Locale('zh', 'CN'),
|
||||
'zh-TW' => const Locale('zh', 'TW'),
|
||||
'en-US' => const Locale('en'),
|
||||
_ => null
|
||||
};
|
||||
}(),
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('zh', 'CN'),
|
||||
Locale('zh', 'TW'),
|
||||
],
|
||||
builder: (context, widget) {
|
||||
ErrorWidget.builder = (details) {
|
||||
Log.error(
|
||||
"Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||
return Material(
|
||||
child: Center(
|
||||
child: Text(details.exception.toString()),
|
||||
),
|
||||
);
|
||||
};
|
||||
if (widget != null) {
|
||||
widget = OverlayWidget(widget);
|
||||
if (App.isDesktop) {
|
||||
widget = Shortcuts(
|
||||
shortcuts: {
|
||||
LogicalKeySet(LogicalKeyboardKey.escape): VoidCallbackIntent(
|
||||
App.pop,
|
||||
),
|
||||
},
|
||||
child: MouseBackDetector(
|
||||
onTapDown: App.pop,
|
||||
child: WindowFrame(widget),
|
||||
navigatorKey: App.rootNavigatorKey,
|
||||
darkTheme: ThemeData(
|
||||
colorScheme: dark.copyWith(
|
||||
surface: Colors.black,
|
||||
),
|
||||
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
||||
),
|
||||
themeMode: switch (appdata.settings['theme_mode']) {
|
||||
'light' => ThemeMode.light,
|
||||
'dark' => ThemeMode.dark,
|
||||
_ => ThemeMode.system
|
||||
},
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
locale: () {
|
||||
var lang = appdata.settings['language'];
|
||||
if (lang == 'system') {
|
||||
return null;
|
||||
}
|
||||
return switch (lang) {
|
||||
'zh-CN' => const Locale('zh', 'CN'),
|
||||
'zh-TW' => const Locale('zh', 'TW'),
|
||||
'en-US' => const Locale('en'),
|
||||
_ => null
|
||||
};
|
||||
}(),
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('zh', 'CN'),
|
||||
Locale('zh', 'TW'),
|
||||
],
|
||||
builder: (context, widget) {
|
||||
ErrorWidget.builder = (details) {
|
||||
Log.error(
|
||||
"Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||
return Material(
|
||||
child: Center(
|
||||
child: Text(details.exception.toString()),
|
||||
),
|
||||
);
|
||||
};
|
||||
if (widget != null) {
|
||||
widget = OverlayWidget(widget);
|
||||
if (App.isDesktop) {
|
||||
widget = Shortcuts(
|
||||
shortcuts: {
|
||||
LogicalKeySet(LogicalKeyboardKey.escape): VoidCallbackIntent(
|
||||
App.pop,
|
||||
),
|
||||
},
|
||||
child: MouseBackDetector(
|
||||
onTapDown: App.pop,
|
||||
child: WindowFrame(widget),
|
||||
),
|
||||
);
|
||||
}
|
||||
return _SystemUiProvider(Material(
|
||||
child: widget,
|
||||
));
|
||||
}
|
||||
return _SystemUiProvider(Material(
|
||||
child: widget,
|
||||
));
|
||||
}
|
||||
throw ('widget is null');
|
||||
},
|
||||
);
|
||||
throw ('widget is null');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -281,13 +280,8 @@ class RHttpAdapter implements HttpClientAdapter {
|
||||
headers[key] ??= [];
|
||||
headers[key]!.add(entry.$2);
|
||||
}
|
||||
var data = res.body;
|
||||
if (headers['content-encoding']?.contains('gzip') ?? false) {
|
||||
// rhttp does not support gzip decoding
|
||||
data = gzip.decoder.bind(data).map((data) => Uint8List.fromList(data));
|
||||
}
|
||||
return ResponseBody(
|
||||
data,
|
||||
res.body,
|
||||
res.statusCode,
|
||||
statusMessage: null,
|
||||
isRedirect: false,
|
||||
|
230
lib/pages/aggregated_search_page.dart
Normal file
230
lib/pages/aggregated_search_page.dart
Normal file
@@ -0,0 +1,230 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:shimmer/shimmer.dart";
|
||||
import "package:venera/components/components.dart";
|
||||
import "package:venera/foundation/app.dart";
|
||||
import "package:venera/foundation/comic_source/comic_source.dart";
|
||||
import "package:venera/foundation/image_provider/cached_image.dart";
|
||||
import "package:venera/pages/search_result_page.dart";
|
||||
import "package:venera/utils/translations.dart";
|
||||
|
||||
import "comic_page.dart";
|
||||
|
||||
class AggregatedSearchPage extends StatefulWidget {
|
||||
const AggregatedSearchPage({super.key, required this.keyword});
|
||||
|
||||
final String keyword;
|
||||
|
||||
@override
|
||||
State<AggregatedSearchPage> createState() => _AggregatedSearchPageState();
|
||||
}
|
||||
|
||||
class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
|
||||
late final List<ComicSource> sources;
|
||||
|
||||
late final SearchBarController controller;
|
||||
|
||||
var _keyword = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
sources = ComicSource.all().where((e) => e.searchPageData != null).toList();
|
||||
_keyword = widget.keyword;
|
||||
controller = SearchBarController(
|
||||
currentText: widget.keyword,
|
||||
onSearch: (text) {
|
||||
setState(() {
|
||||
_keyword = text;
|
||||
});
|
||||
},
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SmoothCustomScrollView(slivers: [
|
||||
SliverSearchBar(controller: controller),
|
||||
SliverList(
|
||||
key: ValueKey(_keyword),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final source = sources[index];
|
||||
return _SliverSearchResult(source: source, keyword: _keyword);
|
||||
},
|
||||
childCount: sources.length,
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class _SliverSearchResult extends StatefulWidget {
|
||||
const _SliverSearchResult({required this.source, required this.keyword});
|
||||
|
||||
final ComicSource source;
|
||||
|
||||
final String keyword;
|
||||
|
||||
@override
|
||||
State<_SliverSearchResult> createState() => _SliverSearchResultState();
|
||||
}
|
||||
|
||||
class _SliverSearchResultState extends State<_SliverSearchResult>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
bool isLoading = true;
|
||||
|
||||
static const _kComicHeight = 144.0;
|
||||
|
||||
get _comicWidth => _kComicHeight * 0.72;
|
||||
|
||||
static const _kLeftPadding = 16.0;
|
||||
|
||||
List<Comic>? comics;
|
||||
|
||||
void load() async {
|
||||
final data = widget.source.searchPageData!;
|
||||
var options =
|
||||
(data.searchOptions ?? []).map((e) => e.defaultValue).toList();
|
||||
if (data.loadPage != null) {
|
||||
var res = await data.loadPage!(widget.keyword, 1, options);
|
||||
if (!res.error) {
|
||||
setState(() {
|
||||
comics = res.data;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
} else if (data.loadNext != null) {
|
||||
var res = await data.loadNext!(widget.keyword, null, options);
|
||||
if (!res.error) {
|
||||
setState(() {
|
||||
comics = res.data;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
load();
|
||||
}
|
||||
|
||||
Widget buildPlaceHolder() {
|
||||
return Container(
|
||||
height: _kComicHeight,
|
||||
width: _comicWidth,
|
||||
margin: const EdgeInsets.only(left: _kLeftPadding),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildComic(Comic c) {
|
||||
return AnimatedTapRegion(
|
||||
borderRadius: 8,
|
||||
onTap: () {
|
||||
context.to(() => ComicPage(
|
||||
id: c.id,
|
||||
sourceKey: c.sourceKey,
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
height: _kComicHeight,
|
||||
width: _comicWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainerLow,
|
||||
),
|
||||
child: AnimatedImage(
|
||||
width: _comicWidth,
|
||||
height: _kComicHeight,
|
||||
fit: BoxFit.cover,
|
||||
image: CachedImageProvider(c.cover),
|
||||
),
|
||||
),
|
||||
).paddingLeft(_kLeftPadding);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
context.to(
|
||||
() => SearchResultPage(
|
||||
text: widget.keyword,
|
||||
sourceKey: widget.source.key,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
title: Text(widget.source.name),
|
||||
),
|
||||
if (isLoading)
|
||||
SizedBox(
|
||||
height: _kComicHeight,
|
||||
width: double.infinity,
|
||||
child: Shimmer.fromColors(
|
||||
baseColor: context.colorScheme.surfaceContainerLow,
|
||||
highlightColor: context.colorScheme.surfaceContainer,
|
||||
direction: ShimmerDirection.ltr,
|
||||
child: LayoutBuilder(builder: (context, constrains) {
|
||||
var itemWidth = _comicWidth + _kLeftPadding;
|
||||
var items = (constrains.maxWidth / itemWidth).ceil();
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
items,
|
||||
(index) => buildPlaceHolder(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
else if (comics == null || comics!.isEmpty)
|
||||
SizedBox(
|
||||
height: _kComicHeight,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline),
|
||||
const SizedBox(width: 8),
|
||||
Text("No search results found".tl),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
).paddingHorizontal(16),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: _kComicHeight,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
for (var c in comics!) buildComic(c),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingBottom(16),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
@@ -53,6 +53,7 @@ class CategoriesPage extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
FilledTabBar(
|
||||
key: PageStorageKey(categories.toString()),
|
||||
tabs: categories.map((e) {
|
||||
String title = e;
|
||||
try {
|
||||
@@ -261,7 +262,7 @@ class _CategoryPage extends StatelessWidget {
|
||||
builder: (context) {
|
||||
return Material(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: context.colorScheme.primaryContainer.withOpacity(0.72),
|
||||
color: context.colorScheme.primaryContainer.toOpacity(0.72),
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
onTap: () => onClick(tag, param),
|
||||
|
@@ -223,7 +223,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
||||
children: [
|
||||
SelectableText(comic.title, style: ts.s18),
|
||||
if (comic.subTitle != null)
|
||||
SelectableText(comic.subTitle!, style: ts.s14).paddingVertical(4),
|
||||
SelectableText(comic.subTitle!, style: ts.s14)
|
||||
.paddingVertical(4),
|
||||
Text(
|
||||
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
||||
style: ts.s12,
|
||||
@@ -1115,14 +1116,12 @@ class _ComicChaptersState extends State<_ComicChapters> {
|
||||
(state.history?.readEpisode ?? const {}).contains(i + 1);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Material(
|
||||
elevation: 5,
|
||||
color: context.colorScheme.surface,
|
||||
surfaceTintColor: context.colorScheme.surfaceTint,
|
||||
child: Material(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: InkWell(
|
||||
onTap: () => state.read(i + 1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
shadowColor: Colors.transparent,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
@@ -1133,19 +1132,18 @@ class _ComicChaptersState extends State<_ComicChapters> {
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color:
|
||||
visited ? context.colorScheme.outline : null),
|
||||
color: visited ? context.colorScheme.outline : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () => state.read(i + 1),
|
||||
),
|
||||
);
|
||||
}),
|
||||
gridDelegate: const SliverGridDelegateWithFixedHeight(
|
||||
maxCrossAxisExtent: 200, itemHeight: 48),
|
||||
),
|
||||
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
|
||||
if (eps.length > 20 && !showAll)
|
||||
SliverToBoxAdapter(
|
||||
child: Align(
|
||||
@@ -1328,9 +1326,7 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
|
||||
),
|
||||
)
|
||||
else if (isLoading)
|
||||
const SliverToBoxAdapter(
|
||||
child: ListLoadingIndicator(),
|
||||
),
|
||||
const SliverListLoadingIndicator(),
|
||||
const SliverToBoxAdapter(
|
||||
child: Divider(),
|
||||
),
|
||||
|
@@ -110,7 +110,9 @@ class _ExplorePageState extends State<ExplorePage>
|
||||
return Tab(text: i.ts(comicSource.key), key: Key(i));
|
||||
}
|
||||
|
||||
Widget buildBody(String i) => _SingleExplorePage(i, key: Key(i));
|
||||
Widget buildBody(String i) => Material(
|
||||
child: _SingleExplorePage(i, key: PageStorageKey(i)),
|
||||
);
|
||||
|
||||
Widget buildEmpty() {
|
||||
var msg = "No Explore Pages".tl;
|
||||
@@ -147,7 +149,7 @@ class _ExplorePageState extends State<ExplorePage>
|
||||
|
||||
Widget tabBar = Material(
|
||||
child: FilledTabBar(
|
||||
key: Key(pages.toString()),
|
||||
key: PageStorageKey(pages.toString()),
|
||||
tabs: pages.map((e) => buildTab(e)).toList(),
|
||||
controller: controller,
|
||||
),
|
||||
@@ -240,20 +242,14 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
|
||||
with AutomaticKeepAliveClientMixin<_SingleExplorePage> {
|
||||
late final ExplorePageData data;
|
||||
|
||||
bool loading = true;
|
||||
|
||||
String? message;
|
||||
|
||||
List<ExplorePagePart>? parts;
|
||||
|
||||
late final String comicSourceKey;
|
||||
|
||||
int key = 0;
|
||||
|
||||
bool _wantKeepAlive = true;
|
||||
|
||||
var scrollController = ScrollController();
|
||||
|
||||
VoidCallback? refreshHandler;
|
||||
|
||||
void onSettingsChanged() {
|
||||
var explorePages = appdata.settings["explore_pages"];
|
||||
if (!explorePages.contains(widget.title)) {
|
||||
@@ -288,15 +284,34 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
if (data.loadMultiPart != null) {
|
||||
return buildMultiPart();
|
||||
return _MultiPartExplorePage(
|
||||
key: const PageStorageKey("comic_list"),
|
||||
data: data,
|
||||
controller: scrollController,
|
||||
comicSourceKey: comicSourceKey,
|
||||
refreshHandlerCallback: (c) {
|
||||
refreshHandler = c;
|
||||
},
|
||||
);
|
||||
} else if (data.loadPage != null || data.loadNext != null) {
|
||||
return buildComicList();
|
||||
return ComicList(
|
||||
loadPage: data.loadPage,
|
||||
loadNext: data.loadNext,
|
||||
key: const PageStorageKey("comic_list"),
|
||||
controller: scrollController,
|
||||
refreshHandlerCallback: (c) {
|
||||
refreshHandler = c;
|
||||
},
|
||||
);
|
||||
} else if (data.loadMixed != null) {
|
||||
return _MixedExplorePage(
|
||||
data,
|
||||
comicSourceKey,
|
||||
key: ValueKey(key),
|
||||
key: const PageStorageKey("comic_list"),
|
||||
controller: scrollController,
|
||||
refreshHandlerCallback: (c) {
|
||||
refreshHandler = c;
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
@@ -305,74 +320,12 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildComicList() {
|
||||
return ComicList(
|
||||
loadPage: data.loadPage,
|
||||
loadNext: data.loadNext,
|
||||
key: ValueKey(key),
|
||||
controller: scrollController,
|
||||
);
|
||||
}
|
||||
|
||||
void load() async {
|
||||
var res = await data.loadMultiPart!();
|
||||
loading = false;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (res.error) {
|
||||
message = res.errorMessage;
|
||||
} else {
|
||||
parts = res.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildMultiPart() {
|
||||
if (loading) {
|
||||
load();
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
} else if (message != null) {
|
||||
return NetworkError(
|
||||
message: message!,
|
||||
retry: refresh,
|
||||
withAppbar: false,
|
||||
);
|
||||
} else {
|
||||
return buildPage();
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildPage() {
|
||||
return SmoothCustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: _buildPage().toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Iterable<Widget> _buildPage() sync* {
|
||||
for (var part in parts!) {
|
||||
yield* _buildExplorePagePart(part, comicSourceKey);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Object? get tag => widget.title;
|
||||
|
||||
@override
|
||||
void refresh() {
|
||||
message = null;
|
||||
if (data.loadMultiPart != null) {
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
key++;
|
||||
});
|
||||
}
|
||||
refreshHandler?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -393,7 +346,8 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage>
|
||||
}
|
||||
|
||||
class _MixedExplorePage extends StatefulWidget {
|
||||
const _MixedExplorePage(this.data, this.sourceKey, {super.key, this.controller});
|
||||
const _MixedExplorePage(this.data, this.sourceKey,
|
||||
{super.key, this.controller, required this.refreshHandlerCallback});
|
||||
|
||||
final ExplorePageData data;
|
||||
|
||||
@@ -401,12 +355,24 @@ class _MixedExplorePage extends StatefulWidget {
|
||||
|
||||
final ScrollController? controller;
|
||||
|
||||
final void Function(VoidCallback c) refreshHandlerCallback;
|
||||
|
||||
@override
|
||||
State<_MixedExplorePage> createState() => _MixedExplorePageState();
|
||||
}
|
||||
|
||||
class _MixedExplorePageState
|
||||
extends MultiPageLoadingState<_MixedExplorePage, Object> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
widget.refreshHandlerCallback(refresh);
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
reset();
|
||||
}
|
||||
|
||||
Iterable<Widget> buildSlivers(BuildContext context, List<Object> data) sync* {
|
||||
List<Comic> cache = [];
|
||||
for (var part in data) {
|
||||
@@ -437,7 +403,7 @@ class _MixedExplorePageState
|
||||
controller: widget.controller,
|
||||
slivers: [
|
||||
...buildSlivers(context, data),
|
||||
if (haveNextPage) const ListLoadingIndicator().toSliver()
|
||||
const SliverListLoadingIndicator(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -518,3 +484,125 @@ Iterable<Widget> _buildExplorePagePart(
|
||||
yield buildTitle(part);
|
||||
yield buildComics(part);
|
||||
}
|
||||
|
||||
class _MultiPartExplorePage extends StatefulWidget {
|
||||
const _MultiPartExplorePage({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.controller,
|
||||
required this.comicSourceKey,
|
||||
required this.refreshHandlerCallback,
|
||||
});
|
||||
|
||||
final ExplorePageData data;
|
||||
|
||||
final ScrollController controller;
|
||||
|
||||
final String comicSourceKey;
|
||||
|
||||
final void Function(VoidCallback c) refreshHandlerCallback;
|
||||
|
||||
@override
|
||||
State<_MultiPartExplorePage> createState() => _MultiPartExplorePageState();
|
||||
}
|
||||
|
||||
class _MultiPartExplorePageState extends State<_MultiPartExplorePage> {
|
||||
late final ExplorePageData data;
|
||||
|
||||
List<ExplorePagePart>? parts;
|
||||
|
||||
bool loading = true;
|
||||
|
||||
String? message;
|
||||
|
||||
Map<String, dynamic> get state => {
|
||||
"loading": loading,
|
||||
"message": message,
|
||||
"parts": parts,
|
||||
};
|
||||
|
||||
void restoreState(dynamic state) {
|
||||
if (state == null) return;
|
||||
loading = state["loading"];
|
||||
message = state["message"];
|
||||
parts = state["parts"];
|
||||
}
|
||||
|
||||
void storeState() {
|
||||
PageStorage.of(context).writeState(context, state);
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
setState(() {
|
||||
loading = true;
|
||||
message = null;
|
||||
parts = null;
|
||||
});
|
||||
storeState();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
data = widget.data;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
restoreState(PageStorage.of(context).readState(context));
|
||||
widget.refreshHandlerCallback(refresh);
|
||||
}
|
||||
|
||||
void load() async {
|
||||
var res = await data.loadMultiPart!();
|
||||
loading = false;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (res.error) {
|
||||
message = res.errorMessage;
|
||||
} else {
|
||||
parts = res.data;
|
||||
}
|
||||
});
|
||||
storeState();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (loading) {
|
||||
load();
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
} else if (message != null) {
|
||||
return NetworkError(
|
||||
message: message!,
|
||||
retry: () {
|
||||
setState(() {
|
||||
loading = true;
|
||||
message = null;
|
||||
});
|
||||
},
|
||||
withAppbar: false,
|
||||
);
|
||||
} else {
|
||||
return buildPage();
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildPage() {
|
||||
return SmoothCustomScrollView(
|
||||
key: const PageStorageKey('scroll'),
|
||||
controller: widget.controller,
|
||||
slivers: _buildPage().toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Iterable<Widget> _buildPage() sync* {
|
||||
for (var part in parts!) {
|
||||
yield* _buildExplorePagePart(part, widget.comicSourceKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
|
||||
part of 'favorites_page.dart';
|
||||
|
||||
/// Open a dialog to create a new favorite folder.
|
||||
|
@@ -10,7 +10,9 @@ import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||
import 'package:venera/foundation/comic_type.dart';
|
||||
import 'package:venera/foundation/consts.dart';
|
||||
import 'package:venera/foundation/favorites.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/foundation/res.dart';
|
||||
import 'package:venera/network/download.dart';
|
||||
import 'package:venera/pages/comic_page.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
@@ -92,7 +94,7 @@ class _FavoritesPageState extends State<FavoritesPage> {
|
||||
barrierDismissible: true,
|
||||
fullscreenDialog: true,
|
||||
opaque: false,
|
||||
barrierColor: Colors.black.withOpacity(0.36),
|
||||
barrierColor: Colors.black.toOpacity(0.36),
|
||||
pageBuilder: (context, animation, secondary) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
@@ -150,14 +152,14 @@ class _FavoritesPageState extends State<FavoritesPage> {
|
||||
);
|
||||
}
|
||||
if (!isNetwork) {
|
||||
return _LocalFavoritesPage(folder: folder!, key: Key(folder!));
|
||||
return _LocalFavoritesPage(folder: folder!, key: PageStorageKey("local_$folder"));
|
||||
} else {
|
||||
var favoriteData = getFavoriteDataOrNull(folder!);
|
||||
if (favoriteData == null) {
|
||||
folder = null;
|
||||
return buildBody();
|
||||
} else {
|
||||
return NetworkFavoritePage(favoriteData, key: Key(folder!));
|
||||
return NetworkFavoritePage(favoriteData, key: PageStorageKey("network_$folder"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,4 +169,4 @@ abstract interface class FolderList {
|
||||
void update();
|
||||
|
||||
void updateFolders();
|
||||
}
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
comics = LocalFavoritesManager().search(keyword);
|
||||
comics = LocalFavoritesManager().searchInFolder(widget.folder, keyword);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -53,23 +53,57 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void selectAll() {
|
||||
setState(() {
|
||||
selectedComics = comics.asMap().map((k, v) => MapEntry(v, true));
|
||||
});
|
||||
}
|
||||
|
||||
void invertSelection() {
|
||||
setState(() {
|
||||
comics.asMap().forEach((k, v) {
|
||||
selectedComics[v] = !selectedComics.putIfAbsent(v, () => false);
|
||||
});
|
||||
selectedComics.removeWhere((k, v) => !v);
|
||||
});
|
||||
}
|
||||
|
||||
bool downloadComic(FavoriteItem c) {
|
||||
var source = c.type.comicSource;
|
||||
if (source != null) {
|
||||
bool isDownloaded = LocalManager().isDownloaded(
|
||||
c.id,
|
||||
(c).type,
|
||||
);
|
||||
if (isDownloaded) {
|
||||
return false;
|
||||
}
|
||||
LocalManager().addTask(ImagesDownloadTask(
|
||||
source: source,
|
||||
comicId: c.id,
|
||||
comicTitle: c.title,
|
||||
));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void downloadSelected() {
|
||||
int count = 0;
|
||||
for (var c in selectedComics.keys) {
|
||||
if (downloadComic(c as FavoriteItem)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
context.showMessage(
|
||||
message: "Added @c comics to download queue.".tlParams({"c": count}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
void selectAll() {
|
||||
setState(() {
|
||||
selectedComics = comics.asMap().map((k, v) => MapEntry(v, true));
|
||||
});
|
||||
}
|
||||
|
||||
void invertSelection() {
|
||||
setState(() {
|
||||
comics.asMap().forEach((k, v) {
|
||||
selectedComics[v] = !selectedComics.putIfAbsent(v, () => false);
|
||||
});
|
||||
selectedComics.removeWhere((k, v) => !v);
|
||||
});
|
||||
}
|
||||
|
||||
var body = Scaffold(
|
||||
body: SmoothCustomScrollView(slivers: [
|
||||
if (!searchMode && !multiSelectMode)
|
||||
@@ -300,6 +334,11 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
||||
},
|
||||
);
|
||||
}),
|
||||
MenuEntry(
|
||||
icon: Icons.download,
|
||||
text: "Download".tl,
|
||||
onClick: downloadSelected,
|
||||
),
|
||||
]),
|
||||
],
|
||||
)
|
||||
@@ -336,6 +375,20 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
||||
SliverGridComics(
|
||||
comics: comics,
|
||||
selections: selectedComics,
|
||||
menuBuilder: (c) {
|
||||
return [
|
||||
MenuEntry(
|
||||
icon: Icons.download,
|
||||
text: "Download".tl,
|
||||
onClick: () {
|
||||
downloadComic(c as FavoriteItem);
|
||||
context.showMessage(
|
||||
message: "Download started".tl,
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
},
|
||||
onTap: multiSelectMode
|
||||
? (c) {
|
||||
setState(() {
|
||||
@@ -425,7 +478,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
||||
padding: EdgeInsets.only(bottom: context.padding.bottom + 16),
|
||||
child: Container(
|
||||
constraints:
|
||||
const BoxConstraints(maxHeight: 700, maxWidth: 500),
|
||||
const BoxConstraints(maxHeight: 700, maxWidth: 500),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -443,7 +496,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
||||
targetFolders = LocalFavoritesManager()
|
||||
.folderNames
|
||||
.where((folder) =>
|
||||
folder != favPage.folder)
|
||||
folder != favPage.folder)
|
||||
.toList();
|
||||
});
|
||||
});
|
||||
@@ -482,14 +535,14 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
||||
onChanged: disabled
|
||||
? null
|
||||
: (v) {
|
||||
setState(() {
|
||||
if (v!) {
|
||||
selectedLocalFolders.add(folder);
|
||||
} else {
|
||||
selectedLocalFolders.remove(folder);
|
||||
}
|
||||
});
|
||||
},
|
||||
setState(() {
|
||||
if (v!) {
|
||||
selectedLocalFolders.add(folder);
|
||||
} else {
|
||||
selectedLocalFolders.remove(folder);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -592,12 +645,19 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
|
||||
late var comics = LocalFavoritesManager().getAllComics(widget.name);
|
||||
bool changed = false;
|
||||
|
||||
Color lightenColor(Color color, double lightenValue) {
|
||||
int red = (color.red + ((255 - color.red) * lightenValue)).round();
|
||||
int green = (color.green + ((255 - color.green) * lightenValue)).round();
|
||||
int blue = (color.blue + ((255 - color.blue) * lightenValue)).round();
|
||||
static int _floatToInt8(double x) {
|
||||
return (x * 255.0).round() & 0xff;
|
||||
}
|
||||
|
||||
return Color.fromARGB(color.alpha, red, green, blue);
|
||||
Color lightenColor(Color color, double lightenValue) {
|
||||
int red =
|
||||
(_floatToInt8(color.r) + ((255 - color.r) * lightenValue)).round();
|
||||
int green = (_floatToInt8(color.g) * 255 + ((255 - color.g) * lightenValue))
|
||||
.round();
|
||||
int blue = (_floatToInt8(color.b) * 255 + ((255 - color.b) * lightenValue))
|
||||
.round();
|
||||
|
||||
return Color.fromARGB(_floatToInt8(color.a), red, green, blue);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -650,7 +710,7 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ReorderableBuilder(
|
||||
body: ReorderableBuilder<FavoriteItem>(
|
||||
key: reorderWidgetKey,
|
||||
scrollController: _scrollController,
|
||||
longPressDelay: App.isDesktop
|
||||
@@ -659,14 +719,14 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> {
|
||||
onReorder: (reorderFunc) {
|
||||
changed = true;
|
||||
setState(() {
|
||||
comics = reorderFunc(comics) as List<FavoriteItem>;
|
||||
comics = reorderFunc(comics);
|
||||
});
|
||||
widget.onReorder(comics);
|
||||
},
|
||||
dragChildBoxDecoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: lightenColor(
|
||||
Theme.of(context).splashColor.withOpacity(1),
|
||||
Theme.of(context).splashColor.withAlpha(255),
|
||||
0.2,
|
||||
),
|
||||
),
|
||||
|
@@ -179,7 +179,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
||||
alignment: Alignment.centerLeft,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? context.colorScheme.primaryContainer.withOpacity(0.36)
|
||||
? context.colorScheme.primaryContainer.toOpacity(0.36)
|
||||
: null,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
@@ -214,7 +214,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
||||
alignment: Alignment.centerLeft,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? context.colorScheme.primaryContainer.withOpacity(0.36)
|
||||
? context.colorScheme.primaryContainer.toOpacity(0.36)
|
||||
: null,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
|
@@ -4,8 +4,6 @@ import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||
import 'package:venera/foundation/comic_type.dart';
|
||||
import 'package:venera/foundation/history.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
|
||||
class HistoryPage extends StatefulWidget {
|
||||
|
@@ -6,7 +6,6 @@ import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||
import 'package:venera/foundation/consts.dart';
|
||||
import 'package:venera/foundation/favorites.dart';
|
||||
import 'package:venera/foundation/history.dart';
|
||||
import 'package:venera/foundation/image_provider/cached_image.dart';
|
||||
import 'package:venera/foundation/image_provider/history_image_provider.dart';
|
||||
import 'package:venera/foundation/image_provider/local_comic_image.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
@@ -17,7 +16,6 @@ import 'package:venera/pages/downloading_page.dart';
|
||||
import 'package:venera/pages/history_page.dart';
|
||||
import 'package:venera/pages/search_page.dart';
|
||||
import 'package:venera/utils/data_sync.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
import 'package:venera/utils/import_comic.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
|
||||
@@ -55,7 +53,7 @@ class _SearchBar extends StatelessWidget {
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
child: Material(
|
||||
color: context.colorScheme.surfaceContainer,
|
||||
color: context.colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
@@ -580,7 +578,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor: Colors.black.withOpacity(0.2),
|
||||
barrierColor: Colors.black.toOpacity(0.2),
|
||||
builder: (context) {
|
||||
var help = '';
|
||||
help +=
|
||||
|
@@ -7,6 +7,7 @@ import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/foundation/log.dart';
|
||||
import 'package:venera/pages/downloading_page.dart';
|
||||
import 'package:venera/utils/cbz.dart';
|
||||
import 'package:venera/utils/epub.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:venera/utils/pdf.dart';
|
||||
import 'package:venera/utils/translations.dart';
|
||||
@@ -389,6 +390,33 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
||||
File(cache).deleteIgnoreError();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (!multiSelectMode)
|
||||
MenuEntry(
|
||||
icon: Icons.import_contacts_outlined,
|
||||
text: "Export as epub".tl,
|
||||
onClick: () async {
|
||||
var controller = showLoadingDialog(
|
||||
context,
|
||||
allowCancel: false,
|
||||
);
|
||||
File? file;
|
||||
try {
|
||||
file = await createEpubWithLocalComic(
|
||||
c as LocalComic,
|
||||
);
|
||||
await saveFile(
|
||||
file: file,
|
||||
filename: "${c.title}.epub",
|
||||
);
|
||||
} catch (e, s) {
|
||||
Log.error("EPUB Export", e, s);
|
||||
context.showMessage(message: e.toString());
|
||||
} finally {
|
||||
controller.close();
|
||||
file?.deleteIgnoreError();
|
||||
}
|
||||
},
|
||||
)
|
||||
];
|
||||
},
|
||||
|
@@ -62,10 +62,18 @@ class _MainPageState extends State<MainPage> {
|
||||
}
|
||||
|
||||
final _pages = [
|
||||
const HomePage(),
|
||||
const FavoritesPage(),
|
||||
const ExplorePage(),
|
||||
const CategoriesPage(),
|
||||
const HomePage(
|
||||
key: PageStorageKey('home'),
|
||||
),
|
||||
const FavoritesPage(
|
||||
key: PageStorageKey('favorites'),
|
||||
),
|
||||
const ExplorePage(
|
||||
key: PageStorageKey('explore'),
|
||||
),
|
||||
const CategoriesPage(
|
||||
key: PageStorageKey('categories'),
|
||||
),
|
||||
];
|
||||
|
||||
var index = 0;
|
||||
|
@@ -268,5 +268,5 @@ class _DragListener {
|
||||
void Function(Offset offset)? onMove;
|
||||
void Function()? onEnd;
|
||||
|
||||
_DragListener({this.onStart, this.onMove, this.onEnd});
|
||||
_DragListener({this.onMove, this.onEnd});
|
||||
}
|
@@ -162,19 +162,22 @@ class _GalleryModeState extends State<_GalleryMode>
|
||||
} else {
|
||||
int pageIndex = index - 1;
|
||||
int startIndex = pageIndex * reader.imagesPerPage;
|
||||
int endIndex = math.min(startIndex + reader.imagesPerPage, reader.images!.length);
|
||||
List<String> pageImages = reader.images!.sublist(startIndex, endIndex);
|
||||
int endIndex = math.min(
|
||||
startIndex + reader.imagesPerPage, reader.images!.length);
|
||||
List<String> pageImages =
|
||||
reader.images!.sublist(startIndex, endIndex);
|
||||
|
||||
cached[index] = true;
|
||||
cache(index);
|
||||
|
||||
photoViewControllers[index] = PhotoViewController();
|
||||
|
||||
if(reader.imagesPerPage == 1) {
|
||||
if (reader.imagesPerPage == 1) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
filterQuality: FilterQuality.medium,
|
||||
controller: photoViewControllers[index],
|
||||
imageProvider: _createImageProviderFromKey(pageImages[0], context),
|
||||
imageProvider:
|
||||
_createImageProviderFromKey(pageImages[0], context),
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, error, s, retry) {
|
||||
return NetworkError(message: error.toString(), retry: retry);
|
||||
@@ -645,32 +648,19 @@ class _ContinuousModeState extends State<_ContinuousMode>
|
||||
|
||||
ImageProvider _createImageProviderFromKey(
|
||||
String imageKey, BuildContext context) {
|
||||
if (imageKey.startsWith('file://')) {
|
||||
return FileImage(File(imageKey.replaceFirst("file://", '')));
|
||||
} else {
|
||||
var reader = context.reader;
|
||||
return ReaderImageProvider(
|
||||
imageKey,
|
||||
reader.type.comicSource!.key,
|
||||
reader.cid,
|
||||
reader.eid,
|
||||
);
|
||||
}
|
||||
var reader = context.reader;
|
||||
return ReaderImageProvider(
|
||||
imageKey,
|
||||
reader.type.comicSource?.key,
|
||||
reader.cid,
|
||||
reader.eid,
|
||||
);
|
||||
}
|
||||
|
||||
ImageProvider _createImageProvider(int page, BuildContext context) {
|
||||
var reader = context.reader;
|
||||
var imageKey = reader.images![page - 1];
|
||||
if (imageKey.startsWith('file://')) {
|
||||
return FileImage(File(imageKey.replaceFirst("file://", '')));
|
||||
} else {
|
||||
return ReaderImageProvider(
|
||||
imageKey,
|
||||
reader.type.comicSource!.key,
|
||||
reader.cid,
|
||||
reader.eid,
|
||||
);
|
||||
}
|
||||
return _createImageProviderFromKey(imageKey, context);
|
||||
}
|
||||
|
||||
void _precacheImage(int page, BuildContext context) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
library venera_reader;
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
@@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_memory_info/flutter_memory_info.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:photo_view/photo_view_gallery.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
@@ -21,6 +22,7 @@ import 'package:venera/foundation/comic_type.dart';
|
||||
import 'package:venera/foundation/history.dart';
|
||||
import 'package:venera/foundation/image_provider/reader_image.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/foundation/log.dart';
|
||||
import 'package:venera/pages/settings/settings_page.dart';
|
||||
import 'package:venera/utils/data_sync.dart';
|
||||
import 'package:venera/utils/file_type.dart';
|
||||
@@ -60,7 +62,7 @@ class Reader extends StatefulWidget {
|
||||
|
||||
final String name;
|
||||
|
||||
/// Map<Chapter ID, Chapter Name>.
|
||||
/// key: Chapter ID, value: Chapter Name
|
||||
/// null if the comic is a gallery
|
||||
final Map<String, String>? chapters;
|
||||
|
||||
@@ -142,9 +144,27 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
||||
if(appdata.settings['enableTurnPageByVolumeKey']) {
|
||||
handleVolumeEvent();
|
||||
}
|
||||
setImageCacheSize();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void setImageCacheSize() async {
|
||||
var availableRAM = await MemoryInfo.getFreePhysicalMemorySize();
|
||||
if (availableRAM == null) return;
|
||||
int maxImageCacheSize;
|
||||
if (availableRAM < 1 << 30) {
|
||||
maxImageCacheSize = 100 << 20;
|
||||
} else if (availableRAM < 2 << 30) {
|
||||
maxImageCacheSize = 200 << 20;
|
||||
} else if (availableRAM < 4 << 30) {
|
||||
maxImageCacheSize = 300 << 20;
|
||||
} else {
|
||||
maxImageCacheSize = 500 << 20;
|
||||
}
|
||||
Log.info("Reader", "Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize");
|
||||
PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
autoPageTurningTimer?.cancel();
|
||||
@@ -154,6 +174,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
||||
Future.microtask(() {
|
||||
DataSync().onDataChanged();
|
||||
});
|
||||
PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@@ -167,10 +167,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: context.padding.top),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surface.withOpacity(0.82),
|
||||
color: context.colorScheme.surface.toOpacity(0.82),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
color: Colors.grey.toOpacity(0.5),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
@@ -357,10 +357,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
return BlurEffect(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surface.withOpacity(0.82),
|
||||
color: context.colorScheme.surface.toOpacity(0.82),
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
color: Colors.grey.toOpacity(0.5),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
@@ -641,7 +641,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceTint
|
||||
.withOpacity(0.2),
|
||||
.toOpacity(0.2),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
|
@@ -7,6 +7,7 @@ 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/state_controller.dart';
|
||||
import 'package:venera/pages/aggregated_search_page.dart';
|
||||
import 'package:venera/pages/search_result_page.dart';
|
||||
import 'package:venera/utils/app_links.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
@@ -27,6 +28,8 @@ class _SearchPageState extends State<SearchPage> {
|
||||
|
||||
String searchTarget = "";
|
||||
|
||||
bool aggregatedSearch = false;
|
||||
|
||||
var focusNode = FocusNode();
|
||||
|
||||
var options = <String>[];
|
||||
@@ -36,15 +39,21 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
|
||||
void search([String? text]) {
|
||||
context
|
||||
.to(
|
||||
() => SearchResultPage(
|
||||
text: text ?? controller.text,
|
||||
sourceKey: searchTarget,
|
||||
options: options,
|
||||
),
|
||||
)
|
||||
.then((_) => update());
|
||||
if (aggregatedSearch) {
|
||||
context
|
||||
.to(() => AggregatedSearchPage(keyword: text ?? controller.text))
|
||||
.then((_) => update());
|
||||
} else {
|
||||
context
|
||||
.to(
|
||||
() => SearchResultPage(
|
||||
text: text ?? controller.text,
|
||||
sourceKey: searchTarget,
|
||||
options: options,
|
||||
),
|
||||
)
|
||||
.then((_) => update());
|
||||
}
|
||||
}
|
||||
|
||||
var suggestions = <Pair<String, TranslationType>>[];
|
||||
@@ -189,6 +198,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.search),
|
||||
title: Text("Search in".tl),
|
||||
),
|
||||
Wrap(
|
||||
@@ -197,8 +207,9 @@ class _SearchPageState extends State<SearchPage> {
|
||||
children: sources.map((e) {
|
||||
return OptionChip(
|
||||
text: e.name,
|
||||
isSelected: searchTarget == e.key,
|
||||
isSelected: searchTarget == e.key || aggregatedSearch,
|
||||
onTap: () {
|
||||
if (aggregatedSearch) return;
|
||||
setState(() {
|
||||
searchTarget = e.key;
|
||||
useDefaultOptions();
|
||||
@@ -207,6 +218,18 @@ class _SearchPageState extends State<SearchPage> {
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text("Aggregated Search".tl),
|
||||
leading: Checkbox(
|
||||
value: aggregatedSearch,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
aggregatedSearch = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -221,6 +244,10 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
|
||||
Widget buildSearchOptions() {
|
||||
if (aggregatedSearch) {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
|
||||
var children = <Widget>[];
|
||||
|
||||
final searchOptions =
|
||||
@@ -262,9 +289,9 @@ class _SearchPageState extends State<SearchPage> {
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == 0) {
|
||||
return const Divider(
|
||||
thickness: 0.6,
|
||||
).paddingTop(16);
|
||||
return const SizedBox(
|
||||
height: 16,
|
||||
);
|
||||
}
|
||||
if (index == 1) {
|
||||
return ListTile(
|
||||
|
@@ -14,14 +14,14 @@ class SearchResultPage extends StatefulWidget {
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.sourceKey,
|
||||
required this.options,
|
||||
this.options,
|
||||
});
|
||||
|
||||
final String text;
|
||||
|
||||
final String sourceKey;
|
||||
|
||||
final List<String> options;
|
||||
final List<String>? options;
|
||||
|
||||
@override
|
||||
State<SearchResultPage> createState() => _SearchResultPageState();
|
||||
@@ -99,7 +99,7 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
onSearch: search,
|
||||
);
|
||||
sourceKey = widget.sourceKey;
|
||||
options = widget.options;
|
||||
options = widget.options ?? const [];
|
||||
validateOptions();
|
||||
text = widget.text;
|
||||
appdata.addSearchHistory(text);
|
||||
|
@@ -29,6 +29,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||
title: "Theme Color".tl,
|
||||
settingKey: "color",
|
||||
optionTranslation: {
|
||||
"system": "System".tl,
|
||||
"red": "Red".tl,
|
||||
"pink": "Pink".tl,
|
||||
"purple": "Purple".tl,
|
||||
|
@@ -94,7 +94,7 @@ class _ExploreSettingsState extends State<ExploreSettings> {
|
||||
}
|
||||
|
||||
class _ManageBlockingWordView extends StatefulWidget {
|
||||
const _ManageBlockingWordView({super.key});
|
||||
const _ManageBlockingWordView();
|
||||
|
||||
@override
|
||||
State<_ManageBlockingWordView> createState() =>
|
||||
@@ -135,7 +135,7 @@ class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> {
|
||||
void add() {
|
||||
showDialog(
|
||||
context: App.rootContext,
|
||||
barrierColor: Colors.black.withOpacity(0.1),
|
||||
barrierColor: Colors.black.toOpacity(0.1),
|
||||
builder: (context) {
|
||||
var controller = TextEditingController();
|
||||
String? error;
|
||||
|
@@ -384,7 +384,7 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
|
||||
Widget build(BuildContext context) {
|
||||
var tiles = keys.map((e) => buildItem(e)).toList();
|
||||
|
||||
var view = ReorderableBuilder(
|
||||
var view = ReorderableBuilder<String>(
|
||||
key: reorderWidgetKey,
|
||||
scrollController: scrollController,
|
||||
longPressDelay: App.isDesktop
|
||||
@@ -542,7 +542,7 @@ class _SettingPartTitle extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: context.colorScheme.onSurface.withOpacity(0.1),
|
||||
color: context.colorScheme.onSurface.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@@ -267,7 +267,7 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
|
||||
height: 46,
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? colors.primaryContainer.withOpacity(0.36) : null,
|
||||
color: selected ? colors.primaryContainer.toOpacity(0.36) : null,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: selected ? colors.primary : Colors.transparent,
|
||||
|
210
lib/utils/epub.dart
Normal file
210
lib/utils/epub.dart
Normal file
@@ -0,0 +1,210 @@
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/utils/file_type.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:zip_flutter/zip_flutter.dart';
|
||||
|
||||
class EpubData {
|
||||
final String title;
|
||||
|
||||
final String author;
|
||||
|
||||
final File cover;
|
||||
|
||||
final Map<String, List<File>> chapters;
|
||||
|
||||
const EpubData({
|
||||
required this.title,
|
||||
required this.author,
|
||||
required this.cover,
|
||||
required this.chapters,
|
||||
});
|
||||
}
|
||||
|
||||
Future<File> createEpubComic(EpubData data, String cacheDir) async {
|
||||
final workingDir = Directory(FilePath.join(cacheDir, 'epub'));
|
||||
if (workingDir.existsSync()) {
|
||||
workingDir.deleteSync(recursive: true);
|
||||
}
|
||||
workingDir.createSync(recursive: true);
|
||||
|
||||
// mimetype
|
||||
workingDir.joinFile('mimetype').writeAsStringSync('application/epub+zip');
|
||||
|
||||
// META-INF
|
||||
Directory(FilePath.join(workingDir.path, 'META-INF')).createSync();
|
||||
File(FilePath.join(workingDir.path, 'META-INF', 'container.xml'))
|
||||
.writeAsStringSync('''
|
||||
<?xml version="1.0"?>
|
||||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||
<rootfiles>
|
||||
<rootfile full-path="content.opf" media-type="application/oebps-package+xml"/>
|
||||
</rootfiles>
|
||||
</container>
|
||||
''');
|
||||
|
||||
Directory(FilePath.join(workingDir.path, 'OEBPS')).createSync();
|
||||
|
||||
// copy images, create html files
|
||||
final imageDir = Directory(FilePath.join(workingDir.path, 'OEBPS', 'images'));
|
||||
imageDir.createSync();
|
||||
final coverExt = data.cover.extension;
|
||||
final coverMime = FileType.fromExtension(coverExt).mime;
|
||||
imageDir
|
||||
.joinFile('cover.$coverExt')
|
||||
.writeAsBytesSync(data.cover.readAsBytesSync());
|
||||
int imgIndex = 0;
|
||||
int chapterIndex = 0;
|
||||
var manifestStrBuilder = StringBuffer();
|
||||
manifestStrBuilder.writeln(
|
||||
' <item id="cover_image" href="OEBPS/images/cover.$coverExt" media-type="$coverMime"/>');
|
||||
manifestStrBuilder.writeln(
|
||||
' <item id="toc" href="toc.ncx" media-type="application/x-dtbncx+xml"/>');
|
||||
for (final chapter in data.chapters.keys) {
|
||||
var images = <String>[];
|
||||
for (final image in data.chapters[chapter]!) {
|
||||
final ext = image.extension;
|
||||
imageDir
|
||||
.joinFile('img$imgIndex.$ext')
|
||||
.writeAsBytesSync(image.readAsBytesSync());
|
||||
images.add('images/img$imgIndex.$ext');
|
||||
var mime = FileType.fromExtension(ext).mime;
|
||||
manifestStrBuilder.writeln(
|
||||
' <item id="img$imgIndex" href="OEBPS/images/img$imgIndex$ext" media-type="$mime"/>');
|
||||
imgIndex++;
|
||||
}
|
||||
var html =
|
||||
File(FilePath.join(workingDir.path, 'OEBPS', '$chapterIndex.html'));
|
||||
html.writeAsStringSync('''
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
|
||||
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>$chapter</title>
|
||||
<style type="text/css">
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>$chapter</h1>
|
||||
<div>
|
||||
${images.map((e) => ' <img src="$e" alt="$e"/>').join('\n')}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
''');
|
||||
manifestStrBuilder.writeln(
|
||||
' <item id="chapter$chapterIndex" href="OEBPS/$chapterIndex.html" media-type="application/xhtml+xml"/>');
|
||||
chapterIndex++;
|
||||
}
|
||||
|
||||
// content.opf
|
||||
final contentOpf =
|
||||
File(FilePath.join(workingDir.path, 'content.opf'));
|
||||
final uuid = const Uuid().v4();
|
||||
var spineStrBuilder = StringBuffer();
|
||||
for (var i = 0; i < chapterIndex; i++) {
|
||||
var idRef = 'idref="chapter$i"';
|
||||
spineStrBuilder.writeln(' <itemref $idRef/>');
|
||||
}
|
||||
contentOpf.writeAsStringSync('''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<package version="3.0"
|
||||
xmlns="http://www.idpf.org/2007/opf"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<metadata>
|
||||
<dc:title>${data.title}</dc:title>
|
||||
<dc:creator>${data.author}</dc:creator>
|
||||
<dc:identifier id="book_id">urn:uuid:$uuid</dc:identifier>
|
||||
<meta name="cover" content="cover_image"/>
|
||||
</metadata>
|
||||
<manifest>
|
||||
${manifestStrBuilder.toString()}
|
||||
</manifest>
|
||||
<spine toc="toc">
|
||||
${spineStrBuilder.toString()}
|
||||
</spine>
|
||||
</package>
|
||||
''');
|
||||
|
||||
// toc.ncx
|
||||
final tocNcx = File(FilePath.join(workingDir.path, 'toc.ncx'));
|
||||
var navMapStrBuilder = StringBuffer();
|
||||
var playOrder = 2;
|
||||
final chapterNames = data.chapters.keys.toList();
|
||||
for (var i = 0; i < chapterIndex; i++) {
|
||||
navMapStrBuilder
|
||||
.writeln(' <navPoint id="chapter$i" playOrder="$playOrder">');
|
||||
navMapStrBuilder.writeln(
|
||||
' <navLabel><text>${chapterNames[i]}</text></navLabel>');
|
||||
navMapStrBuilder.writeln(' <content src="OEBPS/$i.html"/>');
|
||||
navMapStrBuilder.writeln(' </navPoint>');
|
||||
playOrder++;
|
||||
}
|
||||
|
||||
tocNcx.writeAsStringSync('''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">
|
||||
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx" version="2005-1">
|
||||
<head>
|
||||
<meta name="dtb:uid" content="urn:uuid:$uuid"/>
|
||||
<meta name="dtb:depth" content="1"/>
|
||||
<meta name="dtb:totalPageCount" content="0"/>
|
||||
<meta name="dtb:maxPageNumber" content="0"/>
|
||||
</head>
|
||||
<docTitle>
|
||||
<text>${data.title}</text>
|
||||
</docTitle>
|
||||
<navMap>
|
||||
${navMapStrBuilder.toString()}
|
||||
</navMap>
|
||||
</ncx>
|
||||
''');
|
||||
|
||||
// zip
|
||||
final zipPath = FilePath.join(cacheDir, '${data.title}.epub');
|
||||
ZipFile.compressFolder(workingDir.path, zipPath);
|
||||
|
||||
workingDir.deleteSync(recursive: true);
|
||||
|
||||
return File(zipPath);
|
||||
}
|
||||
|
||||
Future<File> createEpubWithLocalComic(LocalComic comic) async {
|
||||
var chapters = <String, List<File>>{};
|
||||
if (comic.chapters == null) {
|
||||
chapters[comic.title] =
|
||||
(await LocalManager().getImages(comic.id, comic.comicType, 0))
|
||||
.map((e) => File(e))
|
||||
.toList();
|
||||
} else {
|
||||
for (var chapter in comic.chapters!.keys) {
|
||||
chapters[comic.chapters![chapter]!] = (await LocalManager()
|
||||
.getImages(comic.id, comic.comicType, chapter))
|
||||
.map((e) => File(e))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
var data = EpubData(
|
||||
title: comic.title,
|
||||
author: comic.subtitle,
|
||||
cover: comic.coverFile,
|
||||
chapters: chapters,
|
||||
);
|
||||
|
||||
final cacheDir = App.cachePath;
|
||||
|
||||
return Isolate.run(() => overrideIO(() async {
|
||||
return createEpubComic(data, cacheDir);
|
||||
}));
|
||||
}
|
182
pubspec.lock
182
pubspec.lock
@@ -125,18 +125,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.19.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -157,10 +157,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.0.2"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -194,6 +194,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
dynamic_color:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dynamic_color
|
||||
sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -214,10 +222,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
version: "7.0.1"
|
||||
file_selector:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -230,10 +238,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_android
|
||||
sha256: ec439df07c4999faad319ce8ad9e971795c2f1d7132ad5a793b9370a863c6128
|
||||
sha256: "98ac58e878b05ea2fdb204e7f4fc4978d90406c9881874f901428e01d3b18fbc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.1+10"
|
||||
version: "0.5.1+12"
|
||||
file_selector_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -246,10 +254,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2"
|
||||
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3"
|
||||
version: "0.9.3+2"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -286,10 +294,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -323,10 +331,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_inappwebview_internal_annotations
|
||||
sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8"
|
||||
sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.2.0"
|
||||
flutter_inappwebview_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -371,15 +379,23 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "5.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_memory_info:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_memory_info
|
||||
sha256: "1f112f1d7503aa1681fc8e923f6cd0e847bb2fbeec3753ed021cf1e5f7e9cd74"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.1"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -401,18 +417,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_reorderable_grid_view
|
||||
sha256: "93a2b9e279bf40b9333428a67e70e520ca1528554984eb6f6304538400897e64"
|
||||
sha256: "732bcb1b29d5130c11a70e6acec512941fafe241f0e80bffd93ca6e415819915"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.2"
|
||||
version: "5.4.0"
|
||||
flutter_rust_bridge:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_rust_bridge
|
||||
sha256: "5fe868d3cb8cbc4d83091748552e03f00ccfa41b8e44691bc382611f831d5f8b"
|
||||
sha256: fb9d3c9395eae3c71d4fe3ec343b9f30636c9988150c8bb33b60047549b34e3d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.1"
|
||||
version: "2.6.0"
|
||||
flutter_saf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -485,10 +501,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
version: "4.1.1"
|
||||
http_profile:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_profile
|
||||
sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -509,10 +533,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.0.5"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -533,18 +557,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.5"
|
||||
version: "10.0.7"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.8"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -557,10 +581,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "5.1.0"
|
||||
local_auth:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -670,26 +694,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.1.5"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
|
||||
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.10"
|
||||
version: "2.2.15"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
|
||||
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -743,10 +767,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
version: "3.1.6"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -775,10 +799,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: rhttp
|
||||
sha256: "92fb57dea6338370efe1e4e2101e8b521f91f15bc60ef6908469b4392dd9803a"
|
||||
sha256: "581d57b5b6056d31489af94db8653a1c11d7b59050cbbc41ece4279e50414de5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.1"
|
||||
version: "0.9.6"
|
||||
screen_retriever:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -844,11 +868,19 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1"
|
||||
shimmer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shimmer
|
||||
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
version: "0.0.0"
|
||||
sliver_tools:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -877,26 +909,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3
|
||||
sha256: bb174b3ec2527f9c5f680f73a89af8149dd99782fbb56ea88ad0807c5638f2ed
|
||||
sha256: cb7f4e9dc1b52b1fa350f7b3d41c662e75fc3d399555fa4e5efcf267e9a4fbb5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.7"
|
||||
version: "2.5.0"
|
||||
sqlite3_flutter_libs:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqlite3_flutter_libs
|
||||
sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1"
|
||||
sha256: "636b0fe8a2de894e5455572f6cbbc458f4ffecfe9f860b79439e27041ea4f0b9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.24"
|
||||
version: "0.5.27"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "1.12.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -909,10 +941,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -925,18 +957,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
version: "0.7.3"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
version: "1.4.0"
|
||||
upower:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -949,42 +981,42 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.3.1"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79
|
||||
sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.9"
|
||||
version: "6.3.14"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
|
||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.1"
|
||||
version: "6.3.2"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
|
||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.2.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672"
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.2.2"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1005,10 +1037,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
|
||||
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
version: "3.1.3"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1029,10 +1061,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.5"
|
||||
version: "14.3.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1054,10 +1086,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a"
|
||||
sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.4"
|
||||
version: "5.9.0"
|
||||
window_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1070,10 +1102,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1100,5 +1132,5 @@ packages:
|
||||
source: git
|
||||
version: "0.0.1"
|
||||
sdks:
|
||||
dart: ">=3.5.4 <4.0.0"
|
||||
flutter: ">=3.24.5"
|
||||
dart: ">=3.6.0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
|
17
pubspec.yaml
17
pubspec.yaml
@@ -2,11 +2,11 @@ name: venera
|
||||
description: "A comic app."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.8+108
|
||||
version: 1.1.1+111
|
||||
|
||||
environment:
|
||||
sdk: '>=3.5.0 <4.0.0'
|
||||
flutter: 3.24.5
|
||||
sdk: '>=3.6.0 <4.0.0'
|
||||
flutter: 3.27.0
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
@@ -14,7 +14,7 @@ dependencies:
|
||||
path_provider: any
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
intl: any
|
||||
intl: ^0.19.0
|
||||
window_manager: ^0.4.3
|
||||
sqlite3: ^2.4.7
|
||||
sqlite3_flutter_libs: any
|
||||
@@ -39,7 +39,7 @@ dependencies:
|
||||
url: https://github.com/venera-app/flutter.widgets
|
||||
ref: 09e756b1f1b04e6298318d99ec20a787fb360f59
|
||||
path: packages/scrollable_positioned_list
|
||||
flutter_reorderable_grid_view: 5.3.2
|
||||
flutter_reorderable_grid_view: ^5.4.0
|
||||
yaml: any
|
||||
uuid: ^4.5.1
|
||||
desktop_webview_window:
|
||||
@@ -58,7 +58,7 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/venera-app/lodepng_flutter
|
||||
ref: d1c96cd6503103b3270dfe2f320d4a1c93780f53
|
||||
rhttp: 0.9.1
|
||||
rhttp: 0.9.6
|
||||
webdav_client:
|
||||
git:
|
||||
url: https://github.com/wgh136/webdav_client
|
||||
@@ -70,11 +70,14 @@ dependencies:
|
||||
url: https://github.com/pkuislm/flutter_saf.git
|
||||
ref: 3315082b9f7055655610e4f6f136b69e48228c05
|
||||
pdf: ^3.11.1
|
||||
dynamic_color: ^1.7.0
|
||||
shimmer: ^3.0.0
|
||||
flutter_memory_info: ^0.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^3.0.0
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_to_arch:
|
||||
git: https://github.com/wgh136/flutter_to_arch
|
||||
flutter_to_debian:
|
||||
|
@@ -58,6 +58,7 @@ Source: "{#RootPath}\build\windows\x64\runner\Release\local_auth_windows_plugin.
|
||||
Source: "{#RootPath}\build\windows\x64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\x64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\x64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\x64\runner\Release\dynamic_color_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "{#RootPath}\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
@@ -66,4 +67,4 @@ Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall
|
||||
|
Reference in New Issue
Block a user