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 @@
-
+
+
+
+
+
+
+
+
+