initial commit

This commit is contained in:
nyne
2024-09-29 16:17:03 +08:00
commit f08c5cccb9
196 changed files with 16761 additions and 0 deletions

43
.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

42
.metadata Normal file
View File

@@ -0,0 +1,42 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "b0850beeb25f6d5b10426284f506557f66181b36"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: android
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: ios
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: linux
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: macos
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: windows
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
README.md Normal file
View File

@@ -0,0 +1,16 @@
# venera
A comic app.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

29
analysis_options.yaml Normal file
View File

@@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
- collection_methods_unrelated_type
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

96
android/app/build.gradle Normal file
View File

@@ -0,0 +1,96 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader("UTF-8") { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
if (flutterVersionCode == null) {
flutterVersionCode = "1"
}
def flutterVersionName = localProperties.getProperty("flutter.versionName")
if (flutterVersionName == null) {
flutterVersionName = "1.0"
}
def keystorePropertiesFile = rootProject.file("key.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {
namespace = "com.github.wgh136.venera"
compileSdk = flutter.compileSdkVersion
ndkVersion "25.1.8937393"
splits{
abi {
enable true
universalApk true
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
signingConfigs {
debug {
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
}
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.github.wgh136.venera"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
}
buildTypes {
release {
signingConfig signingConfigs.release
applicationVariants.all { variant ->
variant.outputs.all { output ->
def abi = output.getFilter(com.android.build.OutputFile.ABI)
if (abi != null) {
outputFileName = "venera-${variant.versionName}-${abi}.apk"
} else {
outputFileName = "venera-${variant.versionName}.apk"
}
}
}
}
}
}
flutter {
source = "../.."
}
dependencies {
implementation "androidx.activity:activity-ktx:1.9.2"
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,51 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="venera"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".DirectoryPickerActivity"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
/>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,32 @@
package com.github.wgh136.venera
import android.app.Instrumentation
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
class DirectoryPickerActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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)
resultLauncher.launch(intent)
}
private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
val returnIntent = Intent()
if (result.resultCode == RESULT_OK) {
val uri: Uri? = result.data?.data
if (uri != null) {
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
returnIntent.putExtra("directoryUri", uri.toString())
}
}
setResult(RESULT_OK, returnIntent)
finish()
}
}

View File

