mirror of
https://github.com/venera-app/venera.git
synced 2025-09-28 08:17:25 +00:00
Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8868a02a7e | ||
![]() |
e1b95c9e23 | ||
0b65b4ab53 | |||
df4263f969 | |||
17ef17ca5b | |||
![]() |
e55c45a589 | ||
591f2836d4 | |||
8ab4f7a34b | |||
614c01872b | |||
6be258092a | |||
ce50812857 | |||
f0b1135eb7 | |||
![]() |
cc0f070df5 | ||
35429c132c | |||
998d4c31d3 | |||
0122bb8f28 | |||
33a9fa062b | |||
13081332f2 | |||
![]() |
cdc6c95579 | ||
![]() |
3aca3baafc | ||
58d6ccdde1 | |||
23404b86f6 | |||
![]() |
965187e9de | ||
![]() |
24155746f2 | ||
340496da30 | |||
28a56b4612 | |||
4e6f71ef36 | |||
739685f60f | |||
8c5dae1e59 | |||
e2c69d882f | |||
0b9f0b7d35 | |||
9ea749a84a | |||
d675af3fb4 | |||
d99a30b7d8 | |||
![]() |
3c3c07b6fb | ||
![]() |
e688ab759a | ||
![]() |
64a3ef352f | ||
ef8dc9e8d4 |
61
.github/workflows/main.yml
vendored
61
.github/workflows/main.yml
vendored
@@ -39,12 +39,18 @@ jobs:
|
|||||||
ln -s /Applications dist/dmg_contents/Applications
|
ln -s /Applications dist/dmg_contents/Applications
|
||||||
hdiutil create -volname "venera" -srcfolder dist/dmg_contents -ov -format UDZO "dist/venera.dmg"
|
hdiutil create -volname "venera" -srcfolder dist/dmg_contents -ov -format UDZO "dist/venera.dmg"
|
||||||
|
|
||||||
|
- name: Add version to filename
|
||||||
|
run: |
|
||||||
|
APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ')
|
||||||
|
mkdir -p result
|
||||||
|
mv dist/venera.dmg result/venera-$APP_VERSION.dmg
|
||||||
|
|
||||||
# Step 4: Attach and upload artifacts (optional)
|
# Step 4: Attach and upload artifacts (optional)
|
||||||
- name: Upload DMG
|
- name: Upload DMG
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: venera.dmg
|
name: macos_build
|
||||||
path: dist/venera.dmg
|
path: result/
|
||||||
Build_IOS:
|
Build_IOS:
|
||||||
runs-on: macos-15
|
runs-on: macos-15
|
||||||
steps:
|
steps:
|
||||||
@@ -62,12 +68,17 @@ jobs:
|
|||||||
mv /Users/runner/work/venera/venera/build/ios/iphoneos/Runner.app /Users/runner/work/venera/venera/build/ios/iphoneos/Payload
|
mv /Users/runner/work/venera/venera/build/ios/iphoneos/Runner.app /Users/runner/work/venera/venera/build/ios/iphoneos/Payload
|
||||||
cd /Users/runner/work/venera/venera/build/ios/iphoneos/
|
cd /Users/runner/work/venera/venera/build/ios/iphoneos/
|
||||||
zip -r venera-ios.ipa Payload
|
zip -r venera-ios.ipa Payload
|
||||||
|
- name: Add version to filename
|
||||||
|
run: |
|
||||||
|
APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ')
|
||||||
|
mkdir -p result
|
||||||
|
mv build/ios/iphoneos/venera-ios.ipa result/venera-ios-$APP_VERSION.ipa
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: app-ios.ipa
|
name: ios_build
|
||||||
path: /Users/runner/work/venera/venera/build/ios/iphoneos/venera-ios.ipa
|
path: result/
|
||||||
Build_Android:
|
Build_Android:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
@@ -118,7 +129,7 @@ jobs:
|
|||||||
name: windows_build
|
name: windows_build
|
||||||
path: build/windows/Venera-*
|
path: build/windows/Venera-*
|
||||||
Build_Linux:
|
Build_Linux:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
@@ -130,7 +141,7 @@ jobs:
|
|||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
||||||
dart pub global activate flutter_to_debian
|
dart pub global activate flutter_to_debian
|
||||||
- run: python3 debian/build.py
|
- run: python3 debian/build.py x64
|
||||||
- run: dart run flutter_to_arch
|
- run: dart run flutter_to_arch
|
||||||
- run: |
|
- run: |
|
||||||
sudo rm -rf build/linux/arch/app.tar.gz
|
sudo rm -rf build/linux/arch/app.tar.gz
|
||||||
@@ -145,19 +156,43 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: arch_build
|
name: arch_build
|
||||||
path: build/linux/arch/
|
path: build/linux/arch/
|
||||||
|
Build_Linux_ARM64:
|
||||||
|
runs-on: ubuntu-22.04-arm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Flutter
|
||||||
|
run: |
|
||||||
|
FLUTTER_VERSION=$(grep " flutter:" pubspec.yaml | cut -d':' -f2 | tr -d ' ')
|
||||||
|
sudo apt-get update -y && sudo apt-get upgrade -y;
|
||||||
|
sudo apt-get install -y curl git unzip xz-utils zip libglu1-mesa clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev
|
||||||
|
git clone --depth 1 --branch $FLUTTER_VERSION https://github.com/flutter/flutter.git $RUNNER_TEMP/flutter
|
||||||
|
echo "$RUNNER_TEMP/flutter/bin" >> $GITHUB_PATH
|
||||||
|
- name: Install Flutter
|
||||||
|
run: flutter doctor
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
flutter pub get
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
|
||||||
|
dart pub global activate flutter_to_debian
|
||||||
|
- run: python3 debian/build.py arm64
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deb_arm64_build
|
||||||
|
path: build/linux/x64/release/debian # This is a bug related to flutter_to_debian, but it's not a big deal.
|
||||||
|
|
||||||
Release:
|
Release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
needs: [Build_MacOS, Build_IOS, Build_Android, Build_Windows, Build_Linux]
|
needs: [Build_MacOS, Build_IOS, Build_Android, Build_Windows, Build_Linux, Build_Linux_ARM64]
|
||||||
if: github.event_name == 'release' # 仅在 push 事件时执行
|
if: github.event_name == 'release' # 仅在 push 事件时执行
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: venera.dmg
|
name: macos_build
|
||||||
path: outputs
|
path: outputs
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: app-ios.ipa
|
name: ios_build
|
||||||
path: outputs
|
path: outputs
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -175,6 +210,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: arch_build
|
name: arch_build
|
||||||
path: outputs
|
path: outputs
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deb_arm64_build
|
||||||
|
path: outputs
|
||||||
- uses: softprops/action-gh-release@v2
|
- uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
# 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/releases)
|
||||||
@@ -13,7 +12,6 @@ A comic reader that support reading local and network comics.
|
|||||||
height="75">](https://f-droid.org/packages/com.github.wgh136.venera/)
|
height="75">](https://f-droid.org/packages/com.github.wgh136.venera/)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Read local comics
|
- Read local comics
|
||||||
- Use javascript to create comic sources
|
- Use javascript to create comic sources
|
||||||
- Read comics from network sources
|
- Read comics from network sources
|
||||||
@@ -23,14 +21,12 @@ A comic reader that support reading local and network comics.
|
|||||||
- Login to comment, rate, and other operations if the source supports
|
- Login to comment, rate, and other operations if the source supports
|
||||||
|
|
||||||
## Build from source
|
## Build from source
|
||||||
|
|
||||||
1. Clone the repository
|
1. Clone the repository
|
||||||
2. Install flutter, see [flutter.dev](https://flutter.dev/docs/get-started/install)
|
2. Install flutter, see [flutter.dev](https://flutter.dev/docs/get-started/install)
|
||||||
3. Install rust, see [rustup.rs](https://rustup.rs/)
|
3. Install rust, see [rustup.rs](https://rustup.rs/)
|
||||||
4. Build for your platform: e.g. `flutter build apk`
|
4. Build for your platform: e.g. `flutter build apk`
|
||||||
|
|
||||||
## Create a new comic source
|
## Create a new comic source
|
||||||
|
|
||||||
See [Comic Source](doc/comic_source.md)
|
See [Comic Source](doc/comic_source.md)
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|
@@ -83,31 +83,31 @@ android {
|
|||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
|
||||||
}
|
}
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
applicationVariants.all { variant ->
|
|
||||||
variant.outputs.all { output ->
|
|
||||||
def abi = output.getFilter(com.android.build.OutputFile.ABI)
|
|
||||||
if (abi != null) {
|
|
||||||
outputFileName = "venera-${variant.versionName}-${abi}.apk"
|
|
||||||
def abiVersionCode = project.ext.abiCodes.get(abi)
|
|
||||||
if (abiVersionCode != null) {
|
|
||||||
versionCodeOverride = variant.versionCode * 10 + abiVersionCode
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
outputFileName = "venera-${variant.versionName}.apk"
|
|
||||||
versionCodeOverride = variant.versionCode * 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
|
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
|
||||||
}
|
}
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
applicationVariants.all { variant ->
|
}
|
||||||
variant.outputs.all { output ->
|
}
|
||||||
versionCodeOverride = variant.versionCode * 10 + 4
|
|
||||||
|
applicationVariants.all { variant ->
|
||||||
|
variant.outputs.all { output ->
|
||||||
|
def abi = output.getFilter(com.android.build.OutputFile.ABI)
|
||||||
|
if (variant.buildType.name == "release") {
|
||||||
|
if (abi != null) {
|
||||||
|
outputFileName = "venera-${variant.versionName}-${abi}.apk"
|
||||||
|
def abiVersionCode = project.ext.abiCodes.get(abi)
|
||||||
|
if (abiVersionCode != null) {
|
||||||
|
versionCodeOverride = variant.versionCode * 10 + abiVersionCode
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputFileName = "venera-${variant.versionName}.apk"
|
||||||
|
versionCodeOverride = variant.versionCode * 10
|
||||||
}
|
}
|
||||||
|
} else if (variant.buildType.name == "debug") {
|
||||||
|
versionCodeOverride = variant.versionCode * 10 + 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -318,7 +318,13 @@
|
|||||||
"Deselect All": "取消全选",
|
"Deselect All": "取消全选",
|
||||||
"Add keyword": "添加关键词",
|
"Add keyword": "添加关键词",
|
||||||
"Keyword": "关键词",
|
"Keyword": "关键词",
|
||||||
"Manage": "管理"
|
"Manage": "管理",
|
||||||
|
"Verify": "验证",
|
||||||
|
"Cloudflare verification required": "需要Cloudflare验证",
|
||||||
|
"Success": "成功",
|
||||||
|
"Compressing": "压缩中",
|
||||||
|
"Exporting": "导出中",
|
||||||
|
"Search Sources": "搜索源"
|
||||||
},
|
},
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -639,6 +645,12 @@
|
|||||||
"Deselect All": "取消全選",
|
"Deselect All": "取消全選",
|
||||||
"Add keyword": "添加關鍵詞",
|
"Add keyword": "添加關鍵詞",
|
||||||
"Keyword": "關鍵詞",
|
"Keyword": "關鍵詞",
|
||||||
"Manage": "管理"
|
"Manage": "管理",
|
||||||
|
"Verify": "驗證",
|
||||||
|
"Cloudflare verification required": "需要Cloudflare驗證",
|
||||||
|
"Success": "成功",
|
||||||
|
"Compressing": "壓縮中",
|
||||||
|
"Exporting": "匯出中",
|
||||||
|
"Search Sources": "搜索源"
|
||||||
}
|
}
|
||||||
}
|
}
|
11
debian/build.py
vendored
11
debian/build.py
vendored
@@ -1,5 +1,7 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
arch = sys.argv[1]
|
||||||
debianContent = ''
|
debianContent = ''
|
||||||
desktopContent = ''
|
desktopContent = ''
|
||||||
version = ''
|
version = ''
|
||||||
@@ -12,7 +14,14 @@ with open('pubspec.yaml', 'r') as f:
|
|||||||
version = str.split(str.split(f.read(), 'version: ')[1], '+')[0]
|
version = str.split(str.split(f.read(), 'version: ')[1], '+')[0]
|
||||||
|
|
||||||
with open('debian/debian.yaml', 'w') as f:
|
with open('debian/debian.yaml', 'w') as f:
|
||||||
f.write(debianContent.replace('{{Version}}', version))
|
content = debianContent.replace('{{Version}}', version)
|
||||||
|
if arch == 'x64':
|
||||||
|
content = content.replace('{{Arch}}', 'x64')
|
||||||
|
content = content.replace('{{Architecture}}', 'amd64')
|
||||||
|
elif arch == 'arm64':
|
||||||
|
content = content.replace('{{Arch}}', 'arm64')
|
||||||
|
content = content.replace('{{Architecture}}', 'arm64')
|
||||||
|
f.write(content)
|
||||||
with open('debian/gui/venera.desktop', 'w') as f:
|
with open('debian/gui/venera.desktop', 'w') as f:
|
||||||
f.write(desktopContent.replace('{{Version}}', version))
|
f.write(desktopContent.replace('{{Version}}', version))
|
||||||
|
|
||||||
|
6
debian/debian.yaml
vendored
6
debian/debian.yaml
vendored
@@ -1,13 +1,13 @@
|
|||||||
flutter_app:
|
flutter_app:
|
||||||
command: venera
|
command: venera
|
||||||
arch: x64
|
arch: {{Arch}}
|
||||||
parent: /usr/local/lib
|
parent: /usr/local/lib
|
||||||
nonInteractive: false
|
nonInteractive: true
|
||||||
|
|
||||||
control:
|
control:
|
||||||
Package: venera
|
Package: venera
|
||||||
Version: {{Version}}
|
Version: {{Version}}
|
||||||
Architecture: amd64
|
Architecture: {{Architecture}}
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Depends: libwebkit2gtk-4.1-0, libgtk-3-0
|
Depends: libwebkit2gtk-4.1-0, libgtk-3-0
|
||||||
Maintainer: nyne
|
Maintainer: nyne
|
||||||
|
1
debian/gui/venera.desktop
vendored
1
debian/gui/venera.desktop
vendored
@@ -6,3 +6,4 @@ Terminal=false
|
|||||||
Type=Application
|
Type=Application
|
||||||
Categories=Utility
|
Categories=Utility
|
||||||
Keywords=Flutter;comic;images;
|
Keywords=Flutter;comic;images;
|
||||||
|
Icon=venera
|
@@ -550,7 +550,7 @@ class _ComicDescription extends StatelessWidget {
|
|||||||
int cnt = (constraints.maxHeight - 22).toInt() ~/ 25;
|
int cnt = (constraints.maxHeight - 22).toInt() ~/ 25;
|
||||||
return Container(
|
return Container(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
height: 22 + cnt * 25,
|
height: 21 + cnt * 24,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: const BoxDecoration(),
|
decoration: const BoxDecoration(),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
@@ -562,31 +562,30 @@ class _ComicDescription extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
for (var s in tags!)
|
for (var s in tags!)
|
||||||
Container(
|
Container(
|
||||||
height: 22,
|
height: 21,
|
||||||
padding: const EdgeInsets.fromLTRB(3, 2, 3, 2),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: constraints.maxWidth * 0.45,
|
maxWidth: constraints.maxWidth * 0.45,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: s == "Unavailable"
|
||||||
|
? context.colorScheme.errorContainer
|
||||||
|
: context.colorScheme.secondaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
widthFactor: 1,
|
||||||
|
child: Text(
|
||||||
|
enableTranslate
|
||||||
|
? TagsTranslation.translateTag(s)
|
||||||
|
: s.split(':').last,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
softWrap: true,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: s == "Unavailable"
|
),
|
||||||
? Theme.of(context).colorScheme.errorContainer
|
|
||||||
: Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.secondaryContainer,
|
|
||||||
borderRadius:
|
|
||||||
const BorderRadius.all(Radius.circular(8)),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
widthFactor: 1,
|
|
||||||
child: Text(
|
|
||||||
enableTranslate
|
|
||||||
? TagsTranslation.translateTag(s)
|
|
||||||
: s.split(':').last,
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
softWrap: true,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
))),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).toAlign(Alignment.topCenter);
|
).toAlign(Alignment.topCenter);
|
||||||
@@ -1520,14 +1519,15 @@ class SimpleComicTile extends StatelessWidget {
|
|||||||
|
|
||||||
return AnimatedTapRegion(
|
return AnimatedTapRegion(
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
onTap: onTap ?? () {
|
onTap: onTap ??
|
||||||
context.to(
|
() {
|
||||||
() => ComicPage(
|
context.to(
|
||||||
id: comic.id,
|
() => ComicPage(
|
||||||
sourceKey: comic.sourceKey,
|
id: comic.id,
|
||||||
),
|
sourceKey: comic.sourceKey,
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 92,
|
width: 92,
|
||||||
height: 114,
|
height: 114,
|
||||||
|
@@ -148,3 +148,18 @@ class SliverGridDelegateWithComics extends SliverGridDelegate {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SliverLazyToBoxAdapter extends StatelessWidget {
|
||||||
|
/// Creates a sliver that contains a single box widget which can be lazy loaded.
|
||||||
|
const SliverLazyToBoxAdapter({super.key, required this.child});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverList.list(children: [
|
||||||
|
SizedBox(),
|
||||||
|
child,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -57,7 +57,9 @@ class NetworkError extends StatelessWidget {
|
|||||||
if (cfe != null)
|
if (cfe != null)
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => passCloudflare(
|
onPressed: () => passCloudflare(
|
||||||
CloudflareException.fromString(message)!, retry!),
|
CloudflareException.fromString(message)!,
|
||||||
|
retry!,
|
||||||
|
),
|
||||||
child: Text('Verify'.tl),
|
child: Text('Verify'.tl),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@@ -130,7 +132,7 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
if (res.success) {
|
if (res.success) {
|
||||||
return res;
|
return res;
|
||||||
} else {
|
} else {
|
||||||
if(!mounted) return res;
|
if (!mounted) return res;
|
||||||
if (retry >= 3) {
|
if (retry >= 3) {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -188,7 +190,7 @@ abstract class LoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
loadDataWithRetry().then((value) async {
|
loadDataWithRetry().then((value) async {
|
||||||
if(!mounted) return;
|
if (!mounted) return;
|
||||||
if (value.success) {
|
if (value.success) {
|
||||||
data = value.data;
|
data = value.data;
|
||||||
await onDataLoaded();
|
await onDataLoaded();
|
||||||
@@ -321,21 +323,11 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildError(BuildContext context, String error) {
|
Widget buildError(BuildContext context, String error) {
|
||||||
return Center(
|
return NetworkError(
|
||||||
child: Column(
|
withAppbar: false,
|
||||||
mainAxisSize: MainAxisSize.min,
|
message: error,
|
||||||
children: [
|
retry: reset,
|
||||||
Text(error, maxLines: 3),
|
);
|
||||||
const SizedBox(height: 12),
|
|
||||||
Button.outlined(
|
|
||||||
onPressed: () {
|
|
||||||
reset();
|
|
||||||
},
|
|
||||||
child: const Text("Retry"),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
).paddingHorizontal(16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@@ -168,7 +168,15 @@ Future<void> showConfirmDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LoadingDialogController {
|
class LoadingDialogController {
|
||||||
void Function()? closeDialog;
|
double? _progress;
|
||||||
|
|
||||||
|
String? _message;
|
||||||
|
|
||||||
|
void Function()? _closeDialog;
|
||||||
|
|
||||||
|
void Function(double? value)? _serProgress;
|
||||||
|
|
||||||
|
void Function(String message)? _setMessage;
|
||||||
|
|
||||||
bool closed = false;
|
bool closed = false;
|
||||||
|
|
||||||
@@ -177,63 +185,86 @@ class LoadingDialogController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
closed = true;
|
closed = true;
|
||||||
if (closeDialog == null) {
|
if (_closeDialog == null) {
|
||||||
Future.microtask(closeDialog!);
|
Future.microtask(_closeDialog!);
|
||||||
} else {
|
} else {
|
||||||
closeDialog!();
|
_closeDialog!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setProgress(double? value) {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_serProgress?.call(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMessage(String message) {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_setMessage?.call(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadingDialogController showLoadingDialog(BuildContext context,
|
LoadingDialogController showLoadingDialog(
|
||||||
{void Function()? onCancel,
|
BuildContext context, {
|
||||||
bool barrierDismissible = true,
|
void Function()? onCancel,
|
||||||
bool allowCancel = true,
|
bool barrierDismissible = true,
|
||||||
String? message,
|
bool allowCancel = true,
|
||||||
String cancelButtonText = "Cancel"}) {
|
String? message,
|
||||||
|
String cancelButtonText = "Cancel",
|
||||||
|
bool withProgress = false,
|
||||||
|
}) {
|
||||||
var controller = LoadingDialogController();
|
var controller = LoadingDialogController();
|
||||||
|
controller._message = message;
|
||||||
|
|
||||||
|
if (withProgress) {
|
||||||
|
controller._progress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
var loadingDialogRoute = DialogRoute(
|
var loadingDialogRoute = DialogRoute(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: barrierDismissible,
|
barrierDismissible: barrierDismissible,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return Dialog(
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
child: Container(
|
controller._serProgress = (value) {
|
||||||
width: 100,
|
setState(() {
|
||||||
padding: const EdgeInsets.all(16.0),
|
controller._progress = value;
|
||||||
child: Row(
|
});
|
||||||
children: [
|
};
|
||||||
const SizedBox(
|
controller._setMessage = (message) {
|
||||||
width: 30,
|
setState(() {
|
||||||
height: 30,
|
controller._message = message;
|
||||||
child: CircularProgressIndicator(),
|
});
|
||||||
),
|
};
|
||||||
const SizedBox(
|
return ContentDialog(
|
||||||
width: 16,
|
title: controller._message ?? 'Loading',
|
||||||
),
|
content: LinearProgressIndicator(
|
||||||
Text(
|
value: controller._progress,
|
||||||
message ?? 'Loading',
|
backgroundColor: context.colorScheme.surfaceContainer,
|
||||||
style: const TextStyle(fontSize: 16),
|
).paddingHorizontal(16).paddingVertical(16),
|
||||||
),
|
actions: [
|
||||||
const Spacer(),
|
FilledButton(
|
||||||
if (allowCancel)
|
onPressed: allowCancel
|
||||||
TextButton(
|
? () {
|
||||||
onPressed: () {
|
controller.close();
|
||||||
controller.close();
|
onCancel?.call();
|
||||||
onCancel?.call();
|
}
|
||||||
},
|
: null,
|
||||||
child: Text(cancelButtonText.tl))
|
child: Text(cancelButtonText.tl),
|
||||||
],
|
)
|
||||||
),
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
var navigator = Navigator.of(context, rootNavigator: true);
|
var navigator = Navigator.of(context, rootNavigator: true);
|
||||||
|
|
||||||
navigator.push(loadingDialogRoute).then((value) => controller.closed = true);
|
navigator.push(loadingDialogRoute).then((value) => controller.closed = true);
|
||||||
|
|
||||||
controller.closeDialog = () {
|
controller._closeDialog = () {
|
||||||
navigator.removeRoute(loadingDialogRoute);
|
navigator.removeRoute(loadingDialogRoute);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -444,9 +475,7 @@ Future<int?> showSelectDialog({
|
|||||||
child: Text('Cancel'.tl),
|
child: Text('Cancel'.tl),
|
||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: current == null
|
onPressed: current == null ? null : context.pop,
|
||||||
? null
|
|
||||||
: context.pop,
|
|
||||||
child: Text('Confirm'.tl),
|
child: Text('Confirm'.tl),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@@ -10,7 +10,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.2.2";
|
final version = "1.2.4";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class _App {
|
|||||||
BuildContext get rootContext => rootNavigatorKey.currentContext!;
|
BuildContext get rootContext => rootNavigatorKey.currentContext!;
|
||||||
|
|
||||||
void rootPop() {
|
void rootPop() {
|
||||||
rootNavigatorKey.currentState?.pop();
|
rootNavigatorKey.currentState?.maybePop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void pop() {
|
void pop() {
|
||||||
|
@@ -126,6 +126,7 @@ class _Settings with ChangeNotifier {
|
|||||||
'explore_pages': [],
|
'explore_pages': [],
|
||||||
'categories': [],
|
'categories': [],
|
||||||
'favorites': [],
|
'favorites': [],
|
||||||
|
'searchSources': null,
|
||||||
'showFavoriteStatusOnTile': true,
|
'showFavoriteStatusOnTile': true,
|
||||||
'showHistoryStatusOnTile': false,
|
'showHistoryStatusOnTile': false,
|
||||||
'blockedWords': [],
|
'blockedWords': [],
|
||||||
@@ -155,7 +156,7 @@ class _Settings with ChangeNotifier {
|
|||||||
'customImageProcessing': defaultCustomImageProcessing,
|
'customImageProcessing': defaultCustomImageProcessing,
|
||||||
'sni': true,
|
'sni': true,
|
||||||
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
'autoAddLanguageFilter': 'none', // none, chinese, english, japanese
|
||||||
'comicSourceListUrl': "https://raw.githubusercontent.com/venera-app/venera-configs/master/index.json",
|
'comicSourceListUrl': "https://cdn.jsdelivr.net/gh/venera-app/venera-configs@latest/index.json",
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
|
@@ -42,11 +42,16 @@ Future<void> init() async {
|
|||||||
await ComicSource.init().wait();
|
await ComicSource.init().wait();
|
||||||
await LocalManager().init().wait();
|
await LocalManager().init().wait();
|
||||||
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
CacheManager().setLimitSize(appdata.settings['cacheSize']);
|
||||||
|
if (appdata.settings['searchSources'] == null) {
|
||||||
|
appdata.settings['searchSources'] = ComicSource.all()
|
||||||
|
.where((e) => e.searchPageData != null)
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
if (App.isAndroid) {
|
if (App.isAndroid) {
|
||||||
handleLinks();
|
handleLinks();
|
||||||
}
|
}
|
||||||
FlutterError.onError = (details) {
|
FlutterError.onError = (details) {
|
||||||
Log.error(
|
Log.error("Unhandled Exception", "${details.exception}\n${details.stack}");
|
||||||
"Unhandled Exception", "${details.exception}\n${details.stack}");
|
|
||||||
};
|
};
|
||||||
}
|
}
|
@@ -132,6 +132,38 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ThemeData getTheme(
|
||||||
|
Color primary,
|
||||||
|
Color? secondary,
|
||||||
|
Color? tertiary,
|
||||||
|
Brightness brightness,
|
||||||
|
) {
|
||||||
|
String? font;
|
||||||
|
List<String>? fallback;
|
||||||
|
if (App.isWindows) {
|
||||||
|
font = 'Segoe UI';
|
||||||
|
fallback = [
|
||||||
|
'Segoe UI',
|
||||||
|
'Microsoft YaHei',
|
||||||
|
'PingFang SC',
|
||||||
|
'Noto Sans CJK',
|
||||||
|
'Arial',
|
||||||
|
'sans-serif'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return ThemeData(
|
||||||
|
colorScheme: SeedColorScheme.fromSeeds(
|
||||||
|
primaryKey: primary,
|
||||||
|
secondaryKey: secondary,
|
||||||
|
tertiaryKey: tertiary,
|
||||||
|
brightness: brightness,
|
||||||
|
tones: FlexTones.vividBackground(brightness),
|
||||||
|
),
|
||||||
|
fontFamily: font,
|
||||||
|
fontFamilyFallback: fallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget home;
|
Widget home;
|
||||||
@@ -158,24 +190,9 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: home,
|
home: home,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: getTheme(primary, secondary, tertiary, Brightness.light),
|
||||||
colorScheme: SeedColorScheme.fromSeeds(
|
|
||||||
primaryKey: primary,
|
|
||||||
secondaryKey: secondary,
|
|
||||||
tertiaryKey: tertiary,
|
|
||||||
tones: FlexTones.vividBackground(Brightness.light),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
navigatorKey: App.rootNavigatorKey,
|
navigatorKey: App.rootNavigatorKey,
|
||||||
darkTheme: ThemeData(
|
darkTheme: getTheme(primary, secondary, tertiary, Brightness.dark),
|
||||||
colorScheme: SeedColorScheme.fromSeeds(
|
|
||||||
primaryKey: primary,
|
|
||||||
secondaryKey: secondary,
|
|
||||||
tertiaryKey: tertiary,
|
|
||||||
brightness: Brightness.dark,
|
|
||||||
tones: FlexTones.vividBackground(Brightness.dark),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
themeMode: switch (appdata.settings['theme_mode']) {
|
themeMode: switch (appdata.settings['theme_mode']) {
|
||||||
'light' => ThemeMode.light,
|
'light' => ThemeMode.light,
|
||||||
'dark' => ThemeMode.dark,
|
'dark' => ThemeMode.dark,
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import 'dart:io' as io;
|
import 'dart:io' as io;
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
import 'package:venera/foundation/consts.dart';
|
import 'package:venera/foundation/consts.dart';
|
||||||
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/pages/webview.dart';
|
import 'package:venera/pages/webview.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
|
|
||||||
@@ -58,7 +60,7 @@ class CloudflareException implements DioException {
|
|||||||
class CloudflareInterceptor extends Interceptor {
|
class CloudflareInterceptor extends Interceptor {
|
||||||
@override
|
@override
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
if(options.headers['cookie'].toString().contains('cf_clearance')) {
|
if (options.headers['cookie'].toString().contains('cf_clearance')) {
|
||||||
options.headers['user-agent'] = appdata.implicitData['ua'] ?? webUA;
|
options.headers['user-agent'] = appdata.implicitData['ua'] ?? webUA;
|
||||||
}
|
}
|
||||||
handler.next(options);
|
handler.next(options);
|
||||||
@@ -120,16 +122,25 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
|
|||||||
var webview = DesktopWebview(
|
var webview = DesktopWebview(
|
||||||
initialUrl: url,
|
initialUrl: url,
|
||||||
onTitleChange: (title, controller) async {
|
onTitleChange: (title, controller) async {
|
||||||
var res = await controller.evaluateJavascript(
|
var head =
|
||||||
"document.head.innerHTML.includes('#challenge-success-text')");
|
await controller.evaluateJavascript("document.head.innerHTML") ??
|
||||||
if (res == 'false') {
|
"";
|
||||||
|
Log.info("Cloudflare", "Checking head: $head");
|
||||||
|
var isChallenging = head.contains('#challenge-success-text') ||
|
||||||
|
head.contains("#challenge-error-text") ||
|
||||||
|
head.contains("#challenge-form");
|
||||||
|
if (!isChallenging) {
|
||||||
|
Log.info(
|
||||||
|
"Cloudflare",
|
||||||
|
"Cloudflare is passed due to there is no challenge css",
|
||||||
|
);
|
||||||
var ua = controller.userAgent;
|
var ua = controller.userAgent;
|
||||||
if (ua != null) {
|
if (ua != null) {
|
||||||
appdata.implicitData['ua'] = ua;
|
appdata.implicitData['ua'] = ua;
|
||||||
appdata.writeImplicitData();
|
appdata.writeImplicitData();
|
||||||
}
|
}
|
||||||
var cookiesMap = await controller.getCookies(url);
|
var cookiesMap = await controller.getCookies(url);
|
||||||
if(cookiesMap['cf_clearance'] == null) {
|
if (cookiesMap['cf_clearance'] == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
saveCookies(cookiesMap);
|
saveCookies(cookiesMap);
|
||||||
@@ -137,30 +148,51 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
|
|||||||
onFinished();
|
onFinished();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onClose: onFinished,
|
||||||
);
|
);
|
||||||
webview.open();
|
webview.open();
|
||||||
} else {
|
} else {
|
||||||
|
bool success = false;
|
||||||
|
void check(InAppWebViewController controller) async {
|
||||||
|
var head = await controller.evaluateJavascript(
|
||||||
|
source: "document.head.innerHTML") as String;
|
||||||
|
Log.info("Cloudflare", "Checking head: $head");
|
||||||
|
var isChallenging = head.contains('#challenge-success-text') ||
|
||||||
|
head.contains("#challenge-error-text") ||
|
||||||
|
head.contains("#challenge-form");
|
||||||
|
if (!isChallenging) {
|
||||||
|
Log.info(
|
||||||
|
"Cloudflare",
|
||||||
|
"Cloudflare is passed due to there is no challenge css",
|
||||||
|
);
|
||||||
|
var ua = await controller.getUA();
|
||||||
|
if (ua != null) {
|
||||||
|
appdata.implicitData['ua'] = ua;
|
||||||
|
appdata.writeImplicitData();
|
||||||
|
}
|
||||||
|
var cookies = await controller.getCookies(url) ?? [];
|
||||||
|
if (cookies.firstWhereOrNull(
|
||||||
|
(element) => element.name == 'cf_clearance') ==
|
||||||
|
null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies);
|
||||||
|
if (!success) {
|
||||||
|
App.rootPop();
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await App.rootContext.to(
|
await App.rootContext.to(
|
||||||
() => AppWebview(
|
() => AppWebview(
|
||||||
initialUrl: url,
|
initialUrl: url,
|
||||||
singlePage: true,
|
singlePage: true,
|
||||||
|
onTitleChange: (title, controller) async {
|
||||||
|
check(controller);
|
||||||
|
},
|
||||||
onLoadStop: (controller) async {
|
onLoadStop: (controller) async {
|
||||||
var res = await controller.platform.evaluateJavascript(
|
check(controller);
|
||||||
source:
|
|
||||||
"document.head.innerHTML.includes('#challenge-success-text')");
|
|
||||||
if (res == false) {
|
|
||||||
var ua = await controller.getUA();
|
|
||||||
if (ua != null) {
|
|
||||||
appdata.implicitData['ua'] = ua;
|
|
||||||
appdata.writeImplicitData();
|
|
||||||
}
|
|
||||||
var cookies = await controller.getCookies(url) ?? [];
|
|
||||||
if(cookies.firstWhereOrNull((element) => element.name == 'cf_clearance') == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies);
|
|
||||||
App.rootPop();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onStarted: (controller) async {
|
onStarted: (controller) async {
|
||||||
var ua = await controller.getUA();
|
var ua = await controller.getUA();
|
||||||
|
@@ -59,6 +59,16 @@ abstract class DownloadTask with ChangeNotifier {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is DownloadTask &&
|
||||||
|
other.id == id &&
|
||||||
|
other.comicType == comicType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(id, comicType);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
||||||
@@ -220,7 +230,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
runRecorder();
|
runRecorder();
|
||||||
|
|
||||||
if (comic == null) {
|
if (comic == null) {
|
||||||
var res = await runWithRetry(() async {
|
_message = "Fetching comic info...";
|
||||||
|
notifyListeners();
|
||||||
|
var res = await _runWithRetry(() async {
|
||||||
var r = await source.loadComicInfo!(comicId);
|
var r = await source.loadComicInfo!(comicId);
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
throw r.errorMessage!;
|
throw r.errorMessage!;
|
||||||
@@ -260,7 +272,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
await LocalManager().saveCurrentDownloadingTasks();
|
await LocalManager().saveCurrentDownloadingTasks();
|
||||||
|
|
||||||
if (_cover == null) {
|
if (_cover == null) {
|
||||||
var res = await runWithRetry(() async {
|
_message = "Downloading cover...";
|
||||||
|
notifyListeners();
|
||||||
|
var res = await _runWithRetry(() async {
|
||||||
Uint8List? data;
|
Uint8List? data;
|
||||||
await for (var progress
|
await for (var progress
|
||||||
in ImageDownloader.loadThumbnail(comic!.cover, source.key)) {
|
in ImageDownloader.loadThumbnail(comic!.cover, source.key)) {
|
||||||
@@ -272,8 +286,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
throw "Failed to download cover";
|
throw "Failed to download cover";
|
||||||
}
|
}
|
||||||
var fileType = detectFileType(data);
|
var fileType = detectFileType(data);
|
||||||
var file =
|
var file = File(FilePath.join(path!, "cover${fileType.ext}"));
|
||||||
File(FilePath.join(path!, "cover${fileType.ext}"));
|
|
||||||
file.writeAsBytesSync(data);
|
file.writeAsBytesSync(data);
|
||||||
return "file://${file.path}";
|
return "file://${file.path}";
|
||||||
});
|
});
|
||||||
@@ -290,7 +303,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
|
|
||||||
if (_images == null) {
|
if (_images == null) {
|
||||||
if (comic!.chapters == null) {
|
if (comic!.chapters == null) {
|
||||||
var res = await runWithRetry(() async {
|
_message = "Fetching image list...";
|
||||||
|
notifyListeners();
|
||||||
|
var res = await _runWithRetry(() async {
|
||||||
var r = await source.loadComicPages!(comicId, null);
|
var r = await source.loadComicPages!(comicId, null);
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
throw r.errorMessage!;
|
throw r.errorMessage!;
|
||||||
@@ -312,6 +327,8 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
} else {
|
} else {
|
||||||
_images = {};
|
_images = {};
|
||||||
_totalCount = 0;
|
_totalCount = 0;
|
||||||
|
int cpCount = 0;
|
||||||
|
int totalCpCount = chapters?.length ?? comic!.chapters!.length;
|
||||||
for (var i in comic!.chapters!.keys) {
|
for (var i in comic!.chapters!.keys) {
|
||||||
if (chapters != null && !chapters!.contains(i)) {
|
if (chapters != null && !chapters!.contains(i)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -320,7 +337,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
_totalCount += _images![i]!.length;
|
_totalCount += _images![i]!.length;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var res = await runWithRetry(() async {
|
_message = "Fetching image list ($cpCount/$totalCpCount)...";
|
||||||
|
notifyListeners();
|
||||||
|
var res = await _runWithRetry(() async {
|
||||||
var r = await source.loadComicPages!(comicId, i);
|
var r = await source.loadComicPages!(comicId, i);
|
||||||
if (r.error) {
|
if (r.error) {
|
||||||
throw r.errorMessage!;
|
throw r.errorMessage!;
|
||||||
@@ -458,8 +477,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
}).toList(),
|
}).toList(),
|
||||||
directory: Directory(path!).name,
|
directory: Directory(path!).name,
|
||||||
chapters: comic!.chapters,
|
chapters: comic!.chapters,
|
||||||
cover:
|
cover: File(_cover!.split("file://").last).name,
|
||||||
File(_cover!.split("file://").last).name,
|
|
||||||
comicType: ComicType(source.key.hashCode),
|
comicType: ComicType(source.key.hashCode),
|
||||||
downloadedChapters: chapters ?? [],
|
downloadedChapters: chapters ?? [],
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
@@ -478,7 +496,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
|
|||||||
int get hashCode => Object.hash(comicId, source.key);
|
int get hashCode => Object.hash(comicId, source.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Res<T>> runWithRetry<T>(Future<T> Function() task,
|
Future<Res<T>> _runWithRetry<T>(Future<T> Function() task,
|
||||||
{int retry = 3}) async {
|
{int retry = 3}) async {
|
||||||
for (var i = 0; i < retry; i++) {
|
for (var i = 0; i < retry; i++) {
|
||||||
try {
|
try {
|
||||||
|
@@ -1,349 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
import 'package:venera/components/components.dart';
|
|
||||||
import 'package:venera/foundation/app.dart';
|
|
||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
|
||||||
import 'package:venera/foundation/state_controller.dart';
|
|
||||||
import 'package:venera/network/cookie_jar.dart';
|
|
||||||
import 'package:venera/pages/webview.dart';
|
|
||||||
import 'package:venera/utils/translations.dart';
|
|
||||||
|
|
||||||
class AccountsPageLogic extends StateController {
|
|
||||||
final _reLogin = <String, bool>{};
|
|
||||||
}
|
|
||||||
|
|
||||||
class AccountsPage extends StatelessWidget {
|
|
||||||
const AccountsPage({super.key});
|
|
||||||
|
|
||||||
AccountsPageLogic get logic => StateController.find<AccountsPageLogic>();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var body = StateBuilder<AccountsPageLogic>(
|
|
||||||
init: AccountsPageLogic(),
|
|
||||||
builder: (logic) {
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverAppbar(title: Text("Accounts".tl)),
|
|
||||||
SliverList(
|
|
||||||
delegate: SliverChildListDelegate(
|
|
||||||
buildContent(context).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverPadding(
|
|
||||||
padding: EdgeInsets.only(bottom: context.padding.bottom),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
body: body,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<Widget> buildContent(BuildContext context) sync* {
|
|
||||||
var sources = ComicSource.all().where((element) => element.account != null);
|
|
||||||
if (sources.isEmpty) return;
|
|
||||||
|
|
||||||
for (var element in sources) {
|
|
||||||
final bool logged = element.isLogged;
|
|
||||||
yield Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
|
||||||
child: Text(
|
|
||||||
element.name,
|
|
||||||
style: const TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (!logged) {
|
|
||||||
yield ListTile(
|
|
||||||
title: Text("Log in".tl),
|
|
||||||
trailing: const Icon(Icons.arrow_right),
|
|
||||||
onTap: () async {
|
|
||||||
await context.to(
|
|
||||||
() => _LoginPage(
|
|
||||||
config: element.account!,
|
|
||||||
source: element,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
element.saveData();
|
|
||||||
ComicSource.notifyListeners();
|
|
||||||
logic.update();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (logged) {
|
|
||||||
for (var item in element.account!.infoItems) {
|
|
||||||
if (item.builder != null) {
|
|
||||||
yield item.builder!(context);
|
|
||||||
} else {
|
|
||||||
yield ListTile(
|
|
||||||
title: Text(item.title.tl),
|
|
||||||
subtitle: item.data == null ? null : Text(item.data!()),
|
|
||||||
onTap: item.onTap,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (element.data["account"] is List) {
|
|
||||||
bool loading = logic._reLogin[element.key] == true;
|
|
||||||
yield ListTile(
|
|
||||||
title: Text("Re-login".tl),
|
|
||||||
subtitle: Text("Click if login expired".tl),
|
|
||||||
onTap: () async {
|
|
||||||
if (element.data["account"] == null) {
|
|
||||||
context.showMessage(message: "No data".tl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logic._reLogin[element.key] = true;
|
|
||||||
logic.update();
|
|
||||||
final List account = element.data["account"];
|
|
||||||
var res = await element.account!.login!(account[0], account[1]);
|
|
||||||
if (res.error) {
|
|
||||||
context.showMessage(message: res.errorMessage!);
|
|
||||||
} else {
|
|
||||||
context.showMessage(message: "Success".tl);
|
|
||||||
}
|
|
||||||
logic._reLogin[element.key] = false;
|
|
||||||
logic.update();
|
|
||||||
},
|
|
||||||
trailing: loading
|
|
||||||
? const SizedBox.square(
|
|
||||||
dimension: 24,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.refresh),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
yield ListTile(
|
|
||||||
title: Text("Log out".tl),
|
|
||||||
onTap: () {
|
|
||||||
element.data["account"] = null;
|
|
||||||
element.account?.logout();
|
|
||||||
element.saveData();
|
|
||||||
ComicSource.notifyListeners();
|
|
||||||
logic.update();
|
|
||||||
},
|
|
||||||
trailing: const Icon(Icons.logout),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
yield const Divider(thickness: 0.6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setClipboard(String text) {
|
|
||||||
Clipboard.setData(ClipboardData(text: text));
|
|
||||||
showToast(
|
|
||||||
message: "Copied".tl,
|
|
||||||
icon: const Icon(Icons.check),
|
|
||||||
context: App.rootContext,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LoginPage extends StatefulWidget {
|
|
||||||
const _LoginPage({required this.config, required this.source});
|
|
||||||
|
|
||||||
final AccountConfig config;
|
|
||||||
|
|
||||||
final ComicSource source;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_LoginPage> createState() => _LoginPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LoginPageState extends State<_LoginPage> {
|
|
||||||
String username = "";
|
|
||||||
String password = "";
|
|
||||||
bool loading = false;
|
|
||||||
|
|
||||||
final Map<String, String> _cookies = {};
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: const Appbar(
|
|
||||||
title: Text(''),
|
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
constraints: const BoxConstraints(maxWidth: 400),
|
|
||||||
child: AutofillGroup(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text("Login".tl, style: const TextStyle(fontSize: 24)),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
if (widget.config.cookieFields == null)
|
|
||||||
TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Username".tl,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
enabled: widget.config.login != null,
|
|
||||||
onChanged: (s) {
|
|
||||||
username = s;
|
|
||||||
},
|
|
||||||
autofillHints: const [AutofillHints.username],
|
|
||||||
).paddingBottom(16),
|
|
||||||
if (widget.config.cookieFields == null)
|
|
||||||
TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: "Password".tl,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
obscureText: true,
|
|
||||||
enabled: widget.config.login != null,
|
|
||||||
onChanged: (s) {
|
|
||||||
password = s;
|
|
||||||
},
|
|
||||||
onSubmitted: (s) => login(),
|
|
||||||
autofillHints: const [AutofillHints.password],
|
|
||||||
).paddingBottom(16),
|
|
||||||
for (var field in widget.config.cookieFields ?? <String>[])
|
|
||||||
TextField(
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: field,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
obscureText: true,
|
|
||||||
enabled: widget.config.validateCookies != null,
|
|
||||||
onChanged: (s) {
|
|
||||||
_cookies[field] = s;
|
|
||||||
},
|
|
||||||
).paddingBottom(16),
|
|
||||||
if (widget.config.login == null &&
|
|
||||||
widget.config.cookieFields == null)
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.error_outline),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text("Login with password is disabled".tl),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Button.filled(
|
|
||||||
isLoading: loading,
|
|
||||||
onPressed: login,
|
|
||||||
child: Text("Continue".tl),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
if (widget.config.loginWebsite != null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: loginWithWebview,
|
|
||||||
child: Text("Login with webview".tl),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
if (widget.config.registerWebsite != null)
|
|
||||||
TextButton(
|
|
||||||
onPressed: () =>
|
|
||||||
launchUrlString(widget.config.registerWebsite!),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.link),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text("Create Account".tl),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void login() {
|
|
||||||
if (widget.config.login != null) {
|
|
||||||
if (username.isEmpty || password.isEmpty) {
|
|
||||||
showToast(
|
|
||||||
message: "Cannot be empty".tl,
|
|
||||||
icon: const Icon(Icons.error_outline),
|
|
||||||
context: context,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
loading = true;
|
|
||||||
});
|
|
||||||
widget.config.login!(username, password).then((value) {
|
|
||||||
if (value.error) {
|
|
||||||
context.showMessage(message: value.errorMessage!);
|
|
||||||
setState(() {
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (mounted) {
|
|
||||||
context.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (widget.config.validateCookies != null) {
|
|
||||||
setState(() {
|
|
||||||
loading = true;
|
|
||||||
});
|
|
||||||
var cookies =
|
|
||||||
widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList();
|
|
||||||
widget.config.validateCookies!(cookies).then((value) {
|
|
||||||
if (value) {
|
|
||||||
widget.source.data['account'] = 'ok';
|
|
||||||
widget.source.saveData();
|
|
||||||
context.pop();
|
|
||||||
} else {
|
|
||||||
context.showMessage(message: "Invalid cookies".tl);
|
|
||||||
setState(() {
|
|
||||||
loading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void loginWithWebview() async {
|
|
||||||
var url = widget.config.loginWebsite!;
|
|
||||||
var title = '';
|
|
||||||
bool success = false;
|
|
||||||
|
|
||||||
void validate(InAppWebViewController c) async {
|
|
||||||
if (widget.config.checkLoginStatus != null
|
|
||||||
&& widget.config.checkLoginStatus!(url, title)) {
|
|
||||||
var cookies = (await c.getCookies(url)) ?? [];
|
|
||||||
SingleInstanceCookieJar.instance?.saveFromResponse(
|
|
||||||
Uri.parse(url),
|
|
||||||
cookies,
|
|
||||||
);
|
|
||||||
success = true;
|
|
||||||
widget.config.onLoginWithWebviewSuccess?.call();
|
|
||||||
App.mainNavigatorKey?.currentContext?.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await context.to(
|
|
||||||
() => AppWebview(
|
|
||||||
initialUrl: widget.config.loginWebsite!,
|
|
||||||
onNavigation: (u, c) {
|
|
||||||
url = u;
|
|
||||||
validate(c);
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
onTitleChange: (t, c) {
|
|
||||||
title = t;
|
|
||||||
validate(c);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (success) {
|
|
||||||
widget.source.data['account'] = 'ok';
|
|
||||||
widget.source.saveData();
|
|
||||||
context.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,6 +2,7 @@ import "package:flutter/material.dart";
|
|||||||
import 'package:shimmer_animation/shimmer_animation.dart';
|
import 'package:shimmer_animation/shimmer_animation.dart';
|
||||||
import "package:venera/components/components.dart";
|
import "package:venera/components/components.dart";
|
||||||
import "package:venera/foundation/app.dart";
|
import "package:venera/foundation/app.dart";
|
||||||
|
import "package:venera/foundation/appdata.dart";
|
||||||
import "package:venera/foundation/comic_source/comic_source.dart";
|
import "package:venera/foundation/comic_source/comic_source.dart";
|
||||||
import "package:venera/pages/search_result_page.dart";
|
import "package:venera/pages/search_result_page.dart";
|
||||||
import "package:venera/utils/translations.dart";
|
import "package:venera/utils/translations.dart";
|
||||||
@@ -24,7 +25,18 @@ class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
sources = ComicSource.all().where((e) => e.searchPageData != null).toList();
|
var all = ComicSource.all()
|
||||||
|
.where((e) => e.searchPageData != null)
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
var settings = appdata.settings['searchSources'] as List;
|
||||||
|
var sources = <String>[];
|
||||||
|
for (var source in settings) {
|
||||||
|
if (all.contains(source)) {
|
||||||
|
sources.add(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.sources = sources.map((e) => ComicSource.find(e)!).toList();
|
||||||
_keyword = widget.keyword;
|
_keyword = widget.keyword;
|
||||||
controller = SearchBarController(
|
controller = SearchBarController(
|
||||||
currentText: widget.keyword,
|
currentText: widget.keyword,
|
||||||
|
@@ -206,62 +206,64 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
|
|
||||||
yield const SliverPadding(padding: EdgeInsets.only(top: 8));
|
yield const SliverPadding(padding: EdgeInsets.only(top: 8));
|
||||||
|
|
||||||
yield Row(
|
yield SliverLazyToBoxAdapter(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Row(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
const SizedBox(width: 16),
|
children: [
|
||||||
Hero(
|
const SizedBox(width: 16),
|
||||||
tag: "cover${comic.id}${comic.sourceKey}",
|
Hero(
|
||||||
child: Container(
|
tag: "cover${comic.id}${comic.sourceKey}",
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
color: context.colorScheme.primaryContainer,
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
color: context.colorScheme.primaryContainer,
|
||||||
boxShadow: [
|
borderRadius: BorderRadius.circular(8),
|
||||||
BoxShadow(
|
boxShadow: [
|
||||||
color: context.colorScheme.outlineVariant,
|
BoxShadow(
|
||||||
blurRadius: 1,
|
color: context.colorScheme.outlineVariant,
|
||||||
offset: const Offset(0, 1),
|
blurRadius: 1,
|
||||||
|
offset: const Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
height: 144,
|
||||||
|
width: 144 * 0.72,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: AnimatedImage(
|
||||||
|
image: CachedImageProvider(
|
||||||
|
widget.cover ?? comic.cover,
|
||||||
|
sourceKey: comic.sourceKey,
|
||||||
|
cid: comic.id,
|
||||||
|
),
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SelectableText(comic.title, style: ts.s18),
|
||||||
|
if (comic.subTitle != null)
|
||||||
|
SelectableText(comic.subTitle!, style: ts.s14)
|
||||||
|
.paddingVertical(4),
|
||||||
|
Text(
|
||||||
|
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
||||||
|
style: ts.s12,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
height: 144,
|
|
||||||
width: 144 * 0.72,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: AnimatedImage(
|
|
||||||
image: CachedImageProvider(
|
|
||||||
widget.cover ?? comic.cover,
|
|
||||||
sourceKey: comic.sourceKey,
|
|
||||||
cid: comic.id,
|
|
||||||
),
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(width: 16),
|
),
|
||||||
Expanded(
|
);
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
SelectableText(comic.title, style: ts.s18),
|
|
||||||
if (comic.subTitle != null)
|
|
||||||
SelectableText(comic.subTitle!, style: ts.s14)
|
|
||||||
.paddingVertical(4),
|
|
||||||
Text(
|
|
||||||
(ComicSource.find(comic.sourceKey)?.name) ?? '',
|
|
||||||
style: ts.s12,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).toSliver();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildActions() {
|
Widget buildActions() {
|
||||||
bool isMobile = context.width < changePoint;
|
bool isMobile = context.width < changePoint;
|
||||||
bool hasHistory = history != null && (history!.ep > 1 || history!.page > 1);
|
bool hasHistory = history != null && (history!.ep > 1 || history!.page > 1);
|
||||||
return SliverToBoxAdapter(
|
return SliverLazyToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ListView(
|
ListView(
|
||||||
@@ -354,7 +356,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
if (comic.description == null || comic.description!.trim().isEmpty) {
|
if (comic.description == null || comic.description!.trim().isEmpty) {
|
||||||
return const SliverPadding(padding: EdgeInsets.zero);
|
return const SliverPadding(padding: EdgeInsets.zero);
|
||||||
}
|
}
|
||||||
return SliverToBoxAdapter(
|
return SliverLazyToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
@@ -482,7 +484,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
bool enableTranslation =
|
bool enableTranslation =
|
||||||
App.locale.languageCode == 'zh' && comicSource.enableTagsTranslate;
|
App.locale.languageCode == 'zh' && comicSource.enableTagsTranslate;
|
||||||
|
|
||||||
return SliverToBoxAdapter(
|
return SliverLazyToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -1388,42 +1390,67 @@ class _FavoritePanel extends StatefulWidget {
|
|||||||
State<_FavoritePanel> createState() => _FavoritePanelState();
|
State<_FavoritePanel> createState() => _FavoritePanelState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FavoritePanelState extends State<_FavoritePanel> {
|
class _FavoritePanelState extends State<_FavoritePanel>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
late ComicSource comicSource;
|
late ComicSource comicSource;
|
||||||
|
|
||||||
|
late TabController tabController;
|
||||||
|
|
||||||
|
late bool hasNetwork;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
comicSource = widget.type.comicSource!;
|
comicSource = widget.type.comicSource!;
|
||||||
localFolders = LocalFavoritesManager().folderNames;
|
localFolders = LocalFavoritesManager().folderNames;
|
||||||
added = LocalFavoritesManager().find(widget.cid, widget.type);
|
added = LocalFavoritesManager().find(widget.cid, widget.type);
|
||||||
|
hasNetwork = comicSource.favoriteData != null && comicSource.isLogged;
|
||||||
|
var initIndex = 0;
|
||||||
|
if (appdata.implicitData['favoritePanelIndex'] is int) {
|
||||||
|
initIndex = appdata.implicitData['favoritePanelIndex'];
|
||||||
|
}
|
||||||
|
initIndex = initIndex.clamp(0, hasNetwork ? 1 : 0);
|
||||||
|
tabController = TabController(
|
||||||
|
initialIndex: initIndex,
|
||||||
|
length: hasNetwork ? 2 : 1,
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
var currentIndex = tabController.index;
|
||||||
|
appdata.implicitData['favoritePanelIndex'] = currentIndex;
|
||||||
|
appdata.writeImplicitData();
|
||||||
|
tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var hasNetwork = comicSource.favoriteData != null && comicSource.isLogged;
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: Appbar(
|
appBar: Appbar(
|
||||||
title: Text("Favorite".tl),
|
title: Text("Favorite".tl),
|
||||||
),
|
),
|
||||||
body: DefaultTabController(
|
body: Column(
|
||||||
length: hasNetwork ? 2 : 1,
|
children: [
|
||||||
child: Column(
|
TabBar(
|
||||||
children: [
|
controller: tabController,
|
||||||
TabBar(tabs: [
|
tabs: [
|
||||||
Tab(text: "Local".tl),
|
Tab(text: "Local".tl),
|
||||||
if (hasNetwork) Tab(text: "Network".tl),
|
if (hasNetwork) Tab(text: "Network".tl),
|
||||||
]),
|
],
|
||||||
Expanded(
|
),
|
||||||
child: TabBarView(
|
Expanded(
|
||||||
children: [
|
child: TabBarView(
|
||||||
buildLocal(),
|
controller: tabController,
|
||||||
if (hasNetwork) buildNetwork(),
|
children: [
|
||||||
],
|
buildLocal(),
|
||||||
),
|
if (hasNetwork) buildNetwork(),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1850,7 +1877,7 @@ class _CommentsPartState extends State<_CommentsPart> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiSliver(
|
return MultiSliver(
|
||||||
children: [
|
children: [
|
||||||
SliverToBoxAdapter(
|
SliverLazyToBoxAdapter(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text("Comments".tl),
|
title: Text("Comments".tl),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io' as io;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
@@ -7,11 +9,13 @@ import 'package:venera/foundation/appdata.dart';
|
|||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.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/pages/webview.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
class ComicSourcePage extends StatefulWidget {
|
class ComicSourcePage extends StatelessWidget {
|
||||||
const ComicSourcePage({super.key});
|
const ComicSourcePage({super.key});
|
||||||
|
|
||||||
static Future<int> checkComicSourceUpdate() async {
|
static Future<int> checkComicSourceUpdate() async {
|
||||||
@@ -44,11 +48,6 @@ class ComicSourcePage extends StatefulWidget {
|
|||||||
return shouldUpdate.length;
|
return shouldUpdate.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
State<ComicSourcePage> createState() => _ComicSourcePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ComicSourcePageState extends State<ComicSourcePage> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -92,167 +91,19 @@ class _BodyState extends State<_Body> {
|
|||||||
style: AppbarStyle.shadow,
|
style: AppbarStyle.shadow,
|
||||||
),
|
),
|
||||||
buildCard(context),
|
buildCard(context),
|
||||||
for (var source in ComicSource.all()) buildSource(context, source),
|
for (var source in ComicSource.all())
|
||||||
|
_SliverComicSource(
|
||||||
|
key: ValueKey(source.key),
|
||||||
|
source: source,
|
||||||
|
edit: edit,
|
||||||
|
update: update,
|
||||||
|
delete: delete,
|
||||||
|
),
|
||||||
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)),
|
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildSource(BuildContext context, ComicSource source) {
|
|
||||||
var newVersion = ComicSource.availableUpdates[source.key];
|
|
||||||
bool hasUpdate =
|
|
||||||
newVersion != null && compareSemVer(newVersion, source.version);
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
Text(source.name),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
if (hasUpdate)
|
|
||||||
Tooltip(
|
|
||||||
message: newVersion,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 6, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.colorScheme.primaryContainer,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
"New Version".tl,
|
|
||||||
style: const TextStyle(fontSize: 13),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Tooltip(
|
|
||||||
message: "Edit".tl,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () => edit(source),
|
|
||||||
icon: const Icon(Icons.edit_note)),
|
|
||||||
),
|
|
||||||
Tooltip(
|
|
||||||
message: "Update".tl,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () => update(source),
|
|
||||||
icon: const Icon(Icons.update)),
|
|
||||||
),
|
|
||||||
Tooltip(
|
|
||||||
message: "Delete".tl,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () => delete(source),
|
|
||||||
icon: const Icon(Icons.delete)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
title: const Text("Version"),
|
|
||||||
subtitle: Text(source.version),
|
|
||||||
),
|
|
||||||
...buildSourceSettings(source),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<Widget> buildSourceSettings(ComicSource source) sync* {
|
|
||||||
if (source.settings == null) {
|
|
||||||
return;
|
|
||||||
} else if (source.data['settings'] == null) {
|
|
||||||
source.data['settings'] = {};
|
|
||||||
}
|
|
||||||
for (var item in source.settings!.entries) {
|
|
||||||
var key = item.key;
|
|
||||||
String type = item.value['type'];
|
|
||||||
try {
|
|
||||||
if (type == "select") {
|
|
||||||
var current = source.data['settings'][key];
|
|
||||||
if (current == null) {
|
|
||||||
var d = item.value['default'];
|
|
||||||
for (var option in item.value['options']) {
|
|
||||||
if (option['value'] == d) {
|
|
||||||
current = option['text'] ?? option['value'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current = item.value['options']
|
|
||||||
.firstWhere((e) => e['value'] == current)['text'] ??
|
|
||||||
current;
|
|
||||||
}
|
|
||||||
yield ListTile(
|
|
||||||
title: Text((item.value['title'] as String).ts(source.key)),
|
|
||||||
trailing: Select(
|
|
||||||
current: (current as String).ts(source.key),
|
|
||||||
values: (item.value['options'] as List)
|
|
||||||
.map<String>((e) =>
|
|
||||||
((e['text'] ?? e['value']) as String).ts(source.key))
|
|
||||||
.toList(),
|
|
||||||
onTap: (i) {
|
|
||||||
source.data['settings'][key] =
|
|
||||||
item.value['options'][i]['value'];
|
|
||||||
source.saveData();
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (type == "switch") {
|
|
||||||
var current = source.data['settings'][key] ?? item.value['default'];
|
|
||||||
yield ListTile(
|
|
||||||
title: Text((item.value['title'] as String).ts(source.key)),
|
|
||||||
trailing: Switch(
|
|
||||||
value: current,
|
|
||||||
onChanged: (v) {
|
|
||||||
source.data['settings'][key] = v;
|
|
||||||
source.saveData();
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (type == "input") {
|
|
||||||
var current =
|
|
||||||
source.data['settings'][key] ?? item.value['default'] ?? '';
|
|
||||||
yield ListTile(
|
|
||||||
title: Text((item.value['title'] as String).ts(source.key)),
|
|
||||||
subtitle:
|
|
||||||
Text(current, maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(Icons.edit),
|
|
||||||
onPressed: () {
|
|
||||||
showInputDialog(
|
|
||||||
context: context,
|
|
||||||
title: (item.value['title'] as String).ts(source.key),
|
|
||||||
initialValue: current,
|
|
||||||
inputValidator: item.value['validator'] == null
|
|
||||||
? null
|
|
||||||
: RegExp(item.value['validator']),
|
|
||||||
onConfirm: (value) {
|
|
||||||
source.data['settings'][key] = value;
|
|
||||||
source.saveData();
|
|
||||||
setState(() {});
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (type == "callback") {
|
|
||||||
yield _CallbackSetting(setting: item, sourceKey: source.key);
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void delete(ComicSource source) {
|
void delete(ComicSource source) {
|
||||||
showConfirmDialog(
|
showConfirmDialog(
|
||||||
context: App.rootContext,
|
context: App.rootContext,
|
||||||
@@ -297,10 +148,12 @@ class _BodyState extends State<_Body> {
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.to(() => _EditFilePage(source.filePath, () async {
|
context.to(
|
||||||
await ComicSource.reload();
|
() => _EditFilePage(source.filePath, () async {
|
||||||
setState(() {});
|
await ComicSource.reload();
|
||||||
}));
|
setState(() {});
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> update(ComicSource source) async {
|
static Future<void> update(ComicSource source) async {
|
||||||
@@ -764,3 +617,566 @@ class _CallbackSettingState extends State<_CallbackSetting> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _SliverComicSource extends StatefulWidget {
|
||||||
|
const _SliverComicSource({
|
||||||
|
super.key,
|
||||||
|
required this.source,
|
||||||
|
required this.edit,
|
||||||
|
required this.update,
|
||||||
|
required this.delete,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ComicSource source;
|
||||||
|
|
||||||
|
final void Function(ComicSource source) edit;
|
||||||
|
final void Function(ComicSource source) update;
|
||||||
|
final void Function(ComicSource source) delete;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SliverComicSource> createState() => _SliverComicSourceState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliverComicSourceState extends State<_SliverComicSource> {
|
||||||
|
ComicSource get source => widget.source;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var newVersion = ComicSource.availableUpdates[source.key];
|
||||||
|
bool hasUpdate =
|
||||||
|
newVersion != null && compareSemVer(newVersion, source.version);
|
||||||
|
|
||||||
|
return SliverMainAxisGroup(
|
||||||
|
slivers: [
|
||||||
|
SliverPadding(padding: const EdgeInsets.only(top: 16)),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: ListTile(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
source.name,
|
||||||
|
style: ts.s18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
source.version,
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasUpdate)
|
||||||
|
Tooltip(
|
||||||
|
message: newVersion,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.primaryContainer,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"New Version".tl,
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingLeft(4)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Tooltip(
|
||||||
|
message: "Edit".tl,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => widget.edit(source),
|
||||||
|
icon: const Icon(Icons.edit_note),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: "Update".tl,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => widget.update(source),
|
||||||
|
icon: const Icon(Icons.update),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Tooltip(
|
||||||
|
message: "Delete".tl,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => widget.delete(source),
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: context.colorScheme.outlineVariant,
|
||||||
|
width: 0.6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
children: buildSourceSettings().toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Column(
|
||||||
|
children: _buildAccount().toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<Widget> buildSourceSettings() sync* {
|
||||||
|
if (source.settings == null) {
|
||||||
|
return;
|
||||||
|
} else if (source.data['settings'] == null) {
|
||||||
|
source.data['settings'] = {};
|
||||||
|
}
|
||||||
|
for (var item in source.settings!.entries) {
|
||||||
|
var key = item.key;
|
||||||
|
String type = item.value['type'];
|
||||||
|
try {
|
||||||
|
if (type == "select") {
|
||||||
|
var current = source.data['settings'][key];
|
||||||
|
if (current == null) {
|
||||||
|
var d = item.value['default'];
|
||||||
|
for (var option in item.value['options']) {
|
||||||
|
if (option['value'] == d) {
|
||||||
|
current = option['text'] ?? option['value'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current = item.value['options']
|
||||||
|
.firstWhere((e) => e['value'] == current)['text'] ??
|
||||||
|
current;
|
||||||
|
}
|
||||||
|
yield ListTile(
|
||||||
|
title: Text((item.value['title'] as String).ts(source.key)),
|
||||||
|
trailing: Select(
|
||||||
|
current: (current as String).ts(source.key),
|
||||||
|
values: (item.value['options'] as List)
|
||||||
|
.map<String>((e) =>
|
||||||
|
((e['text'] ?? e['value']) as String).ts(source.key))
|
||||||
|
.toList(),
|
||||||
|
onTap: (i) {
|
||||||
|
source.data['settings'][key] =
|
||||||
|
item.value['options'][i]['value'];
|
||||||
|
source.saveData();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (type == "switch") {
|
||||||
|
var current = source.data['settings'][key] ?? item.value['default'];
|
||||||
|
yield ListTile(
|
||||||
|
title: Text((item.value['title'] as String).ts(source.key)),
|
||||||
|
trailing: Switch(
|
||||||
|
value: current,
|
||||||
|
onChanged: (v) {
|
||||||
|
source.data['settings'][key] = v;
|
||||||
|
source.saveData();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (type == "input") {
|
||||||
|
var current =
|
||||||
|
source.data['settings'][key] ?? item.value['default'] ?? '';
|
||||||
|
yield ListTile(
|
||||||
|
title: Text((item.value['title'] as String).ts(source.key)),
|
||||||
|
subtitle:
|
||||||
|
Text(current, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.edit),
|
||||||
|
onPressed: () {
|
||||||
|
showInputDialog(
|
||||||
|
context: context,
|
||||||
|
title: (item.value['title'] as String).ts(source.key),
|
||||||
|
initialValue: current,
|
||||||
|
inputValidator: item.value['validator'] == null
|
||||||
|
? null
|
||||||
|
: RegExp(item.value['validator']),
|
||||||
|
onConfirm: (value) {
|
||||||
|
source.data['settings'][key] = value;
|
||||||
|
source.saveData();
|
||||||
|
setState(() {});
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (type == "callback") {
|
||||||
|
yield _CallbackSetting(setting: item, sourceKey: source.key);
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final _reLogin = <String, bool>{};
|
||||||
|
|
||||||
|
Iterable<Widget> _buildAccount() sync* {
|
||||||
|
if (source.account == null) return;
|
||||||
|
final bool logged = source.isLogged;
|
||||||
|
if (!logged) {
|
||||||
|
yield ListTile(
|
||||||
|
title: Text("Log in".tl),
|
||||||
|
trailing: const Icon(Icons.arrow_right),
|
||||||
|
onTap: () async {
|
||||||
|
await context.to(
|
||||||
|
() => _LoginPage(
|
||||||
|
config: source.account!,
|
||||||
|
source: source,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
source.saveData();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (logged) {
|
||||||
|
for (var item in source.account!.infoItems) {
|
||||||
|
if (item.builder != null) {
|
||||||
|
yield item.builder!(context);
|
||||||
|
} else {
|
||||||
|
yield ListTile(
|
||||||
|
title: Text(item.title.tl),
|
||||||
|
subtitle: item.data == null ? null : Text(item.data!()),
|
||||||
|
onTap: item.onTap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (source.data["account"] is List) {
|
||||||
|
bool loading = _reLogin[source.key] == true;
|
||||||
|
yield ListTile(
|
||||||
|
title: Text("Re-login".tl),
|
||||||
|
subtitle: Text("Click if login expired".tl),
|
||||||
|
onTap: () async {
|
||||||
|
if (source.data["account"] == null) {
|
||||||
|
context.showMessage(message: "No data".tl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_reLogin[source.key] = true;
|
||||||
|
});
|
||||||
|
final List account = source.data["account"];
|
||||||
|
var res = await source.account!.login!(account[0], account[1]);
|
||||||
|
if (res.error) {
|
||||||
|
context.showMessage(message: res.errorMessage!);
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: "Success".tl);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_reLogin[source.key] = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
trailing: loading
|
||||||
|
? const SizedBox.square(
|
||||||
|
dimension: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.refresh),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
yield ListTile(
|
||||||
|
title: Text("Log out".tl),
|
||||||
|
onTap: () {
|
||||||
|
source.data["account"] = null;
|
||||||
|
source.account?.logout();
|
||||||
|
source.saveData();
|
||||||
|
ComicSource.notifyListeners();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
trailing: const Icon(Icons.logout),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginPage extends StatefulWidget {
|
||||||
|
const _LoginPage({required this.config, required this.source});
|
||||||
|
|
||||||
|
final AccountConfig config;
|
||||||
|
|
||||||
|
final ComicSource source;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_LoginPage> createState() => _LoginPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginPageState extends State<_LoginPage> {
|
||||||
|
String username = "";
|
||||||
|
String password = "";
|
||||||
|
bool loading = false;
|
||||||
|
|
||||||
|
final Map<String, String> _cookies = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: const Appbar(
|
||||||
|
title: Text(''),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: AutofillGroup(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text("Login".tl, style: const TextStyle(fontSize: 24)),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
if (widget.config.cookieFields == null)
|
||||||
|
TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Username".tl,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
enabled: widget.config.login != null,
|
||||||
|
onChanged: (s) {
|
||||||
|
username = s;
|
||||||
|
},
|
||||||
|
autofillHints: const [AutofillHints.username],
|
||||||
|
).paddingBottom(16),
|
||||||
|
if (widget.config.cookieFields == null)
|
||||||
|
TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Password".tl,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
obscureText: true,
|
||||||
|
enabled: widget.config.login != null,
|
||||||
|
onChanged: (s) {
|
||||||
|
password = s;
|
||||||
|
},
|
||||||
|
onSubmitted: (s) => login(),
|
||||||
|
autofillHints: const [AutofillHints.password],
|
||||||
|
).paddingBottom(16),
|
||||||
|
for (var field in widget.config.cookieFields ?? <String>[])
|
||||||
|
TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: field,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
obscureText: true,
|
||||||
|
enabled: widget.config.validateCookies != null,
|
||||||
|
onChanged: (s) {
|
||||||
|
_cookies[field] = s;
|
||||||
|
},
|
||||||
|
).paddingBottom(16),
|
||||||
|
if (widget.config.login == null &&
|
||||||
|
widget.config.cookieFields == null)
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error_outline),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text("Login with password is disabled".tl),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Button.filled(
|
||||||
|
isLoading: loading,
|
||||||
|
onPressed: login,
|
||||||
|
child: Text("Continue".tl),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (widget.config.loginWebsite != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (App.isWindows || App.isLinux) {
|
||||||
|
loginWithWebview2();
|
||||||
|
} else {
|
||||||
|
loginWithWebview();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text("Login with webview".tl),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (widget.config.registerWebsite != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () =>
|
||||||
|
launchUrlString(widget.config.registerWebsite!),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.link),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text("Create Account".tl),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void login() {
|
||||||
|
if (widget.config.login != null) {
|
||||||
|
if (username.isEmpty || password.isEmpty) {
|
||||||
|
showToast(
|
||||||
|
message: "Cannot be empty".tl,
|
||||||
|
icon: const Icon(Icons.error_outline),
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
loading = true;
|
||||||
|
});
|
||||||
|
widget.config.login!(username, password).then((value) {
|
||||||
|
if (value.error) {
|
||||||
|
context.showMessage(message: value.errorMessage!);
|
||||||
|
setState(() {
|
||||||
|
loading = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (mounted) {
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (widget.config.validateCookies != null) {
|
||||||
|
setState(() {
|
||||||
|
loading = true;
|
||||||
|
});
|
||||||
|
var cookies =
|
||||||
|
widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList();
|
||||||
|
widget.config.validateCookies!(cookies).then((value) {
|
||||||
|
if (value) {
|
||||||
|
widget.source.data['account'] = 'ok';
|
||||||
|
widget.source.saveData();
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
|
context.showMessage(message: "Invalid cookies".tl);
|
||||||
|
setState(() {
|
||||||
|
loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loginWithWebview() async {
|
||||||
|
var url = widget.config.loginWebsite!;
|
||||||
|
var title = '';
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
void validate(InAppWebViewController c) async {
|
||||||
|
if (widget.config.checkLoginStatus != null &&
|
||||||
|
widget.config.checkLoginStatus!(url, title)) {
|
||||||
|
var cookies = (await c.getCookies(url)) ?? [];
|
||||||
|
SingleInstanceCookieJar.instance?.saveFromResponse(
|
||||||
|
Uri.parse(url),
|
||||||
|
cookies,
|
||||||
|
);
|
||||||
|
success = true;
|
||||||
|
widget.config.onLoginWithWebviewSuccess?.call();
|
||||||
|
App.mainNavigatorKey?.currentContext?.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.to(
|
||||||
|
() => AppWebview(
|
||||||
|
initialUrl: widget.config.loginWebsite!,
|
||||||
|
onNavigation: (u, c) {
|
||||||
|
url = u;
|
||||||
|
validate(c);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onTitleChange: (t, c) {
|
||||||
|
title = t;
|
||||||
|
validate(c);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (success) {
|
||||||
|
widget.source.data['account'] = 'ok';
|
||||||
|
widget.source.saveData();
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for windows and linux
|
||||||
|
void loginWithWebview2() async {
|
||||||
|
if (!await DesktopWebview.isAvailable()) {
|
||||||
|
context.showMessage(message: "Webview is not available".tl);
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = widget.config.loginWebsite!;
|
||||||
|
var title = '';
|
||||||
|
bool success = false;
|
||||||
|
|
||||||
|
void onClose() {
|
||||||
|
if (success) {
|
||||||
|
widget.source.data['account'] = 'ok';
|
||||||
|
widget.source.saveData();
|
||||||
|
context.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void validate(DesktopWebview webview) async {
|
||||||
|
if (widget.config.checkLoginStatus != null &&
|
||||||
|
widget.config.checkLoginStatus!(url, title)) {
|
||||||
|
var cookiesMap = await webview.getCookies(url);
|
||||||
|
var cookies = <io.Cookie>[];
|
||||||
|
cookiesMap.forEach((key, value) {
|
||||||
|
cookies.add(io.Cookie(key, value));
|
||||||
|
});
|
||||||
|
SingleInstanceCookieJar.instance?.saveFromResponse(
|
||||||
|
Uri.parse(url),
|
||||||
|
cookies,
|
||||||
|
);
|
||||||
|
success = true;
|
||||||
|
widget.config.onLoginWithWebviewSuccess?.call();
|
||||||
|
webview.close();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var webview = DesktopWebview(
|
||||||
|
initialUrl: widget.config.loginWebsite!,
|
||||||
|
onTitleChange: (t, webview) {
|
||||||
|
title = t;
|
||||||
|
validate(webview);
|
||||||
|
},
|
||||||
|
onNavigation: (u, webview) {
|
||||||
|
url = u;
|
||||||
|
validate(webview);
|
||||||
|
},
|
||||||
|
onClose: onClose,
|
||||||
|
);
|
||||||
|
|
||||||
|
webview.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -42,7 +42,7 @@ class _CommentsPageState extends State<CommentsPage> {
|
|||||||
_error = res.errorMessage;
|
_error = res.errorMessage;
|
||||||
_loading = false;
|
_loading = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_comments = res.data;
|
_comments = res.data;
|
||||||
_loading = false;
|
_loading = false;
|
||||||
|
@@ -46,6 +46,7 @@ class _DownloadingPageState extends State<DownloadingPage> {
|
|||||||
i--;
|
i--;
|
||||||
|
|
||||||
return _DownloadTaskTile(
|
return _DownloadTaskTile(
|
||||||
|
key: ValueKey(LocalManager().downloadingTasks[i]),
|
||||||
task: LocalManager().downloadingTasks[i],
|
task: LocalManager().downloadingTasks[i],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -120,7 +121,7 @@ class _DownloadingPageState extends State<DownloadingPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DownloadTaskTile extends StatefulWidget {
|
class _DownloadTaskTile extends StatefulWidget {
|
||||||
const _DownloadTaskTile({required this.task});
|
const _DownloadTaskTile({required this.task, super.key});
|
||||||
|
|
||||||
final DownloadTask task;
|
final DownloadTask task;
|
||||||
|
|
||||||
@@ -129,20 +130,33 @@ class _DownloadTaskTile extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DownloadTaskTileState extends State<_DownloadTaskTile> {
|
class _DownloadTaskTileState extends State<_DownloadTaskTile> {
|
||||||
|
late DownloadTask task;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
widget.task.addListener(update);
|
task = widget.task;
|
||||||
|
task.addListener(update);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.task.removeListener(update);
|
task.removeListener(update);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant _DownloadTaskTile oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.task != widget.task) {
|
||||||
|
task.removeListener(update);
|
||||||
|
task = widget.task;
|
||||||
|
task.addListener(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
context.findAncestorStateOfType<_DownloadingPageState>()?.update();
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@@ -16,6 +16,7 @@ import 'package:venera/foundation/res.dart';
|
|||||||
import 'package:venera/network/download.dart';
|
import 'package:venera/network/download.dart';
|
||||||
import 'package:venera/pages/comic_page.dart';
|
import 'package:venera/pages/comic_page.dart';
|
||||||
import 'package:venera/pages/reader/reader.dart';
|
import 'package:venera/pages/reader/reader.dart';
|
||||||
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
|
@@ -20,22 +20,35 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
|
|
||||||
var networkFolders = <String>[];
|
var networkFolders = <String>[];
|
||||||
|
|
||||||
|
void findNetworkFolders() {
|
||||||
|
networkFolders.clear();
|
||||||
|
var all = ComicSource.all()
|
||||||
|
.where((e) => e.favoriteData != null)
|
||||||
|
.map((e) => e.favoriteData!.key)
|
||||||
|
.toList();
|
||||||
|
var settings = appdata.settings['favorites'] as List;
|
||||||
|
for (var p in settings) {
|
||||||
|
if (all.contains(p) && !networkFolders.contains(p)) {
|
||||||
|
networkFolders.add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
favPage = widget.favPage ??
|
favPage = widget.favPage ??
|
||||||
context.findAncestorStateOfType<_FavoritesPageState>()!;
|
context.findAncestorStateOfType<_FavoritesPageState>()!;
|
||||||
favPage.folderList = this;
|
favPage.folderList = this;
|
||||||
folders = LocalFavoritesManager().folderNames;
|
folders = LocalFavoritesManager().folderNames;
|
||||||
networkFolders = ComicSource.all()
|
findNetworkFolders();
|
||||||
.where((e) => e.favoriteData != null && e.isLogged)
|
appdata.settings.addListener(updateFolders);
|
||||||
.map((e) => e.favoriteData!.key)
|
|
||||||
.toList();
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
appdata.settings.removeListener(updateFolders);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -102,7 +115,8 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
onClick: () {
|
onClick: () {
|
||||||
newFolder().then((value) {
|
newFolder().then((value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
folders = LocalFavoritesManager().folderNames;
|
folders =
|
||||||
|
LocalFavoritesManager().folderNames;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -113,7 +127,8 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
onClick: () {
|
onClick: () {
|
||||||
sortFolders().then((value) {
|
sortFolders().then((value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
folders = LocalFavoritesManager().folderNames;
|
folders =
|
||||||
|
LocalFavoritesManager().folderNames;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -143,15 +158,24 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 16),
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.cloud,
|
Icons.cloud,
|
||||||
color: context.colorScheme.secondary,
|
color: context.colorScheme.secondary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text("Network".tl),
|
Text("Network".tl),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
onPressed: () {
|
||||||
|
showPopUpWidget(
|
||||||
|
App.rootContext,
|
||||||
|
setFavoritesPagesWidget(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
).paddingHorizontal(16),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
index--;
|
index--;
|
||||||
@@ -241,10 +265,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
folders = LocalFavoritesManager().folderNames;
|
folders = LocalFavoritesManager().folderNames;
|
||||||
networkFolders = ComicSource.all()
|
findNetworkFolders();
|
||||||
.where((e) => e.favoriteData != null)
|
|
||||||
.map((e) => e.favoriteData!.key)
|
|
||||||
.toList();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,6 @@ import 'package:venera/foundation/favorites.dart';
|
|||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
import 'package:venera/foundation/log.dart';
|
import 'package:venera/foundation/log.dart';
|
||||||
import 'package:venera/pages/accounts_page.dart';
|
|
||||||
import 'package:venera/pages/comic_page.dart';
|
import 'package:venera/pages/comic_page.dart';
|
||||||
import 'package:venera/pages/comic_source_page.dart';
|
import 'package:venera/pages/comic_source_page.dart';
|
||||||
import 'package:venera/pages/downloading_page.dart';
|
import 'package:venera/pages/downloading_page.dart';
|
||||||
@@ -36,7 +35,6 @@ class HomePage extends StatelessWidget {
|
|||||||
const _History(),
|
const _History(),
|
||||||
const _Local(),
|
const _Local(),
|
||||||
const _ComicSourceWidget(),
|
const _ComicSourceWidget(),
|
||||||
const _AccountsWidget(),
|
|
||||||
const ImageFavorites(),
|
const ImageFavorites(),
|
||||||
SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)),
|
SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)),
|
||||||
],
|
],
|
||||||
@@ -698,115 +696,6 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AccountsWidget extends StatefulWidget {
|
|
||||||
const _AccountsWidget();
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_AccountsWidget> createState() => _AccountsWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AccountsWidgetState extends State<_AccountsWidget> {
|
|
||||||
late List<String> accounts;
|
|
||||||
|
|
||||||
void onComicSourceChange() {
|
|
||||||
setState(() {
|
|
||||||
accounts.clear();
|
|
||||||
for (var c in ComicSource.all()) {
|
|
||||||
if (c.isLogged) {
|
|
||||||
accounts.add(c.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
accounts = [];
|
|
||||||
for (var c in ComicSource.all()) {
|
|
||||||
if (c.isLogged) {
|
|
||||||
accounts.add(c.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ComicSource.addListener(onComicSourceChange);
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
ComicSource.removeListener(onComicSourceChange);
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SliverToBoxAdapter(
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).colorScheme.outlineVariant,
|
|
||||||
width: 0.6,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
onTap: () {
|
|
||||||
context.to(() => const AccountsPage());
|
|
||||||
},
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 56,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Center(
|
|
||||||
child: Text('Accounts'.tl, style: ts.s18),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8, vertical: 2),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(accounts.length.toString(), style: ts.s12),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
const Icon(Icons.arrow_right),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
).paddingHorizontal(16),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 8,
|
|
||||||
spacing: 8,
|
|
||||||
children: accounts.map((e) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(e),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
).paddingHorizontal(16).paddingBottom(16),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedDownloadingIcon extends StatefulWidget {
|
class _AnimatedDownloadingIcon extends StatefulWidget {
|
||||||
const _AnimatedDownloadingIcon();
|
const _AnimatedDownloadingIcon();
|
||||||
|
|
||||||
|
@@ -12,6 +12,7 @@ import 'package:venera/utils/epub.dart';
|
|||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/pdf.dart';
|
import 'package:venera/utils/pdf.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
import 'package:zip_flutter/zip_flutter.dart';
|
||||||
|
|
||||||
class LocalComicsPage extends StatefulWidget {
|
class LocalComicsPage extends StatefulWidget {
|
||||||
const LocalComicsPage({super.key});
|
const LocalComicsPage({super.key});
|
||||||
@@ -147,13 +148,13 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
text: "View Detail".tl,
|
text: "View Detail".tl,
|
||||||
onClick: () {
|
onClick: () {
|
||||||
context.to(() => ComicPage(
|
context.to(() => ComicPage(
|
||||||
id: selectedComics.keys.first.id,
|
id: selectedComics.keys.first.id,
|
||||||
sourceKey: selectedComics.keys.first.sourceKey,
|
sourceKey: selectedComics.keys.first.sourceKey,
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (selectedComics.length == 1)
|
if (selectedComics.isNotEmpty)
|
||||||
...exportActions(selectedComics.keys.first),
|
...exportActions(selectedComics.keys.toList()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,7 +323,7 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
...exportActions(c as LocalComic),
|
...exportActions([c as LocalComic]),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -390,79 +391,102 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
|
|||||||
return isDeleted;
|
return isDeleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MenuEntry> exportActions(LocalComic c) {
|
List<MenuEntry> exportActions(List<LocalComic> comics) {
|
||||||
return [
|
return [
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon: Icons.outbox_outlined,
|
icon: Icons.outbox_outlined,
|
||||||
text: "Export as cbz".tl,
|
text: "Export as cbz".tl,
|
||||||
onClick: () async {
|
onClick: () {
|
||||||
var controller = showLoadingDialog(
|
exportComics(comics, CBZ.export, ".cbz");
|
||||||
context,
|
},
|
||||||
allowCancel: false,
|
),
|
||||||
);
|
|
||||||
try {
|
|
||||||
var file = await CBZ.export(c);
|
|
||||||
await saveFile(filename: file.name, file: file);
|
|
||||||
await file.delete();
|
|
||||||
} catch (e, s) {
|
|
||||||
context.showMessage(message: e.toString());
|
|
||||||
Log.error("CBZ Export", e, s);
|
|
||||||
}
|
|
||||||
controller.close();
|
|
||||||
}),
|
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon: Icons.picture_as_pdf_outlined,
|
icon: Icons.picture_as_pdf_outlined,
|
||||||
text: "Export as pdf".tl,
|
text: "Export as pdf".tl,
|
||||||
onClick: () async {
|
onClick: () async {
|
||||||
var cache = FilePath.join(App.cachePath, 'temp.pdf');
|
exportComics(comics, createPdfFromComicIsolate, ".pdf");
|
||||||
var controller = showLoadingDialog(
|
|
||||||
context,
|
|
||||||
allowCancel: false,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await createPdfFromComicIsolate(
|
|
||||||
comic: c,
|
|
||||||
savePath: cache,
|
|
||||||
);
|
|
||||||
await saveFile(
|
|
||||||
file: File(cache),
|
|
||||||
filename: "${c.title}.pdf",
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
Log.error("PDF Export", e, s);
|
|
||||||
context.showMessage(message: e.toString());
|
|
||||||
} finally {
|
|
||||||
controller.close();
|
|
||||||
File(cache).deleteIgnoreError();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon: Icons.import_contacts_outlined,
|
icon: Icons.import_contacts_outlined,
|
||||||
text: "Export as epub".tl,
|
text: "Export as epub".tl,
|
||||||
onClick: () async {
|
onClick: () async {
|
||||||
var controller = showLoadingDialog(
|
exportComics(comics, createEpubWithLocalComic, ".epub");
|
||||||
context,
|
|
||||||
allowCancel: false,
|
|
||||||
);
|
|
||||||
File? file;
|
|
||||||
try {
|
|
||||||
file = await createEpubWithLocalComic(
|
|
||||||
c,
|
|
||||||
);
|
|
||||||
await saveFile(
|
|
||||||
file: file,
|
|
||||||
filename: "${c.title}.epub",
|
|
||||||
);
|
|
||||||
} catch (e, s) {
|
|
||||||
Log.error("EPUB Export", e, s);
|
|
||||||
context.showMessage(message: e.toString());
|
|
||||||
} finally {
|
|
||||||
controller.close();
|
|
||||||
file?.deleteIgnoreError();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Export given comics to a file
|
||||||
|
void exportComics(
|
||||||
|
List<LocalComic> comics, ExportComicFunc export, String ext) async {
|
||||||
|
var current = 0;
|
||||||
|
var cacheDir = FilePath.join(App.cachePath, 'comics_export');
|
||||||
|
var outFile = FilePath.join(App.cachePath, 'comics_export.zip');
|
||||||
|
bool canceled = false;
|
||||||
|
if (Directory(cacheDir).existsSync()) {
|
||||||
|
Directory(cacheDir).deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
Directory(cacheDir).createSync();
|
||||||
|
var loadingController = showLoadingDialog(
|
||||||
|
context,
|
||||||
|
allowCancel: true,
|
||||||
|
message: "${"Exporting".tl} $current/${comics.length}",
|
||||||
|
withProgress: comics.length > 1,
|
||||||
|
onCancel: () {
|
||||||
|
canceled = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
var fileName = "";
|
||||||
|
// For each comic, export it to a file
|
||||||
|
for (var comic in comics) {
|
||||||
|
fileName = FilePath.join(cacheDir, sanitizeFileName(comic.title) + ext);
|
||||||
|
await export(comic, fileName);
|
||||||
|
current++;
|
||||||
|
if (comics.length > 1) {
|
||||||
|
loadingController
|
||||||
|
.setMessage("${"Exporting".tl} $current/${comics.length}");
|
||||||
|
loadingController.setProgress(current / comics.length);
|
||||||
|
}
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For single comic, just save the file
|
||||||
|
if (comics.length == 1) {
|
||||||
|
await saveFile(
|
||||||
|
file: File(fileName),
|
||||||
|
filename: File(fileName).name,
|
||||||
|
);
|
||||||
|
Directory(cacheDir).deleteSync(recursive: true);
|
||||||
|
loadingController.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// For multiple comics, compress the folder
|
||||||
|
loadingController.setProgress(null);
|
||||||
|
loadingController.setMessage("Compressing".tl);
|
||||||
|
await ZipFile.compressFolderAsync(cacheDir, outFile);
|
||||||
|
if (canceled) {
|
||||||
|
File(outFile).deleteIgnoreError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Log.error("Export Comics", e, s);
|
||||||
|
context.showMessage(message: e.toString());
|
||||||
|
loadingController.close();
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
Directory(cacheDir).deleteIgnoreError(recursive: true);
|
||||||
|
}
|
||||||
|
await saveFile(
|
||||||
|
file: File(outFile),
|
||||||
|
filename: "comics_export.zip",
|
||||||
|
);
|
||||||
|
loadingController.close();
|
||||||
|
File(outFile).deleteIgnoreError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef ExportComicFunc = Future<File> Function(
|
||||||
|
LocalComic comic, String outFilePath);
|
||||||
|
@@ -111,9 +111,7 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
late _ReaderState reader;
|
late _ReaderState reader;
|
||||||
|
|
||||||
int get totalPages => ((reader.images!.length + reader.imagesPerPage - 1) /
|
int get totalPages => (reader.images!.length / reader.imagesPerPage).ceil();
|
||||||
reader.imagesPerPage)
|
|
||||||
.ceil();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -228,6 +226,8 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
? Axis.vertical
|
? Axis.vertical
|
||||||
: Axis.horizontal;
|
: Axis.horizontal;
|
||||||
|
|
||||||
|
bool reverse = reader.mode == ReaderMode.galleryRightToLeft;
|
||||||
|
|
||||||
List<Widget> imageWidgets = images.map((imageKey) {
|
List<Widget> imageWidgets = images.map((imageKey) {
|
||||||
ImageProvider imageProvider =
|
ImageProvider imageProvider =
|
||||||
_createImageProviderFromKey(imageKey, context);
|
_createImageProviderFromKey(imageKey, context);
|
||||||
@@ -239,6 +239,10 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
if (reverse) {
|
||||||
|
imageWidgets = imageWidgets.reversed.toList();
|
||||||
|
}
|
||||||
|
|
||||||
return axis == Axis.vertical
|
return axis == Axis.vertical
|
||||||
? Column(children: imageWidgets)
|
? Column(children: imageWidgets)
|
||||||
: Row(children: imageWidgets);
|
: Row(children: imageWidgets);
|
||||||
|
@@ -98,8 +98,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get maxPage =>
|
int get maxPage => ((images?.length ?? 1) / imagesPerPage).ceil();
|
||||||
((images?.length ?? 1) + imagesPerPage - 1) ~/ imagesPerPage;
|
|
||||||
|
|
||||||
ComicType get type => widget.type;
|
ComicType get type => widget.type;
|
||||||
|
|
||||||
|
@@ -10,12 +10,14 @@ import 'package:venera/foundation/comic_source/comic_source.dart';
|
|||||||
import 'package:venera/foundation/state_controller.dart';
|
import 'package:venera/foundation/state_controller.dart';
|
||||||
import 'package:venera/pages/aggregated_search_page.dart';
|
import 'package:venera/pages/aggregated_search_page.dart';
|
||||||
import 'package:venera/pages/search_result_page.dart';
|
import 'package:venera/pages/search_result_page.dart';
|
||||||
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
import 'package:venera/utils/app_links.dart';
|
import 'package:venera/utils/app_links.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:venera/utils/tags_translation.dart';
|
import 'package:venera/utils/tags_translation.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
import 'comic_page.dart';
|
import 'comic_page.dart';
|
||||||
|
import 'comic_source_page.dart';
|
||||||
|
|
||||||
class SearchPage extends StatefulWidget {
|
class SearchPage extends StatefulWidget {
|
||||||
const SearchPage({super.key});
|
const SearchPage({super.key});
|
||||||
@@ -27,8 +29,13 @@ class SearchPage extends StatefulWidget {
|
|||||||
class _SearchPageState extends State<SearchPage> {
|
class _SearchPageState extends State<SearchPage> {
|
||||||
late final SearchBarController controller;
|
late final SearchBarController controller;
|
||||||
|
|
||||||
|
late List<String> searchSources;
|
||||||
|
|
||||||
String searchTarget = "";
|
String searchTarget = "";
|
||||||
|
|
||||||
|
SearchPageData get currentSearchPageData =>
|
||||||
|
ComicSource.find(searchTarget)!.searchPageData!;
|
||||||
|
|
||||||
bool aggregatedSearch = false;
|
bool aggregatedSearch = false;
|
||||||
|
|
||||||
var focusNode = FocusNode();
|
var focusNode = FocusNode();
|
||||||
@@ -139,29 +146,85 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
findSearchSources();
|
||||||
var defaultSearchTarget = appdata.settings['defaultSearchTarget'];
|
var defaultSearchTarget = appdata.settings['defaultSearchTarget'];
|
||||||
if (defaultSearchTarget == "_aggregated_") {
|
if (defaultSearchTarget == "_aggregated_") {
|
||||||
aggregatedSearch = true;
|
aggregatedSearch = true;
|
||||||
} else if (defaultSearchTarget != null &&
|
} else if (defaultSearchTarget != null &&
|
||||||
ComicSource.find(defaultSearchTarget) != null) {
|
searchSources.contains(defaultSearchTarget)) {
|
||||||
searchTarget = defaultSearchTarget;
|
searchTarget = defaultSearchTarget;
|
||||||
} else {
|
|
||||||
searchTarget = ComicSource.all().first.key;
|
|
||||||
}
|
}
|
||||||
controller = SearchBarController(
|
controller = SearchBarController(
|
||||||
onSearch: search,
|
onSearch: search,
|
||||||
);
|
);
|
||||||
|
appdata.settings.addListener(updateSearchSourcesIfNeeded);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
focusNode.dispose();
|
focusNode.dispose();
|
||||||
|
appdata.settings.removeListener(updateSearchSourcesIfNeeded);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void findSearchSources() {
|
||||||
|
var all = ComicSource.all()
|
||||||
|
.where((e) => e.searchPageData != null)
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
var settings = appdata.settings['searchSources'] as List;
|
||||||
|
var sources = <String>[];
|
||||||
|
for (var source in settings) {
|
||||||
|
if (all.contains(source)) {
|
||||||
|
sources.add(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searchSources = sources;
|
||||||
|
if (!searchSources.contains(searchTarget)) {
|
||||||
|
searchTarget = searchSources.firstOrNull ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSearchSourcesIfNeeded() {
|
||||||
|
var old = searchSources;
|
||||||
|
findSearchSources();
|
||||||
|
if (old.isEqualsTo(searchSources)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void manageSearchSources() {
|
||||||
|
showPopUpWidget(App.rootContext, setSearchSourcesWidget());
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildEmpty() {
|
||||||
|
var msg = "No Search Sources".tl;
|
||||||
|
msg += '\n';
|
||||||
|
VoidCallback onTap;
|
||||||
|
if (ComicSource.isEmpty) {
|
||||||
|
msg += "Please add some sources".tl;
|
||||||
|
onTap = () {
|
||||||
|
context.to(() => ComicSourcePage());
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
msg += "Please check your settings".tl;
|
||||||
|
onTap = manageSearchSources;
|
||||||
|
}
|
||||||
|
return NetworkError(
|
||||||
|
message: msg,
|
||||||
|
retry: onTap,
|
||||||
|
withAppbar: true,
|
||||||
|
buttonText: "Manage".tl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (searchSources.isEmpty) {
|
||||||
|
return buildEmpty();
|
||||||
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SmoothCustomScrollView(
|
body: SmoothCustomScrollView(
|
||||||
slivers: buildSlivers().toList(),
|
slivers: buildSlivers().toList(),
|
||||||
@@ -190,8 +253,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildSearchTarget() {
|
Widget buildSearchTarget() {
|
||||||
var sources =
|
var sources = searchSources.map((e) => ComicSource.find(e)!).toList();
|
||||||
ComicSource.all().where((e) => e.searchPageData != null).toList();
|
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -203,6 +265,10 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
leading: const Icon(Icons.search),
|
leading: const Icon(Icons.search),
|
||||||
title: Text("Search in".tl),
|
title: Text("Search in".tl),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
onPressed: manageSearchSources,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
@@ -229,11 +295,6 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
aggregatedSearch = value ?? false;
|
aggregatedSearch = value ?? false;
|
||||||
if (!aggregatedSearch &&
|
|
||||||
appdata.settings['defaultSearchTarget'] ==
|
|
||||||
"_aggregated_") {
|
|
||||||
searchTarget = sources.first.key;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -245,9 +306,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void useDefaultOptions() {
|
void useDefaultOptions() {
|
||||||
final searchOptions =
|
final searchOptions = currentSearchPageData.searchOptions ?? [];
|
||||||
ComicSource.find(searchTarget)!.searchPageData!.searchOptions ??
|
|
||||||
<SearchOptions>[];
|
|
||||||
options = searchOptions.map((e) => e.defaultValue).toList();
|
options = searchOptions.map((e) => e.defaultValue).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,9 +317,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
|
|
||||||
var children = <Widget>[];
|
var children = <Widget>[];
|
||||||
|
|
||||||
final searchOptions =
|
final searchOptions = currentSearchPageData.searchOptions ?? [];
|
||||||
ComicSource.find(searchTarget)!.searchPageData!.searchOptions ??
|
|
||||||
<SearchOptions>[];
|
|
||||||
if (searchOptions.length != options.length) {
|
if (searchOptions.length != options.length) {
|
||||||
useDefaultOptions();
|
useDefaultOptions();
|
||||||
}
|
}
|
||||||
@@ -394,7 +451,9 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
Text(
|
Text(
|
||||||
subTitle,
|
subTitle,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14, color: Theme.of(context).colorScheme.outline),
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@@ -116,13 +116,13 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
sourceKey = widget.sourceKey;
|
sourceKey = widget.sourceKey;
|
||||||
|
text = checkAutoLanguage(widget.text);
|
||||||
controller = SearchBarController(
|
controller = SearchBarController(
|
||||||
currentText: checkAutoLanguage(widget.text),
|
currentText: text,
|
||||||
onSearch: search,
|
onSearch: search,
|
||||||
);
|
);
|
||||||
options = widget.options ?? const [];
|
options = widget.options ?? const [];
|
||||||
validateOptions();
|
validateOptions();
|
||||||
text = widget.text;
|
|
||||||
appdata.addSearchHistory(text);
|
appdata.addSearchHistory(text);
|
||||||
suggestionsController = _SuggestionsController(controller);
|
suggestionsController = _SuggestionsController(controller);
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@@ -86,7 +86,7 @@ class _AboutSettingsState extends State<AboutSettings> {
|
|||||||
|
|
||||||
Future<bool> checkUpdate() async {
|
Future<bool> checkUpdate() async {
|
||||||
var res = await AppDio().get(
|
var res = await AppDio().get(
|
||||||
"https://raw.githubusercontent.com/venera-app/venera/refs/heads/master/pubspec.yaml");
|
"https://cdn.jsdelivr.net/gh/venera-app/venera@latest/pubspec.yaml");
|
||||||
if (res.statusCode == 200) {
|
if (res.statusCode == 200) {
|
||||||
var data = loadYaml(res.data);
|
var data = loadYaml(res.data);
|
||||||
if (data["version"] != null) {
|
if (data["version"] != null) {
|
||||||
|
@@ -38,19 +38,11 @@ class _ExploreSettingsState extends State<ExploreSettings> {
|
|||||||
).toSliver(),
|
).toSliver(),
|
||||||
_PopupWindowSetting(
|
_PopupWindowSetting(
|
||||||
title: "Network Favorite Pages".tl,
|
title: "Network Favorite Pages".tl,
|
||||||
builder: () {
|
builder: setFavoritesPagesWidget,
|
||||||
var pages = <String, String>{};
|
).toSliver(),
|
||||||
for (var c in ComicSource.all()) {
|
_PopupWindowSetting(
|
||||||
if (c.favoriteData != null) {
|
title: "Search Sources".tl,
|
||||||
pages[c.favoriteData!.key] = c.favoriteData!.title;
|
builder: setSearchSourcesWidget,
|
||||||
}
|
|
||||||
}
|
|
||||||
return _MultiPagesFilter(
|
|
||||||
title: "Network Favorite Pages".tl,
|
|
||||||
settingsIndex: "favorites",
|
|
||||||
pages: pages,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
).toSliver(),
|
).toSliver(),
|
||||||
_SwitchSetting(
|
_SwitchSetting(
|
||||||
title: "Show favorite status on comic tile".tl,
|
title: "Show favorite status on comic tile".tl,
|
||||||
@@ -209,3 +201,31 @@ Widget setCategoryPagesWidget() {
|
|||||||
pages: pages,
|
pages: pages,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget setFavoritesPagesWidget() {
|
||||||
|
var pages = <String, String>{};
|
||||||
|
for (var c in ComicSource.all()) {
|
||||||
|
if (c.favoriteData != null) {
|
||||||
|
pages[c.favoriteData!.key] = c.favoriteData!.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _MultiPagesFilter(
|
||||||
|
title: "Network Favorite Pages".tl,
|
||||||
|
settingsIndex: "favorites",
|
||||||
|
pages: pages,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget setSearchSourcesWidget() {
|
||||||
|
var pages = <String, String>{};
|
||||||
|
for (var c in ComicSource.all()) {
|
||||||
|
if (c.searchPageData != null) {
|
||||||
|
pages[c.key] = c.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _MultiPagesFilter(
|
||||||
|
title: "Search Sources".tl,
|
||||||
|
settingsIndex: "searchSources",
|
||||||
|
pages: pages,
|
||||||
|
);
|
||||||
|
}
|
@@ -206,37 +206,41 @@ class _SettingsPageState extends State<SettingsPage> implements PopEntry {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Stack(
|
return LayoutBuilder(
|
||||||
children: [
|
builder: (context, constrains) {
|
||||||
Positioned.fill(child: buildLeft()),
|
return Stack(
|
||||||
Positioned(
|
children: [
|
||||||
left: offset,
|
Positioned.fill(child: buildLeft()),
|
||||||
width: MediaQuery.of(context).size.width,
|
Positioned(
|
||||||
top: 0,
|
left: offset,
|
||||||
bottom: 0,
|
width: constrains.maxWidth,
|
||||||
child: Listener(
|
top: 0,
|
||||||
onPointerDown: handlePointerDown,
|
bottom: 0,
|
||||||
child: AnimatedSwitcher(
|
child: Listener(
|
||||||
duration: const Duration(milliseconds: 200),
|
onPointerDown: handlePointerDown,
|
||||||
switchInCurve: Curves.fastOutSlowIn,
|
child: AnimatedSwitcher(
|
||||||
switchOutCurve: Curves.fastOutSlowIn,
|
duration: const Duration(milliseconds: 200),
|
||||||
transitionBuilder: (child, animation) {
|
switchInCurve: Curves.fastOutSlowIn,
|
||||||
var tween = Tween<Offset>(
|
switchOutCurve: Curves.fastOutSlowIn,
|
||||||
begin: const Offset(1, 0), end: const Offset(0, 0));
|
transitionBuilder: (child, animation) {
|
||||||
|
var tween = Tween<Offset>(
|
||||||
|
begin: const Offset(1, 0), end: const Offset(0, 0));
|
||||||
|
|
||||||
return SlideTransition(
|
return SlideTransition(
|
||||||
position: tween.animate(animation),
|
position: tween.animate(animation),
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Material(
|
child: Material(
|
||||||
key: ValueKey(currentPage),
|
key: ValueKey(currentPage),
|
||||||
child: buildRight(),
|
child: buildRight(),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
),
|
],
|
||||||
)
|
);
|
||||||
],
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -303,7 +303,10 @@ class DesktopWebview {
|
|||||||
proxy: AppDio.proxy,
|
proxy: AppDio.proxy,
|
||||||
));
|
));
|
||||||
_webview!.addOnWebMessageReceivedCallback(onMessage);
|
_webview!.addOnWebMessageReceivedCallback(onMessage);
|
||||||
_webview!.setOnNavigation((s) => onNavigation?.call(s, this));
|
_webview!.setOnNavigation((s) {
|
||||||
|
s = s.substring(1, s.length - 1);
|
||||||
|
return onNavigation?.call(s, this);
|
||||||
|
});
|
||||||
_webview!.launch(initialUrl, triggerOnUrlRequestEvent: false);
|
_webview!.launch(initialUrl, triggerOnUrlRequestEvent: false);
|
||||||
_runTimer();
|
_runTimer();
|
||||||
_webview!.onClose.then((value) {
|
_webview!.onClose.then((value) {
|
||||||
|
@@ -85,6 +85,10 @@ abstract class CBZ {
|
|||||||
if (cache.existsSync()) cache.deleteSync(recursive: true);
|
if (cache.existsSync()) cache.deleteSync(recursive: true);
|
||||||
cache.createSync();
|
cache.createSync();
|
||||||
await extractArchive(file, cache);
|
await extractArchive(file, cache);
|
||||||
|
var f = cache.listSync();
|
||||||
|
if (f.length == 1 && f.first is Directory) {
|
||||||
|
cache = f.first as Directory;
|
||||||
|
}
|
||||||
var metaDataFile = File(FilePath.join(cache.path, 'metadata.json'));
|
var metaDataFile = File(FilePath.join(cache.path, 'metadata.json'));
|
||||||
ComicMetaData? metaData;
|
ComicMetaData? metaData;
|
||||||
if (metaDataFile.existsSync()) {
|
if (metaDataFile.existsSync()) {
|
||||||
@@ -171,7 +175,7 @@ abstract class CBZ {
|
|||||||
return comic;
|
return comic;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<File> export(LocalComic comic) async {
|
static Future<File> export(LocalComic comic, String outFilePath) async {
|
||||||
var cache = Directory(FilePath.join(App.cachePath, 'cbz_export'));
|
var cache = Directory(FilePath.join(App.cachePath, 'cbz_export'));
|
||||||
if (cache.existsSync()) cache.deleteSync(recursive: true);
|
if (cache.existsSync()) cache.deleteSync(recursive: true);
|
||||||
cache.createSync();
|
cache.createSync();
|
||||||
@@ -230,7 +234,7 @@ abstract class CBZ {
|
|||||||
).toJson(),
|
).toJson(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
var cbz = File(FilePath.join(App.cachePath, sanitizeFileName('${comic.title}.cbz')));
|
var cbz = File(outFilePath);
|
||||||
if (cbz.existsSync()) cbz.deleteSync();
|
if (cbz.existsSync()) cbz.deleteSync();
|
||||||
await _compress(cache.path, cbz.path);
|
await _compress(cache.path, cbz.path);
|
||||||
cache.deleteSync(recursive: true);
|
cache.deleteSync(recursive: true);
|
||||||
|
@@ -118,6 +118,7 @@ class DataSync with ChangeNotifier {
|
|||||||
await client.remove(files.first.name!);
|
await client.remove(files.first.name!);
|
||||||
}
|
}
|
||||||
await client.write(filename, await data.readAsBytes());
|
await client.write(filename, await data.readAsBytes());
|
||||||
|
data.deleteIgnoreError();
|
||||||
Log.info("Upload Data", "Data uploaded successfully");
|
Log.info("Upload Data", "Data uploaded successfully");
|
||||||
return const Res(true);
|
return const Res(true);
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
|
@@ -24,7 +24,8 @@ class EpubData {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File> createEpubComic(EpubData data, String cacheDir) async {
|
Future<File> createEpubComic(
|
||||||
|
EpubData data, String cacheDir, String outFilePath) async {
|
||||||
final workingDir = Directory(FilePath.join(cacheDir, 'epub'));
|
final workingDir = Directory(FilePath.join(cacheDir, 'epub'));
|
||||||
if (workingDir.existsSync()) {
|
if (workingDir.existsSync()) {
|
||||||
workingDir.deleteSync(recursive: true);
|
workingDir.deleteSync(recursive: true);
|
||||||
@@ -109,8 +110,7 @@ ${images.map((e) => ' <img src="$e" alt="$e"/>').join('\n')}
|
|||||||
}
|
}
|
||||||
|
|
||||||
// content.opf
|
// content.opf
|
||||||
final contentOpf =
|
final contentOpf = File(FilePath.join(workingDir.path, 'content.opf'));
|
||||||
File(FilePath.join(workingDir.path, 'content.opf'));
|
|
||||||
final uuid = const Uuid().v4();
|
final uuid = const Uuid().v4();
|
||||||
var spineStrBuilder = StringBuffer();
|
var spineStrBuilder = StringBuffer();
|
||||||
for (var i = 0; i < chapterIndex; i++) {
|
for (var i = 0; i < chapterIndex; i++) {
|
||||||
@@ -171,16 +171,15 @@ ${navMapStrBuilder.toString()}
|
|||||||
</ncx>
|
</ncx>
|
||||||
''');
|
''');
|
||||||
|
|
||||||
// zip
|
ZipFile.compressFolder(workingDir.path, outFilePath);
|
||||||
final zipPath = FilePath.join(cacheDir, '${data.title}.epub');
|
|
||||||
ZipFile.compressFolder(workingDir.path, zipPath);
|
|
||||||
|
|
||||||
workingDir.deleteSync(recursive: true);
|
workingDir.deleteSync(recursive: true);
|
||||||
|
|
||||||
return File(zipPath);
|
return File(outFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File> createEpubWithLocalComic(LocalComic comic) async {
|
Future<File> createEpubWithLocalComic(
|
||||||
|
LocalComic comic, String outFilePath) async {
|
||||||
var chapters = <String, List<File>>{};
|
var chapters = <String, List<File>>{};
|
||||||
if (comic.chapters == null) {
|
if (comic.chapters == null) {
|
||||||
chapters[comic.title] =
|
chapters[comic.title] =
|
||||||
@@ -188,11 +187,11 @@ Future<File> createEpubWithLocalComic(LocalComic comic) async {
|
|||||||
.map((e) => File(e))
|
.map((e) => File(e))
|
||||||
.toList();
|
.toList();
|
||||||
} else {
|
} else {
|
||||||
for (var chapter in comic.chapters!.keys) {
|
for (var chapter in comic.downloadedChapters) {
|
||||||
chapters[comic.chapters![chapter]!] = (await LocalManager()
|
chapters[comic.chapters![chapter]!] =
|
||||||
.getImages(comic.id, comic.comicType, chapter))
|
(await LocalManager().getImages(comic.id, comic.comicType, chapter))
|
||||||
.map((e) => File(e))
|
.map((e) => File(e))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var data = EpubData(
|
var data = EpubData(
|
||||||
@@ -205,6 +204,6 @@ Future<File> createEpubWithLocalComic(LocalComic comic) async {
|
|||||||
final cacheDir = App.cachePath;
|
final cacheDir = App.cachePath;
|
||||||
|
|
||||||
return Isolate.run(() => overrideIO(() async {
|
return Isolate.run(() => overrideIO(() async {
|
||||||
return createEpubComic(data, cacheDir);
|
return createEpubComic(data, cacheDir, outFilePath);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@@ -35,19 +35,9 @@ class FilePath {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension FileSystemEntityExt on FileSystemEntity {
|
extension FileSystemEntityExt on FileSystemEntity {
|
||||||
|
/// Get the base name of the file or directory.
|
||||||
String get name {
|
String get name {
|
||||||
var path = this.path;
|
return p.basename(path);
|
||||||
if (path.endsWith('/') || path.endsWith('\\')) {
|
|
||||||
path = path.substring(0, path.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
int i = path.length - 1;
|
|
||||||
|
|
||||||
while (i >= 0 && path[i] != '\\' && path[i] != '/') {
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.substring(i + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteIgnoreError({bool recursive = false}) async {
|
Future<void> deleteIgnoreError({bool recursive = false}) async {
|
||||||
@@ -83,6 +73,10 @@ extension FileExtension on File {
|
|||||||
// Stream is not usable since [AndroidFile] does not support [openRead].
|
// Stream is not usable since [AndroidFile] does not support [openRead].
|
||||||
await newFile.writeAsBytes(await readAsBytes());
|
await newFile.writeAsBytes(await readAsBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get basenameWithoutExt {
|
||||||
|
return p.basenameWithoutExtension(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DirectoryExtension on Directory {
|
extension DirectoryExtension on Directory {
|
||||||
|
@@ -30,14 +30,14 @@ Future<void> _createPdfFromComic({
|
|||||||
files.removeWhere(
|
files.removeWhere(
|
||||||
(element) => element is! File || element.path.startsWith('cover'));
|
(element) => element is! File || element.path.startsWith('cover'));
|
||||||
files.sort((a, b) {
|
files.sort((a, b) {
|
||||||
var aName = (a as File).name;
|
var aName = (a as File).basenameWithoutExt;
|
||||||
var bName = (b as File).name;
|
var bName = (b as File).basenameWithoutExt;
|
||||||
var aNumber = int.tryParse(aName);
|
var aNumber = int.tryParse(aName);
|
||||||
var bNumber = int.tryParse(bName);
|
var bNumber = int.tryParse(bName);
|
||||||
if (aNumber != null && bNumber != null) {
|
if (aNumber != null && bNumber != null) {
|
||||||
return aNumber.compareTo(bNumber);
|
return aNumber.compareTo(bNumber);
|
||||||
}
|
}
|
||||||
return aName.compareTo(bName);
|
return a.name.compareTo(b.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ Future<void> _createPdfFromComic({
|
|||||||
images.add(file.path);
|
images.add(file.path);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (var chapter in comic.chapters!.keys) {
|
for (var chapter in comic.downloadedChapters) {
|
||||||
var files = Directory(FilePath.join(baseDir, chapter)).listSync();
|
var files = Directory(FilePath.join(baseDir, chapter)).listSync();
|
||||||
reorderFiles(files);
|
reorderFiles(files);
|
||||||
for (var file in files) {
|
for (var file in files) {
|
||||||
@@ -112,10 +112,7 @@ Future<Isolate> _runIsolate(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createPdfFromComicIsolate({
|
Future<File> createPdfFromComicIsolate(LocalComic comic, String savePath) async {
|
||||||
required LocalComic comic,
|
|
||||||
required String savePath,
|
|
||||||
}) async {
|
|
||||||
var receivePort = ReceivePort();
|
var receivePort = ReceivePort();
|
||||||
SendPort? sendPort;
|
SendPort? sendPort;
|
||||||
Isolate? isolate;
|
Isolate? isolate;
|
||||||
@@ -134,7 +131,8 @@ Future<void> createPdfFromComicIsolate({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
isolate = await _runIsolate(comic, savePath, receivePort.sendPort);
|
isolate = await _runIsolate(comic, savePath, receivePort.sendPort);
|
||||||
return completer.future;
|
await completer.future;
|
||||||
|
return File(savePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
class PdfGenerator {
|
class PdfGenerator {
|
||||||
|
@@ -15,9 +15,6 @@ extension TagsTranslation on String{
|
|||||||
static final Map<String, Map<String, String>> _data = {};
|
static final Map<String, Map<String, String>> _data = {};
|
||||||
|
|
||||||
static Future<void> readData() async{
|
static Future<void> readData() async{
|
||||||
if(App.locale.languageCode != "zh"){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var fileName = App.locale.countryCode == 'TW'
|
var fileName = App.locale.countryCode == 'TW'
|
||||||
? "assets/tags_tw.json"
|
? "assets/tags_tw.json"
|
||||||
: "assets/tags.json";
|
: "assets/tags.json";
|
||||||
|
12
pubspec.lock
12
pubspec.lock
@@ -1083,21 +1083,21 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "6.5.0"
|
version: "6.5.0"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: yaml
|
name: yaml
|
||||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
|
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.3"
|
||||||
zip_flutter:
|
zip_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: zip_flutter
|
name: zip_flutter
|
||||||
sha256: fe63ef9098bb2426b001adba2e28029820d71ce80cce957a36676bd6b3227245
|
sha256: bbf3160062610a43901b7ebbc6f6dd46519540f03a84027dc7b1fff399dda1ac
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.9"
|
version: "0.0.10"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.0 <4.0.0"
|
dart: ">=3.6.0 <4.0.0"
|
||||||
flutter: ">=3.27.3"
|
flutter: ">=3.27.4"
|
||||||
|
@@ -2,11 +2,11 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.2.2+122
|
version: 1.2.4+124
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.6.0 <4.0.0'
|
sdk: '>=3.6.0 <4.0.0'
|
||||||
flutter: 3.27.3
|
flutter: 3.27.4
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@@ -48,7 +48,7 @@ dependencies:
|
|||||||
sliver_tools: ^0.2.12
|
sliver_tools: ^0.2.12
|
||||||
flutter_file_dialog: ^3.0.2
|
flutter_file_dialog: ^3.0.2
|
||||||
file_selector: ^1.0.3
|
file_selector: ^1.0.3
|
||||||
zip_flutter: ^0.0.9
|
zip_flutter: ^0.0.10
|
||||||
lodepng_flutter:
|
lodepng_flutter:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/venera-app/lodepng_flutter
|
url: https://github.com/venera-app/lodepng_flutter
|
||||||
@@ -75,13 +75,14 @@ dependencies:
|
|||||||
flex_seed_scheme: ^3.5.0
|
flex_seed_scheme: ^3.5.0
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
yaml: ^3.1.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
flutter_to_arch: ^1.0.1
|
flutter_to_arch: ^1.0.1
|
||||||
flutter_to_debian:
|
flutter_to_debian: ^2.0.2
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
@@ -3,11 +3,36 @@
|
|||||||
|
|
||||||
#define MyAppName "Venera"
|
#define MyAppName "Venera"
|
||||||
#define MyAppVersion "{{version}}"
|
#define MyAppVersion "{{version}}"
|
||||||
#define MyAppPublisher "wgh136"
|
#define MyAppPublisher "nyne"
|
||||||
#define MyAppURL "https://github.com/venera-app/venera"
|
#define MyAppURL "https://github.com/venera-app/venera"
|
||||||
#define MyAppExeName "venera.exe"
|
#define MyAppExeName "venera.exe"
|
||||||
#define RootPath "{{root_path}}"
|
#define RootPath "{{root_path}}"
|
||||||
|
|
||||||
|
[Code]
|
||||||
|
procedure CurStepChanged(CurStep: TSetupStep);
|
||||||
|
var
|
||||||
|
OldVersionPath, ShortcutPath: string;
|
||||||
|
begin
|
||||||
|
if CurStep = ssInstall then
|
||||||
|
begin
|
||||||
|
OldVersionPath := 'C:\Program Files (x86)\Venera';
|
||||||
|
if DirExists(OldVersionPath) then
|
||||||
|
begin
|
||||||
|
DelTree(OldVersionPath, True, True, True);
|
||||||
|
ShortcutPath := GetEnv('USERPROFILE') + '\Desktop\Venera.lnk';
|
||||||
|
if FileExists(ShortcutPath) then
|
||||||
|
begin
|
||||||
|
DeleteFile(ShortcutPath);
|
||||||
|
end;
|
||||||
|
ShortcutPath := 'C:\Users\Public\Desktop\Venera.lnk';
|
||||||
|
if FileExists(ShortcutPath) then
|
||||||
|
begin
|
||||||
|
DeleteFile(ShortcutPath);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||||
@@ -30,6 +55,8 @@ SetupIconFile={#RootPath}\windows\runner\resources\app_icon.ico
|
|||||||
Compression=lzma
|
Compression=lzma
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
|
ArchitecturesInstallIn64BitMode=x64compatible
|
||||||
|
ArchitecturesAllowed=x64compatible
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
@@ -29,7 +29,7 @@ file.close()
|
|||||||
|
|
||||||
if not os.path.exists("windows/ChineseSimplified.isl"):
|
if not os.path.exists("windows/ChineseSimplified.isl"):
|
||||||
# download ChineseSimplified.isl
|
# download ChineseSimplified.isl
|
||||||
url = "https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/refs/heads/main/ChineseSimplified.isl"
|
url = "https://cdn.jsdelivr.net/gh/kira-96/Inno-Setup-Chinese-Simplified-Translation@latest/ChineseSimplified.isl"
|
||||||
response = httpx.get(url)
|
response = httpx.get(url)
|
||||||
with open('windows/ChineseSimplified.isl', 'wb') as file:
|
with open('windows/ChineseSimplified.isl', 'wb') as file:
|
||||||
file.write(response.content)
|
file.write(response.content)
|
||||||
|
Reference in New Issue
Block a user