mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
33
.github/workflows/linux.yml
vendored
33
.github/workflows/linux.yml
vendored
@@ -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/
|
|
80
.github/workflows/main.yml
vendored
80
.github/workflows/main.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
name: Build IOS
|
name: Build ALL
|
||||||
run-name: Build IOS
|
run-name: Build ALL
|
||||||
on:
|
on:
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
jobs:
|
jobs:
|
||||||
@@ -63,3 +63,79 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: app-ios.ipa
|
name: app-ios.ipa
|
||||||
path: /Users/runner/work/venera/venera/build/ios/iphoneos/venera-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/
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||||
<application
|
<application
|
||||||
android:label="venera"
|
android:label="venera"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
@@ -1,101 +1,69 @@
|
|||||||
package com.github.wgh136.venera
|
package com.github.wgh136.venera
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.Manifest
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.provider.DocumentsContract
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import androidx.activity.result.ActivityResultCallback
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import dev.flutter.packages.file_selector_android.FileUtils
|
||||||
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import io.flutter.plugins.GeneratedPluginRegistrant
|
import io.flutter.plugins.GeneratedPluginRegistrant
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.lang.Exception
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterFragmentActivity() {
|
||||||
var volumeListen = VolumeListen()
|
var volumeListen = VolumeListen()
|
||||||
var listening = false
|
var listening = false
|
||||||
|
|
||||||
private val pickDirectoryCode = 1
|
|
||||||
|
|
||||||
private lateinit var result: MethodChannel.Result
|
|
||||||
|
|
||||||
private val storageRequestCode = 0x10
|
private val storageRequestCode = 0x10
|
||||||
private var storagePermissionRequest: ((Boolean) -> Unit)? = null
|
private var storagePermissionRequest: ((Boolean) -> Unit)? = null
|
||||||
|
|
||||||
private val selectFileCode = 0x11
|
private val nextLocalRequestCode = AtomicInteger()
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
private fun <I, O> startContractForResult(
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
contract: ActivityResultContract<I, O>,
|
||||||
if (requestCode == pickDirectoryCode) {
|
input: I,
|
||||||
if(resultCode != Activity.RESULT_OK) {
|
callback: ActivityResultCallback<O>
|
||||||
result.success(null)
|
) {
|
||||||
return
|
val key = "activity_rq_for_result#${nextLocalRequestCode.getAndIncrement()}"
|
||||||
|
val registry = activityResultRegistry
|
||||||
|
var launcher: ActivityResultLauncher<I>? = null
|
||||||
|
val observer = object : LifecycleEventObserver {
|
||||||
|
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||||
|
if (Lifecycle.Event.ON_DESTROY == event) {
|
||||||
|
launcher?.unregister()
|
||||||
|
lifecycle.removeObserver(this)
|
||||||
}
|
}
|
||||||
val pickedDirectoryUri = data?.data
|
|
||||||
if (pickedDirectoryUri == null) {
|
|
||||||
result.success(null)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
Thread {
|
|
||||||
try {
|
|
||||||
result.success(onPickedDirectory(pickedDirectoryUri))
|
|
||||||
}
|
}
|
||||||
catch (e: Exception) {
|
lifecycle.addObserver(observer)
|
||||||
result.error("Failed to Copy Files", e.toString(), null)
|
val newCallback = ActivityResultCallback<O> {
|
||||||
}
|
launcher?.unregister()
|
||||||
}.start()
|
lifecycle.removeObserver(observer)
|
||||||
} else if (requestCode == storageRequestCode) {
|
callback.onActivityResult(it)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
launcher = registry.register(key, contract, newCallback)
|
||||||
|
launcher.launch(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
@@ -115,12 +83,27 @@ class MainActivity : FlutterActivity() {
|
|||||||
}
|
}
|
||||||
res.success(null)
|
res.success(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
"getDirectoryPath" -> {
|
"getDirectoryPath" -> {
|
||||||
this.result = res
|
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
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)
|
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()
|
else -> res.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,6 +120,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
events.success(2)
|
events.success(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCancel(arguments: Any?) {
|
override fun onCancel(arguments: Any?) {
|
||||||
listening = false
|
listening = false
|
||||||
}
|
}
|
||||||
@@ -144,15 +128,14 @@ class MainActivity : FlutterActivity() {
|
|||||||
|
|
||||||
val storageChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/storage")
|
val storageChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/storage")
|
||||||
storageChannel.setMethodCallHandler { _, res ->
|
storageChannel.setMethodCallHandler { _, res ->
|
||||||
requestStoragePermission {result ->
|
requestStoragePermission { result ->
|
||||||
res.success(result)
|
res.success(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectFileChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/select_file")
|
val selectFileChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/select_file")
|
||||||
selectFileChannel.setMethodCallHandler { _, res ->
|
selectFileChannel.setMethodCallHandler { _, res ->
|
||||||
openFile()
|
openFile(res)
|
||||||
result = res
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,12 +150,13 @@ class MainActivity : FlutterActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
if(listening){
|
if (listening) {
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||||
volumeListen.down()
|
volumeListen.down()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||||
volumeListen.up()
|
volumeListen.up()
|
||||||
return true
|
return true
|
||||||
@@ -184,19 +168,30 @@ class MainActivity : FlutterActivity() {
|
|||||||
|
|
||||||
/// copy the directory to tmp directory, return copied directory
|
/// copy the directory to tmp directory, return copied directory
|
||||||
private fun onPickedDirectory(uri: Uri): String {
|
private fun onPickedDirectory(uri: Uri): String {
|
||||||
val contentResolver = context.contentResolver
|
if (!hasStoragePermission()) {
|
||||||
var tmp = context.cacheDir
|
// 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 = File(tmp, "getDirectoryPathTemp")
|
||||||
tmp.mkdir()
|
tmp.mkdir()
|
||||||
|
Thread {
|
||||||
copyDirectory(contentResolver, uri, tmp)
|
copyDirectory(contentResolver, uri, tmp)
|
||||||
|
}.start()
|
||||||
|
|
||||||
return tmp.absolutePath
|
return tmp.absolutePath
|
||||||
|
} else {
|
||||||
|
val docId = DocumentsContract.getTreeDocumentId(uri)
|
||||||
|
val split: Array<String?> = 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) {
|
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()) {
|
for (file in src.listFiles()) {
|
||||||
if(file.isDirectory) {
|
if (file.isDirectory) {
|
||||||
val newDir = File(destDir, file.name!!)
|
val newDir = File(destDir, file.name!!)
|
||||||
newDir.mkdir()
|
newDir.mkdir()
|
||||||
copyDirectory(resolver, file.uri, newDir)
|
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) {
|
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(
|
val readPermission = ContextCompat.checkSelfPermission(
|
||||||
this,
|
this,
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
@@ -241,8 +250,10 @@ class MainActivity : FlutterActivity() {
|
|||||||
try {
|
try {
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||||
intent.addCategory("android.intent.category.DEFAULT")
|
intent.addCategory("android.intent.category.DEFAULT")
|
||||||
intent.data = Uri.parse("package:" + context.packageName)
|
intent.data = Uri.parse("package:$packageName")
|
||||||
startActivityForResult(intent, storageRequestCode)
|
startContractForResult(ActivityResultContracts.StartActivityForResult(), intent){ _ ->
|
||||||
|
result(Environment.isExternalStorageManager())
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
result(false)
|
result(false)
|
||||||
}
|
}
|
||||||
@@ -258,7 +269,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
grantResults: IntArray
|
grantResults: IntArray
|
||||||
) {
|
) {
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
if(requestCode == storageRequestCode) {
|
if (requestCode == storageRequestCode) {
|
||||||
storagePermissionRequest?.invoke(grantResults.all {
|
storagePermissionRequest?.invoke(grantResults.all {
|
||||||
it == PackageManager.PERMISSION_GRANTED
|
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)
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
intent.type = "*/*"
|
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 onUp = fun() {}
|
||||||
var onDown = fun() {}
|
var onDown = fun() {}
|
||||||
fun up(){
|
fun up() {
|
||||||
onUp()
|
onUp()
|
||||||
}
|
}
|
||||||
fun down(){
|
|
||||||
|
fun down() {
|
||||||
onDown()
|
onDown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -43,10 +43,11 @@
|
|||||||
"Confirm": "确认",
|
"Confirm": "确认",
|
||||||
"Are you sure you want to delete this comic?": "您确定要删除这部漫画吗?",
|
"Are you sure you want to delete this comic?": "您确定要删除这部漫画吗?",
|
||||||
"Are you sure you want to delete @a selected comics?": "您确定要删除 @a 部漫画吗?",
|
"Are you sure you want to delete @a selected comics?": "您确定要删除 @a 部漫画吗?",
|
||||||
"Add comic source": "添加漫画来源",
|
"Add comic source": "添加漫画源",
|
||||||
"Select file": "选择文件",
|
"Select file": "选择文件",
|
||||||
"View list": "查看列表",
|
"View list": "查看列表",
|
||||||
"Open help": "打开帮助",
|
"Open help": "打开帮助",
|
||||||
|
"Open in Browser": "打开网页",
|
||||||
"Check updates": "检查更新",
|
"Check updates": "检查更新",
|
||||||
"Edit": "编辑",
|
"Edit": "编辑",
|
||||||
"Update": "更新",
|
"Update": "更新",
|
||||||
@@ -101,8 +102,8 @@
|
|||||||
"Auto page turning interval": "自动翻页间隔",
|
"Auto page turning interval": "自动翻页间隔",
|
||||||
"Theme Mode": "主题模式",
|
"Theme Mode": "主题模式",
|
||||||
"System": "系统",
|
"System": "系统",
|
||||||
"Light": "明亮",
|
"Light": "浅色",
|
||||||
"Dark": "黑暗",
|
"Dark": "深色",
|
||||||
"Theme Color": "主题颜色",
|
"Theme Color": "主题颜色",
|
||||||
"Red": "红色",
|
"Red": "红色",
|
||||||
"Pink": "粉色",
|
"Pink": "粉色",
|
||||||
@@ -209,7 +210,26 @@
|
|||||||
"Update Comics Info": "更新漫画信息",
|
"Update Comics Info": "更新漫画信息",
|
||||||
"Create Folder": "新建文件夹",
|
"Create Folder": "新建文件夹",
|
||||||
"Select an image on screen": "选择屏幕上的图片",
|
"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": {
|
"zh_TW": {
|
||||||
"Home": "首頁",
|
"Home": "首頁",
|
||||||
@@ -257,10 +277,11 @@
|
|||||||
"Confirm": "確認",
|
"Confirm": "確認",
|
||||||
"Are you sure you want to delete this comic?": "您確定要刪除這部漫畫嗎?",
|
"Are you sure you want to delete this comic?": "您確定要刪除這部漫畫嗎?",
|
||||||
"Are you sure you want to delete @a selected comics?": "您確定要刪除 @a 部漫畫嗎?",
|
"Are you sure you want to delete @a selected comics?": "您確定要刪除 @a 部漫畫嗎?",
|
||||||
"Add comic source": "添加漫畫來源",
|
"Add comic source": "添加漫畫源",
|
||||||
"Select file": "選擇文件",
|
"Select file": "選擇文件",
|
||||||
"View list": "查看列表",
|
"View list": "查看列表",
|
||||||
"Open help": "打開幫助",
|
"Open help": "打開幫助",
|
||||||
|
"Open in Browser": "打開網頁",
|
||||||
"Check updates": "檢查更新",
|
"Check updates": "檢查更新",
|
||||||
"Edit": "編輯",
|
"Edit": "編輯",
|
||||||
"Update": "更新",
|
"Update": "更新",
|
||||||
@@ -313,8 +334,8 @@
|
|||||||
"Auto page turning interval": "自動翻頁間隔",
|
"Auto page turning interval": "自動翻頁間隔",
|
||||||
"Theme Mode": "主題模式",
|
"Theme Mode": "主題模式",
|
||||||
"System": "系統",
|
"System": "系統",
|
||||||
"Light": "明亮",
|
"Light": "浅色",
|
||||||
"Dark": "黑暗",
|
"Dark": "深色",
|
||||||
"Theme Color": "主題顏色",
|
"Theme Color": "主題顏色",
|
||||||
"Red": "紅色",
|
"Red": "紅色",
|
||||||
"Pink": "粉色",
|
"Pink": "粉色",
|
||||||
@@ -421,6 +442,25 @@
|
|||||||
"Update Comics Info": "更新漫畫信息",
|
"Update Comics Info": "更新漫畫信息",
|
||||||
"Create Folder": "新建文件夾",
|
"Create Folder": "新建文件夾",
|
||||||
"Select an image on screen": "選擇屏幕上的圖片",
|
"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": "沒有分類頁面"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -51,5 +51,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSFaceIDUsageDescription</key>
|
||||||
|
<string>Ensure that the operation is being performed by the user themselves.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@@ -369,10 +369,14 @@ class _FilledTabBarState extends State<FilledTabBar> {
|
|||||||
final double tabWidth = tabRight - tabLeft;
|
final double tabWidth = tabRight - tabLeft;
|
||||||
final double tabCenter = tabLeft + tabWidth / 2;
|
final double tabCenter = tabLeft + tabWidth / 2;
|
||||||
final double tabBarWidth = tabBarBox.size.width;
|
final double tabBarWidth = tabBarBox.size.width;
|
||||||
final double scrollOffset = tabCenter - tabBarWidth / 2;
|
double scrollOffset = tabCenter - tabBarWidth / 2;
|
||||||
if (scrollOffset == scrollController.offset) {
|
if (scrollOffset == scrollController.offset) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
scrollOffset = scrollOffset.clamp(
|
||||||
|
0.0,
|
||||||
|
scrollController.position.maxScrollExtent,
|
||||||
|
);
|
||||||
scrollController.animateTo(
|
scrollController.animateTo(
|
||||||
scrollOffset,
|
scrollOffset,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
|
@@ -227,16 +227,26 @@ class ComicTile extends StatelessWidget {
|
|||||||
Widget _buildBriefMode(BuildContext context) {
|
Widget _buildBriefMode(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
||||||
child: Material(
|
child: LayoutBuilder(
|
||||||
color: Colors.transparent,
|
builder: (context, constraints) {
|
||||||
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
elevation: 1,
|
onTap: _onTap,
|
||||||
|
onLongPress:
|
||||||
|
enableLongPressed ? () => onLongPress(context) : null,
|
||||||
|
onSecondaryTapDown: (detail) => onSecondaryTap(detail, context),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SizedBox(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.secondaryContainer,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
@@ -245,56 +255,97 @@ class ComicTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
|
||||||
right: 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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
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(
|
child: Text(
|
||||||
comic.title.replaceAll("\n", ""),
|
comic.description.isEmpty
|
||||||
|
? comic.subtitle
|
||||||
|
?.replaceAll('\n', '') ??
|
||||||
|
''
|
||||||
|
: comic.description
|
||||||
|
.split('|')
|
||||||
|
.join('\n'),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
fontSize: 14.0,
|
fontSize: 12,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
|
textAlign: TextAlign.right,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 4, 8, 0),
|
||||||
|
child: Text(
|
||||||
|
comic.title.replaceAll('\n', ''),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 14.0,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _splitText(String text) {
|
||||||
|
// split text by space, comma. text in brackets will be kept together.
|
||||||
|
var words = <String>[];
|
||||||
|
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) {
|
void block(BuildContext comicTileContext) {
|
||||||
@@ -303,7 +354,7 @@ class ComicTile extends StatelessWidget {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
var words = <String>[];
|
var words = <String>[];
|
||||||
var all = <String>[];
|
var all = <String>[];
|
||||||
all.addAll(comic.title.split(' ').where((element) => element != ''));
|
all.addAll(_splitText(comic.title));
|
||||||
if (comic.subtitle != null && comic.subtitle != "") {
|
if (comic.subtitle != null && comic.subtitle != "") {
|
||||||
all.add(comic.subtitle!);
|
all.add(comic.subtitle!);
|
||||||
}
|
}
|
||||||
@@ -691,6 +742,9 @@ class _SliverGridComics extends StatelessWidget {
|
|||||||
menuOptions: menuBuilder?.call(comics[index]),
|
menuOptions: menuBuilder?.call(comics[index]),
|
||||||
onTap: onTap != null ? () => onTap!(comics[index]) : null,
|
onTap: onTap != null ? () => onTap!(comics[index]) : null,
|
||||||
);
|
);
|
||||||
|
if(selection == null) {
|
||||||
|
return comic;
|
||||||
|
}
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
|
224
lib/components/custom_slider.dart
Normal file
224
lib/components/custom_slider.dart
Normal file
@@ -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<WidgetState> 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<CustomSlider> createState() => _CustomSliderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomSliderState extends State<CustomSlider> {
|
||||||
|
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 = <Widget>[];
|
||||||
|
for(int i = 0; i<widget.divisions-1; i++){
|
||||||
|
res.add(const Spacer());
|
||||||
|
res.add(Container(
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surface.withRed(10),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
res.add(const Spacer());
|
||||||
|
return res;
|
||||||
|
}.call(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: widget.reversed ? null : 0,
|
||||||
|
right: widget.reversed ? 0 : null,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: constraints.maxWidth * ((value - widget.min) / (widget.max - widget.min)),
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.activeTrackColor,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(10))
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: widget.reversed ? null : constraints.maxWidth * ((value - widget.min) / (widget.max - widget.min))-11,
|
||||||
|
right: !widget.reversed ? null : constraints.maxWidth * ((value - widget.min) / (widget.max - widget.min))-11,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.activeTrackColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -51,6 +51,10 @@ class Flyout extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<Flyout> createState() => FlyoutState();
|
State<Flyout> createState() => FlyoutState();
|
||||||
|
|
||||||
|
static FlyoutState of(BuildContext context) {
|
||||||
|
return context.findAncestorStateOfType<FlyoutState>()!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FlyoutState extends State<Flyout> {
|
class FlyoutState extends State<Flyout> {
|
||||||
|
@@ -2,10 +2,7 @@ part of 'components.dart';
|
|||||||
|
|
||||||
class SliverGridViewWithFixedItemHeight extends StatelessWidget {
|
class SliverGridViewWithFixedItemHeight extends StatelessWidget {
|
||||||
const SliverGridViewWithFixedItemHeight(
|
const SliverGridViewWithFixedItemHeight(
|
||||||
{required this.delegate,
|
{required this.delegate, required this.maxCrossAxisExtent, required this.itemHeight, super.key});
|
||||||
required this.maxCrossAxisExtent,
|
|
||||||
required this.itemHeight,
|
|
||||||
super.key});
|
|
||||||
|
|
||||||
final SliverChildDelegate delegate;
|
final SliverChildDelegate delegate;
|
||||||
|
|
||||||
@@ -65,8 +62,7 @@ class SliverGridDelegateWithFixedHeight extends SliverGridDelegate {
|
|||||||
@override
|
@override
|
||||||
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
|
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
|
||||||
if (oldDelegate is! SliverGridDelegateWithFixedHeight) return true;
|
if (oldDelegate is! SliverGridDelegateWithFixedHeight) return true;
|
||||||
if (oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent ||
|
if (oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent || oldDelegate.itemHeight != itemHeight) {
|
||||||
oldDelegate.itemHeight != itemHeight) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -95,8 +91,7 @@ class SliverGridDelegateWithComics extends SliverGridDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SliverGridLayout getDetailedModeLayout(
|
SliverGridLayout getDetailedModeLayout(SliverConstraints constraints, double scale) {
|
||||||
SliverConstraints constraints, double scale) {
|
|
||||||
const minCrossAxisExtent = 360;
|
const minCrossAxisExtent = 360;
|
||||||
final itemHeight = 152 * scale;
|
final itemHeight = 152 * scale;
|
||||||
final width = constraints.crossAxisExtent;
|
final width = constraints.crossAxisExtent;
|
||||||
@@ -111,14 +106,11 @@ class SliverGridDelegateWithComics extends SliverGridDelegate {
|
|||||||
reverseCrossAxis: false);
|
reverseCrossAxis: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
SliverGridLayout getBriefModeLayout(
|
SliverGridLayout getBriefModeLayout(SliverConstraints constraints, double scale) {
|
||||||
SliverConstraints constraints, double scale) {
|
|
||||||
final maxCrossAxisExtent = 192.0 * scale;
|
final maxCrossAxisExtent = 192.0 * scale;
|
||||||
const childAspectRatio = 0.72;
|
const childAspectRatio = 0.68;
|
||||||
const crossAxisSpacing = 0.0;
|
const crossAxisSpacing = 0.0;
|
||||||
int crossAxisCount =
|
int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil();
|
||||||
(constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing))
|
|
||||||
.ceil();
|
|
||||||
// Ensure a minimum count of 1, can be zero and result in an infinite extent
|
// Ensure a minimum count of 1, can be zero and result in an infinite extent
|
||||||
// below when the window size is 0.
|
// below when the window size is 0.
|
||||||
crossAxisCount = math.max(1, crossAxisCount);
|
crossAxisCount = math.max(1, crossAxisCount);
|
||||||
|
@@ -31,8 +31,9 @@ class Select extends StatelessWidget {
|
|||||||
var size = renderBox.size;
|
var size = renderBox.size;
|
||||||
showMenu(
|
showMenu(
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
color: context.colorScheme.surface,
|
color: context.brightness == Brightness.light
|
||||||
surfaceTintColor: Colors.transparent,
|
? const Color(0xFFF6F6F6)
|
||||||
|
: const Color(0xFF1E1E1E),
|
||||||
context: context,
|
context: context,
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
@@ -41,8 +42,8 @@ class Select extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
position: RelativeRect.fromLTRB(
|
position: RelativeRect.fromLTRB(
|
||||||
offset.dx,
|
offset.dx,
|
||||||
offset.dy + size.height,
|
offset.dy + size.height + 2,
|
||||||
offset.dx + size.height,
|
offset.dx + size.height + 2,
|
||||||
offset.dy,
|
offset.dy,
|
||||||
),
|
),
|
||||||
items: values
|
items: values
|
||||||
|
@@ -10,7 +10,7 @@ export "widget_utils.dart";
|
|||||||
export "context.dart";
|
export "context.dart";
|
||||||
|
|
||||||
class _App {
|
class _App {
|
||||||
final version = "1.0.5";
|
final version = "1.0.6";
|
||||||
|
|
||||||
bool get isAndroid => Platform.isAndroid;
|
bool get isAndroid => Platform.isAndroid;
|
||||||
|
|
||||||
|
@@ -119,6 +119,8 @@ class _Settings with ChangeNotifier {
|
|||||||
'quickFavorite': null,
|
'quickFavorite': null,
|
||||||
'enableTurnPageByVolumeKey': true,
|
'enableTurnPageByVolumeKey': true,
|
||||||
'enableClockAndBatteryInfoInReader': true,
|
'enableClockAndBatteryInfoInReader': true,
|
||||||
|
'ignoreCertificateErrors': false,
|
||||||
|
'authorizationRequired': false,
|
||||||
};
|
};
|
||||||
|
|
||||||
operator [](String key) {
|
operator [](String key) {
|
||||||
|
@@ -136,6 +136,8 @@ class ComicSource {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool get isEmpty => _sources.isEmpty;
|
||||||
|
|
||||||
/// Name of this source.
|
/// Name of this source.
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
|
@@ -157,9 +157,11 @@ class ComicSourceParser {
|
|||||||
|
|
||||||
await source.loadData();
|
await source.loadData();
|
||||||
|
|
||||||
|
if(_checkExists("init")) {
|
||||||
Future.delayed(const Duration(milliseconds: 50), () {
|
Future.delayed(const Duration(milliseconds: 50), () {
|
||||||
JsEngine().runCode("ComicSource.sources.$_key.init()");
|
JsEngine().runCode("ComicSource.sources.$_key.init()");
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
@@ -346,6 +346,32 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
return name;
|
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) {
|
bool comicExists(String folder, String id, ComicType type) {
|
||||||
var res = _db.select("""
|
var res = _db.select("""
|
||||||
select * from "$folder"
|
select * from "$folder"
|
||||||
@@ -365,20 +391,19 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
return FavoriteItem.fromRow(res.first);
|
return FavoriteItem.fromRow(res.first);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// add comic to a folder
|
/// add comic to a folder.
|
||||||
///
|
/// return true if success, false if already exists
|
||||||
/// This method will download cover to local, to avoid problems like changing url
|
bool addComic(String folder, FavoriteItem comic, [int? order]) {
|
||||||
void addComic(String folder, FavoriteItem comic, [int? order]) async {
|
|
||||||
_modifiedAfterLastCache = true;
|
_modifiedAfterLastCache = true;
|
||||||
if (!existsFolder(folder)) {
|
if (!existsFolder(folder)) {
|
||||||
throw Exception("Folder does not exists");
|
throw Exception("Folder does not exists");
|
||||||
}
|
}
|
||||||
var res = _db.select("""
|
var res = _db.select("""
|
||||||
select * from "$folder"
|
select * from "$folder"
|
||||||
where id == '${comic.id}';
|
where id == ? and type == ?;
|
||||||
""");
|
""", [comic.id, comic.type.value]);
|
||||||
if (res.isNotEmpty) {
|
if (res.isNotEmpty) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
final params = [
|
final params = [
|
||||||
comic.id,
|
comic.id,
|
||||||
@@ -406,6 +431,7 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
""", [...params, minValue(folder) - 1]);
|
""", [...params, minValue(folder) - 1]);
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// delete a folder
|
/// delete a folder
|
||||||
@@ -414,6 +440,10 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
_db.execute("""
|
_db.execute("""
|
||||||
drop table "$name";
|
drop table "$name";
|
||||||
""");
|
""");
|
||||||
|
_db.execute("""
|
||||||
|
delete from folder_order
|
||||||
|
where folder_name == ?;
|
||||||
|
""", [name]);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,6 +491,16 @@ class LocalFavoritesManager with ChangeNotifier {
|
|||||||
ALTER TABLE "$before"
|
ALTER TABLE "$before"
|
||||||
RENAME TO "$after";
|
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();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -19,6 +19,7 @@ import 'package:pointycastle/block/modes/cfb.dart';
|
|||||||
import 'package:pointycastle/block/modes/ecb.dart';
|
import 'package:pointycastle/block/modes/ecb.dart';
|
||||||
import 'package:pointycastle/block/modes/ofb.dart';
|
import 'package:pointycastle/block/modes/ofb.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/network/app_dio.dart';
|
import 'package:venera/network/app_dio.dart';
|
||||||
import 'package:venera/network/cookie_jar.dart';
|
import 'package:venera/network/cookie_jar.dart';
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ class JsEngine with _JSEngineApi {
|
|||||||
var setGlobalFunc =
|
var setGlobalFunc =
|
||||||
_engine!.evaluate("(key, value) => { this[key] = value; }");
|
_engine!.evaluate("(key, value) => { this[key] = value; }");
|
||||||
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
|
(setGlobalFunc as JSInvokable)(["sendMessage", _messageReceiver]);
|
||||||
|
setGlobalFunc(["appVersion", App.version]);
|
||||||
setGlobalFunc.free();
|
setGlobalFunc.free();
|
||||||
var jsInit = await rootBundle.load("assets/init.js");
|
var jsInit = await rootBundle.load("assets/init.js");
|
||||||
_engine!
|
_engine!
|
||||||
|
@@ -5,7 +5,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:rhttp/rhttp.dart';
|
import 'package:rhttp/rhttp.dart';
|
||||||
import 'package:venera/foundation/log.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/comic_source_page.dart';
|
||||||
import 'package:venera/pages/main_page.dart';
|
import 'package:venera/pages/main_page.dart';
|
||||||
import 'package:venera/pages/settings/settings_page.dart';
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
@@ -65,15 +65,59 @@ class MyApp extends StatefulWidget {
|
|||||||
State<MyApp> createState() => _MyAppState();
|
State<MyApp> createState() => _MyAppState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MyAppState extends State<MyApp> {
|
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
checkUpdates();
|
checkUpdates();
|
||||||
App.registerForceRebuild(forceRebuild);
|
App.registerForceRebuild(forceRebuild);
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
super.initState();
|
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 forceRebuild() {
|
||||||
void rebuild(Element el) {
|
void rebuild(Element el) {
|
||||||
el.markNeedsBuild();
|
el.markNeedsBuild();
|
||||||
@@ -86,14 +130,25 @@ class _MyAppState extends State<MyApp> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
Widget home;
|
||||||
|
if (appdata.settings['authorizationRequired']) {
|
||||||
|
home = AuthPage(
|
||||||
|
onSuccessfulAuth: () {
|
||||||
|
App.rootContext.toReplacement(() => const MainPage());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
home = const MainPage();
|
||||||
|
}
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
home: const MainPage(),
|
home: home,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.fromSeed(
|
colorScheme: ColorScheme.fromSeed(
|
||||||
seedColor: App.mainColor,
|
seedColor: App.mainColor,
|
||||||
surface: Colors.white,
|
surface: Colors.white,
|
||||||
primary: App.mainColor.shade600,
|
primary: App.mainColor.shade600,
|
||||||
|
// ignore: deprecated_member_use
|
||||||
background: Colors.white,
|
background: Colors.white,
|
||||||
),
|
),
|
||||||
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
||||||
@@ -105,6 +160,7 @@ class _MyAppState extends State<MyApp> {
|
|||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
surface: Colors.black,
|
surface: Colors.black,
|
||||||
primary: App.mainColor.shade400,
|
primary: App.mainColor.shade400,
|
||||||
|
// ignore: deprecated_member_use
|
||||||
background: Colors.black,
|
background: Colors.black,
|
||||||
),
|
),
|
||||||
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
fontFamily: App.isWindows ? "Microsoft YaHei" : null,
|
||||||
@@ -171,12 +227,12 @@ class _MyAppState extends State<MyApp> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void checkUpdates() async {
|
void checkUpdates() async {
|
||||||
if(!appdata.settings['checkUpdateOnStart']) {
|
if (!appdata.settings['checkUpdateOnStart']) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var lastCheck = appdata.implicitData['lastCheckUpdate'] ?? 0;
|
var lastCheck = appdata.implicitData['lastCheckUpdate'] ?? 0;
|
||||||
var now = DateTime.now().millisecondsSinceEpoch;
|
var now = DateTime.now().millisecondsSinceEpoch;
|
||||||
if(now - lastCheck < 24 * 60 * 60 * 1000) {
|
if (now - lastCheck < 24 * 60 * 60 * 1000) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
appdata.implicitData['lastCheckUpdate'] = now;
|
appdata.implicitData['lastCheckUpdate'] = now;
|
||||||
|
@@ -106,34 +106,24 @@ class MyLogInterceptor implements Interceptor {
|
|||||||
|
|
||||||
class AppDio with DioMixin {
|
class AppDio with DioMixin {
|
||||||
String? _proxy = proxy;
|
String? _proxy = proxy;
|
||||||
|
static bool get ignoreCertificateErrors => appdata.settings['ignoreCertificateErrors'] == true;
|
||||||
|
|
||||||
AppDio([BaseOptions? options]) {
|
AppDio([BaseOptions? options]) {
|
||||||
this.options = options ?? BaseOptions();
|
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(CookieManagerSql(SingleInstanceCookieJar.instance!));
|
||||||
interceptors.add(NetworkCacheManager());
|
interceptors.add(NetworkCacheManager());
|
||||||
interceptors.add(CloudflareInterceptor());
|
interceptors.add(CloudflareInterceptor());
|
||||||
interceptors.add(MyLogInterceptor());
|
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 String? proxy;
|
||||||
|
|
||||||
static Future<String?> getProxy() async {
|
static Future<String?> getProxy() async {
|
||||||
@@ -189,8 +179,8 @@ class AppDio with DioMixin {
|
|||||||
ProgressCallback? onSendProgress,
|
ProgressCallback? onSendProgress,
|
||||||
ProgressCallback? onReceiveProgress,
|
ProgressCallback? onReceiveProgress,
|
||||||
}) async {
|
}) async {
|
||||||
if(options?.headers?['prevent-parallel'] == 'true') {
|
if (options?.headers?['prevent-parallel'] == 'true') {
|
||||||
while(_requests.containsKey(path)) {
|
while (_requests.containsKey(path)) {
|
||||||
await Future.delayed(const Duration(milliseconds: 20));
|
await Future.delayed(const Duration(milliseconds: 20));
|
||||||
}
|
}
|
||||||
_requests[path] = true;
|
_requests[path] = true;
|
||||||
@@ -204,6 +194,9 @@ class AppDio with DioMixin {
|
|||||||
proxySettings: proxy == null
|
proxySettings: proxy == null
|
||||||
? const rhttp.ProxySettings.noProxy()
|
? const rhttp.ProxySettings.noProxy()
|
||||||
: rhttp.ProxySettings.proxy(proxy!),
|
: rhttp.ProxySettings.proxy(proxy!),
|
||||||
|
tlsSettings: rhttp.TlsSettings(
|
||||||
|
verifyCertificates: !ignoreCertificateErrors,
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -216,9 +209,8 @@ class AppDio with DioMixin {
|
|||||||
onSendProgress: onSendProgress,
|
onSendProgress: onSendProgress,
|
||||||
onReceiveProgress: onReceiveProgress,
|
onReceiveProgress: onReceiveProgress,
|
||||||
);
|
);
|
||||||
}
|
} finally {
|
||||||
finally {
|
if (_requests.containsKey(path)) {
|
||||||
if(_requests.containsKey(path)) {
|
|
||||||
_requests.remove(path);
|
_requests.remove(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,6 +229,9 @@ class RHttpAdapter implements HttpClientAdapter {
|
|||||||
keepAlivePing: Duration(seconds: 30),
|
keepAlivePing: Duration(seconds: 30),
|
||||||
),
|
),
|
||||||
throwOnStatusCode: false,
|
throwOnStatusCode: false,
|
||||||
|
tlsSettings: rhttp.TlsSettings(
|
||||||
|
verifyCertificates: !AppDio.ignoreCertificateErrors,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +279,7 @@ class RHttpAdapter implements HttpClientAdapter {
|
|||||||
headers[key]!.add(entry.$2);
|
headers[key]!.add(entry.$2);
|
||||||
}
|
}
|
||||||
var data = res.body;
|
var data = res.body;
|
||||||
if(headers['content-encoding']?.contains('gzip') ?? false) {
|
if (headers['content-encoding']?.contains('gzip') ?? false) {
|
||||||
// rhttp does not support gzip decoding
|
// rhttp does not support gzip decoding
|
||||||
data = gzip.decoder.bind(data).map((data) => Uint8List.fromList(data));
|
data = gzip.decoder.bind(data).map((data) => Uint8List.fromList(data));
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:io' as io;
|
import 'dart:io' as io;
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_qjs/flutter_qjs.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';
|
||||||
|
60
lib/pages/auth_page.dart
Normal file
60
lib/pages/auth_page.dart
Normal file
@@ -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<AuthPage> createState() => _AuthPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AuthPageState extends State<AuthPage> {
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -30,8 +30,15 @@ class CategoriesPage extends StatelessWidget {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if(categories.isEmpty) {
|
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(
|
return NetworkError(
|
||||||
message: "No Category Pages".tl,
|
message: msg,
|
||||||
retry: () {
|
retry: () {
|
||||||
controller.update();
|
controller.update();
|
||||||
},
|
},
|
||||||
@@ -248,36 +255,19 @@ class _CategoryPage extends StatelessWidget {
|
|||||||
|
|
||||||
Widget buildTag(String tag, ClickTagCallback onClick,
|
Widget buildTag(String tag, ClickTagCallback onClick,
|
||||||
[String? namespace, String? param]) {
|
[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(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(8, 6, 8, 6),
|
padding: const EdgeInsets.fromLTRB(8, 6, 8, 6),
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Material(
|
return Material(
|
||||||
elevation: 0.6,
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
color: context.colorScheme.primaryContainer.withOpacity(0.72),
|
||||||
color: context.colorScheme.surfaceContainerLow,
|
|
||||||
surfaceTintColor: Colors.transparent,
|
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
onTap: () => onClick(tag, param),
|
onTap: () => onClick(tag, param),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||||
child: Text(translateTag(tag)),
|
child: Text(tag),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@@ -327,7 +327,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildDescription() {
|
Widget buildDescription() {
|
||||||
if (comic.description == null) {
|
if (comic.description == null || comic.description!.trim().isEmpty) {
|
||||||
return const SliverPadding(padding: EdgeInsets.zero);
|
return const SliverPadding(padding: EdgeInsets.zero);
|
||||||
}
|
}
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
@@ -392,6 +392,27 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
onTap: onTap,
|
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),
|
child: Text(text).padding(padding),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -406,6 +427,26 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<Widget> children}) {
|
Widget buildWrap({required List<Widget> children}) {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
@@ -464,14 +505,14 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
|
|||||||
buildWrap(
|
buildWrap(
|
||||||
children: [
|
children: [
|
||||||
buildTag(text: 'Upload Time'.tl, isTitle: true),
|
buildTag(text: 'Upload Time'.tl, isTitle: true),
|
||||||
buildTag(text: comic.uploadTime!),
|
buildTag(text: formatTime(comic.uploadTime!)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (comic.updateTime != null)
|
if (comic.updateTime != null)
|
||||||
buildWrap(
|
buildWrap(
|
||||||
children: [
|
children: [
|
||||||
buildTag(text: 'Update Time'.tl, isTitle: true),
|
buildTag(text: 'Update Time'.tl, isTitle: true),
|
||||||
buildTag(text: comic.updateTime!),
|
buildTag(text: formatTime(comic.updateTime!)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -575,7 +616,7 @@ abstract mixin class _ComicPageActions {
|
|||||||
|
|
||||||
void quickFavorite() {
|
void quickFavorite() {
|
||||||
var folder = appdata.settings['quickFavorite'];
|
var folder = appdata.settings['quickFavorite'];
|
||||||
if(folder is! String) {
|
if (folder is! String) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LocalFavoritesManager().addComic(
|
LocalFavoritesManager().addComic(
|
||||||
@@ -1037,6 +1078,7 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> {
|
|||||||
if (!isInitialLoading && next == null) {
|
if (!isInitialLoading && next == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isLoading) return;
|
||||||
Future.microtask(() {
|
Future.microtask(() {
|
||||||
setState(() {
|
setState(() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
@@ -1609,7 +1651,9 @@ class _SelectDownloadChapterState extends State<_SelectDownloadChapter> {
|
|||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FilledButton(
|
child: FilledButton(
|
||||||
onPressed: selected.isEmpty ? null : () {
|
onPressed: selected.isEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
widget.finishSelect(selected);
|
widget.finishSelect(selected);
|
||||||
context.pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
|
@@ -40,7 +40,7 @@ class ComicSourcePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
controller?.close();
|
controller?.close();
|
||||||
if (shouldUpdate.isEmpty) {
|
if (shouldUpdate.isEmpty) {
|
||||||
if(!implicit) {
|
if (!implicit) {
|
||||||
App.rootContext.showMessage(message: "No Update Available".tl);
|
App.rootContext.showMessage(message: "No Update Available".tl);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -55,10 +55,10 @@ class ComicSourcePage extends StatefulWidget {
|
|||||||
title: "Updates Available".tl,
|
title: "Updates Available".tl,
|
||||||
content: msg,
|
content: msg,
|
||||||
confirmText: "Update",
|
confirmText: "Update",
|
||||||
onConfirm: () {
|
onConfirm: () async {
|
||||||
for (var key in shouldUpdate) {
|
for (var key in shouldUpdate) {
|
||||||
var source = ComicSource.find(key);
|
var source = ComicSource.find(key);
|
||||||
_BodyState.update(source!);
|
await _BodyState.update(source!);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -95,24 +95,12 @@ class _BodyState extends State<_Body> {
|
|||||||
return SmoothCustomScrollView(
|
return SmoothCustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
buildCard(context),
|
buildCard(context),
|
||||||
buildSettings(),
|
|
||||||
for (var source in ComicSource.all()) buildSource(context, source),
|
for (var source in ComicSource.all()) buildSource(context, source),
|
||||||
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)),
|
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) {
|
Widget buildSource(BuildContext context, ComicSource source) {
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -181,11 +169,12 @@ class _BodyState extends State<_Body> {
|
|||||||
trailing: Select(
|
trailing: Select(
|
||||||
current: (current as String).ts(source.key),
|
current: (current as String).ts(source.key),
|
||||||
values: (item.value['options'] as List)
|
values: (item.value['options'] as List)
|
||||||
.map<String>(
|
.map<String>((e) =>
|
||||||
(e) => ((e['text'] ?? e['value']) as String).ts(source.key))
|
((e['text'] ?? e['value']) as String).ts(source.key))
|
||||||
.toList(),
|
.toList(),
|
||||||
onTap: (i) {
|
onTap: (i) {
|
||||||
source.data['settings'][key] = item.value['options'][i]['value'];
|
source.data['settings'][key] =
|
||||||
|
item.value['options'][i]['value'];
|
||||||
source.saveData();
|
source.saveData();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
@@ -209,7 +198,8 @@ class _BodyState extends State<_Body> {
|
|||||||
source.data['settings'][key] ?? item.value['default'] ?? '';
|
source.data['settings'][key] ?? item.value['default'] ?? '';
|
||||||
yield ListTile(
|
yield ListTile(
|
||||||
title: Text((item.value['title'] as String).ts(source.key)),
|
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(
|
trailing: IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
onPressed: () {
|
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");
|
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<void> update(ComicSource source) async {
|
||||||
if (!source.url.isURL) {
|
if (!source.url.isURL) {
|
||||||
App.rootContext.showMessage(message: "Invalid url config");
|
App.rootContext.showMessage(message: "Invalid url config");
|
||||||
return;
|
return;
|
||||||
@@ -305,8 +294,13 @@ class _BodyState extends State<_Body> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildCard(BuildContext context) {
|
Widget buildCard(BuildContext context) {
|
||||||
|
Widget buildButton({required Widget child, required VoidCallback onPressed}) {
|
||||||
|
return Button.normal(
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: child,
|
||||||
|
).fixHeight(32);
|
||||||
|
}
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Card.outlined(
|
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -321,39 +315,52 @@ class _BodyState extends State<_Body> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: "URL",
|
hintText: "URL",
|
||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
contentPadding:
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
suffix: IconButton(
|
suffix: IconButton(
|
||||||
onPressed: () => handleAddSource(url),
|
onPressed: () => handleAddSource(url),
|
||||||
icon: const Icon(Icons.check))),
|
icon: const Icon(Icons.check))),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
url = value;
|
url = value;
|
||||||
},
|
},
|
||||||
onSubmitted: handleAddSource)
|
onSubmitted: handleAddSource,
|
||||||
.paddingHorizontal(16)
|
).paddingHorizontal(16).paddingBottom(8),
|
||||||
.paddingBottom(32),
|
ListTile(
|
||||||
Row(
|
title: Text("Comic Source list".tl),
|
||||||
children: [
|
trailing: buildButton(
|
||||||
TextButton(
|
child: Text("View".tl),
|
||||||
onPressed: _selectFile, child: Text("Select file".tl))
|
|
||||||
.paddingLeft(8),
|
|
||||||
const Spacer(),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showPopUpWidget(
|
showPopUpWidget(
|
||||||
App.rootContext, _ComicSourceList(handleAddSource));
|
App.rootContext,
|
||||||
|
_ComicSourceList(handleAddSource),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: Text("View list".tl)),
|
),
|
||||||
const Spacer(),
|
),
|
||||||
TextButton(onPressed: help, child: Text("Open help".tl))
|
ListTile(
|
||||||
.paddingRight(8),
|
title: Text("Use a config file".tl),
|
||||||
],
|
trailing: buildButton(
|
||||||
|
onPressed: _selectFile,
|
||||||
|
child: Text("Select".tl),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).paddingHorizontal(12),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,8 +379,7 @@ class _BodyState extends State<_Body> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void help() {
|
void help() {
|
||||||
launchUrlString(
|
launchUrlString("https://github.com/venera-app/venera-configs");
|
||||||
"https://github.com/venera-app/venera/blob/master/doc/comic_source.md");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleAddSource(String url) async {
|
Future<void> handleAddSource(String url) async {
|
||||||
|
@@ -1,8 +1,14 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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';
|
||||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||||
import 'package:venera/foundation/image_provider/cached_image.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';
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
class CommentsPage extends StatefulWidget {
|
class CommentsPage extends StatefulWidget {
|
||||||
@@ -268,7 +274,10 @@ class _CommentTileState extends State<_CommentTile> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(widget.comment.userName, style: ts.bold,),
|
Text(
|
||||||
|
widget.comment.userName,
|
||||||
|
style: ts.bold,
|
||||||
|
),
|
||||||
if (widget.comment.time != null)
|
if (widget.comment.time != null)
|
||||||
Text(widget.comment.time!, style: ts.s12),
|
Text(widget.comment.time!, style: ts.s12),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@@ -426,7 +435,7 @@ class _CommentTileState extends State<_CommentTile> {
|
|||||||
isCancel,
|
isCancel,
|
||||||
);
|
);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
if(isCancel) {
|
if (isCancel) {
|
||||||
voteStatus = 0;
|
voteStatus = 0;
|
||||||
} else {
|
} else {
|
||||||
if (isUp) {
|
if (isUp) {
|
||||||
@@ -498,6 +507,287 @@ class _CommentContent extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (!text.contains('<') && !text.contains('http')) {
|
||||||
return SelectableText(text);
|
return SelectableText(text);
|
||||||
|
} else {
|
||||||
|
return _RichCommentContent(text: text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Tag {
|
||||||
|
final String name;
|
||||||
|
final Map<String, String> 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 = <InlineSpan>[];
|
||||||
|
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 = <String, String>{};
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -93,8 +93,15 @@ class _ExplorePageState extends State<ExplorePage>
|
|||||||
Widget buildBody(String i) => _SingleExplorePage(i, key: Key(i));
|
Widget buildBody(String i) => _SingleExplorePage(i, key: Key(i));
|
||||||
|
|
||||||
Widget buildEmpty() {
|
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(
|
return NetworkError(
|
||||||
message: "No Explore Pages".tl,
|
message: msg,
|
||||||
retry: () {
|
retry: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
pages = ComicSource.all()
|
pages = ComicSource.all()
|
||||||
|
@@ -288,3 +288,178 @@ Future<void> sortFolders() async {
|
|||||||
|
|
||||||
LocalFavoritesManager().updateOrder(folders);
|
LocalFavoritesManager().updateOrder(folders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> 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<void> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
|
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
|
@@ -14,6 +14,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
|
|
||||||
late List<FavoriteItem> comics;
|
late List<FavoriteItem> comics;
|
||||||
|
|
||||||
|
String? networkSource;
|
||||||
|
String? networkFolder;
|
||||||
|
|
||||||
void updateComics() {
|
void updateComics() {
|
||||||
setState(() {
|
setState(() {
|
||||||
comics = LocalFavoritesManager().getAllComics(widget.folder);
|
comics = LocalFavoritesManager().getAllComics(widget.folder);
|
||||||
@@ -24,6 +27,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
|
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
|
||||||
comics = LocalFavoritesManager().getAllComics(widget.folder);
|
comics = LocalFavoritesManager().getAllComics(widget.folder);
|
||||||
|
var (a, b) = LocalFavoritesManager().findLinked(widget.folder);
|
||||||
|
networkSource = a;
|
||||||
|
networkFolder = b;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +55,51 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
child: Text(favPage.folder ?? "Unselected".tl),
|
child: Text(favPage.folder ?? "Unselected".tl),
|
||||||
),
|
),
|
||||||
actions: [
|
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(
|
MenuButton(
|
||||||
entries: [
|
entries: [
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
@@ -136,7 +187,7 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon: Icons.update,
|
icon: Icons.download,
|
||||||
text: "Download All".tl,
|
text: "Download All".tl,
|
||||||
onClick: () async {
|
onClick: () async {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
@@ -108,6 +108,17 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> {
|
|||||||
onTap: context.width < _kTwoPanelChangeWidth ? showFolders : null,
|
onTap: context.width < _kTwoPanelChangeWidth ? showFolders : null,
|
||||||
child: Text(widget.data.title),
|
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(
|
errorLeading: Appbar(
|
||||||
leading: Tooltip(
|
leading: Tooltip(
|
||||||
@@ -533,6 +544,17 @@ class _FavoriteFolder extends StatelessWidget {
|
|||||||
key: comicListKey,
|
key: comicListKey,
|
||||||
leadingSliver: SliverAppbar(
|
leadingSliver: SliverAppbar(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
|
actions: [
|
||||||
|
MenuButton(entries: [
|
||||||
|
MenuEntry(
|
||||||
|
icon: Icons.sync,
|
||||||
|
text: "Convert to local".tl,
|
||||||
|
onClick: () {
|
||||||
|
importNetworkFolder(data.key, title, folderID);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
errorLeading: Appbar(
|
errorLeading: Appbar(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
|
@@ -89,11 +89,13 @@ class _SyncDataWidget extends StatefulWidget {
|
|||||||
State<_SyncDataWidget> createState() => _SyncDataWidgetState();
|
State<_SyncDataWidget> createState() => _SyncDataWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SyncDataWidgetState extends State<_SyncDataWidget> {
|
class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObserver {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
DataSync().addListener(update);
|
DataSync().addListener(update);
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
lastCheck = DateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
@@ -106,6 +108,20 @@ class _SyncDataWidgetState extends State<_SyncDataWidget> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
DataSync().removeListener(update);
|
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
|
@override
|
||||||
|
@@ -12,6 +12,7 @@ import 'package:photo_view/photo_view.dart';
|
|||||||
import 'package:photo_view/photo_view_gallery.dart';
|
import 'package:photo_view/photo_view_gallery.dart';
|
||||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
|
import 'package:venera/components/custom_slider.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/cache_manager.dart';
|
import 'package:venera/foundation/cache_manager.dart';
|
||||||
@@ -244,8 +245,10 @@ abstract mixin class _ReaderLocation {
|
|||||||
bool toPage(int page) {
|
bool toPage(int page) {
|
||||||
if (_validatePage(page)) {
|
if (_validatePage(page)) {
|
||||||
if (page == this.page) {
|
if (page == this.page) {
|
||||||
|
if(!(chapter == 1 && page == 1) && !(chapter == maxChapter && page == maxPage)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this.page = page;
|
this.page = page;
|
||||||
update();
|
update();
|
||||||
if (enablePageAnimation) {
|
if (enablePageAnimation) {
|
||||||
|
@@ -18,6 +18,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
|
|
||||||
bool get isOpen => _isOpen;
|
bool get isOpen => _isOpen;
|
||||||
|
|
||||||
|
bool get isReversed => context.reader.mode == ReaderMode.galleryRightToLeft ||
|
||||||
|
context.reader.mode == ReaderMode.continuousRightToLeft;
|
||||||
|
|
||||||
int showFloatingButtonValue = 0;
|
int showFloatingButtonValue = 0;
|
||||||
|
|
||||||
var lastValue = 0;
|
var lastValue = 0;
|
||||||
@@ -217,34 +220,26 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton.filledTonal(
|
IconButton.filledTonal(
|
||||||
onPressed: () {
|
onPressed: () => !isReversed
|
||||||
if (!context.reader.toPrevChapter()) {
|
? context.reader.chapter > 1
|
||||||
context.reader.toPage(1);
|
? context.reader.toPrevChapter()
|
||||||
} else {
|
: context.reader.toPage(1)
|
||||||
if (showFloatingButtonValue != 0) {
|
: context.reader.chapter < context.reader.maxChapter
|
||||||
setState(() {
|
? context.reader.toNextChapter()
|
||||||
showFloatingButtonValue = 0;
|
: context.reader.toPage(context.reader.maxPage),
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.first_page),
|
icon: const Icon(Icons.first_page),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: buildSlider(),
|
child: buildSlider(),
|
||||||
),
|
),
|
||||||
IconButton.filledTonal(
|
IconButton.filledTonal(
|
||||||
onPressed: () {
|
onPressed: () => !isReversed
|
||||||
if (!context.reader.toNextChapter()) {
|
? context.reader.chapter < context.reader.maxChapter
|
||||||
context.reader.toPage(context.reader.maxPage);
|
? context.reader.toNextChapter()
|
||||||
} else {
|
: context.reader.toPage(context.reader.maxPage)
|
||||||
if (showFloatingButtonValue != 0) {
|
: context.reader.chapter > 1
|
||||||
setState(() {
|
? context.reader.toPrevChapter()
|
||||||
showFloatingButtonValue = 0;
|
: context.reader.toPage(1),
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.last_page)),
|
icon: const Icon(Icons.last_page)),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 8,
|
width: 8,
|
||||||
@@ -379,12 +374,13 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
var sliderFocus = FocusNode();
|
var sliderFocus = FocusNode();
|
||||||
|
|
||||||
Widget buildSlider() {
|
Widget buildSlider() {
|
||||||
return Slider(
|
return CustomSlider(
|
||||||
focusNode: sliderFocus,
|
focusNode: sliderFocus,
|
||||||
value: context.reader.page.toDouble(),
|
value: context.reader.page.toDouble(),
|
||||||
min: 1,
|
min: 1,
|
||||||
max:
|
max:
|
||||||
context.reader.maxPage.clamp(context.reader.page, 1 << 16).toDouble(),
|
context.reader.maxPage.clamp(context.reader.page, 1 << 16).toDouble(),
|
||||||
|
reversed: isReversed,
|
||||||
divisions: (context.reader.maxPage - 1).clamp(2, 1 << 16),
|
divisions: (context.reader.maxPage - 1).clamp(2, 1 << 16),
|
||||||
onChanged: (i) {
|
onChanged: (i) {
|
||||||
context.reader.toPage(i.toInt());
|
context.reader.toPage(i.toInt());
|
||||||
|
@@ -305,13 +305,24 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListTile(
|
return InkWell(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
title: Text(appdata.searchHistory[index - 2]),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
search(appdata.searchHistory[index - 2]);
|
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,
|
childCount: 2 + appdata.searchHistory.length,
|
||||||
),
|
),
|
||||||
@@ -490,7 +501,7 @@ class SearchOptionWidget extends StatelessWidget {
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(option.label.ts(sourceKey)),
|
title: Text(option.label.ts(sourceKey)),
|
||||||
),
|
),
|
||||||
if(option.type == 'select')
|
if (option.type == 'select')
|
||||||
Wrap(
|
Wrap(
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
@@ -504,7 +515,7 @@ class SearchOptionWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
if(option.type == 'multi-select')
|
if (option.type == 'multi-select')
|
||||||
Wrap(
|
Wrap(
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
@@ -514,7 +525,7 @@ class SearchOptionWidget extends StatelessWidget {
|
|||||||
isSelected: (jsonDecode(value) as List).contains(e.key),
|
isSelected: (jsonDecode(value) as List).contains(e.key),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
var list = jsonDecode(value) as List;
|
var list = jsonDecode(value) as List;
|
||||||
if(list.contains(e.key)) {
|
if (list.contains(e.key)) {
|
||||||
list.remove(e.key);
|
list.remove(e.key);
|
||||||
} else {
|
} else {
|
||||||
list.add(e.key);
|
list.add(e.key);
|
||||||
@@ -524,7 +535,7 @@ class SearchOptionWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
if(option.type == 'dropdown')
|
if (option.type == 'dropdown')
|
||||||
Select(
|
Select(
|
||||||
current: option.options[value],
|
current: option.options[value],
|
||||||
values: option.options.values.toList(),
|
values: option.options.values.toList(),
|
||||||
|
@@ -36,12 +36,12 @@ class _AppSettingsState extends State<AppSettings> {
|
|||||||
if (App.isAndroid) {
|
if (App.isAndroid) {
|
||||||
var channel = const MethodChannel("venera/storage");
|
var channel = const MethodChannel("venera/storage");
|
||||||
var permission = await channel.invokeMethod('');
|
var permission = await channel.invokeMethod('');
|
||||||
if(permission != true) {
|
if (permission != true) {
|
||||||
context.showMessage(message: "Permission denied".tl);
|
context.showMessage(message: "Permission denied".tl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var path = await selectDirectory();
|
var path = await selectDirectory();
|
||||||
if(path != null) {
|
if (path != null) {
|
||||||
// check if the path is writable
|
// check if the path is writable
|
||||||
var testFile = File(FilePath.join(path, "test"));
|
var testFile = File(FilePath.join(path, "test"));
|
||||||
try {
|
try {
|
||||||
@@ -177,6 +177,29 @@ class _AppSettingsState extends State<AppSettings> {
|
|||||||
App.forceRebuild();
|
App.forceRebuild();
|
||||||
},
|
},
|
||||||
).toSliver(),
|
).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(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -38,14 +38,11 @@ class _ProxySettingView extends StatefulWidget {
|
|||||||
|
|
||||||
class _ProxySettingViewState extends State<_ProxySettingView> {
|
class _ProxySettingViewState extends State<_ProxySettingView> {
|
||||||
String type = '';
|
String type = '';
|
||||||
|
|
||||||
String host = '';
|
String host = '';
|
||||||
|
|
||||||
String port = '';
|
String port = '';
|
||||||
|
|
||||||
String username = '';
|
String username = '';
|
||||||
|
|
||||||
String password = '';
|
String password = '';
|
||||||
|
bool ignoreCertificateErrors = false;
|
||||||
|
|
||||||
// USERNAME:PASSWORD@HOST:PORT
|
// USERNAME:PASSWORD@HOST:PORT
|
||||||
String toProxyStr() {
|
String toProxyStr() {
|
||||||
@@ -103,6 +100,7 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
var proxy = appdata.settings['proxy'];
|
var proxy = appdata.settings['proxy'];
|
||||||
parseProxyString(proxy);
|
parseProxyString(proxy);
|
||||||
|
ignoreCertificateErrors = appdata.settings['ignoreCertificateErrors'] ?? false;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +146,17 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (type == 'manual') buildManualProxy(),
|
if (type == 'manual') buildManualProxy(),
|
||||||
|
SwitchListTile(
|
||||||
|
title: Text("Ignore Certificate Errors".tl),
|
||||||
|
value: ignoreCertificateErrors,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() {
|
||||||
|
ignoreCertificateErrors = v;
|
||||||
|
});
|
||||||
|
appdata.settings['ignoreCertificateErrors'] = ignoreCertificateErrors;
|
||||||
|
appdata.saveData();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -33,9 +33,10 @@ class _SwitchSettingState extends State<_SwitchSetting> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
appdata.settings[widget.settingKey] = value;
|
appdata.settings[widget.settingKey] = value;
|
||||||
appdata.saveData();
|
|
||||||
});
|
});
|
||||||
|
appdata.saveData().then((_) {
|
||||||
widget.onChanged?.call();
|
widget.onChanged?.call();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -133,7 +134,9 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Help".tl,
|
title: "Help".tl,
|
||||||
content: Text(widget.help!).paddingHorizontal(16).fixWidth(double.infinity),
|
content: Text(widget.help!)
|
||||||
|
.paddingHorizontal(16)
|
||||||
|
.fixWidth(double.infinity),
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
onPressed: context.pop,
|
onPressed: context.pop,
|
||||||
@@ -158,8 +161,9 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> {
|
|||||||
var rect = offset & size;
|
var rect = offset & size;
|
||||||
showMenu(
|
showMenu(
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
color: context.colorScheme.surface,
|
color: context.brightness == Brightness.light
|
||||||
surfaceTintColor: Colors.transparent,
|
? const Color(0xFFF6F6F6)
|
||||||
|
: const Color(0xFF1E1E1E),
|
||||||
context: context,
|
context: context,
|
||||||
position: RelativeRect.fromRect(
|
position: RelativeRect.fromRect(
|
||||||
rect,
|
rect,
|
||||||
@@ -229,7 +233,9 @@ class _EndSelectorSelectSettingState extends State<_EndSelectorSelectSetting> {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
title: "Help".tl,
|
title: "Help".tl,
|
||||||
content: Text(widget.help!).paddingHorizontal(16).fixWidth(double.infinity),
|
content: Text(widget.help!)
|
||||||
|
.paddingHorizontal(16)
|
||||||
|
.fixWidth(double.infinity),
|
||||||
actions: [
|
actions: [
|
||||||
Button.filled(
|
Button.filled(
|
||||||
onPressed: context.pop,
|
onPressed: context.pop,
|
||||||
@@ -460,11 +466,15 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return SimpleDialog(
|
return ContentDialog(
|
||||||
title: const Text("Add"),
|
title: "Add".tl,
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: canAdd.entries
|
children: canAdd.entries
|
||||||
.map((e) => InkWell(
|
.map(
|
||||||
child: ListTile(title: Text(e.value), key: Key(e.key)),
|
(e) => ListTile(
|
||||||
|
title: Text(e.value),
|
||||||
|
key: Key(e.key),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pop();
|
context.pop();
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -472,10 +482,13 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> {
|
|||||||
});
|
});
|
||||||
updateSetting();
|
updateSetting();
|
||||||
},
|
},
|
||||||
))
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateSetting() {
|
void updateSetting() {
|
||||||
|
@@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.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: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';
|
||||||
|
@@ -10,7 +10,7 @@ void handleLinks() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleAppLink(Uri uri) async {
|
Future<bool> handleAppLink(Uri uri) async {
|
||||||
for(var source in ComicSource.all()) {
|
for(var source in ComicSource.all()) {
|
||||||
if(source.linkHandler != null) {
|
if(source.linkHandler != null) {
|
||||||
if(source.linkHandler!.domains.contains(uri.host)) {
|
if(source.linkHandler!.domains.contains(uri.host)) {
|
||||||
@@ -22,9 +22,11 @@ void handleAppLink(Uri uri) async {
|
|||||||
App.mainNavigatorKey!.currentContext?.to(() {
|
App.mainNavigatorKey!.currentContext?.to(() {
|
||||||
return ComicPage(id: id, sourceKey: source.key);
|
return ComicPage(id: id, sourceKey: source.key);
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
@@ -190,7 +190,6 @@ class IOSDirectoryPicker {
|
|||||||
final String? path = await _channel.invokeMethod('selectDirectory');
|
final String? path = await _channel.invokeMethod('selectDirectory');
|
||||||
return path;
|
return path;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Error selecting directory: $e");
|
|
||||||
// 返回报错信息
|
// 返回报错信息
|
||||||
return e.toString();
|
return e.toString();
|
||||||
}
|
}
|
||||||
|
50
pubspec.lock
50
pubspec.lock
@@ -356,6 +356,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_qjs:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -512,6 +520,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
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:
|
lodepng_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -988,4 +1036,4 @@ packages:
|
|||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.5.4 <4.0.0"
|
dart: ">=3.5.4 <4.0.0"
|
||||||
flutter: ">=3.24.4"
|
flutter: ">=3.24.5"
|
||||||
|
@@ -2,11 +2,11 @@ name: venera
|
|||||||
description: "A comic app."
|
description: "A comic app."
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.0.5+105
|
version: 1.0.6+106
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.5.0 <4.0.0'
|
sdk: '>=3.5.0 <4.0.0'
|
||||||
flutter: 3.24.4
|
flutter: 3.24.5
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@@ -64,6 +64,7 @@ dependencies:
|
|||||||
url: https://github.com/wgh136/webdav_client
|
url: https://github.com/wgh136/webdav_client
|
||||||
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
|
ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1
|
||||||
battery_plus: ^6.2.0
|
battery_plus: ^6.2.0
|
||||||
|
local_auth: ^2.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@@ -33,7 +33,7 @@ WizardStyle=modern
|
|||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
|
Name: "chinesesimplified"; MessagesFile: "{#RootPath}\windows\ChineseSimplified.isl"
|
||||||
|
|
||||||
[Tasks]
|
[Tasks]
|
||||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
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\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\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\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\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\rhttp.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "{#RootPath}\build\windows\x64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "{#RootPath}\build\windows\x64\runner\Release\lodepng_flutter.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
|
import httpx
|
||||||
|
|
||||||
file = open('pubspec.yaml', 'r')
|
file = open('pubspec.yaml', 'r')
|
||||||
content = file.read()
|
content = file.read()
|
||||||
@@ -26,6 +27,13 @@ file = open('windows/build.iss', 'w')
|
|||||||
file.write(newContent)
|
file.write(newContent)
|
||||||
file.close()
|
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)
|
subprocess.run(["iscc", "windows/build.iss"], shell=True)
|
||||||
|
|
||||||
with open('windows/build.iss', 'w') as file:
|
with open('windows/build.iss', 'w') as file:
|
||||||
|
Reference in New Issue
Block a user