@@ -0,0 +1,159 @@
package com.github.wgh136.venera
import android.app.Activity
import android.content.ContentResolver
import android.content.Intent
import android.net.Uri
import android.os.Environment
import android.view.KeyEvent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.lang.Exception
class MainActivity : FlutterActivity() {
var volumeListen = VolumeListen()
var listening = false
private val pickDirectoryCode = 1
private lateinit var result: MethodChannel.Result
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == pickDirectoryCode && resultCode == Activity.RESULT_OK) {
val pickedDirectoryUri = data?.getStringExtra("directoryUri")
if (pickedDirectoryUri == null) {
result.success(null)
}
val uri = Uri.parse(pickedDirectoryUri)
Thread {
try {
result.success(onPickedDirectory(uri))
}
catch (e: Exception) {
result.error("Failed to Copy Files", e.toString(), null)
}
}.start()
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"venera/method_channel"
).setMethodCallHandler { call, res ->
when (call.method) {
"getProxy" -> res.success(getProxy())
"setScreenOn" -> {
val set = call.argument<Boolean>("set") ?: false
if (set) {
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
res.success(null)
}
"getDirectoryPath" -> {
this.result = res
val intent = Intent(this, DirectoryPickerActivity::class.java)
startActivityForResult(intent, pickDirectoryCode)
}
else -> res.notImplemented()
}
}
val channel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "venera/volume")
channel.setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
listening = true
volumeListen.onUp = {
events.success(1)
}
volumeListen.onDown = {
events.success(2)
}
}
override fun onCancel(arguments: Any?) {
listening = false
}
})
}
private fun getProxy(): String {
val host = System.getProperty("http.proxyHost")
val port = System.getProperty("http.proxyPort")
return if (host != null && port != null) {
"$host:$port"
} else {
"No Proxy"
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if(listening){
when (keyCode) {
KeyEvent.KEYCODE_VOLUME_DOWN -> {
volumeListen.down()
return true
}
KeyEvent.KEYCODE_VOLUME_UP -> {
volumeListen.up()
return true
}
}
}
return super.onKeyDown(keyCode, event)
}
/// copy the directory to tmp directory, return copied directory
private fun onPickedDirectory(uri: Uri): String {
val tempDir = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "tempDir")
if (!tempDir.exists()) {
tempDir.mkdirs()
}
val contentResolver: ContentResolver = context.contentResolver
val childrenUri = Uri.withAppendedPath(uri, "children")
contentResolver.query(childrenUri, null, null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val documentId = cursor.getString(cursor.getColumnIndexOrThrow("_id"))
val fileUri = Uri.withAppendedPath(uri, documentId)
// 复制文件
val inputStream: InputStream? = contentResolver.openInputStream(fileUri)
val outputStream: OutputStream = FileOutputStream(File(tempDir, documentId))
inputStream?.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
}
}
return tempDir.absolutePath
}
}
class VolumeListen{
var onUp = fun() {}
var onDown = fun() {}
fun up(){
onUp()
}
fun down(){
onDown()
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

18
android/build.gradle Normal file
View File

@@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip

25
android/settings.gradle Normal file
View File

@@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.2.1' apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
}
include ":app"

576
assets/init.js Normal file
View File

@@ -0,0 +1,576 @@
/// encode, decode, hash, decrypt
let Convert = {
/**
* @param {ArrayBuffer} value
* @returns {string}
*/
encodeBase64: (value) => {
return sendMessage({
method: "convert",
type: "base64",
value: value,
isEncode: true
});
},
/**
* @param {string} value
* @returns {ArrayBuffer}
*/
decodeBase64: (value) => {
return sendMessage({
method: "convert",
type: "base64",
value: value,
isEncode: false
});
},
/**
* @param {ArrayBuffer} value
* @returns {ArrayBuffer}
*/
md5: (value) => {
return sendMessage({
method: "convert",
type: "md5",
value: value,
isEncode: true
});
},
/**
* @param {ArrayBuffer} value
* @returns {ArrayBuffer}
*/
sha1: (value) => {
return sendMessage({
method: "convert",
type: "sha1",
value: value,
isEncode: true
});
},
/**
* @param {ArrayBuffer} value
* @returns {ArrayBuffer}
*/
sha256: (value) => {
return sendMessage({
method: "convert",
type: "sha256",
value: value,
isEncode: true
});
},
/**
* @param {ArrayBuffer} value
* @returns {ArrayBuffer}
*/
sha512: (value) => {
return sendMessage({
method: "convert",
type: "sha512",
value: value,
isEncode: true
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @returns {ArrayBuffer}
*/
decryptAesEcb: (value, key) => {
return sendMessage({
method: "convert",
type: "aes-ecb",
value: value,
key: key,
isEncode: false
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @param {ArrayBuffer} iv
* @returns {ArrayBuffer}
*/
decryptAesCbc: (value, key, iv) => {
return sendMessage({
method: "convert",
type: "aes-ecb",
value: value,
key: key,
iv: iv,
isEncode: false
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @param {number} blockSize
* @returns {ArrayBuffer}
*/
decryptAesCfb: (value, key, blockSize) => {
return sendMessage({
method: "convert",
type: "aes-cfb",
value: value,
key: key,
blockSize: blockSize,
isEncode: false
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @param {number} blockSize
* @returns {ArrayBuffer}
*/
decryptAesOfb: (value, key, blockSize) => {
return sendMessage({
method: "convert",
type: "aes-ofb",
value: value,
key: key,
blockSize: blockSize,
isEncode: false
});
},
/**
* @param {ArrayBuffer} value
* @param {ArrayBuffer} key
* @returns {ArrayBuffer}
*/
decryptRsa: (value, key) => {
return sendMessage({
method: "convert",
type: "rsa",
value: value,
key: key,
isEncode: false
});
}
}
function randomInt(min, max) {
return sendMessage({
method: 'random',
min: min,
max: max
});
}
class _Timer {
delay = 0;
callback = () => { };
status = false;
constructor(delay, callback) {
this.delay = delay;
this.callback = callback;
}
run() {
this.status = true;
this._interval();
}
_interval() {
if (!this.status) {
return;
}
this.callback();
setTimeout(this._interval.bind(this), this.delay);
}
cancel() {
this.status = false;
}
}
function setInterval(callback, delay) {
let timer = new _Timer(delay, callback);
timer.run();
return timer;
}
function Cookie(name, value, domain = null) {
let obj = {};
obj.name = name;
obj.value = value;
if (domain) {
obj.domain = domain;
}
return obj;
}
/**
* Network object for sending HTTP requests and managing cookies.
* @namespace Network
*/
let Network = {
/**
* Sends an HTTP request.
* @param {string} method - The HTTP method (e.g., GET, POST, PUT, PATCH, DELETE).
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @returns {Promise<ArrayBuffer>} The response from the request.
*/
async fetchBytes(method, url, headers, data) {
let result = await sendMessage({
method: 'http',
http_method: method,
bytes: true,
url: url,
headers: headers,
data: data,
});
if (result.error) {
throw result.error;
}
return result;
},
/**
* Sends an HTTP request.
* @param {string} method - The HTTP method (e.g., GET, POST, PUT, PATCH, DELETE).
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @returns {Promise<Object>} The response from the request.
*/
async sendRequest(method, url, headers, data) {
let result = await sendMessage({
method: 'http',
http_method: method,
url: url,
headers: headers,
data: data,
});
if (result.error) {
throw result.error;
}
return result;
},
/**
* Sends an HTTP GET request.
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @returns {Promise<Object>} The response from the request.
*/
async get(url, headers) {
return this.sendRequest('GET', url, headers);
},
/**
* Sends an HTTP POST request.
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @returns {Promise<Object>} The response from the request.
*/
async post(url, headers, data) {
return this.sendRequest('POST', url, headers, data);
},
/**
* Sends an HTTP PUT request.
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @returns {Promise<Object>} The response from the request.
*/
async put(url, headers, data) {
return this.sendRequest('PUT', url, headers, data);
},
/**
* Sends an HTTP PATCH request.
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @param data - The data to send with the request.
* @returns {Promise<Object>} The response from the request.
*/
async patch(url, headers, data) {
return this.sendRequest('PATCH', url, headers, data);
},
/**
* Sends an HTTP DELETE request.
* @param {string} url - The URL to send the request to.
* @param {Object} headers - The headers to include in the request.
* @returns {Promise<Object>} The response from the request.
*/
async delete(url, headers) {
return this.sendRequest('DELETE', url, headers);
},
/**
* Sets cookies for a specific URL.
* @param {string} url - The URL to set the cookies for.
* @param {Cookie[]} cookies - The cookies to set.
*/
setCookies(url, cookies) {
sendMessage({
method: 'cookie',
function: 'set',
url: url,
cookies: cookies,
});
},
/**
* Retrieves cookies for a specific URL.
* @param {string} url - The URL to get the cookies from.
* @returns {Promise<Cookie[]>} The cookies for the given URL.
*/
getCookies(url) {
return sendMessage({
method: 'cookie',
function: 'get',
url: url,
});
},
/**
* Deletes cookies for a specific URL.
* @param {string} url - The URL to delete the cookies from.
*/
deleteCookies(url) {
sendMessage({
method: 'cookie',
function: 'delete',
url: url,
});
},
};
/**
* HtmlDocument class for parsing HTML and querying elements.
*/
class HtmlDocument {
static _key = 0;
key = 0;
/**
* Constructor for HtmlDocument.
* @param {string} html - The HTML string to parse.
*/
constructor(html) {
this.key = HtmlDocument._key;
HtmlDocument._key++;
sendMessage({
method: "html",
function: "parse",
key: this.key,
data: html
})
}
/**
* Query a single element from the HTML document.
* @param {string} query - The query string.
* @returns {HtmlElement} The first matching element.
*/
querySelector(query) {
let k = sendMessage({
method: "html",
function: "querySelector",
key: this.key,
query: query
})
if(!k) return null;
return new HtmlElement(k);
}
/**
* Query all matching elements from the HTML document.
* @param {string} query - The query string.
* @returns {HtmlElement[]} An array of matching elements.
*/
querySelectorAll(query) {
let ks = sendMessage({
method: "html",
function: "querySelectorAll",
key: this.key,
query: query
})
return ks.map(k => new HtmlElement(k));
}
}
/**
* HtmlDom class for interacting with HTML elements.
*/
class HtmlElement {
key = 0;
/**
* Constructor for HtmlDom.
* @param {number} k - The key of the element.
*/
constructor(k) {
this.key = k;
}
/**
* Get the text content of the element.
* @returns {string} The text content.
*/
get text() {
return sendMessage({
method: "html",
function: "getText",
key: this.key
})
}
/**
* Get the attributes of the element.
* @returns {Object} The attributes.
*/
get attributes() {
return sendMessage({
method: "html",
function: "getAttributes",
key: this.key
})
}
/**
* Query a single element from the current element.
* @param {string} query - The query string.
* @returns {HtmlElement} The first matching element.
*/
querySelector(query) {
let k = sendMessage({
method: "html",
function: "dom_querySelector",
key: this.key,
query: query
})
if(!k) return null;
return new HtmlElement(k);
}
/**
* Query all matching elements from the current element.
* @param {string} query - The query string.
* @returns {HtmlElement[]} An array of matching elements.
*/
querySelectorAll(query) {
let ks = sendMessage({
method: "html",
function: "dom_querySelectorAll",
key: this.key,
query: query
})
return ks.map(k => new HtmlElement(k));
}
/**
* Get the children of the current element.
* @returns {HtmlElement[]} An array of child elements.
*/
get children() {
let ks = sendMessage({
method: "html",
function: "getChildren",
key: this.key
})
return ks.map(k => new HtmlElement(k));
}
}
function log(level, title, content) {
sendMessage({
method: 'log',
level: level,
title: title,
content: content,
})
}
let console = {
log: (content) => {
log('info', 'JS Console', content)
},
warn: (content) => {
log('warning', 'JS Console', content)
},
error: (content) => {
log('error', 'JS Console', content)
},
};
class ComicSource {
name = ""
key = ""
version = ""
minAppVersion = ""
url = ""
/**
* load data with its key
* @param {string} dataKey
* @returns {any}
*/
loadData(dataKey) {
return sendMessage({
method: 'load_data',
key: this.key,
data_key: dataKey
})
}
/**
* save data
* @param {string} dataKey
* @param data
*/
saveData(dataKey, data) {
return sendMessage({
method: 'save_data',
key: this.key,
data_key: dataKey,
data: data
})
}
/**
* delete data
* @param {string} dataKey
*/
deleteData(dataKey) {
return sendMessage({
method: 'delete_data',
key: this.key,
data_key: dataKey,
})
}
init() { }
static sources = {}
}

3
assets/translation.json Normal file
View File

@@ -0,0 +1,3 @@
{
}

34
ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

44
ios/Podfile Normal file
View File

@@ -0,0 +1,44 @@
# Uncomment this line to define a global platform for your project
platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
#target 'RunnerTests' do
# inherit! :search_paths
#end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.venera;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.venera.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.venera.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.venera.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.venera;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.wgh136.venera;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,80 @@
import Flutter
import UIKit
import UniformTypeIdentifiers
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
var flutterResult: FlutterResult?
var directoryPath: URL!
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let methodChannel = FlutterMethodChannel(name: "venera/method_channel", binaryMessenger: controller.binaryMessenger)
methodChannel.setMethodCallHandler { (call, result) in
if call.method == "getProxy" {
if let proxySettings = CFNetworkCopySystemProxySettings()?.takeUnretainedValue() as NSDictionary?,
let dict = proxySettings.object(forKey: kCFNetworkProxiesHTTPProxy) as? NSDictionary,
let host = dict.object(forKey: kCFNetworkProxiesHTTPProxy) as? String,
let port = dict.object(forKey: kCFNetworkProxiesHTTPPort) as? Int {
let proxyConfig = "\(host):\(port)"
result(proxyConfig)
} else {
result("")
}
} else if call.method == "setScreenOn" {
if let arguments = call.arguments as? Bool {
let screenOn = arguments
UIApplication.shared.isIdleTimerDisabled = screenOn
}
result(nil)
} else if call.method == "getDirectoryPath" {
self.flutterResult = result
self.getDirectoryPath()
} else if call.method == "stopAccessingSecurityScopedResource" {
self.directoryPath?.stopAccessingSecurityScopedResource()
self.directoryPath = nil
result(nil)
} else {
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func getDirectoryPath() {
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.folder], asCopy: false)
documentPicker.delegate = self
documentPicker.allowsMultipleSelection = false
documentPicker.directoryURL = nil
documentPicker.modalPresentationStyle = .formSheet
if let rootViewController = window?.rootViewController {
rootViewController.present(documentPicker, animated: true, completion: nil)
}
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
self.directoryPath = urls.first
if self.directoryPath == nil {
flutterResult?(nil)
return
}
let success = self.directoryPath.startAccessingSecurityScopedResource()
if success {
flutterResult?(self.directoryPath.path)
} else {
flutterResult?(nil)
}
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
flutterResult?(nil)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1,134 @@
{
"images": [
{
"filename": "AppIcon@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "60x60"
},
{
"filename": "AppIcon@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "60x60"
},
{
"filename": "AppIcon~ipad.png",
"idiom": "ipad",
"scale": "1x",
"size": "76x76"
},
{
"filename": "AppIcon@2x~ipad.png",
"idiom": "ipad",
"scale": "2x",
"size": "76x76"
},
{
"filename": "AppIcon-83.5@2x~ipad.png",
"idiom": "ipad",
"scale": "2x",
"size": "83.5x83.5"
},
{
"filename": "AppIcon-40@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "40x40"
},
{
"filename": "AppIcon-40@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "40x40"
},
{
"filename": "AppIcon-40~ipad.png",
"idiom": "ipad",
"scale": "1x",
"size": "40x40"
},
{
"filename": "AppIcon-40@2x~ipad.png",
"idiom": "ipad",
"scale": "2x",
"size": "40x40"
},
{
"filename": "AppIcon-20@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "20x20"
},
{
"filename": "AppIcon-20@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "20x20"
},
{
"filename": "AppIcon-20~ipad.png",
"idiom": "ipad",
"scale": "1x",
"size": "20x20"
},
{
"filename": "AppIcon-20@2x~ipad.png",
"idiom": "ipad",
"scale": "2x",
"size": "20x20"
},
{
"filename": "AppIcon-29.png",
"idiom": "iphone",
"scale": "1x",
"size": "29x29"
},
{
"filename": "AppIcon-29@2x.png",
"idiom": "iphone",
"scale": "2x",
"size": "29x29"
},
{
"filename": "AppIcon-29@3x.png",
"idiom": "iphone",
"scale": "3x",
"size": "29x29"
},
{
"filename": "AppIcon-29~ipad.png",
"idiom": "ipad",
"scale": "1x",
"size": "29x29"
},
{
"filename": "AppIcon-29@2x~ipad.png",
"idiom": "ipad",
"scale": "2x",
"size": "29x29"
},
{
"filename": "AppIcon-60@2x~car.png",
"idiom": "car",
"scale": "2x",
"size": "60x60"
},
{
"filename": "AppIcon-60@3x~car.png",
"idiom": "car",
"scale": "3x",
"size": "60x60"
},
{
"filename": "AppIcon~ios-marketing.png",
"idiom": "ios-marketing",
"scale": "1x",
"size": "1024x1024"
}
],
"info": {
"author": "iconkitchen",
"version": 1
}
}

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

51
ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Venera</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>venera</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSPhotoLibraryUsageDescription</key>
<string>Choose images</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

623
lib/components/appbar.dart Normal file
View File

@@ -0,0 +1,623 @@
part of 'components.dart';
class Appbar extends StatefulWidget implements PreferredSizeWidget {
const Appbar(
{required this.title,
this.leading,
this.actions,
this.backgroundColor,
super.key});
final Widget title;
final Widget? leading;
final List<Widget>? actions;
final Color? backgroundColor;
@override
State<Appbar> createState() => _AppbarState();
@override
Size get preferredSize => const Size.fromHeight(56);
}
class _AppbarState extends State<Appbar> {
ScrollNotificationObserverState? _scrollNotificationObserver;
bool _scrolledUnder = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_scrollNotificationObserver?.removeListener(_handleScrollNotification);
_scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context);
_scrollNotificationObserver?.addListener(_handleScrollNotification);
}
@override
void dispose() {
if (_scrollNotificationObserver != null) {
_scrollNotificationObserver!.removeListener(_handleScrollNotification);
_scrollNotificationObserver = null;
}
super.dispose();
}
void _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification &&
defaultScrollNotificationPredicate(notification)) {
final bool oldScrolledUnder = _scrolledUnder;
final ScrollMetrics metrics = notification.metrics;
switch (metrics.axisDirection) {
case AxisDirection.up:
// Scroll view is reversed
_scrolledUnder = metrics.extentAfter > 0;
case AxisDirection.down:
_scrolledUnder = metrics.extentBefore > 0;
case AxisDirection.right:
case AxisDirection.left:
// Scrolled under is only supported in the vertical axis, and should
// not be altered based on horizontal notifications of the same
// predicate since it could be a 2D scroller.
break;
}
if (_scrolledUnder != oldScrolledUnder) {
setState(() {
// React to a change in MaterialState.scrolledUnder
});
}
}
}
@override
Widget build(BuildContext context) {
var content = SizedBox(
height: _kAppBarHeight,
child: Row(
children: [
const SizedBox(width: 8),
widget.leading ??
Tooltip(
message: "Back".tl,
child: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
),
const SizedBox(
width: 16,
),
Expanded(
child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style.copyWith(fontSize: 20),
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: widget.title,
),
),
...?widget.actions,
const SizedBox(
width: 8,
)
],
),
).paddingTop(context.padding.top);
if (widget.backgroundColor != Colors.transparent) {
return Material(
elevation: (_scrolledUnder && context.width < changePoint) ? 1 : 0,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
color: widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
child: content,
);
}
return content;
}
}
class SliverAppbar extends StatelessWidget {
const SliverAppbar({
super.key,
required this.title,
this.leading,
this.actions,
this.color,
this.radius = 0,
});
final Widget? leading;
final Widget title;
final List<Widget>? actions;
final Color? color;
final double radius;
@override
Widget build(BuildContext context) {
return SliverPersistentHeader(
pinned: true,
delegate: _MySliverAppBarDelegate(
leading: leading,
title: title,
actions: actions,
topPadding: MediaQuery.of(context).padding.top,
color: color,
radius: radius,
),
);
}
}
const _kAppBarHeight = 58.0;
class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final Widget? leading;
final Widget title;
final List<Widget>? actions;
final double topPadding;
final Color? color;
final double radius;
_MySliverAppBarDelegate(
{this.leading,
required this.title,
this.actions,
this.color,
required this.topPadding,
this.radius = 0});
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(
child: Material(
color: color,
elevation: 0,
borderRadius: BorderRadius.circular(radius),
child: Row(
children: [
const SizedBox(width: 8),
leading ??
(Navigator.of(context).canPop()
? Tooltip(
message: "返回".tl,
child: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
)
: const SizedBox()),
const SizedBox(
width: 24,
),
Expanded(
child: DefaultTextStyle(
style:
DefaultTextStyle.of(context).style.copyWith(fontSize: 20),
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: title,
),
),
...?actions,
const SizedBox(
width: 8,
)
],
).paddingTop(topPadding),
),
);
}
@override
double get maxExtent => _kAppBarHeight + topPadding;
@override
double get minExtent => _kAppBarHeight + topPadding;
@override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return oldDelegate is! _MySliverAppBarDelegate ||
leading != oldDelegate.leading ||
title != oldDelegate.title ||
actions != oldDelegate.actions;
}
}
class FloatingSearchBar extends StatefulWidget {
const FloatingSearchBar({
super.key,
this.height = 56,
this.trailing,
required this.onSearch,
required this.controller,
this.onChanged,
});
/// height of search bar
final double height;
/// end of search bar
final Widget? trailing;
/// callback when user do search
final void Function(String) onSearch;
/// controller of [TextField]
final TextEditingController controller;
final void Function(String)? onChanged;
@override
State<FloatingSearchBar> createState() => _FloatingSearchBarState();
}
class _FloatingSearchBarState extends State<FloatingSearchBar> {
double get effectiveHeight {
return math.max(widget.height, 53);
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
var text = widget.controller.text;
if (text.isEmpty) {
text = "Search";
}
var padding = 12.0;
return Container(
padding: EdgeInsets.fromLTRB(padding, 9, padding, 0),
width: double.infinity,
height: effectiveHeight,
child: Material(
elevation: 0,
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(effectiveHeight / 2),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(children: [
Tooltip(
message: "返回".tl,
child: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.pop(),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: TextField(
controller: widget.controller,
decoration: const InputDecoration(
border: InputBorder.none,
),
onSubmitted: (s) {
widget.onSearch(s);
},
onChanged: widget.onChanged,
),
),
),
if (widget.trailing != null) widget.trailing!
]),
),
),
);
}
}
class FilledTabBar extends StatefulWidget {
const FilledTabBar({super.key, this.controller, required this.tabs});
final TabController? controller;
final List<Tab> tabs;
@override
State<FilledTabBar> createState() => _FilledTabBarState();
}
class _FilledTabBarState extends State<FilledTabBar> {
late TabController _controller;
late List<GlobalKey> keys;
static const _kTabHeight = 48.0;
static const tabPadding = EdgeInsets.symmetric(horizontal: 6, vertical: 6);
static const tabRadius = 12.0;
_IndicatorPainter? painter;
var scrollController = ScrollController();
var tabBarKey = GlobalKey();
var offsets = <double>[];
@override
void initState() {
keys = widget.tabs.map((e) => GlobalKey()).toList();
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
void didChangeDependencies() {
_controller = widget.controller ?? DefaultTabController.of(context);
_controller.animation!.addListener(onTabChanged);
initPainter();
super.didChangeDependencies();
}
@override
void didUpdateWidget(covariant FilledTabBar oldWidget) {
if (widget.controller != oldWidget.controller) {
_controller = widget.controller ?? DefaultTabController.of(context);
_controller.animation!.addListener(onTabChanged);
initPainter();
}
super.didUpdateWidget(oldWidget);
}
void initPainter() {
var old = painter;
painter = _IndicatorPainter(
controller: _controller,
color: context.colorScheme.primary,
padding: tabPadding,
radius: tabRadius,
);
if (old != null) {
painter!.update(old.offsets!, old.itemHeight!);
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: buildTabBar,
);
}
void _tabLayoutCallback(List<double> offsets, double itemHeight) {
painter!.update(offsets, itemHeight);
this.offsets = offsets;
}
Widget buildTabBar(BuildContext context, Widget? _) {
var child = SmoothScrollProvider(
controller: scrollController,
builder: (context, controller, physics) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
controller: controller,
physics: physics,
child: CustomPaint(
painter: painter,
child: _TabRow(
callback: _tabLayoutCallback,
children: List.generate(widget.tabs.length, buildTab),
),
).paddingHorizontal(4),
);
},
);
return Container(
key: tabBarKey,
height: _kTabHeight,
width: double.infinity,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: context.colorScheme.outlineVariant,
width: 0.6,
),
),
),
child: widget.tabs.isEmpty
? const SizedBox()
: child
);
}
int? previousIndex;
void onTabChanged() {
final int i = _controller.index;
if (i == previousIndex) {
return;
}
updateScrollOffset(i);
previousIndex = i;
}
void updateScrollOffset(int i) {
// try to scroll to center the tab
final RenderBox tabBarBox =
tabBarKey.currentContext!.findRenderObject() as RenderBox;
final double tabLeft = offsets[i];
final double tabRight = offsets[i + 1];
final double tabWidth = tabRight - tabLeft;
final double tabCenter = tabLeft + tabWidth / 2;
final double tabBarWidth = tabBarBox.size.width;
final double scrollOffset = tabCenter - tabBarWidth / 2;
if (scrollOffset == scrollController.offset) {
return;
}
scrollController.animateTo(
scrollOffset,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
);
}
void onTabClicked(int i) {
_controller.animateTo(i);
}
Widget buildTab(int i) {
return InkWell(
onTap: () => onTabClicked(i),
borderRadius: BorderRadius.circular(tabRadius),
child: KeyedSubtree(
key: keys[i],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style.copyWith(
color: i == _controller.index
? context.colorScheme.primary
: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
child: widget.tabs[i],
),
),
),
).padding(tabPadding);
}
}
typedef _TabRenderCallback = void Function(
List<double> offsets,
double itemHeight,
);
class _TabRow extends Row {
const _TabRow({required this.callback, required super.children});
final _TabRenderCallback callback;
@override
RenderFlex createRenderObject(BuildContext context) {
return _RenderTabFlex(
direction: Axis.horizontal,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
textDirection: Directionality.of(context),
verticalDirection: VerticalDirection.down,
callback: callback);
}
@override
void updateRenderObject(BuildContext context, _RenderTabFlex renderObject) {
super.updateRenderObject(context, renderObject);
renderObject.callback = callback;
}
}
class _RenderTabFlex extends RenderFlex {
_RenderTabFlex({
required super.direction,
required super.mainAxisSize,
required super.mainAxisAlignment,
required super.crossAxisAlignment,
required TextDirection super.textDirection,
required super.verticalDirection,
required this.callback,
});
_TabRenderCallback callback;
@override
void performLayout() {
super.performLayout();
RenderBox? child = firstChild;
final List<double> xOffsets = <double>[];
while (child != null) {
final FlexParentData childParentData =
child.parentData! as FlexParentData;
xOffsets.add(childParentData.offset.dx);
assert(child.parentData == childParentData);
child = childParentData.nextSibling;
}
xOffsets.add(size.width);
callback(xOffsets, firstChild!.size.height);
}
}
class _IndicatorPainter extends CustomPainter {
_IndicatorPainter({
required this.controller,
required this.color,
required this.padding,
this.radius = 4.0,
}) : super(repaint: controller.animation);
final TabController controller;
final Color color;
final EdgeInsets padding;
final double radius;
List<double>? offsets;
double? itemHeight;
Rect? _currentRect;
void update(List<double> offsets, double itemHeight) {
this.offsets = offsets;
this.itemHeight = itemHeight;
}
int get maxTabIndex => offsets!.length - 2;
Rect indicatorRect(Size tabBarSize, int tabIndex) {
assert(offsets != null);
assert(offsets!.isNotEmpty);
assert(tabIndex >= 0);
assert(tabIndex <= maxTabIndex);
var (tabLeft, tabRight) = (offsets![tabIndex], offsets![tabIndex + 1]);
const horizontalPadding = 12.0;
var rect = Rect.fromLTWH(
tabLeft + padding.left + horizontalPadding,
_FilledTabBarState._kTabHeight - 3.6,
tabRight - tabLeft - padding.horizontal - horizontalPadding * 2,
3,
);
return rect;
}
@override
void paint(Canvas canvas, Size size) {
if (offsets == null || itemHeight == null) {
return;
}
final double index = controller.index.toDouble();
final double value = controller.animation!.value;
final bool ltr = index > value;
final int from = (ltr ? value.floor() : value.ceil()).clamp(0, maxTabIndex);
final int to = (ltr ? from + 1 : from - 1).clamp(0, maxTabIndex);
final Rect fromRect = indicatorRect(size, from);
final Rect toRect = indicatorRect(size, to);
_currentRect = Rect.lerp(fromRect, toRect, (value - from).abs());
final Paint paint = Paint()..color = color;
final RRect rrect =
RRect.fromRectAndCorners(_currentRect!, topLeft: Radius.circular(radius), topRight: Radius.circular(radius));
canvas.drawRRect(rrect, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}

309
lib/components/button.dart Normal file
View File

@@ -0,0 +1,309 @@
part of 'components.dart';
class HoverBox extends StatefulWidget {
const HoverBox(
{super.key, required this.child, this.borderRadius = BorderRadius.zero});
final Widget child;
final BorderRadius borderRadius;
@override
State<HoverBox> createState() => _HoverBoxState();
}
class _HoverBoxState extends State<HoverBox> {
bool isHover = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => isHover = true),
onExit: (_) => setState(() => isHover = false),
cursor: SystemMouseCursors.click,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: isHover
? Theme.of(context).colorScheme.surfaceContainerLow
: null,
borderRadius: widget.borderRadius),
child: widget.child,
),
);
}
}
enum ButtonType { filled, outlined, text, normal }
class Button extends StatefulWidget {
const Button(
{super.key,
required this.type,
required this.child,
this.isLoading = false,
this.width,
this.height,
this.padding,
this.color,
this.onPressedAt,
required this.onPressed});
const Button.filled(
{super.key,
required this.child,
required this.onPressed,
this.width,
this.height,
this.padding,
this.color,
this.onPressedAt,
this.isLoading = false})
: type = ButtonType.filled;
const Button.outlined(
{super.key,
required this.child,
required this.onPressed,
this.width,
this.height,
this.padding,
this.color,
this.onPressedAt,
this.isLoading = false})
: type = ButtonType.outlined;
const Button.text(
{super.key,
required this.child,
required this.onPressed,
this.width,
this.height,
this.padding,
this.color,
this.onPressedAt,
this.isLoading = false})
: type = ButtonType.text;
const Button.normal(
{super.key,
required this.child,
required this.onPressed,
this.width,
this.height,
this.padding,
this.color,
this.onPressedAt,
this.isLoading = false})
: type = ButtonType.normal;
static Widget icon(
{Key? key,
required Widget icon,
required VoidCallback onPressed,
double? size,
Color? color,
String? tooltip}) {
return _IconButton(
key: key,
icon: icon,
onPressed: onPressed,
size: size,
color: color,
tooltip: tooltip,
);
}
final ButtonType type;
final Widget child;
final bool isLoading;
final void Function() onPressed;
final void Function(Offset location)? onPressedAt;
final double? width;
final double? height;
final EdgeInsets? padding;
final Color? color;
@override
State<Button> createState() => _ButtonState();
}
class _ButtonState extends State<Button> {
bool isHover = false;
bool isLoading = false;
@override
void didUpdateWidget(covariant Button oldWidget) {
if (oldWidget.isLoading != widget.isLoading) {
setState(() => isLoading = widget.isLoading);
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
var padding = widget.padding ??
const EdgeInsets.symmetric(horizontal: 16, vertical: 6);
var width = widget.width;
if (width != null) {
width = width - padding.horizontal;
}
var height = widget.height;
if (height != null) {
height = height - padding.vertical;
}
Widget child = DefaultTextStyle(
style: TextStyle(
color: textColor,
fontSize: 16,
),
child: isLoading
? CircularProgressIndicator(
color: widget.type == ButtonType.filled
? context.colorScheme.inversePrimary
: context.colorScheme.primary,
strokeWidth: 1.8,
).fixWidth(16).fixHeight(16)
: widget.child,
);
if (width != null || height != null) {
child = child.toCenter();
}
return MouseRegion(
onEnter: (_) => setState(() => isHover = true),
onExit: (_) => setState(() => isHover = false),
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
if (isLoading) return;
widget.onPressed();
if (widget.onPressedAt != null) {
var renderBox = context.findRenderObject() as RenderBox;
var offset = renderBox.localToGlobal(Offset.zero);
widget.onPressedAt!(offset);
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
padding: padding,
decoration: BoxDecoration(
color: buttonColor,
borderRadius: BorderRadius.circular(16),
boxShadow: (isHover && !isLoading && widget.type == ButtonType.filled)
? [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 1),
)
]
: null,
border: widget.type == ButtonType.outlined
? Border.all(
color: widget.color ??
Theme.of(context).colorScheme.outlineVariant,
width: 0.6,
)
: null,
),
child: AnimatedSize(
duration: const Duration(milliseconds: 160),
child: SizedBox(
width: width,
height: height,
child: child,
),
),
),
),
);
}
Color get buttonColor {
if (widget.type == ButtonType.filled) {
var color = widget.color ?? context.colorScheme.primary;
if (isHover) {
return color.withOpacity(0.9);
} else {
return color;
}
}
if (isHover) {
return context.colorScheme.outline.withOpacity(0.2);
}
return Colors.transparent;
}
Color get textColor {
if (widget.type == ButtonType.outlined) {
return widget.color ?? context.colorScheme.onSurface;
}
return widget.type == ButtonType.filled
? context.colorScheme.onPrimary
: (widget.type == ButtonType.text
? widget.color ?? context.colorScheme.primary
: context.colorScheme.onSurface);
}
}
class _IconButton extends StatefulWidget {
const _IconButton(
{super.key,
required this.icon,
required this.onPressed,
this.size,
this.color,
this.tooltip});
final Widget icon;
final VoidCallback onPressed;
final double? size;
final String? tooltip;
final Color? color;
@override
State<_IconButton> createState() => _IconButtonState();
}
class _IconButtonState extends State<_IconButton> {
bool isHover = false;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: widget.onPressed,
mouseCursor: SystemMouseCursors.click,
customBorder: const CircleBorder(),
child: Tooltip(
message: widget.tooltip ?? "",
child: Container(
decoration: BoxDecoration(
color:
isHover ? Theme.of(context).colorScheme.surfaceContainer : null,
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.all(6),
child: IconTheme(
data: IconThemeData(
size: widget.size ?? 24,
color: widget.color ?? context.colorScheme.primary),
child: widget.icon,
),
),
),
);
}
}

