mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
Compare commits
23 Commits
v1.4.7-dev
...
7fa48cec29
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7fa48cec29 | ||
e549a18dbf | |||
c17c4abb5b | |||
af57bc31b1 | |||
16449a1440 | |||
a7c1983f35 | |||
4c257d7178 | |||
3a9d634edf | |||
![]() |
e179c8f67f | ||
![]() |
c4b85471c1 | ||
![]() |
a898b57d96 | ||
50c6bec4cd | |||
![]() |
8c44f83d6c | ||
![]() |
103b6b2832 | ||
4129349c70 | |||
77a9aa5457 | |||
97940b9492 | |||
7945c0e54f | |||
dfee65c3af | |||
fa2dbd79f6 | |||
9a9f539906 | |||
d7331f36e9 | |||
![]() |
d0b76de465 |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version-file: pubspec.yaml
|
flutter-version-file: pubspec.yaml
|
||||||
architecture: x64
|
architecture: x64
|
||||||
- run: sudo xcode-select --switch /Applications/Xcode_16.0.app
|
- run: sudo xcode-select --switch /Applications/Xcode_16.4.app
|
||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
# Step 1: Decode and install the certificate
|
# Step 1: Decode and install the certificate
|
||||||
- name: Decode and install certificate
|
- name: Decode and install certificate
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version-file: pubspec.yaml
|
flutter-version-file: pubspec.yaml
|
||||||
architecture: x64
|
architecture: x64
|
||||||
- run: sudo xcode-select --switch /Applications/Xcode_16.0.app
|
- run: sudo xcode-select --switch /Applications/Xcode_16.4.app
|
||||||
- run: flutter pub get
|
- run: flutter pub get
|
||||||
- run: flutter build ios --release --no-codesign
|
- run: flutter build ios --release --no-codesign
|
||||||
- run: |
|
- run: |
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
# venera
|
# venera
|
||||||
[](https://flutter.dev/)
|
[](https://flutter.dev/)
|
||||||
[](https://github.com/venera-app/venera/blob/master/LICENSE)
|
[](https://github.com/venera-app/venera/blob/master/LICENSE)
|
||||||
[](https://github.com/venera-app/venera/releases)
|
|
||||||
[](https://github.com/venera-app/venera/stargazers)
|
[](https://github.com/venera-app/venera/stargazers)
|
||||||
[](https://t.me/venera_release)
|
[](https://t.me/venera_release)
|
||||||
|
|
||||||
A comic reader that support reading local and network comics.
|
[](https://github.com/venera-app/venera/releases)
|
||||||
|
[](https://aur.archlinux.org/packages/venera-bin)
|
||||||
|
[](https://f-droid.org/packages/com.github.wgh136.venera/)
|
||||||
|
|
||||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
A comic reader that support reading local and network comics.
|
||||||
alt="Get it on F-Droid"
|
|
||||||
height="75">](https://f-droid.org/packages/com.github.wgh136.venera/)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Read local comics
|
- Read local comics
|
||||||
|
@@ -34,6 +34,12 @@ android {
|
|||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion "28.0.13004108"
|
ndkVersion "28.0.13004108"
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
splits{
|
splits{
|
||||||
abi {
|
abi {
|
||||||
reset()
|
reset()
|
||||||
@@ -78,6 +84,9 @@ android {
|
|||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
|
// Temporarily solution to fix crash
|
||||||
|
minifyEnabled false
|
||||||
|
shrinkResources false
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
@@ -58,8 +59,6 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
<!-- [flutter 3.27.1] Impeller is still worse than skia, disable it -->
|
|
||||||
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false"/>
|
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
@@ -19,7 +19,7 @@ pluginManagement {
|
|||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version '8.9.0' apply false
|
id "com.android.application" version '8.9.0' apply false
|
||||||
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
|
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include ":app"
|
include ":app"
|
||||||
|
@@ -4,6 +4,18 @@ Venera JavaScript Library
|
|||||||
This library provides a set of APIs for interacting with the Venera app.
|
This library provides a set of APIs for interacting with the Venera app.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function sendMessage
|
||||||
|
* @global
|
||||||
|
* @param {Object} message
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a timeout to execute a callback function after a specified delay.
|
||||||
|
* @param callback {Function}
|
||||||
|
* @param delay {number} - delay in milliseconds
|
||||||
|
*/
|
||||||
function setTimeout(callback, delay) {
|
function setTimeout(callback, delay) {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
method: 'delay',
|
method: 'delay',
|
||||||
@@ -1412,3 +1424,18 @@ function getClipboard() {
|
|||||||
method: 'getClipboard'
|
method: 'getClipboard'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a function with arguments. The function will be executed in the engine pool which is not in the main thread.
|
||||||
|
* @param func {string} - A js code string which can be evaluated to a function. The function will receive the args as its only argument.
|
||||||
|
* @param args {any[]} - The arguments to pass to the function.
|
||||||
|
* @returns {Promise<any>} - The result of the function.
|
||||||
|
* @since 1.5.0
|
||||||
|
*/
|
||||||
|
function compute(func, ...args) {
|
||||||
|
return sendMessage({
|
||||||
|
method: 'compute',
|
||||||
|
function: func,
|
||||||
|
args: args
|
||||||
|
})
|
||||||
|
}
|
@@ -17,6 +17,7 @@ ImageProvider? _findImageProvider(Comic comic) {
|
|||||||
comic.cover,
|
comic.cover,
|
||||||
sourceKey: comic.sourceKey,
|
sourceKey: comic.sourceKey,
|
||||||
cid: comic.id,
|
cid: comic.id,
|
||||||
|
fallbackToLocalCover: comic is FavoriteItem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return image;
|
return image;
|
||||||
|
@@ -7,6 +7,7 @@ class NetworkError extends StatelessWidget {
|
|||||||
this.retry,
|
this.retry,
|
||||||
this.withAppbar = true,
|
this.withAppbar = true,
|
||||||
this.buttonText,
|
this.buttonText,
|
||||||
|
this.action,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String message;
|
final String message;
|
||||||
@@ -17,6 +18,8 @@ class NetworkError extends StatelessWidget {
|
|||||||
|
|
||||||
final String? buttonText;
|
final String? buttonText;
|
||||||
|
|
||||||
|
final Widget? action;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var cfe = CloudflareException.fromString(message);
|
var cfe = CloudflareException.fromString(message);
|
||||||
@@ -67,9 +70,16 @@ class NetworkError extends StatelessWidget {
|
|||||||
child: Text('Verify'.tl),
|
child: Text('Verify'.tl),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
FilledButton(
|
Row(
|
||||||
onPressed: retry,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: Text(buttonText ?? 'Retry'.tl),
|
children: [
|
||||||
|
if (action != null)
|
||||||
|
action!.paddingRight(8),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: retry,
|
||||||
|
child: Text(buttonText ?? 'Retry'.tl),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@@ -7,8 +7,11 @@ class PaneItemEntry {
|
|||||||
|
|
||||||
IconData activeIcon;
|
IconData activeIcon;
|
||||||
|
|
||||||
PaneItemEntry(
|
PaneItemEntry({
|
||||||
{required this.label, required this.icon, required this.activeIcon});
|
required this.label,
|
||||||
|
required this.icon,
|
||||||
|
required this.activeIcon,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class PaneActionEntry {
|
class PaneActionEntry {
|
||||||
@@ -18,20 +21,24 @@ class PaneActionEntry {
|
|||||||
|
|
||||||
VoidCallback onTap;
|
VoidCallback onTap;
|
||||||
|
|
||||||
PaneActionEntry(
|
PaneActionEntry({
|
||||||
{required this.label, required this.icon, required this.onTap});
|
required this.label,
|
||||||
|
required this.icon,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class NaviPane extends StatefulWidget {
|
class NaviPane extends StatefulWidget {
|
||||||
const NaviPane(
|
const NaviPane({
|
||||||
{required this.paneItems,
|
required this.paneItems,
|
||||||
required this.paneActions,
|
required this.paneActions,
|
||||||
required this.pageBuilder,
|
required this.pageBuilder,
|
||||||
this.initialPage = 0,
|
this.initialPage = 0,
|
||||||
this.onPageChanged,
|
this.onPageChanged,
|
||||||
required this.observer,
|
required this.observer,
|
||||||
required this.navigatorKey,
|
required this.navigatorKey,
|
||||||
super.key});
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final List<PaneItemEntry> paneItems;
|
final List<PaneItemEntry> paneItems;
|
||||||
|
|
||||||
@@ -187,7 +194,8 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
child: buildLeft(),
|
child: buildLeft(),
|
||||||
),
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
left: _kFoldedSideBarWidth * ((value - 1).clamp(0, 1)) +
|
left:
|
||||||
|
_kFoldedSideBarWidth * ((value - 1).clamp(0, 1)) +
|
||||||
(_kSideBarWidth - _kFoldedSideBarWidth) *
|
(_kSideBarWidth - _kFoldedSideBarWidth) *
|
||||||
((value - 2).clamp(0, 1)),
|
((value - 2).clamp(0, 1)),
|
||||||
child: buildMainView(),
|
child: buildMainView(),
|
||||||
@@ -202,14 +210,19 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
Widget buildMainView() {
|
Widget buildMainView() {
|
||||||
return HeroControllerScope(
|
return HeroControllerScope(
|
||||||
controller: MaterialApp.createMaterialHeroController(),
|
controller: MaterialApp.createMaterialHeroController(),
|
||||||
child: Navigator(
|
child: NavigatorPopHandler(
|
||||||
observers: [widget.observer],
|
onPopWithResult: (result) {
|
||||||
key: widget.navigatorKey,
|
widget.navigatorKey.currentState?.maybePop(result);
|
||||||
onGenerateRoute: (settings) => AppPageRoute(
|
},
|
||||||
preventRebuild: false,
|
child: Navigator(
|
||||||
builder: (context) {
|
observers: [widget.observer],
|
||||||
return _NaviMainView(state: this);
|
key: widget.navigatorKey,
|
||||||
},
|
onGenerateRoute: (settings) => AppPageRoute(
|
||||||
|
preventRebuild: false,
|
||||||
|
builder: (context) {
|
||||||
|
return _NaviMainView(state: this);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -239,7 +252,7 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
icon: Icon(action.icon),
|
icon: Icon(action.icon),
|
||||||
onPressed: action.onTap,
|
onPressed: action.onTap,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -261,21 +274,18 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: List<Widget>.generate(
|
children: List<Widget>.generate(widget.paneItems.length, (index) {
|
||||||
widget.paneItems.length,
|
return Expanded(
|
||||||
(index) {
|
child: _SingleBottomNaviWidget(
|
||||||
return Expanded(
|
enabled: currentPage == index,
|
||||||
child: _SingleBottomNaviWidget(
|
entry: widget.paneItems[index],
|
||||||
enabled: currentPage == index,
|
onTap: () {
|
||||||
entry: widget.paneItems[index],
|
updatePage(index);
|
||||||
onTap: () {
|
},
|
||||||
updatePage(index);
|
key: ValueKey(index),
|
||||||
},
|
),
|
||||||
key: ValueKey(index),
|
);
|
||||||
),
|
}),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -286,7 +296,8 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
const paddingHorizontal = 12.0;
|
const paddingHorizontal = 12.0;
|
||||||
return Material(
|
return Material(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: _kFoldedSideBarWidth +
|
width:
|
||||||
|
_kFoldedSideBarWidth +
|
||||||
(_kSideBarWidth - _kFoldedSideBarWidth) * ((value - 2).clamp(0, 1)),
|
(_kSideBarWidth - _kFoldedSideBarWidth) * ((value - 2).clamp(0, 1)),
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: paddingHorizontal),
|
padding: const EdgeInsets.symmetric(horizontal: paddingHorizontal),
|
||||||
@@ -323,9 +334,7 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
key: ValueKey(index + widget.paneItems.length),
|
key: ValueKey(index + widget.paneItems.length),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(height: 16),
|
||||||
height: 16,
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -334,12 +343,13 @@ class NaviPaneState extends State<NaviPane>
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SideNaviWidget extends StatelessWidget {
|
class _SideNaviWidget extends StatelessWidget {
|
||||||
const _SideNaviWidget(
|
const _SideNaviWidget({
|
||||||
{required this.enabled,
|
required this.enabled,
|
||||||
required this.entry,
|
required this.entry,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.showTitle,
|
required this.showTitle,
|
||||||
super.key});
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
|
|
||||||
@@ -368,18 +378,18 @@ class _SideNaviWidget extends StatelessWidget {
|
|||||||
? Row(
|
? Row(
|
||||||
children: [icon, const SizedBox(width: 12), Text(entry.label)],
|
children: [icon, const SizedBox(width: 12), Text(entry.label)],
|
||||||
)
|
)
|
||||||
: Align(
|
: Align(alignment: Alignment.centerLeft, child: icon),
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: icon,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
).paddingVertical(4);
|
).paddingVertical(4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PaneActionWidget extends StatelessWidget {
|
class _PaneActionWidget extends StatelessWidget {
|
||||||
const _PaneActionWidget(
|
const _PaneActionWidget({
|
||||||
{required this.entry, required this.showTitle, super.key});
|
required this.entry,
|
||||||
|
required this.showTitle,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final PaneActionEntry entry;
|
final PaneActionEntry entry;
|
||||||
|
|
||||||
@@ -399,21 +409,19 @@ class _PaneActionWidget extends StatelessWidget {
|
|||||||
? Row(
|
? Row(
|
||||||
children: [icon, const SizedBox(width: 12), Text(entry.label)],
|
children: [icon, const SizedBox(width: 12), Text(entry.label)],
|
||||||
)
|
)
|
||||||
: Align(
|
: Align(alignment: Alignment.centerLeft, child: icon),
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: icon,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
).paddingVertical(4);
|
).paddingVertical(4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SingleBottomNaviWidget extends StatefulWidget {
|
class _SingleBottomNaviWidget extends StatefulWidget {
|
||||||
const _SingleBottomNaviWidget(
|
const _SingleBottomNaviWidget({
|
||||||
{required this.enabled,
|
required this.enabled,
|
||||||
required this.entry,
|
required this.entry,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
super.key});
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
|
|
||||||
@@ -482,8 +490,9 @@ class _SingleBottomNaviWidgetState extends State<_SingleBottomNaviWidget>
|
|||||||
Widget buildContent() {
|
Widget buildContent() {
|
||||||
final value = controller.value;
|
final value = controller.value;
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final icon =
|
final icon = Icon(
|
||||||
Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon);
|
widget.enabled ? widget.entry.activeIcon : widget.entry.icon,
|
||||||
|
);
|
||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 64,
|
width: 64,
|
||||||
@@ -570,8 +579,11 @@ class NaviObserver extends NavigatorObserver implements Listenable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _NaviPopScope extends StatelessWidget {
|
class _NaviPopScope extends StatelessWidget {
|
||||||
const _NaviPopScope(
|
const _NaviPopScope({
|
||||||
{required this.child, this.popGesture = false, required this.action});
|
required this.child,
|
||||||
|
this.popGesture = false,
|
||||||
|
required this.action,
|
||||||
|
});
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final bool popGesture;
|
final bool popGesture;
|
||||||
@@ -581,32 +593,25 @@ class _NaviPopScope extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget res = App.isIOS
|
Widget res = child;
|
||||||
? child
|
|
||||||
: PopScope(
|
|
||||||
canPop: App.isAndroid ? false : true,
|
|
||||||
onPopInvokedWithResult: (value, result) {
|
|
||||||
action();
|
|
||||||
},
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
if (popGesture) {
|
if (popGesture) {
|
||||||
res = GestureDetector(
|
res = GestureDetector(
|
||||||
onPanStart: (details) {
|
onPanStart: (details) {
|
||||||
if (details.globalPosition.dx < 64) {
|
if (details.globalPosition.dx < 64) {
|
||||||
panStartAtEdge = true;
|
panStartAtEdge = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPanEnd: (details) {
|
||||||
|
if (details.velocity.pixelsPerSecond.dx < 0 ||
|
||||||
|
details.velocity.pixelsPerSecond.dx > 0) {
|
||||||
|
if (panStartAtEdge) {
|
||||||
|
action();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onPanEnd: (details) {
|
panStartAtEdge = false;
|
||||||
if (details.velocity.pixelsPerSecond.dx < 0 ||
|
},
|
||||||
details.velocity.pixelsPerSecond.dx > 0) {
|
child: res,
|
||||||
if (panStartAtEdge) {
|
);
|
||||||
action();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panStartAtEdge = false;
|
|
||||||
},
|
|
||||||
child: res);
|
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
@@ -237,7 +237,7 @@ class _AppScrollBarState extends State<AppScrollBar> {
|
|||||||
|
|
||||||
double viewHeight = 0;
|
double viewHeight = 0;
|
||||||
|
|
||||||
final _scrollIndicatorSize = App.isDesktop ? 42.0 : 64.0;
|
final _scrollIndicatorSize = App.isDesktop ? 36.0 : 54.0;
|
||||||
|
|
||||||
late final VerticalDragGestureRecognizer _dragGestureRecognizer;
|
late final VerticalDragGestureRecognizer _dragGestureRecognizer;
|
||||||
|
|
||||||
@@ -354,7 +354,7 @@ class _ScrollIndicatorPainter extends CustomPainter {
|
|||||||
Offset(size.width, 0),
|
Offset(size.width, 0),
|
||||||
radius: Radius.circular(size.width),
|
radius: Radius.circular(size.width),
|
||||||
);
|
);
|
||||||
canvas.drawShadow(path, shadowColor, 4, true);
|
canvas.drawShadow(path, shadowColor, 2, true);
|
||||||
var backgroundPaint = Paint()
|
var backgroundPaint = Paint()
|
||||||
..color = backgroundColor
|
..color = backgroundColor
|
||||||
..style = PaintingStyle.fill;
|
..style = PaintingStyle.fill;
|
||||||
|
@@ -13,7 +13,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.4.6";
|
final version = "1.5.1";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
@@ -30,6 +30,10 @@ class _App {
|
|||||||
|
|
||||||
bool get isMobile => Platform.isAndroid || Platform.isIOS;
|
bool get isMobile => Platform.isAndroid || Platform.isIOS;
|
||||||
|
|
||||||
|
// Whether the app has been initialized.
|
||||||
|
// If current Isolate is main Isolate, this value is always true.
|
||||||
|
bool isInitialized = false;
|
||||||
|
|
||||||
Locale get locale {
|
Locale get locale {
|
||||||
Locale deviceLocale = PlatformDispatcher.instance.locale;
|
Locale deviceLocale = PlatformDispatcher.instance.locale;
|
||||||
if (deviceLocale.languageCode == "zh" &&
|
if (deviceLocale.languageCode == "zh" &&
|
||||||
@@ -81,6 +85,7 @@ class _App {
|
|||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
externalStoragePath = (await getExternalStorageDirectory())!.path;
|
externalStoragePath = (await getExternalStorageDirectory())!.path;
|
||||||
}
|
}
|
||||||
|
isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initComponents() async {
|
Future<void> initComponents() async {
|
||||||
|
@@ -2,6 +2,7 @@ import 'dart:math';
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
|
|
||||||
const double _kBackGestureWidth = 20.0;
|
const double _kBackGestureWidth = 20.0;
|
||||||
const int _kMaxDroppedSwipePageForwardAnimationTime = 800;
|
const int _kMaxDroppedSwipePageForwardAnimationTime = 800;
|
||||||
@@ -115,7 +116,14 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||||
return SlidePageTransitionBuilder().buildTransitions(
|
PageTransitionsBuilder builder;
|
||||||
|
if (App.isAndroid) {
|
||||||
|
builder = PredictiveBackPageTransitionsBuilder();
|
||||||
|
} else {
|
||||||
|
builder = SlidePageTransitionBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.buildTransitions(
|
||||||
this,
|
this,
|
||||||
context,
|
context,
|
||||||
animation,
|
animation,
|
||||||
|
@@ -401,9 +401,14 @@ class SearchOptions {
|
|||||||
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(
|
||||||
String category, String? param, List<String> options, int page);
|
String category, String? param, List<String> options, int page);
|
||||||
|
|
||||||
|
typedef CategoryOptionsLoader = Future<Res<List<CategoryComicsOptions>>> Function(
|
||||||
|
String category, String? param);
|
||||||
|
|
||||||
class CategoryComicsData {
|
class CategoryComicsData {
|
||||||
/// options
|
/// options
|
||||||
final List<CategoryComicsOptions> options;
|
final List<CategoryComicsOptions>? options;
|
||||||
|
|
||||||
|
final CategoryOptionsLoader? optionsLoader;
|
||||||
|
|
||||||
/// [category] is the one clicked by the user on the category page.
|
/// [category] is the one clicked by the user on the category page.
|
||||||
///
|
///
|
||||||
@@ -414,7 +419,7 @@ class CategoryComicsData {
|
|||||||
|
|
||||||
final RankingData? rankingData;
|
final RankingData? rankingData;
|
||||||
|
|
||||||
const CategoryComicsData(this.options, this.load, {this.rankingData});
|
const CategoryComicsData({this.options, this.optionsLoader, required this.load, this.rankingData});
|
||||||
}
|
}
|
||||||
|
|
||||||
class RankingData {
|
class RankingData {
|
||||||
@@ -429,6 +434,9 @@ class RankingData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CategoryComicsOptions {
|
class CategoryComicsOptions {
|
||||||
|
// The label will not be displayed if it is empty.
|
||||||
|
final String label;
|
||||||
|
|
||||||
/// Use a [LinkedHashMap] to describe an option list.
|
/// Use a [LinkedHashMap] to describe an option list.
|
||||||
/// key is for loading comics, value is the name displayed on screen.
|
/// key is for loading comics, value is the name displayed on screen.
|
||||||
/// Default value will be the first of the Map.
|
/// Default value will be the first of the Map.
|
||||||
@@ -439,7 +447,7 @@ class CategoryComicsOptions {
|
|||||||
|
|
||||||
final List<String>? showWhen;
|
final List<String>? showWhen;
|
||||||
|
|
||||||
const CategoryComicsOptions(this.options, this.notShowWhen, this.showWhen);
|
const CategoryComicsOptions(this.label, this.options, this.notShowWhen, this.showWhen);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LinkHandler {
|
class LinkHandler {
|
||||||
|
@@ -64,8 +64,13 @@ class ComicSourceParser {
|
|||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while (file.existsSync()) {
|
while (file.existsSync()) {
|
||||||
file = File(FilePath.join(App.dataPath, "comic_source",
|
file = File(
|
||||||
"${fileName.split('.').first}($i).js"));
|
FilePath.join(
|
||||||
|
App.dataPath,
|
||||||
|
"comic_source",
|
||||||
|
"${fileName.split('.').first}($i).js",
|
||||||
|
),
|
||||||
|
);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,8 +85,9 @@ class ComicSourceParser {
|
|||||||
|
|
||||||
Future<ComicSource> parse(String js, String filePath) async {
|
Future<ComicSource> parse(String js, String filePath) async {
|
||||||
js = js.replaceAll("\r\n", "\n");
|
js = js.replaceAll("\r\n", "\n");
|
||||||
var line1 =
|
var line1 = js
|
||||||
js.split('\n').firstWhereOrNull((e) => e.trim().startsWith("class "));
|
.split('\n')
|
||||||
|
.firstWhereOrNull((e) => e.trim().startsWith("class "));
|
||||||
if (line1 == null ||
|
if (line1 == null ||
|
||||||
!line1.startsWith("class ") ||
|
!line1.startsWith("class ") ||
|
||||||
!line1.contains("extends ComicSource")) {
|
!line1.contains("extends ComicSource")) {
|
||||||
@@ -89,24 +95,27 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
var className = line1.split("class")[1].split("extends ComicSource").first;
|
var className = line1.split("class")[1].split("extends ComicSource").first;
|
||||||
className = className.trim();
|
className = className.trim();
|
||||||
JsEngine().runCode("""
|
JsEngine().runCode("""(() => { $js
|
||||||
(() => { $js
|
|
||||||
this['temp'] = new $className()
|
this['temp'] = new $className()
|
||||||
}).call()
|
}).call()
|
||||||
""", className);
|
""", className);
|
||||||
_name = JsEngine().runCode("this['temp'].name") ??
|
_name =
|
||||||
|
JsEngine().runCode("this['temp'].name") ??
|
||||||
(throw ComicSourceParseException('name is required'));
|
(throw ComicSourceParseException('name is required'));
|
||||||
var key = JsEngine().runCode("this['temp'].key") ??
|
var key =
|
||||||
|
JsEngine().runCode("this['temp'].key") ??
|
||||||
(throw ComicSourceParseException('key is required'));
|
(throw ComicSourceParseException('key is required'));
|
||||||
var version = JsEngine().runCode("this['temp'].version") ??
|
var version =
|
||||||
|
JsEngine().runCode("this['temp'].version") ??
|
||||||
(throw ComicSourceParseException('version is required'));
|
(throw ComicSourceParseException('version is required'));
|
||||||
var minAppVersion = JsEngine().runCode("this['temp'].minAppVersion");
|
var minAppVersion = JsEngine().runCode("this['temp'].minAppVersion");
|
||||||
var url = JsEngine().runCode("this['temp'].url");
|
var url = JsEngine().runCode("this['temp'].url");
|
||||||
if (minAppVersion != null) {
|
if (minAppVersion != null) {
|
||||||
if (compareSemVer(minAppVersion, App.version.split('-').first)) {
|
if (compareSemVer(minAppVersion, App.version.split('-').first)) {
|
||||||
throw ComicSourceParseException(
|
throw ComicSourceParseException(
|
||||||
"minAppVersion @version is required"
|
"minAppVersion @version is required".tlParams({
|
||||||
.tlParams({"version": minAppVersion}),
|
"version": minAppVersion,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,8 +184,10 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _checkExists(String index) {
|
bool _checkExists(String index) {
|
||||||
return JsEngine().runCode("ComicSource.sources.$_key.$index !== null "
|
return JsEngine().runCode(
|
||||||
"&& ComicSource.sources.$_key.$index !== undefined");
|
"ComicSource.sources.$_key.$index !== null "
|
||||||
|
"&& ComicSource.sources.$_key.$index !== undefined",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic _getValue(String index) {
|
dynamic _getValue(String index) {
|
||||||
@@ -277,16 +288,24 @@ class ComicSourceParser {
|
|||||||
if (type == "singlePageWithMultiPart") {
|
if (type == "singlePageWithMultiPart") {
|
||||||
loadMultiPart = () async {
|
loadMultiPart = () async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine()
|
var res = await JsEngine().runCode(
|
||||||
.runCode("ComicSource.sources.$_key.explore[$i].load()");
|
"ComicSource.sources.$_key.explore[$i].load()",
|
||||||
return Res(List.from(res.keys
|
);
|
||||||
.map((e) => ExplorePagePart(
|
return Res(
|
||||||
e,
|
List.from(
|
||||||
(res[e] as List)
|
res.keys
|
||||||
.map<Comic>((e) => Comic.fromJson(e, _key!))
|
.map(
|
||||||
.toList(),
|
(e) => ExplorePagePart(
|
||||||
null))
|
e,
|
||||||
.toList()));
|
(res[e] as List)
|
||||||
|
.map<Comic>((e) => Comic.fromJson(e, _key!))
|
||||||
|
.toList(),
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Data Analysis", "$e\n$s");
|
Log.error("Data Analysis", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
@@ -297,11 +316,15 @@ class ComicSourceParser {
|
|||||||
loadPage = (int page) async {
|
loadPage = (int page) async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine().runCode(
|
var res = await JsEngine().runCode(
|
||||||
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(page)})");
|
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(page)})",
|
||||||
|
);
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
subData: res["maxPage"]);
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
@@ -311,10 +334,13 @@ class ComicSourceParser {
|
|||||||
loadNext = (next) async {
|
loadNext = (next) async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine().runCode(
|
var res = await JsEngine().runCode(
|
||||||
"ComicSource.sources.$_key.explore[$i].loadNext(${jsonEncode(next)})");
|
"ComicSource.sources.$_key.explore[$i].loadNext(${jsonEncode(next)})",
|
||||||
|
);
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
subData: res["next"],
|
subData: res["next"],
|
||||||
);
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -326,8 +352,9 @@ class ComicSourceParser {
|
|||||||
} else if (type == "multiPartPage") {
|
} else if (type == "multiPartPage") {
|
||||||
loadMultiPart = () async {
|
loadMultiPart = () async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine()
|
var res = await JsEngine().runCode(
|
||||||
.runCode("ComicSource.sources.$_key.explore[$i].load()");
|
"ComicSource.sources.$_key.explore[$i].load()",
|
||||||
|
);
|
||||||
return Res(
|
return Res(
|
||||||
List.from(
|
List.from(
|
||||||
(res as List).map((e) {
|
(res as List).map((e) {
|
||||||
@@ -350,19 +377,22 @@ class ComicSourceParser {
|
|||||||
loadMixed = (index) async {
|
loadMixed = (index) async {
|
||||||
try {
|
try {
|
||||||
var res = await JsEngine().runCode(
|
var res = await JsEngine().runCode(
|
||||||
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(index)})");
|
"ComicSource.sources.$_key.explore[$i].load(${jsonEncode(index)})",
|
||||||
|
);
|
||||||
var list = <Object>[];
|
var list = <Object>[];
|
||||||
for (var data in (res['data'] as List)) {
|
for (var data in (res['data'] as List)) {
|
||||||
if (data is List) {
|
if (data is List) {
|
||||||
list.add(data.map((e) => Comic.fromJson(e, _key!)).toList());
|
list.add(data.map((e) => Comic.fromJson(e, _key!)).toList());
|
||||||
} else if (data is Map) {
|
} else if (data is Map) {
|
||||||
list.add(ExplorePagePart(
|
list.add(
|
||||||
data['title'],
|
ExplorePagePart(
|
||||||
(data['comics'] as List).map((e) {
|
data['title'],
|
||||||
return Comic.fromJson(e, _key!);
|
(data['comics'] as List).map((e) {
|
||||||
}).toList(),
|
return Comic.fromJson(e, _key!);
|
||||||
data['viewMore'],
|
}).toList(),
|
||||||
));
|
data['viewMore'],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Res(list, subData: res['maxPage']);
|
return Res(list, subData: res['maxPage']);
|
||||||
@@ -372,21 +402,25 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pages.add(ExplorePageData(
|
pages.add(
|
||||||
title,
|
ExplorePageData(
|
||||||
switch (type) {
|
title,
|
||||||
"singlePageWithMultiPart" => ExplorePageType.singlePageWithMultiPart,
|
switch (type) {
|
||||||
"multiPartPage" => ExplorePageType.singlePageWithMultiPart,
|
"singlePageWithMultiPart" =>
|
||||||
"multiPageComicList" => ExplorePageType.multiPageComicList,
|
ExplorePageType.singlePageWithMultiPart,
|
||||||
"mixed" => ExplorePageType.mixed,
|
"multiPartPage" => ExplorePageType.singlePageWithMultiPart,
|
||||||
_ =>
|
"multiPageComicList" => ExplorePageType.multiPageComicList,
|
||||||
throw ComicSourceParseException("Unknown explore page type $type")
|
"mixed" => ExplorePageType.mixed,
|
||||||
},
|
_ => throw ComicSourceParseException(
|
||||||
loadPage,
|
"Unknown explore page type $type",
|
||||||
loadNext,
|
),
|
||||||
loadMultiPart,
|
},
|
||||||
loadMixed,
|
loadPage,
|
||||||
));
|
loadNext,
|
||||||
|
loadMultiPart,
|
||||||
|
loadMixed,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return pages;
|
return pages;
|
||||||
}
|
}
|
||||||
@@ -426,18 +460,17 @@ class ComicSourceParser {
|
|||||||
if (type == "fixed") {
|
if (type == "fixed") {
|
||||||
categoryParts.add(FixedCategoryPart(name, cs!));
|
categoryParts.add(FixedCategoryPart(name, cs!));
|
||||||
} else if (type == "random") {
|
} else if (type == "random") {
|
||||||
categoryParts
|
categoryParts.add(
|
||||||
.add(RandomCategoryPart(name, cs!, c["randomNumber"] ?? 1));
|
RandomCategoryPart(name, cs!, c["randomNumber"] ?? 1),
|
||||||
|
);
|
||||||
} else if (type == "dynamic" && categories == null) {
|
} else if (type == "dynamic" && categories == null) {
|
||||||
var loader = c["loader"];
|
var loader = c["loader"];
|
||||||
if (loader is! JSInvokable) {
|
if (loader is! JSInvokable) {
|
||||||
throw "DynamicCategoryPart loader must be a function";
|
throw "DynamicCategoryPart loader must be a function";
|
||||||
}
|
}
|
||||||
categoryParts.add(DynamicCategoryPart(
|
categoryParts.add(
|
||||||
name,
|
DynamicCategoryPart(name, JSAutoFreeFunction(loader), _key!),
|
||||||
JSAutoFreeFunction(loader),
|
);
|
||||||
_key!,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// old format
|
// old format
|
||||||
@@ -454,30 +487,16 @@ class ComicSourceParser {
|
|||||||
for (int i = 0; i < tags.length; i++) {
|
for (int i = 0; i < tags.length; i++) {
|
||||||
PageJumpTarget target;
|
PageJumpTarget target;
|
||||||
if (itemType == 'category') {
|
if (itemType == 'category') {
|
||||||
target = PageJumpTarget(
|
target = PageJumpTarget(_key!, 'category', {
|
||||||
_key!,
|
"category": tags[i],
|
||||||
'category',
|
"param": categoryParams?.elementAtOrNull(i),
|
||||||
{
|
});
|
||||||
"category": tags[i],
|
|
||||||
"param": categoryParams?.elementAtOrNull(i),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (itemType == 'search') {
|
} else if (itemType == 'search') {
|
||||||
target = PageJumpTarget(
|
target = PageJumpTarget(_key!, 'search', {"keyword": tags[i]});
|
||||||
_key!,
|
|
||||||
'search',
|
|
||||||
{
|
|
||||||
"keyword": tags[i],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (itemType == 'search_with_namespace') {
|
} else if (itemType == 'search_with_namespace') {
|
||||||
target = PageJumpTarget(
|
target = PageJumpTarget(_key!, 'search', {
|
||||||
_key!,
|
"keyword": "$name:$tags[i]",
|
||||||
'search',
|
});
|
||||||
{
|
|
||||||
"keyword": "$name:$tags[i]",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
target = PageJumpTarget(_key!, itemType, null);
|
target = PageJumpTarget(_key!, itemType, null);
|
||||||
}
|
}
|
||||||
@@ -486,38 +505,96 @@ class ComicSourceParser {
|
|||||||
if (type == "fixed") {
|
if (type == "fixed") {
|
||||||
categoryParts.add(FixedCategoryPart(name, cs));
|
categoryParts.add(FixedCategoryPart(name, cs));
|
||||||
} else if (type == "random") {
|
} else if (type == "random") {
|
||||||
categoryParts
|
categoryParts.add(
|
||||||
.add(RandomCategoryPart(name, cs, c["randomNumber"] ?? 1));
|
RandomCategoryPart(name, cs, c["randomNumber"] ?? 1),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return CategoryData(
|
return CategoryData(
|
||||||
title: title,
|
title: title,
|
||||||
categories: categoryParts,
|
categories: categoryParts,
|
||||||
enableRankingPage: enableRankingPage ?? false,
|
enableRankingPage: enableRankingPage ?? false,
|
||||||
key: title);
|
key: title,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
CategoryComicsData? _loadCategoryComicsData() {
|
CategoryComicsData? _loadCategoryComicsData() {
|
||||||
if (!_checkExists("categoryComics")) return null;
|
if (!_checkExists("categoryComics")) return null;
|
||||||
var options = <CategoryComicsOptions>[];
|
|
||||||
for (var element in _getValue("categoryComics.optionList") ?? []) {
|
List<CategoryComicsOptions>? options;
|
||||||
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
|
if (_checkExists("categoryComics.optionList")) {
|
||||||
for (var option in element["options"]) {
|
options = <CategoryComicsOptions>[];
|
||||||
if (option.isEmpty || !option.contains("-")) {
|
for (var element in _getValue("categoryComics.optionList") ?? []) {
|
||||||
continue;
|
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
|
||||||
|
for (var option in element["options"]) {
|
||||||
|
if (option.isEmpty || !option.contains("-")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var split = option.split("-");
|
||||||
|
var key = split.removeAt(0);
|
||||||
|
var value = split.join("-");
|
||||||
|
map[key] = value;
|
||||||
}
|
}
|
||||||
var split = option.split("-");
|
options.add(
|
||||||
var key = split.removeAt(0);
|
CategoryComicsOptions(
|
||||||
var value = split.join("-");
|
element["label"] ?? "",
|
||||||
map[key] = value;
|
map,
|
||||||
|
List.from(element["notShowWhen"] ?? []),
|
||||||
|
element["showWhen"] == null ? null : List.from(element["showWhen"]),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
options.add(CategoryComicsOptions(
|
|
||||||
map,
|
|
||||||
List.from(element["notShowWhen"] ?? []),
|
|
||||||
element["showWhen"] == null ? null : List.from(element["showWhen"])));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CategoryOptionsLoader? optionLoader;
|
||||||
|
if (_checkExists("categoryComics.optionLoader")) {
|
||||||
|
optionLoader = (category, param) async {
|
||||||
|
try {
|
||||||
|
dynamic res = JsEngine().runCode("""
|
||||||
|
ComicSource.sources.$_key.categoryComics.optionLoader(
|
||||||
|
${jsonEncode(category)}, ${jsonEncode(param)})
|
||||||
|
""");
|
||||||
|
if (res is Future) {
|
||||||
|
res = await res;
|
||||||
|
}
|
||||||
|
if (res is! List) {
|
||||||
|
return Res.error("Invalid data:\nExpected: List\nGot: ${res.runtimeType}");
|
||||||
|
}
|
||||||
|
var options = <CategoryComicsOptions>[];
|
||||||
|
for (var element in res) {
|
||||||
|
if (element is! Map) {
|
||||||
|
return Res.error("Invalid option data:\nExpected: Map\nGot: ${element.runtimeType}");
|
||||||
|
}
|
||||||
|
LinkedHashMap<String, String> map = LinkedHashMap<String, String>();
|
||||||
|
for (var option in element["options"] ?? []) {
|
||||||
|
if (option.isEmpty || !option.contains("-")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var split = option.split("-");
|
||||||
|
var key = split.removeAt(0);
|
||||||
|
var value = split.join("-");
|
||||||
|
map[key] = value;
|
||||||
|
}
|
||||||
|
options.add(
|
||||||
|
CategoryComicsOptions(
|
||||||
|
element["label"] ?? "",
|
||||||
|
map,
|
||||||
|
List.from(element["notShowWhen"] ?? []),
|
||||||
|
element["showWhen"] == null ? null : List.from(element["showWhen"]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Res(options);
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
Log.error("Data Analysis", "Failed to load category options.\n$e");
|
||||||
|
return Res.error(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
RankingData? rankingData;
|
RankingData? rankingData;
|
||||||
if (_checkExists("categoryComics.ranking")) {
|
if (_checkExists("categoryComics.ranking")) {
|
||||||
var options = <String, String>{};
|
var options = <String, String>{};
|
||||||
@@ -532,7 +609,7 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
Future<Res<List<Comic>>> Function(String option, int page)? load;
|
Future<Res<List<Comic>>> Function(String option, int page)? load;
|
||||||
Future<Res<List<Comic>>> Function(String option, String? next)?
|
Future<Res<List<Comic>>> Function(String option, String? next)?
|
||||||
loadWithNext;
|
loadWithNext;
|
||||||
if (_checkExists("categoryComics.ranking.load")) {
|
if (_checkExists("categoryComics.ranking.load")) {
|
||||||
load = (option, page) async {
|
load = (option, page) async {
|
||||||
try {
|
try {
|
||||||
@@ -541,9 +618,12 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(option)}, ${jsonEncode(page)})
|
${jsonEncode(option)}, ${jsonEncode(page)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
subData: res["maxPage"]);
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
@@ -557,8 +637,10 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(option)}, ${jsonEncode(next)})
|
${jsonEncode(option)}, ${jsonEncode(next)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
subData: res["next"],
|
subData: res["next"],
|
||||||
);
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -569,25 +651,38 @@ class ComicSourceParser {
|
|||||||
}
|
}
|
||||||
rankingData = RankingData(options, load, loadWithNext);
|
rankingData = RankingData(options, load, loadWithNext);
|
||||||
}
|
}
|
||||||
return CategoryComicsData(options, (category, param, options, page) async {
|
|
||||||
try {
|
if (options == null && optionLoader == null) {
|
||||||
var res = await JsEngine().runCode("""
|
options = [];
|
||||||
ComicSource.sources.$_key.categoryComics.load(
|
}
|
||||||
${jsonEncode(category)},
|
|
||||||
${jsonEncode(param)},
|
return CategoryComicsData(
|
||||||
${jsonEncode(options)},
|
options: options,
|
||||||
${jsonEncode(page)}
|
optionsLoader: optionLoader,
|
||||||
)
|
load: (category, param, options, page) async {
|
||||||
""");
|
try {
|
||||||
return Res(
|
var res = await JsEngine().runCode("""
|
||||||
List.generate(res["comics"].length,
|
ComicSource.sources.$_key.categoryComics.load(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
${jsonEncode(category)},
|
||||||
subData: res["maxPage"]);
|
${jsonEncode(param)},
|
||||||
} catch (e, s) {
|
${jsonEncode(options)},
|
||||||
Log.error("Network", "$e\n$s");
|
${jsonEncode(page)}
|
||||||
return Res.error(e.toString());
|
)
|
||||||
}
|
""");
|
||||||
}, rankingData: rankingData);
|
return Res(
|
||||||
|
List.generate(
|
||||||
|
res["comics"].length,
|
||||||
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Network", "$e\n$s");
|
||||||
|
return Res.error(e.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rankingData: rankingData,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchPageData? _loadSearchData() {
|
SearchPageData? _loadSearchData() {
|
||||||
@@ -604,12 +699,14 @@ class ComicSourceParser {
|
|||||||
var value = split.join("-");
|
var value = split.join("-");
|
||||||
map[key] = value;
|
map[key] = value;
|
||||||
}
|
}
|
||||||
options.add(SearchOptions(
|
options.add(
|
||||||
map,
|
SearchOptions(
|
||||||
element["label"],
|
map,
|
||||||
element['type'] ?? 'select',
|
element["label"],
|
||||||
element['default'] == null ? null : jsonEncode(element['default']),
|
element['type'] ?? 'select',
|
||||||
));
|
element['default'] == null ? null : jsonEncode(element['default']),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchFunction? loadPage;
|
SearchFunction? loadPage;
|
||||||
@@ -624,9 +721,12 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(page)})
|
${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(page)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
subData: res["maxPage"]);
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
@@ -640,8 +740,10 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(next)})
|
${jsonEncode(keyword)}, ${jsonEncode(searchOption)}, ${jsonEncode(next)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
subData: res["next"],
|
subData: res["next"],
|
||||||
);
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -690,8 +792,9 @@ class ComicSourceParser {
|
|||||||
|
|
||||||
final bool multiFolder = _getValue("favorites.multiFolder");
|
final bool multiFolder = _getValue("favorites.multiFolder");
|
||||||
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
|
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
|
||||||
final bool? singleFolderForSingleComic =
|
final bool? singleFolderForSingleComic = _getValue(
|
||||||
_getValue("favorites.singleFolderForSingleComic");
|
"favorites.singleFolderForSingleComic",
|
||||||
|
);
|
||||||
|
|
||||||
Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async {
|
Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async {
|
||||||
if (!ComicSource.find(_key!)!.isLogged) {
|
if (!ComicSource.find(_key!)!.isLogged) {
|
||||||
@@ -744,9 +847,12 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(page)}, ${jsonEncode(folder)})
|
${jsonEncode(page)}, ${jsonEncode(folder)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
subData: res["maxPage"]);
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
@@ -766,8 +872,10 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(next)}, ${jsonEncode(folder)})
|
${jsonEncode(next)}, ${jsonEncode(folder)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
List.generate(res["comics"].length,
|
List.generate(
|
||||||
(index) => Comic.fromJson(res["comics"][index], _key!)),
|
res["comics"].length,
|
||||||
|
(index) => Comic.fromJson(res["comics"][index], _key!),
|
||||||
|
),
|
||||||
subData: res["next"],
|
subData: res["next"],
|
||||||
);
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
@@ -858,8 +966,9 @@ class ComicSourceParser {
|
|||||||
${jsonEncode(id)}, ${jsonEncode(subId)}, ${jsonEncode(page)}, ${jsonEncode(replyTo)})
|
${jsonEncode(id)}, ${jsonEncode(subId)}, ${jsonEncode(page)}, ${jsonEncode(replyTo)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
(res["comments"] as List).map((e) => Comment.fromJson(e)).toList(),
|
(res["comments"] as List).map((e) => Comment.fromJson(e)).toList(),
|
||||||
subData: res["maxPage"]);
|
subData: res["maxPage"],
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
@@ -1114,7 +1223,8 @@ class ComicSourceParser {
|
|||||||
ComicSource.sources.$_key.comic.archive.getArchives(${jsonEncode(cid)})
|
ComicSource.sources.$_key.comic.archive.getArchives(${jsonEncode(cid)})
|
||||||
""");
|
""");
|
||||||
return Res(
|
return Res(
|
||||||
(res as List).map((e) => ArchiveInfo.fromJson(e)).toList());
|
(res as List).map((e) => ArchiveInfo.fromJson(e)).toList(),
|
||||||
|
);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error("Network", "$e\n$s");
|
Log.error("Network", "$e\n$s");
|
||||||
return Res.error(e.toString());
|
return Res.error(e.toString());
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:async' show Future;
|
import 'dart:async' show Future;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/network/images.dart';
|
import 'package:venera/network/images.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'base_image_provider.dart';
|
import 'base_image_provider.dart';
|
||||||
@@ -11,7 +13,12 @@ class CachedImageProvider
|
|||||||
/// Image provider for normal image.
|
/// Image provider for normal image.
|
||||||
///
|
///
|
||||||
/// [url] is the url of the image. Local file path is also supported.
|
/// [url] is the url of the image. Local file path is also supported.
|
||||||
const CachedImageProvider(this.url, {this.headers, this.sourceKey, this.cid});
|
const CachedImageProvider(this.url, {
|
||||||
|
this.headers,
|
||||||
|
this.sourceKey,
|
||||||
|
this.cid,
|
||||||
|
this.fallbackToLocalCover = false,
|
||||||
|
});
|
||||||
|
|
||||||
final String url;
|
final String url;
|
||||||
|
|
||||||
@@ -21,6 +28,9 @@ class CachedImageProvider
|
|||||||
|
|
||||||
final String? cid;
|
final String? cid;
|
||||||
|
|
||||||
|
// Use local cover if network image fails to load.
|
||||||
|
final bool fallbackToLocalCover;
|
||||||
|
|
||||||
static int loadingCount = 0;
|
static int loadingCount = 0;
|
||||||
|
|
||||||
static const _kMaxLoadingCount = 8;
|
static const _kMaxLoadingCount = 8;
|
||||||
@@ -49,6 +59,24 @@ class CachedImageProvider
|
|||||||
}
|
}
|
||||||
throw "Error: Empty response body.";
|
throw "Error: Empty response body.";
|
||||||
}
|
}
|
||||||
|
catch(e) {
|
||||||
|
if (fallbackToLocalCover && sourceKey != null && cid != null) {
|
||||||
|
final localComic = LocalManager().find(
|
||||||
|
cid!,
|
||||||
|
ComicType.fromKey(sourceKey!),
|
||||||
|
);
|
||||||
|
if (localComic != null) {
|
||||||
|
var file = localComic.coverFile;
|
||||||
|
if (await file.exists()) {
|
||||||
|
var data = await file.readAsBytes();
|
||||||
|
if (data.isNotEmpty) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
finally {
|
finally {
|
||||||
loadingCount--;
|
loadingCount--;
|
||||||
}
|
}
|
||||||
|
@@ -24,6 +24,7 @@ import 'package:pointycastle/block/modes/ofb.dart';
|
|||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'package:venera/components/js_ui.dart';
|
import 'package:venera/components/js_ui.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/js_pool.dart';
|
||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/app_dio.dart';
|
||||||
import 'package:venera/network/cookie_jar.dart';
|
import 'package:venera/network/cookie_jar.dart';
|
||||||
import 'package:venera/network/proxy.dart';
|
import 'package:venera/network/proxy.dart';
|
||||||
@@ -68,6 +69,12 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
responseType: ResponseType.plain, validateStatus: (status) => true));
|
responseType: ResponseType.plain, validateStatus: (status) => true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Uint8List? _jsInitCache;
|
||||||
|
|
||||||
|
static void cacheJsInit(Uint8List jsInit) {
|
||||||
|
_jsInitCache = jsInit;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@protected
|
@protected
|
||||||
Future<void> doInit() async {
|
Future<void> doInit() async {
|
||||||
@@ -75,9 +82,11 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
if (App.isInitialized) {
|
||||||
|
_cookieJar ??= await SingleInstanceCookieJar.createInstance();
|
||||||
|
}
|
||||||
_dio ??= AppDio(BaseOptions(
|
_dio ??= AppDio(BaseOptions(
|
||||||
responseType: ResponseType.plain, validateStatus: (status) => true));
|
responseType: ResponseType.plain, validateStatus: (status) => true));
|
||||||
_cookieJar ??= SingleInstanceCookieJar.instance!;
|
|
||||||
_closed = false;
|
_closed = false;
|
||||||
_engine = FlutterQjs();
|
_engine = FlutterQjs();
|
||||||
_engine!.dispatch();
|
_engine!.dispatch();
|
||||||
@@ -86,9 +95,15 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
|
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
|
||||||
setGlobalFunc(["appVersion", App.version]);
|
setGlobalFunc(["appVersion", App.version]);
|
||||||
setGlobalFunc.free();
|
setGlobalFunc.free();
|
||||||
var jsInit = await rootBundle.load("assets/init.js");
|
Uint8List jsInit;
|
||||||
|
if (_jsInitCache != null) {
|
||||||
|
jsInit = _jsInitCache!;
|
||||||
|
} else {
|
||||||
|
var buffer = await rootBundle.load("assets/init.js");
|
||||||
|
jsInit = buffer.buffer.asUint8List();
|
||||||
|
}
|
||||||
_engine!
|
_engine!
|
||||||
.evaluate(utf8.decode(jsInit.buffer.asUint8List()), name: "<init>");
|
.evaluate(utf8.decode(jsInit), name: "<init>");
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
Log.error('JS Engine', 'JS Engine Init Error:\n$e\n$s');
|
Log.error('JS Engine', 'JS Engine Init Error:\n$e\n$s');
|
||||||
}
|
}
|
||||||
@@ -97,6 +112,7 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
Object? _messageReceiver(dynamic message) {
|
Object? _messageReceiver(dynamic message) {
|
||||||
try {
|
try {
|
||||||
if (message is Map<dynamic, dynamic>) {
|
if (message is Map<dynamic, dynamic>) {
|
||||||
|
if (message["method"] == null) return null;
|
||||||
String method = message["method"] as String;
|
String method = message["method"] as String;
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case "log":
|
case "log":
|
||||||
@@ -172,6 +188,20 @@ class JsEngine with _JSEngineApi, JsUiApi, Init {
|
|||||||
var res = await Clipboard.getData(Clipboard.kTextPlain);
|
var res = await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
return res?.text;
|
return res?.text;
|
||||||
});
|
});
|
||||||
|
case "compute":
|
||||||
|
final func = message["function"];
|
||||||
|
final args = message["args"];
|
||||||
|
if (func is JSInvokable) {
|
||||||
|
func.free();
|
||||||
|
throw "Function must be a string";
|
||||||
|
}
|
||||||
|
if (func is! String) {
|
||||||
|
throw "Function must be a string";
|
||||||
|
}
|
||||||
|
if (args != null && args is! List) {
|
||||||
|
throw "Args must be a list";
|
||||||
|
}
|
||||||
|
return JSPool().execute(func, args ?? []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
163
lib/foundation/js_pool.dart
Normal file
163
lib/foundation/js_pool.dart
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_qjs/flutter_qjs.dart';
|
||||||
|
import 'package:venera/foundation/js_engine.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
|
|
||||||
|
class JSPool {
|
||||||
|
static final int _maxInstances = 4;
|
||||||
|
final List<IsolateJsEngine> _instances = [];
|
||||||
|
bool _isInitializing = false;
|
||||||
|
|
||||||
|
static final JSPool _singleton = JSPool._internal();
|
||||||
|
factory JSPool() {
|
||||||
|
return _singleton;
|
||||||
|
}
|
||||||
|
JSPool._internal();
|
||||||
|
|
||||||
|
Future<void> init() async {
|
||||||
|
if (_isInitializing) return;
|
||||||
|
_isInitializing = true;
|
||||||
|
var jsInitBuffer = await rootBundle.load("assets/init.js");
|
||||||
|
var jsInit = jsInitBuffer.buffer.asUint8List();
|
||||||
|
for (int i = 0; i < _maxInstances; i++) {
|
||||||
|
_instances.add(IsolateJsEngine(jsInit));
|
||||||
|
}
|
||||||
|
_isInitializing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> execute(String jsFunction, List<dynamic> args) async {
|
||||||
|
await init();
|
||||||
|
var selectedInstance = _instances[0];
|
||||||
|
for (var instance in _instances) {
|
||||||
|
if (instance.pendingTasks < selectedInstance.pendingTasks) {
|
||||||
|
selectedInstance = instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectedInstance.execute(jsFunction, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IsolateJsEngineInitParam {
|
||||||
|
final SendPort sendPort;
|
||||||
|
|
||||||
|
final Uint8List jsInit;
|
||||||
|
|
||||||
|
_IsolateJsEngineInitParam(this.sendPort, this.jsInit);
|
||||||
|
}
|
||||||
|
|
||||||
|
class IsolateJsEngine {
|
||||||
|
Isolate? _isolate;
|
||||||
|
|
||||||
|
SendPort? _sendPort;
|
||||||
|
ReceivePort? _receivePort;
|
||||||
|
|
||||||
|
int _counter = 0;
|
||||||
|
final Map<int, Completer<dynamic>> _tasks = {};
|
||||||
|
|
||||||
|
bool _isClosed = false;
|
||||||
|
|
||||||
|
int get pendingTasks => _tasks.length;
|
||||||
|
|
||||||
|
IsolateJsEngine(Uint8List jsInit) {
|
||||||
|
_receivePort = ReceivePort();
|
||||||
|
_receivePort!.listen(_onMessage);
|
||||||
|
Isolate.spawn(_run, _IsolateJsEngineInitParam(_receivePort!.sendPort, jsInit));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMessage(dynamic message) {
|
||||||
|
if (message is SendPort) {
|
||||||
|
_sendPort = message;
|
||||||
|
} else if (message is TaskResult) {
|
||||||
|
final completer = _tasks.remove(message.id);
|
||||||
|
if (completer != null) {
|
||||||
|
if (message.error != null) {
|
||||||
|
completer.completeError(message.error!);
|
||||||
|
} else {
|
||||||
|
completer.complete(message.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (message is Exception) {
|
||||||
|
Log.error("IsolateJsEngine", message.toString());
|
||||||
|
for (var completer in _tasks.values) {
|
||||||
|
completer.completeError(message);
|
||||||
|
}
|
||||||
|
_tasks.clear();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _run(_IsolateJsEngineInitParam params) async {
|
||||||
|
var sendPort = params.sendPort;
|
||||||
|
final port = ReceivePort();
|
||||||
|
sendPort.send(port.sendPort);
|
||||||
|
final engine = JsEngine();
|
||||||
|
try {
|
||||||
|
JsEngine.cacheJsInit(params.jsInit);
|
||||||
|
await engine.init();
|
||||||
|
}
|
||||||
|
catch(e, s) {
|
||||||
|
sendPort.send(Exception("Failed to initialize JS engine: $e\n$s"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await for (final message in port) {
|
||||||
|
if (message is Task) {
|
||||||
|
try {
|
||||||
|
final jsFunc = engine.runCode(message.jsFunction);
|
||||||
|
if (jsFunc is! JSInvokable) {
|
||||||
|
throw Exception("The provided code does not evaluate to a function.");
|
||||||
|
}
|
||||||
|
final result = jsFunc.invoke(message.args);
|
||||||
|
jsFunc.free();
|
||||||
|
sendPort.send(TaskResult(message.id, result, null));
|
||||||
|
} catch (e) {
|
||||||
|
sendPort.send(TaskResult(message.id, null, e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> execute(String jsFunction, List<dynamic> args) async {
|
||||||
|
if (_isClosed) {
|
||||||
|
throw Exception("IsolateJsEngine is closed.");
|
||||||
|
}
|
||||||
|
while (_sendPort == null) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 10));
|
||||||
|
}
|
||||||
|
final completer = Completer<dynamic>();
|
||||||
|
final taskId = _counter++;
|
||||||
|
_tasks[taskId] = completer;
|
||||||
|
final task = Task(taskId, jsFunction, args);
|
||||||
|
_sendPort?.send(task);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() async {
|
||||||
|
if (!_isClosed) {
|
||||||
|
_isClosed = true;
|
||||||
|
while (_tasks.isNotEmpty) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
_receivePort?.close();
|
||||||
|
_isolate?.kill(priority: Isolate.immediate);
|
||||||
|
_isolate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Task {
|
||||||
|
final int id;
|
||||||
|
final String jsFunction;
|
||||||
|
final List<dynamic> args;
|
||||||
|
|
||||||
|
const Task(this.id, this.jsFunction, this.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TaskResult {
|
||||||
|
final int id;
|
||||||
|
final Object? result;
|
||||||
|
final String? error;
|
||||||
|
|
||||||
|
const TaskResult(this.id, this.result, this.error);
|
||||||
|
}
|
@@ -42,7 +42,7 @@ class Log {
|
|||||||
|
|
||||||
static void addLog(LogLevel level, String title, String content) {
|
static void addLog(LogLevel level, String title, String content) {
|
||||||
if (isMuted) return;
|
if (isMuted) return;
|
||||||
if (_file == null) {
|
if (_file == null && App.isInitialized) {
|
||||||
Directory dir;
|
Directory dir;
|
||||||
if (App.isAndroid) {
|
if (App.isAndroid) {
|
||||||
dir = Directory(App.externalStoragePath!);
|
dir = Directory(App.externalStoragePath!);
|
||||||
|
@@ -248,7 +248,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
MediaQuery.of(context).viewPadding.top <= 0 ||
|
MediaQuery.of(context).viewPadding.top <= 0 ||
|
||||||
MediaQuery.of(context).viewPadding.top > 50;
|
MediaQuery.of(context).viewPadding.top > 50;
|
||||||
|
|
||||||
if (isPaddingCheckError) {
|
if (isPaddingCheckError && Platform.isAndroid) {
|
||||||
widget = MediaQuery(
|
widget = MediaQuery(
|
||||||
data: MediaQuery.of(context).copyWith(
|
data: MediaQuery.of(context).copyWith(
|
||||||
viewPadding: const EdgeInsets.only(
|
viewPadding: const EdgeInsets.only(
|
||||||
|
@@ -112,10 +112,12 @@ class AppDio with DioMixin {
|
|||||||
AppDio([BaseOptions? options]) {
|
AppDio([BaseOptions? options]) {
|
||||||
this.options = options ?? BaseOptions();
|
this.options = options ?? BaseOptions();
|
||||||
httpClientAdapter = RHttpAdapter();
|
httpClientAdapter = RHttpAdapter();
|
||||||
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
if (App.isInitialized) {
|
||||||
interceptors.add(NetworkCacheManager());
|
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
||||||
interceptors.add(CloudflareInterceptor());
|
interceptors.add(NetworkCacheManager());
|
||||||
interceptors.add(MyLogInterceptor());
|
interceptors.add(CloudflareInterceptor());
|
||||||
|
interceptors.add(MyLogInterceptor());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static final Map<String, bool> _requests = {};
|
static final Map<String, bool> _requests = {};
|
||||||
|
@@ -202,9 +202,13 @@ class SingleInstanceCookieJar extends CookieJarSql {
|
|||||||
|
|
||||||
static SingleInstanceCookieJar? instance;
|
static SingleInstanceCookieJar? instance;
|
||||||
|
|
||||||
static Future<void> createInstance() async {
|
static Future<SingleInstanceCookieJar> createInstance() async {
|
||||||
|
if (instance != null) {
|
||||||
|
return instance!;
|
||||||
|
}
|
||||||
var dataPath = (await getApplicationSupportDirectory()).path;
|
var dataPath = (await getApplicationSupportDirectory()).path;
|
||||||
instance = SingleInstanceCookieJar("$dataPath/cookie.db");
|
instance = SingleInstanceCookieJar("$dataPath/cookie.db");
|
||||||
|
return instance!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -181,12 +181,17 @@ abstract class ImageDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (configs['onResponse'] is JSInvokable) {
|
if (configs['onResponse'] is JSInvokable) {
|
||||||
buffer = (configs['onResponse'] as JSInvokable)([buffer]);
|
buffer = (configs['onResponse'] as JSInvokable)([Uint8List.fromList(buffer)]);
|
||||||
(configs['onResponse'] as JSInvokable).free();
|
(configs['onResponse'] as JSInvokable).free();
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = Uint8List.fromList(buffer);
|
Uint8List data;
|
||||||
buffer.clear();
|
if (buffer is Uint8List) {
|
||||||
|
data = buffer;
|
||||||
|
} else {
|
||||||
|
data = Uint8List.fromList(buffer);
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (configs['modifyImage'] != null) {
|
if (configs['modifyImage'] != null) {
|
||||||
var newData = await modifyImageWithScript(
|
var newData = await modifyImageWithScript(
|
||||||
|
@@ -27,9 +27,11 @@ class CategoryComicsPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
||||||
late final CategoryComicsData data;
|
late final CategoryComicsData data;
|
||||||
late final List<CategoryComicsOptions> options;
|
late List<CategoryComicsOptions>? options;
|
||||||
|
late final CategoryOptionsLoader? optionsLoader;
|
||||||
late List<String> optionsValue;
|
late List<String> optionsValue;
|
||||||
late String sourceKey;
|
late String sourceKey;
|
||||||
|
String? error;
|
||||||
|
|
||||||
void findData() {
|
void findData() {
|
||||||
for (final source in ComicSource.all()) {
|
for (final source in ComicSource.all()) {
|
||||||
@@ -38,24 +40,23 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
throw "The comic source ${source.name} does not support category comics";
|
throw "The comic source ${source.name} does not support category comics";
|
||||||
}
|
}
|
||||||
data = source.categoryComicsData!;
|
data = source.categoryComicsData!;
|
||||||
options = data.options.where((element) {
|
if (data.options != null) {
|
||||||
if (element.notShowWhen.contains(widget.category)) {
|
options = data.options!.where((element) {
|
||||||
return false;
|
if (element.notShowWhen.contains(widget.category)) {
|
||||||
} else if (element.showWhen != null) {
|
return false;
|
||||||
return element.showWhen!.contains(widget.category);
|
} else if (element.showWhen != null) {
|
||||||
}
|
return element.showWhen!.contains(widget.category);
|
||||||
return true;
|
}
|
||||||
}).toList();
|
return true;
|
||||||
var defaultOptionsValue =
|
}).toList();
|
||||||
options.map((e) => e.options.keys.first).toList();
|
} else {
|
||||||
if (optionsValue.length != options.length) {
|
options = null;
|
||||||
var newOptionsValue = List<String>.filled(options.length, "");
|
|
||||||
for (var i = 0; i < options.length; i++) {
|
|
||||||
newOptionsValue[i] =
|
|
||||||
optionsValue.elementAtOrNull(i) ?? defaultOptionsValue[i];
|
|
||||||
}
|
|
||||||
optionsValue = newOptionsValue;
|
|
||||||
}
|
}
|
||||||
|
if (data.optionsLoader != null) {
|
||||||
|
optionsLoader = data.optionsLoader;
|
||||||
|
loadOptions();
|
||||||
|
}
|
||||||
|
resetOptionsValue();
|
||||||
sourceKey = source.key;
|
sourceKey = source.key;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -63,6 +64,36 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
throw "${widget.categoryKey} Not found";
|
throw "${widget.categoryKey} Not found";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void resetOptionsValue() {
|
||||||
|
if (options == null) return;
|
||||||
|
var defaultOptionsValue = options!
|
||||||
|
.map((e) => e.options.keys.first)
|
||||||
|
.toList();
|
||||||
|
if (optionsValue.length != options!.length) {
|
||||||
|
var newOptionsValue = List<String>.filled(options!.length, "");
|
||||||
|
for (var i = 0; i < options!.length; i++) {
|
||||||
|
newOptionsValue[i] =
|
||||||
|
optionsValue.elementAtOrNull(i) ?? defaultOptionsValue[i];
|
||||||
|
}
|
||||||
|
optionsValue = newOptionsValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadOptions() async {
|
||||||
|
final res = await optionsLoader!(widget.category, widget.param);
|
||||||
|
if (res.error) {
|
||||||
|
setState(() {
|
||||||
|
error = res.errorMessage;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
options = res.data;
|
||||||
|
resetOptionsValue();
|
||||||
|
error = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
if (widget.options != null) {
|
if (widget.options != null) {
|
||||||
@@ -77,27 +108,44 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var topPadding = context.padding.top + 56.0;
|
var topPadding = context.padding.top + 56.0;
|
||||||
|
|
||||||
|
Widget body;
|
||||||
|
|
||||||
|
if (options == null) {
|
||||||
|
body = Center(child: CircularProgressIndicator());
|
||||||
|
} else if (error != null) {
|
||||||
|
body = NetworkError(
|
||||||
|
message: error!,
|
||||||
|
retry: () {
|
||||||
|
setState(() {
|
||||||
|
error = null;
|
||||||
|
});
|
||||||
|
loadOptions();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
body = ComicList(
|
||||||
|
key: Key(widget.category + optionsValue.toString()),
|
||||||
|
errorLeading: buildOptions().paddingTop(topPadding),
|
||||||
|
leadingSliver: buildOptions().paddingTop(topPadding).toSliver(),
|
||||||
|
loadPage: (i) =>
|
||||||
|
data.load(widget.category, widget.param, optionsValue, i),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
extendBodyBehindAppBar: true,
|
extendBodyBehindAppBar: true,
|
||||||
appBar: Appbar(
|
appBar: Appbar(title: Text(widget.category)),
|
||||||
title: Text(widget.category),
|
body: body,
|
||||||
),
|
|
||||||
body: ComicList(
|
|
||||||
key: Key(widget.category + optionsValue.toString()),
|
|
||||||
errorLeading: SizedBox(height: topPadding),
|
|
||||||
leadingSliver: buildOptions().paddingTop(topPadding).toSliver(),
|
|
||||||
loadPage: (i) => data.load(
|
|
||||||
widget.category,
|
|
||||||
widget.param,
|
|
||||||
optionsValue,
|
|
||||||
i,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildOptionItem(
|
Widget buildOptionItem(
|
||||||
String text, String value, int group, BuildContext context) {
|
String text,
|
||||||
|
String value,
|
||||||
|
int group,
|
||||||
|
BuildContext context,
|
||||||
|
) {
|
||||||
return OptionChip(
|
return OptionChip(
|
||||||
text: text.ts(sourceKey),
|
text: text.ts(sourceKey),
|
||||||
isSelected: value == optionsValue[group],
|
isSelected: value == optionsValue[group],
|
||||||
@@ -112,23 +160,57 @@ class _CategoryComicsPageState extends State<CategoryComicsPage> {
|
|||||||
|
|
||||||
Widget buildOptions() {
|
Widget buildOptions() {
|
||||||
List<Widget> children = [];
|
List<Widget> children = [];
|
||||||
for (var optionList in options) {
|
var group = 0;
|
||||||
children.add(Wrap(
|
for (var optionList in options!) {
|
||||||
spacing: 8,
|
if (optionList.label.isNotEmpty) {
|
||||||
runSpacing: 8,
|
children.add(Padding(
|
||||||
children: [
|
padding: const EdgeInsets.only(
|
||||||
for (var option in optionList.options.entries)
|
bottom: 8.0,
|
||||||
buildOptionItem(
|
left: 4.0,
|
||||||
option.value.tl,
|
),
|
||||||
option.key,
|
child: Text(
|
||||||
options.indexOf(optionList),
|
optionList.label.ts(sourceKey),
|
||||||
context,
|
style: TextStyle(
|
||||||
)
|
fontSize: 14,
|
||||||
],
|
fontWeight: FontWeight.bold,
|
||||||
));
|
),
|
||||||
if (options.last != optionList) {
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (optionList.options.length <= 8) {
|
||||||
|
children.add(
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
for (var option in optionList.options.entries)
|
||||||
|
buildOptionItem(
|
||||||
|
option.value.tl,
|
||||||
|
option.key,
|
||||||
|
group,
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
var g = group;
|
||||||
|
children.add(Select(
|
||||||
|
current: optionList.options[optionsValue[g]],
|
||||||
|
values: optionList.options.values.toList(),
|
||||||
|
onTap: (i) {
|
||||||
|
var key = optionList.options.keys.elementAt(i);
|
||||||
|
if (key == optionsValue[g]) return;
|
||||||
|
setState(() {
|
||||||
|
optionsValue[g] = key;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (options!.last != optionList) {
|
||||||
children.add(const SizedBox(height: 8));
|
children.add(const SizedBox(height: 8));
|
||||||
}
|
}
|
||||||
|
group++;
|
||||||
}
|
}
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
@@ -77,8 +77,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onReadEnd() {
|
void onReadEnd() {
|
||||||
history ??=
|
history ??= HistoryManager().find(
|
||||||
HistoryManager().find(widget.id, ComicType(widget.sourceKey.hashCode));
|
widget.id,
|
||||||
|
ComicType(widget.sourceKey.hashCode),
|
||||||
|
);
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +95,32 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildError() {
|
||||||
|
final isDownloaded = LocalManager().isDownloaded(
|
||||||
|
widget.id,
|
||||||
|
ComicType.fromKey(widget.sourceKey),
|
||||||
|
);
|
||||||
|
Widget? action;
|
||||||
|
if (isDownloaded) {
|
||||||
|
action = FilledButton.tonal(
|
||||||
|
child: Text("Read".tl),
|
||||||
|
onPressed: () {
|
||||||
|
final localComic = LocalManager().find(
|
||||||
|
widget.id,
|
||||||
|
ComicType.fromKey(widget.sourceKey),
|
||||||
|
);
|
||||||
|
if (localComic == null) {
|
||||||
|
context.showMessage(message: "Local comic not found".tl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localComic.read();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NetworkError(message: error!, retry: retry, action: action);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
scrollController.addListener(onScroll);
|
scrollController.addListener(onScroll);
|
||||||
@@ -114,7 +142,8 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
ComicDetails get comic => data!;
|
ComicDetails get comic => data!;
|
||||||
|
|
||||||
void onScroll() {
|
void onScroll() {
|
||||||
var offset = scrollController.position.pixels -
|
var offset =
|
||||||
|
scrollController.position.pixels -
|
||||||
scrollController.position.minScrollExtent;
|
scrollController.position.minScrollExtent;
|
||||||
var showFAB = offset > 0;
|
var showFAB = offset > 0;
|
||||||
if (showFAB != this.showFAB) {
|
if (showFAB != this.showFAB) {
|
||||||
@@ -145,9 +174,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
floatingActionButton: showFAB
|
floatingActionButton: showFAB
|
||||||
? FloatingActionButton(
|
? FloatingActionButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
scrollController.animateTo(0,
|
scrollController.animateTo(
|
||||||
duration: const Duration(milliseconds: 200),
|
0,
|
||||||
curve: Curves.ease);
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.ease,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.arrow_upward),
|
child: const Icon(Icons.arrow_upward),
|
||||||
)
|
)
|
||||||
@@ -164,7 +195,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
buildThumbnails(),
|
buildThumbnails(),
|
||||||
buildRecommend(),
|
buildRecommend(),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: EdgeInsets.only(bottom: context.padding.bottom + 80), // Add additional padding for FAB
|
padding: EdgeInsets.only(
|
||||||
|
bottom: context.padding.bottom + 80,
|
||||||
|
), // Add additional padding for FAB
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -190,12 +223,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
initialPage: history?.page,
|
initialPage: history?.page,
|
||||||
initialChapter: history?.ep,
|
initialChapter: history?.ep,
|
||||||
initialChapterGroup: history?.group,
|
initialChapterGroup: history?.group,
|
||||||
history: history ??
|
history:
|
||||||
History.fromModel(
|
history ??
|
||||||
model: localComic,
|
History.fromModel(model: localComic, ep: 0, page: 0),
|
||||||
ep: 0,
|
|
||||||
page: 0,
|
|
||||||
),
|
|
||||||
author: localComic.subTitle ?? '',
|
author: localComic.subTitle ?? '',
|
||||||
tags: localComic.tags,
|
tags: localComic.tags,
|
||||||
);
|
);
|
||||||
@@ -215,8 +245,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
widget.id,
|
widget.id,
|
||||||
ComicType(widget.sourceKey.hashCode),
|
ComicType(widget.sourceKey.hashCode),
|
||||||
);
|
);
|
||||||
history =
|
history = HistoryManager().find(
|
||||||
HistoryManager().find(widget.id, ComicType(widget.sourceKey.hashCode));
|
widget.id,
|
||||||
|
ComicType(widget.sourceKey.hashCode),
|
||||||
|
);
|
||||||
return comicSource.loadComicInfo!(widget.id);
|
return comicSource.loadComicInfo!(widget.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,11 +257,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
isLiked = comic.isLiked ?? false;
|
isLiked = comic.isLiked ?? false;
|
||||||
isFavorite = comic.isFavorite ?? false;
|
isFavorite = comic.isFavorite ?? false;
|
||||||
if (comic.chapters == null) {
|
if (comic.chapters == null) {
|
||||||
isDownloaded = LocalManager().isDownloaded(
|
isDownloaded = LocalManager().isDownloaded(comic.id, comic.comicType, 0);
|
||||||
comic.id,
|
|
||||||
comic.comicType,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +270,9 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: showMoreActions, icon: const Icon(Icons.more_horiz))
|
onPressed: showMoreActions,
|
||||||
|
icon: const Icon(Icons.more_horiz),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -288,8 +318,10 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
children: [
|
children: [
|
||||||
SelectableText(comic.title, style: ts.s18),
|
SelectableText(comic.title, style: ts.s18),
|
||||||
if (comic.subTitle != null)
|
if (comic.subTitle != null)
|
||||||
SelectableText(comic.subTitle!, style: ts.s14)
|
SelectableText(
|
||||||
.paddingVertical(4),
|
comic.subTitle!,
|
||||||
|
style: ts.s14,
|
||||||
|
).paddingVertical(4),
|
||||||
Text(
|
Text(
|
||||||
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
||||||
style: ts.s12,
|
style: ts.s12,
|
||||||
@@ -338,10 +370,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
icon: const Icon(Icons.favorite_border),
|
icon: const Icon(Icons.favorite_border),
|
||||||
activeIcon: const Icon(Icons.favorite),
|
activeIcon: const Icon(Icons.favorite),
|
||||||
isActive: isLiked,
|
isActive: isLiked,
|
||||||
text: ((data!.likesCount != null)
|
text:
|
||||||
? (data!.likesCount! + (isLiked ? 1 : 0))
|
((data!.likesCount != null)
|
||||||
: (isLiked ? 'Liked'.tl : 'Like'.tl))
|
? (data!.likesCount! + (isLiked ? 1 : 0))
|
||||||
.toString(),
|
: (isLiked ? 'Liked'.tl : 'Like'.tl))
|
||||||
|
.toString(),
|
||||||
isLoading: isLiking,
|
isLoading: isLiking,
|
||||||
onPressed: likeOrUnlike,
|
onPressed: likeOrUnlike,
|
||||||
iconColor: context.useTextColor(Colors.red),
|
iconColor: context.useTextColor(Colors.red),
|
||||||
@@ -383,9 +416,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: hasHistory
|
child: hasHistory
|
||||||
? FilledButton(
|
? FilledButton(
|
||||||
onPressed: continueRead, child: Text("Continue".tl))
|
onPressed: continueRead,
|
||||||
|
child: Text("Continue".tl),
|
||||||
|
)
|
||||||
: FilledButton(onPressed: read, child: Text("Read".tl)),
|
: FilledButton(onPressed: read, child: Text("Read".tl)),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
).paddingHorizontal(16).paddingVertical(8),
|
).paddingHorizontal(16).paddingVertical(8),
|
||||||
if (history != null)
|
if (history != null)
|
||||||
@@ -412,19 +447,20 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
var epName = "E$ep";
|
var epName = "E$ep";
|
||||||
String? groupName;
|
String? groupName;
|
||||||
try {
|
try {
|
||||||
if (group == null){
|
if (group == null) {
|
||||||
epName = comic.chapters!.titles.elementAt(
|
epName = comic.chapters!.titles.elementAt(
|
||||||
math.min(ep - 1, comic.chapters!.length - 1),
|
math.min(ep - 1, comic.chapters!.length - 1),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
groupName = comic.chapters!.groups.elementAt(group - 1);
|
groupName = comic.chapters!.groups.elementAt(
|
||||||
|
group - 1,
|
||||||
|
);
|
||||||
epName = comic.chapters!
|
epName = comic.chapters!
|
||||||
.getGroupByIndex(group - 1)
|
.getGroupByIndex(group - 1)
|
||||||
.values
|
.values
|
||||||
.elementAt(ep - 1);
|
.elementAt(ep - 1);
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
catch(e) {
|
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
text = groupName == null
|
text = groupName == null
|
||||||
@@ -453,9 +489,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
return SliverLazyToBoxAdapter(
|
return SliverLazyToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(title: Text("Description".tl)),
|
||||||
title: Text("Description".tl),
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: SelectableText(comic.description!).fixWidth(double.infinity),
|
child: SelectableText(comic.description!).fixWidth(double.infinity),
|
||||||
@@ -539,10 +573,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(color: color, borderRadius: borderRadius),
|
||||||
color: color,
|
|
||||||
borderRadius: borderRadius,
|
|
||||||
),
|
|
||||||
child: Text(text).padding(padding),
|
child: Text(text).padding(padding),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -552,13 +583,13 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
if (int.tryParse(time) != null) {
|
if (int.tryParse(time) != null) {
|
||||||
var t = int.tryParse(time);
|
var t = int.tryParse(time);
|
||||||
if (t! > 1000000000000) {
|
if (t! > 1000000000000) {
|
||||||
return DateTime.fromMillisecondsSinceEpoch(t)
|
return DateTime.fromMillisecondsSinceEpoch(
|
||||||
.toString()
|
t,
|
||||||
.substring(0, 19);
|
).toString().substring(0, 19);
|
||||||
} else {
|
} else {
|
||||||
return DateTime.fromMillisecondsSinceEpoch(t * 1000)
|
return DateTime.fromMillisecondsSinceEpoch(
|
||||||
.toString()
|
t * 1000,
|
||||||
.substring(0, 19);
|
).toString().substring(0, 19);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (time.contains('T') || time.contains('Z')) {
|
if (time.contains('T') || time.contains('Z')) {
|
||||||
@@ -583,17 +614,11 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(title: Text("Information".tl)),
|
||||||
title: Text("Information".tl),
|
|
||||||
),
|
|
||||||
if (comic.stars != null)
|
if (comic.stars != null)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
StarRating(
|
StarRating(value: comic.stars!, size: 24, onTap: starRating),
|
||||||
value: comic.stars!,
|
|
||||||
size: 24,
|
|
||||||
onTap: starRating,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(comic.stars!.toStringAsFixed(2)),
|
Text(comic.stars!.toStringAsFixed(2)),
|
||||||
],
|
],
|
||||||
@@ -671,24 +696,19 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
if (comic.recommend == null || comic.recommend!.isEmpty) {
|
if (comic.recommend == null || comic.recommend!.isEmpty) {
|
||||||
return const SliverPadding(padding: EdgeInsets.zero);
|
return const SliverPadding(padding: EdgeInsets.zero);
|
||||||
}
|
}
|
||||||
return SliverMainAxisGroup(slivers: [
|
return SliverMainAxisGroup(
|
||||||
SliverToBoxAdapter(
|
slivers: [
|
||||||
child: ListTile(
|
SliverToBoxAdapter(child: ListTile(title: Text("Related".tl))),
|
||||||
title: Text("Related".tl),
|
SliverGridComics(comics: comic.recommend!),
|
||||||
),
|
],
|
||||||
),
|
);
|
||||||
SliverGridComics(comics: comic.recommend!),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildComments() {
|
Widget buildComments() {
|
||||||
if (comic.comments == null || comic.comments!.isEmpty) {
|
if (comic.comments == null || comic.comments!.isEmpty) {
|
||||||
return const SliverPadding(padding: EdgeInsets.zero);
|
return const SliverPadding(padding: EdgeInsets.zero);
|
||||||
}
|
}
|
||||||
return _CommentsPart(
|
return _CommentsPart(comments: comic.comments!, showMore: showComments);
|
||||||
comments: comic.comments!,
|
|
||||||
showMore: showComments,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -792,20 +812,21 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
|
|||||||
itemCount: widget.eps.length,
|
itemCount: widget.eps.length,
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
return CheckboxListTile(
|
return CheckboxListTile(
|
||||||
title: Text(widget.eps[i]),
|
title: Text(widget.eps[i]),
|
||||||
value: selected.contains(i) ||
|
value:
|
||||||
widget.downloadedEps.contains(i),
|
selected.contains(i) || widget.downloadedEps.contains(i),
|
||||||
onChanged: widget.downloadedEps.contains(i)
|
onChanged: widget.downloadedEps.contains(i)
|
||||||
? null
|
? null
|
||||||
: (v) {
|
: (v) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selected.contains(i)) {
|
if (selected.contains(i)) {
|
||||||
selected.remove(i);
|
selected.remove(i);
|
||||||
} else {
|
} else {
|
||||||
selected.add(i);
|
selected.add(i);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -813,9 +834,7 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
|
|||||||
height: 50,
|
height: 50,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(
|
top: BorderSide(color: context.colorScheme.outlineVariant),
|
||||||
color: context.colorScheme.outlineVariant,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -880,8 +899,12 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget buildContainer(double? width, double? height,
|
Widget buildContainer(
|
||||||
{Color? color, double? radius}) {
|
double? width,
|
||||||
|
double? height, {
|
||||||
|
Color? color,
|
||||||
|
double? radius,
|
||||||
|
}) {
|
||||||
return Container(
|
return Container(
|
||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
@@ -923,13 +946,9 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
|
|||||||
if (context.width < changePoint)
|
if (context.width < changePoint)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(child: buildContainer(null, 36, radius: 18)),
|
||||||
child: buildContainer(null, 36, radius: 18),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(child: buildContainer(null, 36, radius: 18)),
|
||||||
child: buildContainer(null, 36, radius: 18),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
).paddingHorizontal(16),
|
).paddingHorizontal(16),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
@@ -938,7 +957,7 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
|
|||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2.4,
|
strokeWidth: 2.4,
|
||||||
).fixHeight(24).fixWidth(24),
|
).fixHeight(24).fixWidth(24),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -948,11 +967,7 @@ class _ComicPageLoadingPlaceHolder extends StatelessWidget {
|
|||||||
Widget child;
|
Widget child;
|
||||||
if (cover != null) {
|
if (cover != null) {
|
||||||
child = AnimatedImage(
|
child = AnimatedImage(
|
||||||
image: CachedImageProvider(
|
image: CachedImageProvider(cover!, sourceKey: sourceKey, cid: cid),
|
||||||
cover!,
|
|
||||||
sourceKey: sourceKey,
|
|
||||||
cid: cid,
|
|
||||||
),
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
@@ -43,7 +43,10 @@ class ComicSourcePage extends StatelessWidget {
|
|||||||
try {
|
try {
|
||||||
var res = await AppDio().get<String>(
|
var res = await AppDio().get<String>(
|
||||||
source.url,
|
source.url,
|
||||||
options: Options(responseType: ResponseType.plain),
|
options: Options(
|
||||||
|
responseType: ResponseType.plain,
|
||||||
|
headers: {"cache-time": "no"},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (cancel) return;
|
if (cancel) return;
|
||||||
controller?.close();
|
controller?.close();
|
||||||
@@ -200,10 +203,7 @@ class _BodyState extends State<_Body> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void update(
|
void update(ComicSource source, [bool showLoading = true]) {
|
||||||
ComicSource source, [
|
|
||||||
bool showLoading = true,
|
|
||||||
]) {
|
|
||||||
ComicSourcePage.update(source, showLoading);
|
ComicSourcePage.update(source, showLoading);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +304,10 @@ class _BodyState extends State<_Body> {
|
|||||||
try {
|
try {
|
||||||
var res = await AppDio().get<String>(
|
var res = await AppDio().get<String>(
|
||||||
url,
|
url,
|
||||||
options: Options(responseType: ResponseType.plain),
|
options: Options(
|
||||||
|
responseType: ResponseType.plain,
|
||||||
|
headers: {"cache-time": "no"},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (cancel) return;
|
if (cancel) return;
|
||||||
controller.close();
|
controller.close();
|
||||||
@@ -710,11 +713,13 @@ class _CheckUpdatesButtonState extends State<_CheckUpdatesButton> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FilledButton.tonalIcon(
|
return FilledButton.tonalIcon(
|
||||||
icon: isLoading ? SizedBox(
|
icon: isLoading
|
||||||
width: 18,
|
? SizedBox(
|
||||||
height: 18,
|
width: 18,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
height: 18,
|
||||||
) : Icon(Icons.update),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: Icon(Icons.update),
|
||||||
label: Text("Check updates".tl),
|
label: Text("Check updates".tl),
|
||||||
onPressed: check,
|
onPressed: check,
|
||||||
);
|
);
|
||||||
|
@@ -512,6 +512,18 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (selectedComics.length == 1)
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.chrome_reader_mode_outlined,
|
||||||
|
text: "Read".tl,
|
||||||
|
onClick: () {
|
||||||
|
final c = selectedComics.keys.first as FavoriteItem;
|
||||||
|
App.rootContext.to(() => ReaderWithLoading(
|
||||||
|
id: c.id,
|
||||||
|
sourceKey: c.sourceKey,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@@ -62,7 +62,7 @@ class DebugPageState extends State<DebugPage> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
try {
|
try {
|
||||||
var res = JsEngine().runCode(controller.text);
|
var res = JsEngine().runCode(controller.text, "<debug>");
|
||||||
setState(() {
|
setState(() {
|
||||||
result = res.toString();
|
result = res.toString();
|
||||||
});
|
});
|
||||||
|
22
pubspec.lock
22
pubspec.lock
@@ -556,26 +556,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.9"
|
version: "11.0.1"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.9"
|
version: "3.0.10"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -941,10 +941,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
version: "0.7.6"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1037,10 +1037,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.2.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1116,4 +1116,4 @@ packages:
|
|||||||
version: "0.0.12"
|
version: "0.0.12"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.0 <4.0.0"
|
dart: ">=3.8.0 <4.0.0"
|
||||||
flutter: ">=3.32.6"
|
flutter: ">=3.35.2"
|
||||||
|
@@ -2,11 +2,11 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.4.6+146
|
version: 1.5.1+151
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.8.0 <4.0.0'
|
sdk: '>=3.8.0 <4.0.0'
|
||||||
flutter: 3.32.6
|
flutter: 3.35.3
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
Reference in New Issue
Block a user