Merge remote-tracking branch 'temp/clean'
1
flutter/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto
|
||||||
44
flutter/.gitignore
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
|
||||||
|
# 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
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Web related
|
||||||
|
lib/generated_plugin_registrant.dart
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
jniLibs
|
||||||
|
|
||||||
|
.vscode
|
||||||
10
flutter/.metadata
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# 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: 8874f21e79d7ec66d0457c7ab338348e31b17f1d
|
||||||
|
channel: stable
|
||||||
|
|
||||||
|
project_type: app
|
||||||
11
flutter/android/.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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
|
||||||
82
flutter/android/app/build.gradle
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
def keystoreProperties = new Properties()
|
||||||
|
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||||
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
def localProperties = new Properties()
|
||||||
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
|
if (localPropertiesFile.exists()) {
|
||||||
|
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||||
|
localProperties.load(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||||
|
if (flutterRoot == null) {
|
||||||
|
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
|
if (flutterVersionCode == null) {
|
||||||
|
flutterVersionCode = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||||
|
if (flutterVersionName == null) {
|
||||||
|
flutterVersionName = '1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 31
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId "com.carriez.flutter_hbb"
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion 31
|
||||||
|
versionCode flutterVersionCode.toInteger()
|
||||||
|
versionName flutterVersionName
|
||||||
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
keyAlias keystoreProperties['keyAlias']
|
||||||
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
||||||
|
storePassword keystoreProperties['storePassword']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source '../..'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "androidx.media:media:1.4.3"
|
||||||
|
implementation 'com.github.getActivity:XXPermissions:13.2'
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
40
flutter/android/app/google-services.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "768133699366",
|
||||||
|
"firebase_url": "https://rustdesk.firebaseio.com",
|
||||||
|
"project_id": "rustdesk",
|
||||||
|
"storage_bucket": "rustdesk.appspot.com"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:768133699366:android:5fc9015370e344457993e7",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.carriez.flutter_hbb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyAPOsKcXjrAR-7Z148sYr_gdB_JQZkamTM"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
7
flutter/android/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.carriez.flutter_hbb">
|
||||||
|
<!-- Flutter 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>
|
||||||
73
flutter/android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.carriez.flutter_hbb">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<!--<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />-->
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="RustDesk"
|
||||||
|
android:requestLegacyExternalStorage="true">
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".BootReceiver"
|
||||||
|
android:enabled="false"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter android:priority="1000">
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".InputService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="RustDesk Input"
|
||||||
|
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.accessibilityservice"
|
||||||
|
android:resource="@xml/accessibility_service_config" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:exported="true"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".MainService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:foregroundServiceType="mediaProjection" />
|
||||||
|
<!--
|
||||||
|
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>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
|
|
||||||
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if ("android.intent.action.BOOT_COMPLETED" == intent.action){
|
||||||
|
val it = Intent(context,MainService::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(it)
|
||||||
|
}else{
|
||||||
|
context.startService(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,221 @@
|
|||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
import android.accessibilityservice.AccessibilityService
|
||||||
|
import android.accessibilityservice.GestureDescription
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
const val LIFT_DOWN = 9
|
||||||
|
const val LIFT_MOVE = 8
|
||||||
|
const val LIFT_UP = 10
|
||||||
|
const val RIGHT_UP = 18
|
||||||
|
const val WHEEL_BUTTON_DOWN = 33
|
||||||
|
const val WHEEL_BUTTON_UP = 34
|
||||||
|
const val WHEEL_DOWN = 523331
|
||||||
|
const val WHEEL_UP = 963
|
||||||
|
|
||||||
|
const val WHEEL_STEP = 120
|
||||||
|
const val WHEEL_DURATION = 50L
|
||||||
|
const val LONG_TAP_DELAY = 200L
|
||||||
|
|
||||||
|
class InputService : AccessibilityService() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var ctx: InputService? = null
|
||||||
|
val isOpen: Boolean
|
||||||
|
get() = ctx != null
|
||||||
|
}
|
||||||
|
|
||||||
|
private external fun init(ctx: Context)
|
||||||
|
|
||||||
|
init {
|
||||||
|
System.loadLibrary("rustdesk")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val logTag = "input service"
|
||||||
|
private var leftIsDown = false
|
||||||
|
private var touchPath = Path()
|
||||||
|
private var lastTouchGestureStartTime = 0L
|
||||||
|
private var mouseX = 0
|
||||||
|
private var mouseY = 0
|
||||||
|
private var timer = Timer()
|
||||||
|
private var recentActionTask: TimerTask? = null
|
||||||
|
|
||||||
|
private val wheelActionsQueue = LinkedList<GestureDescription>()
|
||||||
|
private var isWheelActionsPolling = false
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
|
fun rustMouseInput(mask: Int, _x: Int, _y: Int) {
|
||||||
|
val x = if (_x < 0) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
_x
|
||||||
|
}
|
||||||
|
|
||||||
|
val y = if (_y < 0) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
_y
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask == 0 || mask == LIFT_MOVE) {
|
||||||
|
mouseX = x * SCREEN_INFO.scale
|
||||||
|
mouseY = y * SCREEN_INFO.scale
|
||||||
|
}
|
||||||
|
|
||||||
|
// left button down ,was up
|
||||||
|
if (mask == LIFT_DOWN) {
|
||||||
|
leftIsDown = true
|
||||||
|
startGesture(mouseX, mouseY)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// left down ,was down
|
||||||
|
if (leftIsDown) {
|
||||||
|
continueGesture(mouseX, mouseY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// left up ,was down
|
||||||
|
if (mask == LIFT_UP) {
|
||||||
|
leftIsDown = false
|
||||||
|
endGesture(mouseX, mouseY)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask == RIGHT_UP) {
|
||||||
|
performGlobalAction(GLOBAL_ACTION_BACK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// long WHEEL_BUTTON_DOWN -> GLOBAL_ACTION_RECENTS
|
||||||
|
if (mask == WHEEL_BUTTON_DOWN) {
|
||||||
|
timer.purge()
|
||||||
|
recentActionTask = object : TimerTask() {
|
||||||
|
override fun run() {
|
||||||
|
performGlobalAction(GLOBAL_ACTION_RECENTS)
|
||||||
|
recentActionTask = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timer.schedule(recentActionTask, LONG_TAP_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wheel button up
|
||||||
|
if (mask == WHEEL_BUTTON_UP) {
|
||||||
|
if (recentActionTask != null) {
|
||||||
|
recentActionTask!!.cancel()
|
||||||
|
performGlobalAction(GLOBAL_ACTION_HOME)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask == WHEEL_DOWN) {
|
||||||
|
if (mouseY < WHEEL_STEP) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val path = Path()
|
||||||
|
path.moveTo(mouseX.toFloat(), mouseY.toFloat())
|
||||||
|
path.lineTo(mouseX.toFloat(), (mouseY - WHEEL_STEP).toFloat())
|
||||||
|
val stroke = GestureDescription.StrokeDescription(
|
||||||
|
path,
|
||||||
|
0,
|
||||||
|
WHEEL_DURATION
|
||||||
|
)
|
||||||
|
val builder = GestureDescription.Builder()
|
||||||
|
builder.addStroke(stroke)
|
||||||
|
wheelActionsQueue.offer(builder.build())
|
||||||
|
consumeWheelActions()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask == WHEEL_UP) {
|
||||||
|
if (mouseY < WHEEL_STEP) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val path = Path()
|
||||||
|
path.moveTo(mouseX.toFloat(), mouseY.toFloat())
|
||||||
|
path.lineTo(mouseX.toFloat(), (mouseY + WHEEL_STEP).toFloat())
|
||||||
|
val stroke = GestureDescription.StrokeDescription(
|
||||||
|
path,
|
||||||
|
0,
|
||||||
|
WHEEL_DURATION
|
||||||
|
)
|
||||||
|
val builder = GestureDescription.Builder()
|
||||||
|
builder.addStroke(stroke)
|
||||||
|
wheelActionsQueue.offer(builder.build())
|
||||||
|
consumeWheelActions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
|
private fun consumeWheelActions() {
|
||||||
|
if (isWheelActionsPolling) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
isWheelActionsPolling = true
|
||||||
|
}
|
||||||
|
wheelActionsQueue.poll()?.let {
|
||||||
|
dispatchGesture(it, null, null)
|
||||||
|
timer.purge()
|
||||||
|
timer.schedule(object : TimerTask() {
|
||||||
|
override fun run() {
|
||||||
|
isWheelActionsPolling = false
|
||||||
|
consumeWheelActions()
|
||||||
|
}
|
||||||
|
}, WHEEL_DURATION + 10)
|
||||||
|
} ?: let {
|
||||||
|
isWheelActionsPolling = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startGesture(x: Int, y: Int) {
|
||||||
|
touchPath = Path()
|
||||||
|
touchPath.moveTo(x.toFloat(), y.toFloat())
|
||||||
|
lastTouchGestureStartTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun continueGesture(x: Int, y: Int) {
|
||||||
|
touchPath.lineTo(x.toFloat(), y.toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
|
private fun endGesture(x: Int, y: Int) {
|
||||||
|
try {
|
||||||
|
touchPath.lineTo(x.toFloat(), y.toFloat())
|
||||||
|
var duration = System.currentTimeMillis() - lastTouchGestureStartTime
|
||||||
|
if (duration <= 0) {
|
||||||
|
duration = 1
|
||||||
|
}
|
||||||
|
val stroke = GestureDescription.StrokeDescription(
|
||||||
|
touchPath,
|
||||||
|
0,
|
||||||
|
duration
|
||||||
|
)
|
||||||
|
val builder = GestureDescription.Builder()
|
||||||
|
builder.addStroke(stroke)
|
||||||
|
Log.d(logTag, "end gesture x:$x y:$y time:$duration")
|
||||||
|
dispatchGesture(builder.build(), null, null)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(logTag, "endGesture error:$e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
override fun onServiceConnected() {
|
||||||
|
super.onServiceConnected()
|
||||||
|
ctx = this
|
||||||
|
Log.d(logTag, "onServiceConnected!")
|
||||||
|
init(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
|
||||||
|
|
||||||
|
override fun onInterrupt() {}
|
||||||
|
}
|
||||||
@ -0,0 +1,229 @@
|
|||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.media.projection.MediaProjectionManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
|
const val MEDIA_REQUEST_CODE = 42
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity() {
|
||||||
|
companion object {
|
||||||
|
lateinit var flutterMethodChannel: MethodChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
private val channelTag = "mChannel"
|
||||||
|
private val logTag = "mMainActivity"
|
||||||
|
private var mediaProjectionResultIntent: Intent? = null
|
||||||
|
private var mainService: MainService? = null
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
if (MainService.isReady) {
|
||||||
|
Intent(activity, MainService::class.java).also {
|
||||||
|
bindService(it, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flutterMethodChannel = MethodChannel(
|
||||||
|
flutterEngine.dartExecutor.binaryMessenger,
|
||||||
|
channelTag
|
||||||
|
).apply {
|
||||||
|
// make sure result is set, otherwise flutter will await forever
|
||||||
|
setMethodCallHandler { call, result ->
|
||||||
|
when (call.method) {
|
||||||
|
"init_service" -> {
|
||||||
|
Intent(activity, MainService::class.java).also {
|
||||||
|
bindService(it, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
|
if (MainService.isReady) {
|
||||||
|
result.success(false)
|
||||||
|
return@setMethodCallHandler
|
||||||
|
}
|
||||||
|
getMediaProjection()
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
"start_capture" -> {
|
||||||
|
mainService?.let {
|
||||||
|
result.success(it.startCapture())
|
||||||
|
} ?: let {
|
||||||
|
result.success(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"stop_service" -> {
|
||||||
|
Log.d(logTag, "Stop service")
|
||||||
|
mainService?.let {
|
||||||
|
it.destroy()
|
||||||
|
result.success(true)
|
||||||
|
} ?: let {
|
||||||
|
result.success(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"check_permission" -> {
|
||||||
|
if (call.arguments is String) {
|
||||||
|
result.success(checkPermission(context, call.arguments as String))
|
||||||
|
} else {
|
||||||
|
result.success(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"request_permission" -> {
|
||||||
|
if (call.arguments is String) {
|
||||||
|
requestPermission(context, call.arguments as String)
|
||||||
|
result.success(true)
|
||||||
|
} else {
|
||||||
|
result.success(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"check_video_permission" -> {
|
||||||
|
mainService?.let {
|
||||||
|
result.success(it.checkMediaPermission())
|
||||||
|
} ?: let {
|
||||||
|
result.success(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"check_service" -> {
|
||||||
|
flutterMethodChannel.invokeMethod(
|
||||||
|
"on_state_changed",
|
||||||
|
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
||||||
|
)
|
||||||
|
flutterMethodChannel.invokeMethod(
|
||||||
|
"on_state_changed",
|
||||||
|
mapOf("name" to "media", "value" to MainService.isReady.toString())
|
||||||
|
)
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
"init_input" -> {
|
||||||
|
initInput()
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
"stop_input" -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
InputService.ctx?.disableSelf()
|
||||||
|
}
|
||||||
|
InputService.ctx = null
|
||||||
|
flutterMethodChannel.invokeMethod(
|
||||||
|
"on_state_changed",
|
||||||
|
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
||||||
|
)
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
"cancel_notification" -> {
|
||||||
|
try {
|
||||||
|
val id = call.arguments as Int
|
||||||
|
mainService?.cancelNotification(id)
|
||||||
|
} finally {
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"enable_soft_keyboard" -> {
|
||||||
|
// https://blog.csdn.net/hanye2020/article/details/105553780
|
||||||
|
try {
|
||||||
|
if (call.arguments as Boolean) {
|
||||||
|
window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
|
||||||
|
} else {
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
result.error("-1", "No such method", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMediaProjection() {
|
||||||
|
val mMediaProjectionManager =
|
||||||
|
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||||
|
val mIntent = mMediaProjectionManager.createScreenCaptureIntent()
|
||||||
|
startActivityForResult(mIntent, MEDIA_REQUEST_CODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initService() {
|
||||||
|
if (mediaProjectionResultIntent == null) {
|
||||||
|
Log.w(logTag, "initService fail,mediaProjectionResultIntent is null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.d(logTag, "Init service")
|
||||||
|
val serviceIntent = Intent(this, MainService::class.java)
|
||||||
|
serviceIntent.action = INIT_SERVICE
|
||||||
|
serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent)
|
||||||
|
|
||||||
|
launchMainService(serviceIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchMainService(intent: Intent) {
|
||||||
|
// TEST api < O
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initInput() {
|
||||||
|
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
||||||
|
if (intent.resolveActivity(packageManager) != null) {
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
val inputPer = InputService.isOpen
|
||||||
|
Log.d(logTag, "onResume inputPer:$inputPer")
|
||||||
|
activity.runOnUiThread {
|
||||||
|
flutterMethodChannel.invokeMethod(
|
||||||
|
"on_state_changed",
|
||||||
|
mapOf("name" to "input", "value" to inputPer.toString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (requestCode == MEDIA_REQUEST_CODE) {
|
||||||
|
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||||
|
mediaProjectionResultIntent = data
|
||||||
|
initService()
|
||||||
|
} else {
|
||||||
|
flutterMethodChannel.invokeMethod("on_media_projection_canceled", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
Log.e(logTag, "onDestroy")
|
||||||
|
mainService?.let {
|
||||||
|
unbindService(serviceConnection)
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val serviceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
Log.d(logTag, "onServiceConnected")
|
||||||
|
val binder = service as MainService.LocalBinder
|
||||||
|
mainService = binder.getService()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
Log.d(logTag, "onServiceDisconnected")
|
||||||
|
mainService = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,670 @@
|
|||||||
|
/**
|
||||||
|
* Capture screen,get video and audio,send to rust.
|
||||||
|
* Handle notification
|
||||||
|
*/
|
||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.*
|
||||||
|
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||||
|
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
import android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
|
||||||
|
import android.hardware.display.VirtualDisplay
|
||||||
|
import android.media.*
|
||||||
|
import android.media.projection.MediaProjection
|
||||||
|
import android.media.projection.MediaProjectionManager
|
||||||
|
import android.os.*
|
||||||
|
import android.util.DisplayMetrics
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Surface
|
||||||
|
import android.view.Surface.FRAME_RATE_COMPATIBILITY_DEFAULT
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.annotation.Keep
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
const val EXTRA_MP_DATA = "mp_intent"
|
||||||
|
const val INIT_SERVICE = "init_service"
|
||||||
|
const val ACTION_LOGIN_REQ_NOTIFY = "ACTION_LOGIN_REQ_NOTIFY"
|
||||||
|
const val EXTRA_LOGIN_REQ_NOTIFY = "EXTRA_LOGIN_REQ_NOTIFY"
|
||||||
|
|
||||||
|
const val DEFAULT_NOTIFY_TITLE = "RustDesk"
|
||||||
|
const val DEFAULT_NOTIFY_TEXT = "Service is running"
|
||||||
|
const val DEFAULT_NOTIFY_ID = 1
|
||||||
|
const val NOTIFY_ID_OFFSET = 100
|
||||||
|
|
||||||
|
const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
|
||||||
|
|
||||||
|
// video const
|
||||||
|
const val MAX_SCREEN_SIZE = 1200
|
||||||
|
|
||||||
|
const val VIDEO_KEY_BIT_RATE = 1024_000
|
||||||
|
const val VIDEO_KEY_FRAME_RATE = 30
|
||||||
|
|
||||||
|
// audio const
|
||||||
|
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
|
||||||
|
const val AUDIO_SAMPLE_RATE = 48000
|
||||||
|
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
||||||
|
|
||||||
|
class MainService : Service() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
System.loadLibrary("rustdesk")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
fun rustGetByName(name: String): String {
|
||||||
|
return when (name) {
|
||||||
|
"screen_size" -> "${SCREEN_INFO.width}:${SCREEN_INFO.height}"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
fun rustSetByName(name: String, arg1: String, arg2: String) {
|
||||||
|
when (name) {
|
||||||
|
"try_start_without_auth" -> {
|
||||||
|
try {
|
||||||
|
val jsonObject = JSONObject(arg1)
|
||||||
|
val id = jsonObject["id"] as Int
|
||||||
|
val username = jsonObject["name"] as String
|
||||||
|
val peerId = jsonObject["peer_id"] as String
|
||||||
|
val type = if (jsonObject["is_file_transfer"] as Boolean) {
|
||||||
|
translate("File Connection")
|
||||||
|
} else {
|
||||||
|
translate("Screen Connection")
|
||||||
|
}
|
||||||
|
loginRequestNotification(id, type, username, peerId)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"on_client_authorized" -> {
|
||||||
|
Log.d(logTag, "from rust:on_client_authorized")
|
||||||
|
try {
|
||||||
|
val jsonObject = JSONObject(arg1)
|
||||||
|
val id = jsonObject["id"] as Int
|
||||||
|
val username = jsonObject["name"] as String
|
||||||
|
val peerId = jsonObject["peer_id"] as String
|
||||||
|
val isFileTransfer = jsonObject["is_file_transfer"] as Boolean
|
||||||
|
val type = if (isFileTransfer) {
|
||||||
|
translate("File Connection")
|
||||||
|
} else {
|
||||||
|
translate("Screen Connection")
|
||||||
|
}
|
||||||
|
if (!isFileTransfer && !isStart) {
|
||||||
|
startCapture()
|
||||||
|
}
|
||||||
|
onClientAuthorizedNotification(id, type, username, peerId)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
"stop_capture" -> {
|
||||||
|
Log.d(logTag, "from rust:stop_capture")
|
||||||
|
stopCapture()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var serviceLooper: Looper? = null
|
||||||
|
private var serviceHandler: Handler? = null
|
||||||
|
|
||||||
|
// jvm call rust
|
||||||
|
private external fun init(ctx: Context)
|
||||||
|
private external fun startServer()
|
||||||
|
private external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||||
|
private external fun onAudioFrameUpdate(buf: ByteBuffer)
|
||||||
|
private external fun translateLocale(localeName: String, input: String): String
|
||||||
|
private external fun refreshScreen()
|
||||||
|
private external fun setFrameRawEnable(name: String, value: Boolean)
|
||||||
|
// private external fun sendVp9(data: ByteArray)
|
||||||
|
|
||||||
|
private fun translate(input: String): String {
|
||||||
|
Log.d(logTag, "translate:$LOCAL_NAME")
|
||||||
|
return translateLocale(LOCAL_NAME, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var _isReady = false // media permission ready status
|
||||||
|
private var _isStart = false // screen capture start status
|
||||||
|
val isReady: Boolean
|
||||||
|
get() = _isReady
|
||||||
|
val isStart: Boolean
|
||||||
|
get() = _isStart
|
||||||
|
}
|
||||||
|
|
||||||
|
private val logTag = "LOG_SERVICE"
|
||||||
|
private val useVP9 = false
|
||||||
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
|
|
||||||
|
// video
|
||||||
|
private var mediaProjection: MediaProjection? = null
|
||||||
|
private var surface: Surface? = null
|
||||||
|
private val sendVP9Thread = Executors.newSingleThreadExecutor()
|
||||||
|
private var videoEncoder: MediaCodec? = null
|
||||||
|
private var imageReader: ImageReader? = null
|
||||||
|
private var virtualDisplay: VirtualDisplay? = null
|
||||||
|
|
||||||
|
// audio
|
||||||
|
private var audioRecorder: AudioRecord? = null
|
||||||
|
private var audioReader: AudioReader? = null
|
||||||
|
private var minBufferSize = 0
|
||||||
|
private var audioRecordStat = false
|
||||||
|
|
||||||
|
// notification
|
||||||
|
private lateinit var notificationManager: NotificationManager
|
||||||
|
private lateinit var notificationChannel: String
|
||||||
|
private lateinit var notificationBuilder: NotificationCompat.Builder
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
|
||||||
|
start()
|
||||||
|
serviceLooper = looper
|
||||||
|
serviceHandler = Handler(looper)
|
||||||
|
}
|
||||||
|
updateScreenInfo(resources.configuration.orientation)
|
||||||
|
initNotification()
|
||||||
|
startServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
InputService.ctx?.disableSelf()
|
||||||
|
}
|
||||||
|
InputService.ctx = null
|
||||||
|
checkMediaPermission()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateScreenInfo(orientation: Int) {
|
||||||
|
var w: Int
|
||||||
|
var h: Int
|
||||||
|
var dpi: Int
|
||||||
|
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
val m = windowManager.maximumWindowMetrics
|
||||||
|
w = m.bounds.width()
|
||||||
|
h = m.bounds.height()
|
||||||
|
dpi = resources.configuration.densityDpi
|
||||||
|
} else {
|
||||||
|
val dm = DisplayMetrics()
|
||||||
|
windowManager.defaultDisplay.getRealMetrics(dm)
|
||||||
|
w = dm.widthPixels
|
||||||
|
h = dm.heightPixels
|
||||||
|
dpi = dm.densityDpi
|
||||||
|
}
|
||||||
|
|
||||||
|
val max = max(w,h)
|
||||||
|
val min = min(w,h)
|
||||||
|
if (orientation == ORIENTATION_LANDSCAPE) {
|
||||||
|
w = max
|
||||||
|
h = min
|
||||||
|
} else {
|
||||||
|
w = min
|
||||||
|
h = max
|
||||||
|
}
|
||||||
|
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
||||||
|
var scale = 1
|
||||||
|
if (w != 0 && h != 0) {
|
||||||
|
if (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE) {
|
||||||
|
scale = 2
|
||||||
|
w /= scale
|
||||||
|
h /= scale
|
||||||
|
dpi /= scale
|
||||||
|
}
|
||||||
|
if (SCREEN_INFO.width != w) {
|
||||||
|
SCREEN_INFO.width = w
|
||||||
|
SCREEN_INFO.height = h
|
||||||
|
SCREEN_INFO.scale = scale
|
||||||
|
SCREEN_INFO.dpi = dpi
|
||||||
|
if (isStart) {
|
||||||
|
stopCapture()
|
||||||
|
refreshScreen()
|
||||||
|
startCapture()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder {
|
||||||
|
Log.d(logTag, "service onBind")
|
||||||
|
return binder
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class LocalBinder : Binder() {
|
||||||
|
init {
|
||||||
|
Log.d(logTag, "LocalBinder init")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getService(): MainService = this@MainService
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
Log.d("whichService", "this service:${Thread.currentThread()}")
|
||||||
|
super.onStartCommand(intent, flags, startId)
|
||||||
|
if (intent?.action == INIT_SERVICE) {
|
||||||
|
Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}")
|
||||||
|
createForegroundNotification()
|
||||||
|
val mMediaProjectionManager =
|
||||||
|
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||||
|
intent.getParcelableExtra<Intent>(EXTRA_MP_DATA)?.let {
|
||||||
|
mediaProjection =
|
||||||
|
mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
|
||||||
|
checkMediaPermission()
|
||||||
|
init(this)
|
||||||
|
_isReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return START_NOT_STICKY // don't use sticky (auto restart),the new service (from auto restart) will lose control
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
updateScreenInfo(newConfig.orientation)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("WrongConstant")
|
||||||
|
private fun createSurface(): Surface? {
|
||||||
|
return if (useVP9) {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Log.d(logTag, "ImageReader.newInstance:INFO:$SCREEN_INFO")
|
||||||
|
imageReader =
|
||||||
|
ImageReader.newInstance(
|
||||||
|
SCREEN_INFO.width,
|
||||||
|
SCREEN_INFO.height,
|
||||||
|
PixelFormat.RGBA_8888,
|
||||||
|
4
|
||||||
|
).apply {
|
||||||
|
setOnImageAvailableListener({ imageReader: ImageReader ->
|
||||||
|
try {
|
||||||
|
imageReader.acquireLatestImage().use { image ->
|
||||||
|
if (image == null) return@setOnImageAvailableListener
|
||||||
|
val planes = image.planes
|
||||||
|
val buffer = planes[0].buffer
|
||||||
|
buffer.rewind()
|
||||||
|
onVideoFrameUpdate(buffer)
|
||||||
|
}
|
||||||
|
} catch (ignored: java.lang.Exception) {
|
||||||
|
}
|
||||||
|
}, serviceHandler)
|
||||||
|
}
|
||||||
|
Log.d(logTag, "ImageReader.setOnImageAvailableListener done")
|
||||||
|
imageReader?.surface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startCapture(): Boolean {
|
||||||
|
if (isStart) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (mediaProjection == null) {
|
||||||
|
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
updateScreenInfo(resources.configuration.orientation)
|
||||||
|
Log.d(logTag, "Start Capture")
|
||||||
|
surface = createSurface()
|
||||||
|
|
||||||
|
if (useVP9) {
|
||||||
|
startVP9VideoRecorder(mediaProjection!!)
|
||||||
|
} else {
|
||||||
|
startRawVideoRecorder(mediaProjection!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
startAudioRecorder()
|
||||||
|
}
|
||||||
|
checkMediaPermission()
|
||||||
|
_isStart = true
|
||||||
|
setFrameRawEnable("video",true)
|
||||||
|
setFrameRawEnable("audio",true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopCapture() {
|
||||||
|
Log.d(logTag, "Stop Capture")
|
||||||
|
setFrameRawEnable("video",false)
|
||||||
|
setFrameRawEnable("audio",false)
|
||||||
|
_isStart = false
|
||||||
|
// release video
|
||||||
|
virtualDisplay?.release()
|
||||||
|
surface?.release()
|
||||||
|
imageReader?.close()
|
||||||
|
videoEncoder?.let {
|
||||||
|
it.signalEndOfInputStream()
|
||||||
|
it.stop()
|
||||||
|
it.release()
|
||||||
|
}
|
||||||
|
virtualDisplay = null
|
||||||
|
videoEncoder = null
|
||||||
|
|
||||||
|
// release audio
|
||||||
|
audioRecordStat = false
|
||||||
|
audioRecorder?.release()
|
||||||
|
audioRecorder = null
|
||||||
|
minBufferSize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
Log.d(logTag, "destroy service")
|
||||||
|
_isReady = false
|
||||||
|
|
||||||
|
stopCapture()
|
||||||
|
imageReader?.close()
|
||||||
|
imageReader = null
|
||||||
|
|
||||||
|
mediaProjection = null
|
||||||
|
checkMediaPermission()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
InputService.ctx?.disableSelf()
|
||||||
|
}
|
||||||
|
InputService.ctx = null
|
||||||
|
stopForeground(true)
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkMediaPermission(): Boolean {
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
MainActivity.flutterMethodChannel.invokeMethod(
|
||||||
|
"on_state_changed",
|
||||||
|
mapOf("name" to "media", "value" to isReady.toString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
MainActivity.flutterMethodChannel.invokeMethod(
|
||||||
|
"on_state_changed",
|
||||||
|
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return isReady
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startRawVideoRecorder(mp: MediaProjection) {
|
||||||
|
Log.d(logTag, "startRawVideoRecorder,screen info:$SCREEN_INFO")
|
||||||
|
if (surface == null) {
|
||||||
|
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
virtualDisplay = mp.createVirtualDisplay(
|
||||||
|
"RustDeskVD",
|
||||||
|
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||||
|
surface, null, null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startVP9VideoRecorder(mp: MediaProjection) {
|
||||||
|
createMediaCodec()
|
||||||
|
videoEncoder?.let {
|
||||||
|
surface = it.createInputSurface()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
surface!!.setFrameRate(1F, FRAME_RATE_COMPATIBILITY_DEFAULT)
|
||||||
|
}
|
||||||
|
it.setCallback(cb)
|
||||||
|
it.start()
|
||||||
|
virtualDisplay = mp.createVirtualDisplay(
|
||||||
|
"RustDeskVD",
|
||||||
|
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||||
|
surface, null, null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cb: MediaCodec.Callback = object : MediaCodec.Callback() {
|
||||||
|
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {}
|
||||||
|
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {}
|
||||||
|
|
||||||
|
override fun onOutputBufferAvailable(
|
||||||
|
codec: MediaCodec,
|
||||||
|
index: Int,
|
||||||
|
info: MediaCodec.BufferInfo
|
||||||
|
) {
|
||||||
|
codec.getOutputBuffer(index)?.let { buf ->
|
||||||
|
sendVP9Thread.execute {
|
||||||
|
val byteArray = ByteArray(buf.limit())
|
||||||
|
buf.get(byteArray)
|
||||||
|
// sendVp9(byteArray)
|
||||||
|
codec.releaseOutputBuffer(index, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
|
||||||
|
Log.e(logTag, "MediaCodec.Callback error:$e")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun createMediaCodec() {
|
||||||
|
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
|
||||||
|
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
|
||||||
|
val mFormat =
|
||||||
|
MediaFormat.createVideoFormat(MIME_TYPE, SCREEN_INFO.width, SCREEN_INFO.height)
|
||||||
|
mFormat.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_KEY_BIT_RATE)
|
||||||
|
mFormat.setInteger(MediaFormat.KEY_FRAME_RATE, VIDEO_KEY_FRAME_RATE)
|
||||||
|
mFormat.setInteger(
|
||||||
|
MediaFormat.KEY_COLOR_FORMAT,
|
||||||
|
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
|
||||||
|
)
|
||||||
|
mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
|
||||||
|
try {
|
||||||
|
videoEncoder!!.configure(mFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(logTag, "mEncoder.configure fail!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun startAudioRecorder() {
|
||||||
|
checkAudioRecorder()
|
||||||
|
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
||||||
|
try {
|
||||||
|
audioRecorder!!.startRecording()
|
||||||
|
audioRecordStat = true
|
||||||
|
thread {
|
||||||
|
while (audioRecordStat) {
|
||||||
|
audioReader!!.readSync(audioRecorder!!)?.let {
|
||||||
|
onAudioFrameUpdate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(logTag, "Exit audio thread")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(logTag, "startAudioRecorder fail:$e")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(logTag, "startAudioRecorder fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun checkAudioRecorder() {
|
||||||
|
if (audioRecorder != null && audioRecorder != null && minBufferSize != 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// read f32 to byte , length * 4
|
||||||
|
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
||||||
|
AUDIO_SAMPLE_RATE,
|
||||||
|
AUDIO_CHANNEL_MASK,
|
||||||
|
AUDIO_ENCODING
|
||||||
|
)
|
||||||
|
if (minBufferSize == 0) {
|
||||||
|
Log.d(logTag, "get min buffer size fail!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioReader = AudioReader(minBufferSize, 4)
|
||||||
|
Log.d(logTag, "init audioData len:$minBufferSize")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
mediaProjection?.let {
|
||||||
|
val apcc = AudioPlaybackCaptureConfiguration.Builder(it)
|
||||||
|
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
|
||||||
|
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
|
||||||
|
.addMatchingUsage(AudioAttributes.USAGE_GAME)
|
||||||
|
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build()
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
this,
|
||||||
|
Manifest.permission.RECORD_AUDIO
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioRecorder = AudioRecord.Builder()
|
||||||
|
.setAudioFormat(
|
||||||
|
AudioFormat.Builder()
|
||||||
|
.setEncoding(AUDIO_ENCODING)
|
||||||
|
.setSampleRate(AUDIO_SAMPLE_RATE)
|
||||||
|
.setChannelMask(AUDIO_CHANNEL_MASK).build()
|
||||||
|
)
|
||||||
|
.setAudioPlaybackCaptureConfig(apcc)
|
||||||
|
.setBufferSizeInBytes(minBufferSize).build()
|
||||||
|
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(logTag, "createAudioRecorder fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNotification() {
|
||||||
|
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val channelId = "RustDesk"
|
||||||
|
val channelName = "RustDesk Service"
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
channelId,
|
||||||
|
channelName, NotificationManager.IMPORTANCE_HIGH
|
||||||
|
).apply {
|
||||||
|
description = "RustDesk Service Channel"
|
||||||
|
}
|
||||||
|
channel.lightColor = Color.BLUE
|
||||||
|
channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
channelId
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
notificationBuilder = NotificationCompat.Builder(this, notificationChannel)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnspecifiedImmutableFlag")
|
||||||
|
private fun createForegroundNotification() {
|
||||||
|
val intent = Intent(this, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
|
||||||
|
action = Intent.ACTION_MAIN
|
||||||
|
addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
|
putExtra("type", type)
|
||||||
|
}
|
||||||
|
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
PendingIntent.getActivity(this, 0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||||
|
} else {
|
||||||
|
PendingIntent.getActivity(this, 0, intent, FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
val notification = notificationBuilder
|
||||||
|
.setOngoing(true)
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setDefaults(Notification.DEFAULT_ALL)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setContentTitle(DEFAULT_NOTIFY_TITLE)
|
||||||
|
.setContentText(translate(DEFAULT_NOTIFY_TEXT) + '!')
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setColor(ContextCompat.getColor(this, R.color.primary))
|
||||||
|
.setWhen(System.currentTimeMillis())
|
||||||
|
.build()
|
||||||
|
startForeground(DEFAULT_NOTIFY_ID, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loginRequestNotification(
|
||||||
|
clientID: Int,
|
||||||
|
type: String,
|
||||||
|
username: String,
|
||||||
|
peerId: String
|
||||||
|
) {
|
||||||
|
val notification = notificationBuilder
|
||||||
|
.setOngoing(false)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
|
.setContentTitle(translate("Do you accept?"))
|
||||||
|
.setContentText("$type:$username-$peerId")
|
||||||
|
// .setStyle(MediaStyle().setShowActionsInCompactView(0, 1))
|
||||||
|
// .addAction(R.drawable.check_blue, "check", genLoginRequestPendingIntent(true))
|
||||||
|
// .addAction(R.drawable.close_red, "close", genLoginRequestPendingIntent(false))
|
||||||
|
.build()
|
||||||
|
notificationManager.notify(getClientNotifyID(clientID), notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onClientAuthorizedNotification(
|
||||||
|
clientID: Int,
|
||||||
|
type: String,
|
||||||
|
username: String,
|
||||||
|
peerId: String
|
||||||
|
) {
|
||||||
|
cancelNotification(clientID)
|
||||||
|
val notification = notificationBuilder
|
||||||
|
.setOngoing(false)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
|
.setContentTitle("$type ${translate("Established")}")
|
||||||
|
.setContentText("$username - $peerId")
|
||||||
|
.build()
|
||||||
|
notificationManager.notify(getClientNotifyID(clientID), notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getClientNotifyID(clientID: Int): Int {
|
||||||
|
return clientID + NOTIFY_ID_OFFSET
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelNotification(clientID: Int) {
|
||||||
|
notificationManager.cancel(getClientNotifyID(clientID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnspecifiedImmutableFlag")
|
||||||
|
private fun genLoginRequestPendingIntent(res: Boolean): PendingIntent {
|
||||||
|
val intent = Intent(this, MainService::class.java).apply {
|
||||||
|
action = ACTION_LOGIN_REQ_NOTIFY
|
||||||
|
putExtra(EXTRA_LOGIN_REQ_NOTIFY, res)
|
||||||
|
}
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
PendingIntent.getService(this, 111, intent, FLAG_IMMUTABLE)
|
||||||
|
} else {
|
||||||
|
PendingIntent.getService(this, 111, intent, FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTextNotification(_title: String?, _text: String?) {
|
||||||
|
val title = _title ?: DEFAULT_NOTIFY_TITLE
|
||||||
|
val text = _text ?: translate(DEFAULT_NOTIFY_TEXT) + '!'
|
||||||
|
val notification = notificationBuilder
|
||||||
|
.clearActions()
|
||||||
|
.setStyle(null)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(text)
|
||||||
|
.build()
|
||||||
|
notificationManager.notify(DEFAULT_NOTIFY_ID, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.AudioRecord
|
||||||
|
import android.media.AudioRecord.READ_BLOCKING
|
||||||
|
import android.media.MediaCodecList
|
||||||
|
import android.media.MediaFormat
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import com.hjq.permissions.Permission
|
||||||
|
import com.hjq.permissions.XXPermissions
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@SuppressLint("ConstantLocale")
|
||||||
|
val LOCAL_NAME = Locale.getDefault().toString()
|
||||||
|
val SCREEN_INFO = Info(0, 0, 1, 200)
|
||||||
|
|
||||||
|
data class Info(
|
||||||
|
var width: Int, var height: Int, var scale: Int, var dpi: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
fun testVP9Support(): Boolean {
|
||||||
|
return true
|
||||||
|
val res = MediaCodecList(MediaCodecList.ALL_CODECS)
|
||||||
|
.findEncoderForFormat(
|
||||||
|
MediaFormat.createVideoFormat(
|
||||||
|
MediaFormat.MIMETYPE_VIDEO_VP9,
|
||||||
|
SCREEN_INFO.width,
|
||||||
|
SCREEN_INFO.width
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return res != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestPermission(context: Context, type: String) {
|
||||||
|
val permission = when (type) {
|
||||||
|
"audio" -> {
|
||||||
|
Permission.RECORD_AUDIO
|
||||||
|
}
|
||||||
|
"file" -> {
|
||||||
|
Permission.MANAGE_EXTERNAL_STORAGE
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
XXPermissions.with(context)
|
||||||
|
.permission(permission)
|
||||||
|
.request { permissions, all ->
|
||||||
|
if (all) {
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
MainActivity.flutterMethodChannel.invokeMethod(
|
||||||
|
"on_android_permission_result",
|
||||||
|
mapOf("type" to type, "result" to all)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkPermission(context: Context, type: String): Boolean {
|
||||||
|
val permission = when (type) {
|
||||||
|
"audio" -> {
|
||||||
|
Permission.RECORD_AUDIO
|
||||||
|
}
|
||||||
|
"file" -> {
|
||||||
|
Permission.MANAGE_EXTERNAL_STORAGE
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return XXPermissions.isGranted(context, permission)
|
||||||
|
}
|
||||||
|
|
||||||
|
class AudioReader(val bufSize: Int, private val maxFrames: Int) {
|
||||||
|
private var currentPos = 0
|
||||||
|
private val bufferPool: Array<ByteBuffer>
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (maxFrames < 0 || maxFrames > 32) {
|
||||||
|
throw Exception("Out of bounds")
|
||||||
|
}
|
||||||
|
if (bufSize <= 0) {
|
||||||
|
throw Exception("Wrong bufSize")
|
||||||
|
}
|
||||||
|
bufferPool = Array(maxFrames) {
|
||||||
|
ByteBuffer.allocateDirect(bufSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun next() {
|
||||||
|
currentPos++
|
||||||
|
if (currentPos >= maxFrames) {
|
||||||
|
currentPos = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun readSync(audioRecord: AudioRecord): ByteBuffer? {
|
||||||
|
val buffer = bufferPool[currentPos]
|
||||||
|
val res = audioRecord.read(buffer, bufSize, READ_BLOCKING)
|
||||||
|
return if (res > 0) {
|
||||||
|
next()
|
||||||
|
buffer
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
5
flutter/android/app/src/main/res/drawable/check_blue.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#0071FF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||||
|
</vector>
|
||||||
5
flutter/android/app/src/main/res/drawable/close_red.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#D74E4E"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||||
|
</vector>
|
||||||
@ -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>
|
||||||
BIN
flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
18
flutter/android/app/src/main/res/values-night/styles.xml
Normal 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
|
||||||
|
Flutter 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>
|
||||||
4
flutter/android/app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="primary">#FF0071FF</color>
|
||||||
|
</resources>
|
||||||
4
flutter/android/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">RustDesk</string>
|
||||||
|
<string name="accessibility_service_description">Allow other devices to control your phone using virtual touch, when RustDesk screen sharing is established</string>
|
||||||
|
</resources>
|
||||||
18
flutter/android/app/src/main/res/values/styles.xml
Normal 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
|
||||||
|
Flutter 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>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:accessibilityEventTypes="typeWindowsChanged"
|
||||||
|
android:accessibilityFlags="flagDefault"
|
||||||
|
android:notificationTimeout="50"
|
||||||
|
android:description="@string/accessibility_service_description"
|
||||||
|
android:canPerformGestures="true"/>
|
||||||
7
flutter/android/app/src/profile/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.carriez.flutter_hbb">
|
||||||
|
<!-- Flutter 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>
|
||||||
32
flutter/android/build.gradle
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.6.10'
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:7.0.2'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
classpath 'com.google.gms:google-services:4.3.3'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.buildDir = '../build'
|
||||||
|
subprojects {
|
||||||
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
project.evaluationDependsOn(':app')
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
29
flutter/android/flutter_hbb_android.iml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="FacetManager">
|
||||||
|
<facet type="android" name="Android">
|
||||||
|
<configuration>
|
||||||
|
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||||
|
<option name="GEN_FOLDER_RELATIVE_PATH_APT" value="/gen" />
|
||||||
|
<option name="GEN_FOLDER_RELATIVE_PATH_AIDL" value="/gen" />
|
||||||
|
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/app/src/main/AndroidManifest.xml" />
|
||||||
|
<option name="RES_FOLDER_RELATIVE_PATH" value="/app/src/main/res" />
|
||||||
|
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/app/src/main/assets" />
|
||||||
|
<option name="LIBS_FOLDER_RELATIVE_PATH" value="/app/src/main/libs" />
|
||||||
|
<option name="PROGUARD_LOGS_FOLDER_RELATIVE_PATH" value="/app/src/main/proguard_logs" />
|
||||||
|
</configuration>
|
||||||
|
</facet>
|
||||||
|
</component>
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/app/src/main/java" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/app/src/main/kotlin" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Android API 29 Platform" jdkType="Android SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" name="Flutter for Android" level="project" />
|
||||||
|
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
3
flutter/android/gradle.properties
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
6
flutter/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#Fri Jun 23 08:50:38 CEST 2017
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
||||||
BIN
flutter/android/key.jks
Normal file
11
flutter/android/settings.gradle
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
include ':app'
|
||||||
|
|
||||||
|
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||||
|
def properties = new Properties()
|
||||||
|
|
||||||
|
assert localPropertiesFile.exists()
|
||||||
|
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||||
|
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||||
BIN
flutter/assets/android.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
flutter/assets/gestures.ttf
Normal file
BIN
flutter/assets/insecure.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
flutter/assets/insecure_relay.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
flutter/assets/linux.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
flutter/assets/mac.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
flutter/assets/secure.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
flutter/assets/secure_relay.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
flutter/assets/win.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
7
flutter/build_android.sh
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
$ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/*
|
||||||
|
flutter build apk --target-platform android-arm64 --release --obfuscate --split-debug-info ./split-debug-info
|
||||||
|
flutter build appbundle --target-platform android-arm64 --release --obfuscate --split-debug-info ./split-debug-info
|
||||||
|
|
||||||
|
# build in linux
|
||||||
|
# $ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/*
|
||||||
2
flutter/build_ios.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
|
||||||
9
flutter/deploy.sh
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
cd build/web/
|
||||||
|
python3 -c 'x=open("./main.dart.js", "rt").read();import re;y=re.search("https://.*canvaskit-wasm@([\d\.]+)/bin/",x);dirname="canvaskit@"+y.groups()[0];z=x.replace(y.group(),"/"+dirname+"/");f=open("./main.dart.js", "wt");f.write(z);import os;os.system("ln -s canvaskit " + dirname);'
|
||||||
|
python3 -c 'import hashlib;x=hashlib.sha1(open("./main.dart.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("main.dart.js", "main.dart.js?v="+x);open("index.html","wt").write(y)'
|
||||||
|
tar czf x *
|
||||||
|
scp x sg:/tmp/
|
||||||
|
ssh sg "sudo tar xzf /tmp/x -C /var/www/html/web.rustdesk.com/ && /bin/rm /tmp/x && sudo chown www-data:www-data /var/www/html/web.rustdesk.com/ -R"
|
||||||
|
/bin/rm x
|
||||||
|
cd -
|
||||||
33
flutter/ios/.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
*.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
|
||||||
26
flutter/ios/Flutter/AppFrameworkInfo.plist
Normal 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>9.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
2
flutter/ios/Flutter/Debug.xcconfig
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
2
flutter/ios/Flutter/Release.xcconfig
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
45
flutter/ios/Podfile
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Uncomment this line to define a global platform for your project
|
||||||
|
# platform :ios, '9.0'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
platform :ios, '11.0'
|
||||||
|
|
||||||
|
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__))
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_ios_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
194
flutter/ios/Podfile.lock
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
PODS:
|
||||||
|
- device_info (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- Firebase/Analytics (8.14.0):
|
||||||
|
- Firebase/Core
|
||||||
|
- Firebase/Core (8.14.0):
|
||||||
|
- Firebase/CoreOnly
|
||||||
|
- FirebaseAnalytics (~> 8.14.0)
|
||||||
|
- Firebase/CoreOnly (8.14.0):
|
||||||
|
- FirebaseCore (= 8.14.0)
|
||||||
|
- firebase_analytics (9.1.5):
|
||||||
|
- Firebase/Analytics (= 8.14.0)
|
||||||
|
- firebase_core
|
||||||
|
- Flutter
|
||||||
|
- firebase_core (1.14.1):
|
||||||
|
- Firebase/CoreOnly (= 8.14.0)
|
||||||
|
- Flutter
|
||||||
|
- FirebaseAnalytics (8.14.0):
|
||||||
|
- FirebaseAnalytics/AdIdSupport (= 8.14.0)
|
||||||
|
- FirebaseCore (~> 8.0)
|
||||||
|
- FirebaseInstallations (~> 8.0)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/Network (~> 7.7)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- FirebaseAnalytics/AdIdSupport (8.14.0):
|
||||||
|
- FirebaseCore (~> 8.0)
|
||||||
|
- FirebaseInstallations (~> 8.0)
|
||||||
|
- GoogleAppMeasurement (= 8.14.0)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/Network (~> 7.7)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- FirebaseCore (8.14.0):
|
||||||
|
- FirebaseCoreDiagnostics (~> 8.0)
|
||||||
|
- GoogleUtilities/Environment (~> 7.7)
|
||||||
|
- GoogleUtilities/Logger (~> 7.7)
|
||||||
|
- FirebaseCoreDiagnostics (8.15.0):
|
||||||
|
- GoogleDataTransport (~> 9.1)
|
||||||
|
- GoogleUtilities/Environment (~> 7.7)
|
||||||
|
- GoogleUtilities/Logger (~> 7.7)
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- FirebaseInstallations (8.15.0):
|
||||||
|
- FirebaseCore (~> 8.0)
|
||||||
|
- GoogleUtilities/Environment (~> 7.7)
|
||||||
|
- GoogleUtilities/UserDefaults (~> 7.7)
|
||||||
|
- PromisesObjC (< 3.0, >= 1.2)
|
||||||
|
- Flutter (1.0.0)
|
||||||
|
- GoogleAppMeasurement (8.14.0):
|
||||||
|
- GoogleAppMeasurement/AdIdSupport (= 8.14.0)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/Network (~> 7.7)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- GoogleAppMeasurement/AdIdSupport (8.14.0):
|
||||||
|
- GoogleAppMeasurement/WithoutAdIdSupport (= 8.14.0)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/Network (~> 7.7)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- GoogleAppMeasurement/WithoutAdIdSupport (8.14.0):
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/MethodSwizzler (~> 7.7)
|
||||||
|
- GoogleUtilities/Network (~> 7.7)
|
||||||
|
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- GoogleDataTransport (9.1.2):
|
||||||
|
- GoogleUtilities/Environment (~> 7.2)
|
||||||
|
- nanopb (~> 2.30908.0)
|
||||||
|
- PromisesObjC (< 3.0, >= 1.2)
|
||||||
|
- GoogleUtilities/AppDelegateSwizzler (7.7.0):
|
||||||
|
- GoogleUtilities/Environment
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Network
|
||||||
|
- GoogleUtilities/Environment (7.7.0):
|
||||||
|
- PromisesObjC (< 3.0, >= 1.2)
|
||||||
|
- GoogleUtilities/Logger (7.7.0):
|
||||||
|
- GoogleUtilities/Environment
|
||||||
|
- GoogleUtilities/MethodSwizzler (7.7.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/Network (7.7.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- "GoogleUtilities/NSData+zlib"
|
||||||
|
- GoogleUtilities/Reachability
|
||||||
|
- "GoogleUtilities/NSData+zlib (7.7.0)"
|
||||||
|
- GoogleUtilities/Reachability (7.7.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- GoogleUtilities/UserDefaults (7.7.0):
|
||||||
|
- GoogleUtilities/Logger
|
||||||
|
- image_picker_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- MTBBarcodeScanner (5.0.11)
|
||||||
|
- nanopb (2.30908.0):
|
||||||
|
- nanopb/decode (= 2.30908.0)
|
||||||
|
- nanopb/encode (= 2.30908.0)
|
||||||
|
- nanopb/decode (2.30908.0)
|
||||||
|
- nanopb/encode (2.30908.0)
|
||||||
|
- package_info (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- path_provider_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- PromisesObjC (2.1.0)
|
||||||
|
- qr_code_scanner (0.2.0):
|
||||||
|
- Flutter
|
||||||
|
- MTBBarcodeScanner
|
||||||
|
- shared_preferences_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- url_launcher_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- wakelock (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- device_info (from `.symlinks/plugins/device_info/ios`)
|
||||||
|
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
||||||
|
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||||
|
- Flutter (from `Flutter`)
|
||||||
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
|
- package_info (from `.symlinks/plugins/package_info/ios`)
|
||||||
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
|
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
|
||||||
|
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||||
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
- wakelock (from `.symlinks/plugins/wakelock/ios`)
|
||||||
|
|
||||||
|
SPEC REPOS:
|
||||||
|
trunk:
|
||||||
|
- Firebase
|
||||||
|
- FirebaseAnalytics
|
||||||
|
- FirebaseCore
|
||||||
|
- FirebaseCoreDiagnostics
|
||||||
|
- FirebaseInstallations
|
||||||
|
- GoogleAppMeasurement
|
||||||
|
- GoogleDataTransport
|
||||||
|
- GoogleUtilities
|
||||||
|
- MTBBarcodeScanner
|
||||||
|
- nanopb
|
||||||
|
- PromisesObjC
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
device_info:
|
||||||
|
:path: ".symlinks/plugins/device_info/ios"
|
||||||
|
firebase_analytics:
|
||||||
|
:path: ".symlinks/plugins/firebase_analytics/ios"
|
||||||
|
firebase_core:
|
||||||
|
:path: ".symlinks/plugins/firebase_core/ios"
|
||||||
|
Flutter:
|
||||||
|
:path: Flutter
|
||||||
|
image_picker_ios:
|
||||||
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
|
package_info:
|
||||||
|
:path: ".symlinks/plugins/package_info/ios"
|
||||||
|
path_provider_ios:
|
||||||
|
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||||
|
qr_code_scanner:
|
||||||
|
:path: ".symlinks/plugins/qr_code_scanner/ios"
|
||||||
|
shared_preferences_ios:
|
||||||
|
:path: ".symlinks/plugins/shared_preferences_ios/ios"
|
||||||
|
url_launcher_ios:
|
||||||
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
wakelock:
|
||||||
|
:path: ".symlinks/plugins/wakelock/ios"
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
device_info: d7d233b645a32c40dfdc212de5cf646ca482f175
|
||||||
|
Firebase: 7e8fe528c161b9271d365217a74c16aaf834578e
|
||||||
|
firebase_analytics: 8a36c76380be1fca3bab69534cf911082e0d7ab8
|
||||||
|
firebase_core: cdef02fcf55872191eb0568d4c31a7a700e38582
|
||||||
|
FirebaseAnalytics: 2fc3876e2eb347673ad2f35e249ae7b15d6c88f5
|
||||||
|
FirebaseCore: b84a44ee7ba999e0f9f76d198a9c7f60a797b848
|
||||||
|
FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb
|
||||||
|
FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd
|
||||||
|
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||||
|
GoogleAppMeasurement: 71156240babd3cc6ced03e0d54816f01a880c730
|
||||||
|
GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940
|
||||||
|
GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1
|
||||||
|
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
|
||||||
|
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||||
|
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
|
||||||
|
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
|
||||||
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
|
PromisesObjC: 99b6f43f9e1044bd87a95a60beff28c2c44ddb72
|
||||||
|
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||||
|
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||||
|
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||||
|
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: a00077baecbb97321490c14848fceed3893ca92a
|
||||||
|
|
||||||
|
COCOAPODS: 1.11.3
|
||||||
575
flutter/ios/Runner.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,575 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 51;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
7E078EE926BAB4720036E738 /* liblibrustdesk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E078EE826BAB4710036E738 /* liblibrustdesk.a */; };
|
||||||
|
7E078EEC26BADB3D0036E738 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7E078EEB26BADB3D0036E738 /* GoogleService-Info.plist */; };
|
||||||
|
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 */; };
|
||||||
|
C362FA62593C0A953D788A2B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D81DD31BD179189F3EA0124A /* Pods_Runner.framework */; };
|
||||||
|
/* End PBXBuildFile 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>"; };
|
||||||
|
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>"; };
|
||||||
|
7B0BE50AED65C219B81E506C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
7E078EE826BAB4710036E738 /* liblibrustdesk.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = liblibrustdesk.a; path = "../../target/aarch64-apple-ios/release/liblibrustdesk.a"; sourceTree = "<group>"; };
|
||||||
|
7E078EEA26BABB100036E738 /* ffi.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ffi.h; sourceTree = "<group>"; };
|
||||||
|
7E078EEB26BADB3D0036E738 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||||
|
7E0A73A826CAB3C100FF94B3 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||||
|
94AF76B3E95A41AD3421FB7B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.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>"; };
|
||||||
|
C9E47BCA6B42B34FF7A143DA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
D81DD31BD179189F3EA0124A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
7E078EE926BAB4720036E738 /* liblibrustdesk.a in Frameworks */,
|
||||||
|
C362FA62593C0A953D788A2B /* Pods_Runner.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
4064E93F6C682BED4ADBDEEA /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
94AF76B3E95A41AD3421FB7B /* Pods-Runner.debug.xcconfig */,
|
||||||
|
7B0BE50AED65C219B81E506C /* Pods-Runner.release.xcconfig */,
|
||||||
|
C9E47BCA6B42B34FF7A143DA /* Pods-Runner.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
path = Pods;
|
||||||
|
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 */,
|
||||||
|
4064E93F6C682BED4ADBDEEA /* Pods */,
|
||||||
|
9F474765C702311B2610E104 /* Frameworks */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
7E0A73A826CAB3C100FF94B3 /* Runner.entitlements */,
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
7E078EEB26BADB3D0036E738 /* GoogleService-Info.plist */,
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
|
7E078EEA26BABB100036E738 /* ffi.h */,
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9F474765C702311B2610E104 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
7E078EE826BAB4710036E738 /* liblibrustdesk.a */,
|
||||||
|
D81DD31BD179189F3EA0124A /* Pods_Runner.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
7E81731B2965F8CA22E67C02 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
3974543C243EF943511C7BDD /* [CP] Embed Pods Frameworks */,
|
||||||
|
);
|
||||||
|
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 = {
|
||||||
|
LastUpgradeCheck = 1300;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
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 */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
|
7E078EEC26BADB3D0036E738 /* GoogleService-Info.plist in Resources */,
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
3974543C243EF943511C7BDD /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
7E81731B2965F8CA22E67C02 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
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 */
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase 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;
|
||||||
|
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;
|
||||||
|
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 = 9.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;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = HZF9JMC8YN;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/../../target/aarch64-apple-ios/release/";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
STRIP_STYLE = "non-global";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
97C147031CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
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;
|
||||||
|
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 = 9.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;
|
||||||
|
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;
|
||||||
|
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 = 9.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;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = HZF9JMC8YN;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/../../target/aarch64-apple-ios/release/";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
STRIP_STYLE = "non-global";
|
||||||
|
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;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = HZF9JMC8YN;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/../../target/aarch64-apple-ios/release/";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
STRIP_STYLE = "non-global";
|
||||||
|
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 */
|
||||||
|
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 */;
|
||||||
|
}
|
||||||
7
flutter/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1300"
|
||||||
|
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">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</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>
|
||||||
|
<AdditionalOptions>
|
||||||
|
</AdditionalOptions>
|
||||||
|
</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>
|
||||||
10
flutter/ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
21
flutter/ios/Runner/AppDelegate.swift
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import UIKit
|
||||||
|
import Flutter
|
||||||
|
|
||||||
|
@UIApplicationMain
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
dummyMethodToEnforceBundling();
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func dummyMethodToEnforceBundling() {
|
||||||
|
get_rgba();
|
||||||
|
free_rgba(nil);
|
||||||
|
get_by_name("", "");
|
||||||
|
set_by_name("", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 558 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 790 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
23
flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal 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.
|
||||||
37
flutter/ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal 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>
|
||||||
26
flutter/ios/Runner/Base.lproj/Main.storyboard
Normal 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>
|
||||||
36
flutter/ios/Runner/GoogleService-Info.plist
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?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>CLIENT_ID</key>
|
||||||
|
<string>768133699366-k1rn3ls1u2n3nklmgd9t4cmpdob0c8bn.apps.googleusercontent.com</string>
|
||||||
|
<key>REVERSED_CLIENT_ID</key>
|
||||||
|
<string>com.googleusercontent.apps.768133699366-k1rn3ls1u2n3nklmgd9t4cmpdob0c8bn</string>
|
||||||
|
<key>API_KEY</key>
|
||||||
|
<string>AIzaSyCf57HjCwSokt91CqFI0Mwf8D--ek0jvfc</string>
|
||||||
|
<key>GCM_SENDER_ID</key>
|
||||||
|
<string>768133699366</string>
|
||||||
|
<key>PLIST_VERSION</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>BUNDLE_ID</key>
|
||||||
|
<string>com.carriez.flutterHbb</string>
|
||||||
|
<key>PROJECT_ID</key>
|
||||||
|
<string>rustdesk</string>
|
||||||
|
<key>STORAGE_BUCKET</key>
|
||||||
|
<string>rustdesk.appspot.com</string>
|
||||||
|
<key>IS_ADS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_ANALYTICS_ENABLED</key>
|
||||||
|
<false></false>
|
||||||
|
<key>IS_APPINVITE_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_GCM_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>IS_SIGNIN_ENABLED</key>
|
||||||
|
<true></true>
|
||||||
|
<key>GOOGLE_APP_ID</key>
|
||||||
|
<string>1:768133699366:ios:c33078a6181b9d507993e7</string>
|
||||||
|
<key>DATABASE_URL</key>
|
||||||
|
<string>https://rustdesk.firebaseio.com</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
53
flutter/ios/Runner/Info.plist
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?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>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>RustDesk</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>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
|
<false/>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
<key>io.flutter.embedded_views_preview</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>This app needs camera access to scan QR codes</string>
|
||||||
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
|
<string>This app needs photo library access to get QR codes from image</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
3
flutter/ios/Runner/Runner-Bridging-Header.h
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
|
|
||||||
|
#import "ffi.h"
|
||||||
8
flutter/ios/Runner/Runner.entitlements
Normal 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>com.apple.developer.networking.wifi-info</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
4
flutter/ios/Runner/ffi.h
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
void* get_rgba();
|
||||||
|
void free_rgba(void*);
|
||||||
|
void set_by_name(const char*, const char*);
|
||||||
|
const char* get_by_name(const char*, const char*);
|
||||||
2
flutter/ios_arm64.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
cargo build --release --target aarch64-apple-ios
|
||||||
2
flutter/ios_x64.sh
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
cargo build --release --target x86_64-apple-ios
|
||||||
307
flutter/lib/common.dart
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
|
||||||
|
import 'models/model.dart';
|
||||||
|
|
||||||
|
final globalKey = GlobalKey<NavigatorState>();
|
||||||
|
final navigationBarKey = GlobalKey();
|
||||||
|
|
||||||
|
var isAndroid = false;
|
||||||
|
var isIOS = false;
|
||||||
|
var isWeb = false;
|
||||||
|
var isDesktop = false;
|
||||||
|
var version = "";
|
||||||
|
int androidVersion = 0;
|
||||||
|
|
||||||
|
typedef F = String Function(String);
|
||||||
|
typedef FMethod = String Function(String, dynamic);
|
||||||
|
|
||||||
|
class Translator {
|
||||||
|
static late F call;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyTheme {
|
||||||
|
MyTheme._();
|
||||||
|
|
||||||
|
static const Color grayBg = Color(0xFFEEEEEE);
|
||||||
|
static const Color white = Color(0xFFFFFFFF);
|
||||||
|
static const Color accent = Color(0xFF0071FF);
|
||||||
|
static const Color accent50 = Color(0x770071FF);
|
||||||
|
static const Color accent80 = Color(0xAA0071FF);
|
||||||
|
static const Color canvasColor = Color(0xFF212121);
|
||||||
|
static const Color border = Color(0xFFCCCCCC);
|
||||||
|
static const Color idColor = Color(0xFF00B6F0);
|
||||||
|
static const Color darkGray = Color(0xFFB9BABC);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ButtonStyle flatButtonStyle = TextButton.styleFrom(
|
||||||
|
minimumSize: Size(88, 36),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(2.0)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
void showToast(String text, {Duration? duration}) {
|
||||||
|
SmartDialog.showToast(text, displayTime: duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showLoading(String text, {bool clickMaskDismiss = false}) {
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
SmartDialog.showLoading(
|
||||||
|
clickMaskDismiss: false,
|
||||||
|
builder: (context) {
|
||||||
|
return Container(
|
||||||
|
color: MyTheme.white,
|
||||||
|
constraints: BoxConstraints(maxWidth: 240),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(height: 30),
|
||||||
|
Center(child: CircularProgressIndicator()),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Center(
|
||||||
|
child: Text(Translator.call(text),
|
||||||
|
style: TextStyle(fontSize: 15))),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Center(
|
||||||
|
child: TextButton(
|
||||||
|
style: flatButtonStyle,
|
||||||
|
onPressed: () {
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
backToHome();
|
||||||
|
},
|
||||||
|
child: Text(Translator.call('Cancel'),
|
||||||
|
style: TextStyle(color: MyTheme.accent))))
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
backToHome() {
|
||||||
|
Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef DialogBuilder = CustomAlertDialog Function(
|
||||||
|
StateSetter setState, void Function([dynamic]) close);
|
||||||
|
|
||||||
|
class DialogManager {
|
||||||
|
static int _tag = 0;
|
||||||
|
|
||||||
|
static dismissByTag(String tag, [result]) {
|
||||||
|
SmartDialog.dismiss(tag: tag, result: result);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<T?> show<T>(DialogBuilder builder,
|
||||||
|
{bool clickMaskDismiss = false,
|
||||||
|
bool backDismiss = false,
|
||||||
|
String? tag,
|
||||||
|
bool useAnimation = true}) async {
|
||||||
|
final t;
|
||||||
|
if (tag != null) {
|
||||||
|
t = tag;
|
||||||
|
} else {
|
||||||
|
_tag += 1;
|
||||||
|
t = _tag.toString();
|
||||||
|
}
|
||||||
|
SmartDialog.dismiss(status: SmartStatus.allToast);
|
||||||
|
SmartDialog.dismiss(status: SmartStatus.loading);
|
||||||
|
final close = ([res]) {
|
||||||
|
SmartDialog.dismiss(tag: t, result: res);
|
||||||
|
};
|
||||||
|
final res = await SmartDialog.show<T>(
|
||||||
|
tag: t,
|
||||||
|
clickMaskDismiss: clickMaskDismiss,
|
||||||
|
backDismiss: backDismiss,
|
||||||
|
useAnimation: useAnimation,
|
||||||
|
builder: (_) => StatefulBuilder(
|
||||||
|
builder: (_, setState) => builder(setState, close)));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomAlertDialog extends StatelessWidget {
|
||||||
|
CustomAlertDialog(
|
||||||
|
{required this.title,
|
||||||
|
required this.content,
|
||||||
|
required this.actions,
|
||||||
|
this.contentPadding});
|
||||||
|
|
||||||
|
final Widget title;
|
||||||
|
final Widget content;
|
||||||
|
final List<Widget> actions;
|
||||||
|
final double? contentPadding;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
title: title,
|
||||||
|
contentPadding:
|
||||||
|
EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10),
|
||||||
|
content: content,
|
||||||
|
actions: actions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void msgBox(String type, String title, String text, {bool? hasCancel}) {
|
||||||
|
var wrap = (String text, void Function() onPressed) => ButtonTheme(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
//limits the touch area to the button area
|
||||||
|
minWidth: 0,
|
||||||
|
//wraps child's width
|
||||||
|
height: 0,
|
||||||
|
child: TextButton(
|
||||||
|
style: flatButtonStyle,
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Text(Translator.call(text),
|
||||||
|
style: TextStyle(color: MyTheme.accent))));
|
||||||
|
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
final buttons = [
|
||||||
|
wrap(Translator.call('OK'), () {
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
backToHome();
|
||||||
|
})
|
||||||
|
];
|
||||||
|
if (hasCancel == null) {
|
||||||
|
hasCancel = type != 'error';
|
||||||
|
}
|
||||||
|
if (hasCancel) {
|
||||||
|
buttons.insert(
|
||||||
|
0,
|
||||||
|
wrap(Translator.call('Cancel'), () {
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
DialogManager.show((setState, close) => CustomAlertDialog(
|
||||||
|
title: Text(translate(title), style: TextStyle(fontSize: 21)),
|
||||||
|
content: Text(Translator.call(text), style: TextStyle(fontSize: 15)),
|
||||||
|
actions: buttons));
|
||||||
|
}
|
||||||
|
|
||||||
|
Color str2color(String str, [alpha = 0xFF]) {
|
||||||
|
var hash = 160 << 16 + 114 << 8 + 91;
|
||||||
|
for (var i = 0; i < str.length; i += 1) {
|
||||||
|
hash = str.codeUnitAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
hash = hash % 16777216;
|
||||||
|
return Color((hash & 0xFF7FFF) | (alpha << 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
const K = 1024;
|
||||||
|
const M = K * K;
|
||||||
|
const G = M * K;
|
||||||
|
|
||||||
|
String readableFileSize(double size) {
|
||||||
|
if (size < K) {
|
||||||
|
return size.toString() + " B";
|
||||||
|
} else if (size < M) {
|
||||||
|
return (size / K).toStringAsFixed(2) + " KB";
|
||||||
|
} else if (size < G) {
|
||||||
|
return (size / M).toStringAsFixed(2) + " MB";
|
||||||
|
} else {
|
||||||
|
return (size / G).toStringAsFixed(2) + " GB";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flutter can't not catch PointerMoveEvent when size is 1
|
||||||
|
/// This will happen in Android AccessibilityService Input
|
||||||
|
/// android can't init dispatching size yet ,see: https://stackoverflow.com/questions/59960451/android-accessibility-dispatchgesture-is-it-possible-to-specify-pressure-for-a
|
||||||
|
/// use this temporary solution until flutter or android fixes the bug
|
||||||
|
class AccessibilityListener extends StatelessWidget {
|
||||||
|
final Widget? child;
|
||||||
|
static final offset = 100;
|
||||||
|
|
||||||
|
AccessibilityListener({this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Listener(
|
||||||
|
onPointerDown: (evt) {
|
||||||
|
if (evt.size == 1 && GestureBinding.instance != null) {
|
||||||
|
GestureBinding.instance!.handlePointerEvent(PointerAddedEvent(
|
||||||
|
pointer: evt.pointer + offset, position: evt.position));
|
||||||
|
GestureBinding.instance!.handlePointerEvent(PointerDownEvent(
|
||||||
|
pointer: evt.pointer + offset,
|
||||||
|
size: 0.1,
|
||||||
|
position: evt.position));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPointerUp: (evt) {
|
||||||
|
if (evt.size == 1 && GestureBinding.instance != null) {
|
||||||
|
GestureBinding.instance!.handlePointerEvent(PointerUpEvent(
|
||||||
|
pointer: evt.pointer + offset,
|
||||||
|
size: 0.1,
|
||||||
|
position: evt.position));
|
||||||
|
GestureBinding.instance!.handlePointerEvent(PointerRemovedEvent(
|
||||||
|
pointer: evt.pointer + offset, position: evt.position));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPointerMove: (evt) {
|
||||||
|
if (evt.size == 1 && GestureBinding.instance != null) {
|
||||||
|
GestureBinding.instance!.handlePointerEvent(PointerMoveEvent(
|
||||||
|
pointer: evt.pointer + offset,
|
||||||
|
size: 0.1,
|
||||||
|
delta: evt.delta,
|
||||||
|
position: evt.position));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PermissionManager {
|
||||||
|
static Completer<bool>? _completer;
|
||||||
|
static Timer? _timer;
|
||||||
|
static var _current = "";
|
||||||
|
|
||||||
|
static final permissions = ["audio", "file"];
|
||||||
|
|
||||||
|
static bool isWaitingFile() {
|
||||||
|
if (_completer != null) {
|
||||||
|
return !_completer!.isCompleted && _current == "file";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> check(String type) {
|
||||||
|
if (!permissions.contains(type))
|
||||||
|
return Future.error("Wrong permission!$type");
|
||||||
|
return FFI.invokeMethod("check_permission", type);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> request(String type) {
|
||||||
|
if (!permissions.contains(type))
|
||||||
|
return Future.error("Wrong permission!$type");
|
||||||
|
|
||||||
|
_current = type;
|
||||||
|
_completer = Completer<bool>();
|
||||||
|
FFI.invokeMethod("request_permission", type);
|
||||||
|
|
||||||
|
// timeout
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = Timer(Duration(seconds: 60), () {
|
||||||
|
if (_completer == null) return;
|
||||||
|
if (!_completer!.isCompleted) {
|
||||||
|
_completer!.complete(false);
|
||||||
|
}
|
||||||
|
_completer = null;
|
||||||
|
_current = "";
|
||||||
|
});
|
||||||
|
return _completer!.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
static complete(String type, bool res) {
|
||||||
|
if (type != _current) {
|
||||||
|
res = false;
|
||||||
|
}
|
||||||
|
_timer?.cancel();
|
||||||
|
_completer?.complete(res);
|
||||||
|
_current = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
55
flutter/lib/main.dart
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
import 'common.dart';
|
||||||
|
import 'models/model.dart';
|
||||||
|
import 'pages/home_page.dart';
|
||||||
|
import 'pages/server_page.dart';
|
||||||
|
import 'pages/settings_page.dart';
|
||||||
|
|
||||||
|
Future<Null> main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
var a = FFI.ffiModel.init();
|
||||||
|
var b = Firebase.initializeApp();
|
||||||
|
await a;
|
||||||
|
await b;
|
||||||
|
refreshCurrentUser();
|
||||||
|
toAndroidChannelInit();
|
||||||
|
runApp(App());
|
||||||
|
}
|
||||||
|
|
||||||
|
class App extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final analytics = FirebaseAnalytics.instance;
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
ChangeNotifierProvider.value(value: FFI.ffiModel),
|
||||||
|
ChangeNotifierProvider.value(value: FFI.imageModel),
|
||||||
|
ChangeNotifierProvider.value(value: FFI.cursorModel),
|
||||||
|
ChangeNotifierProvider.value(value: FFI.canvasModel),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
navigatorKey: globalKey,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
title: 'RustDesk',
|
||||||
|
theme: ThemeData(
|
||||||
|
primarySwatch: Colors.blue,
|
||||||
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||||
|
),
|
||||||
|
home: !isAndroid ? WebHomePage() : HomePage(),
|
||||||
|
navigatorObservers: [
|
||||||
|
FirebaseAnalyticsObserver(analytics: analytics),
|
||||||
|
FlutterSmartDialog.observer
|
||||||
|
],
|
||||||
|
builder: FlutterSmartDialog.init(
|
||||||
|
builder: isAndroid
|
||||||
|
? (_, child) => AccessibilityListener(
|
||||||
|
child: child,
|
||||||
|
)
|
||||||
|
: null)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
flutter/lib/models/chat_model.dart
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:dash_chat/dash_chat.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../widgets/overlay.dart';
|
||||||
|
import 'model.dart';
|
||||||
|
|
||||||
|
class ChatModel with ChangeNotifier {
|
||||||
|
static final clientModeID = -1;
|
||||||
|
|
||||||
|
final Map<int, List<ChatMessage>> _messages = Map()..[clientModeID] = [];
|
||||||
|
|
||||||
|
final ChatUser me = ChatUser(
|
||||||
|
uid: "",
|
||||||
|
name: "Me",
|
||||||
|
);
|
||||||
|
|
||||||
|
final _scroller = ScrollController();
|
||||||
|
|
||||||
|
var _currentID = clientModeID;
|
||||||
|
|
||||||
|
ScrollController get scroller => _scroller;
|
||||||
|
|
||||||
|
Map<int, List<ChatMessage>> get messages => _messages;
|
||||||
|
|
||||||
|
int get currentID => _currentID;
|
||||||
|
|
||||||
|
ChatUser get currentUser =>
|
||||||
|
FFI.serverModel.clients[_currentID]?.chatUser ?? me;
|
||||||
|
|
||||||
|
changeCurrentID(int id) {
|
||||||
|
if (_messages.containsKey(id)) {
|
||||||
|
_currentID = id;
|
||||||
|
notifyListeners();
|
||||||
|
} else {
|
||||||
|
final chatUser = FFI.serverModel.clients[id]?.chatUser;
|
||||||
|
if (chatUser == null) {
|
||||||
|
return debugPrint(
|
||||||
|
"Failed to changeCurrentID,remote user doesn't exist");
|
||||||
|
}
|
||||||
|
_messages[id] = [];
|
||||||
|
_currentID = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
receive(int id, String text) {
|
||||||
|
if (text.isEmpty) return;
|
||||||
|
// first message show overlay icon
|
||||||
|
if (chatIconOverlayEntry == null) {
|
||||||
|
showChatIconOverlay();
|
||||||
|
}
|
||||||
|
late final chatUser;
|
||||||
|
if (id == clientModeID) {
|
||||||
|
chatUser = ChatUser(
|
||||||
|
name: FFI.ffiModel.pi.username,
|
||||||
|
uid: FFI.getId(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
chatUser = FFI.serverModel.clients[id]?.chatUser;
|
||||||
|
}
|
||||||
|
if (chatUser == null) {
|
||||||
|
return debugPrint("Failed to receive msg,user doesn't exist");
|
||||||
|
}
|
||||||
|
if (!_messages.containsKey(id)) {
|
||||||
|
_messages[id] = [];
|
||||||
|
}
|
||||||
|
_messages[id]!.add(ChatMessage(text: text, user: chatUser));
|
||||||
|
_currentID = id;
|
||||||
|
notifyListeners();
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom() {
|
||||||
|
Future.delayed(Duration(milliseconds: 500), () {
|
||||||
|
_scroller.animateTo(_scroller.position.maxScrollExtent,
|
||||||
|
duration: Duration(milliseconds: 200),
|
||||||
|
curve: Curves.fastLinearToSlowEaseIn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
send(ChatMessage message) {
|
||||||
|
if (message.text != null && message.text!.isNotEmpty) {
|
||||||
|
_messages[_currentID]?.add(message);
|
||||||
|
if (_currentID == clientModeID) {
|
||||||
|
FFI.setByName("chat_client_mode", message.text!);
|
||||||
|
} else {
|
||||||
|
final msg = Map()
|
||||||
|
..["id"] = _currentID
|
||||||
|
..["text"] = message.text!;
|
||||||
|
FFI.setByName("chat_server_mode", jsonEncode(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
hideChatIconOverlay();
|
||||||
|
hideChatWindowOverlay();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
775
flutter/lib/models/file_model.dart
Normal file
@ -0,0 +1,775 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/pages/file_manager_page.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:path/path.dart' as Path;
|
||||||
|
|
||||||
|
import 'model.dart';
|
||||||
|
|
||||||
|
enum SortBy { Name, Type, Modified, Size }
|
||||||
|
|
||||||
|
class FileModel extends ChangeNotifier {
|
||||||
|
var _isLocal = false;
|
||||||
|
var _selectMode = false;
|
||||||
|
|
||||||
|
var _localOption = DirectoryOption();
|
||||||
|
var _remoteOption = DirectoryOption();
|
||||||
|
|
||||||
|
var _jobId = 0;
|
||||||
|
|
||||||
|
var _jobProgress = JobProgress(); // from rust update
|
||||||
|
|
||||||
|
bool get isLocal => _isLocal;
|
||||||
|
|
||||||
|
bool get selectMode => _selectMode;
|
||||||
|
|
||||||
|
JobProgress get jobProgress => _jobProgress;
|
||||||
|
|
||||||
|
JobState get jobState => _jobProgress.state;
|
||||||
|
|
||||||
|
SortBy _sortStyle = SortBy.Name;
|
||||||
|
|
||||||
|
SortBy get sortStyle => _sortStyle;
|
||||||
|
|
||||||
|
FileDirectory _currentLocalDir = FileDirectory();
|
||||||
|
|
||||||
|
FileDirectory get currentLocalDir => _currentLocalDir;
|
||||||
|
|
||||||
|
FileDirectory _currentRemoteDir = FileDirectory();
|
||||||
|
|
||||||
|
FileDirectory get currentRemoteDir => _currentRemoteDir;
|
||||||
|
|
||||||
|
FileDirectory get currentDir => _isLocal ? currentLocalDir : currentRemoteDir;
|
||||||
|
|
||||||
|
String get currentHome => _isLocal ? _localOption.home : _remoteOption.home;
|
||||||
|
|
||||||
|
String get currentShortPath {
|
||||||
|
if (currentDir.path.startsWith(currentHome)) {
|
||||||
|
var path = currentDir.path.replaceFirst(currentHome, "");
|
||||||
|
if (path.length == 0) return "";
|
||||||
|
if (path[0] == "/" || path[0] == "\\") {
|
||||||
|
// remove more '/' or '\'
|
||||||
|
path = path.replaceFirst(path[0], "");
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
} else {
|
||||||
|
return currentDir.path.replaceFirst(currentHome, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get currentShowHidden =>
|
||||||
|
_isLocal ? _localOption.showHidden : _remoteOption.showHidden;
|
||||||
|
|
||||||
|
bool get currentIsWindows =>
|
||||||
|
_isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
||||||
|
|
||||||
|
final _fileFetcher = FileFetcher();
|
||||||
|
|
||||||
|
final _jobResultListener = JobResultListener<Map<String, dynamic>>();
|
||||||
|
|
||||||
|
toggleSelectMode() {
|
||||||
|
if (jobState == JobState.inProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_selectMode = !_selectMode;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePage() {
|
||||||
|
_isLocal = !_isLocal;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleShowHidden({bool? showHidden, bool? local}) {
|
||||||
|
final isLocal = local ?? _isLocal;
|
||||||
|
if (isLocal) {
|
||||||
|
_localOption.showHidden = showHidden ?? !_localOption.showHidden;
|
||||||
|
} else {
|
||||||
|
_remoteOption.showHidden = showHidden ?? !_remoteOption.showHidden;
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
tryUpdateJobProgress(Map<String, dynamic> evt) {
|
||||||
|
try {
|
||||||
|
int id = int.parse(evt['id']);
|
||||||
|
_jobProgress.id = id;
|
||||||
|
_jobProgress.fileNum = int.parse(evt['file_num']);
|
||||||
|
_jobProgress.speed = double.parse(evt['speed']);
|
||||||
|
_jobProgress.finishedSize = int.parse(evt['finished_size']);
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveFileDir(Map<String, dynamic> evt) {
|
||||||
|
if (_remoteOption.home.isEmpty && evt['is_local'] == "false") {
|
||||||
|
// init remote home, the connection will automatic read remote home when established,
|
||||||
|
try {
|
||||||
|
final fd = FileDirectory.fromJson(jsonDecode(evt['value']));
|
||||||
|
fd.format(_remoteOption.isWindows, sort: _sortStyle);
|
||||||
|
_remoteOption.home = fd.path;
|
||||||
|
debugPrint("init remote home:${fd.path}");
|
||||||
|
_currentRemoteDir = fd;
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
} finally {}
|
||||||
|
}
|
||||||
|
_fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
|
||||||
|
}
|
||||||
|
|
||||||
|
jobDone(Map<String, dynamic> evt) {
|
||||||
|
if (_jobResultListener.isListening) {
|
||||||
|
_jobResultListener.complete(evt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_selectMode = false;
|
||||||
|
_jobProgress.state = JobState.done;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
jobError(Map<String, dynamic> evt) {
|
||||||
|
if (_jobResultListener.isListening) {
|
||||||
|
_jobResultListener.complete(evt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("jobError $evt");
|
||||||
|
_selectMode = false;
|
||||||
|
_jobProgress.clear();
|
||||||
|
_jobProgress.state = JobState.error;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
jobReset() {
|
||||||
|
_jobProgress.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
onReady() async {
|
||||||
|
_localOption.home = FFI.getByName("get_home_dir");
|
||||||
|
_localOption.showHidden =
|
||||||
|
FFI.getByName("peer_option", "local_show_hidden").isNotEmpty;
|
||||||
|
|
||||||
|
_remoteOption.showHidden =
|
||||||
|
FFI.getByName("peer_option", "remote_show_hidden").isNotEmpty;
|
||||||
|
_remoteOption.isWindows = FFI.ffiModel.pi.platform == "Windows";
|
||||||
|
|
||||||
|
debugPrint("remote platform: ${FFI.ffiModel.pi.platform}");
|
||||||
|
|
||||||
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
final local = FFI.getByName("peer_option", "local_dir");
|
||||||
|
final remote = FFI.getByName("peer_option", "remote_dir");
|
||||||
|
openDirectory(local.isEmpty ? _localOption.home : local, isLocal: true);
|
||||||
|
openDirectory(remote.isEmpty ? _remoteOption.home : remote, isLocal: false);
|
||||||
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
if (_currentLocalDir.path.isEmpty) {
|
||||||
|
openDirectory(_localOption.home, isLocal: true);
|
||||||
|
}
|
||||||
|
if (_currentRemoteDir.path.isEmpty) {
|
||||||
|
openDirectory(_remoteOption.home, isLocal: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
|
||||||
|
// save config
|
||||||
|
Map<String, String> msg = Map();
|
||||||
|
|
||||||
|
msg["name"] = "local_dir";
|
||||||
|
msg["value"] = _currentLocalDir.path;
|
||||||
|
FFI.setByName('peer_option', jsonEncode(msg));
|
||||||
|
|
||||||
|
msg["name"] = "local_show_hidden";
|
||||||
|
msg["value"] = _localOption.showHidden ? "Y" : "";
|
||||||
|
FFI.setByName('peer_option', jsonEncode(msg));
|
||||||
|
|
||||||
|
msg["name"] = "remote_dir";
|
||||||
|
msg["value"] = _currentRemoteDir.path;
|
||||||
|
FFI.setByName('peer_option', jsonEncode(msg));
|
||||||
|
|
||||||
|
msg["name"] = "remote_show_hidden";
|
||||||
|
msg["value"] = _remoteOption.showHidden ? "Y" : "";
|
||||||
|
FFI.setByName('peer_option', jsonEncode(msg));
|
||||||
|
_currentLocalDir.clear();
|
||||||
|
_currentRemoteDir.clear();
|
||||||
|
_localOption.clear();
|
||||||
|
_remoteOption.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
openDirectory(currentDir.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
openDirectory(String path, {bool? isLocal}) async {
|
||||||
|
isLocal = isLocal ?? _isLocal;
|
||||||
|
final showHidden =
|
||||||
|
isLocal ? _localOption.showHidden : _remoteOption.showHidden;
|
||||||
|
final isWindows =
|
||||||
|
isLocal ? _localOption.isWindows : _remoteOption.isWindows;
|
||||||
|
try {
|
||||||
|
final fd = await _fileFetcher.fetchDirectory(path, isLocal, showHidden);
|
||||||
|
fd.format(isWindows, sort: _sortStyle);
|
||||||
|
if (isLocal) {
|
||||||
|
_currentLocalDir = fd;
|
||||||
|
} else {
|
||||||
|
_currentRemoteDir = fd;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Failed to openDirectory :$e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goHome() {
|
||||||
|
openDirectory(currentHome);
|
||||||
|
}
|
||||||
|
|
||||||
|
goToParentDirectory() {
|
||||||
|
final parent = PathUtil.dirname(currentDir.path, currentIsWindows);
|
||||||
|
openDirectory(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendFiles(SelectedItems items) {
|
||||||
|
if (items.isLocal == null) {
|
||||||
|
debugPrint("Failed to sendFiles ,wrong path state");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_jobProgress.state = JobState.inProgress;
|
||||||
|
final toPath =
|
||||||
|
items.isLocal! ? currentRemoteDir.path : currentLocalDir.path;
|
||||||
|
final isWindows =
|
||||||
|
items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows;
|
||||||
|
final showHidden =
|
||||||
|
items.isLocal! ? _localOption.showHidden : _remoteOption.showHidden;
|
||||||
|
items.items.forEach((from) {
|
||||||
|
_jobId++;
|
||||||
|
final msg = {
|
||||||
|
"id": _jobId.toString(),
|
||||||
|
"path": from.path,
|
||||||
|
"to": PathUtil.join(toPath, from.name, isWindows),
|
||||||
|
"show_hidden": showHidden.toString(),
|
||||||
|
"is_remote": (!(items.isLocal!)).toString()
|
||||||
|
};
|
||||||
|
FFI.setByName("send_files", jsonEncode(msg));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool removeCheckboxRemember = false;
|
||||||
|
|
||||||
|
removeAction(SelectedItems items) async {
|
||||||
|
removeCheckboxRemember = false;
|
||||||
|
if (items.isLocal == null) {
|
||||||
|
debugPrint("Failed to removeFile, wrong path state");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final isWindows =
|
||||||
|
items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows;
|
||||||
|
await Future.forEach(items.items, (Entry item) async {
|
||||||
|
_jobId++;
|
||||||
|
var title = "";
|
||||||
|
var content = "";
|
||||||
|
late final List<Entry> entries;
|
||||||
|
if (item.isFile) {
|
||||||
|
title = translate("Are you sure you want to delete this file?");
|
||||||
|
content = "${item.name}";
|
||||||
|
entries = [item];
|
||||||
|
} else if (item.isDirectory) {
|
||||||
|
title = translate("Not an empty directory");
|
||||||
|
showLoading(translate("Waiting"));
|
||||||
|
final fd = await _fileFetcher.fetchDirectoryRecursive(
|
||||||
|
_jobId, item.path, items.isLocal!, true);
|
||||||
|
if (fd.path.isEmpty) {
|
||||||
|
fd.path = item.path;
|
||||||
|
}
|
||||||
|
fd.format(isWindows);
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
if (fd.entries.isEmpty) {
|
||||||
|
final confirm = await showRemoveDialog(
|
||||||
|
translate(
|
||||||
|
"Are you sure you want to delete this empty directory?"),
|
||||||
|
item.name,
|
||||||
|
false);
|
||||||
|
if (confirm == true) {
|
||||||
|
sendRemoveEmptyDir(item.path, 0, items.isLocal!);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entries = fd.entries;
|
||||||
|
} else {
|
||||||
|
entries = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < entries.length; i++) {
|
||||||
|
final dirShow = item.isDirectory
|
||||||
|
? "${translate("Are you sure you want to delete the file of this directory?")}\n"
|
||||||
|
: "";
|
||||||
|
final count = entries.length > 1 ? "${i + 1}/${entries.length}" : "";
|
||||||
|
content = dirShow + "$count \n${entries[i].path}";
|
||||||
|
final confirm =
|
||||||
|
await showRemoveDialog(title, content, item.isDirectory);
|
||||||
|
try {
|
||||||
|
if (confirm == true) {
|
||||||
|
sendRemoveFile(entries[i].path, i, items.isLocal!);
|
||||||
|
final res = await _jobResultListener.start();
|
||||||
|
// handle remove res;
|
||||||
|
if (item.isDirectory &&
|
||||||
|
res['file_num'] == (entries.length - 1).toString()) {
|
||||||
|
sendRemoveEmptyDir(item.path, i, items.isLocal!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removeCheckboxRemember) {
|
||||||
|
if (confirm == true) {
|
||||||
|
for (var j = i + 1; j < entries.length; j++) {
|
||||||
|
sendRemoveFile(entries[j].path, j, items.isLocal!);
|
||||||
|
final res = await _jobResultListener.start();
|
||||||
|
if (item.isDirectory &&
|
||||||
|
res['file_num'] == (entries.length - 1).toString()) {
|
||||||
|
sendRemoveEmptyDir(item.path, i, items.isLocal!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_selectMode = false;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool?> showRemoveDialog(
|
||||||
|
String title, String content, bool showCheckbox) async {
|
||||||
|
return await DialogManager.show<bool>(
|
||||||
|
(setState, Function(bool v) close) => CustomAlertDialog(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.warning, color: Colors.red),
|
||||||
|
SizedBox(width: 20),
|
||||||
|
Text(title)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(content),
|
||||||
|
SizedBox(height: 5),
|
||||||
|
Text(translate("This is irreversible!"),
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
showCheckbox
|
||||||
|
? CheckboxListTile(
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
dense: true,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
title: Text(
|
||||||
|
translate("Do this for all conflicts"),
|
||||||
|
),
|
||||||
|
value: removeCheckboxRemember,
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v == null) return;
|
||||||
|
setState(() => removeCheckboxRemember = v);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: SizedBox.shrink()
|
||||||
|
]),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
style: flatButtonStyle,
|
||||||
|
onPressed: () => close(false),
|
||||||
|
child: Text(translate("Cancel"))),
|
||||||
|
TextButton(
|
||||||
|
style: flatButtonStyle,
|
||||||
|
onPressed: () => close(true),
|
||||||
|
child: Text(translate("OK"))),
|
||||||
|
]),
|
||||||
|
useAnimation: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRemoveFile(String path, int fileNum, bool isLocal) {
|
||||||
|
final msg = {
|
||||||
|
"id": _jobId.toString(),
|
||||||
|
"path": path,
|
||||||
|
"file_num": fileNum.toString(),
|
||||||
|
"is_remote": (!(isLocal)).toString()
|
||||||
|
};
|
||||||
|
FFI.setByName("remove_file", jsonEncode(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRemoveEmptyDir(String path, int fileNum, bool isLocal) {
|
||||||
|
final msg = {
|
||||||
|
"id": _jobId.toString(),
|
||||||
|
"path": path,
|
||||||
|
"is_remote": (!isLocal).toString()
|
||||||
|
};
|
||||||
|
FFI.setByName("remove_all_empty_dirs", jsonEncode(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
createDir(String path) {
|
||||||
|
_jobId++;
|
||||||
|
final msg = {
|
||||||
|
"id": _jobId.toString(),
|
||||||
|
"path": path,
|
||||||
|
"is_remote": (!isLocal).toString()
|
||||||
|
};
|
||||||
|
FFI.setByName("create_dir", jsonEncode(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelJob(int id) {
|
||||||
|
FFI.setByName("cancel_job", id.toString());
|
||||||
|
jobReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
changeSortStyle(SortBy sort) {
|
||||||
|
_sortStyle = sort;
|
||||||
|
_currentLocalDir.changeSortStyle(sort);
|
||||||
|
_currentRemoteDir.changeSortStyle(sort);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JobResultListener<T> {
|
||||||
|
Completer<T>? _completer;
|
||||||
|
Timer? _timer;
|
||||||
|
int _timeoutSecond = 5;
|
||||||
|
|
||||||
|
bool get isListening => _completer != null;
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
if (_completer != null) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
_completer!.completeError("Cancel manually");
|
||||||
|
_completer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> start() {
|
||||||
|
if (_completer != null) return Future.error("Already start listen");
|
||||||
|
_completer = Completer();
|
||||||
|
_timer = Timer(Duration(seconds: _timeoutSecond), () {
|
||||||
|
if (!_completer!.isCompleted) {
|
||||||
|
_completer!.completeError("Time out");
|
||||||
|
}
|
||||||
|
_completer = null;
|
||||||
|
});
|
||||||
|
return _completer!.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
complete(T res) {
|
||||||
|
if (_completer != null) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
_completer!.complete(res);
|
||||||
|
_completer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileFetcher {
|
||||||
|
// Map<String,Completer<FileDirectory>> localTasks = Map(); // now we only use read local dir sync
|
||||||
|
Map<String, Completer<FileDirectory>> remoteTasks = Map();
|
||||||
|
Map<int, Completer<FileDirectory>> readRecursiveTasks = Map();
|
||||||
|
|
||||||
|
Future<FileDirectory> registerReadTask(bool isLocal, String path) {
|
||||||
|
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
|
||||||
|
final tasks = remoteTasks; // bypass now
|
||||||
|
if (tasks.containsKey(path)) {
|
||||||
|
throw "Failed to registerReadTask, already have same read job";
|
||||||
|
}
|
||||||
|
final c = Completer<FileDirectory>();
|
||||||
|
tasks[path] = c;
|
||||||
|
|
||||||
|
Timer(Duration(seconds: 2), () {
|
||||||
|
tasks.remove(path);
|
||||||
|
if (c.isCompleted) return;
|
||||||
|
c.completeError("Failed to read dir,timeout");
|
||||||
|
});
|
||||||
|
return c.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<FileDirectory> registerReadRecursiveTask(int id) {
|
||||||
|
final tasks = readRecursiveTasks;
|
||||||
|
if (tasks.containsKey(id)) {
|
||||||
|
throw "Failed to registerRemoveTask, already have same ReadRecursive job";
|
||||||
|
}
|
||||||
|
final c = Completer<FileDirectory>();
|
||||||
|
tasks[id] = c;
|
||||||
|
|
||||||
|
Timer(Duration(seconds: 2), () {
|
||||||
|
tasks.remove(id);
|
||||||
|
if (c.isCompleted) return;
|
||||||
|
c.completeError("Failed to read dir,timeout");
|
||||||
|
});
|
||||||
|
return c.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
tryCompleteTask(String? msg, String? isLocalStr) {
|
||||||
|
if (msg == null || isLocalStr == null) return;
|
||||||
|
late final isLocal;
|
||||||
|
late final tasks;
|
||||||
|
if (isLocalStr == "true") {
|
||||||
|
isLocal = true;
|
||||||
|
} else if (isLocalStr == "false") {
|
||||||
|
isLocal = false;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final fd = FileDirectory.fromJson(jsonDecode(msg));
|
||||||
|
if (fd.id > 0) {
|
||||||
|
// fd.id > 0 is result for read recursive
|
||||||
|
// to-do later,will be better if every fetch use ID,so that there will only one task map for read and recursive read
|
||||||
|
tasks = readRecursiveTasks;
|
||||||
|
final completer = tasks.remove(fd.id);
|
||||||
|
completer?.complete(fd);
|
||||||
|
} else if (fd.path.isNotEmpty) {
|
||||||
|
// result for normal read dir
|
||||||
|
// final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
|
||||||
|
tasks = remoteTasks; // bypass now
|
||||||
|
final completer = tasks.remove(fd.path);
|
||||||
|
completer?.complete(fd);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("tryCompleteJob err :$e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<FileDirectory> fetchDirectory(
|
||||||
|
String path, bool isLocal, bool showHidden) async {
|
||||||
|
try {
|
||||||
|
final msg = {"path": path, "show_hidden": showHidden.toString()};
|
||||||
|
if (isLocal) {
|
||||||
|
final res = FFI.getByName("read_local_dir_sync", jsonEncode(msg));
|
||||||
|
final fd = FileDirectory.fromJson(jsonDecode(res));
|
||||||
|
return fd;
|
||||||
|
} else {
|
||||||
|
FFI.setByName("read_remote_dir", jsonEncode(msg));
|
||||||
|
return registerReadTask(isLocal, path);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return Future.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<FileDirectory> fetchDirectoryRecursive(
|
||||||
|
int id, String path, bool isLocal, bool showHidden) async {
|
||||||
|
// TODO test Recursive is show hidden default?
|
||||||
|
try {
|
||||||
|
final msg = {
|
||||||
|
"id": id.toString(),
|
||||||
|
"path": path,
|
||||||
|
"show_hidden": showHidden.toString(),
|
||||||
|
"is_remote": (!isLocal).toString()
|
||||||
|
};
|
||||||
|
FFI.setByName("read_dir_recursive", jsonEncode(msg));
|
||||||
|
return registerReadRecursiveTask(id);
|
||||||
|
} catch (e) {
|
||||||
|
return Future.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileDirectory {
|
||||||
|
List<Entry> entries = [];
|
||||||
|
int id = 0;
|
||||||
|
String path = "";
|
||||||
|
|
||||||
|
FileDirectory();
|
||||||
|
|
||||||
|
FileDirectory.fromJson(Map<String, dynamic> json) {
|
||||||
|
id = json['id'];
|
||||||
|
path = json['path'];
|
||||||
|
json['entries'].forEach((v) {
|
||||||
|
entries.add(new Entry.fromJson(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate full path for every entry , init sort style if need.
|
||||||
|
format(bool isWindows, {SortBy? sort}) {
|
||||||
|
entries.forEach((entry) {
|
||||||
|
entry.path = PathUtil.join(path, entry.name, isWindows);
|
||||||
|
});
|
||||||
|
if (sort != null) {
|
||||||
|
changeSortStyle(sort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeSortStyle(SortBy sort) {
|
||||||
|
entries = _sortList(entries, sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
entries = [];
|
||||||
|
id = 0;
|
||||||
|
path = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Entry {
|
||||||
|
int entryType = 4;
|
||||||
|
int modifiedTime = 0;
|
||||||
|
String name = "";
|
||||||
|
String path = "";
|
||||||
|
int size = 0;
|
||||||
|
|
||||||
|
Entry();
|
||||||
|
|
||||||
|
Entry.fromJson(Map<String, dynamic> json) {
|
||||||
|
entryType = json['entry_type'];
|
||||||
|
modifiedTime = json['modified_time'];
|
||||||
|
name = json['name'];
|
||||||
|
size = json['size'];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isFile => entryType > 3;
|
||||||
|
|
||||||
|
bool get isDirectory => entryType <= 3;
|
||||||
|
|
||||||
|
DateTime lastModified() {
|
||||||
|
return DateTime.fromMillisecondsSinceEpoch(modifiedTime * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum JobState { none, inProgress, done, error }
|
||||||
|
|
||||||
|
class JobProgress {
|
||||||
|
JobState state = JobState.none;
|
||||||
|
var id = 0;
|
||||||
|
var fileNum = 0;
|
||||||
|
var speed = 0.0;
|
||||||
|
var finishedSize = 0;
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
state = JobState.none;
|
||||||
|
id = 0;
|
||||||
|
fileNum = 0;
|
||||||
|
speed = 0;
|
||||||
|
finishedSize = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PathStat {
|
||||||
|
final String path;
|
||||||
|
final DateTime dateTime;
|
||||||
|
|
||||||
|
_PathStat(this.path, this.dateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PathUtil {
|
||||||
|
static final windowsContext = Path.Context(style: Path.Style.windows);
|
||||||
|
static final posixContext = Path.Context(style: Path.Style.posix);
|
||||||
|
|
||||||
|
static String join(String path1, String path2, bool isWindows) {
|
||||||
|
final pathUtil = isWindows ? windowsContext : posixContext;
|
||||||
|
return pathUtil.join(path1, path2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<String> split(String path, bool isWindows) {
|
||||||
|
final pathUtil = isWindows ? windowsContext : posixContext;
|
||||||
|
return pathUtil.split(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String dirname(String path, bool isWindows) {
|
||||||
|
final pathUtil = isWindows ? windowsContext : posixContext;
|
||||||
|
return pathUtil.dirname(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectoryOption {
|
||||||
|
String home;
|
||||||
|
bool showHidden;
|
||||||
|
bool isWindows;
|
||||||
|
|
||||||
|
DirectoryOption(
|
||||||
|
{this.home = "", this.showHidden = false, this.isWindows = false});
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
home = "";
|
||||||
|
showHidden = false;
|
||||||
|
isWindows = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// code from file_manager pkg after edit
|
||||||
|
List<Entry> _sortList(List<Entry> list, SortBy sortType) {
|
||||||
|
if (sortType == SortBy.Name) {
|
||||||
|
// making list of only folders.
|
||||||
|
final dirs = list.where((element) => element.isDirectory).toList();
|
||||||
|
// sorting folder list by name.
|
||||||
|
dirs.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||||
|
|
||||||
|
// making list of only flies.
|
||||||
|
final files = list.where((element) => element.isFile).toList();
|
||||||
|
// sorting files list by name.
|
||||||
|
files.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||||
|
|
||||||
|
// first folders will go to list (if available) then files will go to list.
|
||||||
|
return [...dirs, ...files];
|
||||||
|
} else if (sortType == SortBy.Modified) {
|
||||||
|
// making the list of Path & DateTime
|
||||||
|
List<_PathStat> _pathStat = [];
|
||||||
|
for (Entry e in list) {
|
||||||
|
_pathStat.add(_PathStat(e.name, e.lastModified()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort _pathStat according to date
|
||||||
|
_pathStat.sort((b, a) => a.dateTime.compareTo(b.dateTime));
|
||||||
|
|
||||||
|
// sorting [list] according to [_pathStat]
|
||||||
|
list.sort((a, b) => _pathStat
|
||||||
|
.indexWhere((element) => element.path == a.name)
|
||||||
|
.compareTo(_pathStat.indexWhere((element) => element.path == b.name)));
|
||||||
|
return list;
|
||||||
|
} else if (sortType == SortBy.Type) {
|
||||||
|
// making list of only folders.
|
||||||
|
final dirs = list.where((element) => element.isDirectory).toList();
|
||||||
|
|
||||||
|
// sorting folders by name.
|
||||||
|
dirs.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||||
|
|
||||||
|
// making the list of files
|
||||||
|
final files = list.where((element) => element.isFile).toList();
|
||||||
|
|
||||||
|
// sorting files list by extension.
|
||||||
|
files.sort((a, b) => a.name
|
||||||
|
.toLowerCase()
|
||||||
|
.split('.')
|
||||||
|
.last
|
||||||
|
.compareTo(b.name.toLowerCase().split('.').last));
|
||||||
|
return [...dirs, ...files];
|
||||||
|
} else if (sortType == SortBy.Size) {
|
||||||
|
// create list of path and size
|
||||||
|
Map<String, int> _sizeMap = {};
|
||||||
|
for (Entry e in list) {
|
||||||
|
_sizeMap[e.name] = e.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// making list of only folders.
|
||||||
|
final dirs = list.where((element) => element.isDirectory).toList();
|
||||||
|
// sorting folder list by name.
|
||||||
|
dirs.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||||
|
|
||||||
|
// making list of only flies.
|
||||||
|
final files = list.where((element) => element.isFile).toList();
|
||||||
|
|
||||||
|
// creating sorted list of [_sizeMapList] by size.
|
||||||
|
final List<MapEntry<String, int>> _sizeMapList = _sizeMap.entries.toList();
|
||||||
|
_sizeMapList.sort((b, a) => a.value.compareTo(b.value));
|
||||||
|
|
||||||
|
// sort [list] according to [_sizeMapList]
|
||||||
|
files.sort((a, b) => _sizeMapList
|
||||||
|
.indexWhere((element) => element.key == a.name)
|
||||||
|
.compareTo(
|
||||||
|
_sizeMapList.indexWhere((element) => element.key == b.name)));
|
||||||
|
return [...dirs, ...files];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
967
flutter/lib/models/model.dart
Normal file
@ -0,0 +1,967 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
|
import 'package:flutter_hbb/models/file_model.dart';
|
||||||
|
import 'package:flutter_hbb/models/server_model.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
import '../common.dart';
|
||||||
|
import '../widgets/dialog.dart';
|
||||||
|
import '../widgets/overlay.dart';
|
||||||
|
import 'native_model.dart' if (dart.library.html) 'web_model.dart';
|
||||||
|
|
||||||
|
typedef HandleMsgBox = void Function(Map<String, dynamic> evt, String id);
|
||||||
|
|
||||||
|
class FfiModel with ChangeNotifier {
|
||||||
|
PeerInfo _pi = PeerInfo();
|
||||||
|
Display _display = Display();
|
||||||
|
var _decoding = false;
|
||||||
|
bool _waitForImage = false;
|
||||||
|
var _inputBlocked = false;
|
||||||
|
final _permissions = Map<String, bool>();
|
||||||
|
bool? _secure;
|
||||||
|
bool? _direct;
|
||||||
|
bool _touchMode = false;
|
||||||
|
Timer? _timer;
|
||||||
|
var _reconnects = 1;
|
||||||
|
|
||||||
|
Map<String, bool> get permissions => _permissions;
|
||||||
|
|
||||||
|
Display get display => _display;
|
||||||
|
|
||||||
|
bool? get secure => _secure;
|
||||||
|
|
||||||
|
bool? get direct => _direct;
|
||||||
|
|
||||||
|
PeerInfo get pi => _pi;
|
||||||
|
|
||||||
|
bool get inputBlocked => _inputBlocked;
|
||||||
|
|
||||||
|
bool get touchMode => _touchMode;
|
||||||
|
|
||||||
|
bool get isPeerAndroid => _pi.platform == "Android";
|
||||||
|
|
||||||
|
set inputBlocked(v) {
|
||||||
|
_inputBlocked = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
FfiModel() {
|
||||||
|
Translator.call = translate;
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> init() async {
|
||||||
|
await PlatformFFI.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleTouchMode() {
|
||||||
|
if (!isPeerAndroid) {
|
||||||
|
_touchMode = !_touchMode;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePermission(Map<String, dynamic> evt) {
|
||||||
|
evt.forEach((k, v) {
|
||||||
|
if (k == 'name') return;
|
||||||
|
_permissions[k] = v == 'true';
|
||||||
|
});
|
||||||
|
print('$_permissions');
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateUser() {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool keyboard() => _permissions['keyboard'] != false;
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_pi = PeerInfo();
|
||||||
|
_display = Display();
|
||||||
|
_waitForImage = false;
|
||||||
|
_secure = null;
|
||||||
|
_direct = null;
|
||||||
|
_inputBlocked = false;
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = null;
|
||||||
|
clearPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setConnectionType(bool secure, bool direct) {
|
||||||
|
_secure = secure;
|
||||||
|
_direct = direct;
|
||||||
|
}
|
||||||
|
|
||||||
|
Image? getConnectionImage() {
|
||||||
|
String? icon;
|
||||||
|
if (secure == true && direct == true) {
|
||||||
|
icon = 'secure';
|
||||||
|
} else if (secure == false && direct == true) {
|
||||||
|
icon = 'insecure';
|
||||||
|
} else if (secure == false && direct == false) {
|
||||||
|
icon = 'insecure_relay';
|
||||||
|
} else if (secure == true && direct == false) {
|
||||||
|
icon = 'secure_relay';
|
||||||
|
}
|
||||||
|
return icon == null
|
||||||
|
? null
|
||||||
|
: Image.asset('assets/$icon.png', width: 48, height: 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearPermissions() {
|
||||||
|
_inputBlocked = false;
|
||||||
|
_permissions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(String peerId) {
|
||||||
|
var pos;
|
||||||
|
for (;;) {
|
||||||
|
var evt = FFI.popEvent();
|
||||||
|
if (evt == null) break;
|
||||||
|
var name = evt['name'];
|
||||||
|
if (name == 'msgbox') {
|
||||||
|
handleMsgBox(evt, peerId);
|
||||||
|
} else if (name == 'peer_info') {
|
||||||
|
handlePeerInfo(evt);
|
||||||
|
} else if (name == 'connection_ready') {
|
||||||
|
FFI.ffiModel.setConnectionType(
|
||||||
|
evt['secure'] == 'true', evt['direct'] == 'true');
|
||||||
|
} else if (name == 'switch_display') {
|
||||||
|
handleSwitchDisplay(evt);
|
||||||
|
} else if (name == 'cursor_data') {
|
||||||
|
FFI.cursorModel.updateCursorData(evt);
|
||||||
|
} else if (name == 'cursor_id') {
|
||||||
|
FFI.cursorModel.updateCursorId(evt);
|
||||||
|
} else if (name == 'cursor_position') {
|
||||||
|
pos = evt;
|
||||||
|
} else if (name == 'clipboard') {
|
||||||
|
Clipboard.setData(ClipboardData(text: evt['content']));
|
||||||
|
} else if (name == 'permission') {
|
||||||
|
FFI.ffiModel.updatePermission(evt);
|
||||||
|
} else if (name == 'chat_client_mode') {
|
||||||
|
FFI.chatModel.receive(ChatModel.clientModeID, evt['text'] ?? "");
|
||||||
|
} else if (name == 'chat_server_mode') {
|
||||||
|
FFI.chatModel
|
||||||
|
.receive(int.parse(evt['id'] as String), evt['text'] ?? "");
|
||||||
|
} else if (name == 'file_dir') {
|
||||||
|
FFI.fileModel.receiveFileDir(evt);
|
||||||
|
} else if (name == 'job_progress') {
|
||||||
|
FFI.fileModel.tryUpdateJobProgress(evt);
|
||||||
|
} else if (name == 'job_done') {
|
||||||
|
FFI.fileModel.jobDone(evt);
|
||||||
|
} else if (name == 'job_error') {
|
||||||
|
FFI.fileModel.jobError(evt);
|
||||||
|
} else if (name == 'try_start_without_auth') {
|
||||||
|
FFI.serverModel.loginRequest(evt);
|
||||||
|
} else if (name == 'on_client_authorized') {
|
||||||
|
FFI.serverModel.onClientAuthorized(evt);
|
||||||
|
} else if (name == 'on_client_remove') {
|
||||||
|
FFI.serverModel.onClientRemove(evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pos != null) FFI.cursorModel.updateCursorPosition(pos);
|
||||||
|
if (!_decoding) {
|
||||||
|
var rgba = PlatformFFI.getRgba();
|
||||||
|
if (rgba != null) {
|
||||||
|
if (_waitForImage) {
|
||||||
|
_waitForImage = false;
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
}
|
||||||
|
_decoding = true;
|
||||||
|
final pid = FFI.id;
|
||||||
|
ui.decodeImageFromPixels(rgba, _display.width, _display.height,
|
||||||
|
isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) {
|
||||||
|
PlatformFFI.clearRgbaFrame();
|
||||||
|
_decoding = false;
|
||||||
|
if (FFI.id != pid) return;
|
||||||
|
try {
|
||||||
|
// my throw exception, because the listener maybe already dispose
|
||||||
|
FFI.imageModel.update(image);
|
||||||
|
} catch (e) {
|
||||||
|
print('update image: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleSwitchDisplay(Map<String, dynamic> evt) {
|
||||||
|
var old = _pi.currentDisplay;
|
||||||
|
_pi.currentDisplay = int.parse(evt['display']);
|
||||||
|
_display.x = double.parse(evt['x']);
|
||||||
|
_display.y = double.parse(evt['y']);
|
||||||
|
_display.width = int.parse(evt['width']);
|
||||||
|
_display.height = int.parse(evt['height']);
|
||||||
|
if (old != _pi.currentDisplay)
|
||||||
|
FFI.cursorModel.updateDisplayOrigin(_display.x, _display.y);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleMsgBox(Map<String, dynamic> evt, String id) {
|
||||||
|
var type = evt['type'];
|
||||||
|
var title = evt['title'];
|
||||||
|
var text = evt['text'];
|
||||||
|
if (type == 're-input-password') {
|
||||||
|
wrongPasswordDialog(id);
|
||||||
|
} else if (type == 'input-password') {
|
||||||
|
enterPasswordDialog(id);
|
||||||
|
} else {
|
||||||
|
var hasRetry = evt['hasRetry'] == 'true';
|
||||||
|
print(evt);
|
||||||
|
showMsgBox(type, title, text, hasRetry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showMsgBox(String type, String title, String text, bool hasRetry) {
|
||||||
|
msgBox(type, title, text);
|
||||||
|
if (hasRetry) {
|
||||||
|
_timer?.cancel();
|
||||||
|
_timer = Timer(Duration(seconds: _reconnects), () {
|
||||||
|
FFI.reconnect();
|
||||||
|
showLoading(translate('Connecting...'));
|
||||||
|
});
|
||||||
|
_reconnects *= 2;
|
||||||
|
} else {
|
||||||
|
_reconnects = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handlePeerInfo(Map<String, dynamic> evt) {
|
||||||
|
SmartDialog.dismiss();
|
||||||
|
_pi.version = evt['version'];
|
||||||
|
_pi.username = evt['username'];
|
||||||
|
_pi.hostname = evt['hostname'];
|
||||||
|
_pi.platform = evt['platform'];
|
||||||
|
_pi.sasEnabled = evt['sas_enabled'] == "true";
|
||||||
|
_pi.currentDisplay = int.parse(evt['current_display']);
|
||||||
|
|
||||||
|
if (isPeerAndroid) {
|
||||||
|
_touchMode = true;
|
||||||
|
if (FFI.ffiModel.permissions['keyboard'] != false) {
|
||||||
|
showMobileActionsOverlay();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_touchMode = FFI.getByName('peer_option', "touch-mode") != '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt['is_file_transfer'] == "true") {
|
||||||
|
FFI.fileModel.onReady();
|
||||||
|
} else {
|
||||||
|
_pi.displays = [];
|
||||||
|
List<dynamic> displays = json.decode(evt['displays']);
|
||||||
|
for (int i = 0; i < displays.length; ++i) {
|
||||||
|
Map<String, dynamic> d0 = displays[i];
|
||||||
|
var d = Display();
|
||||||
|
d.x = d0['x'].toDouble();
|
||||||
|
d.y = d0['y'].toDouble();
|
||||||
|
d.width = d0['width'];
|
||||||
|
d.height = d0['height'];
|
||||||
|
_pi.displays.add(d);
|
||||||
|
}
|
||||||
|
if (_pi.currentDisplay < _pi.displays.length) {
|
||||||
|
_display = _pi.displays[_pi.currentDisplay];
|
||||||
|
}
|
||||||
|
if (displays.length > 0) {
|
||||||
|
showLoading(translate('Connected, waiting for image...'));
|
||||||
|
_waitForImage = true;
|
||||||
|
_reconnects = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageModel with ChangeNotifier {
|
||||||
|
ui.Image? _image;
|
||||||
|
|
||||||
|
ui.Image? get image => _image;
|
||||||
|
|
||||||
|
void update(ui.Image? image) {
|
||||||
|
if (_image == null && image != null) {
|
||||||
|
if (isDesktop) {
|
||||||
|
FFI.canvasModel.updateViewStyle();
|
||||||
|
} else {
|
||||||
|
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||||
|
final xscale = size.width / image.width;
|
||||||
|
final yscale = size.height / image.height;
|
||||||
|
FFI.canvasModel.scale = max(xscale, yscale);
|
||||||
|
}
|
||||||
|
initializeCursorAndCanvas();
|
||||||
|
Future.delayed(Duration(milliseconds: 1), () {
|
||||||
|
if (FFI.ffiModel.isPeerAndroid) {
|
||||||
|
FFI.setByName(
|
||||||
|
'peer_option', '{"name": "view-style", "value": "shrink"}');
|
||||||
|
FFI.canvasModel.updateViewStyle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_image = image;
|
||||||
|
if (image != null) notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
double get maxScale {
|
||||||
|
if (_image == null) return 1.0;
|
||||||
|
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||||
|
final xscale = size.width / _image!.width;
|
||||||
|
final yscale = size.height / _image!.height;
|
||||||
|
return max(1.0, max(xscale, yscale));
|
||||||
|
}
|
||||||
|
|
||||||
|
double get minScale {
|
||||||
|
if (_image == null) return 1.0;
|
||||||
|
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||||
|
final xscale = size.width / _image!.width;
|
||||||
|
final yscale = size.height / _image!.height;
|
||||||
|
return min(xscale, yscale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CanvasModel with ChangeNotifier {
|
||||||
|
double _x = 0;
|
||||||
|
double _y = 0;
|
||||||
|
double _scale = 1.0;
|
||||||
|
|
||||||
|
CanvasModel();
|
||||||
|
|
||||||
|
double get x => _x;
|
||||||
|
|
||||||
|
double get y => _y;
|
||||||
|
|
||||||
|
double get scale => _scale;
|
||||||
|
|
||||||
|
void updateViewStyle() {
|
||||||
|
final s = FFI.getByName('peer_option', 'view-style');
|
||||||
|
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||||
|
final s1 = size.width / FFI.ffiModel.display.width;
|
||||||
|
final s2 = size.height / FFI.ffiModel.display.height;
|
||||||
|
if (s == 'shrink') {
|
||||||
|
final s = s1 < s2 ? s1 : s2;
|
||||||
|
if (s < 1) {
|
||||||
|
_scale = s;
|
||||||
|
}
|
||||||
|
} else if (s == 'stretch') {
|
||||||
|
final s = s1 > s2 ? s1 : s2;
|
||||||
|
if (s > 1) {
|
||||||
|
_scale = s;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_scale = 1;
|
||||||
|
}
|
||||||
|
_x = (size.width - FFI.ffiModel.display.width * _scale) / 2;
|
||||||
|
_y = (size.height - FFI.ffiModel.display.height * _scale) / 2;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(double x, double y, double scale) {
|
||||||
|
_x = x;
|
||||||
|
_y = y;
|
||||||
|
_scale = scale;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveDesktopMouse(double x, double y) {
|
||||||
|
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||||
|
final dw = FFI.ffiModel.display.width * _scale;
|
||||||
|
final dh = FFI.ffiModel.display.height * _scale;
|
||||||
|
var dxOffset = 0;
|
||||||
|
var dyOffset = 0;
|
||||||
|
if (dw > size.width) {
|
||||||
|
dxOffset = (x - dw * (x / size.width) - _x).toInt();
|
||||||
|
}
|
||||||
|
if (dh > size.height) {
|
||||||
|
dyOffset = (y - dh * (y / size.height) - _y).toInt();
|
||||||
|
}
|
||||||
|
_x += dxOffset;
|
||||||
|
_y += dyOffset;
|
||||||
|
if (dxOffset != 0 || dyOffset != 0) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
FFI.cursorModel.moveLocal(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
set scale(v) {
|
||||||
|
_scale = v;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void panX(double dx) {
|
||||||
|
_x += dx;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetOffset() {
|
||||||
|
if (isDesktop) {
|
||||||
|
updateViewStyle();
|
||||||
|
} else {
|
||||||
|
_x = 0;
|
||||||
|
_y = 0;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void panY(double dy) {
|
||||||
|
_y += dy;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateScale(double v) {
|
||||||
|
if (FFI.imageModel.image == null) return;
|
||||||
|
final offset = FFI.cursorModel.offset;
|
||||||
|
var r = FFI.cursorModel.getVisibleRect();
|
||||||
|
final px0 = (offset.dx - r.left) * _scale;
|
||||||
|
final py0 = (offset.dy - r.top) * _scale;
|
||||||
|
_scale *= v;
|
||||||
|
final maxs = FFI.imageModel.maxScale;
|
||||||
|
final mins = FFI.imageModel.minScale;
|
||||||
|
if (_scale > maxs) _scale = maxs;
|
||||||
|
if (_scale < mins) _scale = mins;
|
||||||
|
r = FFI.cursorModel.getVisibleRect();
|
||||||
|
final px1 = (offset.dx - r.left) * _scale;
|
||||||
|
final py1 = (offset.dy - r.top) * _scale;
|
||||||
|
_x -= px1 - px0;
|
||||||
|
_y -= py1 - py0;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear([bool notify = false]) {
|
||||||
|
_x = 0;
|
||||||
|
_y = 0;
|
||||||
|
_scale = 1.0;
|
||||||
|
if (notify) notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CursorModel with ChangeNotifier {
|
||||||
|
ui.Image? _image;
|
||||||
|
final _images = Map<int, Tuple3<ui.Image, double, double>>();
|
||||||
|
double _x = -10000;
|
||||||
|
double _y = -10000;
|
||||||
|
double _hotx = 0;
|
||||||
|
double _hoty = 0;
|
||||||
|
double _displayOriginX = 0;
|
||||||
|
double _displayOriginY = 0;
|
||||||
|
|
||||||
|
ui.Image? get image => _image;
|
||||||
|
|
||||||
|
double get x => _x - _displayOriginX;
|
||||||
|
|
||||||
|
double get y => _y - _displayOriginY;
|
||||||
|
|
||||||
|
Offset get offset => Offset(_x, _y);
|
||||||
|
|
||||||
|
double get hotx => _hotx;
|
||||||
|
|
||||||
|
double get hoty => _hoty;
|
||||||
|
|
||||||
|
// remote physical display coordinate
|
||||||
|
Rect getVisibleRect() {
|
||||||
|
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||||
|
final xoffset = FFI.canvasModel.x;
|
||||||
|
final yoffset = FFI.canvasModel.y;
|
||||||
|
final scale = FFI.canvasModel.scale;
|
||||||
|
final x0 = _displayOriginX - xoffset / scale;
|
||||||
|
final y0 = _displayOriginY - yoffset / scale;
|
||||||
|
return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
double adjustForKeyboard() {
|
||||||
|
final m = MediaQueryData.fromWindow(ui.window);
|
||||||
|
var keyboardHeight = m.viewInsets.bottom;
|
||||||
|
final size = m.size;
|
||||||
|
if (keyboardHeight < 100) return 0;
|
||||||
|
final s = FFI.canvasModel.scale;
|
||||||
|
final thresh = (size.height - keyboardHeight) / 2;
|
||||||
|
var h = (_y - getVisibleRect().top) * s; // local physical display height
|
||||||
|
return h - thresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
void touch(double x, double y, MouseButtons button) {
|
||||||
|
moveLocal(x, y);
|
||||||
|
FFI.moveMouse(_x, _y);
|
||||||
|
FFI.tap(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
void move(double x, double y) {
|
||||||
|
moveLocal(x, y);
|
||||||
|
FFI.moveMouse(_x, _y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveLocal(double x, double y) {
|
||||||
|
final scale = FFI.canvasModel.scale;
|
||||||
|
final xoffset = FFI.canvasModel.x;
|
||||||
|
final yoffset = FFI.canvasModel.y;
|
||||||
|
_x = (x - xoffset) / scale + _displayOriginX;
|
||||||
|
_y = (y - yoffset) / scale + _displayOriginY;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
_x = _displayOriginX;
|
||||||
|
_y = _displayOriginY;
|
||||||
|
FFI.moveMouse(_x, _y);
|
||||||
|
FFI.canvasModel.clear(true);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePan(double dx, double dy, bool touchMode) {
|
||||||
|
if (FFI.imageModel.image == null) return;
|
||||||
|
if (touchMode) {
|
||||||
|
final scale = FFI.canvasModel.scale;
|
||||||
|
_x += dx / scale;
|
||||||
|
_y += dy / scale;
|
||||||
|
FFI.moveMouse(_x, _y);
|
||||||
|
notifyListeners();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final scale = FFI.canvasModel.scale;
|
||||||
|
dx /= scale;
|
||||||
|
dy /= scale;
|
||||||
|
final r = getVisibleRect();
|
||||||
|
var cx = r.center.dx;
|
||||||
|
var cy = r.center.dy;
|
||||||
|
var tryMoveCanvasX = false;
|
||||||
|
if (dx > 0) {
|
||||||
|
final maxCanvasCanMove = _displayOriginX +
|
||||||
|
FFI.imageModel.image!.width -
|
||||||
|
r.right.roundToDouble();
|
||||||
|
tryMoveCanvasX = _x + dx > cx && maxCanvasCanMove > 0;
|
||||||
|
if (tryMoveCanvasX) {
|
||||||
|
dx = min(dx, maxCanvasCanMove);
|
||||||
|
} else {
|
||||||
|
final maxCursorCanMove = r.right - _x;
|
||||||
|
dx = min(dx, maxCursorCanMove);
|
||||||
|
}
|
||||||
|
} else if (dx < 0) {
|
||||||
|
final maxCanvasCanMove = _displayOriginX - r.left.roundToDouble();
|
||||||
|
tryMoveCanvasX = _x + dx < cx && maxCanvasCanMove < 0;
|
||||||
|
if (tryMoveCanvasX) {
|
||||||
|
dx = max(dx, maxCanvasCanMove);
|
||||||
|
} else {
|
||||||
|
final maxCursorCanMove = r.left - _x;
|
||||||
|
dx = max(dx, maxCursorCanMove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var tryMoveCanvasY = false;
|
||||||
|
if (dy > 0) {
|
||||||
|
final mayCanvasCanMove = _displayOriginY +
|
||||||
|
FFI.imageModel.image!.height -
|
||||||
|
r.bottom.roundToDouble();
|
||||||
|
tryMoveCanvasY = _y + dy > cy && mayCanvasCanMove > 0;
|
||||||
|
if (tryMoveCanvasY) {
|
||||||
|
dy = min(dy, mayCanvasCanMove);
|
||||||
|
} else {
|
||||||
|
final mayCursorCanMove = r.bottom - _y;
|
||||||
|
dy = min(dy, mayCursorCanMove);
|
||||||
|
}
|
||||||
|
} else if (dy < 0) {
|
||||||
|
final mayCanvasCanMove = _displayOriginY - r.top.roundToDouble();
|
||||||
|
tryMoveCanvasY = _y + dy < cy && mayCanvasCanMove < 0;
|
||||||
|
if (tryMoveCanvasY) {
|
||||||
|
dy = max(dy, mayCanvasCanMove);
|
||||||
|
} else {
|
||||||
|
final mayCursorCanMove = r.top - _y;
|
||||||
|
dy = max(dy, mayCursorCanMove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dx == 0 && dy == 0) return;
|
||||||
|
_x += dx;
|
||||||
|
_y += dy;
|
||||||
|
if (tryMoveCanvasX && dx != 0) {
|
||||||
|
FFI.canvasModel.panX(-dx);
|
||||||
|
}
|
||||||
|
if (tryMoveCanvasY && dy != 0) {
|
||||||
|
FFI.canvasModel.panY(-dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
FFI.moveMouse(_x, _y);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCursorData(Map<String, dynamic> evt) {
|
||||||
|
var id = int.parse(evt['id']);
|
||||||
|
_hotx = double.parse(evt['hotx']);
|
||||||
|
_hoty = double.parse(evt['hoty']);
|
||||||
|
var width = int.parse(evt['width']);
|
||||||
|
var height = int.parse(evt['height']);
|
||||||
|
List<dynamic> colors = json.decode(evt['colors']);
|
||||||
|
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
|
||||||
|
var pid = FFI.id;
|
||||||
|
ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888,
|
||||||
|
(image) {
|
||||||
|
if (FFI.id != pid) return;
|
||||||
|
_image = image;
|
||||||
|
_images[id] = Tuple3(image, _hotx, _hoty);
|
||||||
|
try {
|
||||||
|
// my throw exception, because the listener maybe already dispose
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
print('notify cursor: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCursorId(Map<String, dynamic> evt) {
|
||||||
|
final tmp = _images[int.parse(evt['id'])];
|
||||||
|
if (tmp != null) {
|
||||||
|
_image = tmp.item1;
|
||||||
|
_hotx = tmp.item2;
|
||||||
|
_hoty = tmp.item3;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCursorPosition(Map<String, dynamic> evt) {
|
||||||
|
_x = double.parse(evt['x']);
|
||||||
|
_y = double.parse(evt['y']);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateDisplayOrigin(double x, double y) {
|
||||||
|
_displayOriginX = x;
|
||||||
|
_displayOriginY = y;
|
||||||
|
_x = x + 1;
|
||||||
|
_y = y + 1;
|
||||||
|
FFI.moveMouse(x, y);
|
||||||
|
FFI.canvasModel.resetOffset();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateDisplayOriginWithCursor(
|
||||||
|
double x, double y, double xCursor, double yCursor) {
|
||||||
|
_displayOriginX = x;
|
||||||
|
_displayOriginY = y;
|
||||||
|
_x = xCursor;
|
||||||
|
_y = yCursor;
|
||||||
|
FFI.moveMouse(x, y);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
_x = -10000;
|
||||||
|
_x = -10000;
|
||||||
|
_image = null;
|
||||||
|
_images.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MouseButtons { left, right, wheel }
|
||||||
|
|
||||||
|
extension ToString on MouseButtons {
|
||||||
|
String get value {
|
||||||
|
switch (this) {
|
||||||
|
case MouseButtons.left:
|
||||||
|
return "left";
|
||||||
|
case MouseButtons.right:
|
||||||
|
return "right";
|
||||||
|
case MouseButtons.wheel:
|
||||||
|
return "wheel";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FFI {
|
||||||
|
static var id = "";
|
||||||
|
static var shift = false;
|
||||||
|
static var ctrl = false;
|
||||||
|
static var alt = false;
|
||||||
|
static var command = false;
|
||||||
|
static var version = "";
|
||||||
|
static final imageModel = ImageModel();
|
||||||
|
static final ffiModel = FfiModel();
|
||||||
|
static final cursorModel = CursorModel();
|
||||||
|
static final canvasModel = CanvasModel();
|
||||||
|
static final serverModel = ServerModel();
|
||||||
|
static final chatModel = ChatModel();
|
||||||
|
static final fileModel = FileModel();
|
||||||
|
|
||||||
|
static String getId() {
|
||||||
|
return getByName('remote_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tap(MouseButtons button) {
|
||||||
|
sendMouse('down', button);
|
||||||
|
sendMouse('up', button);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void scroll(double y) {
|
||||||
|
var y2 = y.round();
|
||||||
|
if (y2 == 0) return;
|
||||||
|
setByName('send_mouse',
|
||||||
|
json.encode(modify({'type': 'wheel', 'y': y2.toString()})));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reconnect() {
|
||||||
|
setByName('reconnect');
|
||||||
|
FFI.ffiModel.clearPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void resetModifiers() {
|
||||||
|
shift = ctrl = alt = command = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, String> modify(Map<String, String> evt) {
|
||||||
|
if (ctrl) evt['ctrl'] = 'true';
|
||||||
|
if (shift) evt['shift'] = 'true';
|
||||||
|
if (alt) evt['alt'] = 'true';
|
||||||
|
if (command) evt['command'] = 'true';
|
||||||
|
return evt;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sendMouse(String type, MouseButtons button) {
|
||||||
|
if (!ffiModel.keyboard()) return;
|
||||||
|
setByName('send_mouse',
|
||||||
|
json.encode(modify({'type': type, 'buttons': button.value})));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void inputKey(String name, {bool? down, bool? press}) {
|
||||||
|
if (!ffiModel.keyboard()) return;
|
||||||
|
setByName(
|
||||||
|
'input_key',
|
||||||
|
json.encode(modify({
|
||||||
|
'name': name,
|
||||||
|
'down': (down ?? false).toString(),
|
||||||
|
'press': (press ?? true).toString()
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void moveMouse(double x, double y) {
|
||||||
|
if (!ffiModel.keyboard()) return;
|
||||||
|
var x2 = x.toInt();
|
||||||
|
var y2 = y.toInt();
|
||||||
|
setByName('send_mouse', json.encode(modify({'x': '$x2', 'y': '$y2'})));
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Peer> peers() {
|
||||||
|
try {
|
||||||
|
var str = getByName('peers');
|
||||||
|
if (str == "") return [];
|
||||||
|
List<dynamic> peers = json.decode(str);
|
||||||
|
return peers
|
||||||
|
.map((s) => s as List<dynamic>)
|
||||||
|
.map((s) =>
|
||||||
|
Peer.fromJson(s[0] as String, s[1] as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
print('peers(): $e');
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void connect(String id, {bool isFileTransfer = false}) {
|
||||||
|
if (isFileTransfer) {
|
||||||
|
setByName('connect_file_transfer', id);
|
||||||
|
} else {
|
||||||
|
setByName('connect', id);
|
||||||
|
}
|
||||||
|
FFI.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, dynamic>? popEvent() {
|
||||||
|
var s = getByName('event');
|
||||||
|
if (s == '') return null;
|
||||||
|
try {
|
||||||
|
Map<String, dynamic> event = json.decode(s);
|
||||||
|
return event;
|
||||||
|
} catch (e) {
|
||||||
|
print('popEvent(): $e');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void login(String password, bool remember) {
|
||||||
|
setByName(
|
||||||
|
'login',
|
||||||
|
json.encode({
|
||||||
|
'password': password,
|
||||||
|
'remember': remember ? 'true' : 'false',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void close() {
|
||||||
|
chatModel.close();
|
||||||
|
if (FFI.imageModel.image != null && !isDesktop) {
|
||||||
|
savePreference(id, cursorModel.x, cursorModel.y, canvasModel.x,
|
||||||
|
canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay);
|
||||||
|
}
|
||||||
|
id = "";
|
||||||
|
setByName('close', '');
|
||||||
|
imageModel.update(null);
|
||||||
|
cursorModel.clear();
|
||||||
|
ffiModel.clear();
|
||||||
|
canvasModel.clear();
|
||||||
|
resetModifiers();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getByName(String name, [String arg = '']) {
|
||||||
|
return PlatformFFI.getByName(name, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setByName(String name, [String value = '']) {
|
||||||
|
PlatformFFI.setByName(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static handleMouse(Map<String, dynamic> evt) {
|
||||||
|
var type = '';
|
||||||
|
var isMove = false;
|
||||||
|
switch (evt['type']) {
|
||||||
|
case 'mousedown':
|
||||||
|
type = 'down';
|
||||||
|
break;
|
||||||
|
case 'mouseup':
|
||||||
|
type = 'up';
|
||||||
|
break;
|
||||||
|
case 'mousemove':
|
||||||
|
isMove = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
evt['type'] = type;
|
||||||
|
var x = evt['x'];
|
||||||
|
var y = evt['y'];
|
||||||
|
if (isMove) {
|
||||||
|
FFI.canvasModel.moveDesktopMouse(x, y);
|
||||||
|
}
|
||||||
|
final d = FFI.ffiModel.display;
|
||||||
|
x -= FFI.canvasModel.x;
|
||||||
|
y -= FFI.canvasModel.y;
|
||||||
|
if (!isMove && (x < 0 || x > d.width || y < 0 || y > d.height)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
x /= FFI.canvasModel.scale;
|
||||||
|
y /= FFI.canvasModel.scale;
|
||||||
|
x += d.x;
|
||||||
|
y += d.y;
|
||||||
|
if (type != '') {
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
evt['x'] = '${x.round()}';
|
||||||
|
evt['y'] = '${y.round()}';
|
||||||
|
var buttons = '';
|
||||||
|
switch (evt['buttons']) {
|
||||||
|
case 1:
|
||||||
|
buttons = 'left';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
buttons = 'right';
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
buttons = 'wheel';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
evt['buttons'] = buttons;
|
||||||
|
setByName('send_mouse', json.encode(evt));
|
||||||
|
}
|
||||||
|
|
||||||
|
static listenToMouse(bool yesOrNo) {
|
||||||
|
if (yesOrNo) {
|
||||||
|
PlatformFFI.startDesktopWebListener();
|
||||||
|
} else {
|
||||||
|
PlatformFFI.stopDesktopWebListener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setMethodCallHandler(FMethod callback) {
|
||||||
|
PlatformFFI.setMethodCallHandler(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<bool> invokeMethod(String method, [dynamic arguments]) async {
|
||||||
|
return await PlatformFFI.invokeMethod(method, arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Peer {
|
||||||
|
final String id;
|
||||||
|
final String username;
|
||||||
|
final String hostname;
|
||||||
|
final String platform;
|
||||||
|
|
||||||
|
Peer.fromJson(String id, Map<String, dynamic> json)
|
||||||
|
: id = id,
|
||||||
|
username = json['username'],
|
||||||
|
hostname = json['hostname'],
|
||||||
|
platform = json['platform'];
|
||||||
|
}
|
||||||
|
|
||||||
|
class Display {
|
||||||
|
double x = 0;
|
||||||
|
double y = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PeerInfo {
|
||||||
|
String version = "";
|
||||||
|
String username = "";
|
||||||
|
String hostname = "";
|
||||||
|
String platform = "";
|
||||||
|
bool sasEnabled = false;
|
||||||
|
int currentDisplay = 0;
|
||||||
|
List<Display> displays = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
void savePreference(String id, double xCursor, double yCursor, double xCanvas,
|
||||||
|
double yCanvas, double scale, int currentDisplay) async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
final p = Map<String, dynamic>();
|
||||||
|
p['xCursor'] = xCursor;
|
||||||
|
p['yCursor'] = yCursor;
|
||||||
|
p['xCanvas'] = xCanvas;
|
||||||
|
p['yCanvas'] = yCanvas;
|
||||||
|
p['scale'] = scale;
|
||||||
|
p['currentDisplay'] = currentDisplay;
|
||||||
|
prefs.setString('peer' + id, json.encode(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>?> getPreference(String id) async {
|
||||||
|
if (!isDesktop) return null;
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
var p = prefs.getString('peer' + id);
|
||||||
|
if (p == null) return null;
|
||||||
|
Map<String, dynamic> m = json.decode(p);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
void removePreference(String id) async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.remove('peer' + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void initializeCursorAndCanvas() async {
|
||||||
|
var p = await getPreference(FFI.id);
|
||||||
|
int currentDisplay = 0;
|
||||||
|
if (p != null) {
|
||||||
|
currentDisplay = p['currentDisplay'];
|
||||||
|
}
|
||||||
|
if (p == null || currentDisplay != FFI.ffiModel.pi.currentDisplay) {
|
||||||
|
FFI.cursorModel
|
||||||
|
.updateDisplayOrigin(FFI.ffiModel.display.x, FFI.ffiModel.display.y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double xCursor = p['xCursor'];
|
||||||
|
double yCursor = p['yCursor'];
|
||||||
|
double xCanvas = p['xCanvas'];
|
||||||
|
double yCanvas = p['yCanvas'];
|
||||||
|
double scale = p['scale'];
|
||||||
|
FFI.cursorModel.updateDisplayOriginWithCursor(
|
||||||
|
FFI.ffiModel.display.x, FFI.ffiModel.display.y, xCursor, yCursor);
|
||||||
|
FFI.canvasModel.update(xCanvas, yCanvas, scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
String translate(String name) {
|
||||||
|
if (name.startsWith('Failed to') && name.contains(': ')) {
|
||||||
|
return name.split(': ').map((x) => translate(x)).join(': ');
|
||||||
|
}
|
||||||
|
var a = 'translate';
|
||||||
|
var b = '{"locale": "$localeName", "text": "$name"}';
|
||||||
|
return FFI.getByName(a, b);
|
||||||
|
}
|
||||||
136
flutter/lib/models/native_model.dart
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:device_info/device_info.dart';
|
||||||
|
import 'package:package_info/package_info.dart';
|
||||||
|
import 'package:external_path/external_path.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import '../common.dart';
|
||||||
|
|
||||||
|
class RgbaFrame extends Struct {
|
||||||
|
@Uint32()
|
||||||
|
external int len;
|
||||||
|
external Pointer<Uint8> data;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef F2 = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
|
||||||
|
typedef F3 = void Function(Pointer<Utf8>, Pointer<Utf8>);
|
||||||
|
typedef F4 = void Function(Pointer<RgbaFrame>);
|
||||||
|
typedef F5 = Pointer<RgbaFrame> Function();
|
||||||
|
|
||||||
|
class PlatformFFI {
|
||||||
|
static Pointer<RgbaFrame>? _lastRgbaFrame;
|
||||||
|
static String _dir = '';
|
||||||
|
static String _homeDir = '';
|
||||||
|
static F2? _getByName;
|
||||||
|
static F3? _setByName;
|
||||||
|
static F4? _freeRgba;
|
||||||
|
static F5? _getRgba;
|
||||||
|
|
||||||
|
static void clearRgbaFrame() {
|
||||||
|
if (_lastRgbaFrame != null &&
|
||||||
|
_lastRgbaFrame != nullptr &&
|
||||||
|
_freeRgba != null) _freeRgba!(_lastRgbaFrame!);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Uint8List? getRgba() {
|
||||||
|
if (_getRgba == null) return null;
|
||||||
|
_lastRgbaFrame = _getRgba!();
|
||||||
|
if (_lastRgbaFrame == null || _lastRgbaFrame == nullptr) return null;
|
||||||
|
final ref = _lastRgbaFrame!.ref;
|
||||||
|
return Uint8List.sublistView(ref.data.asTypedList(ref.len));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String> getVersion() async {
|
||||||
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
|
return packageInfo.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getByName(String name, [String arg = '']) {
|
||||||
|
if (_getByName == null) return '';
|
||||||
|
var a = name.toNativeUtf8();
|
||||||
|
var b = arg.toNativeUtf8();
|
||||||
|
var p = _getByName!(a, b);
|
||||||
|
assert(p != nullptr);
|
||||||
|
var res = p.toDartString();
|
||||||
|
calloc.free(p);
|
||||||
|
calloc.free(a);
|
||||||
|
calloc.free(b);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setByName(String name, [String value = '']) {
|
||||||
|
if (_setByName == null) return;
|
||||||
|
var a = name.toNativeUtf8();
|
||||||
|
var b = value.toNativeUtf8();
|
||||||
|
_setByName!(a, b);
|
||||||
|
calloc.free(a);
|
||||||
|
calloc.free(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Null> init() async {
|
||||||
|
isIOS = Platform.isIOS;
|
||||||
|
isAndroid = Platform.isAndroid;
|
||||||
|
final dylib = Platform.isAndroid
|
||||||
|
? DynamicLibrary.open('librustdesk.so')
|
||||||
|
: DynamicLibrary.process();
|
||||||
|
print('initializing FFI');
|
||||||
|
try {
|
||||||
|
_getByName = dylib.lookupFunction<F2, F2>('get_by_name');
|
||||||
|
_setByName =
|
||||||
|
dylib.lookupFunction<Void Function(Pointer<Utf8>, Pointer<Utf8>), F3>(
|
||||||
|
'set_by_name');
|
||||||
|
_freeRgba = dylib
|
||||||
|
.lookupFunction<Void Function(Pointer<RgbaFrame>), F4>('free_rgba');
|
||||||
|
_getRgba = dylib.lookupFunction<F5, F5>('get_rgba');
|
||||||
|
_dir = (await getApplicationDocumentsDirectory()).path;
|
||||||
|
try {
|
||||||
|
_homeDir = (await ExternalPath.getExternalStorageDirectories())[0];
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
String id = 'NA';
|
||||||
|
String name = 'Flutter';
|
||||||
|
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||||
|
name = '${androidInfo.brand}-${androidInfo.model}';
|
||||||
|
id = androidInfo.id.hashCode.toString();
|
||||||
|
androidVersion = androidInfo.version.sdkInt;
|
||||||
|
} else {
|
||||||
|
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
||||||
|
name = iosInfo.utsname.machine;
|
||||||
|
id = iosInfo.identifierForVendor.hashCode.toString();
|
||||||
|
}
|
||||||
|
print("info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir");
|
||||||
|
setByName('info1', id);
|
||||||
|
setByName('info2', name);
|
||||||
|
setByName('home_dir', _homeDir);
|
||||||
|
setByName('init', _dir);
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
|
version = await getVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void startDesktopWebListener() {}
|
||||||
|
|
||||||
|
static void stopDesktopWebListener() {}
|
||||||
|
|
||||||
|
static void setMethodCallHandler(FMethod callback) {
|
||||||
|
toAndroidChannel.setMethodCallHandler((call) async {
|
||||||
|
callback(call.method, call.arguments);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static invokeMethod(String method, [dynamic arguments]) async {
|
||||||
|
if (!isAndroid) return Future<bool>(() => false);
|
||||||
|
return await toAndroidChannel.invokeMethod(method, arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final localeName = Platform.localeName;
|
||||||
|
final toAndroidChannel = MethodChannel("mChannel");
|
||||||
510
flutter/lib/models/server_model.dart
Normal file
@ -0,0 +1,510 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:dash_chat/dash_chat.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:wakelock/wakelock.dart';
|
||||||
|
import '../common.dart';
|
||||||
|
import '../pages/server_page.dart';
|
||||||
|
import 'model.dart';
|
||||||
|
|
||||||
|
const loginDialogTag = "LOGIN";
|
||||||
|
final _emptyIdShow = translate("Generating ...");
|
||||||
|
|
||||||
|
class ServerModel with ChangeNotifier {
|
||||||
|
Timer? _interval;
|
||||||
|
bool _isStart = false; // Android MainService status
|
||||||
|
bool _mediaOk = false;
|
||||||
|
bool _inputOk = false;
|
||||||
|
bool _audioOk = false;
|
||||||
|
bool _fileOk = false;
|
||||||
|
int _connectStatus = 0; // Rendezvous Server status
|
||||||
|
|
||||||
|
final _serverId = TextEditingController(text: _emptyIdShow);
|
||||||
|
final _serverPasswd = TextEditingController(text: "");
|
||||||
|
|
||||||
|
Map<int, Client> _clients = {};
|
||||||
|
|
||||||
|
bool get isStart => _isStart;
|
||||||
|
|
||||||
|
bool get mediaOk => _mediaOk;
|
||||||
|
|
||||||
|
bool get inputOk => _inputOk;
|
||||||
|
|
||||||
|
bool get audioOk => _audioOk;
|
||||||
|
|
||||||
|
bool get fileOk => _fileOk;
|
||||||
|
|
||||||
|
int get connectStatus => _connectStatus;
|
||||||
|
|
||||||
|
TextEditingController get serverId => _serverId;
|
||||||
|
|
||||||
|
TextEditingController get serverPasswd => _serverPasswd;
|
||||||
|
|
||||||
|
Map<int, Client> get clients => _clients;
|
||||||
|
|
||||||
|
final controller = ScrollController();
|
||||||
|
|
||||||
|
ServerModel() {
|
||||||
|
() async {
|
||||||
|
/**
|
||||||
|
* 1. check android permission
|
||||||
|
* 2. check config
|
||||||
|
* audio true by default (if permission on) (false default < Android 10)
|
||||||
|
* file true by default (if permission on)
|
||||||
|
* input false by default (it need turning on manually everytime)
|
||||||
|
*/
|
||||||
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
|
||||||
|
// audio
|
||||||
|
if (androidVersion < 30 || !await PermissionManager.check("audio")) {
|
||||||
|
_audioOk = false;
|
||||||
|
FFI.setByName(
|
||||||
|
'option',
|
||||||
|
jsonEncode(Map()
|
||||||
|
..["name"] = "enable-audio"
|
||||||
|
..["value"] = "N"));
|
||||||
|
} else {
|
||||||
|
final audioOption = FFI.getByName('option', 'enable-audio');
|
||||||
|
_audioOk = audioOption.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// file
|
||||||
|
if (!await PermissionManager.check("file")) {
|
||||||
|
_fileOk = false;
|
||||||
|
FFI.setByName(
|
||||||
|
'option',
|
||||||
|
jsonEncode(Map()
|
||||||
|
..["name"] = "enable-file-transfer"
|
||||||
|
..["value"] = "N"));
|
||||||
|
} else {
|
||||||
|
final fileOption = FFI.getByName('option', 'enable-file-transfer');
|
||||||
|
_fileOk = fileOption.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// input (mouse control)
|
||||||
|
Map<String, String> res = Map()
|
||||||
|
..["name"] = "enable-keyboard"
|
||||||
|
..["value"] = 'N';
|
||||||
|
FFI.setByName('option', jsonEncode(res)); // input false by default
|
||||||
|
notifyListeners();
|
||||||
|
}();
|
||||||
|
|
||||||
|
Timer.periodic(Duration(seconds: 1), (timer) {
|
||||||
|
var status = int.tryParse(FFI.getByName('connect_statue')) ?? 0;
|
||||||
|
if (status > 0) {
|
||||||
|
status = 1;
|
||||||
|
}
|
||||||
|
if (status != _connectStatus) {
|
||||||
|
_connectStatus = status;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
final res =
|
||||||
|
FFI.getByName('check_clients_length', _clients.length.toString());
|
||||||
|
if (res.isNotEmpty) {
|
||||||
|
debugPrint("clients not match!");
|
||||||
|
updateClientState(res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAudio() async {
|
||||||
|
if (!_audioOk && !await PermissionManager.check("audio")) {
|
||||||
|
final res = await PermissionManager.request("audio");
|
||||||
|
if (!res) {
|
||||||
|
// TODO handle fail
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_audioOk = !_audioOk;
|
||||||
|
Map<String, String> res = Map()
|
||||||
|
..["name"] = "enable-audio"
|
||||||
|
..["value"] = _audioOk ? '' : 'N';
|
||||||
|
FFI.setByName('option', jsonEncode(res));
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFile() async {
|
||||||
|
if (!_fileOk && !await PermissionManager.check("file")) {
|
||||||
|
final res = await PermissionManager.request("file");
|
||||||
|
if (!res) {
|
||||||
|
// TODO handle fail
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileOk = !_fileOk;
|
||||||
|
Map<String, String> res = Map()
|
||||||
|
..["name"] = "enable-file-transfer"
|
||||||
|
..["value"] = _fileOk ? '' : 'N';
|
||||||
|
FFI.setByName('option', jsonEncode(res));
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleInput() {
|
||||||
|
if (_inputOk) {
|
||||||
|
FFI.invokeMethod("stop_input");
|
||||||
|
} else {
|
||||||
|
showInputWarnAlert();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleService() async {
|
||||||
|
if (_isStart) {
|
||||||
|
final res =
|
||||||
|
await DialogManager.show<bool>((setState, close) => CustomAlertDialog(
|
||||||
|
title: Row(children: [
|
||||||
|
Icon(Icons.warning_amber_sharp,
|
||||||
|
color: Colors.redAccent, size: 28),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Text(translate("Warning")),
|
||||||
|
]),
|
||||||
|
content: Text(translate("android_stop_service_tip")),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(),
|
||||||
|
child: Text(translate("Cancel"))),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => close(true),
|
||||||
|
child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
if (res == true) {
|
||||||
|
stopService();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final res =
|
||||||
|
await DialogManager.show<bool>((setState, close) => CustomAlertDialog(
|
||||||
|
title: Row(children: [
|
||||||
|
Icon(Icons.warning_amber_sharp,
|
||||||
|
color: Colors.redAccent, size: 28),
|
||||||
|
SizedBox(width: 10),
|
||||||
|
Text(translate("Warning")),
|
||||||
|
]),
|
||||||
|
content: Text(translate("android_service_will_start_tip")),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => close(),
|
||||||
|
child: Text(translate("Cancel"))),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => close(true),
|
||||||
|
child: Text(translate("OK"))),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
if (res == true) {
|
||||||
|
startService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Null> startService() async {
|
||||||
|
_isStart = true;
|
||||||
|
notifyListeners();
|
||||||
|
FFI.setByName("ensure_init_event_queue");
|
||||||
|
_interval?.cancel();
|
||||||
|
_interval = Timer.periodic(Duration(milliseconds: 30), (timer) {
|
||||||
|
FFI.ffiModel.update("");
|
||||||
|
});
|
||||||
|
await FFI.invokeMethod("init_service");
|
||||||
|
FFI.setByName("start_service");
|
||||||
|
getIDPasswd();
|
||||||
|
updateClientState();
|
||||||
|
Wakelock.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Null> stopService() async {
|
||||||
|
_isStart = false;
|
||||||
|
_interval?.cancel();
|
||||||
|
_interval = null;
|
||||||
|
FFI.serverModel.closeAll();
|
||||||
|
await FFI.invokeMethod("stop_service");
|
||||||
|
FFI.setByName("stop_service");
|
||||||
|
notifyListeners();
|
||||||
|
Wakelock.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Null> initInput() async {
|
||||||
|
await FFI.invokeMethod("init_input");
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> updatePassword(String pw) async {
|
||||||
|
final oldPasswd = _serverPasswd.text;
|
||||||
|
FFI.setByName("update_password", pw);
|
||||||
|
await Future.delayed(Duration(milliseconds: 500));
|
||||||
|
await getIDPasswd(force: true);
|
||||||
|
|
||||||
|
// check result
|
||||||
|
if (pw == "") {
|
||||||
|
if (_serverPasswd.text.isNotEmpty && _serverPasswd.text != oldPasswd) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_serverPasswd.text == pw) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getIDPasswd({bool force = false}) async {
|
||||||
|
if (!force && _serverId.text != _emptyIdShow && _serverPasswd.text != "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var count = 0;
|
||||||
|
const maxCount = 10;
|
||||||
|
while (count < maxCount) {
|
||||||
|
await Future.delayed(Duration(seconds: 1));
|
||||||
|
final id = FFI.getByName("server_id");
|
||||||
|
final passwd = FFI.getByName("server_password");
|
||||||
|
if (id.isEmpty) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
_serverId.text = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwd.isEmpty) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
_serverPasswd.text = passwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
"fetch id & passwd again at $count:id:${_serverId.text},passwd:${_serverPasswd.text}");
|
||||||
|
count++;
|
||||||
|
if (_serverId.text != _emptyIdShow && _serverPasswd.text.isNotEmpty) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
changeStatue(String name, bool value) {
|
||||||
|
debugPrint("changeStatue value $value");
|
||||||
|
switch (name) {
|
||||||
|
case "media":
|
||||||
|
_mediaOk = value;
|
||||||
|
if (value && !_isStart) {
|
||||||
|
startService();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "input":
|
||||||
|
if (_inputOk != value) {
|
||||||
|
Map<String, String> res = Map()
|
||||||
|
..["name"] = "enable-keyboard"
|
||||||
|
..["value"] = value ? '' : 'N';
|
||||||
|
FFI.setByName('option', jsonEncode(res));
|
||||||
|
}
|
||||||
|
_inputOk = value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClientState([String? json]) {
|
||||||
|
var res = json ?? FFI.getByName("clients_state");
|
||||||
|
try {
|
||||||
|
final List clientsJson = jsonDecode(res);
|
||||||
|
for (var clientJson in clientsJson) {
|
||||||
|
final client = Client.fromJson(clientJson);
|
||||||
|
_clients[client.id] = client;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Failed to updateClientState:$e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loginRequest(Map<String, dynamic> evt) {
|
||||||
|
try {
|
||||||
|
final client = Client.fromJson(jsonDecode(evt["client"]));
|
||||||
|
if (_clients.containsKey(client.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_clients[client.id] = client;
|
||||||
|
scrollToBottom();
|
||||||
|
notifyListeners();
|
||||||
|
showLoginDialog(client);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Failed to call loginRequest,error:$e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showLoginDialog(Client client) {
|
||||||
|
DialogManager.show(
|
||||||
|
(setState, close) => CustomAlertDialog(
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(translate(client.isFileTransfer
|
||||||
|
? "File Connection"
|
||||||
|
: "Screen Connection")),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.close))
|
||||||
|
]),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(translate("Do you accept?")),
|
||||||
|
clientInfo(client),
|
||||||
|
Text(
|
||||||
|
translate("android_new_connection_tip"),
|
||||||
|
style: TextStyle(color: Colors.black54),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: Text(translate("Dismiss")),
|
||||||
|
onPressed: () {
|
||||||
|
sendLoginResponse(client, false);
|
||||||
|
close();
|
||||||
|
}),
|
||||||
|
ElevatedButton(
|
||||||
|
child: Text(translate("Accept")),
|
||||||
|
onPressed: () {
|
||||||
|
sendLoginResponse(client, true);
|
||||||
|
close();
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
tag: getLoginDialogTag(client.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom() {
|
||||||
|
Future.delayed(Duration(milliseconds: 200), () {
|
||||||
|
controller.animateTo(controller.position.maxScrollExtent,
|
||||||
|
duration: Duration(milliseconds: 200),
|
||||||
|
curve: Curves.fastLinearToSlowEaseIn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendLoginResponse(Client client, bool res) {
|
||||||
|
final Map<String, dynamic> response = Map();
|
||||||
|
response["id"] = client.id;
|
||||||
|
response["res"] = res;
|
||||||
|
if (res) {
|
||||||
|
FFI.setByName("login_res", jsonEncode(response));
|
||||||
|
if (!client.isFileTransfer) {
|
||||||
|
FFI.invokeMethod("start_capture");
|
||||||
|
}
|
||||||
|
FFI.invokeMethod("cancel_notification", client.id);
|
||||||
|
_clients[client.id]?.authorized = true;
|
||||||
|
notifyListeners();
|
||||||
|
} else {
|
||||||
|
FFI.setByName("login_res", jsonEncode(response));
|
||||||
|
FFI.invokeMethod("cancel_notification", client.id);
|
||||||
|
_clients.remove(client.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onClientAuthorized(Map<String, dynamic> evt) {
|
||||||
|
try {
|
||||||
|
final client = Client.fromJson(jsonDecode(evt['client']));
|
||||||
|
DialogManager.dismissByTag(getLoginDialogTag(client.id));
|
||||||
|
_clients[client.id] = client;
|
||||||
|
scrollToBottom();
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onClientRemove(Map<String, dynamic> evt) {
|
||||||
|
try {
|
||||||
|
final id = int.parse(evt['id'] as String);
|
||||||
|
if (_clients.containsKey(id)) {
|
||||||
|
_clients.remove(id);
|
||||||
|
DialogManager.dismissByTag(getLoginDialogTag(id));
|
||||||
|
FFI.invokeMethod("cancel_notification", id);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("onClientRemove failed,error:$e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAll() {
|
||||||
|
_clients.forEach((id, client) {
|
||||||
|
FFI.setByName("close_conn", id.toString());
|
||||||
|
});
|
||||||
|
_clients.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Client {
|
||||||
|
int id = 0; // client connections inner count id
|
||||||
|
bool authorized = false;
|
||||||
|
bool isFileTransfer = false;
|
||||||
|
String name = "";
|
||||||
|
String peerId = ""; // peer user's id,show at app
|
||||||
|
bool keyboard = false;
|
||||||
|
bool clipboard = false;
|
||||||
|
bool audio = false;
|
||||||
|
late ChatUser chatUser;
|
||||||
|
|
||||||
|
Client(this.authorized, this.isFileTransfer, this.name, this.peerId,
|
||||||
|
this.keyboard, this.clipboard, this.audio);
|
||||||
|
|
||||||
|
Client.fromJson(Map<String, dynamic> json) {
|
||||||
|
id = json['id'];
|
||||||
|
authorized = json['authorized'];
|
||||||
|
isFileTransfer = json['is_file_transfer'];
|
||||||
|
name = json['name'];
|
||||||
|
peerId = json['peer_id'];
|
||||||
|
keyboard = json['keyboard'];
|
||||||
|
clipboard = json['clipboard'];
|
||||||
|
audio = json['audio'];
|
||||||
|
chatUser = ChatUser(
|
||||||
|
uid: peerId,
|
||||||
|
name: name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||||
|
data['id'] = this.id;
|
||||||
|
data['is_start'] = this.authorized;
|
||||||
|
data['is_file_transfer'] = this.isFileTransfer;
|
||||||
|
data['name'] = this.name;
|
||||||
|
data['peer_id'] = this.peerId;
|
||||||
|
data['keyboard'] = this.keyboard;
|
||||||
|
data['clipboard'] = this.clipboard;
|
||||||
|
data['audio'] = this.audio;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getLoginDialogTag(int id) {
|
||||||
|
return loginDialogTag + id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
showInputWarnAlert() {
|
||||||
|
DialogManager.show((setState, close) => CustomAlertDialog(
|
||||||
|
title: Text(translate("How to get Android input permission?")),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(translate("android_input_permission_tip1")),
|
||||||
|
SizedBox(height: 10),
|
||||||
|
Text(translate("android_input_permission_tip2")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(child: Text(translate("Cancel")), onPressed: close),
|
||||||
|
ElevatedButton(
|
||||||
|
child: Text(translate("Open System Setting")),
|
||||||
|
onPressed: () {
|
||||||
|
FFI.serverModel.initInput();
|
||||||
|
close();
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
60
flutter/lib/models/web_model.dart
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:js' as js;
|
||||||
|
|
||||||
|
import '../common.dart';
|
||||||
|
import 'dart:html';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
final List<StreamSubscription<MouseEvent>> mouseListeners = [];
|
||||||
|
final List<StreamSubscription<KeyboardEvent>> keyListeners = [];
|
||||||
|
int lastMouseDownButtons = 0;
|
||||||
|
bool mouseIn = false;
|
||||||
|
|
||||||
|
class PlatformFFI {
|
||||||
|
static void clearRgbaFrame() {}
|
||||||
|
|
||||||
|
static Uint8List? getRgba() {
|
||||||
|
return js.context.callMethod('getRgba');
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getByName(String name, [String arg = '']) {
|
||||||
|
return js.context.callMethod('getByName', [name, arg]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setByName(String name, [String value = '']) {
|
||||||
|
js.context.callMethod('setByName', [name, value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Null> init() async {
|
||||||
|
isWeb = true;
|
||||||
|
isDesktop = !js.context.callMethod('isMobile');
|
||||||
|
js.context.callMethod('init');
|
||||||
|
version = getByName('version');
|
||||||
|
}
|
||||||
|
|
||||||
|
static void startDesktopWebListener() {
|
||||||
|
mouseIn = true;
|
||||||
|
mouseListeners.add(
|
||||||
|
window.document.onContextMenu.listen((evt) => evt.preventDefault()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stopDesktopWebListener() {
|
||||||
|
mouseIn = true;
|
||||||
|
mouseListeners.forEach((l) {
|
||||||
|
l.cancel();
|
||||||
|
});
|
||||||
|
mouseListeners.clear();
|
||||||
|
keyListeners.forEach((l) {
|
||||||
|
l.cancel();
|
||||||
|
});
|
||||||
|
keyListeners.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setMethodCallHandler(FMethod callback) {}
|
||||||
|
|
||||||
|
static Future<bool> invokeMethod(String method, [dynamic arguments]) async {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final localeName = window.navigator.language;
|
||||||
81
flutter/lib/pages/chat_page.dart
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import 'package:dash_chat/dash_chat.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../models/model.dart';
|
||||||
|
import 'home_page.dart';
|
||||||
|
|
||||||
|
ChatPage chatPage = ChatPage();
|
||||||
|
|
||||||
|
class ChatPage extends StatelessWidget implements PageShape {
|
||||||
|
@override
|
||||||
|
final title = translate("Chat");
|
||||||
|
|
||||||
|
@override
|
||||||
|
final icon = Icon(Icons.chat);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final appBarActions = [
|
||||||
|
PopupMenuButton<int>(
|
||||||
|
icon: Icon(Icons.group),
|
||||||
|
itemBuilder: (context) {
|
||||||
|
final chatModel = FFI.chatModel;
|
||||||
|
final serverModel = FFI.serverModel;
|
||||||
|
return chatModel.messages.entries.map((entry) {
|
||||||
|
final id = entry.key;
|
||||||
|
final user = serverModel.clients[id]?.chatUser ?? chatModel.me;
|
||||||
|
return PopupMenuItem<int>(
|
||||||
|
child: Text("${user.name} ${user.uid}"),
|
||||||
|
value: id,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
},
|
||||||
|
onSelected: (id) {
|
||||||
|
FFI.chatModel.changeCurrentID(id);
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ChangeNotifierProvider.value(
|
||||||
|
value: FFI.chatModel,
|
||||||
|
child: Container(
|
||||||
|
color: MyTheme.grayBg,
|
||||||
|
child: Consumer<ChatModel>(builder: (context, chatModel, child) {
|
||||||
|
final currentUser = chatModel.currentUser;
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
DashChat(
|
||||||
|
inputContainerStyle: BoxDecoration(color: Colors.white70),
|
||||||
|
sendOnEnter: false,
|
||||||
|
// if true,reload keyboard everytime,need fix
|
||||||
|
onSend: (chatMsg) {
|
||||||
|
chatModel.send(chatMsg);
|
||||||
|
},
|
||||||
|
user: chatModel.me,
|
||||||
|
messages: chatModel.messages[chatModel.currentID] ?? [],
|
||||||
|
// default scrollToBottom has bug https://github.com/fayeed/dash_chat/issues/53
|
||||||
|
scrollToBottom: false,
|
||||||
|
scrollController: chatModel.scroller,
|
||||||
|
),
|
||||||
|
chatModel.currentID == ChatModel.clientModeID
|
||||||
|
? SizedBox.shrink()
|
||||||
|
: Padding(
|
||||||
|
padding: EdgeInsets.all(12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.account_circle,
|
||||||
|
color: MyTheme.accent80),
|
||||||
|
SizedBox(width: 5),
|
||||||
|
Text(
|
||||||
|
"${currentUser.name ?? ""} ${currentUser.uid ?? ""}",
|
||||||
|
style: TextStyle(color: MyTheme.accent50),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||