View File

@@ -0,0 +1,34 @@
library components;
import 'dart:async';
import 'dart:collection';
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/app_page_route.dart';
import 'package:venera/foundation/appdata.dart';
import 'package:venera/foundation/consts.dart';
import 'package:venera/foundation/res.dart';
import 'package:venera/foundation/state_controller.dart';
import 'package:venera/utils/translations.dart';
part 'image.dart';
part 'appbar.dart';
part 'button.dart';
part 'consts.dart';
part 'flyout.dart';
part 'layout.dart';
part 'loading.dart';
part 'menu.dart';
part 'message.dart';
part 'navigation_bar.dart';
part 'pop_up_widget.dart';
part 'scroll.dart';
part 'select.dart';
part 'side_bar.dart';

View File

@@ -0,0 +1,3 @@
part of 'components.dart';
const _fastAnimationDuration = Duration(milliseconds: 160);

View File

@@ -0,0 +1,222 @@
import 'package:flutter/material.dart';
/// copied from flutter source
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.hovered)) {
return _colors.primary.withOpacity(0.08);
}
if (states.contains(WidgetState.focused)) {
return _colors.primary.withOpacity(0.12);
}
if (states.contains(WidgetState.dragged)) {
return _colors.primary.withOpacity(0.12);
}
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, this.reversed = false, super.key});
final double min;
final double max;
final double value;
final int divisions;
final void Function(double) onChanged;
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: LayoutBuilder(
builder: (context, constrains) => MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: (details){
var dx = details.localPosition.dx;
if(widget.reversed){
dx = constrains.maxWidth - dx;
}
var gap = constrains.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 > constrains.maxWidth || dx < 0) return;
if(widget.reversed){
dx = constrains.maxWidth - dx;
}
var gap = constrains.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(constrains.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: constrains.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 : constrains.maxWidth * ((value - widget.min) / (widget.max - widget.min))-11,
right: !widget.reversed ? null : constrains.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,
),
),
),
)
],
),
),
),
),
),
),
),
);
}
}

