feat(aiturk): add the connect-to-computer screen (QR scan, code, deep link)
Landing now leads with QR / 8-character code pairing; manual login stays as the secondary action. aiturk://pair links open the same screen and ask for confirmation first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8ppceaR5s6cvMnm7uGRnf
This commit is contained in:
1 parent
c2b08ff73e
commit
6f4309c746
11 files changed
+721
No files matched your search
@@ -12,6 +12,9 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<!-- AITURK IDE pairing: QR scan. Requested at runtime; typing the code works without it. -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
@@ -38,8 +41,23 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- AITURK IDE pairing link: aiturk://pair?v=1&u=...&fp=...&c=... -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="aiturk" android:host="pair" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.aiturkpair.AiturkQrCaptureActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:screenOrientation="fullSensor"
|
||||||
|
android:stateNotNeeded="true"
|
||||||
|
android:theme="@style/zxing_CaptureTheme"
|
||||||
|
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".assistant.AssistantService"
|
android:name=".assistant.AssistantService"
|
||||||
android:permission="android.permission.BIND_VOICE_INTERACTION"
|
android:permission="android.permission.BIND_VOICE_INTERACTION"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import com.m57.hermescontrol.data.ws.HermesWsClient
|
|||||||
import com.m57.hermescontrol.notification.NotificationHelper
|
import com.m57.hermescontrol.notification.NotificationHelper
|
||||||
import com.m57.hermescontrol.notification.NotificationReplyReceiver
|
import com.m57.hermescontrol.notification.NotificationReplyReceiver
|
||||||
import com.m57.hermescontrol.theme.HermesControlTheme
|
import com.m57.hermescontrol.theme.HermesControlTheme
|
||||||
|
import com.m57.hermescontrol.ui.aiturkpair.AiturkPairDeepLink
|
||||||
import com.m57.hermescontrol.ui.common.ErrorState
|
import com.m57.hermescontrol.ui.common.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
import com.m57.hermescontrol.util.LocaleContextWrapper
|
import com.m57.hermescontrol.util.LocaleContextWrapper
|
||||||
@@ -62,6 +63,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
consumeNotificationIntent(intent)
|
consumeNotificationIntent(intent)
|
||||||
|
AiturkPairDeepLink.offer(intent)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
@@ -129,6 +131,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
setIntent(intent)
|
setIntent(intent)
|
||||||
consumeNotificationIntent(intent)
|
consumeNotificationIntent(intent)
|
||||||
|
AiturkPairDeepLink.offer(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import com.m57.hermescontrol.data.local.AuthManager
|
|||||||
import com.m57.hermescontrol.data.ws.ConnectionStatus
|
import com.m57.hermescontrol.data.ws.ConnectionStatus
|
||||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||||
|
import com.m57.hermescontrol.ui.aiturkpair.AiturkPairDeepLinkEffect
|
||||||
import com.m57.hermescontrol.ui.common.DisableDrawerGestures
|
import com.m57.hermescontrol.ui.common.DisableDrawerGestures
|
||||||
import com.m57.hermescontrol.ui.common.DrawerGestureController
|
import com.m57.hermescontrol.ui.common.DrawerGestureController
|
||||||
import com.m57.hermescontrol.ui.common.LocalDrawerGestureController
|
import com.m57.hermescontrol.ui.common.LocalDrawerGestureController
|
||||||
@@ -66,6 +67,7 @@ import com.m57.hermescontrol.ui.settings.SettingsVaultPage
|
|||||||
import com.m57.hermescontrol.ui.settings.SettingsViewModel
|
import com.m57.hermescontrol.ui.settings.SettingsViewModel
|
||||||
import com.m57.hermescontrol.ui.toolsets.ToolsetDetailScreen
|
import com.m57.hermescontrol.ui.toolsets.ToolsetDetailScreen
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.m57.hermescontrol.ui.aiturkpair.AiturkPairScreen as AiturkPairScreenContent
|
||||||
import com.m57.hermescontrol.ui.authlogin.AuthLoginScreen as AuthLoginScreenContent
|
import com.m57.hermescontrol.ui.authlogin.AuthLoginScreen as AuthLoginScreenContent
|
||||||
import com.m57.hermescontrol.ui.landing.LandingScreen as LandingScreenContent
|
import com.m57.hermescontrol.ui.landing.LandingScreen as LandingScreenContent
|
||||||
|
|
||||||
@@ -79,6 +81,9 @@ private fun appEntryProvider(
|
|||||||
onAuthLogin = {
|
onAuthLogin = {
|
||||||
NavigationController.navigateTo(AuthLoginScreen)
|
NavigationController.navigateTo(AuthLoginScreen)
|
||||||
},
|
},
|
||||||
|
onAiturkPair = {
|
||||||
|
NavigationController.navigateTo(AiturkPairScreen)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
// Landing doesn't use HermesScaffold — opt out of drawer gestures explicitly (issue #619).
|
// Landing doesn't use HermesScaffold — opt out of drawer gestures explicitly (issue #619).
|
||||||
DisableDrawerGestures()
|
DisableDrawerGestures()
|
||||||
@@ -96,6 +101,18 @@ private fun appEntryProvider(
|
|||||||
DisableDrawerGestures()
|
DisableDrawerGestures()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry<AiturkPairScreen> {
|
||||||
|
AiturkPairScreenContent(
|
||||||
|
onConnected = {
|
||||||
|
NavigationController.resetTo(ChatScreen)
|
||||||
|
},
|
||||||
|
onBack = {
|
||||||
|
NavigationController.goBack()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
DisableDrawerGestures()
|
||||||
|
}
|
||||||
|
|
||||||
ScreenRegistry.ALL_SCREENS.forEach { screen ->
|
ScreenRegistry.ALL_SCREENS.forEach { screen ->
|
||||||
addEntryProvider(clazz = screen.key::class) {
|
addEntryProvider(clazz = screen.key::class) {
|
||||||
screen.content(sessionId, openDrawer)
|
screen.content(sessionId, openDrawer)
|
||||||
@@ -193,6 +210,7 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
|
|
||||||
val backStack = remember(startScreen) { NavBackStack(startScreen) }
|
val backStack = remember(startScreen) { NavBackStack(startScreen) }
|
||||||
NavigationController.backStack = backStack
|
NavigationController.backStack = backStack
|
||||||
|
AiturkPairDeepLinkEffect()
|
||||||
|
|
||||||
val currentScreen = backStack.lastOrNull() ?: startScreen
|
val currentScreen = backStack.lastOrNull() ?: startScreen
|
||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable data object AuthLoginScreen : NavKey
|
@Serializable data object AuthLoginScreen : NavKey
|
||||||
|
|
||||||
|
@Serializable data object AiturkPairScreen : NavKey
|
||||||
|
|
||||||
@Serializable data object ChatScreen : NavKey
|
@Serializable data object ChatScreen : NavKey
|
||||||
|
|
||||||
@Serializable data object SettingsScreen : NavKey
|
@Serializable data object SettingsScreen : NavKey
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.m57.hermescontrol.ui.aiturkpair
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import com.m57.hermescontrol.AiturkPairScreen
|
||||||
|
import com.m57.hermescontrol.NavigationController
|
||||||
|
import com.m57.hermescontrol.data.aiturk.AiturkPairing
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hands an `aiturk://pair?...` VIEW intent to the pairing screen. The link is
|
||||||
|
* only parked here; the screen asks the user to confirm before connecting,
|
||||||
|
* because any app or web page can fire this intent.
|
||||||
|
*/
|
||||||
|
object AiturkPairDeepLink {
|
||||||
|
private val _pending = MutableStateFlow<String?>(null)
|
||||||
|
val pending: StateFlow<String?> = _pending.asStateFlow()
|
||||||
|
|
||||||
|
/** @return true when [intent] was an AITURK pairing link. */
|
||||||
|
fun offer(intent: Intent?): Boolean {
|
||||||
|
if (intent?.action != Intent.ACTION_VIEW) return false
|
||||||
|
val raw = intent.dataString
|
||||||
|
if (!AiturkPairing.looksLikePairingLink(raw)) return false
|
||||||
|
// Consume it so a recreated activity does not replay the same link.
|
||||||
|
intent.data = null
|
||||||
|
_pending.value = raw
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun take(): String? = _pending.value.also { _pending.value = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opens the pairing screen whenever a deep link is waiting. */
|
||||||
|
@Composable
|
||||||
|
fun AiturkPairDeepLinkEffect() {
|
||||||
|
val pending by AiturkPairDeepLink.pending.collectAsState()
|
||||||
|
LaunchedEffect(pending) {
|
||||||
|
if (pending != null) NavigationController.navigateTo(AiturkPairScreen)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
package com.m57.hermescontrol.ui.aiturkpair
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Computer
|
||||||
|
import androidx.compose.material.icons.filled.QrCodeScanner
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.OffsetMapping
|
||||||
|
import androidx.compose.ui.text.input.TransformedText
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.data.aiturk.AiturkPairing
|
||||||
|
import com.m57.hermescontrol.data.remote.requiresLocalNetworkPermission
|
||||||
|
|
||||||
|
/** Any RFC1918 address: pairing targets are always private IPv4, so the answer is the same. */
|
||||||
|
private const val LAN_PROBE_URL = "https://192.168.0.1/"
|
||||||
|
private const val CODE_GROUP = 4
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AiturkPairScreen(
|
||||||
|
onConnected: () -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: AiturkPairViewModel = viewModel { AiturkPairViewModel() },
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val scanPrompt = stringResource(R.string.aiturk_pair_scan_prompt)
|
||||||
|
val scanLauncher =
|
||||||
|
rememberLauncherForActivityResult(ScanContract()) { result ->
|
||||||
|
viewModel.onQrScanned(result.contents, lanPermissionGranted(context))
|
||||||
|
}
|
||||||
|
val launchScanner = {
|
||||||
|
scanLauncher.launch(
|
||||||
|
ScanOptions()
|
||||||
|
.setDesiredBarcodeFormats(ScanOptions.QR_CODE)
|
||||||
|
.setCaptureActivity(AiturkQrCaptureActivity::class.java)
|
||||||
|
.setPrompt(scanPrompt)
|
||||||
|
.setBeepEnabled(false)
|
||||||
|
.setBarcodeImageEnabled(false)
|
||||||
|
.setOrientationLocked(true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val cameraPermissionLauncher =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
|
if (granted) launchScanner() else viewModel.onCameraPermissionDenied()
|
||||||
|
}
|
||||||
|
val lanPermissionLauncher =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
|
viewModel.onLanPermissionResult(granted)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(state.lanPermissionNeeded) {
|
||||||
|
if (state.lanPermissionNeeded) lanPermissionLauncher.launch(Manifest.permission.ACCESS_LOCAL_NETWORK)
|
||||||
|
}
|
||||||
|
LaunchedEffect(state.connectionSuccess) {
|
||||||
|
if (state.connectionSuccess) onConnected()
|
||||||
|
}
|
||||||
|
val deepLink by AiturkPairDeepLink.pending.collectAsState()
|
||||||
|
LaunchedEffect(deepLink) {
|
||||||
|
if (deepLink != null) AiturkPairDeepLink.take()?.let(viewModel::onDeepLink)
|
||||||
|
}
|
||||||
|
// The ViewModel is Activity-scoped: drop one-shot flags when the screen leaves.
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose { viewModel.clearTransientState() }
|
||||||
|
}
|
||||||
|
|
||||||
|
state.confirmAddress?.let { address ->
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = viewModel::dismissDeepLink,
|
||||||
|
title = { Text(stringResource(R.string.aiturk_pair_confirm_title)) },
|
||||||
|
text = { Text(stringResource(R.string.aiturk_pair_confirm_message, address)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { viewModel.confirmDeepLink(lanPermissionGranted(context)) }) {
|
||||||
|
Text(stringResource(R.string.aiturk_pair_connect))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = viewModel::dismissDeepLink) {
|
||||||
|
Text(stringResource(R.string.aiturk_pair_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxSize().imePadding(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 24.dp, vertical = 32.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Computer,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.aiturk_pair_title),
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.aiturk_pair_desc),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (hasPermission(context, Manifest.permission.CAMERA)) {
|
||||||
|
launchScanner()
|
||||||
|
} else {
|
||||||
|
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = !state.busy,
|
||||||
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.QrCodeScanner, contentDescription = null)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(stringResource(R.string.aiturk_pair_scan_qr), style = MaterialTheme.typography.titleMedium)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.aiturk_pair_or),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.code,
|
||||||
|
onValueChange = viewModel::onCodeChange,
|
||||||
|
label = { Text(stringResource(R.string.aiturk_pair_code_label)) },
|
||||||
|
placeholder = { Text(stringResource(R.string.aiturk_pair_code_placeholder)) },
|
||||||
|
singleLine = true,
|
||||||
|
enabled = !state.busy,
|
||||||
|
isError = state.errorRes != null,
|
||||||
|
visualTransformation = PairingCodeTransformation,
|
||||||
|
textStyle =
|
||||||
|
MaterialTheme.typography.headlineSmall.copy(
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
),
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.Characters,
|
||||||
|
autoCorrectEnabled = false,
|
||||||
|
keyboardType = KeyboardType.Ascii,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { viewModel.submitCode(lanPermissionGranted(context)) }),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { viewModel.submitCode(lanPermissionGranted(context)) },
|
||||||
|
enabled = !state.busy && state.code.length == AiturkPairing.CODE_LENGTH,
|
||||||
|
modifier = Modifier.fillMaxWidth().height(52.dp),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.aiturk_pair_connect), style = MaterialTheme.typography.titleMedium)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.busy) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
stringResource(
|
||||||
|
if (state.step == AiturkPairStep.RESOLVING) {
|
||||||
|
R.string.aiturk_pair_progress_resolve
|
||||||
|
} else {
|
||||||
|
R.string.aiturk_pair_progress_connect
|
||||||
|
},
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.errorRes?.let { res ->
|
||||||
|
Text(
|
||||||
|
text = stringResource(res),
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(onClick = onBack, enabled = !state.busy) {
|
||||||
|
Text(stringResource(R.string.aiturk_pair_back))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasPermission(
|
||||||
|
context: Context,
|
||||||
|
permission: String,
|
||||||
|
): Boolean = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
/** Android 17+ gates LAN hosts behind ACCESS_LOCAL_NETWORK; older releases need nothing. */
|
||||||
|
private fun lanPermissionGranted(context: Context): Boolean =
|
||||||
|
!requiresLocalNetworkPermission(Build.VERSION.SDK_INT, LAN_PROBE_URL) ||
|
||||||
|
hasPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK)
|
||||||
|
|
||||||
|
/** Shows `ABCDEFGH` as `ABCD-EFGH` without storing the dash. */
|
||||||
|
private object PairingCodeTransformation : VisualTransformation {
|
||||||
|
override fun filter(text: AnnotatedString): TransformedText {
|
||||||
|
val raw = text.text
|
||||||
|
val shown = if (raw.length > CODE_GROUP) raw.substring(0, CODE_GROUP) + "-" + raw.substring(CODE_GROUP) else raw
|
||||||
|
val mapping =
|
||||||
|
object : OffsetMapping {
|
||||||
|
override fun originalToTransformed(offset: Int): Int = if (offset > CODE_GROUP) offset + 1 else offset
|
||||||
|
|
||||||
|
override fun transformedToOriginal(offset: Int): Int = if (offset > CODE_GROUP) offset - 1 else offset
|
||||||
|
}
|
||||||
|
return TransformedText(AnnotatedString(shown), mapping)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
package com.m57.hermescontrol.ui.aiturkpair
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.data.aiturk.AiturkConnectionRegistrar
|
||||||
|
import com.m57.hermescontrol.data.aiturk.AiturkPairError
|
||||||
|
import com.m57.hermescontrol.data.aiturk.AiturkPairResult
|
||||||
|
import com.m57.hermescontrol.data.aiturk.AiturkPairedDevice
|
||||||
|
import com.m57.hermescontrol.data.aiturk.AiturkPairing
|
||||||
|
import com.m57.hermescontrol.data.aiturk.AiturkPairingClient
|
||||||
|
import com.m57.hermescontrol.data.aiturk.AiturkPairingOffer
|
||||||
|
import com.m57.hermescontrol.data.aiturk.AiturkPairingPayload
|
||||||
|
import com.m57.hermescontrol.data.aiturk.AiturkPayloadError
|
||||||
|
import com.m57.hermescontrol.data.aiturk.AiturkPayloadResult
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
enum class AiturkPairStep { IDLE, RESOLVING, CONNECTING }
|
||||||
|
|
||||||
|
data class AiturkPairUiState(
|
||||||
|
val code: String = "",
|
||||||
|
val step: AiturkPairStep = AiturkPairStep.IDLE,
|
||||||
|
@param:StringRes val errorRes: Int? = null,
|
||||||
|
/** Address of a deep-link pairing waiting for the user's confirmation. */
|
||||||
|
val confirmAddress: String? = null,
|
||||||
|
val lanPermissionNeeded: Boolean = false,
|
||||||
|
val connectionSuccess: Boolean = false,
|
||||||
|
) {
|
||||||
|
val busy: Boolean get() = step != AiturkPairStep.IDLE
|
||||||
|
}
|
||||||
|
|
||||||
|
class AiturkPairViewModel(
|
||||||
|
private val client: AiturkPairingClient = AiturkPairingClient(),
|
||||||
|
private val register: suspend (AiturkPairedDevice) -> Unit = AiturkConnectionRegistrar::register,
|
||||||
|
private val deviceName: String = defaultDeviceName(),
|
||||||
|
) : ViewModel() {
|
||||||
|
private sealed interface Request {
|
||||||
|
data class Code(
|
||||||
|
val code: String,
|
||||||
|
) : Request
|
||||||
|
|
||||||
|
data class Payload(
|
||||||
|
val payload: AiturkPairingPayload,
|
||||||
|
) : Request
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow(AiturkPairUiState())
|
||||||
|
val uiState: StateFlow<AiturkPairUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private var pending: Request? = null
|
||||||
|
private var confirmPayload: AiturkPairingPayload? = null
|
||||||
|
|
||||||
|
fun onCodeChange(raw: String) {
|
||||||
|
_uiState.update { it.copy(code = AiturkPairing.cleanCodeInput(raw), errorRes = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitCode(lanPermissionGranted: Boolean) {
|
||||||
|
val code = AiturkPairing.normalizeCode(_uiState.value.code)
|
||||||
|
if (code == null) {
|
||||||
|
showError(R.string.aiturk_pair_error_code_invalid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start(Request.Code(code), lanPermissionGranted)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** [raw] is null when the user backed out of the scanner. */
|
||||||
|
fun onQrScanned(
|
||||||
|
raw: String?,
|
||||||
|
lanPermissionGranted: Boolean,
|
||||||
|
) {
|
||||||
|
if (raw == null) return
|
||||||
|
when (val result = AiturkPairing.parse(raw)) {
|
||||||
|
is AiturkPayloadResult.Ok -> start(Request.Payload(result.payload), lanPermissionGranted)
|
||||||
|
is AiturkPayloadResult.Invalid -> showError(payloadErrorRes(result.error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A link from outside the app is never acted on without an explicit confirmation. */
|
||||||
|
fun onDeepLink(raw: String) {
|
||||||
|
if (_uiState.value.busy) return
|
||||||
|
when (val result = AiturkPairing.parse(raw)) {
|
||||||
|
is AiturkPayloadResult.Ok -> {
|
||||||
|
confirmPayload = result.payload
|
||||||
|
val address =
|
||||||
|
result.payload.offer.endpoints
|
||||||
|
.first()
|
||||||
|
.hostPort
|
||||||
|
_uiState.update { it.copy(confirmAddress = address, errorRes = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
is AiturkPayloadResult.Invalid -> {
|
||||||
|
showError(payloadErrorRes(result.error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun confirmDeepLink(lanPermissionGranted: Boolean) {
|
||||||
|
val payload = confirmPayload ?: return
|
||||||
|
dismissDeepLink()
|
||||||
|
start(Request.Payload(payload), lanPermissionGranted)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissDeepLink() {
|
||||||
|
confirmPayload = null
|
||||||
|
_uiState.update { it.copy(confirmAddress = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onCameraPermissionDenied() = showError(R.string.aiturk_pair_error_camera_denied)
|
||||||
|
|
||||||
|
fun onLanPermissionResult(granted: Boolean) {
|
||||||
|
_uiState.update { it.copy(lanPermissionNeeded = false) }
|
||||||
|
val request = pending
|
||||||
|
pending = null
|
||||||
|
if (!granted) {
|
||||||
|
showError(R.string.aiturk_pair_error_lan_permission)
|
||||||
|
} else if (request != null) {
|
||||||
|
run(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The ViewModel outlives the screen; reset what must not replay on re-entry. */
|
||||||
|
fun clearTransientState() {
|
||||||
|
pending = null
|
||||||
|
confirmPayload = null
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
errorRes = null,
|
||||||
|
confirmAddress = null,
|
||||||
|
lanPermissionNeeded = false,
|
||||||
|
connectionSuccess = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun start(
|
||||||
|
request: Request,
|
||||||
|
lanPermissionGranted: Boolean,
|
||||||
|
) {
|
||||||
|
if (_uiState.value.busy) return
|
||||||
|
if (!lanPermissionGranted) {
|
||||||
|
pending = request
|
||||||
|
_uiState.update { it.copy(lanPermissionNeeded = true, errorRes = null) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
run(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun run(request: Request) {
|
||||||
|
if (_uiState.value.busy) return
|
||||||
|
val firstStep = if (request is Request.Code) AiturkPairStep.RESOLVING else AiturkPairStep.CONNECTING
|
||||||
|
_uiState.update { it.copy(step = firstStep, errorRes = null) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
val outcome =
|
||||||
|
try {
|
||||||
|
pair(request)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (_: Exception) {
|
||||||
|
AiturkPairResult.Failure(AiturkPairError.UNEXPECTED_RESPONSE)
|
||||||
|
}
|
||||||
|
when (outcome) {
|
||||||
|
is AiturkPairResult.Success -> {
|
||||||
|
val saved = runCatching { register(outcome.value) }.isSuccess
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
step = AiturkPairStep.IDLE,
|
||||||
|
code = "",
|
||||||
|
connectionSuccess = saved,
|
||||||
|
errorRes = if (saved) null else R.string.aiturk_pair_error_save_failed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is AiturkPairResult.Failure -> {
|
||||||
|
_uiState.update { it.copy(step = AiturkPairStep.IDLE, errorRes = pairErrorRes(outcome.error)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun pair(request: Request): AiturkPairResult<AiturkPairedDevice> {
|
||||||
|
val offer: AiturkPairingOffer
|
||||||
|
val code: String
|
||||||
|
when (request) {
|
||||||
|
is Request.Payload -> {
|
||||||
|
offer = request.payload.offer
|
||||||
|
code = request.payload.code
|
||||||
|
}
|
||||||
|
|
||||||
|
is Request.Code -> {
|
||||||
|
code = request.code
|
||||||
|
when (val resolved = client.resolve(code)) {
|
||||||
|
is AiturkPairResult.Success -> offer = resolved.value
|
||||||
|
is AiturkPairResult.Failure -> return resolved
|
||||||
|
}
|
||||||
|
_uiState.update { it.copy(step = AiturkPairStep.CONNECTING) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return client.pair(offer, code, deviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showError(
|
||||||
|
@StringRes res: Int,
|
||||||
|
) {
|
||||||
|
_uiState.update { it.copy(errorRes = res) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@StringRes
|
||||||
|
fun payloadErrorRes(error: AiturkPayloadError): Int =
|
||||||
|
when (error) {
|
||||||
|
AiturkPayloadError.NOT_AITURK -> R.string.aiturk_pair_error_qr_not_aiturk
|
||||||
|
|
||||||
|
AiturkPayloadError.UNSUPPORTED_VERSION -> R.string.aiturk_pair_error_update_app
|
||||||
|
|
||||||
|
AiturkPayloadError.MALFORMED,
|
||||||
|
AiturkPayloadError.BAD_FINGERPRINT,
|
||||||
|
AiturkPayloadError.BAD_ADDRESS,
|
||||||
|
AiturkPayloadError.BAD_ADDRESS_COUNT,
|
||||||
|
AiturkPayloadError.BAD_CODE,
|
||||||
|
-> R.string.aiturk_pair_error_qr_invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
fun pairErrorRes(error: AiturkPairError): Int =
|
||||||
|
when (error) {
|
||||||
|
AiturkPairError.CODE_NOT_FOUND -> R.string.aiturk_pair_error_code_not_found
|
||||||
|
|
||||||
|
AiturkPairError.RATE_LIMITED -> R.string.aiturk_pair_error_rate_limited
|
||||||
|
|
||||||
|
AiturkPairError.GATEWAY_UNAVAILABLE -> R.string.aiturk_pair_error_gateway
|
||||||
|
|
||||||
|
AiturkPairError.PC_UNREACHABLE -> R.string.aiturk_pair_error_pc_unreachable
|
||||||
|
|
||||||
|
AiturkPairError.WRONG_CODE -> R.string.aiturk_pair_error_wrong_code
|
||||||
|
|
||||||
|
AiturkPairError.PAIRING_CLOSED -> R.string.aiturk_pair_error_pairing_closed
|
||||||
|
|
||||||
|
AiturkPairError.PAIRING_LOCKED -> R.string.aiturk_pair_error_pairing_locked
|
||||||
|
|
||||||
|
AiturkPairError.INVALID_REQUEST,
|
||||||
|
AiturkPairError.UNEXPECTED_RESPONSE,
|
||||||
|
-> R.string.aiturk_pair_error_unexpected
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun defaultDeviceName(): String {
|
||||||
|
val maker = android.os.Build.MANUFACTURER.orEmpty()
|
||||||
|
val model = android.os.Build.MODEL.orEmpty()
|
||||||
|
val name = if (model.startsWith(maker, ignoreCase = true)) model else "$maker $model"
|
||||||
|
return AiturkPairingClient.sanitizeDeviceName(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.m57.hermescontrol.ui.aiturkpair
|
||||||
|
|
||||||
|
import com.journeyapps.barcodescanner.CaptureActivity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ZXing capture screen under our own manifest entry, so it follows the
|
||||||
|
* device orientation instead of the library default (landscape only).
|
||||||
|
*/
|
||||||
|
class AiturkQrCaptureActivity : CaptureActivity()
|
||||||
@@ -16,6 +16,7 @@ import androidx.compose.material.icons.filled.Bolt
|
|||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -31,6 +32,7 @@ import com.m57.hermescontrol.R
|
|||||||
fun LandingScreen(
|
fun LandingScreen(
|
||||||
onAuthLogin: () -> Unit,
|
onAuthLogin: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
onAiturkPair: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -88,6 +90,16 @@ fun LandingScreen(
|
|||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
|
onClick = onAiturkPair,
|
||||||
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.landing_action_aiturk_pair),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedButton(
|
||||||
onClick = onAuthLogin,
|
onClick = onAuthLogin,
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1862,4 +1862,36 @@
|
|||||||
<string name="mcp_servers_catalog_empty_title">Katalog kaydı yok</string>
|
<string name="mcp_servers_catalog_empty_title">Katalog kaydı yok</string>
|
||||||
<string name="mcp_servers_catalog_installed">Kurulu</string>
|
<string name="mcp_servers_catalog_installed">Kurulu</string>
|
||||||
<string name="settings_desc_theme_preset_dynamic">Uygulama renklerini Material You sağlarken durum renkleri için kullanılır.</string>
|
<string name="settings_desc_theme_preset_dynamic">Uygulama renklerini Material You sağlarken durum renkleri için kullanılır.</string>
|
||||||
|
|
||||||
|
<!-- AITURK IDE pairing (QR / code) -->
|
||||||
|
<string name="landing_action_aiturk_pair">Bilgisayara bağlan (QR / kod)</string>
|
||||||
|
<string name="aiturk_pair_title">Bilgisayara bağlan</string>
|
||||||
|
<string name="aiturk_pair_desc">Bilgisayarınızda AITURK IDE → Ayarlar → Telefon bağlantısı ekranını açın. Orada görünen QR kodu okutun ya da 8 karakterli kodu yazın. Telefon ve bilgisayar aynı Wi-Fi ağında olmalı.</string>
|
||||||
|
<string name="aiturk_pair_scan_qr">QR okut</string>
|
||||||
|
<string name="aiturk_pair_scan_prompt">Kamerayı AITURK IDE\'deki QR koda tutun</string>
|
||||||
|
<string name="aiturk_pair_or">ya da</string>
|
||||||
|
<string name="aiturk_pair_code_label">8 karakterli kod</string>
|
||||||
|
<string name="aiturk_pair_code_placeholder">ABCD-EFGH</string>
|
||||||
|
<string name="aiturk_pair_connect">Bağlan</string>
|
||||||
|
<string name="aiturk_pair_cancel">Vazgeç</string>
|
||||||
|
<string name="aiturk_pair_back">Geri</string>
|
||||||
|
<string name="aiturk_pair_progress_resolve">Kod aranıyor…</string>
|
||||||
|
<string name="aiturk_pair_progress_connect">Bilgisayara bağlanılıyor…</string>
|
||||||
|
<string name="aiturk_pair_confirm_title">Bu bilgisayara bağlanılsın mı?</string>
|
||||||
|
<string name="aiturk_pair_confirm_message">Bir bağlantı, AITURK Mobil\'in ağınızdaki %1$s adresli bilgisayara bağlanmasını istiyor. Yalnızca bu bağlantıyı AITURK IDE\'den kendiniz açtıysanız devam edin.</string>
|
||||||
|
<string name="aiturk_pair_error_camera_denied">Kamera izni verilmedi. Bunun yerine 8 karakterli kodu yazabilirsiniz.</string>
|
||||||
|
<string name="aiturk_pair_error_lan_permission">Yerel ağdaki cihazlara erişim izni verilmedi. Bilgisayarınıza bağlanmak için bu izni verin.</string>
|
||||||
|
<string name="aiturk_pair_error_qr_not_aiturk">Bu bir AITURK eşleştirme QR kodu değil.</string>
|
||||||
|
<string name="aiturk_pair_error_update_app">Bu QR kod için AITURK Mobil\'in daha yeni bir sürümü gerekiyor. Lütfen uygulamayı güncelleyin.</string>
|
||||||
|
<string name="aiturk_pair_error_qr_invalid">QR kod eksik ya da bozuk. AITURK IDE\'de Telefon bağlantısı ekranını yeniden açıp tekrar deneyin.</string>
|
||||||
|
<string name="aiturk_pair_error_code_invalid">Kod 8 karakterdir. AITURK IDE\'de göründüğü gibi yazın.</string>
|
||||||
|
<string name="aiturk_pair_error_code_not_found">Kod bulunamadı ya da süresi doldu. Kodu kontrol edin ya da yeni kod için AITURK IDE\'de Telefon bağlantısı ekranını yeniden açın.</string>
|
||||||
|
<string name="aiturk_pair_error_rate_limited">Çok fazla deneme yapıldı. Bir dakika bekleyip tekrar deneyin.</string>
|
||||||
|
<string name="aiturk_pair_error_gateway">Kod servisine ulaşılamadı. İnternet bağlantınızı kontrol edin ya da QR kodu okutun.</string>
|
||||||
|
<string name="aiturk_pair_error_pc_unreachable">Bilgisayarınıza ulaşılamadı. Telefon ile bilgisayarın aynı Wi-Fi ağında olduğundan ve AITURK IDE\'de Telefon bağlantısı ekranının açık olduğundan emin olun.</string>
|
||||||
|
<string name="aiturk_pair_error_wrong_code">Kod yanlış. AITURK IDE\'deki kodu kontrol edip tekrar deneyin.</string>
|
||||||
|
<string name="aiturk_pair_error_pairing_closed">Eşleştirme kapalı ya da kodun süresi dolmuş veya kod kullanılmış. AITURK IDE\'de Telefon bağlantısı ekranını yeniden açın.</string>
|
||||||
|
<string name="aiturk_pair_error_pairing_locked">Çok fazla yanlış kod girildi, eşleştirme iptal edildi. AITURK IDE\'de Telefon bağlantısını yeniden başlatın.</string>
|
||||||
|
<string name="aiturk_pair_error_unexpected">Bilgisayar beklenmeyen bir cevap verdi. AITURK IDE ve AITURK Mobil\'i güncelleyip tekrar deneyin.</string>
|
||||||
|
<string name="aiturk_pair_error_save_failed">Eşleşti ama bağlantı kaydedilemedi. Lütfen tekrar deneyin.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1862,4 +1862,36 @@
|
|||||||
<string name="mcp_servers_catalog_empty_title">No catalog entries</string>
|
<string name="mcp_servers_catalog_empty_title">No catalog entries</string>
|
||||||
<string name="mcp_servers_catalog_installed">Installed</string>
|
<string name="mcp_servers_catalog_installed">Installed</string>
|
||||||
<string name="settings_desc_theme_preset_dynamic">Used for status colors while Material You supplies the app colors.</string>
|
<string name="settings_desc_theme_preset_dynamic">Used for status colors while Material You supplies the app colors.</string>
|
||||||
|
|
||||||
|
<!-- AITURK IDE pairing (QR / code) -->
|
||||||
|
<string name="landing_action_aiturk_pair">Connect to computer (QR / code)</string>
|
||||||
|
<string name="aiturk_pair_title">Connect to your computer</string>
|
||||||
|
<string name="aiturk_pair_desc">On your computer open AITURK IDE → Settings → Phone connection. Scan the QR code shown there or type the 8-character code. The phone and the computer must be on the same Wi-Fi.</string>
|
||||||
|
<string name="aiturk_pair_scan_qr">Scan QR</string>
|
||||||
|
<string name="aiturk_pair_scan_prompt">Point the camera at the QR code in AITURK IDE</string>
|
||||||
|
<string name="aiturk_pair_or">or</string>
|
||||||
|
<string name="aiturk_pair_code_label">8-character code</string>
|
||||||
|
<string name="aiturk_pair_code_placeholder">ABCD-EFGH</string>
|
||||||
|
<string name="aiturk_pair_connect">Connect</string>
|
||||||
|
<string name="aiturk_pair_cancel">Cancel</string>
|
||||||
|
<string name="aiturk_pair_back">Back</string>
|
||||||
|
<string name="aiturk_pair_progress_resolve">Looking up the code…</string>
|
||||||
|
<string name="aiturk_pair_progress_connect">Connecting to your computer…</string>
|
||||||
|
<string name="aiturk_pair_confirm_title">Connect to this computer?</string>
|
||||||
|
<string name="aiturk_pair_confirm_message">A link is asking AITURK Mobil to connect to the computer at %1$s on your network. Continue only if you opened this link yourself from AITURK IDE.</string>
|
||||||
|
<string name="aiturk_pair_error_camera_denied">Camera permission was not granted. You can type the 8-character code instead.</string>
|
||||||
|
<string name="aiturk_pair_error_lan_permission">Access to devices on your local network was not allowed. Allow it to connect to your computer.</string>
|
||||||
|
<string name="aiturk_pair_error_qr_not_aiturk">This is not an AITURK pairing QR code.</string>
|
||||||
|
<string name="aiturk_pair_error_update_app">This QR code needs a newer AITURK Mobil. Please update the app.</string>
|
||||||
|
<string name="aiturk_pair_error_qr_invalid">The QR code is incomplete or damaged. Open Phone connection in AITURK IDE again and retry.</string>
|
||||||
|
<string name="aiturk_pair_error_code_invalid">The code has 8 characters. Type it exactly as shown in AITURK IDE.</string>
|
||||||
|
<string name="aiturk_pair_error_code_not_found">Code not found or expired. Check it, or open Phone connection in AITURK IDE again for a new code.</string>
|
||||||
|
<string name="aiturk_pair_error_rate_limited">Too many attempts. Wait a minute and try again.</string>
|
||||||
|
<string name="aiturk_pair_error_gateway">The code service could not be reached. Check your internet connection, or scan the QR code instead.</string>
|
||||||
|
<string name="aiturk_pair_error_pc_unreachable">Your computer could not be reached. Make sure the phone and the computer are on the same Wi-Fi and Phone connection is open in AITURK IDE.</string>
|
||||||
|
<string name="aiturk_pair_error_wrong_code">Wrong code. Check the code in AITURK IDE and try again.</string>
|
||||||
|
<string name="aiturk_pair_error_pairing_closed">Pairing is closed, or the code expired or was already used. Open Phone connection in AITURK IDE again.</string>
|
||||||
|
<string name="aiturk_pair_error_pairing_locked">Too many wrong codes, pairing was cancelled. Start Phone connection again in AITURK IDE.</string>
|
||||||
|
<string name="aiturk_pair_error_unexpected">The computer gave an unexpected answer. Update AITURK IDE and AITURK Mobil, then try again.</string>
|
||||||
|
<string name="aiturk_pair_error_save_failed">Paired, but the connection could not be saved. Please try again.</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in new issue
Block a user