feat: Implement full synchronization manager, new link repository, various UI screens, and note background resources.
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -212,6 +212,7 @@ constructor(
|
|||||||
description = description ?: "",
|
description = description ?: "",
|
||||||
tags = tags ?: emptyList(),
|
tags = tags ?: emptyList(),
|
||||||
isPrivate = isPrivate,
|
isPrivate = isPrivate,
|
||||||
|
isPinned = tags?.contains("shaarli-pin") ?: false,
|
||||||
createdAt = System.currentTimeMillis(),
|
createdAt = System.currentTimeMillis(),
|
||||||
updatedAt = System.currentTimeMillis(),
|
updatedAt = System.currentTimeMillis(),
|
||||||
syncStatus = SyncStatus.PENDING_CREATE
|
syncStatus = SyncStatus.PENDING_CREATE
|
||||||
@ -255,6 +256,7 @@ constructor(
|
|||||||
description = description ?: existing.description,
|
description = description ?: existing.description,
|
||||||
tags = tags ?: existing.tags,
|
tags = tags ?: existing.tags,
|
||||||
isPrivate = isPrivate,
|
isPrivate = isPrivate,
|
||||||
|
isPinned = tags?.contains("shaarli-pin") ?: existing.isPinned,
|
||||||
updatedAt = System.currentTimeMillis(),
|
updatedAt = System.currentTimeMillis(),
|
||||||
syncStatus = SyncStatus.PENDING_UPDATE
|
syncStatus = SyncStatus.PENDING_UPDATE
|
||||||
)
|
)
|
||||||
@ -318,6 +320,7 @@ constructor(
|
|||||||
description = description ?: existing.description,
|
description = description ?: existing.description,
|
||||||
tags = tags ?: existing.tags,
|
tags = tags ?: existing.tags,
|
||||||
isPrivate = isPrivate,
|
isPrivate = isPrivate,
|
||||||
|
isPinned = tags?.contains("shaarli-pin") ?: existing.isPinned,
|
||||||
updatedAt = System.currentTimeMillis(),
|
updatedAt = System.currentTimeMillis(),
|
||||||
syncStatus = if (existing.syncStatus == SyncStatus.PENDING_CREATE) {
|
syncStatus = if (existing.syncStatus == SyncStatus.PENDING_CREATE) {
|
||||||
SyncStatus.PENDING_CREATE
|
SyncStatus.PENDING_CREATE
|
||||||
@ -572,6 +575,7 @@ constructor(
|
|||||||
description = description ?: "",
|
description = description ?: "",
|
||||||
tags = tags ?: emptyList(),
|
tags = tags ?: emptyList(),
|
||||||
isPrivate = isPrivate ?: false,
|
isPrivate = isPrivate ?: false,
|
||||||
|
isPinned = tags?.contains("shaarli-pin") ?: false,
|
||||||
createdAt = parseDate(created),
|
createdAt = parseDate(created),
|
||||||
updatedAt = parseDate(updated),
|
updatedAt = parseDate(updated),
|
||||||
syncStatus = SyncStatus.SYNCED
|
syncStatus = SyncStatus.SYNCED
|
||||||
|
|||||||
@ -451,7 +451,7 @@ class SyncManager @Inject constructor(
|
|||||||
description = dto.description ?: "",
|
description = dto.description ?: "",
|
||||||
tags = dto.tags ?: emptyList(),
|
tags = dto.tags ?: emptyList(),
|
||||||
isPrivate = dto.isPrivate ?: false,
|
isPrivate = dto.isPrivate ?: false,
|
||||||
isPinned = existing?.isPinned ?: false,
|
isPinned = dto.tags?.contains("shaarli-pin") ?: false,
|
||||||
createdAt = parseDate(dto.created),
|
createdAt = parseDate(dto.created),
|
||||||
updatedAt = if (serverUpdatedAt > 0L) serverUpdatedAt else existing?.updatedAt ?: syncStartTime,
|
updatedAt = if (serverUpdatedAt > 0L) serverUpdatedAt else existing?.updatedAt ?: syncStartTime,
|
||||||
syncStatus = SyncStatus.SYNCED,
|
syncStatus = SyncStatus.SYNCED,
|
||||||
|
|||||||
@ -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
|
// Tags Section avec correction du clavier - se positionne au-dessus du clavier
|
||||||
GlassCard(
|
GlassCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@ -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
|
// Tags Section avec correction du clavier - se positionne au-dessus du clavier
|
||||||
GlassCard(
|
GlassCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@ -297,7 +297,7 @@ fun FeedScreen(
|
|||||||
val bookmarkFilter by viewModel.bookmarkFilter.collectAsState()
|
val bookmarkFilter by viewModel.bookmarkFilter.collectAsState()
|
||||||
|
|
||||||
val collections by viewModel.collections.collectAsState()
|
val collections by viewModel.collections.collectAsState()
|
||||||
val tags by viewModel.tags.collectAsState()
|
val visibleTags by viewModel.visibleTags.collectAsState()
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
@ -728,8 +728,8 @@ fun FeedScreen(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
// Display all tags ordered by bookmark count (highest first)
|
// Display all visible tags ordered by bookmark count (highest first)
|
||||||
tags.forEach { tag ->
|
visibleTags.forEach { tag ->
|
||||||
val isSelected = searchTags?.split(" ")?.contains(tag.name) == true
|
val isSelected = searchTags?.split(" ")?.contains(tag.name) == true
|
||||||
DrawerTagChip(
|
DrawerTagChip(
|
||||||
tag = tag.name,
|
tag = tag.name,
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
|
import androidx.paging.map
|
||||||
|
import com.shaarit.core.storage.HiddenTagsPreferences
|
||||||
import com.shaarit.core.storage.TokenManager
|
import com.shaarit.core.storage.TokenManager
|
||||||
import com.shaarit.data.local.dao.CollectionDao
|
import com.shaarit.data.local.dao.CollectionDao
|
||||||
import com.shaarit.data.local.dao.TagDao
|
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.combine
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -46,7 +49,8 @@ class FeedViewModel @Inject constructor(
|
|||||||
private val collectionDao: CollectionDao,
|
private val collectionDao: CollectionDao,
|
||||||
private val tagDao: TagDao,
|
private val tagDao: TagDao,
|
||||||
private val tokenManager: TokenManager,
|
private val tokenManager: TokenManager,
|
||||||
private val linkDao: LinkDao
|
private val linkDao: LinkDao,
|
||||||
|
val hiddenTagsPreferences: HiddenTagsPreferences
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _searchQuery = MutableStateFlow("")
|
private val _searchQuery = MutableStateFlow("")
|
||||||
@ -76,6 +80,11 @@ class FeedViewModel @Inject constructor(
|
|||||||
.getAllTags()
|
.getAllTags()
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
.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)
|
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||||
val pagedLinks: Flow<PagingData<ShaarliLink>> = run {
|
val pagedLinks: Flow<PagingData<ShaarliLink>> = run {
|
||||||
val debouncedSearch = _searchQuery.debounce(300)
|
val debouncedSearch = _searchQuery.debounce(300)
|
||||||
@ -93,6 +102,12 @@ class FeedViewModel @Inject constructor(
|
|||||||
bookmarkFilter = bookmarkFilter
|
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)
|
.cachedIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
GlassCard(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onClick = { (onItemClick ?: { onLinkClick(link.url) }).invoke() },
|
onClick = { (onItemClick ?: { onLinkClick(link.url) }).invoke() },
|
||||||
onLongClick = onItemLongClick,
|
onLongClick = onItemLongClick,
|
||||||
|
backgroundImage = backgroundResId,
|
||||||
glowColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary
|
glowColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary
|
||||||
) {
|
) {
|
||||||
Column {
|
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(
|
GlassCard(
|
||||||
modifier = Modifier
|
modifier = cardModifier,
|
||||||
.fillMaxWidth(),
|
|
||||||
onClick = { (onItemClick ?: { onLinkClick(link.url) }).invoke() },
|
onClick = { (onItemClick ?: { onLinkClick(link.url) }).invoke() },
|
||||||
onLongClick = onItemLongClick,
|
onLongClick = onItemLongClick,
|
||||||
|
backgroundImage = backgroundResId,
|
||||||
glowColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary
|
glowColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.primary
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
// Selection checkbox
|
// Selection checkbox
|
||||||
if (selectionMode) {
|
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
|
// Metadata row
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@ -36,7 +36,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.shaarit.core.storage.BiometricAuthManager
|
import com.shaarit.core.storage.BiometricAuthManager
|
||||||
import com.shaarit.core.storage.BiometricAvailability
|
import com.shaarit.core.storage.BiometricAvailability
|
||||||
|
import com.shaarit.core.storage.HiddenTagsPreferences
|
||||||
import com.shaarit.core.storage.LockTimeout
|
import com.shaarit.core.storage.LockTimeout
|
||||||
|
import com.shaarit.core.storage.PresetSystemTag
|
||||||
import com.shaarit.core.storage.SecurityPreferences
|
import com.shaarit.core.storage.SecurityPreferences
|
||||||
import com.shaarit.core.storage.TimezonePreferences
|
import com.shaarit.core.storage.TimezonePreferences
|
||||||
import com.shaarit.data.export.BookmarkImporter
|
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
|
// Security Section
|
||||||
item {
|
item {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import android.net.Uri
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.shaarit.core.storage.BiometricAuthManager
|
import com.shaarit.core.storage.BiometricAuthManager
|
||||||
|
import com.shaarit.core.storage.HiddenTagsPreferences
|
||||||
import com.shaarit.core.storage.SecurityPreferences
|
import com.shaarit.core.storage.SecurityPreferences
|
||||||
import com.shaarit.core.storage.TimezonePreferences
|
import com.shaarit.core.storage.TimezonePreferences
|
||||||
import com.shaarit.core.storage.TokenManager
|
import com.shaarit.core.storage.TokenManager
|
||||||
@ -37,7 +38,8 @@ class SettingsViewModel @Inject constructor(
|
|||||||
val themePreferences: ThemePreferences,
|
val themePreferences: ThemePreferences,
|
||||||
val securityPreferences: SecurityPreferences,
|
val securityPreferences: SecurityPreferences,
|
||||||
val biometricAuthManager: BiometricAuthManager,
|
val biometricAuthManager: BiometricAuthManager,
|
||||||
val timezonePreferences: TimezonePreferences
|
val timezonePreferences: TimezonePreferences,
|
||||||
|
val hiddenTagsPreferences: HiddenTagsPreferences
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(SettingsUiState())
|
private val _uiState = MutableStateFlow(SettingsUiState())
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.shaarit.presentation.tags
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.shaarit.core.storage.HiddenTagsPreferences
|
||||||
import com.shaarit.domain.model.ShaarliLink
|
import com.shaarit.domain.model.ShaarliLink
|
||||||
import com.shaarit.domain.model.ShaarliTag
|
import com.shaarit.domain.model.ShaarliTag
|
||||||
import com.shaarit.domain.repository.LinkRepository
|
import com.shaarit.domain.repository.LinkRepository
|
||||||
@ -12,7 +13,10 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@HiltViewModel
|
@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)
|
private val _uiState = MutableStateFlow<TagsUiState>(TagsUiState.Loading)
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
@ -62,11 +66,14 @@ class TagsViewModel @Inject constructor(private val linkRepository: LinkReposito
|
|||||||
if (state !is TagsUiState.Success) return emptyList()
|
if (state !is TagsUiState.Success) return emptyList()
|
||||||
|
|
||||||
val query = _searchQuery.value.lowercase()
|
val query = _searchQuery.value.lowercase()
|
||||||
return if (query.isBlank()) {
|
val filteredBySearch = if (query.isBlank()) {
|
||||||
state.tags
|
state.tags
|
||||||
} else {
|
} else {
|
||||||
state.tags.filter { it.name.lowercase().contains(query) }
|
state.tags.filter { it.name.lowercase().contains(query) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out hidden tags
|
||||||
|
return filteredBySearch.filter { !hiddenTagsPreferences.isTagHidden(it.name) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTagSelected(tag: ShaarliTag) {
|
fun onTagSelected(tag: ShaarliTag) {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import androidx.compose.animation.core.animateFloatAsState
|
|||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.ui.draw.paint
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
@ -34,6 +35,7 @@ fun GlassCard(
|
|||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
onLongClick: (() -> Unit)? = null,
|
onLongClick: (() -> Unit)? = null,
|
||||||
glowColor: Color = MaterialTheme.colorScheme.primary,
|
glowColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
backgroundImage: Int? = null,
|
||||||
content: @Composable ColumnScope.() -> Unit
|
content: @Composable ColumnScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
@ -59,6 +61,13 @@ fun GlassCard(
|
|||||||
animationSpec = tween(150)
|
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 =
|
val cardModifier =
|
||||||
modifier
|
modifier
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
@ -72,30 +81,6 @@ fun GlassCard(
|
|||||||
spotColor = glowColor.copy(alpha = 0.2f)
|
spotColor = glowColor.copy(alpha = 0.2f)
|
||||||
)
|
)
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.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 =
|
val finalModifier =
|
||||||
if (onClick != null || onLongClick != null) {
|
if (onClick != null || onLongClick != null) {
|
||||||
@ -109,7 +94,36 @@ fun GlassCard(
|
|||||||
cardModifier
|
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 */
|
/** Premium gradient button with glow effect */
|
||||||
|
|||||||
BIN
app/src/main/res/drawable-night/note_bg_cafe.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
app/src/main/res/drawable-night/note_bg_codes.jpg
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
app/src/main/res/drawable-night/note_bg_dune.jpg
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
app/src/main/res/drawable-night/note_bg_feuilleligne.jpg
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
app/src/main/res/drawable-night/note_bg_feuillequadrille.jpg
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
app/src/main/res/drawable-night/note_bg_fleurs.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
app/src/main/res/drawable-night/note_bg_foret.jpg
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
app/src/main/res/drawable-night/note_bg_grid.jpg
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
app/src/main/res/drawable-night/note_bg_journal.jpg
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
app/src/main/res/drawable-night/note_bg_lecture.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
app/src/main/res/drawable-night/note_bg_legumes.jpg
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
app/src/main/res/drawable-night/note_bg_montagnes.jpg
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
app/src/main/res/drawable-night/note_bg_ocean.jpg
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
app/src/main/res/drawable-night/note_bg_radio.jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
app/src/main/res/drawable-night/note_bg_sports.jpg
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
app/src/main/res/drawable-night/note_bg_vague1.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
app/src/main/res/drawable-night/note_bg_vague2.jpg
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
app/src/main/res/drawable-night/note_bg_ville.jpg
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
app/src/main/res/drawable-night/note_bg_voyage.jpg
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
app/src/main/res/drawable/note_bg_cafe.jpg
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
app/src/main/res/drawable/note_bg_codes.jpg
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
app/src/main/res/drawable/note_bg_dune.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
app/src/main/res/drawable/note_bg_feuilleligne.jpg
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
app/src/main/res/drawable/note_bg_feuillequadrille.jpg
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
app/src/main/res/drawable/note_bg_fleurs.jpg
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
app/src/main/res/drawable/note_bg_foret.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
app/src/main/res/drawable/note_bg_grid.jpg
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
app/src/main/res/drawable/note_bg_journal.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
app/src/main/res/drawable/note_bg_lecture.jpg
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
app/src/main/res/drawable/note_bg_legumes.jpg
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
app/src/main/res/drawable/note_bg_montagnes.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
app/src/main/res/drawable/note_bg_ocean.jpg
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
app/src/main/res/drawable/note_bg_radio.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
app/src/main/res/drawable/note_bg_sports.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
app/src/main/res/drawable/note_bg_vague1.jpg
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
app/src/main/res/drawable/note_bg_vague2.jpg
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
app/src/main/res/drawable/note_bg_ville.jpg
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
app/src/main/res/drawable/note_bg_voyage.jpg
Normal file
|
After Width: | Height: | Size: 108 KiB |
@ -1,3 +1,3 @@
|
|||||||
#Sun Feb 15 10:38:54 2026
|
#Sun Feb 22 14:59:52 2026
|
||||||
VERSION_NAME=2.1.8
|
VERSION_NAME=2.5.1
|
||||||
VERSION_CODE=23
|
VERSION_CODE=28
|
||||||