315
lib/components/flyout.dart Normal file
View File

@@ -0,0 +1,315 @@
part of "components.dart";
const minFlyoutWidth = 256.0;
const minFlyoutHeight = 128.0;
class FlyoutController {
Function? _show;
void show() {
if (_show == null) {
throw "FlyoutController is not attached to a Flyout";
}
_show!();
}
}
class Flyout extends StatefulWidget {
const Flyout(
{super.key,
required this.flyoutBuilder,
required this.child,
this.enableTap = false,
this.enableDoubleTap = false,
this.enableLongPress = false,
this.enableSecondaryTap = false,
this.withInkWell = false,
this.borderRadius = 0,
this.controller,
this.navigator});
final WidgetBuilder flyoutBuilder;
final Widget child;
final bool enableTap;
final bool enableDoubleTap;
final bool enableLongPress;
final bool enableSecondaryTap;
final bool withInkWell;
final double borderRadius;
final NavigatorState? navigator;
final FlyoutController? controller;
@override
State<Flyout> createState() => _FlyoutState();
}
class _FlyoutState extends State<Flyout> {
@override
void initState() {
if (widget.controller != null) {
widget.controller?._show = show;
}
super.initState();
}
@override
void didChangeDependencies() {
if (widget.controller != null) {
widget.controller?._show = show;
}
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
if (widget.withInkWell) {
return InkWell(
borderRadius: BorderRadius.circular(widget.borderRadius),
onTap: widget.enableTap ? show : null,
onDoubleTap: widget.enableDoubleTap ? show : null,
onLongPress: widget.enableLongPress ? show : null,
onSecondaryTap: widget.enableSecondaryTap ? show : null,
child: widget.child,
);
}
return GestureDetector(
onTap: widget.enableTap ? show : null,
onDoubleTap: widget.enableDoubleTap ? show : null,
onLongPress: widget.enableLongPress ? show : null,
onSecondaryTap: widget.enableSecondaryTap ? show : null,
child: widget.child,
);
}
void show() {
var renderBox = context.findRenderObject() as RenderBox;
var rect = renderBox.localToGlobal(Offset.zero) & renderBox.size;
var navigator = widget.navigator ?? Navigator.of(context);
navigator.push(PageRouteBuilder(
fullscreenDialog: true,
barrierDismissible: true,
opaque: false,
transitionDuration: _fastAnimationDuration,
reverseTransitionDuration: _fastAnimationDuration,
pageBuilder: (context, animation, secondaryAnimation) {
var left = rect.left;
var top = rect.bottom;
if (left + minFlyoutWidth > MediaQuery.of(context).size.width) {
left = MediaQuery.of(context).size.width - minFlyoutWidth;
}
if (top + minFlyoutHeight > MediaQuery.of(context).size.height) {
top = MediaQuery.of(context).size.height - minFlyoutHeight;
}
Widget transition(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget flyout) {
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, -0.05),
end: const Offset(0, 0),
).animate(animation),
child: flyout,
);
}
return Stack(
children: [
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: navigator.pop,
child: AnimatedBuilder(
animation: animation,
builder: (context, builder) {
return ColoredBox(
color: Colors.black.withOpacity(0.3 * animation.value),
);
},
),
),
),
Positioned(
left: left,
right: 0,
top: top,
bottom: 0,
child: transition(
context,
animation,
secondaryAnimation,
Align(
alignment: Alignment.topLeft,
child: widget.flyoutBuilder(context),
)),
)
],
);
}));
}
}
class FlyoutContent extends StatelessWidget {
const FlyoutContent(
{super.key, required this.title, required this.actions, this.content});
final String title;
final String? content;
final List<Widget> actions;
@override
Widget build(BuildContext context) {
return IntrinsicWidth(
child: Material(
borderRadius: BorderRadius.circular(16),
type: MaterialType.card,
elevation: 1,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
child: Container(
constraints: const BoxConstraints(
minWidth: minFlyoutWidth,
),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 16)),
if (content != null)
Padding(
padding: const EdgeInsets.all(8),
child: Text(content!, style: const TextStyle(fontSize: 12)),
),
const SizedBox(
height: 12,
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [const Spacer(), ...actions],
),
],
),
),
).paddingAll(4),
);
}
}
class FlyoutTextButton extends StatefulWidget {
const FlyoutTextButton(
{super.key,
required this.child,
required this.flyoutBuilder,
this.navigator});
final Widget child;
final WidgetBuilder flyoutBuilder;
final NavigatorState? navigator;
@override
State<FlyoutTextButton> createState() => _FlyoutTextButtonState();
}
class _FlyoutTextButtonState extends State<FlyoutTextButton> {
final FlyoutController _controller = FlyoutController();
@override
Widget build(BuildContext context) {
return Flyout(
controller: _controller,
flyoutBuilder: widget.flyoutBuilder,
navigator: widget.navigator,
child: TextButton(
onPressed: () {
_controller.show();
},
child: widget.child,
));
}
}
class FlyoutIconButton extends StatefulWidget {
const FlyoutIconButton(
{super.key,
required this.icon,
required this.flyoutBuilder,
this.navigator});
final Widget icon;
final WidgetBuilder flyoutBuilder;
final NavigatorState? navigator;
@override
State<FlyoutIconButton> createState() => _FlyoutIconButtonState();
}
class _FlyoutIconButtonState extends State<FlyoutIconButton> {
final FlyoutController _controller = FlyoutController();
@override
Widget build(BuildContext context) {
return Flyout(
controller: _controller,
flyoutBuilder: widget.flyoutBuilder,
navigator: widget.navigator,
child: IconButton(
onPressed: () {
_controller.show();
},
icon: widget.icon,
));
}
}
class FlyoutFilledButton extends StatefulWidget {
const FlyoutFilledButton(
{super.key,
required this.child,
required this.flyoutBuilder,
this.navigator});
final Widget child;
final WidgetBuilder flyoutBuilder;
final NavigatorState? navigator;
@override
State<FlyoutFilledButton> createState() => _FlyoutFilledButtonState();
}
class _FlyoutFilledButtonState extends State<FlyoutFilledButton> {
final FlyoutController _controller = FlyoutController();
@override
Widget build(BuildContext context) {
return Flyout(
controller: _controller,
flyoutBuilder: widget.flyoutBuilder,
navigator: widget.navigator,
child: ElevatedButton(
onPressed: () {
_controller.show();
},
child: widget.child,
));
}
}

314
lib/components/image.dart Normal file
View File

@@ -0,0 +1,314 @@
part of 'components.dart';
class AnimatedImage extends StatefulWidget {
/// show animation when loading is complete.
AnimatedImage({
required ImageProvider image,
super.key,
double scale = 1.0,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
this.height,
this.color,
this.opacity,
this.colorBlendMode,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.centerSlice,
this.matchTextDirection = false,
this.gaplessPlayback = false,
this.filterQuality = FilterQuality.medium,
this.isAntiAlias = false,
Map<String, String>? headers,
int? cacheWidth,
int? cacheHeight,
}
): image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, image),
assert(cacheWidth == null || cacheWidth > 0),
assert(cacheHeight == null || cacheHeight > 0);
final ImageProvider image;
final String? semanticLabel;
final bool excludeFromSemantics;
final double? width;
final double? height;
final bool gaplessPlayback;
final bool matchTextDirection;
final Rect? centerSlice;
final ImageRepeat repeat;
final AlignmentGeometry alignment;
final BoxFit? fit;
final BlendMode? colorBlendMode;
final FilterQuality filterQuality;
final Animation<double>? opacity;
final Color? color;
final bool isAntiAlias;
static void clear() => _AnimatedImageState.clear();
@override
State<AnimatedImage> createState() => _AnimatedImageState();
}
class _AnimatedImageState extends State<AnimatedImage> with WidgetsBindingObserver {
ImageStream? _imageStream;
ImageInfo? _imageInfo;
ImageChunkEvent? _loadingProgress;
bool _isListeningToStream = false;
late bool _invertColors;
int? _frameNumber;
bool _wasSynchronouslyLoaded = false;
late DisposableBuildContext<State<AnimatedImage>> _scrollAwareContext;
Object? _lastException;
ImageStreamCompleterHandle? _completerHandle;
static final Map<int, Size> _cache = {};
static clear() => _cache.clear();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_scrollAwareContext = DisposableBuildContext<State<AnimatedImage>>(this);
}
@override
void dispose() {
assert(_imageStream != null);
WidgetsBinding.instance.removeObserver(this);
_stopListeningToStream();
_completerHandle?.dispose();
_scrollAwareContext.dispose();
_replaceImage(info: null);
super.dispose();
}
@override
void didChangeDependencies() {
_updateInvertColors();
_resolveImage();
if (TickerMode.of(context)) {
_listenToStream();
} else {
_stopListeningToStream(keepStreamAlive: true);
}
super.didChangeDependencies();
}
@override
void didUpdateWidget(AnimatedImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.image != oldWidget.image) {
_resolveImage();
}
}
@override
void didChangeAccessibilityFeatures() {
super.didChangeAccessibilityFeatures();
setState(() {
_updateInvertColors();
});
}
@override
void reassemble() {
_resolveImage(); // in case the image cache was flushed
super.reassemble();
}
void _updateInvertColors() {
_invertColors = MediaQuery.maybeInvertColorsOf(context)
?? SemanticsBinding.instance.accessibilityFeatures.invertColors;
}
void _resolveImage() {
final ScrollAwareImageProvider provider = ScrollAwareImageProvider<Object>(
context: _scrollAwareContext,
imageProvider: widget.image,
);
final ImageStream newStream =
provider.resolve(createLocalImageConfiguration(
context,
size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null,
));
_updateSourceStream(newStream);
}
ImageStreamListener? _imageStreamListener;
ImageStreamListener _getListener({bool recreateListener = false}) {
if(_imageStreamListener == null || recreateListener) {
_lastException = null;
_imageStreamListener = ImageStreamListener(
_handleImageFrame,
onChunk: _handleImageChunk,
onError: (Object error, StackTrace? stackTrace) {
setState(() {
_lastException = error;
});
},
);
}
return _imageStreamListener!;
}
void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
_replaceImage(info: imageInfo);
_loadingProgress = null;
_lastException = null;
_frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1;
_wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall;
});
}
void _handleImageChunk(ImageChunkEvent event) {
setState(() {
_loadingProgress = event;
_lastException = null;
});
}
void _replaceImage({required ImageInfo? info}) {
final ImageInfo? oldImageInfo = _imageInfo;
SchedulerBinding.instance.addPostFrameCallback((_) => oldImageInfo?.dispose());
_imageInfo = info;
}
// Updates _imageStream to newStream, and moves the stream listener
// registration from the old stream to the new stream (if a listener was
// registered).
void _updateSourceStream(ImageStream newStream) {
if (_imageStream?.key == newStream.key) {
return;
}
if (_isListeningToStream) {
_imageStream!.removeListener(_getListener());
}
if (!widget.gaplessPlayback) {
setState(() { _replaceImage(info: null); });
}
setState(() {
_loadingProgress = null;
_frameNumber = null;
_wasSynchronouslyLoaded = false;
});
_imageStream = newStream;
if (_isListeningToStream) {
_imageStream!.addListener(_getListener());
}
}
void _listenToStream() {
if (_isListeningToStream) {
return;
}
_imageStream!.addListener(_getListener());
_completerHandle?.dispose();
_completerHandle = null;
_isListeningToStream = true;
}
/// Stops listening to the image stream, if this state object has attached a
/// listener.
///
/// If the listener from this state is the last listener on the stream, the
/// stream will be disposed. To keep the stream alive, set `keepStreamAlive`
/// to true, which create [ImageStreamCompleterHandle] to keep the completer
/// alive and is compatible with the [TickerMode] being off.
void _stopListeningToStream({bool keepStreamAlive = false}) {
if (!_isListeningToStream) {
return;
}
if (keepStreamAlive && _completerHandle == null && _imageStream?.completer != null) {
_completerHandle = _imageStream!.completer!.keepAlive();
}
_imageStream!.removeListener(_getListener());
_isListeningToStream = false;
}
@override
Widget build(BuildContext context) {
Widget result;
if(_imageInfo != null){
result = RawImage(
image: _imageInfo?.image,
width: widget.width,
height: widget.height,
debugImageLabel: _imageInfo?.debugLabel,
scale: _imageInfo?.scale ?? 1.0,
color: widget.color,
opacity: widget.opacity,
colorBlendMode: widget.colorBlendMode,
fit: BoxFit.cover,
alignment: widget.alignment,
repeat: widget.repeat,
centerSlice: widget.centerSlice,
matchTextDirection: widget.matchTextDirection,
invertColors: _invertColors,
isAntiAlias: widget.isAntiAlias,
filterQuality: widget.filterQuality,
);
} else if (_lastException != null) {
result = const Center(
child: Icon(Icons.error),
);
if (!widget.excludeFromSemantics) {
result = Semantics(
container: widget.semanticLabel != null,
image: true,
label: widget.semanticLabel ?? '',
child: result,
);
}
} else{
result = const Center();
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
reverseDuration: const Duration(milliseconds: 200),
child: result,
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(DiagnosticsProperty<ImageStream>('stream', _imageStream));
description.add(DiagnosticsProperty<ImageInfo>('pixels', _imageInfo));
description.add(DiagnosticsProperty<ImageChunkEvent>('loadingProgress', _loadingProgress));
description.add(DiagnosticsProperty<int>('frameNumber', _frameNumber));
description.add(DiagnosticsProperty<bool>('wasSynchronouslyLoaded', _wasSynchronouslyLoaded));
}
}

