diff --git a/apps/mobile/app/src/main/AndroidManifest.xml b/apps/mobile/app/src/main/AndroidManifest.xml index a7b0068..0faa3d3 100644 --- a/apps/mobile/app/src/main/AndroidManifest.xml +++ b/apps/mobile/app/src/main/AndroidManifest.xml @@ -30,14 +30,25 @@ - + - + + + + + + + + + + + + + + diff --git a/apps/mobile/app/src/main/java/com/example/avtex/MainActivity.kt b/apps/mobile/app/src/main/java/com/example/avtex/MainActivity.kt index ae3c52a..b337bd8 100644 --- a/apps/mobile/app/src/main/java/com/example/avtex/MainActivity.kt +++ b/apps/mobile/app/src/main/java/com/example/avtex/MainActivity.kt @@ -2,6 +2,7 @@ package com.example.avtex import android.content.Intent import android.os.Bundle +import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -40,47 +41,147 @@ class MainActivity : ComponentActivity() { private fun handleIncomingIntent(intent: Intent?) { if (intent == null) return + val action = intent.action + Log.d(TAG, "Incoming intent action: $action, data: ${intent.data}") - // 1. Обработка App Actions через Deep Link xflow://lists/add - val data = intent.data - if (data != null && data.scheme == "xflow") { - val listName = data.getQueryParameter("list_name") ?: data.getQueryParameter("name") ?: "Фильмы на просмотр" - val itemName = data.getQueryParameter("item_name") ?: data.getQueryParameter("item") - - if (!itemName.isNullOrBlank()) { - saveItemFromAssistant(itemName, listName) + // 1. Google Assistant / Android System Note Action (android.intent.action.CREATE_NOTE) + if (action == Intent.ACTION_CREATE_NOTE) { + val text = intent.getStringExtra(Intent.EXTRA_TEXT) + ?: intent.getStringExtra("android.intent.extra.TEXT") + ?: intent.getStringExtra(Intent.EXTRA_TITLE) + val title = intent.getStringExtra(Intent.EXTRA_TITLE) + if (!text.isNullOrBlank()) { + val fullText = if (!title.isNullOrBlank() && title != text) "$title: $text" else text + routeAndSaveNote(fullText, preferredList = null) return } } - // 2. Обработка через Intent Extras (Google Assistant Built-in Intents) - val extras = intent.extras - if (extras != null) { - val itemName = extras.getString("itemListElement.name") ?: extras.getString("item_name") - val listName = extras.getString("itemList.name") ?: extras.getString("list_name") ?: "Фильмы на просмотр" + // 2. Android Share Sheet Action (android.intent.action.SEND with text/plain) + if (action == Intent.ACTION_SEND && intent.type?.startsWith("text/") == true) { + val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) + val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT) + if (!sharedText.isNullOrBlank()) { + val fullText = if (!subject.isNullOrBlank()) "$subject — $sharedText" else sharedText + routeAndSaveNote(fullText, preferredList = null) + return + } + } + + // 3. Deep Link Action (xflow://...) + val data = intent.data + if (data != null && data.scheme == "xflow") { + val path = data.path ?: data.host ?: "" + val listName = data.getQueryParameter("list_name") + ?: data.getQueryParameter("name") + ?: data.getQueryParameter("list") + + val itemName = data.getQueryParameter("item_name") + ?: data.getQueryParameter("item") + ?: data.getQueryParameter("note_text") + ?: data.getQueryParameter("text") + ?: data.getQueryParameter("note_title") if (!itemName.isNullOrBlank()) { - saveItemFromAssistant(itemName, listName) + routeAndSaveNote(itemName, preferredList = listName) + return + } + } + + // 4. Google Assistant / Gemini Built-in Intent Extras + val extras = intent.extras + if (extras != null) { + val itemName = extras.getString("itemListElement.name") + ?: extras.getString("item_name") + ?: extras.getString("note.text") + ?: extras.getString("note_text") + ?: extras.getString("text") + + val listName = extras.getString("itemList.name") + ?: extras.getString("list_name") + ?: extras.getString("category") + + if (!itemName.isNullOrBlank()) { + routeAndSaveNote(itemName, preferredList = listName) } } } - private fun saveItemFromAssistant(itemName: String, listName: String) { + private fun routeAndSaveNote(rawText: String, preferredList: String?) { + val cleanText = rawText.trim() + if (cleanText.isBlank()) return + + val (targetList, category) = resolveListAndCategory(cleanText, preferredList) + lifecycleScope.launch { - val repo = AvtexApplication.instance.notesRepository - val category = if (listName.contains("фильм", ignoreCase = true)) "movies" else "general" + try { + val repo = AvtexApplication.instance.notesRepository + repo.addNoteItem( + title = cleanText, + listTitle = targetList, + category = category + ) - repo.addNoteItem( - title = itemName, - listTitle = listName, - category = category - ) + Toast.makeText( + this@MainActivity, + "✅ XFlow: Записано в «$targetList»:\n$cleanText", + Toast.LENGTH_LONG + ).show() - Toast.makeText( - this@MainActivity, - "✅ Записано в XFlow: $itemName ($listName)", - Toast.LENGTH_LONG - ).show() + AvtexApplication.instance.settingsRepository.addLog( + tag = "Assistant", + message = "Заметка через ассистент: \"$cleanText\" в список \"$targetList\"", + isImportant = true + ) + } catch (e: Exception) { + Log.e(TAG, "Failed to save note from assistant", e) + Toast.makeText( + this@MainActivity, + "Ошибка сохранения: ${e.localizedMessage}", + Toast.LENGTH_SHORT + ).show() + } } } + + private fun resolveListAndCategory(text: String, preferredList: String?): Pair { + val lower = text.lowercase() + + // Если список был явно передан в интенте + if (!preferredList.isNullOrBlank()) { + val prefLower = preferredList.lowercase() + return when { + prefLower.contains("фильм") || prefLower.contains("кино") || prefLower.contains("сериал") || prefLower.contains("movie") -> + Pair(preferredList, "movies") + prefLower.contains("покупк") || prefLower.contains("магаз") || prefLower.contains("купить") || prefLower.contains("shop") -> + Pair(preferredList, "shopping") + prefLower.contains("задач") || prefLower.contains("дело") || prefLower.contains("task") || prefLower.contains("todo") -> + Pair(preferredList, "tasks") + else -> Pair(preferredList, "general") + } + } + + // Авто-маршрутизация по содержанию текста + return when { + lower.contains("фильм") || lower.contains("сериал") || lower.contains("кино") || + lower.contains("посмотреть") || lower.contains("сезон") || lower.contains("актер") || + lower.contains("режиссер") || lower.contains("movie") || lower.contains("cinema") -> + Pair("Фильмы на просмотр", "movies") + + lower.contains("купить") || lower.contains("покупки") || lower.contains("магазин") || + lower.contains("взять") || lower.contains("продукты") || lower.contains("цена") || + lower.contains("аптек") || lower.contains("buy") || lower.contains("shop") -> + Pair("Покупки", "shopping") + + lower.contains("задача") || lower.contains("сделать") || lower.contains("напомни") || + lower.contains("позвонить") || lower.contains("отправить") || lower.contains("todo") -> + Pair("Задачи", "tasks") + + else -> Pair("Быстрые заметки", "general") + } + } + + companion object { + private const val TAG = "AvtexMainActivity" + } } diff --git a/apps/mobile/app/src/main/java/com/example/avtex/ui/ListsScreen.kt b/apps/mobile/app/src/main/java/com/example/avtex/ui/ListsScreen.kt index 891bc2e..4a2c13e 100644 --- a/apps/mobile/app/src/main/java/com/example/avtex/ui/ListsScreen.kt +++ b/apps/mobile/app/src/main/java/com/example/avtex/ui/ListsScreen.kt @@ -1,5 +1,10 @@ package com.example.avtex.ui +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -17,24 +22,30 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Movie -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.ShoppingCart -import androidx.compose.material.icons.filled.TaskAlt +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Terminal import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text @@ -48,17 +59,18 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.avtex.AvtexApplication +import com.example.avtex.MainActivity import com.example.avtex.notes.NoteItem import com.example.avtex.notes.NoteList import com.example.avtex.theme.CyanAccent -import com.example.avtex.theme.ElectricIndigo import com.example.avtex.theme.EmeraldMint import com.example.avtex.theme.GlassBorder import com.example.avtex.theme.NeonViolet @@ -74,113 +86,125 @@ import kotlinx.coroutines.launch @Composable fun ListsScreen(modifier: Modifier = Modifier) { + val context = LocalContext.current val repository = remember { AvtexApplication.instance.notesRepository } val lists by repository.listsFlow.collectAsState() val coroutineScope = rememberCoroutineScope() var selectedCategory by remember { mutableStateOf("all") } var newItemText by remember { mutableStateOf("") } - var isAddingActive by remember { mutableStateOf(false) } - - val categories = listOf( - "all" to "Все", - "movies" to "🎬 Фильмы", - "shopping" to "🛒 Покупки", - "tasks" to "⚡ Дела" - ) + var showAssistantTester by remember { mutableStateOf(false) } val filteredLists = remember(lists, selectedCategory) { - if (selectedCategory == "all") lists else lists.filter { it.category == selectedCategory } + if (selectedCategory == "all") lists + else lists.filter { it.category == selectedCategory } } LazyColumn( modifier = modifier .fillMaxSize() - .background(ObsidianBg) - .padding(horizontal = 16.dp), - contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + .background(ObsidianBg), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp) ) { - // Header + // 1. Header item { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = "XFlow Заметки & Списки", - fontSize = 22.sp, - fontWeight = FontWeight.Bold, - color = TextPrimary - ) - Text( - text = "Мульти-записная книжка • Голос Gemini • Синхронизация", - fontSize = 12.sp, - color = TextMuted - ) - } - IconButton( - onClick = { repository.reload() }, - modifier = Modifier - .size(38.dp) - .clip(CircleShape) - .background(ObsidianSurfaceElevated) - .border(1.dp, GlassBorder, CircleShape) + Column(modifier = Modifier.padding(vertical = 4.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = "Обновить", - tint = CyanAccent, - modifier = Modifier.size(18.dp) - ) - } - } - } - - // Category Filter Chips - item { - LazyRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - items(categories) { (key, label) -> - val isSelected = selectedCategory == key - Box( - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background(if (isSelected) NeonViolet.copy(alpha = 0.25f) else ObsidianSurfaceElevated) - .border( - 1.dp, - if (isSelected) NeonViolet else GlassBorder, - RoundedCornerShape(12.dp) - ) - .clickable { selectedCategory = key } - .padding(horizontal = 14.dp, vertical = 8.dp) - ) { + Column { Text( - text = label, + text = "Списки и Заметки", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = TextPrimary + ) + Text( + text = "XFlow синхронизация • Голосовой ввод Ассистента", fontSize = 12.sp, - fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, - color = if (isSelected) TextPrimary else TextSecondary + color = TextMuted + ) + } + + // Button to toggle Assistant Tester + OutlinedButton( + onClick = { showAssistantTester = !showAssistantTester }, + shape = RoundedCornerShape(12.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, NeonViolet.copy(alpha = 0.5f)), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = if (showAssistantTester) NeonViolet.copy(alpha = 0.2f) else ObsidianSurface + ), + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 6.dp) + ) { + Icon( + imageVector = Icons.Default.Mic, + contentDescription = null, + tint = NeonViolet, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = if (showAssistantTester) "Скрыть тест" else "Тест Ассистента", + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = NeonViolet ) } } } } - // Quick Add Item Bar + // 2. Interactive Assistant Simulator Card item { - Column( + AnimatedVisibility(visible = showAssistantTester) { + AssistantTesterCard(context = context) + } + } + + // 3. Category Filter Chips + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + CategoryChip( + label = "Все", + selected = selectedCategory == "all", + onClick = { selectedCategory = "all" } + ) + CategoryChip( + label = "Фильмы 🎬", + selected = selectedCategory == "movies", + onClick = { selectedCategory = "movies" } + ) + CategoryChip( + label = "Покупки 🛒", + selected = selectedCategory == "shopping", + onClick = { selectedCategory = "shopping" } + ) + CategoryChip( + label = "Задачи 📋", + selected = selectedCategory == "tasks", + onClick = { selectedCategory = "tasks" } + ) + } + } + + // 4. Manual Quick Add Box + item { + Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) - .background(ObsidianSurface) + .background(ObsidianSurfaceElevated) .border(1.dp, GlassBorder, RoundedCornerShape(16.dp)) .padding(12.dp) ) { Row( + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { @@ -189,7 +213,9 @@ fun ListsScreen(modifier: Modifier = Modifier) { onValueChange = { newItemText = it }, placeholder = { Text( - if (selectedCategory == "movies") "Название фильма на просмотр..." else "Новая запись...", + if (selectedCategory == "movies") "Название фильма на просмотр..." + else if (selectedCategory == "shopping") "Купить..." + else "Новая запись или заметка...", fontSize = 13.sp, color = TextMuted ) @@ -214,7 +240,8 @@ fun ListsScreen(modifier: Modifier = Modifier) { val listTitle = when (targetCategory) { "movies" -> "Фильмы на просмотр" "shopping" -> "Покупки" - else -> "Общий список" + "tasks" -> "Задачи" + else -> "Быстрые заметки" } repository.addNoteItem( title = newItemText.trim(), @@ -235,20 +262,31 @@ fun ListsScreen(modifier: Modifier = Modifier) { } } - // Lists and Items + // 5. Lists and Items if (filteredLists.isEmpty()) { item { Box( modifier = Modifier .fillMaxWidth() - .padding(vertical = 40.dp), + .clip(RoundedCornerShape(16.dp)) + .background(ObsidianSurface) + .padding(vertical = 40.dp, horizontal = 20.dp), contentAlignment = Alignment.Center ) { - Text( - "Нет записей. Добавьте первую заметку или скажите Gemini!", - fontSize = 13.sp, - color = TextMuted - ) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "Нет записей в выбранной категории", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = TextSecondary + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = "Добавьте запись вручную или нажмите «Тест Ассистента» выше!", + fontSize = 12.sp, + color = TextMuted + ) + } } } } else { @@ -267,6 +305,253 @@ fun ListsScreen(modifier: Modifier = Modifier) { } } +@Composable +private fun AssistantTesterCard(context: Context) { + var customVoiceQuery by remember { mutableStateOf("") } + var selectedPreset by remember { mutableStateOf("") } + + val clipboard = remember { context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } + + val adbCommand = remember(customVoiceQuery) { + val query = if (customVoiceQuery.isNotBlank()) customVoiceQuery else "Интерстеллар" + "adb shell am start -a android.intent.action.CREATE_NOTE -e android.intent.extra.TEXT \"$query\" com.example.avtex" + } + + fun simulateVoiceCommand(text: String) { + val intent = Intent(context, MainActivity::class.java).apply { + action = Intent.ACTION_CREATE_NOTE + putExtra(Intent.EXTRA_TEXT, text) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP + } + context.startActivity(intent) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(20.dp)) + .background(ObsidianSurfaceElevated) + .border(1.dp, NeonViolet.copy(alpha = 0.4f), RoundedCornerShape(20.dp)) + .padding(16.dp) + ) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .clip(CircleShape) + .background(NeonViolet.copy(alpha = 0.2f)) + .padding(8.dp) + ) { + Icon( + imageVector = Icons.Default.Mic, + contentDescription = null, + tint = NeonViolet, + modifier = Modifier.size(18.dp) + ) + } + Spacer(modifier = Modifier.width(10.dp)) + Column { + Text( + text = "Симулятор голосового Ассистента", + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + color = TextPrimary + ) + Text( + text = "Проверка распознавания и авто-сортировки заметок", + fontSize = 11.sp, + color = TextMuted + ) + } + } + } + + Text( + text = "Быстрые сценарии тестирования (нажмите для проверки):", + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = TextSecondary + ) + + // Preset Buttons Grid + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + AssistantPresetButton( + label = "🎬 Фильм: «Запиши фильм Начало на вечер»", + onClick = { simulateVoiceCommand("фильм Начало на вечер") } + ) + AssistantPresetButton( + label = "🛒 Покупки: «Купить молоко, кофе и сыр»", + onClick = { simulateVoiceCommand("купить молоко, кофе и сыр") } + ) + AssistantPresetButton( + label = "📋 Задача: «Позвонить автомеханику в 11»", + onClick = { simulateVoiceCommand("задача позвонить автомеханику в 11") } + ) + AssistantPresetButton( + label = "💡 Заметка: «Идея для нового проекта XFlow»", + onClick = { simulateVoiceCommand("Идея для нового проекта XFlow") } + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + // Custom text input + Text( + text = "Своя фраза для проверки:", + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = TextSecondary + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = customVoiceQuery, + onValueChange = { customVoiceQuery = it }, + placeholder = { Text("Например: сериал Локи 2 сезон", fontSize = 12.sp, color = TextMuted) }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(10.dp), + singleLine = true, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = NeonViolet, + unfocusedBorderColor = GlassBorder, + focusedTextColor = TextPrimary, + unfocusedTextColor = TextPrimary + ) + ) + + Button( + onClick = { + if (customVoiceQuery.isNotBlank()) { + simulateVoiceCommand(customVoiceQuery.trim()) + customVoiceQuery = "" + } + }, + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors(containerColor = NeonViolet) + ) { + Icon(Icons.Default.PlayArrow, contentDescription = "Запуск", tint = Color.White) + } + } + + // ADB command helper + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .background(ObsidianSurface) + .border(1.dp, GlassBorder, RoundedCornerShape(10.dp)) + .padding(10.dp) + ) { + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.Terminal, contentDescription = null, tint = CyanAccent, modifier = Modifier.size(14.dp)) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "Команда для теста через ADB с ПК:", + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = CyanAccent + ) + } + IconButton( + onClick = { + clipboard.setPrimaryClip(ClipData.newPlainText("adb command", adbCommand)) + Toast.makeText(context, "Команда скопирована в буфер", Toast.LENGTH_SHORT).show() + }, + modifier = Modifier.size(24.dp) + ) { + Icon(Icons.Default.ContentCopy, contentDescription = "Копировать", tint = TextMuted, modifier = Modifier.size(14.dp)) + } + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = adbCommand, + fontSize = 10.sp, + fontFamily = FontFamily.Monospace, + color = TextSecondary, + lineHeight = 13.sp + ) + } + } + } + } +} + +@Composable +private fun AssistantPresetButton( + label: String, + onClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(ObsidianSurface) + .border(1.dp, GlassBorder, RoundedCornerShape(8.dp)) + .clickable { onClick() } + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = TextPrimary + ) + Text( + text = "ТЕСТ ▶", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = NeonViolet + ) + } + } +} + +@Composable +private fun CategoryChip( + label: String, + selected: Boolean, + onClick: () -> Unit +) { + FilterChip( + selected = selected, + onClick = onClick, + label = { Text(label, fontSize = 12.sp) }, + shape = RoundedCornerShape(10.dp), + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = NeonViolet.copy(alpha = 0.25f), + selectedLabelColor = Color.White, + containerColor = ObsidianSurfaceElevated, + labelColor = TextMuted + ), + border = FilterChipDefaults.filterChipBorder( + enabled = true, + selected = selected, + borderColor = GlassBorder, + selectedBorderColor = NeonViolet.copy(alpha = 0.6f) + ) + ) +} + @Composable private fun ListCard( list: NoteList, @@ -283,9 +568,9 @@ private fun ListCard( Column( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .background(ObsidianSurface) - .border(1.dp, GlassBorder, RoundedCornerShape(16.dp)) + .clip(RoundedCornerShape(18.dp)) + .background(ObsidianSurfaceElevated) + .border(1.dp, GlassBorder, RoundedCornerShape(18.dp)) .padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp) ) { @@ -307,16 +592,24 @@ private fun ListCard( ) Text( text = list.title, - fontSize = 15.sp, - fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, color = TextPrimary ) } - Text( - text = "${list.items.count { !it.isCompleted }} активных", - fontSize = 11.sp, - color = TextMuted - ) + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(accentColor.copy(alpha = 0.15f)) + .padding(horizontal = 8.dp, vertical = 2.dp) + ) { + Text( + text = "${list.items.count { !it.isCompleted }} активных", + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + color = accentColor + ) + } } // Items list @@ -325,7 +618,7 @@ private fun ListCard( text = "Список пуст", fontSize = 12.sp, color = TextMuted, - modifier = Modifier.padding(start = 16.dp) + modifier = Modifier.padding(start = 16.dp, top = 4.dp, bottom = 4.dp) ) } else { list.items.forEach { item -> @@ -351,10 +644,10 @@ private fun ItemRow( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(10.dp)) - .background(ObsidianSurfaceElevated) + .background(ObsidianSurface) .border(1.dp, GlassBorder.copy(alpha = 0.5f), RoundedCornerShape(10.dp)) .clickable { onToggle() } - .padding(horizontal = 10.dp, vertical = 6.dp), + .padding(horizontal = 10.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Checkbox( @@ -381,13 +674,15 @@ private fun ItemRow( Text( text = "🎬 Буду смотреть", fontSize = 10.sp, - color = CyanAccent + color = CyanAccent, + fontWeight = FontWeight.Medium ) } else if (item.status == "watched") { Text( text = "✔ Просмотрено", fontSize = 10.sp, - color = EmeraldMint + color = EmeraldMint, + fontWeight = FontWeight.Medium ) } } diff --git a/apps/mobile/app/src/main/java/com/example/avtex/ui/SettingsScreen.kt b/apps/mobile/app/src/main/java/com/example/avtex/ui/SettingsScreen.kt index e80a709..83fc4c4 100644 --- a/apps/mobile/app/src/main/java/com/example/avtex/ui/SettingsScreen.kt +++ b/apps/mobile/app/src/main/java/com/example/avtex/ui/SettingsScreen.kt @@ -1,5 +1,7 @@ package com.example.avtex.ui +import android.content.Context + import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -80,6 +82,12 @@ fun SettingsScreen(modifier: Modifier = Modifier) { var token by remember(settings.hacToken) { mutableStateOf(settings.hacToken) } var inactivityMinutes by remember(settings.inactivityMinutes) { mutableFloatStateOf(settings.inactivityMinutes.toFloat()) } var confidenceThreshold by remember(settings.confidenceThreshold) { mutableFloatStateOf(settings.confidenceThreshold.toFloat()) } + val context = androidx.compose.ui.platform.LocalContext.current + val xflowPrefs = remember { context.getSharedPreferences("xflow_prefs", Context.MODE_PRIVATE) } + var xflowServerUrl by remember { + mutableStateOf(xflowPrefs.getString("xflow_server_url", "https://xflow.controlsystem.kz") ?: "https://xflow.controlsystem.kz") + } + var xflowTestStatus by remember { mutableStateOf(null) } var broadcastIntents by remember(settings.broadcastIntents) { mutableStateOf(settings.broadcastIntents) } var isSaved by remember { mutableStateOf(false) } @@ -382,6 +390,107 @@ fun SettingsScreen(modifier: Modifier = Modifier) { } } + // XFlow Cloud Server Card + item { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(22.dp)) + .background(ObsidianSurfaceElevated) + .border(1.dp, GlassBorder, RoundedCornerShape(22.dp)) + .padding(18.dp) + ) { + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .background(CyanAccent.copy(alpha = 0.15f)) + .padding(8.dp) + ) { + Icon( + imageVector = Icons.Default.Link, + contentDescription = null, + tint = CyanAccent, + modifier = Modifier.size(20.dp) + ) + } + Spacer(modifier = Modifier.width(10.dp)) + Column { + Text( + text = "Сервер XFlow (Coolify / Cloud)", + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = TextPrimary + ) + Text( + text = "Синхронизация Canvas майндкарт и заметок", + fontSize = 11.sp, + color = TextMuted + ) + } + } + + Spacer(modifier = Modifier.height(14.dp)) + + OutlinedTextField( + value = xflowServerUrl, + onValueChange = { xflowServerUrl = it; isSaved = false }, + label = { Text("URL сервера XFlow") }, + placeholder = { Text("https://xflow.controlsystem.kz") }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(14.dp), + singleLine = true, + colors = customTextFieldColors() + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = xflowTestStatus ?: "Статус: https://xflow.controlsystem.kz", + fontSize = 12.sp, + color = if (xflowTestStatus?.contains("OK") == true) EmeraldMint else TextMuted, + modifier = Modifier.weight(1f) + ) + androidx.compose.material3.TextButton( + onClick = { + coroutineScope.launch { + xflowTestStatus = "Проверка..." + try { + val client = okhttp3.OkHttpClient.Builder() + .connectTimeout(5, java.util.concurrent.TimeUnit.SECONDS) + .build() + val req = okhttp3.Request.Builder() + .url("${xflowServerUrl.trimEnd('/')}/api/canvas") + .build() + val resp = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { + client.newCall(req).execute() + } + resp.use { + if (it.isSuccessful) { + xflowTestStatus = "Подключено: HTTP ${it.code} OK" + } else { + xflowTestStatus = "Ответ: HTTP ${it.code}" + } + } + } catch (e: Exception) { + xflowTestStatus = "Ошибка: ${e.localizedMessage}" + } + } + } + ) { + Text("Проверить", color = CyanAccent, fontSize = 13.sp) + } + } + } + } + } + // 4. Save Button item { Button( @@ -398,6 +507,7 @@ fun SettingsScreen(modifier: Modifier = Modifier) { confidenceThreshold = confidenceThreshold.roundToInt(), broadcastIntents = broadcastIntents ) + xflowPrefs.edit().putString("xflow_server_url", xflowServerUrl.trim()).apply() isSaved = true } }, diff --git a/apps/mobile/app/src/main/res/xml/shortcuts.xml b/apps/mobile/app/src/main/res/xml/shortcuts.xml index 6cea091..ef1ce7e 100644 --- a/apps/mobile/app/src/main/res/xml/shortcuts.xml +++ b/apps/mobile/app/src/main/res/xml/shortcuts.xml @@ -1,7 +1,23 @@ - + + + + + + + + +