diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
deleted file mode 100644
index 558ea6f..0000000
--- a/.github/workflows/linux.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: Build Linux
-run-name: Build Linux
-on:
- workflow_dispatch: {}
-jobs:
- Build_Linux:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: subosito/flutter-action@v2
- with:
- channel: 'stable'
- flutter-version-file: pubspec.yaml
- architecture: x64
- - run: |
- 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
- - run: dart run flutter_to_arch
- - run: |
- sudo rm -rf build/linux/arch/app.tar.gz
- sudo rm -rf build/linux/arch/pkg
- sudo rm -rf build/linux/arch/src
- sudo rm -rf build/linux/arch/PKGBUILD
- - uses: actions/upload-artifact@v4
- with:
- name: deb_build
- path: build/linux/x64/release/debian
- - uses: actions/upload-artifact@v4
- with:
- name: arch_build
- path: build/linux/arch/
\ No newline at end of file
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 8254751..3aaf354 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,5 +1,5 @@
-name: Build IOS
-run-name: Build IOS
+name: Build ALL
+run-name: Build ALL
on:
workflow_dispatch: {}
jobs:
@@ -63,3 +63,79 @@ jobs:
with:
name: app-ios.ipa
path: /Users/runner/work/venera/venera/build/ios/iphoneos/venera-ios.ipa
+ Build_Android:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+ flutter-version-file: pubspec.yaml
+ architecture: x64
+ - name: Decode and install certificate
+ env:
+ STORE_FILE: ${{ secrets.ANDROID_KEYSTORE }}
+ PROPERTY_FILE: ${{ secrets.ANDROID_KEY_PROPERTIES }}
+ run: |
+ echo "$STORE_FILE" | base64 --decode > android/keystore.jks
+ echo "$PROPERTY_FILE" > android/key.properties
+ - uses: actions/setup-java@v4
+ with:
+ distribution: 'oracle'
+ java-version: '17'
+ - run: flutter pub get
+ - run: flutter build apk --release
+ - uses: actions/upload-artifact@v4
+ with:
+ name: apks
+ path: build/app/outputs/apk/release
+ Build_Windows:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: install dependencies
+ run: |
+ choco install yq -y
+ pip install httpx
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+ flutter-version-file: pubspec.yaml
+ architecture: x64
+ - name: build
+ run: |
+ flutter pub get
+ python windows/build.py
+ - uses: actions/upload-artifact@v4
+ with:
+ name: windows_build
+ path: build/windows/Venera-*
+ Build_Linux:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: 'stable'
+ flutter-version-file: pubspec.yaml
+ architecture: x64
+ - run: |
+ 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
+ - run: dart run flutter_to_arch
+ - run: |
+ sudo rm -rf build/linux/arch/app.tar.gz
+ sudo rm -rf build/linux/arch/pkg
+ sudo rm -rf build/linux/arch/src
+ sudo rm -rf build/linux/arch/PKGBUILD
+ - uses: actions/upload-artifact@v4
+ with:
+ name: deb_build
+ path: build/linux/x64/release/debian
+ - uses: actions/upload-artifact@v4
+ with:
+ name: arch_build
+ path: build/linux/arch/
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 4319b96..2d833ce 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
+
Unit)? = null
- private val selectFileCode = 0x11
+ private val nextLocalRequestCode = AtomicInteger()
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == pickDirectoryCode) {
- if(resultCode != Activity.RESULT_OK) {
- result.success(null)
- return
- }
- val pickedDirectoryUri = data?.data
- if (pickedDirectoryUri == null) {
- result.success(null)
- return
- }
- Thread {
- try {
- result.success(onPickedDirectory(pickedDirectoryUri))
+ private fun startContractForResult(
+ contract: ActivityResultContract,
+ input: I,
+ callback: ActivityResultCallback
+ ) {
+ val key = "activity_rq_for_result#${nextLocalRequestCode.getAndIncrement()}"
+ val registry = activityResultRegistry
+ var launcher: ActivityResultLauncher? = null
+ val observer = object : LifecycleEventObserver {
+ override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+ if (Lifecycle.Event.ON_DESTROY == event) {
+ launcher?.unregister()
+ lifecycle.removeObserver(this)
}
- catch (e: Exception) {
- result.error("Failed to Copy Files", e.toString(), null)
- }
- }.start()
- } else if (requestCode == storageRequestCode) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- storagePermissionRequest?.invoke(Environment.isExternalStorageManager())
}
- storagePermissionRequest = null
- } else if (requestCode == selectFileCode) {
- if (resultCode != Activity.RESULT_OK) {
- result.success(null)
- return
- }
- val uri = data?.data
- if (uri == null) {
- result.success(null)
- return
- }
- val contentResolver = context.contentResolver
- val file = DocumentFile.fromSingleUri(context, uri)
- if (file == null) {
- result.success(null)
- return
- }
- val fileName = file.name
- if (fileName == null) {
- result.success(null)
- return
- }
- // copy file to cache directory
- val cacheDir = context.cacheDir
- val newFile = File(cacheDir, fileName)
- val inputStream = contentResolver.openInputStream(uri)
- if (inputStream == null) {
- result.success(null)
- return
- }
- val outputStream = FileOutputStream(newFile)
- inputStream.copyTo(outputStream)
- inputStream.close()
- outputStream.close()
- // send file path to flutter
- result.success(newFile.absolutePath)
}
+ lifecycle.addObserver(observer)
+ val newCallback = ActivityResultCallback {
+ launcher?.unregister()
+ lifecycle.removeObserver(observer)
+ callback.onActivityResult(it)
+ }
+ launcher = registry.register(key, contract, newCallback)
+ launcher.launch(input)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
@@ -115,12 +83,27 @@ class MainActivity : FlutterActivity() {
}
res.success(null)
}
+
"getDirectoryPath" -> {
- this.result = res
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
- startActivityForResult(intent, pickDirectoryCode)
+ startContractForResult(ActivityResultContracts.StartActivityForResult(), intent) { activityResult ->
+ if (activityResult.resultCode != Activity.RESULT_OK) {
+ res.success(null)
+ return@startContractForResult
+ }
+ val pickedDirectoryUri = activityResult.data?.data
+ if (pickedDirectoryUri == null)
+ res.success(null)
+ else
+ try {
+ res.success(onPickedDirectory(pickedDirectoryUri))
+ } catch (e: Exception) {
+ res.error("Failed to Copy Files", e.toString(), null)
+ }
+ }
}
+
else -> res.notImplemented()
}
}
@@ -137,6 +120,7 @@ class MainActivity : FlutterActivity() {
events.success(2)
}
}
+
override fun onCancel(arguments: Any?) {
listening = false
}
@@ -144,15 +128,14 @@ class MainActivity : FlutterActivity() {
val storageChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/storage")
storageChannel.setMethodCallHandler { _, res ->
- requestStoragePermission {result ->
+ requestStoragePermission { result ->
res.success(result)
}
}
val selectFileChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/select_file")
selectFileChannel.setMethodCallHandler { _, res ->
- openFile()
- result = res
+ openFile(res)
}
}
@@ -167,12 +150,13 @@ class MainActivity : FlutterActivity() {
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
- if(listening){
+ if (listening) {
when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
volumeListen.down()
return true
}
+
KeyEvent.KEYCODE_VOLUME_UP -> {
volumeListen.up()
return true
@@ -184,19 +168,30 @@ class MainActivity : FlutterActivity() {
/// copy the directory to tmp directory, return copied directory
private fun onPickedDirectory(uri: Uri): String {
- val contentResolver = context.contentResolver
- var tmp = context.cacheDir
- tmp = File(tmp, "getDirectoryPathTemp")
- tmp.mkdir()
- copyDirectory(contentResolver, uri, tmp)
+ if (!hasStoragePermission()) {
+ // dart:io cannot access the directory without permission.
+ // so we need to copy the directory to cache directory
+ val contentResolver = contentResolver
+ var tmp = cacheDir
+ tmp = File(tmp, "getDirectoryPathTemp")
+ tmp.mkdir()
+ Thread {
+ copyDirectory(contentResolver, uri, tmp)
+ }.start()
- return tmp.absolutePath
+ return tmp.absolutePath
+ } else {
+ val docId = DocumentsContract.getTreeDocumentId(uri)
+ val split: Array = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ return if ((split.size >= 2) && (split[1] != null)) split[1]!!
+ else File.separator
+ }
}
private fun copyDirectory(resolver: ContentResolver, srcUri: Uri, destDir: File) {
- val src = DocumentFile.fromTreeUri(context, srcUri) ?: return
+ val src = DocumentFile.fromTreeUri(this, srcUri) ?: return
for (file in src.listFiles()) {
- if(file.isDirectory) {
+ if (file.isDirectory) {
val newDir = File(destDir, file.name!!)
newDir.mkdir()
copyDirectory(resolver, file.uri, newDir)
@@ -211,8 +206,22 @@ class MainActivity : FlutterActivity() {
}
}
+ private fun hasStoragePermission(): Boolean {
+ return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ ) == PackageManager.PERMISSION_GRANTED
+ } else {
+ Environment.isExternalStorageManager()
+ }
+ }
+
private fun requestStoragePermission(result: (Boolean) -> Unit) {
- if(Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
val readPermission = ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
@@ -241,8 +250,10 @@ class MainActivity : FlutterActivity() {
try {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.addCategory("android.intent.category.DEFAULT")
- intent.data = Uri.parse("package:" + context.packageName)
- startActivityForResult(intent, storageRequestCode)
+ intent.data = Uri.parse("package:$packageName")
+ startContractForResult(ActivityResultContracts.StartActivityForResult(), intent){ _ ->
+ result(Environment.isExternalStorageManager())
+ }
} catch (e: Exception) {
result(false)
}
@@ -258,7 +269,7 @@ class MainActivity : FlutterActivity() {
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- if(requestCode == storageRequestCode) {
+ if (requestCode == storageRequestCode) {
storagePermissionRequest?.invoke(grantResults.all {
it == PackageManager.PERMISSION_GRANTED
})
@@ -266,21 +277,67 @@ class MainActivity : FlutterActivity() {
}
}
- fun openFile() {
+ private fun openFile(result: MethodChannel.Result) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
- startActivityForResult(intent, selectFileCode)
+ startContractForResult(ActivityResultContracts.StartActivityForResult(), intent){ activityResult ->
+ if (activityResult.resultCode != Activity.RESULT_OK) {
+ result.success(null)
+ return@startContractForResult
+ }
+ val uri = activityResult.data?.data
+ if (uri == null) {
+ result.success(null)
+ return@startContractForResult
+ }
+ val contentResolver = contentResolver
+ val file = DocumentFile.fromSingleUri(this, uri)
+ if (file == null) {
+ result.success(null)
+ return@startContractForResult
+ }
+ val fileName = file.name
+ if (fileName == null) {
+ result.success(null)
+ return@startContractForResult
+ }
+ if(hasStoragePermission()) {
+ try {
+ val filePath = FileUtils.getPathFromUri(this, uri)
+ result.success(filePath)
+ return@startContractForResult
+ }
+ catch (e: Exception) {
+ // ignore
+ }
+ }
+ // copy file to cache directory
+ val cacheDir = cacheDir
+ val newFile = File(cacheDir, fileName)
+ val inputStream = contentResolver.openInputStream(uri)
+ if (inputStream == null) {
+ result.success(null)
+ return@startContractForResult
+ }
+ val outputStream = FileOutputStream(newFile)
+ inputStream.copyTo(outputStream)
+ inputStream.close()
+ outputStream.close()
+ // send file path to flutter
+ result.success(newFile.absolutePath)
+ }
}
}
-class VolumeListen{
+class VolumeListen {
var onUp = fun() {}
var onDown = fun() {}
- fun up(){
+ fun up() {
onUp()
}
- fun down(){
+
+ fun down() {
onDown()
}
}
diff --git a/assets/translation.json b/assets/translation.json
index 9917cd2..d2dc8bc 100644
--- a/assets/translation.json
+++ b/assets/translation.json
@@ -43,10 +43,11 @@
"Confirm": "确认",
"Are you sure you want to delete this comic?": "您确定要删除这部漫画吗?",
"Are you sure you want to delete @a selected comics?": "您确定要删除 @a 部漫画吗?",
- "Add comic source": "添加漫画来源",
+ "Add comic source": "添加漫画源",
"Select file": "选择文件",
"View list": "查看列表",
"Open help": "打开帮助",
+ "Open in Browser": "打开网页",
"Check updates": "检查更新",
"Edit": "编辑",
"Update": "更新",
@@ -101,8 +102,8 @@
"Auto page turning interval": "自动翻页间隔",
"Theme Mode": "主题模式",
"System": "系统",
- "Light": "明亮",
- "Dark": "黑暗",
+ "Light": "浅色",
+ "Dark": "深色",
"Theme Color": "主题颜色",
"Red": "红色",
"Pink": "粉色",
@@ -209,7 +210,26 @@
"Update Comics Info": "更新漫画信息",
"Create Folder": "新建文件夹",
"Select an image on screen": "选择屏幕上的图片",
- "Added @count comics to download queue.": "已添加 @count 本漫画到下载队列"
+ "Added @count comics to download queue.": "已添加 @count 本漫画到下载队列",
+ "Ignore Certificate Errors": "忽略证书错误",
+ "Authorization Required": "需要身份验证",
+ "Sync": "同步",
+ "The folder is Linked to @source": "文件夹已关联到 @source",
+ "Source Folder": "源收藏夹",
+ "Use a config file": "使用配置文件",
+ "Comic Source list": "漫画源列表",
+ "View": "查看",
+ "Copy": "复制",
+ "Copied": "已复制",
+ "Search History": "搜索历史",
+ "Clear Search History": "清除搜索历史",
+ "Search in": "搜索于",
+ "Clear History": "清除历史",
+ "Are you sure you want to clear your history?": "确定要清除您的历史记录吗?",
+ "No Explore Pages": "没有探索页面",
+ "Add a comic source in home page": "在主页添加一个漫画源",
+ "Please check your settings": "请检查您的设置",
+ "No Category Pages": "没有分类页面"
},
"zh_TW": {
"Home": "首頁",
@@ -257,10 +277,11 @@
"Confirm": "確認",
"Are you sure you want to delete this comic?": "您確定要刪除這部漫畫嗎?",
"Are you sure you want to delete @a selected comics?": "您確定要刪除 @a 部漫畫嗎?",
- "Add comic source": "添加漫畫來源",
+ "Add comic source": "添加漫畫源",
"Select file": "選擇文件",
"View list": "查看列表",
"Open help": "打開幫助",
+ "Open in Browser": "打開網頁",
"Check updates": "檢查更新",
"Edit": "編輯",
"Update": "更新",
@@ -313,8 +334,8 @@
"Auto page turning interval": "自動翻頁間隔",
"Theme Mode": "主題模式",
"System": "系統",
- "Light": "明亮",
- "Dark": "黑暗",
+ "Light": "浅色",
+ "Dark": "深色",
"Theme Color": "主題顏色",
"Red": "紅色",
"Pink": "粉色",
@@ -421,6 +442,25 @@
"Update Comics Info": "更新漫畫信息",
"Create Folder": "新建文件夾",
"Select an image on screen": "選擇屏幕上的圖片",
- "Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列"
+ "Added @count comics to download queue.": "已添加 @count 本漫畫到下載隊列",
+ "Ignore Certificate Errors": "忽略證書錯誤",
+ "Authorization Required": "需要身份驗證",
+ "Sync": "同步",
+ "The folder is Linked to @source": "文件夾已關聯到 @source",
+ "Source Folder": "源收藏夾",
+ "Use a config file": "使用配置文件",
+ "Comic Source list": "漫畫源列表",
+ "View": "查看",
+ "Copy": "複製",
+ "Copied": "已複製",
+ "Search History": "搜索歷史",
+ "Clear Search History": "清除搜索歷史",
+ "Search in": "搜索於",
+ "Clear History": "清除歷史",
+ "Are you sure you want to clear your history?": "確定要清除您的歷史記錄嗎?",
+ "No Explore Pages": "沒有探索頁面",
+ "Add a comic source in home page": "在主頁添加一個漫畫源",
+ "Please check your settings": "請檢查您的設定",
+ "No Category Pages": "沒有分類頁面"
}
}
\ No newline at end of file
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 93a0af2..be8d5d6 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -51,5 +51,7 @@
LSSupportsOpeningDocumentsInPlace
+ NSFaceIDUsageDescription
+ Ensure that the operation is being performed by the user themselves.
diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart
index 6573dea..80c67f7 100644
--- a/lib/components/appbar.dart
+++ b/lib/components/appbar.dart
@@ -369,10 +369,14 @@ class _FilledTabBarState extends State {
final double tabWidth = tabRight - tabLeft;
final double tabCenter = tabLeft + tabWidth / 2;
final double tabBarWidth = tabBarBox.size.width;
- final double scrollOffset = tabCenter - tabBarWidth / 2;
+ double scrollOffset = tabCenter - tabBarWidth / 2;
if (scrollOffset == scrollController.offset) {
return;
}
+ scrollOffset = scrollOffset.clamp(
+ 0.0,
+ scrollController.position.maxScrollExtent,
+ );
scrollController.animateTo(
scrollOffset,
duration: const Duration(milliseconds: 200),
diff --git a/lib/components/comic.dart b/lib/components/comic.dart
index 70c9e3e..268f1b6 100644
--- a/lib/components/comic.dart
+++ b/lib/components/comic.dart
@@ -226,75 +226,126 @@ class ComicTile extends StatelessWidget {
Widget _buildBriefMode(BuildContext context) {
return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
- child: Material(
- color: Colors.transparent,
- borderRadius: BorderRadius.circular(8),
- elevation: 1,
- child: Stack(
- children: [
- Positioned.fill(
- child: Container(
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.secondaryContainer,
- borderRadius: BorderRadius.circular(8),
- ),
- clipBehavior: Clip.antiAlias,
- child: buildImage(context),
- ),
- ),
- Positioned(
- bottom: 0,
- left: 0,
- right: 0,
- child: Container(
- width: double.infinity,
- decoration: BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.topCenter,
- end: Alignment.bottomCenter,
- colors: [
- Colors.transparent,
- Colors.black.withOpacity(0.3),
- Colors.black.withOpacity(0.5),
- ]),
- borderRadius: const BorderRadius.only(
- bottomLeft: Radius.circular(8),
- bottomRight: Radius.circular(8),
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ return InkWell(
+ borderRadius: BorderRadius.circular(8),
+ onTap: _onTap,
+ onLongPress:
+ enableLongPressed ? () => onLongPress(context) : null,
+ onSecondaryTapDown: (detail) => onSecondaryTap(detail, context),
+ child: Column(
+ children: [
+ Expanded(
+ child: SizedBox(
+ child: Stack(
+ children: [
+ Positioned.fill(
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context)
+ .colorScheme
+ .secondaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ clipBehavior: Clip.antiAlias,
+ child: buildImage(context),
+ ),
+ ),
+ Positioned(
+ bottom: 0,
+ right: 0,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 4, vertical: 4),
+ child: ClipRRect(
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(10.0),
+ topRight: Radius.circular(10.0),
+ bottomRight: Radius.circular(10.0),
+ bottomLeft: Radius.circular(10.0),
+ ),
+ child: Container(
+ color: Colors.black.withOpacity(0.5),
+ child: Padding(
+ padding:
+ const EdgeInsets.fromLTRB(8, 6, 8, 6),
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ maxWidth: constraints.maxWidth * 0.88,
+ ),
+ child: Text(
+ comic.description.isEmpty
+ ? comic.subtitle
+ ?.replaceAll('\n', '') ??
+ ''
+ : comic.description
+ .split('|')
+ .join('\n'),
+ style: const TextStyle(
+ fontWeight: FontWeight.w500,
+ fontSize: 12,
+ color: Colors.white,
+ ),
+ textAlign: TextAlign.right,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ),
+ ),
+ ),
+ )),
+ ],
+ ),
),
),
- child: Padding(
- padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(8, 4, 8, 0),
child: Text(
- comic.title.replaceAll("\n", ""),
+ comic.title.replaceAll('\n', ''),
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14.0,
- color: Colors.white,
),
- maxLines: 2,
+ maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
- )),
- Positioned.fill(
- child: Material(
- color: Colors.transparent,
- child: InkWell(
- onTap: _onTap,
- onLongPress:
- enableLongPressed ? () => onLongPress(context) : null,
- onSecondaryTapDown: (detail) =>
- onSecondaryTap(detail, context),
- borderRadius: BorderRadius.circular(8),
- child: const SizedBox.expand(),
- ),
+ ],
),
- )
- ],
- ),
- ),
- );
+ );
+ },
+ ));
+ }
+
+ List _splitText(String text) {
+ // split text by space, comma. text in brackets will be kept together.
+ var words = [];
+ var buffer = StringBuffer();
+ var inBracket = false;
+ for (var i = 0; i < text.length; i++) {
+ var c = text[i];
+ if (c == '[' || c == '(') {
+ inBracket = true;
+ } else if (c == ']' || c == ')') {
+ inBracket = false;
+ } else if (c == ' ' || c == ',') {
+ if (inBracket) {
+ buffer.write(c);
+ } else {
+ words.add(buffer.toString());
+ buffer.clear();
+ }
+ } else {
+ buffer.write(c);
+ }
+ }
+ if (buffer.isNotEmpty) {
+ words.add(buffer.toString());
+ }
+ return words;
}
void block(BuildContext comicTileContext) {
@@ -303,7 +354,7 @@ class ComicTile extends StatelessWidget {
builder: (context) {
var words = [];
var all = [];
- all.addAll(comic.title.split(' ').where((element) => element != ''));
+ all.addAll(_splitText(comic.title));
if (comic.subtitle != null && comic.subtitle != "") {
all.add(comic.subtitle!);
}
@@ -691,6 +742,9 @@ class _SliverGridComics extends StatelessWidget {
menuOptions: menuBuilder?.call(comics[index]),
onTap: onTap != null ? () => onTap!(comics[index]) : null,
);
+ if(selection == null) {
+ return comic;
+ }
return Container(
decoration: BoxDecoration(
color: isSelected
diff --git a/lib/components/custom_slider.dart b/lib/components/custom_slider.dart
new file mode 100644
index 0000000..ae9f04f
--- /dev/null
+++ b/lib/components/custom_slider.dart
@@ -0,0 +1,224 @@
+import 'package:flutter/material.dart';
+
+/// patched slider.dart with RtL support
+class _SliderDefaultsM3 extends SliderThemeData {
+ _SliderDefaultsM3(this.context)
+ : super(trackHeight: 4.0);
+
+ final BuildContext context;
+ late final ColorScheme _colors = Theme.of(context).colorScheme;
+
+ @override
+ Color? get activeTrackColor => _colors.primary;
+
+ @override
+ Color? get inactiveTrackColor => _colors.surfaceContainerHighest;
+
+ @override
+ Color? get secondaryActiveTrackColor => _colors.primary.withOpacity(0.54);
+
+ @override
+ Color? get disabledActiveTrackColor => _colors.onSurface.withOpacity(0.38);
+
+ @override
+ Color? get disabledInactiveTrackColor => _colors.onSurface.withOpacity(0.12);
+
+ @override
+ Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withOpacity(0.12);
+
+ @override
+ Color? get activeTickMarkColor => _colors.onPrimary.withOpacity(0.38);
+
+ @override
+ Color? get inactiveTickMarkColor => _colors.onSurfaceVariant.withOpacity(0.38);
+
+ @override
+ Color? get disabledActiveTickMarkColor => _colors.onSurface.withOpacity(0.38);
+
+ @override
+ Color? get disabledInactiveTickMarkColor => _colors.onSurface.withOpacity(0.38);
+
+ @override
+ Color? get thumbColor => _colors.primary;
+
+ @override
+ Color? get disabledThumbColor => Color.alphaBlend(_colors.onSurface.withOpacity(0.38), _colors.surface);
+
+ @override
+ Color? get overlayColor => WidgetStateColor.resolveWith((Set states) {
+ if (states.contains(WidgetState.dragged)) {
+ return _colors.primary.withOpacity(0.1);
+ }
+ if (states.contains(WidgetState.hovered)) {
+ return _colors.primary.withOpacity(0.08);
+ }
+ if (states.contains(WidgetState.focused)) {
+ return _colors.primary.withOpacity(0.1);
+ }
+
+ return Colors.transparent;
+ });
+
+ @override
+ TextStyle? get valueIndicatorTextStyle => Theme.of(context).textTheme.labelMedium!.copyWith(
+ color: _colors.onPrimary,
+ );
+
+ @override
+ SliderComponentShape? get valueIndicatorShape => const DropSliderValueIndicatorShape();
+}
+
+class CustomSlider extends StatefulWidget {
+ const CustomSlider({required this.min, required this.max, required this.value, required this.divisions, required this.onChanged, required this.focusNode, this.reversed = false, super.key});
+
+ final double min;
+
+ final double max;
+
+ final double value;
+
+ final int divisions;
+
+ final void Function(double) onChanged;
+
+ final FocusNode? focusNode;
+
+ final bool reversed;
+
+ @override
+ State createState() => _CustomSliderState();
+}
+
+class _CustomSliderState extends State {
+ late double value;
+
+ @override
+ void initState() {
+ super.initState();
+ value = widget.value;
+ }
+
+ @override
+ void didUpdateWidget(CustomSlider oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.value != oldWidget.value) {
+ setState(() {
+ value = widget.value;
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final colorScheme = Theme.of(context).colorScheme;
+ final theme = _SliderDefaultsM3(context);
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(24, 12, 24, 12),
+ child: widget.max - widget.min > 0 ? LayoutBuilder(
+ builder: (context, constraints) => MouseRegion(
+ cursor: SystemMouseCursors.click,
+ child: GestureDetector(
+ behavior: HitTestBehavior.translucent,
+ onTapDown: (details){
+ var dx = details.localPosition.dx;
+ if(widget.reversed){
+ dx = constraints.maxWidth - dx;
+ }
+ var gap = constraints.maxWidth / widget.divisions;
+ var gapValue = (widget.max - widget.min) / widget.divisions;
+ widget.onChanged.call((dx / gap).round() * gapValue + widget.min);
+ },
+ onVerticalDragUpdate: (details){
+ var dx = details.localPosition.dx;
+ if(dx > constraints.maxWidth || dx < 0) return;
+ if(widget.reversed){
+ dx = constraints.maxWidth - dx;
+ }
+ var gap = constraints.maxWidth / widget.divisions;
+ var gapValue = (widget.max - widget.min) / widget.divisions;
+ widget.onChanged.call((dx / gap).round() * gapValue + widget.min);
+ },
+ child: SizedBox(
+ height: 24,
+ child: Center(
+ child: SizedBox(
+ height: 24,
+ child: Stack(
+ clipBehavior: Clip.none,
+ children: [
+ Positioned.fill(
+ child: Center(
+ child: Container(
+ width: double.infinity,
+ height: 6,
+ decoration: BoxDecoration(
+ color: theme.inactiveTrackColor,
+ borderRadius: const BorderRadius.all(Radius.circular(10))
+ ),
+ ),
+ ),
+ ),
+ if(constraints.maxWidth / widget.divisions > 10)
+ Positioned.fill(
+ child: Row(
+ children: (){
+ var res = [];
+ for(int i = 0; i createState() => FlyoutState();
+
+ static FlyoutState of(BuildContext context) {
+ return context.findAncestorStateOfType()!;
+ }
}
class FlyoutState extends State {
diff --git a/lib/components/layout.dart b/lib/components/layout.dart
index fa65475..1d098a2 100644
--- a/lib/components/layout.dart
+++ b/lib/components/layout.dart
@@ -2,10 +2,7 @@ part of 'components.dart';
class SliverGridViewWithFixedItemHeight extends StatelessWidget {
const SliverGridViewWithFixedItemHeight(
- {required this.delegate,
- required this.maxCrossAxisExtent,
- required this.itemHeight,
- super.key});
+ {required this.delegate, required this.maxCrossAxisExtent, required this.itemHeight, super.key});
final SliverChildDelegate delegate;
@@ -65,8 +62,7 @@ class SliverGridDelegateWithFixedHeight extends SliverGridDelegate {
@override
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
if (oldDelegate is! SliverGridDelegateWithFixedHeight) return true;
- if (oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent ||
- oldDelegate.itemHeight != itemHeight) {
+ if (oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent || oldDelegate.itemHeight != itemHeight) {
return true;
}
return false;
@@ -95,8 +91,7 @@ class SliverGridDelegateWithComics extends SliverGridDelegate {
}
}
- SliverGridLayout getDetailedModeLayout(
- SliverConstraints constraints, double scale) {
+ SliverGridLayout getDetailedModeLayout(SliverConstraints constraints, double scale) {
const minCrossAxisExtent = 360;
final itemHeight = 152 * scale;
final width = constraints.crossAxisExtent;
@@ -111,14 +106,11 @@ class SliverGridDelegateWithComics extends SliverGridDelegate {
reverseCrossAxis: false);
}
- SliverGridLayout getBriefModeLayout(
- SliverConstraints constraints, double scale) {
+ SliverGridLayout getBriefModeLayout(SliverConstraints constraints, double scale) {
final maxCrossAxisExtent = 192.0 * scale;
- const childAspectRatio = 0.72;
+ const childAspectRatio = 0.68;
const crossAxisSpacing = 0.0;
- int crossAxisCount =
- (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing))
- .ceil();
+ int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil();
// Ensure a minimum count of 1, can be zero and result in an infinite extent
// below when the window size is 0.
crossAxisCount = math.max(1, crossAxisCount);
diff --git a/lib/components/select.dart b/lib/components/select.dart
index 66c746e..e2b9e05 100644
--- a/lib/components/select.dart
+++ b/lib/components/select.dart
@@ -31,8 +31,9 @@ class Select extends StatelessWidget {
var size = renderBox.size;
showMenu(
elevation: 3,
- color: context.colorScheme.surface,
- surfaceTintColor: Colors.transparent,
+ color: context.brightness == Brightness.light
+ ? const Color(0xFFF6F6F6)
+ : const Color(0xFF1E1E1E),
context: context,
useRootNavigator: true,
constraints: BoxConstraints(
@@ -41,8 +42,8 @@ class Select extends StatelessWidget {
),
position: RelativeRect.fromLTRB(
offset.dx,
- offset.dy + size.height,
- offset.dx + size.height,
+ offset.dy + size.height + 2,
+ offset.dx + size.height + 2,
offset.dy,
),
items: values
diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart
index 61fcb34..1cd8ab9 100644
--- a/lib/foundation/app.dart
+++ b/lib/foundation/app.dart
@@ -10,7 +10,7 @@ export "widget_utils.dart";
export "context.dart";
class _App {
- final version = "1.0.5";
+ final version = "1.0.6";
bool get isAndroid => Platform.isAndroid;
diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart
index afd1522..8c5d347 100644
--- a/lib/foundation/appdata.dart
+++ b/lib/foundation/appdata.dart
@@ -119,6 +119,8 @@ class _Settings with ChangeNotifier {
'quickFavorite': null,
'enableTurnPageByVolumeKey': true,
'enableClockAndBatteryInfoInReader': true,
+ 'ignoreCertificateErrors': false,
+ 'authorizationRequired': false,
};
operator [](String key) {
diff --git a/lib/foundation/comic_source/comic_source.dart b/lib/foundation/comic_source/comic_source.dart
index 758d8f3..a537918 100644
--- a/lib/foundation/comic_source/comic_source.dart
+++ b/lib/foundation/comic_source/comic_source.dart
@@ -136,6 +136,8 @@ class ComicSource {
notifyListeners();
}
+ static bool get isEmpty => _sources.isEmpty;
+
/// Name of this source.
final String name;
diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart
index b02c54f..23b8d37 100644
--- a/lib/foundation/comic_source/parser.dart
+++ b/lib/foundation/comic_source/parser.dart
@@ -157,9 +157,11 @@ class ComicSourceParser {
await source.loadData();
- Future.delayed(const Duration(milliseconds: 50), () {
- JsEngine().runCode("ComicSource.sources.$_key.init()");
- });
+ if(_checkExists("init")) {
+ Future.delayed(const Duration(milliseconds: 50), () {
+ JsEngine().runCode("ComicSource.sources.$_key.init()");
+ });
+ }
return source;
}
diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart
index eef3fa3..d6f245c 100644
--- a/lib/foundation/favorites.dart
+++ b/lib/foundation/favorites.dart
@@ -346,6 +346,32 @@ class LocalFavoritesManager with ChangeNotifier {
return name;
}
+ void linkFolderToNetwork(String folder, String source, String networkFolder) {
+ _db.execute("""
+ insert or replace into folder_sync (folder_name, source_key, source_folder)
+ values (?, ?, ?);
+ """, [folder, source, networkFolder]);
+ }
+
+ bool isLinkedToNetworkFolder(String folder, String source, String networkFolder) {
+ var res = _db.select("""
+ select * from folder_sync
+ where folder_name == ? and source_key == ? and source_folder == ?;
+ """, [folder, source, networkFolder]);
+ return res.isNotEmpty;
+ }
+
+ (String?, String?) findLinked(String folder) {
+ var res = _db.select("""
+ select * from folder_sync
+ where folder_name == ?;
+ """, [folder]);
+ if (res.isEmpty) {
+ return (null, null);
+ }
+ return (res.first["source_key"], res.first["source_folder"]);
+ }
+
bool comicExists(String folder, String id, ComicType type) {
var res = _db.select("""
select * from "$folder"
@@ -365,20 +391,19 @@ class LocalFavoritesManager with ChangeNotifier {
return FavoriteItem.fromRow(res.first);
}
- /// add comic to a folder
- ///
- /// This method will download cover to local, to avoid problems like changing url
- void addComic(String folder, FavoriteItem comic, [int? order]) async {
+ /// add comic to a folder.
+ /// return true if success, false if already exists
+ bool addComic(String folder, FavoriteItem comic, [int? order]) {
_modifiedAfterLastCache = true;
if (!existsFolder(folder)) {
throw Exception("Folder does not exists");
}
var res = _db.select("""
select * from "$folder"
- where id == '${comic.id}';
- """);
+ where id == ? and type == ?;
+ """, [comic.id, comic.type.value]);
if (res.isNotEmpty) {
- return;
+ return false;
}
final params = [
comic.id,
@@ -406,6 +431,7 @@ class LocalFavoritesManager with ChangeNotifier {
""", [...params, minValue(folder) - 1]);
}
notifyListeners();
+ return true;
}
/// delete a folder
@@ -414,6 +440,10 @@ class LocalFavoritesManager with ChangeNotifier {
_db.execute("""
drop table "$name";
""");
+ _db.execute("""
+ delete from folder_order
+ where folder_name == ?;
+ """, [name]);
notifyListeners();
}
@@ -461,6 +491,16 @@ class LocalFavoritesManager with ChangeNotifier {
ALTER TABLE "$before"
RENAME TO "$after";
""");
+ _db.execute("""
+ update folder_order
+ set folder_name = ?
+ where folder_name == ?;
+ """, [after, before]);
+ _db.execute("""
+ update folder_sync
+ set folder_name = ?
+ where folder_name == ?;
+ """, [after, before]);
notifyListeners();
}
diff --git a/lib/foundation/js_engine.dart b/lib/foundation/js_engine.dart
index 8ae647f..88eae18 100644
--- a/lib/foundation/js_engine.dart
+++ b/lib/foundation/js_engine.dart
@@ -19,6 +19,7 @@ import 'package:pointycastle/block/modes/cfb.dart';
import 'package:pointycastle/block/modes/ecb.dart';
import 'package:pointycastle/block/modes/ofb.dart';
import 'package:uuid/uuid.dart';
+import 'package:venera/foundation/app.dart';
import 'package:venera/network/app_dio.dart';
import 'package:venera/network/cookie_jar.dart';
@@ -70,6 +71,7 @@ class JsEngine with _JSEngineApi {
var setGlobalFunc =
_engine!.evaluate("(key, value) => { this[key] = value; }");
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
+ setGlobalFunc(["appVersion", App.version]);
setGlobalFunc.free();
var jsInit = await rootBundle.load("assets/init.js");
_engine!
diff --git a/lib/main.dart b/lib/main.dart
index 7351392..2000267 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -5,7 +5,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:rhttp/rhttp.dart';
import 'package:venera/foundation/log.dart';
-import 'package:venera/network/app_dio.dart';
+import 'package:venera/pages/auth_page.dart';
import 'package:venera/pages/comic_source_page.dart';
import 'package:venera/pages/main_page.dart';
import 'package:venera/pages/settings/settings_page.dart';
@@ -65,15 +65,59 @@ class MyApp extends StatefulWidget {
State createState() => _MyAppState();
}
-class _MyAppState extends State {
+class _MyAppState extends State with WidgetsBindingObserver {
@override
void initState() {
checkUpdates();
App.registerForceRebuild(forceRebuild);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
+ WidgetsBinding.instance.addObserver(this);
super.initState();
}
+ bool isAuthPageActive = false;
+
+ OverlayEntry? hideContentOverlay;
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if(!App.isMobile) {
+ return;
+ }
+ if (state == AppLifecycleState.inactive && hideContentOverlay == null) {
+ hideContentOverlay = OverlayEntry(
+ builder: (context) {
+ return Positioned.fill(
+ child: Container(
+ width: double.infinity,
+ height: double.infinity,
+ color: App.rootContext.colorScheme.surface,
+ ),
+ );
+ },
+ );
+ Overlay.of(App.rootContext).insert(hideContentOverlay!);
+ } else if (hideContentOverlay != null &&
+ state == AppLifecycleState.resumed) {
+ hideContentOverlay!.remove();
+ hideContentOverlay = null;
+ }
+ if (state == AppLifecycleState.hidden &&
+ appdata.settings['authorizationRequired'] &&
+ !isAuthPageActive) {
+ isAuthPageActive = true;
+ App.rootContext.to(
+ () => AuthPage(
+ onSuccessfulAuth: () {
+ App.rootContext.pop();
+ isAuthPageActive = false;
+ },
+ ),
+ );
+ }
+ super.didChangeAppLifecycleState(state);
+ }
+
void forceRebuild() {
void rebuild(Element el) {
el.markNeedsBuild();
@@ -86,14 +130,25 @@ class _MyAppState extends State {
@override
Widget build(BuildContext context) {
+ Widget home;
+ if (appdata.settings['authorizationRequired']) {
+ home = AuthPage(
+ onSuccessfulAuth: () {
+ App.rootContext.toReplacement(() => const MainPage());
+ },
+ );
+ } else {
+ home = const MainPage();
+ }
return MaterialApp(
- home: const MainPage(),
+ home: home,
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: App.mainColor,
surface: Colors.white,
primary: App.mainColor.shade600,
+ // ignore: deprecated_member_use
background: Colors.white,
),
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
@@ -105,6 +160,7 @@ class _MyAppState extends State {
brightness: Brightness.dark,
surface: Colors.black,
primary: App.mainColor.shade400,
+ // ignore: deprecated_member_use
background: Colors.black,
),
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
@@ -171,12 +227,12 @@ class _MyAppState extends State {
}
void checkUpdates() async {
- if(!appdata.settings['checkUpdateOnStart']) {
+ if (!appdata.settings['checkUpdateOnStart']) {
return;
}
var lastCheck = appdata.implicitData['lastCheckUpdate'] ?? 0;
var now = DateTime.now().millisecondsSinceEpoch;
- if(now - lastCheck < 24 * 60 * 60 * 1000) {
+ if (now - lastCheck < 24 * 60 * 60 * 1000) {
return;
}
appdata.implicitData['lastCheckUpdate'] = now;
diff --git a/lib/network/app_dio.dart b/lib/network/app_dio.dart
index 4608952..233b932 100644
--- a/lib/network/app_dio.dart
+++ b/lib/network/app_dio.dart
@@ -106,34 +106,24 @@ class MyLogInterceptor implements Interceptor {
class AppDio with DioMixin {
String? _proxy = proxy;
+ static bool get ignoreCertificateErrors => appdata.settings['ignoreCertificateErrors'] == true;
AppDio([BaseOptions? options]) {
this.options = options ?? BaseOptions();
- httpClientAdapter = RHttpAdapter(const rhttp.ClientSettings());
+ httpClientAdapter = RHttpAdapter(rhttp.ClientSettings(
+ proxySettings: proxy == null
+ ? const rhttp.ProxySettings.noProxy()
+ : rhttp.ProxySettings.proxy(proxy!),
+ tlsSettings: rhttp.TlsSettings(
+ verifyCertificates: !ignoreCertificateErrors,
+ ),
+ ));
interceptors.add(CookieManagerSql(SingleInstanceCookieJar.instance!));
interceptors.add(NetworkCacheManager());
interceptors.add(CloudflareInterceptor());
interceptors.add(MyLogInterceptor());
}
- static HttpClient createHttpClient() {
- final client = HttpClient();
- client.connectionTimeout = const Duration(seconds: 5);
- client.findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy";
- client.idleTimeout = const Duration(seconds: 100);
- client.badCertificateCallback =
- (X509Certificate cert, String host, int port) {
- if (host.contains("cdn")) return true;
- final ipv4RegExp = RegExp(
- r'^((25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})$');
- if (ipv4RegExp.hasMatch(host)) {
- return true;
- }
- return false;
- };
- return client;
- }
-
static String? proxy;
static Future getProxy() async {
@@ -189,8 +179,8 @@ class AppDio with DioMixin {
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
- if(options?.headers?['prevent-parallel'] == 'true') {
- while(_requests.containsKey(path)) {
+ if (options?.headers?['prevent-parallel'] == 'true') {
+ while (_requests.containsKey(path)) {
await Future.delayed(const Duration(milliseconds: 20));
}
_requests[path] = true;
@@ -204,6 +194,9 @@ class AppDio with DioMixin {
proxySettings: proxy == null
? const rhttp.ProxySettings.noProxy()
: rhttp.ProxySettings.proxy(proxy!),
+ tlsSettings: rhttp.TlsSettings(
+ verifyCertificates: !ignoreCertificateErrors,
+ ),
));
}
try {
@@ -216,9 +209,8 @@ class AppDio with DioMixin {
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
- }
- finally {
- if(_requests.containsKey(path)) {
+ } finally {
+ if (_requests.containsKey(path)) {
_requests.remove(path);
}
}
@@ -237,6 +229,9 @@ class RHttpAdapter implements HttpClientAdapter {
keepAlivePing: Duration(seconds: 30),
),
throwOnStatusCode: false,
+ tlsSettings: rhttp.TlsSettings(
+ verifyCertificates: !AppDio.ignoreCertificateErrors,
+ ),
);
}
@@ -284,7 +279,7 @@ class RHttpAdapter implements HttpClientAdapter {
headers[key]!.add(entry.$2);
}
var data = res.body;
- if(headers['content-encoding']?.contains('gzip') ?? false) {
+ if (headers['content-encoding']?.contains('gzip') ?? false) {
// rhttp does not support gzip decoding
data = gzip.decoder.bind(data).map((data) => Uint8List.fromList(data));
}
diff --git a/lib/network/cloudflare.dart b/lib/network/cloudflare.dart
index 7d8c5f2..ff22041 100644
--- a/lib/network/cloudflare.dart
+++ b/lib/network/cloudflare.dart
@@ -1,7 +1,6 @@
import 'dart:io' as io;
import 'package:dio/dio.dart';
-import 'package:flutter_qjs/flutter_qjs.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/consts.dart';
diff --git a/lib/pages/auth_page.dart b/lib/pages/auth_page.dart
new file mode 100644
index 0000000..67b0d29
--- /dev/null
+++ b/lib/pages/auth_page.dart
@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:local_auth/local_auth.dart';
+import 'package:venera/utils/translations.dart';
+
+class AuthPage extends StatefulWidget {
+ const AuthPage({super.key, this.onSuccessfulAuth});
+
+ final void Function()? onSuccessfulAuth;
+
+ @override
+ State createState() => _AuthPageState();
+}
+
+class _AuthPageState extends State {
+
+ @override
+ Widget build(BuildContext context) {
+ return PopScope(
+ canPop: false,
+ onPopInvokedWithResult: (didPop, result) {
+ if (!didPop) {
+ SystemNavigator.pop();
+ }
+ },
+ child: Material(
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(Icons.security, size: 36),
+ const SizedBox(height: 16),
+ Text("Authentication Required".tl),
+ const SizedBox(height: 16),
+ FilledButton(
+ onPressed: auth,
+ child: Text("Continue".tl),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ void auth() async {
+ var localAuth = LocalAuthentication();
+ var canCheckBiometrics = await localAuth.canCheckBiometrics;
+ if (!canCheckBiometrics && !await localAuth.isDeviceSupported()) {
+ widget.onSuccessfulAuth?.call();
+ return;
+ }
+ var isAuthorized = await localAuth.authenticate(
+ localizedReason: "Please authenticate to continue".tl,
+ );
+ if (isAuthorized) {
+ widget.onSuccessfulAuth?.call();
+ }
+ }
+}
diff --git a/lib/pages/categories_page.dart b/lib/pages/categories_page.dart
index c55d1ac..fe2bb45 100644
--- a/lib/pages/categories_page.dart
+++ b/lib/pages/categories_page.dart
@@ -30,8 +30,15 @@ class CategoriesPage extends StatelessWidget {
.toList();
if(categories.isEmpty) {
+ var msg = "No Category Pages".tl;
+ msg += '\n';
+ if(ComicSource.isEmpty) {
+ msg += "Add a comic source in home page".tl;
+ } else {
+ msg += "Please check your settings".tl;
+ }
return NetworkError(
- message: "No Category Pages".tl,
+ message: msg,
retry: () {
controller.update();
},
@@ -248,36 +255,19 @@ class _CategoryPage extends StatelessWidget {
Widget buildTag(String tag, ClickTagCallback onClick,
[String? namespace, String? param]) {
- String translateTag(String tag) {
- /*
- // TODO: Implement translation
- if (enableTranslation) {
- if (namespace != null) {
- tag = TagsTranslation.translationTagWithNamespace(tag, namespace);
- } else {
- tag = tag.translateTagsToCN;
- }
- }
-
- */
- return tag;
- }
-
return Padding(
padding: const EdgeInsets.fromLTRB(8, 6, 8, 6),
child: Builder(
builder: (context) {
return Material(
- elevation: 0.6,
- borderRadius: const BorderRadius.all(Radius.circular(4)),
- color: context.colorScheme.surfaceContainerLow,
- surfaceTintColor: Colors.transparent,
+ borderRadius: const BorderRadius.all(Radius.circular(8)),
+ color: context.colorScheme.primaryContainer.withOpacity(0.72),
child: InkWell(
- borderRadius: const BorderRadius.all(Radius.circular(4)),
+ borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () => onClick(tag, param),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
- child: Text(translateTag(tag)),
+ child: Text(tag),
),
),
);
diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart
index c12e67c..081ad0f 100644
--- a/lib/pages/comic_page.dart
+++ b/lib/pages/comic_page.dart
@@ -327,7 +327,7 @@ class _ComicPageState extends LoadingState
}
Widget buildDescription() {
- if (comic.description == null) {
+ if (comic.description == null || comic.description!.trim().isEmpty) {
return const SliverPadding(padding: EdgeInsets.zero);
}
return SliverToBoxAdapter(
@@ -392,6 +392,27 @@ class _ComicPageState extends LoadingState
child: InkWell(
borderRadius: borderRadius,
onTap: onTap,
+ onLongPress: () {
+ Clipboard.setData(ClipboardData(text: text));
+ context.showMessage(message: "Copied".tl);
+ },
+ onSecondaryTapDown: (details) {
+ showMenuX(context, details.globalPosition, [
+ MenuEntry(
+ icon: Icons.remove_red_eye,
+ text: "View".tl,
+ onClick: onTap,
+ ),
+ MenuEntry(
+ icon: Icons.copy,
+ text: "Copy".tl,
+ onClick: () {
+ Clipboard.setData(ClipboardData(text: text));
+ context.showMessage(message: "Copied".tl);
+ },
+ ),
+ ]);
+ },
child: Text(text).padding(padding),
),
);
@@ -406,6 +427,26 @@ class _ComicPageState extends LoadingState
}
}
+ String formatTime(String time) {
+ if (int.tryParse(time) != null) {
+ var t = int.tryParse(time);
+ if (t! > 1000000000000) {
+ return DateTime.fromMillisecondsSinceEpoch(t)
+ .toString()
+ .substring(0, 19);
+ } else {
+ return DateTime.fromMillisecondsSinceEpoch(t * 1000)
+ .toString()
+ .substring(0, 19);
+ }
+ }
+ if (time.contains('T') || time.contains('Z')) {
+ var t = DateTime.parse(time);
+ return t.toString().substring(0, 19);
+ }
+ return time;
+ }
+
Widget buildWrap({required List children}) {
return Wrap(
runSpacing: 8,
@@ -464,14 +505,14 @@ class _ComicPageState extends LoadingState
buildWrap(
children: [
buildTag(text: 'Upload Time'.tl, isTitle: true),
- buildTag(text: comic.uploadTime!),
+ buildTag(text: formatTime(comic.uploadTime!)),
],
),
if (comic.updateTime != null)
buildWrap(
children: [
buildTag(text: 'Update Time'.tl, isTitle: true),
- buildTag(text: comic.updateTime!),
+ buildTag(text: formatTime(comic.updateTime!)),
],
),
const SizedBox(height: 12),
@@ -575,7 +616,7 @@ abstract mixin class _ComicPageActions {
void quickFavorite() {
var folder = appdata.settings['quickFavorite'];
- if(folder is! String) {
+ if (folder is! String) {
return;
}
LocalFavoritesManager().addComic(
@@ -1037,6 +1078,7 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
if (!isInitialLoading && next == null) {
return;
}
+ if (isLoading) return;
Future.microtask(() {
setState(() {
isLoading = true;
@@ -1609,10 +1651,12 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
const SizedBox(width: 16),
Expanded(
child: FilledButton(
- onPressed: selected.isEmpty ? null : () {
- widget.finishSelect(selected);
- context.pop();
- },
+ onPressed: selected.isEmpty
+ ? null
+ : () {
+ widget.finishSelect(selected);
+ context.pop();
+ },
child: Text("Download Selected".tl),
),
),
diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart
index 95c5f67..2ae6a71 100644
--- a/lib/pages/comic_source_page.dart
+++ b/lib/pages/comic_source_page.dart
@@ -40,7 +40,7 @@ class ComicSourcePage extends StatefulWidget {
}
controller?.close();
if (shouldUpdate.isEmpty) {
- if(!implicit) {
+ if (!implicit) {
App.rootContext.showMessage(message: "No Update Available".tl);
}
return;
@@ -55,10 +55,10 @@ class ComicSourcePage extends StatefulWidget {
title: "Updates Available".tl,
content: msg,
confirmText: "Update",
- onConfirm: () {
+ onConfirm: () async {
for (var key in shouldUpdate) {
var source = ComicSource.find(key);
- _BodyState.update(source!);
+ await _BodyState.update(source!);
}
},
);
@@ -95,24 +95,12 @@ class _BodyState extends State<_Body> {
return SmoothCustomScrollView(
slivers: [
buildCard(context),
- buildSettings(),
for (var source in ComicSource.all()) buildSource(context, source),
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)),
],
);
}
- Widget buildSettings() {
- return SliverToBoxAdapter(
- child: ListTile(
- leading: const Icon(Icons.update_outlined),
- title: Text("Check updates".tl),
- onTap: () => ComicSourcePage.checkComicSourceUpdate(false),
- trailing: const Icon(Icons.arrow_right),
- ),
- );
- }
-
Widget buildSource(BuildContext context, ComicSource source) {
return SliverToBoxAdapter(
child: Column(
@@ -181,11 +169,12 @@ class _BodyState extends State<_Body> {
trailing: Select(
current: (current as String).ts(source.key),
values: (item.value['options'] as List)
- .map(
- (e) => ((e['text'] ?? e['value']) as String).ts(source.key))
+ .map((e) =>
+ ((e['text'] ?? e['value']) as String).ts(source.key))
.toList(),
onTap: (i) {
- source.data['settings'][key] = item.value['options'][i]['value'];
+ source.data['settings'][key] =
+ item.value['options'][i]['value'];
source.saveData();
setState(() {});
},
@@ -209,7 +198,8 @@ class _BodyState extends State<_Body> {
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),
+ subtitle:
+ Text(current, maxLines: 1, overflow: TextOverflow.ellipsis),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
@@ -231,8 +221,7 @@ class _BodyState extends State<_Body> {
),
);
}
- }
- catch(e, s) {
+ } catch (e, s) {
Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s");
}
}
@@ -277,7 +266,7 @@ class _BodyState extends State<_Body> {
}
}
- static void update(ComicSource source) async {
+ static Future update(ComicSource source) async {
if (!source.url.isURL) {
App.rootContext.showMessage(message: "Invalid url config");
return;
@@ -305,55 +294,73 @@ class _BodyState extends State<_Body> {
}
Widget buildCard(BuildContext context) {
+ Widget buildButton({required Widget child, required VoidCallback onPressed}) {
+ return Button.normal(
+ onPressed: onPressed,
+ child: child,
+ ).fixHeight(32);
+ }
return SliverToBoxAdapter(
- child: Card.outlined(
- child: SizedBox(
- width: double.infinity,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- ListTile(
- title: Text("Add comic source".tl),
- leading: const Icon(Icons.dashboard_customize),
+ child: SizedBox(
+ width: double.infinity,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ListTile(
+ title: Text("Add comic source".tl),
+ leading: const Icon(Icons.dashboard_customize),
+ ),
+ TextField(
+ decoration: InputDecoration(
+ hintText: "URL",
+ border: const UnderlineInputBorder(),
+ contentPadding: const EdgeInsets.symmetric(horizontal: 12),
+ suffix: IconButton(
+ onPressed: () => handleAddSource(url),
+ icon: const Icon(Icons.check))),
+ onChanged: (value) {
+ url = value;
+ },
+ onSubmitted: handleAddSource,
+ ).paddingHorizontal(16).paddingBottom(8),
+ ListTile(
+ title: Text("Comic Source list".tl),
+ trailing: buildButton(
+ child: Text("View".tl),
+ onPressed: () {
+ showPopUpWidget(
+ App.rootContext,
+ _ComicSourceList(handleAddSource),
+ );
+ },
),
- TextField(
- decoration: InputDecoration(
- hintText: "URL",
- border: const UnderlineInputBorder(),
- contentPadding:
- const EdgeInsets.symmetric(horizontal: 12),
- suffix: IconButton(
- onPressed: () => handleAddSource(url),
- icon: const Icon(Icons.check))),
- onChanged: (value) {
- url = value;
- },
- onSubmitted: handleAddSource)
- .paddingHorizontal(16)
- .paddingBottom(32),
- Row(
- children: [
- TextButton(
- onPressed: _selectFile, child: Text("Select file".tl))
- .paddingLeft(8),
- const Spacer(),
- TextButton(
- onPressed: () {
- showPopUpWidget(
- App.rootContext, _ComicSourceList(handleAddSource));
- },
- child: Text("View list".tl)),
- const Spacer(),
- TextButton(onPressed: help, child: Text("Open help".tl))
- .paddingRight(8),
- ],
+ ),
+ ListTile(
+ title: Text("Use a config file".tl),
+ trailing: buildButton(
+ onPressed: _selectFile,
+ child: Text("Select".tl),
),
- const SizedBox(height: 8),
- ],
- ),
+ ),
+ ListTile(
+ title: Text("Help".tl),
+ trailing: buildButton(
+ onPressed: help,
+ child: Text("Open".tl),
+ ),
+ ),
+ ListTile(
+ title: Text("Check updates".tl),
+ trailing: buildButton(
+ onPressed: () => ComicSourcePage.checkComicSourceUpdate(false),
+ child: Text("Check".tl),
+ ),
+ ),
+ const SizedBox(height: 8),
+ ],
),
- ).paddingHorizontal(12),
+ ),
);
}
@@ -372,8 +379,7 @@ class _BodyState extends State<_Body> {
}
void help() {
- launchUrlString(
- "https://github.com/venera-app/venera/blob/master/doc/comic_source.md");
+ launchUrlString("https://github.com/venera-app/venera-configs");
}
Future handleAddSource(String url) async {
diff --git a/lib/pages/comments_page.dart b/lib/pages/comments_page.dart
index cc345e7..aa60868 100644
--- a/lib/pages/comments_page.dart
+++ b/lib/pages/comments_page.dart
@@ -1,8 +1,14 @@
+import 'dart:collection';
+
+import 'package:flutter/gestures.dart';
import 'package:flutter/material.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/image_provider/cached_image.dart';
+import 'package:venera/utils/app_links.dart';
+import 'package:venera/utils/ext.dart';
import 'package:venera/utils/translations.dart';
class CommentsPage extends StatefulWidget {
@@ -268,7 +274,10 @@ class _CommentTileState extends State<_CommentTile> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(widget.comment.userName, style: ts.bold,),
+ Text(
+ widget.comment.userName,
+ style: ts.bold,
+ ),
if (widget.comment.time != null)
Text(widget.comment.time!, style: ts.s12),
const SizedBox(height: 4),
@@ -426,7 +435,7 @@ class _CommentTileState extends State<_CommentTile> {
isCancel,
);
if (res.success) {
- if(isCancel) {
+ if (isCancel) {
voteStatus = 0;
} else {
if (isUp) {
@@ -498,6 +507,287 @@ class _CommentContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return SelectableText(text);
+ if (!text.contains('<') && !text.contains('http')) {
+ return SelectableText(text);
+ } else {
+ return _RichCommentContent(text: text);
+ }
+ }
+}
+
+class _Tag {
+ final String name;
+ final Map attributes;
+
+ const _Tag(this.name, this.attributes);
+
+ TextSpan merge(TextSpan s, BuildContext context) {
+ var style = s.style ?? ts;
+ style = switch (name) {
+ 'b' => style.bold,
+ 'i' => style.italic,
+ 'u' => style.underline,
+ 's' => style.lineThrough,
+ 'a' => style.withColor(context.colorScheme.primary),
+ 'span' => () {
+ if (attributes.containsKey('style')) {
+ var s = attributes['style']!;
+ var css = s.split(';');
+ for (var c in css) {
+ var kv = c.split(':');
+ if (kv.length == 2) {
+ var key = kv[0].trim();
+ var value = kv[1].trim();
+ switch (key) {
+ case 'color':
+ // Color is not supported, we should make text display well in light and dark mode.
+ break;
+ case 'font-weight':
+ if (value == 'bold') {
+ style = style.bold;
+ } else if (value == 'lighter') {
+ style = style.light;
+ }
+ break;
+ case 'font-style':
+ if (value == 'italic') {
+ style = style.italic;
+ }
+ break;
+ case 'text-decoration':
+ if (value == 'underline') {
+ style = style.underline;
+ } else if (value == 'line-through') {
+ style = style.lineThrough;
+ }
+ break;
+ case 'font-size':
+ // Font size is not supported.
+ break;
+ }
+ }
+ }
+ }
+ return style;
+ }(),
+ _ => style,
+ };
+ if (style.color != null) {
+ style = style.copyWith(decorationColor: style.color);
+ }
+ var recognizer = s.recognizer;
+ if (name == 'a') {
+ var link = attributes['href'];
+ if (link != null && link.isURL) {
+ recognizer = TapGestureRecognizer()
+ ..onTap = () {
+ handleLink(link);
+ };
+ }
+ }
+ return TextSpan(
+ text: s.text,
+ style: style,
+ recognizer: recognizer,
+ );
+ }
+
+ static void handleLink(String link) async {
+ if (link.isURL) {
+ if (await handleAppLink(Uri.parse(link))) {
+ App.rootContext.pop();
+ } else {
+ launchUrlString(link);
+ }
+ }
+ }
+}
+
+class _CommentImage {
+ final String url;
+ final String? link;
+
+ const _CommentImage(this.url, this.link);
+}
+
+class _RichCommentContent extends StatefulWidget {
+ const _RichCommentContent({required this.text});
+
+ final String text;
+
+ @override
+ State<_RichCommentContent> createState() => _RichCommentContentState();
+}
+
+class _RichCommentContentState extends State<_RichCommentContent> {
+ var textSpan = [];
+ var images = <_CommentImage>[];
+
+ @override
+ void didChangeDependencies() {
+ render();
+ super.didChangeDependencies();
+ }
+
+ bool isValidUrlChar(String char) {
+ return RegExp(r'[a-zA-Z0-9%:/.@\-_?&=#*!+;]').hasMatch(char);
+ }
+
+ void render() {
+ var s = Queue<_Tag>();
+
+ int i = 0;
+ var buffer = StringBuffer();
+ var text = widget.text;
+
+ void writeBuffer() {
+ if (buffer.isEmpty) return;
+ var span = TextSpan(text: buffer.toString());
+ for (var tag in s) {
+ span = tag.merge(span, context);
+ }
+ textSpan.add(span);
+ buffer.clear();
+ }
+
+ while (i < text.length) {
+ if (text[i] == '<' && i != text.length - 1) {
+ if (text[i + 1] != '/') {
+ // start tag
+ var j = text.indexOf('>', i);
+ if (j != -1) {
+ var tagContent = text.substring(i + 1, j);
+ var splits = tagContent.split(' ');
+ splits.removeWhere((element) => element.isEmpty);
+ var tagName = splits[0];
+ var attributes = {};
+ for (var k = 1; k < splits.length; k++) {
+ var attr = splits[k];
+ var attrSplits = attr.split('=');
+ if (attrSplits.length == 2) {
+ attributes[attrSplits[0]] = attrSplits[1].replaceAll('"', '');
+ }
+ }
+ const acceptedTags = ['img', 'a', 'b', 'i', 'u', 's', 'br', 'span'];
+ if (acceptedTags.contains(tagName)) {
+ writeBuffer();
+ if (tagName == 'img') {
+ var url = attributes['src'];
+ String? link;
+ for (var tag in s) {
+ if (tag.name == 'a') {
+ link = tag.attributes['href'];
+ break;
+ }
+ }
+ if (url != null) {
+ images.add(_CommentImage(url, link));
+ }
+ } else if (tagName == 'br') {
+ buffer.write('\n');
+ } else {
+ s.add(_Tag(tagName, attributes));
+ }
+ i = j + 1;
+ continue;
+ }
+ }
+ } else {
+ // end tag
+ var j = text.indexOf('>', i);
+ if (j != -1) {
+ var tagContent = text.substring(i + 2, j);
+ var splits = tagContent.split(' ');
+ splits.removeWhere((element) => element.isEmpty);
+ var tagName = splits[0];
+ if (s.isNotEmpty && s.last.name == tagName) {
+ writeBuffer();
+ s.removeLast();
+ i = j + 1;
+ continue;
+ }
+ if (tagName == 'br') {
+ i = j + 1;
+ buffer.write('\n');
+ continue;
+ }
+ }
+ }
+ } else if (text.length - i > 8 &&
+ text.substring(i, i + 4) == 'http' &&
+ !s.any((e) => e.name == 'a')) {
+ // auto link
+ int j = i;
+ for (; j < text.length; j++) {
+ if (!isValidUrlChar(text[j])) {
+ break;
+ }
+ }
+ var url = text.substring(i, j);
+ if (url.isURL) {
+ writeBuffer();
+ textSpan.add(TextSpan(
+ text: url,
+ style: ts.withColor(context.colorScheme.primary),
+ recognizer: TapGestureRecognizer()
+ ..onTap = () {
+ _Tag.handleLink(url);
+ },
+ ));
+ i = j;
+ continue;
+ }
+ }
+ buffer.write(text[i]);
+ i++;
+ }
+ writeBuffer();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ Widget content = SelectableText.rich(
+ TextSpan(
+ style: DefaultTextStyle.of(context).style,
+ children: textSpan,
+ ),
+ );
+ if (images.isNotEmpty) {
+ content = Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ content,
+ Wrap(
+ runSpacing: 4,
+ spacing: 4,
+ children: images.map((e) {
+ Widget image = Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(8),
+ color: Theme.of(context).colorScheme.surfaceContainerLow,
+ ),
+ width: 100,
+ height: 100,
+ child: Image(
+ width: 100,
+ height: 100,
+ image: CachedImageProvider(e.url),
+ ),
+ );
+ if (e.link != null) {
+ image = InkWell(
+ onTap: () {
+ _Tag.handleLink(e.link!);
+ },
+ child: image,
+ );
+ }
+ return image;
+ }).toList(),
+ )
+ ],
+ );
+ }
+ return content;
}
}
diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart
index d575b7f..125bd7d 100644
--- a/lib/pages/explore_page.dart
+++ b/lib/pages/explore_page.dart
@@ -93,8 +93,15 @@ class _ExplorePageState extends State
Widget buildBody(String i) => _SingleExplorePage(i, key: Key(i));
Widget buildEmpty() {
+ var msg = "No Explore Pages".tl;
+ msg += '\n';
+ if(ComicSource.isEmpty) {
+ msg += "Add a comic source in home page".tl;
+ } else {
+ msg += "Please check your settings".tl;
+ }
return NetworkError(
- message: "No Explore Pages".tl,
+ message: msg,
retry: () {
setState(() {
pages = ComicSource.all()
diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart
index 1cb8847..e19686f 100644
--- a/lib/pages/favorites/favorite_actions.dart
+++ b/lib/pages/favorites/favorite_actions.dart
@@ -288,3 +288,178 @@ Future sortFolders() async {
LocalFavoritesManager().updateOrder(folders);
}
+
+Future importNetworkFolder(
+ String source,
+ String? folder,
+ String? folderID,
+) async {
+ var comicSource = ComicSource.find(source);
+ if (comicSource == null) {
+ return;
+ }
+ if(folder != null && folder.isEmpty) {
+ folder = null;
+ }
+ var resultName = folder ?? comicSource.name;
+ var exists = LocalFavoritesManager().existsFolder(resultName);
+ if (exists) {
+ if (!LocalFavoritesManager()
+ .isLinkedToNetworkFolder(resultName, source, folderID ?? "")) {
+ App.rootContext.showMessage(message: "Folder already exists".tl);
+ return;
+ }
+ }
+ if(!exists) {
+ LocalFavoritesManager().createFolder(resultName);
+ LocalFavoritesManager().linkFolderToNetwork(
+ resultName,
+ source,
+ folderID ?? "",
+ );
+ }
+
+ var current = 0;
+ var isFinished = false;
+ String? next;
+
+ Future fetchNext() async {
+ var retry = 3;
+
+ while (true) {
+ try {
+ if (comicSource.favoriteData?.loadComic != null) {
+ next ??= '1';
+ var page = int.parse(next!);
+ var res = await comicSource.favoriteData!.loadComic!(page, folderID);
+ var count = 0;
+ for (var c in res.data) {
+ var result = LocalFavoritesManager().addComic(
+ resultName,
+ FavoriteItem(
+ id: c.id,
+ name: c.title,
+ coverPath: c.cover,
+ type: ComicType(source.hashCode),
+ author: c.subtitle ?? '',
+ tags: c.tags ?? [],
+ ),
+ );
+ if (result) {
+ count++;
+ }
+ }
+ current += count;
+ if (res.data.isEmpty || res.subData == page) {
+ isFinished = true;
+ next = null;
+ } else {
+ next = (page + 1).toString();
+ }
+ } else if (comicSource.favoriteData?.loadNext != null) {
+ var res = await comicSource.favoriteData!.loadNext!(next, folderID);
+ var count = 0;
+ for (var c in res.data) {
+ var result = LocalFavoritesManager().addComic(
+ resultName,
+ FavoriteItem(
+ id: c.id,
+ name: c.title,
+ coverPath: c.cover,
+ type: ComicType(source.hashCode),
+ author: c.subtitle ?? '',
+ tags: c.tags ?? [],
+ ),
+ );
+ if (result) {
+ count++;
+ }
+ }
+ current += count;
+ if (res.data.isEmpty || res.subData == null) {
+ isFinished = true;
+ next = null;
+ } else {
+ next = res.subData;
+ }
+ } else {
+ throw "Unsupported source";
+ }
+ return;
+ } catch (e) {
+ retry--;
+ if (retry == 0) {
+ rethrow;
+ }
+ continue;
+ }
+ }
+ }
+
+ bool isCanceled = false;
+ String? errorMsg;
+ bool isErrored() => errorMsg != null;
+
+ void Function()? updateDialog;
+
+ showDialog(
+ context: App.rootContext,
+ builder: (context) {
+ return StatefulBuilder(
+ builder: (context, setState) {
+ updateDialog = () => setState(() {});
+ return ContentDialog(
+ title: isFinished
+ ? "Finished".tl
+ : isErrored()
+ ? "Error".tl
+ : "Importing".tl,
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 4),
+ LinearProgressIndicator(
+ value: isFinished ? 1 : null,
+ ),
+ const SizedBox(height: 4),
+ Text("Imported @c comics".tlParams({
+ "c": current,
+ })),
+ const SizedBox(height: 4),
+ if (isErrored()) Text("Error: $errorMsg"),
+ ],
+ ).paddingHorizontal(16),
+ actions: [
+ Button.filled(
+ color: (isFinished || isErrored())
+ ? null
+ : context.colorScheme.error,
+ onPressed: () {
+ isCanceled = true;
+ context.pop();
+ },
+ child: (isFinished || isErrored())
+ ? Text("OK".tl)
+ : Text("Cancel".tl),
+ ),
+ ],
+ );
+ },
+ );
+ },
+ ).then((_) {
+ isCanceled = true;
+ });
+
+ while (!isFinished && !isCanceled) {
+ try {
+ await fetchNext();
+ updateDialog?.call();
+ } catch (e) {
+ errorMsg = e.toString();
+ updateDialog?.call();
+ break;
+ }
+ }
+}
diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart
index c380a9a..f815506 100644
--- a/lib/pages/favorites/favorites_page.dart
+++ b/lib/pages/favorites/favorites_page.dart
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:math';
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
import 'package:venera/components/components.dart';
diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart
index 4bbef38..460aedd 100644
--- a/lib/pages/favorites/local_favorites_page.dart
+++ b/lib/pages/favorites/local_favorites_page.dart
@@ -14,6 +14,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
late List comics;
+ String? networkSource;
+ String? networkFolder;
+
void updateComics() {
setState(() {
comics = LocalFavoritesManager().getAllComics(widget.folder);
@@ -24,6 +27,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
void initState() {
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
comics = LocalFavoritesManager().getAllComics(widget.folder);
+ var (a, b) = LocalFavoritesManager().findLinked(widget.folder);
+ networkSource = a;
+ networkFolder = b;
super.initState();
}
@@ -49,6 +55,51 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
child: Text(favPage.folder ?? "Unselected".tl),
),
actions: [
+ if (networkSource != null)
+ Tooltip(
+ message: "Sync".tl,
+ child: Flyout(
+ flyoutBuilder: (context) {
+ var sourceName = ComicSource.find(networkSource!)?.name ??
+ networkSource!;
+ var text = "The folder is Linked to @source".tlParams({
+ "source": sourceName,
+ });
+ if(networkFolder != null && networkFolder!.isNotEmpty) {
+ text += "\n${"Source Folder".tl}: $networkFolder";
+ }
+ return FlyoutContent(
+ title: "Sync".tl,
+ content: Text(text),
+ actions: [
+ Button.filled(
+ child: Text("Update".tl),
+ onPressed: () {
+ context.pop();
+ importNetworkFolder(
+ networkSource!,
+ widget.folder,
+ networkFolder!,
+ ).then(
+ (value) {
+ updateComics();
+ },
+ );
+ },
+ ),
+ ],
+ );
+ },
+ child: Builder(builder: (context) {
+ return IconButton(
+ icon: const Icon(Icons.sync),
+ onPressed: () {
+ Flyout.of(context).show();
+ },
+ );
+ }),
+ ),
+ ),
MenuButton(
entries: [
MenuEntry(
@@ -136,7 +187,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
});
}),
MenuEntry(
- icon: Icons.update,
+ icon: Icons.download,
text: "Download All".tl,
onClick: () async {
int count = 0;
diff --git a/lib/pages/favorites/network_favorites_page.dart b/lib/pages/favorites/network_favorites_page.dart
index d08d1fc..b842d55 100644
--- a/lib/pages/favorites/network_favorites_page.dart
+++ b/lib/pages/favorites/network_favorites_page.dart
@@ -108,6 +108,17 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> {
onTap: context.width < _kTwoPanelChangeWidth ? showFolders : null,
child: Text(widget.data.title),
),
+ actions: [
+ MenuButton(entries: [
+ MenuEntry(
+ icon: Icons.sync,
+ text: "Convert to local".tl,
+ onClick: () {
+ importNetworkFolder(widget.data.key, null, null);
+ },
+ )
+ ]),
+ ],
),
errorLeading: Appbar(
leading: Tooltip(
@@ -533,6 +544,17 @@ class _FavoriteFolder extends StatelessWidget {
key: comicListKey,
leadingSliver: SliverAppbar(
title: Text(title),
+ actions: [
+ MenuButton(entries: [
+ MenuEntry(
+ icon: Icons.sync,
+ text: "Convert to local".tl,
+ onClick: () {
+ importNetworkFolder(data.key, title, folderID);
+ },
+ )
+ ]),
+ ],
),
errorLeading: Appbar(
title: Text(title),
diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart
index f73446c..097b9de 100644
--- a/lib/pages/home_page.dart
+++ b/lib/pages/home_page.dart
@@ -89,11 +89,13 @@ class _SyncDataWidget extends StatefulWidget {
State<_SyncDataWidget> createState() => _SyncDataWidgetState();
}
-class _SyncDataWidgetState extends State<_SyncDataWidget> {
+class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
DataSync().addListener(update);
+ WidgetsBinding.instance.addObserver(this);
+ lastCheck = DateTime.now();
}
void update() {
@@ -106,6 +108,20 @@ class _SyncDataWidgetState extends State<_SyncDataWidget> {
void dispose() {
super.dispose();
DataSync().removeListener(update);
+ WidgetsBinding.instance.removeObserver(this);
+ }
+
+ late DateTime lastCheck;
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ super.didChangeAppLifecycleState(state);
+ if(state == AppLifecycleState.resumed) {
+ if(DateTime.now().difference(lastCheck) > const Duration(minutes: 10)) {
+ lastCheck = DateTime.now();
+ DataSync().downloadData();
+ }
+ }
}
@override
diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart
index df36314..5a828de 100644
--- a/lib/pages/reader/reader.dart
+++ b/lib/pages/reader/reader.dart
@@ -12,6 +12,7 @@ import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:venera/components/components.dart';
+import 'package:venera/components/custom_slider.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/cache_manager.dart';
@@ -244,7 +245,9 @@ abstract mixin class _ReaderLocation {
bool toPage(int page) {
if (_validatePage(page)) {
if (page == this.page) {
- return false;
+ if(!(chapter == 1 && page == 1) && !(chapter == maxChapter && page == maxPage)) {
+ return false;
+ }
}
this.page = page;
update();
diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart
index 69fb918..95043c3 100644
--- a/lib/pages/reader/scaffold.dart
+++ b/lib/pages/reader/scaffold.dart
@@ -18,6 +18,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
bool get isOpen => _isOpen;
+ bool get isReversed => context.reader.mode == ReaderMode.galleryRightToLeft ||
+ context.reader.mode == ReaderMode.continuousRightToLeft;
+
int showFloatingButtonValue = 0;
var lastValue = 0;
@@ -217,34 +220,26 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
children: [
const SizedBox(width: 8),
IconButton.filledTonal(
- onPressed: () {
- if (!context.reader.toPrevChapter()) {
- context.reader.toPage(1);
- } else {
- if (showFloatingButtonValue != 0) {
- setState(() {
- showFloatingButtonValue = 0;
- });
- }
- }
- },
+ onPressed: () => !isReversed
+ ? context.reader.chapter > 1
+ ? context.reader.toPrevChapter()
+ : context.reader.toPage(1)
+ : context.reader.chapter < context.reader.maxChapter
+ ? context.reader.toNextChapter()
+ : context.reader.toPage(context.reader.maxPage),
icon: const Icon(Icons.first_page),
),
Expanded(
child: buildSlider(),
),
IconButton.filledTonal(
- onPressed: () {
- if (!context.reader.toNextChapter()) {
- context.reader.toPage(context.reader.maxPage);
- } else {
- if (showFloatingButtonValue != 0) {
- setState(() {
- showFloatingButtonValue = 0;
- });
- }
- }
- },
+ onPressed: () => !isReversed
+ ? context.reader.chapter < context.reader.maxChapter
+ ? context.reader.toNextChapter()
+ : context.reader.toPage(context.reader.maxPage)
+ : context.reader.chapter > 1
+ ? context.reader.toPrevChapter()
+ : context.reader.toPage(1),
icon: const Icon(Icons.last_page)),
const SizedBox(
width: 8,
@@ -379,12 +374,13 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
var sliderFocus = FocusNode();
Widget buildSlider() {
- return Slider(
+ return CustomSlider(
focusNode: sliderFocus,
value: context.reader.page.toDouble(),
min: 1,
max:
context.reader.maxPage.clamp(context.reader.page, 1 << 16).toDouble(),
+ reversed: isReversed,
divisions: (context.reader.maxPage - 1).clamp(2, 1 << 16),
onChanged: (i) {
context.reader.toPage(i.toInt());
diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart
index 6bfc070..735c7e5 100644
--- a/lib/pages/search_page.dart
+++ b/lib/pages/search_page.dart
@@ -305,13 +305,24 @@ class _SearchPageState extends State {
),
);
}
- return ListTile(
- contentPadding: const EdgeInsets.symmetric(horizontal: 12),
- title: Text(appdata.searchHistory[index - 2]),
+ return InkWell(
onTap: () {
search(appdata.searchHistory[index - 2]);
},
- );
+ child: Container(
+ decoration: BoxDecoration(
+ // color: context.colorScheme.surfaceContainer,
+ border: Border(
+ left: BorderSide(
+ color: context.colorScheme.outlineVariant,
+ width: 2,
+ ),
+ ),
+ ),
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ child: Text(appdata.searchHistory[index - 2], style: ts.s14),
+ ),
+ ).paddingBottom(8).paddingHorizontal(4);
},
childCount: 2 + appdata.searchHistory.length,
),
@@ -490,7 +501,7 @@ class SearchOptionWidget extends StatelessWidget {
contentPadding: EdgeInsets.zero,
title: Text(option.label.ts(sourceKey)),
),
- if(option.type == 'select')
+ if (option.type == 'select')
Wrap(
runSpacing: 8,
spacing: 8,
@@ -504,7 +515,7 @@ class SearchOptionWidget extends StatelessWidget {
);
}).toList(),
),
- if(option.type == 'multi-select')
+ if (option.type == 'multi-select')
Wrap(
runSpacing: 8,
spacing: 8,
@@ -514,7 +525,7 @@ class SearchOptionWidget extends StatelessWidget {
isSelected: (jsonDecode(value) as List).contains(e.key),
onTap: () {
var list = jsonDecode(value) as List;
- if(list.contains(e.key)) {
+ if (list.contains(e.key)) {
list.remove(e.key);
} else {
list.add(e.key);
@@ -524,7 +535,7 @@ class SearchOptionWidget extends StatelessWidget {
);
}).toList(),
),
- if(option.type == 'dropdown')
+ if (option.type == 'dropdown')
Select(
current: option.options[value],
values: option.options.values.toList(),
diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart
index c491f51..b35301f 100644
--- a/lib/pages/settings/app.dart
+++ b/lib/pages/settings/app.dart
@@ -36,12 +36,12 @@ class _AppSettingsState extends State {
if (App.isAndroid) {
var channel = const MethodChannel("venera/storage");
var permission = await channel.invokeMethod('');
- if(permission != true) {
+ if (permission != true) {
context.showMessage(message: "Permission denied".tl);
return;
}
var path = await selectDirectory();
- if(path != null) {
+ if (path != null) {
// check if the path is writable
var testFile = File(FilePath.join(path, "test"));
try {
@@ -177,6 +177,29 @@ class _AppSettingsState extends State {
App.forceRebuild();
},
).toSliver(),
+ if (!App.isLinux)
+ _SwitchSetting(
+ title: "Authorization Required".tl,
+ settingKey: "authorizationRequired",
+ onChanged: () async {
+ var current = appdata.settings['authorizationRequired'];
+ if (current) {
+ final auth = LocalAuthentication();
+ final bool canAuthenticateWithBiometrics =
+ await auth.canCheckBiometrics;
+ final bool canAuthenticate = canAuthenticateWithBiometrics ||
+ await auth.isDeviceSupported();
+ if (!canAuthenticate) {
+ context.showMessage(message: "Biometrics not supported".tl);
+ setState(() {
+ appdata.settings['authorizationRequired'] = false;
+ });
+ appdata.saveData();
+ return;
+ }
+ }
+ },
+ ).toSliver(),
],
);
}
diff --git a/lib/pages/settings/network.dart b/lib/pages/settings/network.dart
index 21042ea..9396b7e 100644
--- a/lib/pages/settings/network.dart
+++ b/lib/pages/settings/network.dart
@@ -38,14 +38,11 @@ class _ProxySettingView extends StatefulWidget {
class _ProxySettingViewState extends State<_ProxySettingView> {
String type = '';
-
String host = '';
-
String port = '';
-
String username = '';
-
String password = '';
+ bool ignoreCertificateErrors = false;
// USERNAME:PASSWORD@HOST:PORT
String toProxyStr() {
@@ -103,6 +100,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
void initState() {
var proxy = appdata.settings['proxy'];
parseProxyString(proxy);
+ ignoreCertificateErrors = appdata.settings['ignoreCertificateErrors'] ?? false;
super.initState();
}
@@ -148,6 +146,17 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
},
),
if (type == 'manual') buildManualProxy(),
+ SwitchListTile(
+ title: Text("Ignore Certificate Errors".tl),
+ value: ignoreCertificateErrors,
+ onChanged: (v) {
+ setState(() {
+ ignoreCertificateErrors = v;
+ });
+ appdata.settings['ignoreCertificateErrors'] = ignoreCertificateErrors;
+ appdata.saveData();
+ },
+ ),
],
),
),
diff --git a/lib/pages/settings/setting_components.dart b/lib/pages/settings/setting_components.dart
index ee14ddc..6e43be1 100644
--- a/lib/pages/settings/setting_components.dart
+++ b/lib/pages/settings/setting_components.dart
@@ -33,9 +33,10 @@ class _SwitchSettingState extends State<_SwitchSetting> {
onChanged: (value) {
setState(() {
appdata.settings[widget.settingKey] = value;
- appdata.saveData();
});
- widget.onChanged?.call();
+ appdata.saveData().then((_) {
+ widget.onChanged?.call();
+ });
},
),
);
@@ -133,7 +134,9 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> {
builder: (context) {
return ContentDialog(
title: "Help".tl,
- content: Text(widget.help!).paddingHorizontal(16).fixWidth(double.infinity),
+ content: Text(widget.help!)
+ .paddingHorizontal(16)
+ .fixWidth(double.infinity),
actions: [
Button.filled(
onPressed: context.pop,
@@ -158,8 +161,9 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> {
var rect = offset & size;
showMenu(
elevation: 3,
- color: context.colorScheme.surface,
- surfaceTintColor: Colors.transparent,
+ color: context.brightness == Brightness.light
+ ? const Color(0xFFF6F6F6)
+ : const Color(0xFF1E1E1E),
context: context,
position: RelativeRect.fromRect(
rect,
@@ -229,7 +233,9 @@ class _EndSelectorSelectSettingState extends State<_EndSelectorSelectSetting> {
builder: (context) {
return ContentDialog(
title: "Help".tl,
- content: Text(widget.help!).paddingHorizontal(16).fixWidth(double.infinity),
+ content: Text(widget.help!)
+ .paddingHorizontal(16)
+ .fixWidth(double.infinity),
actions: [
Button.filled(
onPressed: context.pop,
@@ -458,24 +464,31 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
}
});
showDialog(
- context: context,
- builder: (context) {
- return SimpleDialog(
- title: const Text("Add"),
+ context: context,
+ builder: (context) {
+ return ContentDialog(
+ title: "Add".tl,
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
children: canAdd.entries
- .map((e) => InkWell(
- child: ListTile(title: Text(e.value), key: Key(e.key)),
- onTap: () {
- context.pop();
- setState(() {
- keys.add(e.key);
- });
- updateSetting();
- },
- ))
+ .map(
+ (e) => ListTile(
+ title: Text(e.value),
+ key: Key(e.key),
+ onTap: () {
+ context.pop();
+ setState(() {
+ keys.add(e.key);
+ });
+ updateSetting();
+ },
+ ),
+ )
.toList(),
- );
- });
+ ),
+ );
+ },
+ );
}
void updateSetting() {
diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart
index 889a7c6..04a0215 100644
--- a/lib/pages/settings/settings_page.dart
+++ b/lib/pages/settings/settings_page.dart
@@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
+import 'package:local_auth/local_auth.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.dart';
diff --git a/lib/utils/app_links.dart b/lib/utils/app_links.dart
index 2e36be3..e6947c7 100644
--- a/lib/utils/app_links.dart
+++ b/lib/utils/app_links.dart
@@ -10,7 +10,7 @@ void handleLinks() {
});
}
-void handleAppLink(Uri uri) async {
+Future handleAppLink(Uri uri) async {
for(var source in ComicSource.all()) {
if(source.linkHandler != null) {
if(source.linkHandler!.domains.contains(uri.host)) {
@@ -22,9 +22,11 @@ void handleAppLink(Uri uri) async {
App.mainNavigatorKey!.currentContext?.to(() {
return ComicPage(id: id, sourceKey: source.key);
});
+ return true;
}
- return;
+ return false;
}
}
}
+ return false;
}
\ No newline at end of file
diff --git a/lib/utils/io.dart b/lib/utils/io.dart
index 8ddf9c6..5e41265 100644
--- a/lib/utils/io.dart
+++ b/lib/utils/io.dart
@@ -190,7 +190,6 @@ class IOSDirectoryPicker {
final String? path = await _channel.invokeMethod('selectDirectory');
return path;
} catch (e) {
- print("Error selecting directory: $e");
// 返回报错信息
return e.toString();
}
diff --git a/pubspec.lock b/pubspec.lock
index 88ac6d3..1d9b852 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -356,6 +356,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_plugin_android_lifecycle:
+ dependency: transitive
+ description:
+ name: flutter_plugin_android_lifecycle
+ sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.23"
flutter_qjs:
dependency: "direct main"
description:
@@ -512,6 +520,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
+ local_auth:
+ dependency: "direct main"
+ description:
+ name: local_auth
+ sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.0"
+ local_auth_android:
+ dependency: transitive
+ description:
+ name: local_auth_android
+ sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.46"
+ local_auth_darwin:
+ dependency: transitive
+ description:
+ name: local_auth_darwin
+ sha256: "6d2950da311d26d492a89aeb247c72b4653ddc93601ea36a84924a396806d49c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.1"
+ local_auth_platform_interface:
+ dependency: transitive
+ description:
+ name: local_auth_platform_interface
+ sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.10"
+ local_auth_windows:
+ dependency: transitive
+ description:
+ name: local_auth_windows
+ sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.11"
lodepng_flutter:
dependency: "direct main"
description:
@@ -988,4 +1036,4 @@ packages:
version: "0.0.1"
sdks:
dart: ">=3.5.4 <4.0.0"
- flutter: ">=3.24.4"
+ flutter: ">=3.24.5"
diff --git a/pubspec.yaml b/pubspec.yaml
index 89e929b..0c27fe0 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -2,11 +2,11 @@ name: venera
description: "A comic app."
publish_to: 'none'
-version: 1.0.5+105
+version: 1.0.6+106
environment:
sdk: '>=3.5.0 <4.0.0'
- flutter: 3.24.4
+ flutter: 3.24.5
dependencies:
flutter:
@@ -64,6 +64,7 @@ dependencies:
url: https://github.com/wgh136/webdav_client
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
battery_plus: ^6.2.0
+ local_auth: ^2.3.0
dev_dependencies:
flutter_test:
diff --git a/windows/build.iss b/windows/build.iss
index 01d6c2b..3f9953b 100644
--- a/windows/build.iss
+++ b/windows/build.iss
@@ -33,7 +33,7 @@ WizardStyle=modern
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
-Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
+Name: "chinesesimplified"; MessagesFile: "{#RootPath}\windows\ChineseSimplified.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
@@ -54,6 +54,7 @@ Source: "{#RootPath}\build\windows\x64\runner\Release\url_launcher_windows_plugi
Source: "{#RootPath}\build\windows\x64\runner\Release\battery_plus_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\screen_retriever_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\window_manager_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "{#RootPath}\build\windows\x64\runner\Release\local_auth_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\zip_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#RootPath}\build\windows\x64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
diff --git a/windows/build.py b/windows/build.py
index 55bb0c1..c3d1a4f 100644
--- a/windows/build.py
+++ b/windows/build.py
@@ -1,5 +1,6 @@
import subprocess
import os
+import httpx
file = open('pubspec.yaml', 'r')
content = file.read()
@@ -26,6 +27,13 @@ file = open('windows/build.iss', 'w')
file.write(newContent)
file.close()
+if not os.path.exists("windows/ChineseSimplified.isl"):
+ # download ChineseSimplified.isl
+ url = "https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/refs/heads/main/ChineseSimplified.isl"
+ response = httpx.get(url)
+ with open('windows/ChineseSimplified.isl', 'wb') as file:
+ file.write(response.content)
+
subprocess.run(["iscc", "windows/build.iss"], shell=True)
with open('windows/build.iss', 'w') as file: