From b6196130fb040e96020b6f0779ec85610a9072c6 Mon Sep 17 00:00:00 2001 From: Yiqun Zhang Date: Mon, 24 Jun 2024 13:35:49 +0800 Subject: [PATCH] :lipstick: Optimize mac permission request UI (#1286) --- .../kotlin/com/clipevery/ClipeveryApp.kt | 3 - .../com/clipevery/listener/GlobalListener.kt | 6 - .../ui/base/ComposeMessageViewFactory.kt | 11 - .../com/clipevery/ui/base/MessageManager.kt | 10 - .../com/clipevery/ui/base/MessageView.kt | 56 ---- .../kotlin/com/clipevery/Clipevery.kt | 5 +- .../clipevery/listen/DesktopGlobalListener.kt | 257 ++---------------- .../com/clipevery/os/macos/api/MacosApi.kt | 2 + .../ui/base/DesktopMessageManager.kt | 26 -- .../clipevery/ui/base/MacAcessibilityView.kt | 196 +++++++++++++ .../desktopMain/resources/i18n/en.properties | 5 +- .../desktopMain/resources/i18n/es.properties | 5 +- .../desktopMain/resources/i18n/jp.properties | 5 +- .../desktopMain/resources/i18n/zh.properties | 5 +- .../src/desktopMain/swift/MacosApi.swift | 11 + 15 files changed, 255 insertions(+), 348 deletions(-) delete mode 100644 composeApp/src/commonMain/kotlin/com/clipevery/ui/base/ComposeMessageViewFactory.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/clipevery/ui/base/MessageManager.kt delete mode 100644 composeApp/src/commonMain/kotlin/com/clipevery/ui/base/MessageView.kt delete mode 100644 composeApp/src/desktopMain/kotlin/com/clipevery/ui/base/DesktopMessageManager.kt create mode 100644 composeApp/src/desktopMain/kotlin/com/clipevery/ui/base/MacAcessibilityView.kt diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/ClipeveryApp.kt b/composeApp/src/commonMain/kotlin/com/clipevery/ClipeveryApp.kt index 83fc0bf13..f476ab3cd 100644 --- a/composeApp/src/commonMain/kotlin/com/clipevery/ClipeveryApp.kt +++ b/composeApp/src/commonMain/kotlin/com/clipevery/ClipeveryApp.kt @@ -27,7 +27,6 @@ import com.clipevery.ui.ClipeveryTheme import com.clipevery.ui.HomeView import com.clipevery.ui.PageViewType import com.clipevery.ui.base.DialogService -import com.clipevery.ui.base.MessageView import com.clipevery.ui.base.ToastManager import com.clipevery.ui.base.ToastView import com.clipevery.ui.devices.DeviceDetailView @@ -106,8 +105,6 @@ fun ClipeveryWindow(hideWindow: suspend () -> Unit) { ClipeveryContent() } - MessageView() - toast?.let { ToastView(toast = it) { toastManager.cancel() diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/listener/GlobalListener.kt b/composeApp/src/commonMain/kotlin/com/clipevery/listener/GlobalListener.kt index 73acba40e..79a893fef 100644 --- a/composeApp/src/commonMain/kotlin/com/clipevery/listener/GlobalListener.kt +++ b/composeApp/src/commonMain/kotlin/com/clipevery/listener/GlobalListener.kt @@ -1,16 +1,10 @@ package com.clipevery.listener -import com.clipevery.ui.base.ComposeMessageViewFactory - interface GlobalListener { - var errorCode: Int? - fun isRegistered(): Boolean fun start() fun stop() - - fun getComposeMessageViewFactory(): ComposeMessageViewFactory } diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/ui/base/ComposeMessageViewFactory.kt b/composeApp/src/commonMain/kotlin/com/clipevery/ui/base/ComposeMessageViewFactory.kt deleted file mode 100644 index 2d8323741..000000000 --- a/composeApp/src/commonMain/kotlin/com/clipevery/ui/base/ComposeMessageViewFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.clipevery.ui.base - -import androidx.compose.runtime.Composable - -interface ComposeMessageViewFactory { - - var showMessage: Boolean - - @Composable - fun MessageView(key: Any) -} diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/ui/base/MessageManager.kt b/composeApp/src/commonMain/kotlin/com/clipevery/ui/base/MessageManager.kt deleted file mode 100644 index 446a23f3e..000000000 --- a/composeApp/src/commonMain/kotlin/com/clipevery/ui/base/MessageManager.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.clipevery.ui.base - -import androidx.compose.runtime.Composable - -interface MessageManager { - - var messageId: Int - - fun getCurrentMessageView(): (@Composable () -> Unit)? -} diff --git a/composeApp/src/commonMain/kotlin/com/clipevery/ui/base/MessageView.kt b/composeApp/src/commonMain/kotlin/com/clipevery/ui/base/MessageView.kt deleted file mode 100644 index e5ca71277..000000000 --- a/composeApp/src/commonMain/kotlin/com/clipevery/ui/base/MessageView.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.clipevery.ui.base - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.unit.dp -import com.clipevery.LocalKoinApplication - -@Composable -fun MessageView() { - val current = LocalKoinApplication.current - val messageManager = current.koin.get() - - messageManager.getCurrentMessageView()?.let { messageView -> - Box( - modifier = - Modifier.fillMaxSize() - .background(Color.Black.copy(alpha = 0.5f)) - .clip(RoundedCornerShape(10.dp)) - .pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - awaitPointerEvent() - } - } - }, - contentAlignment = Alignment.Center, - ) { - Box(modifier = Modifier.shadow(5.dp, RoundedCornerShape(5.dp))) { - Column( - modifier = - Modifier.width(320.dp) - .wrapContentHeight() - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colors.surface) - .padding(10.dp), - ) { - messageView() - } - } - } - } -} diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/Clipevery.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/Clipevery.kt index e2b2340de..59db1a574 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/Clipevery.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/Clipevery.kt @@ -137,13 +137,11 @@ import com.clipevery.ui.ThemeDetector import com.clipevery.ui.WindowsTray import com.clipevery.ui.base.DesktopDialogService import com.clipevery.ui.base.DesktopIconStyle -import com.clipevery.ui.base.DesktopMessageManager import com.clipevery.ui.base.DesktopNotificationManager import com.clipevery.ui.base.DesktopToastManager import com.clipevery.ui.base.DesktopUISupport import com.clipevery.ui.base.DialogService import com.clipevery.ui.base.IconStyle -import com.clipevery.ui.base.MessageManager import com.clipevery.ui.base.NotificationManager import com.clipevery.ui.base.ToastManager import com.clipevery.ui.base.UISupport @@ -303,12 +301,11 @@ class Clipevery { single { DesktopMouseListener } single { get() } single { get() } - single { DesktopGlobalListener(get(), get()) } + single { DesktopGlobalListener(get(), get(), get(), get(), get()) } single { DesktopThemeDetector(get()) } single { DesktopAbsoluteClipResourceLoader } single { DesktopToastManager() } single { DesktopNotificationManager } - single { DesktopMessageManager(get()) } single { DesktopIconStyle } single { DesktopUISupport(get(), get()) } single { DesktopShortcutKeys(get(), get()) } diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/listen/DesktopGlobalListener.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/listen/DesktopGlobalListener.kt index f49520293..eec1fb269 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/listen/DesktopGlobalListener.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/listen/DesktopGlobalListener.kt @@ -1,70 +1,34 @@ package com.clipevery.listen -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.clipevery.LocalExitApplication -import com.clipevery.LocalKoinApplication -import com.clipevery.app.AppRestartService -import com.clipevery.app.AppWindowManager import com.clipevery.i18n.GlobalCopywriter import com.clipevery.listener.GlobalListener -import com.clipevery.ui.base.ComposeMessageViewFactory +import com.clipevery.ui.base.ClipDialog +import com.clipevery.ui.base.DialogService +import com.clipevery.ui.base.MacAcessibilityView import com.clipevery.ui.base.MessageType +import com.clipevery.ui.base.Toast +import com.clipevery.ui.base.ToastManager import com.clipevery.utils.getSystemProperty import com.github.kwhat.jnativehook.GlobalScreen import com.github.kwhat.jnativehook.NativeHookException +import com.github.kwhat.jnativehook.NativeHookException.DARWIN_AXAPI_DISABLED import com.github.kwhat.jnativehook.keyboard.NativeKeyListener import com.github.kwhat.jnativehook.mouse.NativeMouseListener import io.github.oshai.kotlinlogging.KotlinLogging -import kotlinx.coroutines.delay -import java.awt.Desktop -import java.net.URI val logger = KotlinLogging.logger {} class DesktopGlobalListener( private val shortcutKeysListener: NativeKeyListener, private val mouseListener: NativeMouseListener, + private val dialogService: DialogService, + private val toastManager: ToastManager, + private val copywriter: GlobalCopywriter, ) : GlobalListener { private val systemProperty = getSystemProperty() - override var errorCode: Int? by mutableStateOf(null) - - private val messageFactory = GlobalListenerMessageViewFactory() - override fun isRegistered(): Boolean { return GlobalScreen.isNativeHookRegistered() } @@ -78,7 +42,26 @@ class DesktopGlobalListener( GlobalScreen.addNativeMouseListener(mouseListener) } } catch (e: NativeHookException) { - errorCode = e.code + if (e.code == DARWIN_AXAPI_DISABLED) { + dialogService.pushDialog( + ClipDialog( + key = e.code, + title = "Global_Shortcut_Activation_Failed", + width = 320.dp, + ) { + MacAcessibilityView() + }, + ) + } else { + toastManager.setToast( + Toast( + messageType = MessageType.Error, + message = + "${copywriter.getText("Failed_to_register_keyboard_listener")}. " + + "${copywriter.getText("Error_Code")} ${e.code}", + ), + ) + } logger.error(e) { "There was a problem registering the native hook" } } } @@ -97,186 +80,4 @@ class DesktopGlobalListener( } } } - - override fun getComposeMessageViewFactory(): ComposeMessageViewFactory { - return messageFactory - } -} - -private class GlobalListenerMessageViewFactory : ComposeMessageViewFactory { - - override var showMessage: Boolean by mutableStateOf(true) - - private var jumpPrivacyAccessibility: Boolean by mutableStateOf(false) - - @Composable - override fun MessageView(key: Any) { - if (key == NativeHookException.DARWIN_AXAPI_DISABLED) { - GlobalShortcutActivationFailedMessageView() - } - } - - private fun jumpPrivacyAccessibility() { - if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { - Desktop.getDesktop() - .browse(URI("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")) - } - } - - @Composable - fun GlobalShortcutActivationFailedMessageView() { - val current = LocalKoinApplication.current - val appWindowManager = current.koin.get() - val exitApplication = LocalExitApplication.current - val globalListener = current.koin.get() - val copywriter = current.koin.get() - val appRestartService = current.koin.get() - - val messageStyle = MessageType.Error.getMessageStyle() - - LaunchedEffect(appWindowManager.showMainWindow) { - if (appWindowManager.showMainWindow) { - if (globalListener.isRegistered()) { - showMessage = false - } else { - globalListener.errorCode?.let { code -> - if (code == NativeHookException.DARWIN_AXAPI_DISABLED && !jumpPrivacyAccessibility) { - delay(8000) // wait to read the message - jumpPrivacyAccessibility() - jumpPrivacyAccessibility = true - } - } - } - } - } - - Row( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = copywriter.getText("Global_Shortcut_Activation_Failed"), - modifier = Modifier.wrapContentWidth(), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = messageStyle.messageColor, - style = - TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - fontFamily = FontFamily.SansSerif, - ), - ) - Spacer(modifier = Modifier.weight(1f)) - - Icon( - modifier = - Modifier.clickable { - showMessage = false - }, - painter = painterResource("icon/toast/close.svg"), - contentDescription = "Cancel", - tint = messageStyle.messageColor, - ) - } - Spacer(modifier = Modifier.height(8.dp)) - Row( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.Top, - ) { - val content = copywriter.getText("Global_Shortcut_Activation_Failed_Content") - - val click = copywriter.getText("Global_Shortcut_Activation_Failed_Click") - - val index = content.indexOf(click) - - val annotatedText = - buildAnnotatedString { - withStyle( - style = - SpanStyle( - color = MaterialTheme.colors.onBackground, - fontSize = 14.sp, - fontWeight = FontWeight.Light, - ), - ) { - append(content.substring(0, index)) - } - pushStringAnnotation(tag = "clickable", annotation = "click_here") - withStyle( - style = - SpanStyle( - color = MaterialTheme.colors.primary, - fontSize = 14.sp, - fontWeight = FontWeight.Light, - ), - ) { - append(click) - } - pop() - withStyle( - style = - SpanStyle( - color = MaterialTheme.colors.primary, - fontSize = 14.sp, - fontWeight = FontWeight.Light, - ), - ) { - append(content.substring(index + click.length)) - } - } - - ClickableText( - text = annotatedText, - onClick = { offset -> - annotatedText.getStringAnnotations(tag = "clickable", start = offset, end = offset) - .firstOrNull()?.let { - if (it.item == "click_here") { - jumpPrivacyAccessibility() - } - } - }, - ) - } - - if (jumpPrivacyAccessibility) { - Spacer(modifier = Modifier.height(8.dp)) - - Row( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - Button( - modifier = Modifier.wrapContentWidth().height(28.dp), - border = BorderStroke(1.dp, Color(0xFFAFCBE1)), - colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.primary), - contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp), - elevation = - ButtonDefaults.elevation( - defaultElevation = 0.dp, - pressedElevation = 0.dp, - hoveredElevation = 0.dp, - focusedElevation = 0.dp, - ), - onClick = { - appRestartService.restart { exitApplication() } - }, - ) { - Text( - text = copywriter.getText("Restart_Application"), - style = - TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Light, - fontFamily = FontFamily.SansSerif, - ), - color = MaterialTheme.colors.onBackground, - ) - } - } - } - } } diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/os/macos/api/MacosApi.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/os/macos/api/MacosApi.kt index 2c0d1aebd..5f069aed6 100644 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/os/macos/api/MacosApi.kt +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/os/macos/api/MacosApi.kt @@ -62,6 +62,8 @@ interface MacosApi : Library { count: Int, ) + fun checkAccessibilityPermissions(): Boolean + companion object { val INSTANCE: MacosApi = Native.load("MacosApi", MacosApi::class.java) } diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/ui/base/DesktopMessageManager.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/ui/base/DesktopMessageManager.kt deleted file mode 100644 index 2dc391931..000000000 --- a/composeApp/src/desktopMain/kotlin/com/clipevery/ui/base/DesktopMessageManager.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.clipevery.ui.base - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import com.clipevery.listener.GlobalListener - -class DesktopMessageManager( - private val globalListener: GlobalListener, -) : MessageManager { - - override var messageId by mutableStateOf(0) - - override fun getCurrentMessageView(): (@Composable () -> Unit)? { - globalListener.errorCode?.let { code -> - val factory = globalListener.getComposeMessageViewFactory() - if (factory.showMessage) { - return { - factory.MessageView(code) - } - } - } - return null - } -} diff --git a/composeApp/src/desktopMain/kotlin/com/clipevery/ui/base/MacAcessibilityView.kt b/composeApp/src/desktopMain/kotlin/com/clipevery/ui/base/MacAcessibilityView.kt new file mode 100644 index 000000000..c553a237f --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/clipevery/ui/base/MacAcessibilityView.kt @@ -0,0 +1,196 @@ +package com.clipevery.ui.base + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.clipevery.LocalExitApplication +import com.clipevery.LocalKoinApplication +import com.clipevery.app.AppRestartService +import com.clipevery.i18n.GlobalCopywriter +import com.clipevery.os.macos.api.MacosApi +import kotlinx.coroutines.delay +import java.awt.Desktop +import java.net.URI + +@Composable +fun MacAcessibilityView() { + val current = LocalKoinApplication.current + val exitApplication = LocalExitApplication.current + val copywriter = current.koin.get() + val appRestartService = current.koin.get() + + var toRestart by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + while (true) { + val checkAccessibilityPermissions = MacosApi.INSTANCE.checkAccessibilityPermissions() + if (checkAccessibilityPermissions) { + toRestart = true + break + } else { + delay(500) + } + } + } + + Row( + modifier = + Modifier.fillMaxWidth() + .wrapContentHeight() + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (!toRestart) { + Row( + modifier = + Modifier.fillMaxWidth() + .wrapContentHeight(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + val content = copywriter.getText("Global_Shortcut_Activation_Failed_Content") + + val click = copywriter.getText("Global_Shortcut_Activation_Failed_Click") + + val index = content.indexOf(click) + + val annotatedText = + buildAnnotatedString { + withStyle( + style = + SpanStyle( + color = MaterialTheme.colors.onBackground, + fontSize = 14.sp, + fontWeight = FontWeight.Light, + ), + ) { + append(content.substring(0, index)) + } + pushStringAnnotation(tag = "clickable", annotation = "click_here") + withStyle( + style = + SpanStyle( + color = MaterialTheme.colors.primary, + fontSize = 14.sp, + fontWeight = FontWeight.Light, + ), + ) { + append(click) + } + pop() + withStyle( + style = + SpanStyle( + color = MaterialTheme.colors.onBackground, + fontSize = 14.sp, + fontWeight = FontWeight.Light, + ), + ) { + append(content.substring(index + click.length)) + } + } + + ClickableText( + text = annotatedText, + onClick = { offset -> + annotatedText.getStringAnnotations(tag = "clickable", start = offset, end = offset) + .firstOrNull()?.let { + if (it.item == "click_here") { + jumpPrivacyAccessibility() + } + } + }, + ) + } + } else { + Column( + modifier = + Modifier.fillMaxWidth() + .wrapContentHeight(), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = copywriter.getText("Restart_Application_Tip"), + color = MaterialTheme.colors.onBackground, + style = + TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Light, + fontFamily = FontFamily.SansSerif, + ), + ) + Spacer(modifier = Modifier.height(6.dp)) + Row( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Button( + modifier = Modifier.wrapContentWidth().height(28.dp), + border = BorderStroke(1.dp, Color(0xFFAFCBE1)), + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.primary), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp), + elevation = + ButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + hoveredElevation = 0.dp, + focusedElevation = 0.dp, + ), + onClick = { + appRestartService.restart { exitApplication() } + }, + ) { + Text( + text = copywriter.getText("Restart_Application"), + style = + TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Light, + fontFamily = FontFamily.SansSerif, + ), + color = Color.White, + ) + } + } + } + } + } +} + +private fun jumpPrivacyAccessibility() { + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop() + .browse(URI("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")) + } +} diff --git a/composeApp/src/desktopMain/resources/i18n/en.properties b/composeApp/src/desktopMain/resources/i18n/en.properties index ffaefe4d2..f861f3403 100644 --- a/composeApp/src/desktopMain/resources/i18n/en.properties +++ b/composeApp/src/desktopMain/resources/i18n/en.properties @@ -43,12 +43,14 @@ Do_you_trust_this_device?=Do you trust this device? Edit_Shortcut_Key=Edit Shortcut Key Empty=Empty Encrypted_sync=Encrypted sync +Error_Code=Error Code Expiration_Cleanup=Expiration Cleanup FQA=FQA↗ Failed_to_browse_File_pasteboard=Failed to browse File pasteboard Failed_to_open_Html_pasteboard=Failed to open Html pasteboard Failed_to_open_Image_pasteboard=Failed to open Image pasteboard Failed_to_open_Text_pasteboard=Failed to open Text pasteboard +Failed_to_register_keyboard_listener=Failed to register keyboard listener Favorite=Favorite Favorite_Storage=Favorite Storage Feedback=Feedback @@ -96,7 +98,8 @@ Port=Port Quit=Quit Remote=Remote Remove_Device=Remove Device -Restart_Application=If authorized, please click to restart the application +Restart_Application_Tip=If authorized, please click to restart the application +Restart_Application=Restart Application Return=Return Scan=Scan Scroll_to_top=Scroll to top diff --git a/composeApp/src/desktopMain/resources/i18n/es.properties b/composeApp/src/desktopMain/resources/i18n/es.properties index e824af997..84f3b337e 100644 --- a/composeApp/src/desktopMain/resources/i18n/es.properties +++ b/composeApp/src/desktopMain/resources/i18n/es.properties @@ -44,12 +44,14 @@ Do_you_trust_this_device?=Confía en este dispositivo? Edit_Shortcut_Key=Editar tecla de acceso rápido Empty=Vacío Encrypted_sync=Sincronización cifrada +Error_Code=Código de error Expiration_Cleanup=Limpieza de expiración FQA=Preguntas frecuentes↗ Failed_to_browse_File_pasteboard=Error al navegar por el portapapeles de archivo Failed_to_open_Html_pasteboard=Error al abrir el portapapeles Html Failed_to_open_Image_pasteboard=Error al abrir el portapapeles de imagen Failed_to_open_Text_pasteboard=Error al abrir el portapapeles de texto +Failed_to_register_keyboard_listener=Error al registrar el escuchador de teclado Favorite=Favorito Favorite_Storage=Almacenamiento favorito Feedback=Comentarios @@ -97,7 +99,8 @@ Port=Puerto Quit=Salir Remote=Remoto Remove_Device=Eliminar dispositivo -Restart_Application=Si está autorizado, haga clic para reiniciar la aplicación. +Restart_Application_Tip=Si está autorizado, haga clic para reiniciar la aplicación. +Restart_Application=Reiniciar aplicación Return=Volver Scan=Escanear Scroll_to_top=Desplazarse hacia arriba diff --git a/composeApp/src/desktopMain/resources/i18n/jp.properties b/composeApp/src/desktopMain/resources/i18n/jp.properties index efd753ea8..4e94c54aa 100644 --- a/composeApp/src/desktopMain/resources/i18n/jp.properties +++ b/composeApp/src/desktopMain/resources/i18n/jp.properties @@ -43,12 +43,14 @@ Do_you_trust_this_device?=このデバイスを信頼しますか? Edit_Shortcut_Key=ショートカットキーを編集 Empty=空 Encrypted_sync=暗号化同期 +Error_Code=エラーコード Expiration_Cleanup=有効期限クリーンアップ FQA=よくある問題↗ Failed_to_browse_File_pasteboard=ファイルペーストボードをブラウズできませんでした Failed_to_open_Html_pasteboard=Html ペーストボードを開けませんでした Failed_to_open_Image_pasteboard=画像ペーストボードを開けませんでした Failed_to_open_Text_pasteboard=テキストペーストボードを開けませんでした +Failed_to_register_keyboard_listener=キーボードリスナーの登録に失敗しました Favorite=お気に入り Favorite_Storage=お気に入りのストレージ Feedback=フィードバック @@ -96,7 +98,8 @@ Port=ポート Quit=終了 Remote=リモート Remove_Device=デバイスを削除 -Restart_Application=許可されている場合は、クリックしてアプリケーションを再起動してください +Restart_Application_Tip=許可されている場合は、クリックしてアプリケーションを再起動してください +Restart_Application=アプリケーションを再起動 Return=戻る Scan=スキャン Scroll_to_top=トップにスクロール diff --git a/composeApp/src/desktopMain/resources/i18n/zh.properties b/composeApp/src/desktopMain/resources/i18n/zh.properties index e5fc4285b..a7d44bfdc 100644 --- a/composeApp/src/desktopMain/resources/i18n/zh.properties +++ b/composeApp/src/desktopMain/resources/i18n/zh.properties @@ -43,12 +43,14 @@ Do_you_trust_this_device?=您信任此设备吗? Edit_Shortcut_Key=编辑快捷键 Empty=空 Encrypted_sync=加密同步 +Error_Code=错误码 Expiration_Cleanup=过期清理 FQA=常见问题↗ Failed_to_browse_File_pasteboard=无法浏览文件剪贴板 Failed_to_open_Html_pasteboard=无法打开 Html 剪贴板 Failed_to_open_Image_pasteboard=无法打开图片剪贴板 Failed_to_open_Text_pasteboard=无法打开文本剪贴板 +Failed_to_register_keyboard_listener=无法注册键盘监听器 Favorite=收藏 Favorite_Storage=收藏存储 Feedback=问题反馈 @@ -96,7 +98,8 @@ Port=端口 Quit=退出 Remote=远程 Remove_Device=移除设备 -Restart_Application=如果已授权,请点击重启应用 +Restart_Application_Tip=如果已授权,请点击重启应用 +Restart_Application=重启应用 Return=返回 Scan=扫描 Scroll_to_top=滚动到顶部 diff --git a/composeApp/src/desktopMain/swift/MacosApi.swift b/composeApp/src/desktopMain/swift/MacosApi.swift index 9ee6bfd42..20688cd48 100644 --- a/composeApp/src/desktopMain/swift/MacosApi.swift +++ b/composeApp/src/desktopMain/swift/MacosApi.swift @@ -1,4 +1,5 @@ import AppKit +import ApplicationServices import Cocoa import Security @@ -282,3 +283,13 @@ public func simulatePasteCommand(keyCodesPointer: UnsafePointer, count: I } } } + +@_cdecl("checkAccessibilityPermissions") +public func checkAccessibilityPermissions() -> Bool { + let checkOptionPrompt = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String + let options = [checkOptionPrompt: true] as CFDictionary + let accessEnabled = AXIsProcessTrustedWithOptions(options) + + return accessEnabled +} +