feat(assistant): add full Assistant note-taking pipeline, intent routing, and in-app Assistant Simulator

This commit is contained in:
2026-09-28 21:52:04 +05:00
parent f60ac2a5f3
commit 02de1aaf02
5 changed files with 671 additions and 138 deletions
+15 -4
View File
@@ -30,14 +30,25 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep link intent-filter for Gemini / Assistant App Actions -->
<!-- Deep link intent-filter for Gemini / Assistant App Actions & XFlow links -->
<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="xflow"
android:host="lists" />
<data android:scheme="xflow" />
</intent-filter>
<!-- Google Assistant Native Note-Taking Action ("Сделай заметку...") -->
<intent-filter>
<action android:name="android.intent.action.CREATE_NOTE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- Android System Share Action (Поделиться текстом из любого приложения или ассистента) -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
@@ -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 {
try {
val repo = AvtexApplication.instance.notesRepository
val category = if (listName.contains("фильм", ignoreCase = true)) "movies" else "general"
repo.addNoteItem(
title = itemName,
listTitle = listName,
title = cleanText,
listTitle = targetList,
category = category
)
Toast.makeText(
this@MainActivity,
"✅ Записано в XFlow: $itemName ($listName)",
"✅ XFlow: Записано в «$targetList»:\n$cleanText",
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<String, String> {
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"
}
}
@@ -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,35 +86,30 @@ 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 {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -110,77 +117,94 @@ fun ListsScreen(modifier: Modifier = Modifier) {
) {
Column {
Text(
text = "XFlow Заметки & Списки",
fontSize = 22.sp,
text = "Списки и Заметки",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = TextPrimary
)
Text(
text = "Мульти-записная книжка • Голос Gemini • Синхронизация",
text = "XFlow синхронизация • Голосовой ввод Ассистента",
fontSize = 12.sp,
color = TextMuted
)
}
IconButton(
onClick = { repository.reload() },
modifier = Modifier
.size(38.dp)
.clip(CircleShape)
.background(ObsidianSurfaceElevated)
.border(1.dp, GlassBorder, CircleShape)
// 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.Refresh,
contentDescription = "Обновить",
tint = CyanAccent,
modifier = Modifier.size(18.dp)
imageVector = Icons.Default.Mic,
contentDescription = null,
tint = NeonViolet,
modifier = Modifier.size(16.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)
) {
Spacer(modifier = Modifier.width(6.dp))
Text(
text = label,
fontSize = 12.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
color = if (isSelected) TextPrimary else TextSecondary
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,22 +262,33 @@ 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
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
"Нет записей. Добавьте первую заметку или скажите Gemini!",
fontSize = 13.sp,
text = "Нет записей в выбранной категории",
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
color = TextSecondary
)
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "Добавьте запись вручную или нажмите «Тест Ассистента» выше!",
fontSize = 12.sp,
color = TextMuted
)
}
}
}
} else {
items(filteredLists, key = { it.id }) { list ->
ListCard(
@@ -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,17 +592,25 @@ private fun ListCard(
)
Text(
text = list.title,
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = TextPrimary
)
}
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,
color = TextMuted
fontWeight = FontWeight.Bold,
color = accentColor
)
}
}
// Items list
if (list.items.isEmpty()) {
@@ -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
)
}
}
@@ -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<String?>(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
}
},
+17 -1
View File
@@ -1,7 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Добавление элемента в список через Gemini / Google Assistant -->
<!-- Создание заметки через Gemini / Google Assistant ("Сделай заметку...") -->
<capability android:name="actions.intent.CREATE_NOTE">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.example.avtex"
android:targetClass="com.example.avtex.MainActivity"
android:data="xflow://notes/create">
<parameter
android:name="note.text"
android:key="note_text" />
<parameter
android:name="note.name"
android:key="note_title" />
</intent>
</capability>
<!-- Добавление элемента в список через Gemini / Google Assistant ("Добавь фильм в Avtex...") -->
<capability android:name="actions.intent.ADD_ITEM_TO_LIST">
<intent
android:action="android.intent.action.VIEW"