139
lib/components/layout.dart Normal file
View File

@@ -0,0 +1,139 @@
part of 'components.dart';
class SliverGridViewWithFixedItemHeight extends StatelessWidget {
const SliverGridViewWithFixedItemHeight(
{required this.delegate,
required this.maxCrossAxisExtent,
required this.itemHeight,
super.key});
final SliverChildDelegate delegate;
final double maxCrossAxisExtent;
final double itemHeight;
@override
Widget build(BuildContext context) {
return SliverLayoutBuilder(
builder: ((context, constraints) => SliverGrid(
delegate: delegate,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: maxCrossAxisExtent,
childAspectRatio:
calcChildAspectRatio(constraints.crossAxisExtent)),
)));
}
double calcChildAspectRatio(double width) {
var crossItems = width ~/ maxCrossAxisExtent;
if (width % maxCrossAxisExtent != 0) {
crossItems += 1;
}
final itemWidth = width / crossItems;
return itemWidth / itemHeight;
}
}
class SliverGridDelegateWithFixedHeight extends SliverGridDelegate{
const SliverGridDelegateWithFixedHeight({
required this.maxCrossAxisExtent,
required this.itemHeight,
});
final double maxCrossAxisExtent;
final double itemHeight;
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
final width = constraints.crossAxisExtent;
var crossItems = width ~/ maxCrossAxisExtent;
if (width % maxCrossAxisExtent != 0) {
crossItems += 1;
}
return SliverGridRegularTileLayout(
crossAxisCount: crossItems,
mainAxisStride: itemHeight,
crossAxisStride: width / crossItems,
childMainAxisExtent: itemHeight,
childCrossAxisExtent: width / crossItems,
reverseCrossAxis: false
);
}
@override
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
if(oldDelegate is! SliverGridDelegateWithFixedHeight) return true;
if(oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent
|| oldDelegate.itemHeight != itemHeight){
return true;
}
return false;
}
}
class SliverGridDelegateWithComics extends SliverGridDelegate{
SliverGridDelegateWithComics([this.useBriefMode = false, this.scale]);
final bool useBriefMode;
final double? scale;
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
if(appdata.settings['comicDisplayMode'] == 'brief' || useBriefMode){
return getBriefModeLayout(constraints, scale ?? appdata.settings['comicTileScale']);
} else {
return getDetailedModeLayout(constraints, scale ?? appdata.settings['comicTileScale']);
}
}
SliverGridLayout getDetailedModeLayout(SliverConstraints constraints, double scale){
const maxCrossAxisExtent = 650;
final itemHeight = 164 * scale;
final width = constraints.crossAxisExtent;
var crossItems = width ~/ maxCrossAxisExtent;
if (width % maxCrossAxisExtent != 0) {
crossItems += 1;
}
return SliverGridRegularTileLayout(
crossAxisCount: crossItems,
mainAxisStride: itemHeight,
crossAxisStride: width / crossItems,
childMainAxisExtent: itemHeight,
childCrossAxisExtent: width / crossItems,
reverseCrossAxis: false
);
}
SliverGridLayout getBriefModeLayout(SliverConstraints constraints, double scale){
final maxCrossAxisExtent = 192.0 * scale;
const childAspectRatio = 0.72;
const crossAxisSpacing = 0.0;
int crossAxisCount = (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing)).ceil();
// Ensure a minimum count of 1, can be zero and result in an infinite extent
// below when the window size is 0.
crossAxisCount = math.max(1, crossAxisCount);
final double usableCrossAxisExtent = math.max(
0.0,
constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1),
);
final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
final double childMainAxisExtent = childCrossAxisExtent / childAspectRatio;
return SliverGridRegularTileLayout(
crossAxisCount: crossAxisCount,
mainAxisStride: childMainAxisExtent,
crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
childMainAxisExtent: childMainAxisExtent,
childCrossAxisExtent: childCrossAxisExtent,
reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
);
}
@override
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
return true;
}
}

391
lib/components/loading.dart Normal file
View File

@@ -0,0 +1,391 @@
part of 'components.dart';
class NetworkError extends StatelessWidget {
const NetworkError({
super.key,
required this.message,
this.retry,
this.withAppbar = true,
});
final String message;
final void Function()? retry;
final bool withAppbar;
@override
Widget build(BuildContext context) {
Widget body = Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline,
size: 60,
),
const SizedBox(
height: 4,
),
Text(
message,
textAlign: TextAlign.center,
maxLines: 3,
),
if (retry != null)
const SizedBox(
height: 4,
),
if (retry != null)
FilledButton(onPressed: retry, child: Text('重试'.tl))
],
),
);
if (withAppbar) {
body = Column(
children: [
const Appbar(title: Text("")),
Expanded(
child: body,
)
],
);
}
return Material(
child: body,
);
}
}
class ListLoadingIndicator extends StatelessWidget {
const ListLoadingIndicator({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox(
width: double.infinity,
height: 80,
child: Center(
child: FiveDotLoadingAnimation(),
),
);
}
}
abstract class LoadingState<T extends StatefulWidget, S extends Object>
extends State<T> {
bool isLoading = false;
S? data;
String? error;
Future<Res<S>> loadData();
Widget buildContent(BuildContext context, S data);
Widget? buildFrame(BuildContext context, Widget child) => null;
Widget buildLoading() {
return Center(
child: const CircularProgressIndicator(
strokeWidth: 2,
).fixWidth(32).fixHeight(32),
);
}
void retry() {
setState(() {
isLoading = true;
error = null;
});
loadData().then((value) {
if (value.success) {
setState(() {
isLoading = false;
data = value.data;
});
} else {
setState(() {
isLoading = false;
error = value.errorMessage!;
});
}
});
}
Widget buildError() {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
error!,
maxLines: 3,
),
const SizedBox(height: 12),
Button.text(
onPressed: retry,
child: const Text("Retry"),
)
],
),
).paddingHorizontal(16);
}
@override
@mustCallSuper
void initState() {
isLoading = true;
Future.microtask(() {
loadData().then((value) {
if (value.success) {
setState(() {
isLoading = false;
data = value.data;
});
} else {
setState(() {
isLoading = false;
error = value.errorMessage!;
});
}
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
Widget child;
if (isLoading) {
child = buildLoading();
} else if (error != null) {
child = buildError();
} else {
child = buildContent(context, data!);
}
return buildFrame(context, child) ?? child;
}
}
abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
extends State<T> {
bool _isFirstLoading = true;
bool _isLoading = false;
List<S>? data;
String? _error;
int _page = 1;
int _maxPage = 1;
Future<Res<List<S>>> loadData(int page);
Widget? buildFrame(BuildContext context, Widget child) => null;
Widget buildContent(BuildContext context, List<S> data);
bool get isLoading => _isLoading || _isFirstLoading;
bool get isFirstLoading => _isFirstLoading;
bool get haveNextPage => _page <= _maxPage;
void nextPage() {
if (_page > _maxPage) return;
if (_isLoading) return;
_isLoading = true;
loadData(_page).then((value) {
_isLoading = false;
if (mounted) {
if (value.success) {
_page++;
if (value.subData is int) {
_maxPage = value.subData as int;
}
setState(() {
data!.addAll(value.data);
});
} else {
var message = value.errorMessage ?? "Network Error";
if (message.length > 20) {
message = "${message.substring(0, 20)}...";
}
context.showMessage(message: message);
}
}
});
}
void reset() {
setState(() {
_isFirstLoading = true;
_isLoading = false;
data = null;
_error = null;
_page = 1;
});
firstLoad();
}
void firstLoad() {
Future.microtask(() {
loadData(_page).then((value) {
if (!mounted) return;
if (value.success) {
_page++;
if (value.subData is int) {
_maxPage = value.subData as int;
}
setState(() {
_isFirstLoading = false;
data = value.data;
});
} else {
setState(() {
_isFirstLoading = false;
_error = value.errorMessage!;
});
}
});
});
}
@override
void initState() {
firstLoad();
super.initState();
}
Widget buildLoading(BuildContext context) {
return Center(
child: const CircularProgressIndicator(
strokeWidth: 2,
).fixWidth(32).fixHeight(32),
);
}
Widget buildError(BuildContext context, String error) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(error, maxLines: 3),
const SizedBox(height: 12),
Button.outlined(
onPressed: () {
reset();
},
child: const Text("Retry"),
)
],
),
).paddingHorizontal(16);
}
@override
Widget build(BuildContext context) {
Widget child;
if (_isFirstLoading) {
child = buildLoading(context);
} else if (_error != null) {
child = buildError(context, _error!);
} else {
child = NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (notification.metrics.pixels ==
notification.metrics.maxScrollExtent) {
nextPage();
}
return false;
},
child: buildContent(context, data!),
);
}
return buildFrame(context, child) ?? child;
}
}
class FiveDotLoadingAnimation extends StatefulWidget {
const FiveDotLoadingAnimation({super.key});
@override
State<FiveDotLoadingAnimation> createState() =>
_FiveDotLoadingAnimationState();
}
class _FiveDotLoadingAnimationState extends State<FiveDotLoadingAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
upperBound: 6,
)..repeat(min: 0, max: 5.2, period: const Duration(milliseconds: 1200));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
static const _colors = [
Colors.red,
Colors.green,
Colors.blue,
Colors.yellow,
Colors.purple
];
static const _padding = 12.0;
static const _dotSize = 12.0;
static const _height = 24.0;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return SizedBox(
width: _dotSize * 5 + _padding * 6,
height: _height,
child: Stack(
children: List.generate(5, (index) => buildDot(index)),
),
);
});
}
Widget buildDot(int index) {
var value = _controller.value;
var startValue = index * 0.8;
return Positioned(
left: index * _dotSize + (index + 1) * _padding,
bottom: (math.sin(math.pi / 2 * (value - startValue).clamp(0, 2))) *
(_height - _dotSize),
child: Container(
width: _dotSize,
height: _dotSize,
decoration: BoxDecoration(
color: _colors[index],
shape: BoxShape.circle,
),
),
);
}
}

