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 ?: "",
|
||||
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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 */
|
||||
|
||||
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
|
||||
VERSION_NAME=2.1.8
|
||||
VERSION_CODE=23
|
||||
#Sun Feb 22 14:59:52 2026
|
||||
VERSION_NAME=2.5.1
|
||||
VERSION_CODE=28
|
||||