Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.clawdroid.core.ui.theme.ClawDroidTheme
import io.clawdroid.core.ui.theme.GlassBorder
Expand Down Expand Up @@ -94,7 +95,7 @@ class CalendarPickerActivity : ComponentActivity() {
onDismissRequest = onCancel,
containerColor = DarkCard,
title = {
Text("Select Calendar", color = NeonCyan)
Text(stringResource(R.string.calendar_picker_title), color = NeonCyan)
},
text = {
Surface(
Expand Down Expand Up @@ -126,7 +127,7 @@ class CalendarPickerActivity : ComponentActivity() {
confirmButton = {},
dismissButton = {
TextButton(onClick = onCancel) {
Text("Cancel", color = TextSecondary)
Text(stringResource(R.string.action_cancel), color = TextSecondary)
}
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {

serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

connection = AssistantConnectionImpl(httpClient)
connection = AssistantConnectionImpl(httpClient, applicationContext)

toolRequestHandler = ToolRequestHandler(
context = applicationContext,
Expand Down Expand Up @@ -345,15 +345,11 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
) {
Column(modifier = Modifier.padding(24.dp)) {
Text(
text = "画面キャプチャの設定",
text = getString(io.clawdroid.R.string.assistant_accessibility_title),
style = MaterialTheme.typography.headlineSmall
)
Text(
text = "画面キャプチャを使用するには、ユーザー補助の設定で" +
" ClawDroid を有効にしてください。\n\n" +
"有効にできない場合は、アプリ情報から" +
"「制限付き設定を許可」を実行してから" +
"再度お試しください。",
text = getString(io.clawdroid.R.string.assistant_accessibility_body),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 16.dp)
)
Expand All @@ -364,15 +360,15 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { showAccessibilityGuide = false }) {
Text("キャンセル")
Text(getString(io.clawdroid.R.string.action_cancel))
}
TextButton(onClick = {
showAccessibilityGuide = false
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}) {
Text("設定を開く")
Text(getString(io.clawdroid.R.string.assistant_accessibility_open_settings))
}
}
}
Expand Down Expand Up @@ -414,7 +410,7 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
return NotificationCompat.Builder(this, NotificationHelper.ASSISTANT_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("ClawDroid Assistant")
.setContentText("Listening...")
.setContentText(getString(io.clawdroid.R.string.assistant_notification_listening))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build()
Expand Down
2 changes: 1 addition & 1 deletion android/app/src/main/java/io/clawdroid/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ val appModule = module {
val clientId = prefs.getString("client_id", null) ?: UUID.randomUUID().toString().also {
prefs.edit { putString("client_id", it) }
}
WebSocketClient(get(), get(), clientId)
WebSocketClient(get(), get(), clientId, context = androidContext())
}

// ImageFileStorage
Expand Down
14 changes: 14 additions & 0 deletions android/app/src/main/res/values-ja/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,18 @@
<string name="notification_channel_gateway">ゲートウェイ</string>
<string name="notification_channel_gateway_desc">ゲートウェイバックエンドサービス</string>
<string name="notification_title">ClawDroid</string>

<!-- Common actions -->
<string name="action_cancel">キャンセル</string>

<!-- Accessibility Guide -->
<string name="assistant_accessibility_title">画面キャプチャの設定</string>
<string name="assistant_accessibility_body">画面キャプチャを使用するには、ユーザー補助の設定で ClawDroid を有効にしてください。\n\n有効にできない場合は、アプリ情報から「制限付き設定を許可」を実行してから再度お試しください。</string>
<string name="assistant_accessibility_open_settings">設定を開く</string>

<!-- Assistant Notification -->
<string name="assistant_notification_listening">聞いています…</string>

<!-- Calendar Picker -->
<string name="calendar_picker_title">カレンダーを選択</string>
</resources>
14 changes: 14 additions & 0 deletions android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,18 @@
<string name="notification_channel_gateway">Gateway</string>
<string name="notification_channel_gateway_desc">Gateway backend service</string>
<string name="notification_title">ClawDroid</string>

<!-- Common actions -->
<string name="action_cancel">Cancel</string>

<!-- Accessibility Guide -->
<string name="assistant_accessibility_title">Screen Capture Setup</string>
<string name="assistant_accessibility_body">To use screen capture, enable ClawDroid in Accessibility settings.\n\nIf you cannot enable it, go to App Info and select \"Allow restricted settings\", then try again.</string>
<string name="assistant_accessibility_open_settings">Open Settings</string>

<!-- Assistant Notification -->
<string name="assistant_notification_listening">Listening…</string>

<!-- Calendar Picker -->
<string name="calendar_picker_title">Select Calendar</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import android.content.Context
import java.io.Closeable
import java.io.IOException
import java.util.Locale

@Serializable
data class ConfigSchema(val sections: List<SchemaSection>)
Expand Down Expand Up @@ -48,7 +48,7 @@ data class SaveConfigResult(

class AuthException(message: String) : IOException(message)

class ConfigApiClient(private val settingsStore: GatewaySettingsStore) : Closeable {
class ConfigApiClient(private val settingsStore: GatewaySettingsStore, private val context: Context) : Closeable {
private val baseUrl: String
get() = settingsStore.settings.value.httpBaseUrl

Expand All @@ -64,7 +64,7 @@ class ConfigApiClient(private val settingsStore: GatewaySettingsStore) : Closeab
suspend fun getSchema(): ConfigSchema {
return client.get("$baseUrl/api/config/schema") {
if (apiKey.isNotEmpty()) header("Authorization", "Bearer $apiKey")
header("Accept-Language", Locale.getDefault().language)
header("Accept-Language", context.resources.configuration.locales[0].language)
}.ensureSuccess().body()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package io.clawdroid.backend.config

import org.koin.android.ext.koin.androidContext
import org.koin.core.definition.Callbacks
import org.koin.core.module.dsl.viewModel
import org.koin.core.module.dsl.withOptions
import org.koin.dsl.module

val configModule = module {
single { ConfigApiClient(get()) } withOptions { callbacks = Callbacks(onClose = { it?.close() }) }
single { ConfigApiClient(get(), androidContext()) } withOptions { callbacks = Callbacks(onClose = { it?.close() }) }
viewModel { ConfigViewModel(get()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
Expand Down Expand Up @@ -85,16 +86,19 @@ fun ConfigSectionDetailScreen(
val detail = uiState.detailState
val isDirty = detail?.fields?.any { it.value != it.originalValue } == true
val isSaving = uiState.saveState is SaveState.Saving
val savedMsg = stringResource(R.string.config_saved)
val savedRestartingMsg = stringResource(R.string.config_saved_restarting)
val errorPrefix = stringResource(R.string.config_error_prefix)

LaunchedEffect(uiState.saveState) {
when (val state = uiState.saveState) {
is SaveState.Success -> {
val msg = if (state.restart) "Saved. Server restarting..." else "Saved."
val msg = if (state.restart) savedRestartingMsg else savedMsg
snackbarHostState.showSnackbar(msg)
viewModel.dismissSaveResult()
}
is SaveState.Error -> {
snackbarHostState.showSnackbar("Error: ${state.message}")
snackbarHostState.showSnackbar("$errorPrefix${state.message}")
viewModel.dismissSaveResult()
}
else -> {}
Expand All @@ -118,7 +122,7 @@ fun ConfigSectionDetailScreen(
}) {
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_arrow_left),
contentDescription = "Back",
contentDescription = stringResource(R.string.config_back),
tint = TextSecondary,
)
}
Expand All @@ -142,7 +146,7 @@ fun ConfigSectionDetailScreen(
strokeWidth = 2.dp,
)
} else {
Text("Save")
Text(stringResource(R.string.config_save))
}
}
},
Expand Down Expand Up @@ -265,7 +269,7 @@ private fun StringField(field: FieldState, onValueChanged: (String) -> Unit) {
{
TextButton(onClick = { hidden = !hidden }) {
Text(
if (hidden) "Show" else "Hide",
stringResource(if (hidden) R.string.config_show else R.string.config_hide),
color = NeonCyan,
style = MaterialTheme.typography.labelSmall,
)
Expand Down Expand Up @@ -334,7 +338,7 @@ private fun StringArrayField(field: FieldState, onValueChanged: (String) -> Unit
value = field.value,
onValueChange = onValueChanged,
label = { Text(field.label, color = TextSecondary) },
placeholder = { Text("Comma-separated values", color = TextSecondary.copy(alpha = 0.5f)) },
placeholder = { Text(stringResource(R.string.config_comma_separated), color = TextSecondary.copy(alpha = 0.5f)) },
singleLine = true,
colors = configFieldColors(),
modifier = Modifier.fillMaxWidth(),
Expand Down Expand Up @@ -375,6 +379,7 @@ private fun DirectoryField(
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val internalStorageOnlyMsg = stringResource(R.string.config_internal_storage_only)

val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree()
Expand All @@ -389,7 +394,7 @@ private fun DirectoryField(
onValueChanged(path)
} else {
scope.launch {
snackbarHostState?.showSnackbar("Internal storage only")
snackbarHostState?.showSnackbar(internalStorageOnlyMsg)
}
}
}
Expand All @@ -404,7 +409,7 @@ private fun DirectoryField(
IconButton(onClick = { launcher.launch(null) }) {
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_folder_open),
contentDescription = "Browse",
contentDescription = stringResource(R.string.config_browse),
tint = NeonCyan,
)
}
Expand All @@ -413,7 +418,7 @@ private fun DirectoryField(
modifier = Modifier.fillMaxWidth(),
)
Text(
"Internal storage only for SAF picker",
stringResource(R.string.config_internal_storage_saf),
style = MaterialTheme.typography.labelSmall,
color = TextSecondary.copy(alpha = 0.6f),
modifier = Modifier.padding(start = 16.dp, top = 2.dp),
Expand Down Expand Up @@ -455,7 +460,7 @@ private fun CalendarField(
null
}
}
val displayText = if (field.value.isEmpty()) "Pick each time"
val displayText = if (field.value.isEmpty()) stringResource(R.string.config_pick_each_time)
else resolvedName ?: field.value

Row(
Expand Down Expand Up @@ -490,7 +495,7 @@ private fun CalendarField(
}
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_chevron_right),
contentDescription = "Select",
contentDescription = stringResource(R.string.config_select),
tint = if (enabled) NeonCyan else NeonCyan.copy(alpha = 0.3f),
modifier = Modifier.size(20.dp),
)
Expand All @@ -499,12 +504,12 @@ private fun CalendarField(
if (showDialog && calendars.isNotEmpty()) {
androidx.compose.material3.AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text("Select Calendar") },
title = { Text(stringResource(R.string.config_select_calendar)) },
text = {
Column {
// "Pick each time" option to clear the fixed setting
Text(
"Pick each time",
stringResource(R.string.config_pick_each_time),
style = MaterialTheme.typography.bodyMedium,
color = NeonCyan,
modifier = Modifier
Expand Down Expand Up @@ -535,7 +540,7 @@ private fun CalendarField(
confirmButton = {},
dismissButton = {
TextButton(onClick = { showDialog = false }) {
Text("Cancel", color = TextSecondary)
Text(stringResource(R.string.config_cancel), color = TextSecondary)
}
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.clawdroid.core.ui.theme.DeepBlack
import io.clawdroid.core.ui.theme.GlassBorder
Expand All @@ -58,15 +59,15 @@ fun ConfigSectionListScreen(
containerColor = Color.Transparent,
topBar = {
TopAppBar(
title = { Text("Backend Config") },
title = { Text(stringResource(R.string.config_backend_config)) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
),
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_arrow_left),
contentDescription = "Back",
contentDescription = stringResource(R.string.config_back),
tint = TextSecondary,
)
}
Expand Down Expand Up @@ -107,7 +108,7 @@ fun ConfigSectionListScreen(
contentColor = DeepBlack,
),
) {
Text("Retry")
Text(stringResource(R.string.config_retry))
}
}
}
Expand All @@ -134,7 +135,7 @@ fun ConfigSectionListScreen(
contentColor = DeepBlack,
),
) {
Text("Connection Settings")
Text(stringResource(R.string.config_connection_settings))
}
Spacer(Modifier.height(8.dp))
Button(
Expand All @@ -144,7 +145,7 @@ fun ConfigSectionListScreen(
contentColor = NeonCyan,
),
) {
Text("Retry")
Text(stringResource(R.string.config_retry))
}
}
}
Expand Down Expand Up @@ -193,14 +194,14 @@ private fun SectionCard(
color = TextPrimary,
)
Text(
"${section.fieldCount} fields",
stringResource(R.string.config_fields_count, section.fieldCount),
style = MaterialTheme.typography.bodySmall,
color = TextSecondary,
)
}
Icon(
painter = painterResource(LucideR.drawable.lucide_ic_chevron_right),
contentDescription = "Open",
contentDescription = stringResource(R.string.config_open),
tint = TextSecondary,
)
}
Expand Down
Loading