117
lib/components/menu.dart Normal file
View File

@@ -0,0 +1,117 @@
part of "components.dart";
void showDesktopMenu(
BuildContext context, Offset location, List<DesktopMenuEntry> entries) {
Navigator.of(context).push(DesktopMenuRoute(entries, location));
}
class DesktopMenuRoute<T> extends PopupRoute<T> {
final List<DesktopMenuEntry> entries;
final Offset location;
DesktopMenuRoute(this.entries, this.location);
@override
Color? get barrierColor => Colors.transparent;
@override
bool get barrierDismissible => true;
@override
String? get barrierLabel => "menu";
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
const width = 196.0;
final size = MediaQuery.of(context).size;
var left = location.dx;
if (left + width > size.width - 10) {
left = size.width - width - 10;
}
var top = location.dy;
var height = 16 + 32 * entries.length;
if (top + height > size.height - 15) {
top = size.height - height - 15;
}
return Stack(
children: [
Positioned(
left: left,
top: top,
child: Container(
width: width,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 6),
decoration: BoxDecoration(
color: context.colorScheme.surface,
borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
]),
child: Material(
child: Column(
mainAxisSize: MainAxisSize.min,
children: entries.map((e) => buildEntry(e, context)).toList(),
),
),
),
)
],
);
}
Widget buildEntry(DesktopMenuEntry entry, BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(4),
onTap: () {
Navigator.of(context).pop();
entry.onClick();
},
child: SizedBox(
height: 32,
child: Row(
children: [
const SizedBox(
width: 4,
),
if (entry.icon != null)
Icon(
entry.icon,
size: 18,
),
const SizedBox(
width: 4,
),
Text(entry.text),
],
),
),
);
}
@override
Duration get transitionDuration => const Duration(milliseconds: 200);
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(
opacity: animation.drive(Tween<double>(begin: 0, end: 1)
.chain(CurveTween(curve: Curves.ease))),
child: child,
);
}
}
class DesktopMenuEntry {
final String text;
final IconData? icon;
final void Function() onClick;
DesktopMenuEntry({required this.text, this.icon, required this.onClick});
}

217
lib/components/message.dart Normal file
View File

@@ -0,0 +1,217 @@
part of "components.dart";
class OverlayWidget extends StatefulWidget {
const OverlayWidget(this.child, {super.key});
final Widget child;
@override
State<OverlayWidget> createState() => OverlayWidgetState();
}
class OverlayWidgetState extends State<OverlayWidget> {
final overlayKey = GlobalKey<OverlayState>();
var entries = <OverlayEntry>[];
void addOverlay(OverlayEntry entry) {
if (overlayKey.currentState != null) {
overlayKey.currentState!.insert(entry);
entries.add(entry);
}
}
void remove(OverlayEntry entry) {
if (entries.remove(entry)) {
entry.remove();
}
}
void removeAll() {
for (var entry in entries) {
entry.remove();
}
entries.clear();
}
@override
Widget build(BuildContext context) {
return Overlay(
key: overlayKey,
initialEntries: [OverlayEntry(builder: (context) => widget.child)],
);
}
}
void showDialogMessage(BuildContext context, String title, String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: context.pop,
child: Text("OK".tl),
)
],
),
);
}
void showConfirmDialog(BuildContext context, String title, String content,
void Function() onConfirm) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
onPressed: context.pop, child: Text("Cancel".tl)),
TextButton(
onPressed: () {
context.pop();
onConfirm();
},
child: Text("Confirm".tl)),
],
));
}
class LoadingDialogController {
void Function()? closeDialog;
bool closed = false;
void close() {
if (closed) {
return;
}
closed = true;
if (closeDialog == null) {
Future.microtask(closeDialog!);
} else {
closeDialog!();
}
}
}
LoadingDialogController showLoadingDialog(BuildContext context,
{void Function()? onCancel,
bool barrierDismissible = true,
bool allowCancel = true,
String? message,
String cancelButtonText = "Cancel"}) {
var controller = LoadingDialogController();
var loadingDialogRoute = DialogRoute(
context: context,
barrierDismissible: barrierDismissible,
builder: (BuildContext context) {
return Dialog(
child: Container(
width: 100,
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
const SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(),
),
const SizedBox(
width: 16,
),
Text(
message ?? 'Loading',
style: const TextStyle(fontSize: 16),
),
const Spacer(),
if (allowCancel)
TextButton(
onPressed: () {
controller.close();
onCancel?.call();
},
child: Text(cancelButtonText.tl))
],
),
),
);
});
var navigator = Navigator.of(context);
navigator.push(loadingDialogRoute).then((value) => controller.closed = true);
controller.closeDialog = () {
navigator.removeRoute(loadingDialogRoute);
};
return controller;
}
class ContentDialog extends StatelessWidget {
const ContentDialog({
super.key,
required this.title,
required this.content,
this.dismissible = true,
this.actions = const [],
});
final String title;
final Widget content;
final List<Widget> actions;
final bool dismissible;
@override
Widget build(BuildContext context) {
var content = Column(
mainAxisSize: MainAxisSize.min,
children: [
Appbar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: dismissible ? context.pop : null,
),
title: Text(title),
backgroundColor: Colors.transparent,
),
this.content,
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: actions,
).paddingRight(12),
const SizedBox(height: 16),
],
);
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
insetPadding: context.width < 400
? const EdgeInsets.symmetric(horizontal: 4)
: const EdgeInsets.symmetric(horizontal: 16),
child: IntrinsicWidth(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 600,
minWidth: math.min(400, context.width - 16),
),
child: MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: content,
),
),
),
);
}
}

View File

