- Add Google Gemini AI SDK dependency (generativeai:0.9.0) - Implement GeminiRepository with API key management in TokenManager - Create AI enrichment feature with loading states and error handling in AddLinkViewModel - Add AI magic button with shimmer animation to AddLinkScreen for automatic bookmark analysis - Extend ContentType enum with MUSIC and NEWS categories - Enhance content type detection with expande
736 lines
35 KiB
Kotlin
736 lines
35 KiB
Kotlin
package com.shaarit.presentation.edit
|
|
|
|
import androidx.compose.animation.*
|
|
import androidx.compose.animation.core.*
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.layout.*
|
|
import androidx.compose.foundation.lazy.LazyRow
|
|
import androidx.compose.foundation.lazy.items
|
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
|
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
|
import androidx.compose.foundation.rememberScrollState
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.foundation.text.KeyboardActions
|
|
import androidx.compose.foundation.text.KeyboardOptions
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.filled.*
|
|
import androidx.compose.material.icons.outlined.AutoAwesome
|
|
import androidx.compose.material3.*
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.draw.clip
|
|
import androidx.compose.ui.focus.onFocusChanged
|
|
import androidx.compose.ui.graphics.Brush
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.layout.ContentScale
|
|
import androidx.compose.ui.platform.LocalFocusManager
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.input.ImeAction
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.hilt.navigation.compose.hiltViewModel
|
|
import coil.compose.AsyncImage
|
|
import com.shaarit.presentation.add.ContentType
|
|
import com.shaarit.ui.components.*
|
|
import com.shaarit.ui.theme.*
|
|
import com.shaarit.ui.theme.Purple
|
|
import kotlinx.coroutines.launch
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class, androidx.compose.foundation.ExperimentalFoundationApi::class)
|
|
@Composable
|
|
fun EditLinkScreen(
|
|
onNavigateBack: () -> Unit,
|
|
viewModel: EditLinkViewModel = hiltViewModel()
|
|
) {
|
|
val uiState by viewModel.uiState.collectAsState()
|
|
val url by viewModel.url.collectAsState()
|
|
val title by viewModel.title.collectAsState()
|
|
val description by viewModel.description.collectAsState()
|
|
val selectedTags by viewModel.selectedTags.collectAsState()
|
|
val newTagInput by viewModel.newTagInput.collectAsState()
|
|
val availableTags by viewModel.availableTags.collectAsState()
|
|
val isPrivate by viewModel.isPrivate.collectAsState()
|
|
val tagSuggestions by viewModel.tagSuggestions.collectAsState()
|
|
val contentType by viewModel.contentType.collectAsState()
|
|
|
|
val snackbarHostState = remember { SnackbarHostState() }
|
|
val focusManager = LocalFocusManager.current
|
|
var showMarkdownPreview by remember { mutableStateOf(false) }
|
|
val scrollState = rememberScrollState()
|
|
val coroutineScope = rememberCoroutineScope()
|
|
|
|
val aiEnrichmentState by viewModel.aiEnrichmentState.collectAsState()
|
|
|
|
// State pour l'éditeur Markdown avec barre d'outils flottante
|
|
val markdownEditorState = rememberMarkdownEditorState()
|
|
|
|
// Pour faire défiler automatiquement vers la section tags quand le clavier s'ouvre
|
|
val tagsSectionBringIntoViewRequester = remember { BringIntoViewRequester() }
|
|
|
|
LaunchedEffect(uiState) {
|
|
when (val state = uiState) {
|
|
is EditLinkUiState.Success -> {
|
|
onNavigateBack()
|
|
}
|
|
is EditLinkUiState.Error -> {
|
|
snackbarHostState.showSnackbar(state.message)
|
|
}
|
|
else -> {}
|
|
}
|
|
}
|
|
|
|
LaunchedEffect(Unit) {
|
|
viewModel.aiErrorMessage.collect { message ->
|
|
snackbarHostState.showSnackbar(message)
|
|
}
|
|
}
|
|
|
|
LaunchedEffect(aiEnrichmentState) {
|
|
if (aiEnrichmentState is AiEnrichmentState.Success) {
|
|
snackbarHostState.showSnackbar("✨ Enrichissement IA appliqué !")
|
|
viewModel.resetAiEnrichmentState()
|
|
}
|
|
}
|
|
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.background(
|
|
brush = Brush.verticalGradient(colors = listOf(DeepNavy, DarkNavy))
|
|
)
|
|
) {
|
|
Scaffold(
|
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
|
topBar = {
|
|
TopAppBar(
|
|
title = {
|
|
Text(
|
|
if (contentType == ContentType.NOTE) "Modifier la note" else "Modifier le lien",
|
|
style = MaterialTheme.typography.headlineSmall,
|
|
fontWeight = FontWeight.Bold
|
|
)
|
|
},
|
|
navigationIcon = {
|
|
IconButton(onClick = onNavigateBack) {
|
|
Icon(
|
|
Icons.Default.ArrowBack,
|
|
contentDescription = "Retour",
|
|
tint = TextPrimary
|
|
)
|
|
}
|
|
},
|
|
colors = TopAppBarDefaults.topAppBarColors(
|
|
containerColor = DeepNavy.copy(alpha = 0.9f),
|
|
titleContentColor = TextPrimary
|
|
)
|
|
)
|
|
},
|
|
containerColor = androidx.compose.ui.graphics.Color.Transparent
|
|
) { paddingValues ->
|
|
when (uiState) {
|
|
is EditLinkUiState.Loading -> {
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(paddingValues),
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
CircularProgressIndicator(color = CyanPrimary)
|
|
Spacer(modifier = Modifier.height(16.dp))
|
|
Text(
|
|
"Chargement...",
|
|
color = TextSecondary,
|
|
style = MaterialTheme.typography.bodyMedium
|
|
)
|
|
}
|
|
}
|
|
}
|
|
else -> {
|
|
Column(
|
|
modifier = Modifier
|
|
.padding(paddingValues)
|
|
.padding(horizontal = 16.dp)
|
|
.fillMaxSize()
|
|
.imePadding()
|
|
.verticalScroll(scrollState),
|
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
) {
|
|
// Content Type Selection (compact)
|
|
GlassCard(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
glowColor = CyanPrimary
|
|
) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
// Bookmark option
|
|
ContentTypeButton(
|
|
icon = Icons.Default.Bookmark,
|
|
label = "Lien",
|
|
isSelected = contentType == ContentType.BOOKMARK,
|
|
onClick = { viewModel.setContentType(ContentType.BOOKMARK) },
|
|
modifier = Modifier.weight(1f)
|
|
)
|
|
|
|
// Note option
|
|
ContentTypeButton(
|
|
icon = Icons.Default.StickyNote2,
|
|
label = "Note",
|
|
isSelected = contentType == ContentType.NOTE,
|
|
onClick = { viewModel.setContentType(ContentType.NOTE) },
|
|
modifier = Modifier.weight(1f)
|
|
)
|
|
}
|
|
}
|
|
|
|
// URL Section (compact, only for Bookmarks)
|
|
AnimatedVisibility(
|
|
visible = contentType == ContentType.BOOKMARK,
|
|
enter = expandVertically() + fadeIn(),
|
|
exit = shrinkVertically() + fadeOut()
|
|
) {
|
|
CompactFieldCard(
|
|
icon = Icons.Default.Link,
|
|
label = "URL"
|
|
) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
OutlinedTextField(
|
|
value = url,
|
|
onValueChange = { viewModel.url.value = it },
|
|
modifier = Modifier.weight(1f),
|
|
placeholder = { Text("https://example.com", color = TextMuted) },
|
|
singleLine = true,
|
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
|
colors = compactTextFieldColors(),
|
|
shape = RoundedCornerShape(8.dp),
|
|
textStyle = MaterialTheme.typography.bodyMedium
|
|
)
|
|
|
|
// AI Magic Button
|
|
AiMagicButton(
|
|
onClick = { viewModel.analyzeUrlWithAi() },
|
|
isLoading = aiEnrichmentState is AiEnrichmentState.Loading,
|
|
enabled = url.isNotBlank() && aiEnrichmentState !is AiEnrichmentState.Loading
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Title Section (compact)
|
|
CompactFieldCard(
|
|
icon = Icons.Default.Title,
|
|
label = if (contentType == ContentType.NOTE) "Titre *" else "Titre"
|
|
) {
|
|
OutlinedTextField(
|
|
value = title,
|
|
onValueChange = { viewModel.title.value = it },
|
|
modifier = Modifier.fillMaxWidth(),
|
|
placeholder = {
|
|
Text(
|
|
if (contentType == ContentType.NOTE)
|
|
"Titre de la note" else "Titre du lien",
|
|
color = TextMuted
|
|
)
|
|
},
|
|
singleLine = true,
|
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
|
colors = compactTextFieldColors(),
|
|
shape = RoundedCornerShape(8.dp),
|
|
textStyle = MaterialTheme.typography.bodyMedium
|
|
)
|
|
}
|
|
|
|
// Description Section - Markdown Editor (plus grand en mode Note)
|
|
GlassCard(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.then(
|
|
if (contentType == ContentType.NOTE)
|
|
Modifier.heightIn(min = 400.dp)
|
|
else
|
|
Modifier
|
|
)
|
|
) {
|
|
Column {
|
|
// Header avec titre et toggle édition/apercu
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.Default.Description,
|
|
contentDescription = null,
|
|
tint = CyanPrimary,
|
|
modifier = Modifier.size(20.dp)
|
|
)
|
|
Column {
|
|
Text(
|
|
text = if (contentType == ContentType.NOTE)
|
|
"Contenu" else "Description",
|
|
style = MaterialTheme.typography.titleMedium,
|
|
fontWeight = FontWeight.SemiBold,
|
|
color = TextPrimary
|
|
)
|
|
if (contentType == ContentType.NOTE) {
|
|
Text(
|
|
text = "Markdown supporté",
|
|
style = MaterialTheme.typography.labelSmall,
|
|
color = TextMuted
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Toggle édition/apercu simple
|
|
Row(
|
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
|
) {
|
|
IconButton(
|
|
onClick = { showMarkdownPreview = false },
|
|
modifier = Modifier.size(32.dp)
|
|
) {
|
|
Icon(
|
|
Icons.Default.Edit,
|
|
contentDescription = "Éditer",
|
|
tint = if (!showMarkdownPreview) CyanPrimary else TextMuted,
|
|
modifier = Modifier.size(18.dp)
|
|
)
|
|
}
|
|
IconButton(
|
|
onClick = { showMarkdownPreview = true },
|
|
modifier = Modifier.size(32.dp)
|
|
) {
|
|
Icon(
|
|
Icons.Default.Preview,
|
|
contentDescription = "Aperçu",
|
|
tint = if (showMarkdownPreview) CyanPrimary else TextMuted,
|
|
modifier = Modifier.size(18.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
|
|
// Éditeur Markdown ou Aperçu
|
|
if (showMarkdownPreview) {
|
|
MarkdownPreview(
|
|
markdown = description,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.heightIn(
|
|
min = if (contentType == ContentType.NOTE) 300.dp else 150.dp,
|
|
max = if (contentType == ContentType.NOTE) 500.dp else 300.dp
|
|
)
|
|
)
|
|
} else {
|
|
SimpleMarkdownEditor(
|
|
value = description,
|
|
onValueChange = { viewModel.description.value = it },
|
|
editorState = markdownEditorState,
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.heightIn(
|
|
min = if (contentType == ContentType.NOTE) 300.dp else 150.dp,
|
|
max = if (contentType == ContentType.NOTE) 500.dp else 300.dp
|
|
),
|
|
isNoteMode = contentType == ContentType.NOTE,
|
|
placeholder = if (contentType == ContentType.NOTE)
|
|
"Écrivez votre note ici..."
|
|
else
|
|
"Ajoutez une description..."
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tags Section avec correction du clavier - se positionne au-dessus du clavier
|
|
GlassCard(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.bringIntoViewRequester(tagsSectionBringIntoViewRequester)
|
|
) {
|
|
Column {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.Default.Tag,
|
|
contentDescription = null,
|
|
tint = CyanPrimary,
|
|
modifier = Modifier.size(20.dp)
|
|
)
|
|
Text(
|
|
text = "Tags",
|
|
style = MaterialTheme.typography.titleMedium,
|
|
fontWeight = FontWeight.SemiBold,
|
|
color = TextPrimary
|
|
)
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(12.dp))
|
|
|
|
// Selected tags
|
|
if (selectedTags.isNotEmpty()) {
|
|
LazyRow(
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
modifier = Modifier.padding(bottom = 12.dp)
|
|
) {
|
|
items(selectedTags) { tag ->
|
|
TagChip(
|
|
tag = tag,
|
|
isSelected = true,
|
|
onClick = { viewModel.removeTag(tag) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// New tag input - fermer le clavier sur Done
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
OutlinedTextField(
|
|
value = newTagInput,
|
|
onValueChange = { viewModel.onNewTagInputChanged(it) },
|
|
modifier = Modifier
|
|
.weight(1f)
|
|
.onFocusChanged { focusState ->
|
|
if (focusState.isFocused) {
|
|
// Faire défiler vers la section tags quand le champ est focusé
|
|
coroutineScope.launch {
|
|
tagsSectionBringIntoViewRequester.bringIntoView()
|
|
}
|
|
}
|
|
},
|
|
placeholder = { Text("Ajouter un tag...", color = TextMuted) },
|
|
singleLine = true,
|
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
|
keyboardActions = KeyboardActions(
|
|
onDone = {
|
|
if (newTagInput.isNotBlank()) {
|
|
viewModel.addNewTag()
|
|
}
|
|
focusManager.clearFocus()
|
|
}
|
|
),
|
|
colors = compactTextFieldColors(),
|
|
shape = RoundedCornerShape(8.dp),
|
|
textStyle = MaterialTheme.typography.bodyMedium
|
|
)
|
|
IconButton(
|
|
onClick = {
|
|
viewModel.addNewTag()
|
|
focusManager.clearFocus()
|
|
},
|
|
enabled = newTagInput.isNotBlank(),
|
|
modifier = Modifier.size(40.dp)
|
|
) {
|
|
Icon(
|
|
Icons.Default.Add,
|
|
contentDescription = "Ajouter",
|
|
tint = if (newTagInput.isNotBlank()) CyanPrimary else TextMuted
|
|
)
|
|
}
|
|
}
|
|
|
|
// Tag suggestions
|
|
AnimatedVisibility(
|
|
visible = tagSuggestions.isNotEmpty(),
|
|
enter = expandVertically() + fadeIn(),
|
|
exit = shrinkVertically() + fadeOut()
|
|
) {
|
|
Column(modifier = Modifier.padding(top = 12.dp)) {
|
|
Text(
|
|
"Suggestions",
|
|
style = MaterialTheme.typography.labelSmall,
|
|
color = TextMuted,
|
|
modifier = Modifier.padding(bottom = 8.dp)
|
|
)
|
|
LazyRow(
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
items(tagSuggestions.take(8)) { tag ->
|
|
TagChip(
|
|
tag = tag.name,
|
|
isSelected = false,
|
|
onClick = {
|
|
viewModel.addTag(tag.name)
|
|
focusManager.clearFocus()
|
|
},
|
|
count = tag.occurrences
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Popular tags
|
|
if (tagSuggestions.isEmpty() && availableTags.isNotEmpty()) {
|
|
Column(modifier = Modifier.padding(top = 12.dp)) {
|
|
Text(
|
|
"Populaires",
|
|
style = MaterialTheme.typography.labelSmall,
|
|
color = TextMuted,
|
|
modifier = Modifier.padding(bottom = 8.dp)
|
|
)
|
|
LazyRow(
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
) {
|
|
items(
|
|
availableTags
|
|
.filter { it.name !in selectedTags }
|
|
.take(8)
|
|
) { tag ->
|
|
TagChip(
|
|
tag = tag.name,
|
|
isSelected = false,
|
|
onClick = { viewModel.addTag(tag.name) },
|
|
count = tag.occurrences
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Privacy Section (compact)
|
|
CompactFieldCard(
|
|
icon = if (isPrivate) Icons.Default.Lock else Icons.Default.Public,
|
|
label = if (isPrivate) "Privé" else "Public",
|
|
onClick = { viewModel.isPrivate.value = !isPrivate }
|
|
) {
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Text(
|
|
if (isPrivate) "Seul vous pouvez voir" else "Visible par tous",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = TextSecondary
|
|
)
|
|
Switch(
|
|
checked = isPrivate,
|
|
onCheckedChange = { viewModel.isPrivate.value = it },
|
|
colors = SwitchDefaults.colors(
|
|
checkedThumbColor = CyanPrimary,
|
|
checkedTrackColor = CyanPrimary.copy(alpha = 0.3f),
|
|
uncheckedThumbColor = TextMuted,
|
|
uncheckedTrackColor = SurfaceVariant
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
|
|
// Update Button
|
|
GradientButton(
|
|
text = if (uiState is EditLinkUiState.Saving) "Enregistrement..." else
|
|
if (contentType == ContentType.NOTE) "Enregistrer la note" else "Enregistrer les modifications",
|
|
onClick = {
|
|
focusManager.clearFocus()
|
|
viewModel.updateLink()
|
|
},
|
|
modifier = Modifier.fillMaxWidth(),
|
|
enabled = when (contentType) {
|
|
ContentType.BOOKMARK -> url.isNotBlank() && uiState !is EditLinkUiState.Saving
|
|
ContentType.NOTE -> title.isNotBlank() && uiState !is EditLinkUiState.Saving
|
|
}
|
|
)
|
|
|
|
if (uiState is EditLinkUiState.Saving) {
|
|
LinearProgressIndicator(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
color = CyanPrimary,
|
|
trackColor = SurfaceVariant
|
|
)
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(80.dp)) // Espace pour la barre d'outils flottante
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Barre d'outils Markdown flottante - collée au-dessus du clavier
|
|
FloatingMarkdownToolbar(
|
|
editorState = markdownEditorState,
|
|
onValueChange = { viewModel.description.value = it },
|
|
visible = !showMarkdownPreview,
|
|
modifier = Modifier.align(Alignment.BottomCenter)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bouton de sélection de type de contenu compact
|
|
*/
|
|
@Composable
|
|
private fun ContentTypeButton(
|
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
|
label: String,
|
|
isSelected: Boolean,
|
|
onClick: () -> Unit,
|
|
modifier: Modifier = Modifier
|
|
) {
|
|
Surface(
|
|
onClick = onClick,
|
|
shape = RoundedCornerShape(10.dp),
|
|
color = if (isSelected) CyanPrimary.copy(alpha = 0.15f) else CardBackgroundElevated,
|
|
border = if (isSelected) androidx.compose.foundation.BorderStroke(1.5.dp, CyanPrimary) else null,
|
|
modifier = modifier
|
|
) {
|
|
Row(
|
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
horizontalArrangement = Arrangement.Center,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(
|
|
imageVector = icon,
|
|
contentDescription = null,
|
|
tint = if (isSelected) CyanPrimary else TextSecondary,
|
|
modifier = Modifier.size(20.dp)
|
|
)
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
Text(
|
|
text = label,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = if (isSelected) CyanPrimary else TextPrimary,
|
|
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Carte de champ compacte
|
|
*/
|
|
@Composable
|
|
private fun CompactFieldCard(
|
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
|
label: String,
|
|
onClick: (() -> Unit)? = null,
|
|
content: @Composable ColumnScope.() -> Unit
|
|
) {
|
|
val cardModifier = Modifier.fillMaxWidth()
|
|
|
|
val finalModifier = if (onClick != null) {
|
|
cardModifier.clickable(onClick = onClick)
|
|
} else {
|
|
cardModifier
|
|
}
|
|
|
|
GlassCard(
|
|
modifier = finalModifier,
|
|
glowColor = CyanPrimary.copy(alpha = 0.3f)
|
|
) {
|
|
Column {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
modifier = Modifier.padding(bottom = 8.dp)
|
|
) {
|
|
Icon(
|
|
imageVector = icon,
|
|
contentDescription = null,
|
|
tint = CyanPrimary,
|
|
modifier = Modifier.size(18.dp)
|
|
)
|
|
Text(
|
|
text = label,
|
|
style = MaterialTheme.typography.labelMedium,
|
|
color = TextSecondary,
|
|
fontWeight = FontWeight.Medium
|
|
)
|
|
}
|
|
content()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Couleurs pour les champs texte compacts
|
|
*/
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
private fun compactTextFieldColors() = OutlinedTextFieldDefaults.colors(
|
|
focusedBorderColor = CyanPrimary,
|
|
unfocusedBorderColor = SurfaceVariant,
|
|
focusedLabelColor = CyanPrimary,
|
|
unfocusedLabelColor = TextSecondary,
|
|
cursorColor = CyanPrimary,
|
|
focusedContainerColor = CardBackground.copy(alpha = 0.3f),
|
|
unfocusedContainerColor = CardBackground.copy(alpha = 0.2f)
|
|
)
|
|
|
|
/**
|
|
* Bouton Magie IA pour enrichir automatiquement les informations du bookmark
|
|
*/
|
|
@Composable
|
|
private fun AiMagicButton(
|
|
onClick: () -> Unit,
|
|
isLoading: Boolean,
|
|
enabled: Boolean,
|
|
modifier: Modifier = Modifier
|
|
) {
|
|
val infiniteTransition = rememberInfiniteTransition(label = "ai_button")
|
|
val shimmerAlpha by infiniteTransition.animateFloat(
|
|
initialValue = 0.6f,
|
|
targetValue = 1f,
|
|
animationSpec = infiniteRepeatable(
|
|
animation = tween(800, easing = LinearEasing),
|
|
repeatMode = RepeatMode.Reverse
|
|
),
|
|
label = "shimmer"
|
|
)
|
|
|
|
Surface(
|
|
onClick = onClick,
|
|
enabled = enabled && !isLoading,
|
|
shape = RoundedCornerShape(10.dp),
|
|
color = if (enabled) Purple.copy(alpha = if (isLoading) shimmerAlpha * 0.3f else 0.15f) else SurfaceVariant,
|
|
border = if (enabled) androidx.compose.foundation.BorderStroke(1.5.dp, Purple) else null,
|
|
modifier = modifier.size(48.dp)
|
|
) {
|
|
Box(
|
|
contentAlignment = Alignment.Center,
|
|
modifier = Modifier.fillMaxSize()
|
|
) {
|
|
if (isLoading) {
|
|
CircularProgressIndicator(
|
|
modifier = Modifier.size(20.dp),
|
|
color = Purple,
|
|
strokeWidth = 2.dp
|
|
)
|
|
} else {
|
|
Icon(
|
|
imageVector = Icons.Outlined.AutoAwesome,
|
|
contentDescription = "Magie IA",
|
|
tint = if (enabled) Purple else TextMuted,
|
|
modifier = Modifier.size(22.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|