diff --git a/app/src/main/java/com/shaarit/core/storage/HiddenTagsPreferences.kt b/app/src/main/java/com/shaarit/core/storage/HiddenTagsPreferences.kt new file mode 100644 index 0000000..6f03337 --- /dev/null +++ b/app/src/main/java/com/shaarit/core/storage/HiddenTagsPreferences.kt @@ -0,0 +1,228 @@ +package com.shaarit.core.storage + +import android.content.Context +import android.content.SharedPreferences +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Définition d'un tag système prédéfini. + * @param tag Le nom du tag (ou pattern wildcard comme "note-color-*") + * @param label Nom affiché à l'utilisateur + * @param description Courte description du rôle du tag + * @param isWildcard Si true, le tag utilise un pattern wildcard (ex: "note-color-*") + * @param defaultHidden Si true, le tag est caché par défaut + */ +data class PresetSystemTag( + val tag: String, + val label: String, + val description: String, + val isWildcard: Boolean = false, + val defaultHidden: Boolean = true +) + +@Singleton +class HiddenTagsPreferences @Inject constructor( + @ApplicationContext private val context: Context +) { + private val prefs: SharedPreferences = + context.getSharedPreferences("hidden_tags_prefs", Context.MODE_PRIVATE) + + companion object { + private const val KEY_PRESET_PREFIX = "preset_hidden_" + private const val KEY_CUSTOM_TAGS = "custom_hidden_tags" + + /** + * Liste des tags systèmes prédéfinis utilisés par l'application. + */ + val PRESET_SYSTEM_TAGS = listOf( + PresetSystemTag( + tag = "note", + label = "note", + description = "Notes - Identifiant interne des notes", + defaultHidden = true + ), + PresetSystemTag( + tag = "shaarli-pin", + label = "shaarli-pin", + description = "Épinglé - Garde les favoris en haut", + defaultHidden = true + ), + PresetSystemTag( + tag = "note-color-*", + label = "note-color-*", + description = "Couleurs des Notes - Wildcard pour les couleurs", + isWildcard = true, + defaultHidden = true + ), + PresetSystemTag( + tag = "notebg-*", + label = "notebg-*", + description = "Fonds des Notes - Wildcard pour les arrière-plans", + isWildcard = true, + defaultHidden = true + ), + PresetSystemTag( + tag = "notefilter-*", + label = "notefilter-*", + description = "Filtres des Notes - Wildcard pour les filtres", + isWildcard = true, + defaultHidden = true + ), + PresetSystemTag( + tag = "readitlater", + label = "readitlater", + description = "À lire plus tard - Liste de lecture temporaire", + defaultHidden = true + ), + PresetSystemTag( + tag = "shaarli-archive", + label = "shaarli-archive", + description = "Archivé - Notes archivées", + defaultHidden = false + ), + PresetSystemTag( + tag = "todo", + label = "todo", + description = "Tâches - Identifiant interne des tâches", + defaultHidden = true + ), + PresetSystemTag( + tag = "brain-dump", + label = "brain-dump", + description = "Brain Dump - Capture rapide d'idées", + defaultHidden = true + ) + ) + } + + // État réactif pour les presets + private val _presetStates = MutableStateFlow(loadAllPresetStates()) + val presetStates: StateFlow> = _presetStates.asStateFlow() + + // État réactif pour les tags custom + private val _customHiddenTags = MutableStateFlow(loadCustomHiddenTags()) + val customHiddenTags: StateFlow> = _customHiddenTags.asStateFlow() + + /** + * Charge l'état (caché ou non) de chaque tag preset depuis SharedPreferences. + */ + private fun loadAllPresetStates(): Map { + return PRESET_SYSTEM_TAGS.associate { preset -> + preset.tag to prefs.getBoolean( + KEY_PRESET_PREFIX + preset.tag, + preset.defaultHidden + ) + } + } + + /** + * Charge la liste des tags custom cachés. + */ + private fun loadCustomHiddenTags(): Set { + return prefs.getStringSet(KEY_CUSTOM_TAGS, emptySet()) ?: emptySet() + } + + /** + * Active/désactive le masquage d'un tag preset. + */ + fun setPresetHidden(tag: String, hidden: Boolean) { + prefs.edit().putBoolean(KEY_PRESET_PREFIX + tag, hidden).apply() + _presetStates.value = loadAllPresetStates() + } + + /** + * Vérifie si un tag preset est caché. + */ + fun isPresetHidden(tag: String): Boolean { + return _presetStates.value[tag] ?: PRESET_SYSTEM_TAGS.find { it.tag == tag }?.defaultHidden ?: false + } + + /** + * Ajoute un tag custom à la liste des tags cachés. + */ + fun addCustomHiddenTag(tag: String) { + val normalizedTag = tag.trim().lowercase() + if (normalizedTag.isBlank()) return + val currentTags = loadCustomHiddenTags().toMutableSet() + currentTags.add(normalizedTag) + prefs.edit().putStringSet(KEY_CUSTOM_TAGS, currentTags).apply() + _customHiddenTags.value = currentTags + } + + /** + * Supprime un tag custom de la liste des tags cachés. + */ + fun removeCustomHiddenTag(tag: String) { + val currentTags = loadCustomHiddenTags().toMutableSet() + currentTags.remove(tag) + prefs.edit().putStringSet(KEY_CUSTOM_TAGS, currentTags).apply() + _customHiddenTags.value = currentTags + } + + /** + * Remet tous les presets à leur valeur par défaut et supprime tous les tags custom. + */ + fun resetToDefaults() { + val editor = prefs.edit() + PRESET_SYSTEM_TAGS.forEach { preset -> + editor.putBoolean(KEY_PRESET_PREFIX + preset.tag, preset.defaultHidden) + } + editor.putStringSet(KEY_CUSTOM_TAGS, emptySet()) + editor.apply() + _presetStates.value = loadAllPresetStates() + _customHiddenTags.value = emptySet() + } + + /** + * Détermine si un tag donné doit être caché (en vérifiant les presets ET les custom). + * Supporte les wildcards (ex: "note-color-*" match "note-color-red"). + */ + fun isTagHidden(tagName: String): Boolean { + val normalizedTag = tagName.lowercase() + + // Vérifier les presets + for (preset in PRESET_SYSTEM_TAGS) { + val isHidden = _presetStates.value[preset.tag] ?: preset.defaultHidden + if (!isHidden) continue + + if (preset.isWildcard) { + val prefix = preset.tag.removeSuffix("*") + if (normalizedTag.startsWith(prefix)) return true + } else { + if (normalizedTag == preset.tag) return true + } + } + + // Vérifier les custom + for (customTag in _customHiddenTags.value) { + if (customTag.endsWith("*")) { + val prefix = customTag.removeSuffix("*") + if (normalizedTag.startsWith(prefix)) return true + } else { + if (normalizedTag == customTag) return true + } + } + + return false + } + + /** + * Retourne tous les tags actuellement cachés (pour debug/info). + */ + fun getAllHiddenTagPatterns(): List { + val result = mutableListOf() + + for (preset in PRESET_SYSTEM_TAGS) { + val isHidden = _presetStates.value[preset.tag] ?: preset.defaultHidden + if (isHidden) result.add(preset.tag) + } + + result.addAll(_customHiddenTags.value) + return result + } +} diff --git a/app/src/main/java/com/shaarit/data/repository/LinkRepositoryImpl.kt b/app/src/main/java/com/shaarit/data/repository/LinkRepositoryImpl.kt index bd90af6..c2e1187 100644 --- a/app/src/main/java/com/shaarit/data/repository/LinkRepositoryImpl.kt +++ b/app/src/main/java/com/shaarit/data/repository/LinkRepositoryImpl.kt @@ -212,6 +212,7 @@ constructor( description = description ?: "", tags = tags ?: emptyList(), isPrivate = isPrivate, + isPinned = tags?.contains("shaarli-pin") ?: false, createdAt = System.currentTimeMillis(), updatedAt = System.currentTimeMillis(), syncStatus = SyncStatus.PENDING_CREATE @@ -255,6 +256,7 @@ constructor( description = description ?: existing.description, tags = tags ?: existing.tags, isPrivate = isPrivate, + isPinned = tags?.contains("shaarli-pin") ?: existing.isPinned, updatedAt = System.currentTimeMillis(), syncStatus = SyncStatus.PENDING_UPDATE ) @@ -318,6 +320,7 @@ constructor( description = description ?: existing.description, tags = tags ?: existing.tags, isPrivate = isPrivate, + isPinned = tags?.contains("shaarli-pin") ?: existing.isPinned, updatedAt = System.currentTimeMillis(), syncStatus = if (existing.syncStatus == SyncStatus.PENDING_CREATE) { SyncStatus.PENDING_CREATE @@ -572,6 +575,7 @@ constructor( description = description ?: "", tags = tags ?: emptyList(), isPrivate = isPrivate ?: false, + isPinned = tags?.contains("shaarli-pin") ?: false, createdAt = parseDate(created), updatedAt = parseDate(updated), syncStatus = SyncStatus.SYNCED diff --git a/app/src/main/java/com/shaarit/data/sync/SyncManager.kt b/app/src/main/java/com/shaarit/data/sync/SyncManager.kt index ae612bb..895464e 100644 --- a/app/src/main/java/com/shaarit/data/sync/SyncManager.kt +++ b/app/src/main/java/com/shaarit/data/sync/SyncManager.kt @@ -451,7 +451,7 @@ class SyncManager @Inject constructor( description = dto.description ?: "", tags = dto.tags ?: emptyList(), isPrivate = dto.isPrivate ?: false, - isPinned = existing?.isPinned ?: false, + isPinned = dto.tags?.contains("shaarli-pin") ?: false, createdAt = parseDate(dto.created), updatedAt = if (serverUpdatedAt > 0L) serverUpdatedAt else existing?.updatedAt ?: syncStartTime, syncStatus = SyncStatus.SYNCED, diff --git a/app/src/main/java/com/shaarit/presentation/add/AddLinkScreen.kt b/app/src/main/java/com/shaarit/presentation/add/AddLinkScreen.kt index 8cfee86..b0cf355 100644 --- a/app/src/main/java/com/shaarit/presentation/add/AddLinkScreen.kt +++ b/app/src/main/java/com/shaarit/presentation/add/AddLinkScreen.kt @@ -458,6 +458,41 @@ fun AddLinkScreen( } } + // Background Selection Section (Compact) + AnimatedVisibility( + visible = contentTypeSelection == ContentType.NOTE, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + CompactFieldCard( + icon = Icons.Default.Image, + label = "Arrière-plan" + ) { + val noteBackgrounds = listOf("Aucun", "cafe", "codes", "dune", "feuilleligne", "feuillequadrille", "fleurs", "foret", "grid", "journal", "lecture", "legumes", "montagnes", "ocean", "radio", "sports", "vague1", "vague2", "ville", "voyage") + val currentBgTag = selectedTags.firstOrNull { it.lowercase().startsWith("notebg-") }?.lowercase()?.removePrefix("notebg-") ?: "Aucun" + + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth().padding(top = 8.dp) + ) { + items(noteBackgrounds) { bgName -> + val isSelected = currentBgTag == bgName + FilterChip( + selected = isSelected, + onClick = { + val existingBgTags = selectedTags.filter { it.lowercase().startsWith("notebg-") } + existingBgTags.forEach { viewModel.removeTag(it) } + if (bgName != "Aucun") { + viewModel.addTag("notebg-$bgName") + } + }, + label = { Text(if (bgName == "Aucun") "Aucun" else bgName.replaceFirstChar { it.uppercase() }) } + ) + } + } + } + } + // Tags Section avec correction du clavier - se positionne au-dessus du clavier GlassCard( modifier = Modifier diff --git a/app/src/main/java/com/shaarit/presentation/edit/EditLinkScreen.kt b/app/src/main/java/com/shaarit/presentation/edit/EditLinkScreen.kt index 9f88063..20527d2 100644 --- a/app/src/main/java/com/shaarit/presentation/edit/EditLinkScreen.kt +++ b/app/src/main/java/com/shaarit/presentation/edit/EditLinkScreen.kt @@ -391,6 +391,41 @@ fun EditLinkScreen( } } + // Background Selection Section (Compact) + AnimatedVisibility( + visible = contentType == ContentType.NOTE, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + CompactFieldCard( + icon = Icons.Default.Image, + label = "Arrière-plan" + ) { + val noteBackgrounds = listOf("Aucun", "cafe", "codes", "dune", "feuilleligne", "feuillequadrille", "fleurs", "foret", "grid", "journal", "lecture", "legumes", "montagnes", "ocean", "radio", "sports", "vague1", "vague2", "ville", "voyage") + val currentBgTag = selectedTags.firstOrNull { it.lowercase().startsWith("notebg-") }?.lowercase()?.removePrefix("notebg-") ?: "Aucun" + + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth().padding(top = 8.dp) + ) { + items(noteBackgrounds) { bgName -> + val isSelected = currentBgTag == bgName + FilterChip( + selected = isSelected, + onClick = { + val existingBgTags = selectedTags.filter { it.lowercase().startsWith("notebg-") } + existingBgTags.forEach { viewModel.removeTag(it) } + if (bgName != "Aucun") { + viewModel.addTag("notebg-$bgName") + } + }, + label = { Text(if (bgName == "Aucun") "Aucun" else bgName.replaceFirstChar { it.uppercase() }) } + ) + } + } + } + } + // Tags Section avec correction du clavier - se positionne au-dessus du clavier GlassCard( modifier = Modifier diff --git a/app/src/main/java/com/shaarit/presentation/feed/FeedScreen.kt b/app/src/main/java/com/shaarit/presentation/feed/FeedScreen.kt index ae76a25..1b5548e 100644 --- a/app/src/main/java/com/shaarit/presentation/feed/FeedScreen.kt +++ b/app/src/main/java/com/shaarit/presentation/feed/FeedScreen.kt @@ -297,7 +297,7 @@ fun FeedScreen( val bookmarkFilter by viewModel.bookmarkFilter.collectAsState() val collections by viewModel.collections.collectAsState() - val tags by viewModel.tags.collectAsState() + val visibleTags by viewModel.visibleTags.collectAsState() val context = LocalContext.current val haptic = LocalHapticFeedback.current @@ -728,8 +728,8 @@ fun FeedScreen( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - // Display all tags ordered by bookmark count (highest first) - tags.forEach { tag -> + // Display all visible tags ordered by bookmark count (highest first) + visibleTags.forEach { tag -> val isSelected = searchTags?.split(" ")?.contains(tag.name) == true DrawerTagChip( tag = tag.name, diff --git a/app/src/main/java/com/shaarit/presentation/feed/FeedViewModel.kt b/app/src/main/java/com/shaarit/presentation/feed/FeedViewModel.kt index 61f347f..71856d8 100644 --- a/app/src/main/java/com/shaarit/presentation/feed/FeedViewModel.kt +++ b/app/src/main/java/com/shaarit/presentation/feed/FeedViewModel.kt @@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn +import androidx.paging.map +import com.shaarit.core.storage.HiddenTagsPreferences import com.shaarit.core.storage.TokenManager import com.shaarit.data.local.dao.CollectionDao import com.shaarit.data.local.dao.TagDao @@ -28,6 +30,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.launch @@ -46,7 +49,8 @@ class FeedViewModel @Inject constructor( private val collectionDao: CollectionDao, private val tagDao: TagDao, private val tokenManager: TokenManager, - private val linkDao: LinkDao + private val linkDao: LinkDao, + val hiddenTagsPreferences: HiddenTagsPreferences ) : ViewModel() { private val _searchQuery = MutableStateFlow("") @@ -76,6 +80,11 @@ class FeedViewModel @Inject constructor( .getAllTags() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + // Tags filtered: hide system/custom hidden tags from the drawer + val visibleTags = combine(tags, hiddenTagsPreferences.presetStates, hiddenTagsPreferences.customHiddenTags) { tagList, _, _ -> + tagList.filter { !hiddenTagsPreferences.isTagHidden(it.name) } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList()) + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) val pagedLinks: Flow> = run { val debouncedSearch = _searchQuery.debounce(300) @@ -93,6 +102,12 @@ class FeedViewModel @Inject constructor( bookmarkFilter = bookmarkFilter ) } + // Filter hidden tags from each link's tag list for display purposes + .map { pagingData: PagingData -> + pagingData.map { link: ShaarliLink -> + link.copy(tags = link.tags.filter { tag -> !hiddenTagsPreferences.isTagHidden(tag) }) + } + } .cachedIn(viewModelScope) } diff --git a/app/src/main/java/com/shaarit/presentation/feed/LinkItemViews.kt b/app/src/main/java/com/shaarit/presentation/feed/LinkItemViews.kt index 5537353..8575026 100644 --- a/app/src/main/java/com/shaarit/presentation/feed/LinkItemViews.kt +++ b/app/src/main/java/com/shaarit/presentation/feed/LinkItemViews.kt @@ -113,10 +113,22 @@ fun ListViewItem( ) } + val context = LocalContext.current + val backgroundResId = remember(link.tags) { + val bgTag = link.tags.firstOrNull { it.lowercase().startsWith("notebg-") } + if (bgTag != null) { + val theme = bgTag.lowercase().removePrefix("notebg-") + val resName = "note_bg_$theme" + val resId = context.resources.getIdentifier(resName, "drawable", context.packageName) + if (resId != 0) resId else null + } else null + } + GlassCard( modifier = Modifier.fillMaxWidth(), onClick = { (onItemClick ?: { onLinkClick(link.url) }).invoke() }, onLongClick = onItemLongClick, + backgroundImage = backgroundResId, glowColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary ) { Column { @@ -338,15 +350,33 @@ fun GridViewItem( ) } + val context = LocalContext.current + val backgroundResId = remember(link.tags) { + val bgTag = link.tags.firstOrNull { it.lowercase().startsWith("notebg-") } + if (bgTag != null) { + val theme = bgTag.lowercase().removePrefix("notebg-") + val resName = "note_bg_$theme" + val resId = context.resources.getIdentifier(resName, "drawable", context.packageName) + if (resId != 0) resId else null + } else null + } + + val baseModifier = Modifier.fillMaxWidth() + val cardModifier = if (backgroundResId != null) { + baseModifier.height(240.dp) + } else { + baseModifier + } + GlassCard( - modifier = Modifier - .fillMaxWidth(), + modifier = cardModifier, onClick = { (onItemClick ?: { onLinkClick(link.url) }).invoke() }, onLongClick = onItemLongClick, + backgroundImage = backgroundResId, glowColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary ) { Column( - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxSize() ) { // Selection checkbox if (selectionMode) { @@ -441,7 +471,7 @@ fun GridViewItem( } } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.weight(1f).heightIn(min = 8.dp)) // Metadata row Row( diff --git a/app/src/main/java/com/shaarit/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/shaarit/presentation/settings/SettingsScreen.kt index 88cf5fc..313c3c3 100644 --- a/app/src/main/java/com/shaarit/presentation/settings/SettingsScreen.kt +++ b/app/src/main/java/com/shaarit/presentation/settings/SettingsScreen.kt @@ -36,7 +36,9 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.shaarit.core.storage.BiometricAuthManager import com.shaarit.core.storage.BiometricAvailability +import com.shaarit.core.storage.HiddenTagsPreferences import com.shaarit.core.storage.LockTimeout +import com.shaarit.core.storage.PresetSystemTag import com.shaarit.core.storage.SecurityPreferences import com.shaarit.core.storage.TimezonePreferences import com.shaarit.data.export.BookmarkImporter @@ -149,6 +151,18 @@ fun SettingsScreen( ) } + // Hidden Tags Section + item { + Spacer(modifier = Modifier.height(16.dp)) + SettingsSection(title = "Tags cachés") + } + + item { + HiddenTagsSettingsItem( + hiddenTagsPreferences = viewModel.hiddenTagsPreferences + ) + } + // Security Section item { Spacer(modifier = Modifier.height(16.dp)) @@ -1320,3 +1334,328 @@ private fun WidgetLinkCountItem() { } } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun HiddenTagsSettingsItem( + hiddenTagsPreferences: HiddenTagsPreferences +) { + var showDialog by remember { mutableStateOf(false) } + val presetStates by hiddenTagsPreferences.presetStates.collectAsState() + val customTags by hiddenTagsPreferences.customHiddenTags.collectAsState() + val hiddenCount = presetStates.count { it.value } + customTags.size + + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { showDialog = true } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.VisibilityOff, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Tags cachés", + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = "$hiddenCount tag(s) masqué(s)", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + if (showDialog) { + HiddenTagsDialog( + hiddenTagsPreferences = hiddenTagsPreferences, + onDismiss = { showDialog = false } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun HiddenTagsDialog( + hiddenTagsPreferences: HiddenTagsPreferences, + onDismiss: () -> Unit +) { + val presetStates by hiddenTagsPreferences.presetStates.collectAsState() + val customTags by hiddenTagsPreferences.customHiddenTags.collectAsState() + var customTagInput by remember { mutableStateOf("") } + + AlertDialog( + onDismissRequest = onDismiss, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp), + title = { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.VisibilityOff, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(28.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = "Tags cachés", + style = MaterialTheme.typography.headlineSmall + ) + } + }, + text = { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + // Info banner + item { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.Top + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Les tags cachés sont des tags fonctionnels utilisés par l'application qui restent invisibles. Ils sont toujours opérationnels pour les fonctions internes.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + // Preset System Tags Section + item { + Spacer(modifier = Modifier.height(16.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "🏷️", + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Tags systèmes prédéfinis", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Activer/désactiver la visibilité des tags système", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // Preset tag items + items( + count = HiddenTagsPreferences.PRESET_SYSTEM_TAGS.size, + key = { HiddenTagsPreferences.PRESET_SYSTEM_TAGS[it].tag } + ) { index -> + val preset = HiddenTagsPreferences.PRESET_SYSTEM_TAGS[index] + val isHidden = presetStates[preset.tag] ?: preset.defaultHidden + + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + hiddenTagsPreferences.setPresetHidden(preset.tag, !isHidden) + } + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = isHidden, + onCheckedChange = { checked -> + hiddenTagsPreferences.setPresetHidden(preset.tag, checked) + }, + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colorScheme.primary, + uncheckedColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = preset.label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = preset.description, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + + // Custom Hidden Tags Section + item { + Spacer(modifier = Modifier.height(20.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "👁️", + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Tags personnalisés cachés", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Ajoutez vos propres tags à masquer (wildcard: note-*)", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // Input field for custom tags + item { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = customTagInput, + onValueChange = { customTagInput = it }, + modifier = Modifier.weight(1f), + placeholder = { + Text( + "Nom du tag...", + style = MaterialTheme.typography.bodySmall + ) + }, + singleLine = true, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), + shape = RoundedCornerShape(12.dp), + textStyle = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.width(8.dp)) + FilledTonalButton( + onClick = { + if (customTagInput.isNotBlank()) { + hiddenTagsPreferences.addCustomHiddenTag(customTagInput) + customTagInput = "" + } + }, + enabled = customTagInput.isNotBlank() + ) { + Icon( + Icons.Default.Add, + contentDescription = "Ajouter", + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text("Ajouter") + } + } + } + + // Custom tags list (chips) + if (customTags.isNotEmpty()) { + item { + Spacer(modifier = Modifier.height(8.dp)) + // Using FlowRow equivalent with wrapping + @OptIn(ExperimentalLayoutApi::class) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + customTags.sorted().forEach { tag -> + InputChip( + selected = true, + onClick = { }, + label = { Text(tag, style = MaterialTheme.typography.bodySmall) }, + trailingIcon = { + IconButton( + onClick = { hiddenTagsPreferences.removeCustomHiddenTag(tag) }, + modifier = Modifier.size(18.dp) + ) { + Icon( + Icons.Default.Close, + contentDescription = "Supprimer", + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.error + ) + } + }, + shape = RoundedCornerShape(20.dp), + colors = InputChipDefaults.inputChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primaryContainer, + selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) + } + } + } + } + + // Reset to defaults + item { + Spacer(modifier = Modifier.height(20.dp)) + OutlinedButton( + onClick = { hiddenTagsPreferences.resetToDefaults() }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp) + ) { + Icon( + Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Réinitialiser par défaut") + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text("Fermer") + } + } + ) +} diff --git a/app/src/main/java/com/shaarit/presentation/settings/SettingsViewModel.kt b/app/src/main/java/com/shaarit/presentation/settings/SettingsViewModel.kt index a3caa0c..ac448df 100644 --- a/app/src/main/java/com/shaarit/presentation/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/shaarit/presentation/settings/SettingsViewModel.kt @@ -4,6 +4,7 @@ import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.shaarit.core.storage.BiometricAuthManager +import com.shaarit.core.storage.HiddenTagsPreferences import com.shaarit.core.storage.SecurityPreferences import com.shaarit.core.storage.TimezonePreferences import com.shaarit.core.storage.TokenManager @@ -37,7 +38,8 @@ class SettingsViewModel @Inject constructor( val themePreferences: ThemePreferences, val securityPreferences: SecurityPreferences, val biometricAuthManager: BiometricAuthManager, - val timezonePreferences: TimezonePreferences + val timezonePreferences: TimezonePreferences, + val hiddenTagsPreferences: HiddenTagsPreferences ) : ViewModel() { private val _uiState = MutableStateFlow(SettingsUiState()) diff --git a/app/src/main/java/com/shaarit/presentation/tags/TagsViewModel.kt b/app/src/main/java/com/shaarit/presentation/tags/TagsViewModel.kt index 968f861..25a4b0d 100644 --- a/app/src/main/java/com/shaarit/presentation/tags/TagsViewModel.kt +++ b/app/src/main/java/com/shaarit/presentation/tags/TagsViewModel.kt @@ -2,6 +2,7 @@ package com.shaarit.presentation.tags import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.shaarit.core.storage.HiddenTagsPreferences import com.shaarit.domain.model.ShaarliLink import com.shaarit.domain.model.ShaarliTag import com.shaarit.domain.repository.LinkRepository @@ -12,7 +13,10 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @HiltViewModel -class TagsViewModel @Inject constructor(private val linkRepository: LinkRepository) : ViewModel() { +class TagsViewModel @Inject constructor( + private val linkRepository: LinkRepository, + private val hiddenTagsPreferences: HiddenTagsPreferences +) : ViewModel() { private val _uiState = MutableStateFlow(TagsUiState.Loading) val uiState = _uiState.asStateFlow() @@ -62,11 +66,14 @@ class TagsViewModel @Inject constructor(private val linkRepository: LinkReposito if (state !is TagsUiState.Success) return emptyList() val query = _searchQuery.value.lowercase() - return if (query.isBlank()) { + val filteredBySearch = if (query.isBlank()) { state.tags } else { state.tags.filter { it.name.lowercase().contains(query) } } + + // Filter out hidden tags + return filteredBySearch.filter { !hiddenTagsPreferences.isTagHidden(it.name) } } fun onTagSelected(tag: ShaarliTag) { diff --git a/app/src/main/java/com/shaarit/ui/components/PremiumComponents.kt b/app/src/main/java/com/shaarit/ui/components/PremiumComponents.kt index 367afd2..66721ee 100644 --- a/app/src/main/java/com/shaarit/ui/components/PremiumComponents.kt +++ b/app/src/main/java/com/shaarit/ui/components/PremiumComponents.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.ui.draw.paint import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.ExperimentalFoundationApi @@ -34,6 +35,7 @@ fun GlassCard( onClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, glowColor: Color = MaterialTheme.colorScheme.primary, + backgroundImage: Int? = null, content: @Composable ColumnScope.() -> Unit ) { val interactionSource = remember { MutableInteractionSource() } @@ -59,43 +61,26 @@ fun GlassCard( animationSpec = tween(150) ) - val cardModifier = - modifier - .graphicsLayer { - scaleX = animatedScale - scaleY = animatedScale - } - .shadow( - elevation = animatedElevation, - shape = RoundedCornerShape(16.dp), - ambientColor = glowColor.copy(alpha = 0.1f), - spotColor = glowColor.copy(alpha = 0.2f) + val glassBackground = Brush.verticalGradient( + colors = listOf( + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (backgroundImage != null) 0.6f else 0.95f), + MaterialTheme.colorScheme.primaryContainer.copy(alpha = if (backgroundImage != null) 0.5f else 0.9f) + ) ) - .clip(RoundedCornerShape(16.dp)) - .background( - brush = - Brush.verticalGradient( - colors = - listOf( - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.95f), - MaterialTheme.colorScheme.primaryContainer.copy( - alpha = 0.9f - ) - ) + + val cardModifier = + modifier + .graphicsLayer { + scaleX = animatedScale + scaleY = animatedScale + } + .shadow( + elevation = animatedElevation, + shape = RoundedCornerShape(16.dp), + ambientColor = glowColor.copy(alpha = 0.1f), + spotColor = glowColor.copy(alpha = 0.2f) ) - ) - .border( - width = 1.dp, - brush = - Brush.linearGradient( - colors = - listOf( - borderColor, - borderColor.copy(alpha = 0.1f) - ) - ), - shape = RoundedCornerShape(16.dp) - ) + .clip(RoundedCornerShape(16.dp)) val finalModifier = if (onClick != null || onLongClick != null) { @@ -109,7 +94,36 @@ fun GlassCard( cardModifier } - Column(modifier = finalModifier.padding(16.dp), content = content) + Box(modifier = finalModifier) { + if (backgroundImage != null) { + androidx.compose.foundation.Image( + painter = androidx.compose.ui.res.painterResource(id = backgroundImage), + contentDescription = null, + contentScale = androidx.compose.ui.layout.ContentScale.Crop, + alignment = Alignment.BottomCenter, + modifier = Modifier.matchParentSize() + ) + } + + // Background and border + Box( + modifier = Modifier + .matchParentSize() + .background(brush = glassBackground) + .border( + width = 1.dp, + brush = Brush.linearGradient( + colors = listOf(borderColor, borderColor.copy(alpha = 0.1f)) + ), + shape = RoundedCornerShape(16.dp) + ) + ) + + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + content = content + ) + } } /** Premium gradient button with glow effect */ diff --git a/app/src/main/res/drawable-night/note_bg_cafe.jpg b/app/src/main/res/drawable-night/note_bg_cafe.jpg new file mode 100644 index 0000000..668ef51 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_cafe.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_codes.jpg b/app/src/main/res/drawable-night/note_bg_codes.jpg new file mode 100644 index 0000000..3ae8aef Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_codes.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_dune.jpg b/app/src/main/res/drawable-night/note_bg_dune.jpg new file mode 100644 index 0000000..05b17e5 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_dune.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_feuilleligne.jpg b/app/src/main/res/drawable-night/note_bg_feuilleligne.jpg new file mode 100644 index 0000000..b79dc4f Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_feuilleligne.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_feuillequadrille.jpg b/app/src/main/res/drawable-night/note_bg_feuillequadrille.jpg new file mode 100644 index 0000000..077e8a8 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_feuillequadrille.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_fleurs.jpg b/app/src/main/res/drawable-night/note_bg_fleurs.jpg new file mode 100644 index 0000000..219ee57 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_fleurs.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_foret.jpg b/app/src/main/res/drawable-night/note_bg_foret.jpg new file mode 100644 index 0000000..59126a7 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_foret.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_grid.jpg b/app/src/main/res/drawable-night/note_bg_grid.jpg new file mode 100644 index 0000000..aa4822f Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_grid.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_journal.jpg b/app/src/main/res/drawable-night/note_bg_journal.jpg new file mode 100644 index 0000000..fdee291 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_journal.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_lecture.jpg b/app/src/main/res/drawable-night/note_bg_lecture.jpg new file mode 100644 index 0000000..03d4d72 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_lecture.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_legumes.jpg b/app/src/main/res/drawable-night/note_bg_legumes.jpg new file mode 100644 index 0000000..96e3bad Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_legumes.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_montagnes.jpg b/app/src/main/res/drawable-night/note_bg_montagnes.jpg new file mode 100644 index 0000000..a3813a9 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_montagnes.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_ocean.jpg b/app/src/main/res/drawable-night/note_bg_ocean.jpg new file mode 100644 index 0000000..61479e2 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_ocean.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_radio.jpg b/app/src/main/res/drawable-night/note_bg_radio.jpg new file mode 100644 index 0000000..72894c3 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_radio.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_sports.jpg b/app/src/main/res/drawable-night/note_bg_sports.jpg new file mode 100644 index 0000000..296c126 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_sports.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_vague1.jpg b/app/src/main/res/drawable-night/note_bg_vague1.jpg new file mode 100644 index 0000000..20c0c38 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_vague1.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_vague2.jpg b/app/src/main/res/drawable-night/note_bg_vague2.jpg new file mode 100644 index 0000000..ae61226 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_vague2.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_ville.jpg b/app/src/main/res/drawable-night/note_bg_ville.jpg new file mode 100644 index 0000000..2326cf9 Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_ville.jpg differ diff --git a/app/src/main/res/drawable-night/note_bg_voyage.jpg b/app/src/main/res/drawable-night/note_bg_voyage.jpg new file mode 100644 index 0000000..b786cad Binary files /dev/null and b/app/src/main/res/drawable-night/note_bg_voyage.jpg differ diff --git a/app/src/main/res/drawable/note_bg_cafe.jpg b/app/src/main/res/drawable/note_bg_cafe.jpg new file mode 100644 index 0000000..885098d Binary files /dev/null and b/app/src/main/res/drawable/note_bg_cafe.jpg differ diff --git a/app/src/main/res/drawable/note_bg_codes.jpg b/app/src/main/res/drawable/note_bg_codes.jpg new file mode 100644 index 0000000..ce4b141 Binary files /dev/null and b/app/src/main/res/drawable/note_bg_codes.jpg differ diff --git a/app/src/main/res/drawable/note_bg_dune.jpg b/app/src/main/res/drawable/note_bg_dune.jpg new file mode 100644 index 0000000..23d21f5 Binary files /dev/null and b/app/src/main/res/drawable/note_bg_dune.jpg differ diff --git a/app/src/main/res/drawable/note_bg_feuilleligne.jpg b/app/src/main/res/drawable/note_bg_feuilleligne.jpg new file mode 100644 index 0000000..eaf05ac Binary files /dev/null and b/app/src/main/res/drawable/note_bg_feuilleligne.jpg differ diff --git a/app/src/main/res/drawable/note_bg_feuillequadrille.jpg b/app/src/main/res/drawable/note_bg_feuillequadrille.jpg new file mode 100644 index 0000000..f4ef2b8 Binary files /dev/null and b/app/src/main/res/drawable/note_bg_feuillequadrille.jpg differ diff --git a/app/src/main/res/drawable/note_bg_fleurs.jpg b/app/src/main/res/drawable/note_bg_fleurs.jpg new file mode 100644 index 0000000..f1668c6 Binary files /dev/null and b/app/src/main/res/drawable/note_bg_fleurs.jpg differ diff --git a/app/src/main/res/drawable/note_bg_foret.jpg b/app/src/main/res/drawable/note_bg_foret.jpg new file mode 100644 index 0000000..ebd110e Binary files /dev/null and b/app/src/main/res/drawable/note_bg_foret.jpg differ diff --git a/app/src/main/res/drawable/note_bg_grid.jpg b/app/src/main/res/drawable/note_bg_grid.jpg new file mode 100644 index 0000000..d9a1928 Binary files /dev/null and b/app/src/main/res/drawable/note_bg_grid.jpg differ diff --git a/app/src/main/res/drawable/note_bg_journal.jpg b/app/src/main/res/drawable/note_bg_journal.jpg new file mode 100644 index 0000000..3095c49 Binary files /dev/null and b/app/src/main/res/drawable/note_bg_journal.jpg differ diff --git a/app/src/main/res/drawable/note_bg_lecture.jpg b/app/src/main/res/drawable/note_bg_lecture.jpg new file mode 100644 index 0000000..cebcee4 Binary files /dev/null and b/app/src/main/res/drawable/note_bg_lecture.jpg differ diff --git a/app/src/main/res/drawable/note_bg_legumes.jpg b/app/src/main/res/drawable/note_bg_legumes.jpg new file mode 100644 index 0000000..18e8ef2 Binary files /dev/null and b/app/src/main/res/drawable/note_bg_legumes.jpg differ diff --git a/app/src/main/res/drawable/note_bg_montagnes.jpg b/app/src/main/res/drawable/note_bg_montagnes.jpg new file mode 100644 index 0000000..f2bb13c Binary files /dev/null and b/app/src/main/res/drawable/note_bg_montagnes.jpg differ diff --git a/app/src/main/res/drawable/note_bg_ocean.jpg b/app/src/main/res/drawable/note_bg_ocean.jpg new file mode 100644 index 0000000..3135cab Binary files /dev/null and b/app/src/main/res/drawable/note_bg_ocean.jpg differ diff --git a/app/src/main/res/drawable/note_bg_radio.jpg b/app/src/main/res/drawable/note_bg_radio.jpg new file mode 100644 index 0000000..ffa07dd Binary files /dev/null and b/app/src/main/res/drawable/note_bg_radio.jpg differ diff --git a/app/src/main/res/drawable/note_bg_sports.jpg b/app/src/main/res/drawable/note_bg_sports.jpg new file mode 100644 index 0000000..3320c62 Binary files /dev/null and b/app/src/main/res/drawable/note_bg_sports.jpg differ diff --git a/app/src/main/res/drawable/note_bg_vague1.jpg b/app/src/main/res/drawable/note_bg_vague1.jpg new file mode 100644 index 0000000..e0212a6 Binary files /dev/null and b/app/src/main/res/drawable/note_bg_vague1.jpg differ diff --git a/app/src/main/res/drawable/note_bg_vague2.jpg b/app/src/main/res/drawable/note_bg_vague2.jpg new file mode 100644 index 0000000..56b62c3 Binary files /dev/null and b/app/src/main/res/drawable/note_bg_vague2.jpg differ diff --git a/app/src/main/res/drawable/note_bg_ville.jpg b/app/src/main/res/drawable/note_bg_ville.jpg new file mode 100644 index 0000000..d586b31 Binary files /dev/null and b/app/src/main/res/drawable/note_bg_ville.jpg differ diff --git a/app/src/main/res/drawable/note_bg_voyage.jpg b/app/src/main/res/drawable/note_bg_voyage.jpg new file mode 100644 index 0000000..cfe95bd Binary files /dev/null and b/app/src/main/res/drawable/note_bg_voyage.jpg differ diff --git a/version.properties b/version.properties index d1164b1..ee274a0 100644 --- a/version.properties +++ b/version.properties @@ -1,3 +1,3 @@ -#Sun Feb 15 10:38:54 2026 -VERSION_NAME=2.1.8 -VERSION_CODE=23 \ No newline at end of file +#Sun Feb 22 14:59:52 2026 +VERSION_NAME=2.5.1 +VERSION_CODE=28 \ No newline at end of file