@@ -0,0 +1,695 @@
part of 'components.dart';
class PaneItemEntry {
String label;
IconData icon;
IconData activeIcon;
PaneItemEntry(
{required this.label, required this.icon, required this.activeIcon});
}
class PaneActionEntry {
String label;
IconData icon;
VoidCallback onTap;
PaneActionEntry(
{required this.label, required this.icon, required this.onTap});
}
class NaviPane extends StatefulWidget {
const NaviPane(
{required this.paneItems,
required this.paneActions,
required this.pageBuilder,
this.initialPage = 0,
this.onPageChange,
required this.observer,
super.key});
final List<PaneItemEntry> paneItems;
final List<PaneActionEntry> paneActions;
final Widget Function(int page) pageBuilder;
final void Function(int index)? onPageChange;
final int initialPage;
final NaviObserver observer;
@override
State<NaviPane> createState() => _NaviPaneState();
}
class _NaviPaneState extends State<NaviPane>
with SingleTickerProviderStateMixin {
late int _currentPage = widget.initialPage;
int get currentPage => _currentPage;
set currentPage(int value) {
if (value == _currentPage) return;
_currentPage = value;
widget.onPageChange?.call(value);
}
late AnimationController controller;
static const _kBottomBarHeight = 58.0;
static const _kFoldedSideBarWidth = 80.0;
static const _kSideBarWidth = 256.0;
static const _kTopBarHeight = 48.0;
double get bottomBarHeight =>
_kBottomBarHeight + MediaQuery.of(context).padding.bottom;
void onNavigatorStateChange() {
onRebuild(context);
}
@override
void initState() {
controller = AnimationController(
duration: const Duration(milliseconds: 250),
lowerBound: 0,
upperBound: 3,
vsync: this,
);
widget.observer.addListener(onNavigatorStateChange);
StateController.put(NaviPaddingWidgetController());
super.initState();
}
@override
void dispose() {
StateController.remove<NaviPaddingWidgetController>();
controller.dispose();
widget.observer.removeListener(onNavigatorStateChange);
super.dispose();
}
double targetFormContext(BuildContext context) {
var width = MediaQuery.of(context).size.width;
double target = 0;
if (widget.observer.pageCount > 1) {
target = 1;
}
if (width > changePoint) {
target = 2;
}
if (width > changePoint2) {
target = 3;
}
return target;
}
double? animationTarget;
void onRebuild(BuildContext context) {
double target = targetFormContext(context);
if (controller.value != target || animationTarget != target) {
if (controller.isAnimating) {
if (animationTarget == target) {
return;
} else {
controller.stop();
}
}
if (target == 1) {
StateController.find<NaviPaddingWidgetController>()
.setWithPadding(true);
controller.value = target;
} else if (controller.value == 1 && target == 0) {
StateController.findOrNull<NaviPaddingWidgetController>()
?.setWithPadding(false);
controller.value = target;
} else {
controller.animateTo(target);
}
animationTarget = target;
}
}
@override
Widget build(BuildContext context) {
onRebuild(context);
return _NaviPopScope(
action: () {
if (App.mainNavigatorKey!.currentState!.canPop()) {
App.mainNavigatorKey!.currentState!.pop();
} else {
SystemNavigator.pop();
}
},
popGesture: App.isIOS && context.width >= changePoint,
child: AnimatedBuilder(
animation: controller,
builder: (context, child) {
final value = controller.value;
return Stack(
children: [
if (value <= 1)
Positioned(
left: 0,
right: 0,
bottom: bottomBarHeight * (0 - value),
child: buildBottom(),
),
if (value <= 1)
Positioned(
left: 0,
right: 0,
top: _kTopBarHeight * (0 - value) +
MediaQuery.of(context).padding.top * (1 - value),
child: buildTop(),
),
Positioned(
left: _kFoldedSideBarWidth * ((value - 2.0).clamp(-1.0, 0.0)),
top: 0,
bottom: 0,
child: buildLeft(),
),
Positioned(
top: _kTopBarHeight * ((1 - value).clamp(0, 1)) +
MediaQuery.of(context).padding.top * (value == 1 ? 0 : 1),
left: _kFoldedSideBarWidth * ((value - 1).clamp(0, 1)) +
(_kSideBarWidth - _kFoldedSideBarWidth) *
((value - 2).clamp(0, 1)),
right: 0,
bottom: bottomBarHeight * ((1 - value).clamp(0, 1)),
child: MediaQuery.removePadding(
removeTop: value >= 2 || value == 0,
context: context,
child: Material(child: widget.pageBuilder(currentPage)),
),
),
],
);
},
),
);
}
Widget buildTop() {
return Material(
child: Container(
padding: const EdgeInsets.only(left: 16, right: 16),
height: _kTopBarHeight,
width: double.infinity,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
width: 1,
),
),
),
child: Row(
children: [
Text(
widget.paneItems[currentPage].label,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const Spacer(),
for (var action in widget.paneActions)
Tooltip(
message: action.label,
child: IconButton(
icon: Icon(action.icon),
onPressed: action.onTap,
),
)
],
),
),
);
}
Widget buildBottom() {
return Material(
textStyle: Theme.of(context).textTheme.labelSmall,
elevation: 0,
child: Container(
height: _kBottomBarHeight + MediaQuery.of(context).padding.bottom,
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
width: 1,
),
),
),
child: Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Row(
children: List<Widget>.generate(
widget.paneItems.length,
(index) => Expanded(
child: _SingleBottomNaviWidget(
enabled: currentPage == index,
entry: widget.paneItems[index],
onTap: () {
setState(() {
currentPage = index;
});
},
key: ValueKey(index),
))),
),
),
),
);
}
Widget buildLeft() {
final value = controller.value;
const paddingHorizontal = 16.0;
return Material(
child: Container(
width: _kFoldedSideBarWidth +
(_kSideBarWidth - _kFoldedSideBarWidth) * ((value - 2).clamp(0, 1)),
height: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: paddingHorizontal),
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
width: 1,
),
),
),
child: Row(
children: [
SizedBox(
width: value == 3
? (_kSideBarWidth - paddingHorizontal * 2 - 1)
: (_kFoldedSideBarWidth - paddingHorizontal * 2 - 1),
child: Column(
children: [
const SizedBox(height: 16),
SizedBox(height: MediaQuery.of(context).padding.top),
...List<Widget>.generate(
widget.paneItems.length,
(index) => _SideNaviWidget(
enabled: currentPage == index,
entry: widget.paneItems[index],
showTitle: value == 3,
onTap: () {
setState(() {
currentPage = index;
});
},
key: ValueKey(index),
),
),
const Spacer(),
...List<Widget>.generate(
widget.paneActions.length,
(index) => _PaneActionWidget(
entry: widget.paneActions[index],
showTitle: value == 3,
key: ValueKey(index + widget.paneItems.length),
),
),
const SizedBox(
height: 16,
)
],
),
),
const Spacer(),
],
),
),
);
}
}
class _SideNaviWidget extends StatefulWidget {
const _SideNaviWidget(
{required this.enabled,
required this.entry,
required this.onTap,
required this.showTitle,
super.key});
final bool enabled;
final PaneItemEntry entry;
final VoidCallback onTap;
final bool showTitle;
@override
State<_SideNaviWidget> createState() => _SideNaviWidgetState();
}
class _SideNaviWidgetState extends State<_SideNaviWidget> {
bool isHovering = false;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final icon =
Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon);
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (details) => setState(() => isHovering = true),
onExit: (details) => setState(() => isHovering = false),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: widget.onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 12),
width: double.infinity,
height: 42,
decoration: BoxDecoration(
color: widget.enabled
? colorScheme.primaryContainer
: isHovering
? colorScheme.surfaceContainerHigh
: null,
borderRadius: BorderRadius.circular(8),
),
child: widget.showTitle
? Row(
children: [
icon,
const SizedBox(
width: 12,
),
Text(widget.entry.label)
],
)
: Center(
child: icon,
)),
),
);
}
}
class _PaneActionWidget extends StatefulWidget {
const _PaneActionWidget(
{required this.entry, required this.showTitle, super.key});
final PaneActionEntry entry;
final bool showTitle;
@override
State<_PaneActionWidget> createState() => _PaneActionWidgetState();
}
class _PaneActionWidgetState extends State<_PaneActionWidget> {
bool isHovering = false;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final icon = Icon(widget.entry.icon);
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (details) => setState(() => isHovering = true),
onExit: (details) => setState(() => isHovering = false),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: widget.entry.onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 12),
width: double.infinity,
height: 42,
decoration: BoxDecoration(
color: isHovering ? colorScheme.surfaceContainerHigh : null,
borderRadius: BorderRadius.circular(8)),
child: widget.showTitle
? Row(
children: [
icon,
const SizedBox(
width: 12,
),
Text(widget.entry.label)
],
)
: Center(
child: icon,
)),
),
);
}
}
class _SingleBottomNaviWidget extends StatefulWidget {
const _SingleBottomNaviWidget(
{required this.enabled,
required this.entry,
required this.onTap,
super.key});
final bool enabled;
final PaneItemEntry entry;
final VoidCallback onTap;
@override
State<_SingleBottomNaviWidget> createState() =>
_SingleBottomNaviWidgetState();
}
class _SingleBottomNaviWidgetState extends State<_SingleBottomNaviWidget>
with SingleTickerProviderStateMixin {
late AnimationController controller;
bool isHovering = false;
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant _SingleBottomNaviWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.enabled != widget.enabled) {
if (widget.enabled) {
controller.forward(from: 0);
} else {
controller.reverse(from: 1);
}
}
}
@override
void initState() {
super.initState();
controller = AnimationController(
value: widget.enabled ? 1 : 0,
vsync: this,
duration: _fastAnimationDuration,
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: CurvedAnimation(parent: controller, curve: Curves.ease),
builder: (context, child) {
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (details) => setState(() => isHovering = true),
onExit: (details) => setState(() => isHovering = false),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: widget.onTap,
child: buildContent(),
),
);
},
);
}
Widget buildContent() {
final value = controller.value;
final colorScheme = Theme.of(context).colorScheme;
final icon =
Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon);
return Center(
child: Container(
width: 64,
height: 28,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(32)),
color: isHovering ? colorScheme.surfaceContainer : Colors.transparent,
),
child: Center(
child: Container(
width: 32 + value * 32,
height: 28,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(32)),
color: value != 0
? colorScheme.secondaryContainer
: Colors.transparent,
),
child: Center(child: icon),
),
),
),
);
}
}
class NaviObserver extends NavigatorObserver implements Listenable {
var routes = Queue<Route>();
int get pageCount => routes.length;
@override
void didPop(Route route, Route? previousRoute) {
routes.removeLast();
notifyListeners();
}
@override
void didPush(Route route, Route? previousRoute) {
routes.addLast(route);
notifyListeners();
}
@override
void didRemove(Route route, Route? previousRoute) {
routes.remove(route);
notifyListeners();
}
@override
void didReplace({Route? newRoute, Route? oldRoute}) {
routes.remove(oldRoute);
if (newRoute != null) {
routes.add(newRoute);
}
notifyListeners();
}
List<VoidCallback> listeners = [];
@override
void addListener(VoidCallback listener) {
listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
listeners.remove(listener);
}
void notifyListeners() {
for (var listener in listeners) {
listener();
}
}
}
class _NaviPopScope extends StatelessWidget {
const _NaviPopScope(
{required this.child, this.popGesture = false, required this.action});
final Widget child;
final bool popGesture;
final VoidCallback action;
static bool panStartAtEdge = false;
@override
Widget build(BuildContext context) {
Widget res = App.isIOS
? child
: PopScope(
canPop: App.isAndroid ? false : true,
// flutter <3.24.0 api
onPopInvoked: (value) {
action();
},
/*
flutter >=3.24.0 api
onPopInvokedWithResult: (value, result) {
action();
},
*/
child: child,
);
if (popGesture) {
res = GestureDetector(
onPanStart: (details) {
if (details.globalPosition.dx < 64) {
panStartAtEdge = true;
}
},
onPanEnd: (details) {
if (details.velocity.pixelsPerSecond.dx < 0 ||
details.velocity.pixelsPerSecond.dx > 0) {
if (panStartAtEdge) {
action();
}
}
panStartAtEdge = false;
},
child: res);
}
return res;
}
}
class NaviPaddingWidgetController extends StateController {
NaviPaddingWidgetController();
bool _withPadding = false;
void setWithPadding(bool value) {
_withPadding = value;
update();
}
}
class NaviPaddingWidget extends StatelessWidget {
const NaviPaddingWidget({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return StateBuilder<NaviPaddingWidgetController>(
builder: (controller) {
return Padding(
padding: controller._withPadding
? EdgeInsets.only(
top: _NaviPaneState._kTopBarHeight + context.padding.top,
bottom:
_NaviPaneState._kBottomBarHeight + context.padding.bottom,
)
: EdgeInsets.zero,
child: child,
);
},
);
}
}

Some files were not shown because too many files have changed in this diff Show More