feat(assistant): add full Assistant note-taking pipeline, intent routing, and in-app Assistant Simulator
This commit is contained in:
@@ -30,14 +30,25 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Deep link intent-filter for Gemini / Assistant App Actions -->
|
<!-- Deep link intent-filter for Gemini / Assistant App Actions & XFlow links -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data
|
<data android:scheme="xflow" />
|
||||||
android:scheme="xflow"
|
</intent-filter>
|
||||||
android:host="lists" />
|
|
||||||
|
<!-- 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>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.example.avtex
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@@ -40,47 +41,147 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
private fun handleIncomingIntent(intent: Intent?) {
|
private fun handleIncomingIntent(intent: Intent?) {
|
||||||
if (intent == null) return
|
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
|
// 1. Google Assistant / Android System Note Action (android.intent.action.CREATE_NOTE)
|
||||||
val data = intent.data
|
if (action == Intent.ACTION_CREATE_NOTE) {
|
||||||
if (data != null && data.scheme == "xflow") {
|
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
val listName = data.getQueryParameter("list_name") ?: data.getQueryParameter("name") ?: "Фильмы на просмотр"
|
?: intent.getStringExtra("android.intent.extra.TEXT")
|
||||||
val itemName = data.getQueryParameter("item_name") ?: data.getQueryParameter("item")
|
?: intent.getStringExtra(Intent.EXTRA_TITLE)
|
||||||
|
val title = intent.getStringExtra(Intent.EXTRA_TITLE)
|
||||||
if (!itemName.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
saveItemFromAssistant(itemName, listName)
|
val fullText = if (!title.isNullOrBlank() && title != text) "$title: $text" else text
|
||||||
|
routeAndSaveNote(fullText, preferredList = null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Обработка через Intent Extras (Google Assistant Built-in Intents)
|
// 2. Android Share Sheet Action (android.intent.action.SEND with text/plain)
|
||||||
val extras = intent.extras
|
if (action == Intent.ACTION_SEND && intent.type?.startsWith("text/") == true) {
|
||||||
if (extras != null) {
|
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||||
val itemName = extras.getString("itemListElement.name") ?: extras.getString("item_name")
|
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
||||||
val listName = extras.getString("itemList.name") ?: extras.getString("list_name") ?: "Фильмы на просмотр"
|
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()) {
|
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 {
|
lifecycleScope.launch {
|
||||||
val repo = AvtexApplication.instance.notesRepository
|
try {
|
||||||
val category = if (listName.contains("фильм", ignoreCase = true)) "movies" else "general"
|
val repo = AvtexApplication.instance.notesRepository
|
||||||
|
repo.addNoteItem(
|
||||||
|
title = cleanText,
|
||||||
|
listTitle = targetList,
|
||||||
|
category = category
|
||||||
|
)
|
||||||
|
|
||||||
repo.addNoteItem(
|
Toast.makeText(
|
||||||
title = itemName,
|
this@MainActivity,
|
||||||
listTitle = listName,
|
"✅ XFlow: Записано в «$targetList»:\n$cleanText",
|
||||||
category = category
|
Toast.LENGTH_LONG
|
||||||
)
|
).show()
|
||||||
|
|
||||||
Toast.makeText(
|
AvtexApplication.instance.settingsRepository.addLog(
|
||||||
this@MainActivity,
|
tag = "Assistant",
|
||||||
"✅ Записано в XFlow: $itemName ($listName)",
|
message = "Заметка через ассистент: \"$cleanText\" в список \"$targetList\"",
|
||||||
Toast.LENGTH_LONG
|
isImportant = true
|
||||||
).show()
|
)
|
||||||
|
} 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
|
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.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
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.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Check
|
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.Delete
|
||||||
import androidx.compose.material.icons.filled.Movie
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material.icons.filled.ShoppingCart
|
import androidx.compose.material.icons.filled.Mic
|
||||||
import androidx.compose.material.icons.filled.TaskAlt
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.Terminal
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CheckboxDefaults
|
import androidx.compose.material3.CheckboxDefaults
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -48,17 +59,18 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.example.avtex.AvtexApplication
|
import com.example.avtex.AvtexApplication
|
||||||
|
import com.example.avtex.MainActivity
|
||||||
import com.example.avtex.notes.NoteItem
|
import com.example.avtex.notes.NoteItem
|
||||||
import com.example.avtex.notes.NoteList
|
import com.example.avtex.notes.NoteList
|
||||||
import com.example.avtex.theme.CyanAccent
|
import com.example.avtex.theme.CyanAccent
|
||||||
import com.example.avtex.theme.ElectricIndigo
|
|
||||||
import com.example.avtex.theme.EmeraldMint
|
import com.example.avtex.theme.EmeraldMint
|
||||||
import com.example.avtex.theme.GlassBorder
|
import com.example.avtex.theme.GlassBorder
|
||||||
import com.example.avtex.theme.NeonViolet
|
import com.example.avtex.theme.NeonViolet
|
||||||
@@ -74,113 +86,125 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ListsScreen(modifier: Modifier = Modifier) {
|
fun ListsScreen(modifier: Modifier = Modifier) {
|
||||||
|
val context = LocalContext.current
|
||||||
val repository = remember { AvtexApplication.instance.notesRepository }
|
val repository = remember { AvtexApplication.instance.notesRepository }
|
||||||
val lists by repository.listsFlow.collectAsState()
|
val lists by repository.listsFlow.collectAsState()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
var selectedCategory by remember { mutableStateOf("all") }
|
var selectedCategory by remember { mutableStateOf("all") }
|
||||||
var newItemText by remember { mutableStateOf("") }
|
var newItemText by remember { mutableStateOf("") }
|
||||||
var isAddingActive by remember { mutableStateOf(false) }
|
var showAssistantTester by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val categories = listOf(
|
|
||||||
"all" to "Все",
|
|
||||||
"movies" to "🎬 Фильмы",
|
|
||||||
"shopping" to "🛒 Покупки",
|
|
||||||
"tasks" to "⚡ Дела"
|
|
||||||
)
|
|
||||||
|
|
||||||
val filteredLists = remember(lists, selectedCategory) {
|
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(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(ObsidianBg)
|
.background(ObsidianBg),
|
||||||
.padding(horizontal = 16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp),
|
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
) {
|
||||||
// Header
|
// 1. Header
|
||||||
item {
|
item {
|
||||||
Row(
|
Column(modifier = Modifier.padding(vertical = 4.dp)) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Column {
|
||||||
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)
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = "Списки и Заметки",
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = TextPrimary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "XFlow синхронизация • Голосовой ввод Ассистента",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
color = TextMuted
|
||||||
color = if (isSelected) TextPrimary else TextSecondary
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
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
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.background(ObsidianSurface)
|
.background(ObsidianSurfaceElevated)
|
||||||
.border(1.dp, GlassBorder, RoundedCornerShape(16.dp))
|
.border(1.dp, GlassBorder, RoundedCornerShape(16.dp))
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
@@ -189,7 +213,9 @@ fun ListsScreen(modifier: Modifier = Modifier) {
|
|||||||
onValueChange = { newItemText = it },
|
onValueChange = { newItemText = it },
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(
|
Text(
|
||||||
if (selectedCategory == "movies") "Название фильма на просмотр..." else "Новая запись...",
|
if (selectedCategory == "movies") "Название фильма на просмотр..."
|
||||||
|
else if (selectedCategory == "shopping") "Купить..."
|
||||||
|
else "Новая запись или заметка...",
|
||||||
fontSize = 13.sp,
|
fontSize = 13.sp,
|
||||||
color = TextMuted
|
color = TextMuted
|
||||||
)
|
)
|
||||||
@@ -214,7 +240,8 @@ fun ListsScreen(modifier: Modifier = Modifier) {
|
|||||||
val listTitle = when (targetCategory) {
|
val listTitle = when (targetCategory) {
|
||||||
"movies" -> "Фильмы на просмотр"
|
"movies" -> "Фильмы на просмотр"
|
||||||
"shopping" -> "Покупки"
|
"shopping" -> "Покупки"
|
||||||
else -> "Общий список"
|
"tasks" -> "Задачи"
|
||||||
|
else -> "Быстрые заметки"
|
||||||
}
|
}
|
||||||
repository.addNoteItem(
|
repository.addNoteItem(
|
||||||
title = newItemText.trim(),
|
title = newItemText.trim(),
|
||||||
@@ -235,20 +262,31 @@ fun ListsScreen(modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lists and Items
|
// 5. Lists and Items
|
||||||
if (filteredLists.isEmpty()) {
|
if (filteredLists.isEmpty()) {
|
||||||
item {
|
item {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 40.dp),
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(ObsidianSurface)
|
||||||
|
.padding(vertical = 40.dp, horizontal = 20.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
"Нет записей. Добавьте первую заметку или скажите Gemini!",
|
Text(
|
||||||
fontSize = 13.sp,
|
text = "Нет записей в выбранной категории",
|
||||||
color = TextMuted
|
fontSize = 14.sp,
|
||||||
)
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = TextSecondary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
text = "Добавьте запись вручную или нажмите «Тест Ассистента» выше!",
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = TextMuted
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
@Composable
|
||||||
private fun ListCard(
|
private fun ListCard(
|
||||||
list: NoteList,
|
list: NoteList,
|
||||||
@@ -283,9 +568,9 @@ private fun ListCard(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(18.dp))
|
||||||
.background(ObsidianSurface)
|
.background(ObsidianSurfaceElevated)
|
||||||
.border(1.dp, GlassBorder, RoundedCornerShape(16.dp))
|
.border(1.dp, GlassBorder, RoundedCornerShape(18.dp))
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
@@ -307,16 +592,24 @@ private fun ListCard(
|
|||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = list.title,
|
text = list.title,
|
||||||
fontSize = 15.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = TextPrimary
|
color = TextPrimary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Box(
|
||||||
text = "${list.items.count { !it.isCompleted }} активных",
|
modifier = Modifier
|
||||||
fontSize = 11.sp,
|
.clip(RoundedCornerShape(8.dp))
|
||||||
color = TextMuted
|
.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
|
// Items list
|
||||||
@@ -325,7 +618,7 @@ private fun ListCard(
|
|||||||
text = "Список пуст",
|
text = "Список пуст",
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
color = TextMuted,
|
color = TextMuted,
|
||||||
modifier = Modifier.padding(start = 16.dp)
|
modifier = Modifier.padding(start = 16.dp, top = 4.dp, bottom = 4.dp)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
list.items.forEach { item ->
|
list.items.forEach { item ->
|
||||||
@@ -351,10 +644,10 @@ private fun ItemRow(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(10.dp))
|
.clip(RoundedCornerShape(10.dp))
|
||||||
.background(ObsidianSurfaceElevated)
|
.background(ObsidianSurface)
|
||||||
.border(1.dp, GlassBorder.copy(alpha = 0.5f), RoundedCornerShape(10.dp))
|
.border(1.dp, GlassBorder.copy(alpha = 0.5f), RoundedCornerShape(10.dp))
|
||||||
.clickable { onToggle() }
|
.clickable { onToggle() }
|
||||||
.padding(horizontal = 10.dp, vertical = 6.dp),
|
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Checkbox(
|
Checkbox(
|
||||||
@@ -381,13 +674,15 @@ private fun ItemRow(
|
|||||||
Text(
|
Text(
|
||||||
text = "🎬 Буду смотреть",
|
text = "🎬 Буду смотреть",
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
color = CyanAccent
|
color = CyanAccent,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
} else if (item.status == "watched") {
|
} else if (item.status == "watched") {
|
||||||
Text(
|
Text(
|
||||||
text = "✔ Просмотрено",
|
text = "✔ Просмотрено",
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
color = EmeraldMint
|
color = EmeraldMint,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.example.avtex.ui
|
package com.example.avtex.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
@@ -80,6 +82,12 @@ fun SettingsScreen(modifier: Modifier = Modifier) {
|
|||||||
var token by remember(settings.hacToken) { mutableStateOf(settings.hacToken) }
|
var token by remember(settings.hacToken) { mutableStateOf(settings.hacToken) }
|
||||||
var inactivityMinutes by remember(settings.inactivityMinutes) { mutableFloatStateOf(settings.inactivityMinutes.toFloat()) }
|
var inactivityMinutes by remember(settings.inactivityMinutes) { mutableFloatStateOf(settings.inactivityMinutes.toFloat()) }
|
||||||
var confidenceThreshold by remember(settings.confidenceThreshold) { mutableFloatStateOf(settings.confidenceThreshold.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 broadcastIntents by remember(settings.broadcastIntents) { mutableStateOf(settings.broadcastIntents) }
|
||||||
|
|
||||||
var isSaved by remember { mutableStateOf(false) }
|
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
|
// 4. Save Button
|
||||||
item {
|
item {
|
||||||
Button(
|
Button(
|
||||||
@@ -398,6 +507,7 @@ fun SettingsScreen(modifier: Modifier = Modifier) {
|
|||||||
confidenceThreshold = confidenceThreshold.roundToInt(),
|
confidenceThreshold = confidenceThreshold.roundToInt(),
|
||||||
broadcastIntents = broadcastIntents
|
broadcastIntents = broadcastIntents
|
||||||
)
|
)
|
||||||
|
xflowPrefs.edit().putString("xflow_server_url", xflowServerUrl.trim()).apply()
|
||||||
isSaved = true
|
isSaved = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
<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">
|
<capability android:name="actions.intent.ADD_ITEM_TO_LIST">
|
||||||
<intent
|
<intent
|
||||||
android:action="android.intent.action.VIEW"
|
android:action="android.intent.action.VIEW"
|
||||||
|
|||||||
Reference in New Issue
Block a user