feat: Implement full synchronization manager, new link repository, various UI screens, and note background resources.

This commit is contained in:
Bruno Charest 2026-02-22 21:07:35 -05:00
parent 34964f7b8c
commit a05f7ce71c
51 changed files with 760 additions and 51 deletions

View File

@ -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<Map<String, Boolean>> = _presetStates.asStateFlow()
// État réactif pour les tags custom
private val _customHiddenTags = MutableStateFlow(loadCustomHiddenTags())
val customHiddenTags: StateFlow<Set<String>> = _customHiddenTags.asStateFlow()
/**
* Charge l'état (caché ou non) de chaque tag preset depuis SharedPreferences.
*/
private fun loadAllPresetStates(): Map<String, Boolean> {
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<String> {
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<String> {
val result = mutableListOf<String>()
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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<PagingData<ShaarliLink>> = 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<ShaarliLink> ->
pagingData.map { link: ShaarliLink ->
link.copy(tags = link.tags.filter { tag -> !hiddenTagsPreferences.isTagHidden(tag) })
}
}
.cachedIn(viewModelScope)
}

View File

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

View File

@ -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")
}
}
)
}

View File

@ -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())

View File

@ -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>(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) {

View File

@ -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,6 +61,13 @@ fun GlassCard(
animationSpec = tween(150)
)
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)
)
)
val cardModifier =
modifier
.graphicsLayer {
@ -72,30 +81,6 @@ fun GlassCard(
spotColor = glowColor.copy(alpha = 0.2f)
)
.clip(RoundedCornerShape(16.dp))
.background(
brush =
Brush.verticalGradient(
colors =
listOf(
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.95f),
MaterialTheme.colorScheme.primaryContainer.copy(
alpha = 0.9f
)
)
)
)
.border(
width = 1.dp,
brush =
Brush.linearGradient(
colors =
listOf(
borderColor,
borderColor.copy(alpha = 0.1f)
)
),
shape = 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 */

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -1,3 +1,3 @@
#Sun Feb 15 10:38:54 2026
VERSION_NAME=2.1.8
VERSION_CODE=23
#Sun Feb 22 14:59:52 2026
VERSION_NAME=2.5.1
VERSION_CODE=28