mirror of
https://github.com/weyne85/rustdesk.git
synced 2025-10-29 17:00:05 +00:00
Merge branch 'master' into modern-dialog
This commit is contained in:
@@ -11,22 +11,25 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<!--<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />-->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="RustDesk"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
android:supportsRtl="true">
|
||||
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter android:priority="1000">
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<!--ACTION_BOOT_COMPLETED for debug test on no root device-->
|
||||
<action android:name="com.carriez.flutter_hbb.DEBUG_BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
@@ -53,8 +56,6 @@
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -62,6 +63,11 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".PermissionRequestTransparentActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/Transparent" />
|
||||
|
||||
<service
|
||||
android:name=".MainService"
|
||||
android:enabled="true"
|
||||
@@ -75,4 +81,4 @@
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -1,21 +1,45 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import android.Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
import android.Manifest.permission.SYSTEM_ALERT_WINDOW
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.hjq.permissions.XXPermissions
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
const val DEBUG_BOOT_COMPLETED = "com.carriez.flutter_hbb.DEBUG_BOOT_COMPLETED"
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
private val logTag = "tagBootReceiver"
|
||||
|
||||
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)
|
||||
Log.d(logTag, "onReceive ${intent.action}")
|
||||
|
||||
if (Intent.ACTION_BOOT_COMPLETED == intent.action || DEBUG_BOOT_COMPLETED == intent.action) {
|
||||
// check SharedPreferences config
|
||||
val prefs = context.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
|
||||
if (!prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) {
|
||||
Log.d(logTag, "KEY_START_ON_BOOT_OPT is false")
|
||||
return
|
||||
}
|
||||
Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show();
|
||||
// check pre-permission
|
||||
if (!XXPermissions.isGranted(context, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, SYSTEM_ALERT_WINDOW)){
|
||||
Log.d(logTag, "REQUEST_IGNORE_BATTERY_OPTIMIZATIONS or SYSTEM_ALERT_WINDOW is not granted")
|
||||
return
|
||||
}
|
||||
|
||||
val it = Intent(context, MainService::class.java).apply {
|
||||
action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE
|
||||
putExtra(EXT_INIT_FROM_BOOT, true)
|
||||
}
|
||||
Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(it)
|
||||
}else{
|
||||
} else {
|
||||
context.startService(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,35 +7,29 @@ package com.carriez.flutter_hbb
|
||||
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
|
||||
*/
|
||||
|
||||
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 com.hjq.permissions.XXPermissions
|
||||
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
|
||||
var flutterMethodChannel: MethodChannel? = null
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -46,169 +40,32 @@ class MainActivity : FlutterActivity() {
|
||||
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)
|
||||
}
|
||||
)
|
||||
initFlutterChannel(flutterMethodChannel!!)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val inputPer = InputService.isOpen
|
||||
activity.runOnUiThread {
|
||||
flutterMethodChannel.invokeMethod(
|
||||
flutterMethodChannel?.invokeMethod(
|
||||
"on_state_changed",
|
||||
mapOf("name" to "input", "value" to inputPer.toString())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestMediaProjection() {
|
||||
val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
|
||||
action = ACT_REQUEST_MEDIA_PROJECTION
|
||||
}
|
||||
startActivityForResult(intent, REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) {
|
||||
flutterMethodChannel?.invokeMethod("on_media_projection_canceled", null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,4 +89,138 @@ class MainActivity : FlutterActivity() {
|
||||
mainService = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun initFlutterChannel(flutterMethodChannel: MethodChannel) {
|
||||
flutterMethodChannel.setMethodCallHandler { call, result ->
|
||||
// make sure result will be invoked, otherwise flutter will await forever
|
||||
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
|
||||
}
|
||||
requestMediaProjection()
|
||||
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(XXPermissions.isGranted(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)
|
||||
}
|
||||
}
|
||||
START_ACTION -> {
|
||||
if (call.arguments is String) {
|
||||
startAction(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" -> {
|
||||
Companion.flutterMethodChannel?.invokeMethod(
|
||||
"on_state_changed",
|
||||
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
||||
)
|
||||
Companion.flutterMethodChannel?.invokeMethod(
|
||||
"on_state_changed",
|
||||
mapOf("name" to "media", "value" to MainService.isReady.toString())
|
||||
)
|
||||
result.success(true)
|
||||
}
|
||||
"stop_input" -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
InputService.ctx?.disableSelf()
|
||||
}
|
||||
InputService.ctx = null
|
||||
Companion.flutterMethodChannel?.invokeMethod(
|
||||
"on_state_changed",
|
||||
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
||||
)
|
||||
result.success(true)
|
||||
}
|
||||
"cancel_notification" -> {
|
||||
if (call.arguments is Int) {
|
||||
val id = call.arguments as Int
|
||||
mainService?.cancelNotification(id)
|
||||
} else {
|
||||
result.success(true)
|
||||
}
|
||||
}
|
||||
"enable_soft_keyboard" -> {
|
||||
// https://blog.csdn.net/hanye2020/article/details/105553780
|
||||
if (call.arguments as Boolean) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
|
||||
} else {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
|
||||
}
|
||||
result.success(true)
|
||||
|
||||
}
|
||||
GET_START_ON_BOOT_OPT -> {
|
||||
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
|
||||
result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false))
|
||||
}
|
||||
SET_START_ON_BOOT_OPT -> {
|
||||
if (call.arguments is Boolean) {
|
||||
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
|
||||
val edit = prefs.edit()
|
||||
edit.putBoolean(KEY_START_ON_BOOT_OPT, call.arguments as Boolean)
|
||||
edit.apply()
|
||||
result.success(true)
|
||||
} else {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
SYNC_APP_DIR_CONFIG_PATH -> {
|
||||
if (call.arguments is String) {
|
||||
val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
|
||||
val edit = prefs.edit()
|
||||
edit.putString(KEY_APP_DIR_CONFIG_PATH, call.arguments as String)
|
||||
edit.apply()
|
||||
result.success(true)
|
||||
} else {
|
||||
result.success(false)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
result.error("-1", "No such method", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.concurrent.thread
|
||||
import org.json.JSONException
|
||||
@@ -43,10 +44,6 @@ 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"
|
||||
@@ -147,7 +144,11 @@ class MainService : Service() {
|
||||
|
||||
// jvm call rust
|
||||
private external fun init(ctx: Context)
|
||||
private external fun startServer()
|
||||
|
||||
/// When app start on boot, app_dir will not be passed from flutter
|
||||
/// so pass a app_dir here to rust server
|
||||
private external fun startServer(app_dir: String)
|
||||
private external fun startService()
|
||||
private external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||
private external fun onAudioFrameUpdate(buf: ByteBuffer)
|
||||
private external fun translateLocale(localeName: String, input: String): String
|
||||
@@ -195,6 +196,7 @@ class MainService : Service() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(logTag,"MainService onCreate")
|
||||
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
|
||||
start()
|
||||
serviceLooper = looper
|
||||
@@ -202,7 +204,13 @@ class MainService : Service() {
|
||||
}
|
||||
updateScreenInfo(resources.configuration.orientation)
|
||||
initNotification()
|
||||
startServer()
|
||||
|
||||
// keep the config dir same with flutter
|
||||
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
|
||||
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
|
||||
startServer(configPath)
|
||||
|
||||
createForegroundNotification()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -277,22 +285,30 @@ class MainService : Service() {
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.d("whichService", "this service:${Thread.currentThread()}")
|
||||
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()}")
|
||||
if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) {
|
||||
createForegroundNotification()
|
||||
val mMediaProjectionManager =
|
||||
|
||||
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
|
||||
startService()
|
||||
}
|
||||
Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}")
|
||||
val mediaProjectionManager =
|
||||
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
intent.getParcelableExtra<Intent>(EXTRA_MP_DATA)?.let {
|
||||
|
||||
intent.getParcelableExtra<Intent>(EXT_MEDIA_PROJECTION_RES_INTENT)?.let {
|
||||
mediaProjection =
|
||||
mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
|
||||
mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
|
||||
checkMediaPermission()
|
||||
init(this)
|
||||
_isReady = true
|
||||
} ?: let {
|
||||
Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection")
|
||||
requestMediaProjection()
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY // don't use sticky (auto restart),the new service (from auto restart) will lose control
|
||||
return START_NOT_STICKY // don't use sticky (auto restart), the new service (from auto restart) will lose control
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
@@ -300,6 +316,14 @@ class MainService : Service() {
|
||||
updateScreenInfo(newConfig.orientation)
|
||||
}
|
||||
|
||||
private fun requestMediaProjection() {
|
||||
val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
|
||||
action = ACT_REQUEST_MEDIA_PROJECTION
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
private fun createSurface(): Surface? {
|
||||
return if (useVP9) {
|
||||
@@ -400,13 +424,13 @@ class MainService : Service() {
|
||||
|
||||
fun checkMediaPermission(): Boolean {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
MainActivity.flutterMethodChannel.invokeMethod(
|
||||
MainActivity.flutterMethodChannel?.invokeMethod(
|
||||
"on_state_changed",
|
||||
mapOf("name" to "media", "value" to isReady.toString())
|
||||
)
|
||||
}
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
MainActivity.flutterMethodChannel.invokeMethod(
|
||||
MainActivity.flutterMethodChannel?.invokeMethod(
|
||||
"on_state_changed",
|
||||
mapOf("name" to "input", "value" to InputService.isOpen.toString())
|
||||
)
|
||||
@@ -653,8 +677,8 @@ class MainService : Service() {
|
||||
@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)
|
||||
action = ACT_LOGIN_REQ_NOTIFY
|
||||
putExtra(EXT_LOGIN_REQ_NOTIFY, res)
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.getService(this, 111, intent, FLAG_IMMUTABLE)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
|
||||
class PermissionRequestTransparentActivity: Activity() {
|
||||
private val logTag = "permissionRequest"
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.d(logTag, "onCreate PermissionRequestTransparentActivity: intent.action: ${intent.action}")
|
||||
|
||||
when (intent.action) {
|
||||
ACT_REQUEST_MEDIA_PROJECTION -> {
|
||||
val mediaProjectionManager =
|
||||
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
val intent = mediaProjectionManager.createScreenCaptureIntent()
|
||||
startActivityForResult(intent, REQ_REQUEST_MEDIA_PROJECTION)
|
||||
}
|
||||
else -> finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == REQ_REQUEST_MEDIA_PROJECTION) {
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
launchService(data)
|
||||
} else {
|
||||
setResult(RES_FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun launchService(mediaProjectionResultIntent: Intent) {
|
||||
Log.d(logTag, "Launch MainService")
|
||||
val serviceIntent = Intent(this, MainService::class.java)
|
||||
serviceIntent.action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE
|
||||
serviceIntent.putExtra(EXT_MEDIA_PROJECTION_RES_INTENT, mediaProjectionResultIntent)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent)
|
||||
} else {
|
||||
startService(serviceIntent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.carriez.flutter_hbb
|
||||
|
||||
import android.Manifest.permission.*
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -12,8 +13,8 @@ import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
|
||||
import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
|
||||
import android.provider.Settings
|
||||
import android.provider.Settings.*
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import com.hjq.permissions.Permission
|
||||
@@ -22,6 +23,31 @@ import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
|
||||
// intent action, extra
|
||||
const val ACT_REQUEST_MEDIA_PROJECTION = "REQUEST_MEDIA_PROJECTION"
|
||||
const val ACT_INIT_MEDIA_PROJECTION_AND_SERVICE = "INIT_MEDIA_PROJECTION_AND_SERVICE"
|
||||
const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
||||
const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT"
|
||||
const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT"
|
||||
const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
|
||||
|
||||
// Activity requestCode
|
||||
const val REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION = 101
|
||||
const val REQ_REQUEST_MEDIA_PROJECTION = 201
|
||||
|
||||
// Activity responseCode
|
||||
const val RES_FAILED = -100
|
||||
|
||||
// Flutter channel
|
||||
const val START_ACTION = "start_action"
|
||||
const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
|
||||
const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
|
||||
const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
|
||||
|
||||
const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
|
||||
const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
|
||||
const val KEY_APP_DIR_CONFIG_PATH = "KEY_APP_DIR_CONFIG_PATH"
|
||||
|
||||
@SuppressLint("ConstantLocale")
|
||||
val LOCAL_NAME = Locale.getDefault().toString()
|
||||
val SCREEN_INFO = Info(0, 0, 1, 200)
|
||||
@@ -30,61 +56,13 @@ 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
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun requestPermission(context: Context, type: String) {
|
||||
val permission = when (type) {
|
||||
"ignore_battery_optimizations" -> {
|
||||
try {
|
||||
context.startActivity(Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
||||
data = Uri.parse("package:" + context.packageName)
|
||||
})
|
||||
} catch (e:Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return
|
||||
}
|
||||
"application_details_settings" -> {
|
||||
try {
|
||||
context.startActivity(Intent().apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
action = "android.settings.APPLICATION_DETAILS_SETTINGS"
|
||||
data = Uri.parse("package:" + context.packageName)
|
||||
})
|
||||
} catch (e:Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return
|
||||
}
|
||||
"audio" -> {
|
||||
Permission.RECORD_AUDIO
|
||||
}
|
||||
"file" -> {
|
||||
Permission.MANAGE_EXTERNAL_STORAGE
|
||||
}
|
||||
else -> {
|
||||
return
|
||||
}
|
||||
}
|
||||
XXPermissions.with(context)
|
||||
.permission(permission)
|
||||
.permission(type)
|
||||
.request { _, all ->
|
||||
if (all) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
MainActivity.flutterMethodChannel.invokeMethod(
|
||||
MainActivity.flutterMethodChannel?.invokeMethod(
|
||||
"on_android_permission_result",
|
||||
mapOf("type" to type, "result" to all)
|
||||
)
|
||||
@@ -93,24 +71,18 @@ fun requestPermission(context: Context, type: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun checkPermission(context: Context, type: String): Boolean {
|
||||
val permission = when (type) {
|
||||
"ignore_battery_optimizations" -> {
|
||||
val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return pw.isIgnoringBatteryOptimizations(context.packageName)
|
||||
}
|
||||
"audio" -> {
|
||||
Permission.RECORD_AUDIO
|
||||
}
|
||||
"file" -> {
|
||||
Permission.MANAGE_EXTERNAL_STORAGE
|
||||
}
|
||||
else -> {
|
||||
return false
|
||||
}
|
||||
fun startAction(context: Context, action: String) {
|
||||
try {
|
||||
context.startActivity(Intent(action).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
// don't pass package name when launch ACTION_ACCESSIBILITY_SETTINGS
|
||||
if (ACTION_ACCESSIBILITY_SETTINGS != action) {
|
||||
data = Uri.parse("package:" + context.packageName)
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return XXPermissions.isGranted(context, permission)
|
||||
}
|
||||
|
||||
class AudioReader(val bufSize: Int, private val maxFrames: Int) {
|
||||
|
||||
@@ -15,4 +15,12 @@
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
<style name="Transparent" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -109,27 +109,32 @@ class IconFont {
|
||||
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
||||
const ColorThemeExtension({
|
||||
required this.border,
|
||||
required this.border2,
|
||||
required this.highlight,
|
||||
});
|
||||
|
||||
final Color? border;
|
||||
final Color? border2;
|
||||
final Color? highlight;
|
||||
|
||||
static const light = ColorThemeExtension(
|
||||
border: Color(0xFFCCCCCC),
|
||||
border2: Color(0xFFBBBBBB),
|
||||
highlight: Color(0xFFE5E5E5),
|
||||
);
|
||||
|
||||
static const dark = ColorThemeExtension(
|
||||
border: Color(0xFF555555),
|
||||
border2: Color(0xFFE5E5E5),
|
||||
highlight: Color(0xFF3F3F3F),
|
||||
);
|
||||
|
||||
@override
|
||||
ThemeExtension<ColorThemeExtension> copyWith(
|
||||
{Color? border, Color? highlight}) {
|
||||
{Color? border, Color? border2, Color? highlight}) {
|
||||
return ColorThemeExtension(
|
||||
border: border ?? this.border,
|
||||
border2: border2 ?? this.border2,
|
||||
highlight: highlight ?? this.highlight,
|
||||
);
|
||||
}
|
||||
@@ -142,6 +147,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
|
||||
}
|
||||
return ColorThemeExtension(
|
||||
border: Color.lerp(border, other.border, t),
|
||||
border2: Color.lerp(border2, other.border2, t),
|
||||
highlight: Color.lerp(highlight, other.highlight, t),
|
||||
);
|
||||
}
|
||||
@@ -207,38 +213,30 @@ class MyTheme {
|
||||
splashFactory: isDesktop ? NoSplash.splashFactory : null,
|
||||
textButtonTheme: isDesktop
|
||||
? TextButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
style: TextButton.styleFrom(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(
|
||||
MyTheme.accent,
|
||||
),
|
||||
shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: MyTheme.accent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(
|
||||
Color(0xFFEEEEEE),
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Color(
|
||||
0xFFEEEEEE,
|
||||
),
|
||||
foregroundColor: MaterialStatePropertyAll(Colors.black87),
|
||||
shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
foregroundColor: Colors.black87,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -306,46 +304,42 @@ class MyTheme {
|
||||
tabBarTheme: const TabBarTheme(
|
||||
labelColor: Colors.white70,
|
||||
),
|
||||
scrollbarTheme: ScrollbarThemeData(
|
||||
thumbColor: MaterialStateProperty.all(Colors.grey[500]),
|
||||
),
|
||||
splashColor: Colors.transparent,
|
||||
highlightColor: Colors.transparent,
|
||||
splashFactory: isDesktop ? NoSplash.splashFactory : null,
|
||||
textButtonTheme: isDesktop
|
||||
? TextButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
style: TextButton.styleFrom(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
),
|
||||
disabledForegroundColor: Colors.white70,
|
||||
foregroundColor: Colors.white70,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(18.0),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(
|
||||
MyTheme.accent,
|
||||
),
|
||||
shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: MyTheme.accent,
|
||||
disabledForegroundColor: Colors.white70,
|
||||
disabledBackgroundColor: Colors.white10,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStatePropertyAll(
|
||||
Color(0xFF24252B),
|
||||
),
|
||||
side: MaterialStatePropertyAll(
|
||||
BorderSide(color: Colors.white12, width: 0.5),
|
||||
),
|
||||
foregroundColor: MaterialStatePropertyAll(Colors.white70),
|
||||
shape: MaterialStatePropertyAll<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF24252B),
|
||||
side: BorderSide(color: Colors.white12, width: 0.5),
|
||||
disabledForegroundColor: Colors.white70,
|
||||
foregroundColor: Colors.white70,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1045,21 +1039,14 @@ class AccessibilityListener extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class PermissionManager {
|
||||
class AndroidPermissionManager {
|
||||
static Completer<bool>? _completer;
|
||||
static Timer? _timer;
|
||||
static var _current = "";
|
||||
|
||||
static final permissions = [
|
||||
"audio",
|
||||
"file",
|
||||
"ignore_battery_optimizations",
|
||||
"application_details_settings"
|
||||
];
|
||||
|
||||
static bool isWaitingFile() {
|
||||
if (_completer != null) {
|
||||
return !_completer!.isCompleted && _current == "file";
|
||||
return !_completer!.isCompleted && _current == kManageExternalStorage;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1068,31 +1055,33 @@ class PermissionManager {
|
||||
if (isDesktop) {
|
||||
return Future.value(true);
|
||||
}
|
||||
if (!permissions.contains(type)) {
|
||||
return Future.error("Wrong permission!$type");
|
||||
}
|
||||
return gFFI.invokeMethod("check_permission", type);
|
||||
}
|
||||
|
||||
// startActivity goto Android Setting's page to request permission manually by user
|
||||
static void startAction(String action) {
|
||||
gFFI.invokeMethod(AndroidChannel.kStartAction, action);
|
||||
}
|
||||
|
||||
/// We use XXPermissions to request permissions,
|
||||
/// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java
|
||||
static Future<bool> request(String type) {
|
||||
if (isDesktop) {
|
||||
return Future.value(true);
|
||||
}
|
||||
if (!permissions.contains(type)) {
|
||||
return Future.error("Wrong permission!$type");
|
||||
}
|
||||
|
||||
gFFI.invokeMethod("request_permission", type);
|
||||
if (type == "ignore_battery_optimizations") {
|
||||
return Future.value(false);
|
||||
|
||||
// clear last task
|
||||
if (_completer?.isCompleted == false) {
|
||||
_completer?.complete(false);
|
||||
}
|
||||
_timer?.cancel();
|
||||
|
||||
_current = type;
|
||||
_completer = Completer<bool>();
|
||||
gFFI.invokeMethod("request_permission", type);
|
||||
|
||||
// timeout
|
||||
_timer?.cancel();
|
||||
_timer = Timer(Duration(seconds: 60), () {
|
||||
_timer = Timer(Duration(seconds: 120), () {
|
||||
if (_completer == null) return;
|
||||
if (!_completer!.isCompleted) {
|
||||
_completer!.complete(false);
|
||||
@@ -1622,8 +1611,8 @@ connect(BuildContext context, String id,
|
||||
}
|
||||
} else {
|
||||
if (isFileTransfer) {
|
||||
if (!await PermissionManager.check("file")) {
|
||||
if (!await PermissionManager.request("file")) {
|
||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||
if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
|
||||
const double kDesktopRemoteTabBarHeight = 28.0;
|
||||
const int kMainWindowId = 0;
|
||||
@@ -58,6 +59,12 @@ const double kDesktopFileTransferMaximumWidth = 300;
|
||||
const double kDesktopFileTransferRowHeight = 30.0;
|
||||
const double kDesktopFileTransferHeaderHeight = 25.0;
|
||||
|
||||
EdgeInsets get kDragToResizeAreaPadding =>
|
||||
!kUseCompatibleUiMode && Platform.isLinux
|
||||
? stateGlobal.fullscreen || stateGlobal.maximize
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.all(5.0)
|
||||
: EdgeInsets.zero;
|
||||
// https://en.wikipedia.org/wiki/Non-breaking_space
|
||||
const int $nbsp = 0x00A0;
|
||||
|
||||
@@ -79,6 +86,7 @@ const kDefaultScrollAmountMultiplier = 5.0;
|
||||
const kDefaultScrollDuration = Duration(milliseconds: 50);
|
||||
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
|
||||
const kFullScreenEdgeSize = 0.0;
|
||||
const kMaximizeEdgeSize = 0.0;
|
||||
var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0;
|
||||
const kWindowBorderWidth = 1.0;
|
||||
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
||||
@@ -129,6 +137,25 @@ const kRemoteAudioDualWay = 'dual-way';
|
||||
|
||||
const kIgnoreDpi = true;
|
||||
|
||||
/// Android constants
|
||||
const kActionApplicationDetailsSettings =
|
||||
"android.settings.APPLICATION_DETAILS_SETTINGS";
|
||||
const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS";
|
||||
|
||||
const kRecordAudio = "android.permission.RECORD_AUDIO";
|
||||
const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE";
|
||||
const kRequestIgnoreBatteryOptimizations =
|
||||
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
|
||||
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
|
||||
|
||||
/// Android channel invoke type key
|
||||
class AndroidChannel {
|
||||
static final kStartAction = "start_action";
|
||||
static final kGetStartOnBootOpt = "get_start_on_boot_opt";
|
||||
static final kSetStartOnBootOpt = "set_start_on_boot_opt";
|
||||
static final kSyncAppDirConfigPath = "sync_app_dir";
|
||||
}
|
||||
|
||||
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
|
||||
/// see [LogicalKeyboardKey.keyLabel]
|
||||
const Map<int, String> logicalKeyMap = <int, String>{
|
||||
|
||||
@@ -19,7 +19,7 @@ import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
|
||||
const double _kTabWidth = 235;
|
||||
const double _kTabWidth = 200;
|
||||
const double _kTabHeight = 42;
|
||||
const double _kCardFixedWidth = 540;
|
||||
const double _kCardLeftMargin = 15;
|
||||
@@ -538,6 +538,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
translate('Screen Share'),
|
||||
translate('Deny remote access'),
|
||||
],
|
||||
enabled: enabled,
|
||||
initialKey: initialKey,
|
||||
onChanged: (mode) async {
|
||||
String modeValue;
|
||||
@@ -667,6 +668,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
|
||||
|
||||
return _Card(title: 'Password', children: [
|
||||
_ComboBox(
|
||||
enabled: !locked,
|
||||
keys: modeKeys,
|
||||
values: modeValues,
|
||||
initialKey: modeInitialKey,
|
||||
@@ -1722,7 +1724,6 @@ class _ComboBox extends StatelessWidget {
|
||||
required this.values,
|
||||
required this.initialKey,
|
||||
required this.onChanged,
|
||||
// ignore: unused_element
|
||||
this.enabled = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -1735,7 +1736,12 @@ class _ComboBox extends StatelessWidget {
|
||||
var ref = values[index].obs;
|
||||
current = keys[index];
|
||||
return Container(
|
||||
decoration: BoxDecoration(border: Border.all(color: MyTheme.border)),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: enabled
|
||||
? MyTheme.color(context).border2 ?? MyTheme.border
|
||||
: MyTheme.border,
|
||||
)),
|
||||
height: 30,
|
||||
child: Obx(() => DropdownButton<String>(
|
||||
isExpanded: true,
|
||||
@@ -1744,6 +1750,10 @@ class _ComboBox extends StatelessWidget {
|
||||
underline: Container(
|
||||
height: 25,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: enabled
|
||||
? Theme.of(context).textTheme.titleMedium?.color
|
||||
: _disabledTextColor(context, enabled)),
|
||||
icon: const Icon(
|
||||
Icons.expand_more_sharp,
|
||||
size: 20,
|
||||
|
||||
@@ -75,7 +75,7 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
||||
isClose: false,
|
||||
),
|
||||
)));
|
||||
return Platform.isMacOS
|
||||
return Platform.isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(
|
||||
() => DragToResizeArea(
|
||||
|
||||
@@ -98,7 +98,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
||||
labelGetter: DesktopTab.labelGetterAlias,
|
||||
)),
|
||||
);
|
||||
return Platform.isMacOS
|
||||
return Platform.isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/state_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
@@ -13,7 +15,51 @@ class InstallPage extends StatefulWidget {
|
||||
State<InstallPage> createState() => _InstallPageState();
|
||||
}
|
||||
|
||||
class _InstallPageState extends State<InstallPage> with WindowListener {
|
||||
class _InstallPageState extends State<InstallPage> {
|
||||
final tabController = DesktopTabController(tabType: DesktopTabType.main);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Get.put<DesktopTabController>(tabController);
|
||||
const lable = "install";
|
||||
tabController.add(TabInfo(
|
||||
key: lable,
|
||||
label: lable,
|
||||
closable: false,
|
||||
page: _InstallPageBody(
|
||||
key: const ValueKey(lable),
|
||||
)));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
Get.delete<DesktopTabController>();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DragToResizeArea(
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
child: Container(
|
||||
child: Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: DesktopTab(controller: tabController)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InstallPageBody extends StatefulWidget {
|
||||
const _InstallPageBody({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_InstallPageBody> createState() => _InstallPageBodyState();
|
||||
}
|
||||
|
||||
class _InstallPageBodyState extends State<_InstallPageBody>
|
||||
with WindowListener {
|
||||
late final TextEditingController controller;
|
||||
final RxBool startmenu = true.obs;
|
||||
final RxBool desktopicon = true.obs;
|
||||
@@ -46,15 +92,19 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
|
||||
final double em = 13;
|
||||
final btnFontSize = 0.9 * em;
|
||||
final double button_radius = 6;
|
||||
final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
|
||||
final buttonStyle = OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(button_radius)),
|
||||
));
|
||||
final inputBorder = OutlineInputBorder(
|
||||
borderRadius: BorderRadius.zero,
|
||||
borderSide: BorderSide(color: Colors.black12));
|
||||
borderSide:
|
||||
BorderSide(color: isDarkTheme ? Colors.white70 : Colors.black12));
|
||||
final textColor = isDarkTheme ? null : Colors.black87;
|
||||
final dividerColor = isDarkTheme ? Colors.white70 : Colors.black87;
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: null,
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -91,8 +141,7 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
|
||||
style: buttonStyle,
|
||||
child: Text(translate('Change Path'),
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: btnFontSize)))
|
||||
color: textColor, fontSize: btnFontSize)))
|
||||
.marginOnly(left: em))
|
||||
],
|
||||
).marginSymmetric(vertical: 2 * em),
|
||||
@@ -127,8 +176,7 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
|
||||
)).marginOnly(top: 2 * em),
|
||||
Row(children: [Text(translate('agreement_tip'))])
|
||||
.marginOnly(top: em),
|
||||
Divider(color: Colors.black87)
|
||||
.marginSymmetric(vertical: 0.5 * em),
|
||||
Divider(color: dividerColor).marginSymmetric(vertical: 0.5 * em),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -143,8 +191,7 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
|
||||
style: buttonStyle,
|
||||
child: Text(translate('Cancel'),
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: btnFontSize)))
|
||||
color: textColor, fontSize: btnFontSize)))
|
||||
.marginOnly(right: 2 * em)),
|
||||
Obx(() => ElevatedButton(
|
||||
onPressed: btnEnabled.value ? install : null,
|
||||
@@ -167,8 +214,7 @@ class _InstallPageState extends State<InstallPage> with WindowListener {
|
||||
style: buttonStyle,
|
||||
child: Text(translate('Run without install'),
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: btnFontSize)))
|
||||
color: textColor, fontSize: btnFontSize)))
|
||||
.marginOnly(left: 2 * em)),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -107,13 +107,15 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
||||
labelGetter: DesktopTab.labelGetterAlias,
|
||||
)),
|
||||
);
|
||||
return Platform.isMacOS
|
||||
return Platform.isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
windowId: stateGlobal.windowId,
|
||||
);
|
||||
: Obx(
|
||||
() => SubWindowDragToResizeArea(
|
||||
child: tabWidget,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
windowId: stateGlobal.windowId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onRemoveId(String id) {
|
||||
|
||||
@@ -205,11 +205,13 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
||||
),
|
||||
),
|
||||
);
|
||||
return Platform.isMacOS
|
||||
return Platform.isMacOS || kUseCompatibleUiMode
|
||||
? tabWidget
|
||||
: Obx(() => SubWindowDragToResizeArea(
|
||||
key: contentKey,
|
||||
child: tabWidget,
|
||||
// Specially configured for a better resize area and remote control.
|
||||
childPadding: kDragToResizeAreaPadding,
|
||||
resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
|
||||
windowId: stateGlobal.windowId,
|
||||
));
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart';
|
||||
@@ -26,6 +28,9 @@ class DesktopRemoteScreen extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: gFFI.canvasModel),
|
||||
],
|
||||
child: Scaffold(
|
||||
// Set transparent background for padding the resize area out of the flutter view.
|
||||
// This allows the wallpaper goes through our resize area. (Linux only now).
|
||||
backgroundColor: Platform.isLinux ? Colors.transparent : null,
|
||||
body: ConnectionTabPage(
|
||||
params: params,
|
||||
),
|
||||
|
||||
@@ -942,6 +942,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
disableClipboard(),
|
||||
lockAfterSessionEnd(),
|
||||
privacyMode(),
|
||||
swapKey(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -975,12 +976,13 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
|
||||
final canvasModel = widget.ffi.canvasModel;
|
||||
final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
|
||||
canvasModel.windowBorderWidth * 2) *
|
||||
CanvasModel.leftToEdge +
|
||||
CanvasModel.rightToEdge) *
|
||||
scale +
|
||||
magicWidth;
|
||||
final height = (canvasModel.getDisplayHeight() * canvasModel.scale +
|
||||
canvasModel.tabBarHeight +
|
||||
canvasModel.windowBorderWidth * 2) *
|
||||
CanvasModel.topToEdge +
|
||||
CanvasModel.bottomToEdge) *
|
||||
scale +
|
||||
magicHeight;
|
||||
double left = wndRect.left + (wndRect.width - width) / 2;
|
||||
@@ -1049,10 +1051,10 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
final canvasModel = widget.ffi.canvasModel;
|
||||
final displayWidth = canvasModel.getDisplayWidth();
|
||||
final displayHeight = canvasModel.getDisplayHeight();
|
||||
final requiredWidth = displayWidth +
|
||||
(canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2);
|
||||
final requiredHeight = displayHeight +
|
||||
(canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2);
|
||||
final requiredWidth =
|
||||
CanvasModel.leftToEdge + displayWidth + CanvasModel.rightToEdge;
|
||||
final requiredHeight =
|
||||
CanvasModel.topToEdge + displayHeight + CanvasModel.bottomToEdge;
|
||||
return selfWidth > (requiredWidth * scale) &&
|
||||
selfHeight > (requiredHeight * scale);
|
||||
}
|
||||
@@ -1549,6 +1551,23 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
||||
ffi: widget.ffi,
|
||||
child: Text(translate('Privacy mode')));
|
||||
}
|
||||
|
||||
swapKey() {
|
||||
final visible = perms['keyboard'] != false &&
|
||||
((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) ||
|
||||
(!Platform.isMacOS && pi.platform == kPeerPlatformMacOS));
|
||||
if (!visible) return Offstage();
|
||||
final option = 'allow_swap_key';
|
||||
final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option);
|
||||
return _CheckboxMenuButton(
|
||||
value: value,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
bind.sessionToggleOption(id: widget.id, value: option);
|
||||
},
|
||||
ffi: widget.ffi,
|
||||
child: Text(translate('Swap control-command key')));
|
||||
}
|
||||
}
|
||||
|
||||
class _KeyboardMenu extends StatelessWidget {
|
||||
@@ -1564,9 +1583,8 @@ class _KeyboardMenu extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Do not check permission here?
|
||||
// var ffiModel = Provider.of<FfiModel>(context);
|
||||
// if (ffiModel.permissions['keyboard'] == false) return Offstage();
|
||||
var ffiModel = Provider.of<FfiModel>(context);
|
||||
if (ffiModel.permissions['keyboard'] == false) return Offstage();
|
||||
if (stateGlobal.grabKeyboard) {
|
||||
if (bind.sessionIsKeyboardModeSupported(id: id, mode: _kKeyMapMode)) {
|
||||
bind.sessionSetKeyboardMode(id: id, value: _kKeyMapMode);
|
||||
|
||||
@@ -14,6 +14,7 @@ class DesktopScrollWrapper extends StatelessWidget {
|
||||
return ImprovedScrolling(
|
||||
scrollController: scrollController,
|
||||
enableCustomMouseWheelScrolling: true,
|
||||
// enableKeyboardScrolling: true, // strange behavior on mac
|
||||
customMouseWheelScrollConfig: CustomMouseWheelScrollConfig(
|
||||
scrollDuration: kDefaultScrollDuration,
|
||||
scrollCurve: Curves.linearToEaseOut,
|
||||
|
||||
@@ -53,6 +53,7 @@ enum DesktopTabType {
|
||||
remoteScreen,
|
||||
fileTransfer,
|
||||
portForward,
|
||||
install,
|
||||
}
|
||||
|
||||
class DesktopTabState {
|
||||
@@ -249,8 +250,9 @@ class DesktopTab extends StatelessWidget {
|
||||
this.unSelectedTabBackgroundColor,
|
||||
}) : super(key: key) {
|
||||
tabType = controller.tabType;
|
||||
isMainWindow =
|
||||
tabType == DesktopTabType.main || tabType == DesktopTabType.cm;
|
||||
isMainWindow = tabType == DesktopTabType.main ||
|
||||
tabType == DesktopTabType.cm ||
|
||||
tabType == DesktopTabType.install;
|
||||
}
|
||||
|
||||
static RxString labelGetterAlias(String peerId) {
|
||||
@@ -361,7 +363,8 @@ class DesktopTab extends StatelessWidget {
|
||||
/// - hide single item when only has one item (home) on [DesktopTabPage].
|
||||
bool isHideSingleItem() {
|
||||
return state.value.tabs.length == 1 &&
|
||||
controller.tabType == DesktopTabType.main;
|
||||
(controller.tabType == DesktopTabType.main ||
|
||||
controller.tabType == DesktopTabType.install);
|
||||
}
|
||||
|
||||
Widget _buildBar() {
|
||||
@@ -523,12 +526,18 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setMaximize(bool maximize) {
|
||||
stateGlobal.setMaximize(maximize);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMaximize() {
|
||||
// catch maximize from system
|
||||
if (!widget.isMaximized.value) {
|
||||
widget.isMaximized.value = true;
|
||||
}
|
||||
_setMaximize(true);
|
||||
super.onWindowMaximize();
|
||||
}
|
||||
|
||||
@@ -538,6 +547,7 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
||||
if (widget.isMaximized.value) {
|
||||
widget.isMaximized.value = false;
|
||||
}
|
||||
_setMaximize(false);
|
||||
super.onWindowUnmaximize();
|
||||
}
|
||||
|
||||
@@ -752,7 +762,8 @@ class _ListView extends StatelessWidget {
|
||||
/// - hide single item when only has one item (home) on [DesktopTabPage].
|
||||
bool isHideSingleItem() {
|
||||
return state.value.tabs.length == 1 &&
|
||||
controller.tabType == DesktopTabType.main;
|
||||
controller.tabType == DesktopTabType.main ||
|
||||
controller.tabType == DesktopTabType.install;
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -153,6 +153,7 @@ void runMainApp(bool startService) async {
|
||||
void runMobileApp() async {
|
||||
await initEnv(kAppTypeMain);
|
||||
if (isAndroid) androidChannelInit();
|
||||
platformFFI.syncAndroidServiceAppDirConfigPath();
|
||||
runApp(App());
|
||||
}
|
||||
|
||||
@@ -291,17 +292,20 @@ void _runApp(
|
||||
void runInstallPage() async {
|
||||
await windowManager.ensureInitialized();
|
||||
await initEnv(kAppTypeMain);
|
||||
_runApp('', const InstallPage(), ThemeMode.light);
|
||||
windowManager.waitUntilReadyToShow(
|
||||
WindowOptions(size: Size(800, 600), center: true), () async {
|
||||
_runApp('', const InstallPage(), MyTheme.currentThemeMode());
|
||||
WindowOptions windowOptions =
|
||||
getHiddenTitleBarWindowOptions(size: Size(800, 600), center: true);
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
windowManager.show();
|
||||
windowManager.focus();
|
||||
windowManager.setOpacity(1);
|
||||
windowManager.setAlignment(Alignment.center); // ensure
|
||||
windowManager.setTitle(getWindowName());
|
||||
});
|
||||
}
|
||||
|
||||
WindowOptions getHiddenTitleBarWindowOptions({Size? size}) {
|
||||
WindowOptions getHiddenTitleBarWindowOptions(
|
||||
{Size? size, bool center = false}) {
|
||||
var defaultTitleBarStyle = TitleBarStyle.hidden;
|
||||
// we do not hide titlebar on win7 because of the frame overflow.
|
||||
if (kUseCompatibleUiMode) {
|
||||
@@ -309,7 +313,7 @@ WindowOptions getHiddenTitleBarWindowOptions({Size? size}) {
|
||||
}
|
||||
return WindowOptions(
|
||||
size: size,
|
||||
center: false,
|
||||
center: center,
|
||||
backgroundColor: Colors.transparent,
|
||||
skipTaskbar: false,
|
||||
titleBarStyle: defaultTitleBarStyle,
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
import '../../consts.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../models/server_model.dart';
|
||||
import 'home_page.dart';
|
||||
@@ -40,14 +41,14 @@ class ServerPage extends StatefulWidget implements PageShape {
|
||||
value: "setTemporaryPasswordLength",
|
||||
enabled:
|
||||
gFFI.serverModel.verificationMethod != kUsePermanentPassword,
|
||||
child: Text(translate("Set temporary password length")),
|
||||
child: Text(translate("One-time password length")),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0.0),
|
||||
value: kUseTemporaryPassword,
|
||||
child: ListTile(
|
||||
title: Text(translate("Use temporary password")),
|
||||
title: Text(translate("Use one-time password")),
|
||||
trailing: Icon(
|
||||
Icons.check,
|
||||
color: gFFI.serverModel.verificationMethod ==
|
||||
@@ -150,10 +151,11 @@ class _ServerPageState extends State<ServerPage> {
|
||||
}
|
||||
|
||||
void checkService() async {
|
||||
gFFI.invokeMethod("check_service"); // jvm
|
||||
// for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page
|
||||
if (PermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) {
|
||||
PermissionManager.complete("file", await PermissionManager.check("file"));
|
||||
gFFI.invokeMethod("check_service");
|
||||
// for Android 10/11, request MANAGE_EXTERNAL_STORAGE permission from system setting page
|
||||
if (AndroidPermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) {
|
||||
AndroidPermissionManager.complete(kManageExternalStorage,
|
||||
await AndroidPermissionManager.check(kManageExternalStorage));
|
||||
debugPrint("file permission finished");
|
||||
}
|
||||
}
|
||||
@@ -567,7 +569,7 @@ void androidChannelInit() {
|
||||
{
|
||||
var type = arguments["type"] as String;
|
||||
var result = arguments["result"] as bool;
|
||||
PermissionManager.complete(type, result);
|
||||
AndroidPermissionManager.complete(type, result);
|
||||
break;
|
||||
}
|
||||
case "on_media_projection_canceled":
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../common.dart';
|
||||
import '../../common/widgets/dialog.dart';
|
||||
import '../../common/widgets/login.dart';
|
||||
import '../../consts.dart';
|
||||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../widgets/dialog.dart';
|
||||
@@ -31,18 +32,20 @@ class SettingsPage extends StatefulWidget implements PageShape {
|
||||
}
|
||||
|
||||
const url = 'https://rustdesk.com/';
|
||||
final _hasIgnoreBattery = androidVersion >= 26;
|
||||
var _ignoreBatteryOpt = false;
|
||||
var _enableAbr = false;
|
||||
var _denyLANDiscovery = false;
|
||||
var _onlyWhiteList = false;
|
||||
var _enableDirectIPAccess = false;
|
||||
var _enableRecordSession = false;
|
||||
var _autoRecordIncomingSession = false;
|
||||
var _localIP = "";
|
||||
var _directAccessPort = "";
|
||||
|
||||
class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
final _hasIgnoreBattery = androidVersion >= 26;
|
||||
var _ignoreBatteryOpt = false;
|
||||
var _enableStartOnBoot = false;
|
||||
var _enableAbr = false;
|
||||
var _denyLANDiscovery = false;
|
||||
var _onlyWhiteList = false;
|
||||
var _enableDirectIPAccess = false;
|
||||
var _enableRecordSession = false;
|
||||
var _autoRecordIncomingSession = false;
|
||||
var _localIP = "";
|
||||
var _directAccessPort = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -50,11 +53,34 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
|
||||
() async {
|
||||
var update = false;
|
||||
|
||||
if (_hasIgnoreBattery) {
|
||||
update = await updateIgnoreBatteryStatus();
|
||||
if (await checkAndUpdateIgnoreBatteryStatus()) {
|
||||
update = true;
|
||||
}
|
||||
}
|
||||
|
||||
final enableAbrRes = await bind.mainGetOption(key: "enable-abr") != "N";
|
||||
if (await checkAndUpdateStartOnBoot()) {
|
||||
update = true;
|
||||
}
|
||||
|
||||
// start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW
|
||||
var enableStartOnBoot =
|
||||
await gFFI.invokeMethod(AndroidChannel.kGetStartOnBootOpt);
|
||||
if (enableStartOnBoot) {
|
||||
if (!await canStartOnBoot()) {
|
||||
enableStartOnBoot = false;
|
||||
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (enableStartOnBoot != _enableStartOnBoot) {
|
||||
update = true;
|
||||
_enableStartOnBoot = enableStartOnBoot;
|
||||
}
|
||||
|
||||
final enableAbrRes = option2bool(
|
||||
"enable-abr", await bind.mainGetOption(key: "enable-abr"));
|
||||
if (enableAbrRes != _enableAbr) {
|
||||
update = true;
|
||||
_enableAbr = enableAbrRes;
|
||||
@@ -125,15 +151,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
() async {
|
||||
if (await updateIgnoreBatteryStatus()) {
|
||||
final ibs = await checkAndUpdateIgnoreBatteryStatus();
|
||||
final sob = await checkAndUpdateStartOnBoot();
|
||||
if (ibs || sob) {
|
||||
setState(() {});
|
||||
}
|
||||
}();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> updateIgnoreBatteryStatus() async {
|
||||
final res = await PermissionManager.check("ignore_battery_optimizations");
|
||||
Future<bool> checkAndUpdateIgnoreBatteryStatus() async {
|
||||
final res = await AndroidPermissionManager.check(
|
||||
kRequestIgnoreBatteryOptimizations);
|
||||
if (_ignoreBatteryOpt != res) {
|
||||
_ignoreBatteryOpt = res;
|
||||
return true;
|
||||
@@ -142,6 +171,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> checkAndUpdateStartOnBoot() async {
|
||||
if (!await canStartOnBoot() && _enableStartOnBoot) {
|
||||
_enableStartOnBoot = false;
|
||||
debugPrint(
|
||||
"checkAndUpdateStartOnBoot and set _enableStartOnBoot -> false");
|
||||
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Provider.of<FfiModel>(context);
|
||||
@@ -265,7 +306,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
]),
|
||||
onToggle: (v) async {
|
||||
if (v) {
|
||||
PermissionManager.request("ignore_battery_optimizations");
|
||||
await AndroidPermissionManager.request(
|
||||
kRequestIgnoreBatteryOptimizations);
|
||||
} else {
|
||||
final res = await gFFI.dialogManager
|
||||
.show<bool>((setState, close) => CustomAlertDialog(
|
||||
@@ -282,11 +324,44 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
],
|
||||
));
|
||||
if (res == true) {
|
||||
PermissionManager.request("application_details_settings");
|
||||
AndroidPermissionManager.startAction(
|
||||
kActionApplicationDetailsSettings);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
enhancementsTiles.add(SettingsTile.switchTile(
|
||||
initialValue: _enableStartOnBoot,
|
||||
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text("${translate('Start on Boot')} (beta)"),
|
||||
Text(
|
||||
'* ${translate('Start the screen sharing service on boot, requires special permissions')}',
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
]),
|
||||
onToggle: (toValue) async {
|
||||
if (toValue) {
|
||||
// 1. request kIgnoreBatteryOptimizations
|
||||
if (!await AndroidPermissionManager.check(
|
||||
kRequestIgnoreBatteryOptimizations)) {
|
||||
if (!await AndroidPermissionManager.request(
|
||||
kRequestIgnoreBatteryOptimizations)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. request kSystemAlertWindow
|
||||
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
||||
if (!await AndroidPermissionManager.request(kSystemAlertWindow)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// (Optional) 3. request input permission
|
||||
}
|
||||
setState(() => _enableStartOnBoot = toValue);
|
||||
|
||||
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
||||
}));
|
||||
|
||||
return SettingsList(
|
||||
sections: [
|
||||
@@ -387,6 +462,17 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> canStartOnBoot() async {
|
||||
// start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW
|
||||
if (_hasIgnoreBattery && !_ignoreBatteryOpt) {
|
||||
return false;
|
||||
}
|
||||
if (!await AndroidPermissionManager.check(kSystemAlertWindow)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void showServerSettings(OverlayDialogManager dialogManager) async {
|
||||
|
||||
@@ -458,10 +458,8 @@ class InputModel {
|
||||
return;
|
||||
}
|
||||
evt['type'] = type;
|
||||
if (isDesktop) {
|
||||
y = y - stateGlobal.tabBarHeight - stateGlobal.windowBorderWidth.value;
|
||||
x -= stateGlobal.windowBorderWidth.value;
|
||||
}
|
||||
y -= CanvasModel.topToEdge;
|
||||
x -= CanvasModel.leftToEdge;
|
||||
final canvasModel = parent.target!.canvasModel;
|
||||
final nearThr = 3;
|
||||
var nearRight = (canvasModel.size.width - x) < nearThr;
|
||||
@@ -503,8 +501,21 @@ class InputModel {
|
||||
}
|
||||
x += d.x;
|
||||
y += d.y;
|
||||
var evtX = 0;
|
||||
var evtY = 0;
|
||||
try {
|
||||
evtX = x.round();
|
||||
evtY = y.round();
|
||||
} catch (e) {
|
||||
debugPrintStack(
|
||||
label: 'canvasModel.scale value ${canvasModel.scale}, $e');
|
||||
return;
|
||||
}
|
||||
|
||||
if (x < d.x || y < d.y || x > (d.x + d.width) || y > (d.y + d.height)) {
|
||||
if (evtX < d.x ||
|
||||
evtY < d.y ||
|
||||
evtX > (d.x + d.width) ||
|
||||
evtY > (d.y + d.height)) {
|
||||
// If left mouse up, no early return.
|
||||
if (evt['buttons'] != kPrimaryMouseButton || type != 'up') {
|
||||
return;
|
||||
@@ -512,12 +523,12 @@ class InputModel {
|
||||
}
|
||||
|
||||
if (type != '') {
|
||||
x = 0;
|
||||
y = 0;
|
||||
evtX = 0;
|
||||
evtY = 0;
|
||||
}
|
||||
|
||||
evt['x'] = '${x.round()}';
|
||||
evt['y'] = '${y.round()}';
|
||||
evt['x'] = '$evtX';
|
||||
evt['y'] = '$evtY';
|
||||
var buttons = '';
|
||||
switch (evt['buttons']) {
|
||||
case kPrimaryMouseButton:
|
||||
|
||||
@@ -617,13 +617,28 @@ class ViewStyle {
|
||||
final int displayWidth;
|
||||
final int displayHeight;
|
||||
ViewStyle({
|
||||
this.style = '',
|
||||
this.width = 0.0,
|
||||
this.height = 0.0,
|
||||
this.displayWidth = 0,
|
||||
this.displayHeight = 0,
|
||||
required this.style,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.displayWidth,
|
||||
required this.displayHeight,
|
||||
});
|
||||
|
||||
static defaultViewStyle() {
|
||||
final desktop = (isDesktop || isWebDesktop);
|
||||
final w =
|
||||
desktop ? kDesktopDefaultDisplayWidth : kMobileDefaultDisplayWidth;
|
||||
final h =
|
||||
desktop ? kDesktopDefaultDisplayHeight : kMobileDefaultDisplayHeight;
|
||||
return ViewStyle(
|
||||
style: '',
|
||||
width: w.toDouble(),
|
||||
height: h.toDouble(),
|
||||
displayWidth: w,
|
||||
displayHeight: h,
|
||||
);
|
||||
}
|
||||
|
||||
static int _double2Int(double v) => (v * 100).round().toInt();
|
||||
|
||||
@override
|
||||
@@ -652,9 +667,14 @@ class ViewStyle {
|
||||
double get scale {
|
||||
double s = 1.0;
|
||||
if (style == kRemoteViewStyleAdaptive) {
|
||||
final s1 = width / displayWidth;
|
||||
final s2 = height / displayHeight;
|
||||
s = s1 < s2 ? s1 : s2;
|
||||
if (width != 0 &&
|
||||
height != 0 &&
|
||||
displayWidth != 0 &&
|
||||
displayHeight != 0) {
|
||||
final s1 = width / displayWidth;
|
||||
final s2 = height / displayHeight;
|
||||
s = s1 < s2 ? s1 : s2;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
@@ -680,7 +700,7 @@ class CanvasModel with ChangeNotifier {
|
||||
// scroll offset y percent
|
||||
double _scrollY = 0.0;
|
||||
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
|
||||
ViewStyle _lastViewStyle = ViewStyle();
|
||||
ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle();
|
||||
|
||||
final _imageOverflow = false.obs;
|
||||
|
||||
@@ -707,12 +727,25 @@ class CanvasModel with ChangeNotifier {
|
||||
double get scrollX => _scrollX;
|
||||
double get scrollY => _scrollY;
|
||||
|
||||
static double get leftToEdge => (isDesktop || isWebDesktop)
|
||||
? windowBorderWidth + kDragToResizeAreaPadding.left
|
||||
: 0;
|
||||
static double get rightToEdge => (isDesktop || isWebDesktop)
|
||||
? windowBorderWidth + kDragToResizeAreaPadding.right
|
||||
: 0;
|
||||
static double get topToEdge => (isDesktop || isWebDesktop)
|
||||
? tabBarHeight + windowBorderWidth + kDragToResizeAreaPadding.top
|
||||
: 0;
|
||||
static double get bottomToEdge => (isDesktop || isWebDesktop)
|
||||
? windowBorderWidth + kDragToResizeAreaPadding.bottom
|
||||
: 0;
|
||||
|
||||
updateViewStyle() async {
|
||||
Size getSize() {
|
||||
final size = MediaQueryData.fromWindow(ui.window).size;
|
||||
// If minimized, w or h may be negative here.
|
||||
double w = size.width - windowBorderWidth * 2;
|
||||
double h = size.height - tabBarHeight - windowBorderWidth * 2;
|
||||
double w = size.width - leftToEdge - rightToEdge;
|
||||
double h = size.height - topToEdge - bottomToEdge;
|
||||
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
|
||||
}
|
||||
|
||||
@@ -786,10 +819,14 @@ class CanvasModel with ChangeNotifier {
|
||||
return parent.target?.ffiModel.display.height ?? defaultHeight;
|
||||
}
|
||||
|
||||
double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
|
||||
double get tabBarHeight => stateGlobal.tabBarHeight;
|
||||
static double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
|
||||
static double get tabBarHeight => stateGlobal.tabBarHeight;
|
||||
|
||||
moveDesktopMouse(double x, double y) {
|
||||
if (size.width == 0 || size.height == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On mobile platforms, move the canvas with the cursor.
|
||||
final dw = getDisplayWidth() * _scale;
|
||||
final dh = getDisplayHeight() * _scale;
|
||||
@@ -803,7 +840,9 @@ class CanvasModel with ChangeNotifier {
|
||||
dyOffset = (y - dh * (y / size.height) - _y).toInt();
|
||||
}
|
||||
} catch (e) {
|
||||
// Unhandled Exception: Unsupported operation: Infinity or NaN toInt
|
||||
debugPrintStack(
|
||||
label:
|
||||
'(x,y) ($x,$y), (_x,_y) ($_x,$_y), _scale $_scale, display size (${getDisplayWidth()},${getDisplayHeight()}), size $size, , $e');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ typedef F4Dart = int Function(Pointer<Utf8>);
|
||||
typedef F5 = Void Function(Pointer<Utf8>);
|
||||
typedef F5Dart = void Function(Pointer<Utf8>);
|
||||
typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
|
||||
// pub fn session_register_texture(id: *const char, ptr: usize)
|
||||
// pub fn session_register_texture(id: *const char, ptr: usize)
|
||||
typedef F6 = Void Function(Pointer<Utf8>, Uint64);
|
||||
typedef F6Dart = void Function(Pointer<Utf8>, int);
|
||||
|
||||
@@ -56,7 +56,6 @@ class PlatformFFI {
|
||||
F4Dart? _session_get_rgba_size;
|
||||
F5Dart? _session_next_rgba;
|
||||
F6Dart? _session_register_texture;
|
||||
|
||||
|
||||
static get localeName => Platform.localeName;
|
||||
|
||||
@@ -162,7 +161,8 @@ class PlatformFFI {
|
||||
dylib.lookupFunction<F4, F4Dart>("session_get_rgba_size");
|
||||
_session_next_rgba =
|
||||
dylib.lookupFunction<F5, F5Dart>("session_next_rgba");
|
||||
_session_register_texture = dylib.lookupFunction<F6, F6Dart>("session_register_texture");
|
||||
_session_register_texture =
|
||||
dylib.lookupFunction<F6, F6Dart>("session_register_texture");
|
||||
try {
|
||||
// SYSTEM user failed
|
||||
_dir = (await getApplicationDocumentsDirectory()).path;
|
||||
@@ -301,4 +301,8 @@ class PlatformFFI {
|
||||
if (!isAndroid) return Future<bool>(() => false);
|
||||
return await _toAndroidChannel.invokeMethod(method, arguments);
|
||||
}
|
||||
|
||||
void syncAndroidServiceAppDirConfigPath() {
|
||||
invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hbb/consts.dart';
|
||||
import 'package:flutter_hbb/main.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -154,7 +155,8 @@ class ServerModel with ChangeNotifier {
|
||||
/// file true by default (if permission on)
|
||||
checkAndroidPermission() async {
|
||||
// audio
|
||||
if (androidVersion < 30 || !await PermissionManager.check("audio")) {
|
||||
if (androidVersion < 30 ||
|
||||
!await AndroidPermissionManager.check(kRecordAudio)) {
|
||||
_audioOk = false;
|
||||
bind.mainSetOption(key: "enable-audio", value: "N");
|
||||
} else {
|
||||
@@ -163,7 +165,7 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
// file
|
||||
if (!await PermissionManager.check("file")) {
|
||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||
_fileOk = false;
|
||||
bind.mainSetOption(key: "enable-file-transfer", value: "N");
|
||||
} else {
|
||||
@@ -229,10 +231,10 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
toggleAudio() async {
|
||||
if (!_audioOk && !await PermissionManager.check("audio")) {
|
||||
final res = await PermissionManager.request("audio");
|
||||
if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) {
|
||||
final res = await AndroidPermissionManager.request(kRecordAudio);
|
||||
if (!res) {
|
||||
// TODO handle fail
|
||||
showToast(translate('Failed'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -243,10 +245,12 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
toggleFile() async {
|
||||
if (!_fileOk && !await PermissionManager.check("file")) {
|
||||
final res = await PermissionManager.request("file");
|
||||
if (!_fileOk &&
|
||||
!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||
final res =
|
||||
await AndroidPermissionManager.request(kManageExternalStorage);
|
||||
if (!res) {
|
||||
// TODO handle fail
|
||||
showToast(translate('Failed'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -344,10 +348,6 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> initInput() async {
|
||||
await parent.target?.invokeMethod("init_input");
|
||||
}
|
||||
|
||||
Future<bool> setPermanentPassword(String newPW) async {
|
||||
await bind.mainSetPermanentPassword(password: newPW);
|
||||
await Future.delayed(Duration(milliseconds: 500));
|
||||
@@ -561,7 +561,8 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> closeAll() async {
|
||||
await Future.wait(_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
|
||||
await Future.wait(
|
||||
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
|
||||
_clients.clear();
|
||||
tabController.state.value.tabs.clear();
|
||||
}
|
||||
@@ -684,7 +685,7 @@ String getLoginDialogTag(int id) {
|
||||
showInputWarnAlert(FFI ffi) {
|
||||
ffi.dialogManager.show((setState, close) {
|
||||
submit() {
|
||||
ffi.serverModel.initInput();
|
||||
AndroidPermissionManager.startAction(kActionAccessibilitySettings);
|
||||
close();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ import '../consts.dart';
|
||||
class StateGlobal {
|
||||
int _windowId = -1;
|
||||
bool _fullscreen = false;
|
||||
bool _maximize = false;
|
||||
bool grabKeyboard = false;
|
||||
final RxBool _showTabBar = true.obs;
|
||||
final RxBool _showResizeEdge = true.obs;
|
||||
final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
|
||||
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
||||
final RxBool showRemoteMenuBar = false.obs;
|
||||
@@ -18,12 +20,20 @@ class StateGlobal {
|
||||
|
||||
int get windowId => _windowId;
|
||||
bool get fullscreen => _fullscreen;
|
||||
bool get maximize => _maximize;
|
||||
double get tabBarHeight => fullscreen ? 0 : kDesktopRemoteTabBarHeight;
|
||||
RxBool get showTabBar => _showTabBar;
|
||||
RxDouble get resizeEdgeSize => _resizeEdgeSize;
|
||||
RxDouble get windowBorderWidth => _windowBorderWidth;
|
||||
|
||||
setWindowId(int id) => _windowId = id;
|
||||
setMaximize(bool v) {
|
||||
if (_maximize != v) {
|
||||
_maximize = v;
|
||||
_resizeEdgeSize.value =
|
||||
_maximize ? kMaximizeEdgeSize : kWindowEdgeSize;
|
||||
}
|
||||
}
|
||||
setFullscreen(bool v) {
|
||||
if (_fullscreen != v) {
|
||||
_fullscreen = v;
|
||||
|
||||
@@ -487,7 +487,7 @@
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
|
||||
@@ -325,8 +325,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a
|
||||
resolved-ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a
|
||||
ref: "3e2655677c54f421f9e378680d8171b95a211e0f"
|
||||
resolved-ref: "3e2655677c54f421f9e378680d8171b95a211e0f"
|
||||
url: "https://github.com/Kingtous/rustdesk_desktop_multi_window"
|
||||
source: git
|
||||
version: "0.1.0"
|
||||
@@ -1563,5 +1563,5 @@ packages:
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
sdks:
|
||||
dart: ">=2.18.0 <4.0.0"
|
||||
dart: ">=2.18.0 <3.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
|
||||
@@ -59,7 +59,7 @@ dependencies:
|
||||
desktop_multi_window:
|
||||
git:
|
||||
url: https://github.com/Kingtous/rustdesk_desktop_multi_window
|
||||
ref: f37357ed98a10717576eb9ed8413e92b2ec5d13a
|
||||
ref: 3e2655677c54f421f9e378680d8171b95a211e0f
|
||||
freezed_annotation: ^2.0.3
|
||||
flutter_custom_cursor: ^0.0.4
|
||||
window_size:
|
||||
@@ -76,7 +76,7 @@ dependencies:
|
||||
file_picker: ^5.1.0
|
||||
flutter_svg: ^1.1.5
|
||||
flutter_improved_scrolling:
|
||||
# currently, we use flutter 3.0.5 for windows build, latest for other builds.
|
||||
# currently, we use flutter 3.7.0+.
|
||||
#
|
||||
# for flutter 3.0.5, please use official version(just comment code below).
|
||||
# if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below).
|
||||
|
||||
Reference in New Issue
Block a user