- Document automatic token verification on startup and note:// prefix for Markdown notes - Add extensive AI capabilities section (Gemini integration, auto-tagging, content classification, multi-model fallback) - Document link health monitoring system with dead link detection and exclusion features - Add file sharing support (Markdown/text files) and deep links documentation - Update technology
1353 lines
47 KiB
Markdown
1353 lines
47 KiB
Markdown
# ShaarIt — Analyse Détaillée des Prochaines Fonctionnalités
|
||
|
||
**Date** : 9 février 2026
|
||
**Version de référence** : v1.0 (47 fichiers Kotlin, ~8 500 lignes)
|
||
**Objectif** : Spécification technique et fonctionnelle des 9 fonctionnalités de la roadmap « Prochaines étapes »
|
||
|
||
---
|
||
|
||
## Table des Matières
|
||
|
||
1. [Mode Lecture sans Distraction (Reader Mode)](#1-mode-lecture-sans-distraction-reader-mode)
|
||
2. [Widget d'Accueil Interactif (Glance)](#2-widget-daccueil-interactif-glance)
|
||
3. [Partage de Collections entre Utilisateurs](#3-partage-de-collections-entre-utilisateurs)
|
||
4. [Support Multi-Instances Shaarli](#4-support-multi-instances-shaarli)
|
||
5. [Verrouillage Biométrique](#5-verrouillage-biométrique)
|
||
6. [Rappels de Lecture (« Lire plus tard »)](#6-rappels-de-lecture--lire-plus-tard-)
|
||
7. [Voice Input (Recherche et Ajout)](#7-voice-input-recherche-et-ajout)
|
||
8. [Adaptive Layouts (Tablettes et Foldables)](#8-adaptive-layouts-tablettes-et-foldables)
|
||
9. [Thème Clair et Material You (Monet)](#9-thème-clair-et-material-you-monet)
|
||
10. [Roadmap Priorisée et Dépendances](#10-roadmap-priorisée-et-dépendances)
|
||
|
||
---
|
||
|
||
## 1. Mode Lecture sans Distraction (Reader Mode)
|
||
|
||
### 1.1 Objectif
|
||
|
||
Proposer un mode lecture immersif qui extrait et affiche le contenu principal d'un article web (texte, images, code) dans une vue Compose épurée, sans publicités, menus ni distractions. Similaire à Firefox Reader View ou Safari Reader Mode.
|
||
|
||
### 1.2 Contexte Existant
|
||
|
||
- L'app dispose déjà d'un éditeur/lecteur Markdown (`MarkdownEditor`, bibliothèque `compose-markdown 0.4.1`)
|
||
- JSoup est déjà intégré pour l'extraction de métadonnées (`LinkMetadataExtractor`)
|
||
- Le champ `excerpt` existe dans `LinkEntity` mais ne contient qu'un résumé court
|
||
- Un `readingTimeMinutes` est déjà calculé et stocké
|
||
|
||
### 1.3 Architecture Proposée
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────┐
|
||
│ ReaderModeScreen │
|
||
│ ┌────────────────────────────────────────────┐ │
|
||
│ │ TopBar : titre, source, temps de lecture │ │
|
||
│ ├────────────────────────────────────────────┤ │
|
||
│ │ │ │
|
||
│ │ Contenu article (Markdown/HTML rendu) │ │
|
||
│ │ - Typographie optimisée lecture │ │
|
||
│ │ - Images inline redimensionnées │ │
|
||
│ │ - Blocs de code avec coloration │ │
|
||
│ │ - Scroll fluide sans jank │ │
|
||
│ │ │ │
|
||
│ ├────────────────────────────────────────────┤ │
|
||
│ │ BottomBar : police, taille, thème lecture │ │
|
||
│ └────────────────────────────────────────────┘ │
|
||
└──────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 1.4 Composants Techniques
|
||
|
||
#### A. Extraction du contenu (Readability)
|
||
|
||
```kotlin
|
||
// Nouveau service dans data/reader/
|
||
interface ArticleExtractor {
|
||
suspend fun extract(url: String): ReadableArticle?
|
||
}
|
||
|
||
data class ReadableArticle(
|
||
val title: String,
|
||
val author: String?,
|
||
val siteName: String?,
|
||
val content: String, // HTML nettoyé ou Markdown
|
||
val leadImage: String?, // Image principale
|
||
val readingTimeMinutes: Int,
|
||
val wordCount: Int
|
||
)
|
||
```
|
||
|
||
**Options d'implémentation** :
|
||
| Option | Bibliothèque | Avantage | Inconvénient |
|
||
|--------|-------------|----------|--------------|
|
||
| **A1** | JSoup + heuristiques maison | Déjà intégré, pas de dépendance | Qualité variable |
|
||
| **A2** | [Readability4J](https://github.com/nicola-moro/readability4j) | Port Java de Mozilla Readability | Dépendance supplémentaire (~50KB) |
|
||
| **A3** | API serveur Shaarli (si disponible) | Extraction côté serveur | Nécessite connexion |
|
||
|
||
**Recommandation** : Option A2 (Readability4J) pour la meilleure qualité d'extraction. Fallback vers JSoup si l'article est déjà en Markdown (notes Shaarli).
|
||
|
||
#### B. Cache offline du contenu
|
||
|
||
```kotlin
|
||
// Nouvelle colonne dans LinkEntity (migration v5 → v6)
|
||
@ColumnInfo(name = "reader_content")
|
||
val readerContent: String? = null,
|
||
|
||
@ColumnInfo(name = "reader_content_fetched_at")
|
||
val readerContentFetchedAt: Long = 0
|
||
```
|
||
|
||
- Contenu extrait et stocké localement pour consultation offline
|
||
- TTL configurable (ex : 7 jours) avant re-extraction
|
||
- WorkManager pour pré-extraction en arrière-plan des liens non lus
|
||
|
||
#### C. Personnalisation de la lecture
|
||
|
||
```kotlin
|
||
data class ReaderPreferences(
|
||
val fontFamily: ReaderFont, // SANS_SERIF, SERIF, MONOSPACE
|
||
val fontSize: TextUnit, // 14sp → 24sp
|
||
val lineSpacing: Float, // 1.2 → 2.0
|
||
val theme: ReaderTheme, // DARK, SEPIA, LIGHT, AUTO
|
||
val textAlign: TextAlign // START, JUSTIFY
|
||
)
|
||
|
||
enum class ReaderFont(val displayName: String) {
|
||
SANS_SERIF("Sans-serif"),
|
||
SERIF("Serif (lecture longue)"),
|
||
MONOSPACE("Monospace (code)")
|
||
}
|
||
```
|
||
|
||
### 1.5 Navigation
|
||
|
||
- Nouveau `Screen.Reader` dans `NavGraph.kt` : `reader/{linkId}`
|
||
- Accessible depuis le bouton « Lire » sur chaque lien dans le feed, l'écran d'édition, et l'écran épinglés
|
||
- Deep link : `shaarit://reader/{linkId}`
|
||
|
||
### 1.6 Estimation
|
||
|
||
| Tâche | Effort | Priorité |
|
||
|-------|--------|----------|
|
||
| `ArticleExtractor` + Readability4J | 4h | Haute |
|
||
| `ReaderModeScreen` (Compose) | 6h | Haute |
|
||
| Personnalisation (police, taille, thème) | 3h | Moyenne |
|
||
| Cache offline + migration Room v6 | 2h | Moyenne |
|
||
| Pré-extraction WorkManager | 2h | Basse |
|
||
| **Total** | **~17h** | |
|
||
|
||
---
|
||
|
||
## 2. Widget d'Accueil Interactif (Glance)
|
||
|
||
### 2.1 Objectif
|
||
|
||
Offrir un widget Android sur l'écran d'accueil permettant de :
|
||
- Voir les derniers liens ajoutés
|
||
- Ajouter rapidement un lien (quick-add)
|
||
- Accéder à un lien aléatoire
|
||
- Voir les statistiques rapides
|
||
|
||
### 2.2 Contexte Existant
|
||
|
||
- Quick Settings Tile (`AddLinkTileService`) déjà implémenté
|
||
- App Shortcuts déjà configurés (Ajouter, Aléatoire, Rechercher, Collections)
|
||
- Deep links fonctionnels (`shaarit://add`, `shaarit://feed`, etc.)
|
||
|
||
### 2.3 Architecture Technique
|
||
|
||
**Framework** : Jetpack Glance (basé sur Compose pour widgets)
|
||
|
||
```kotlin
|
||
// Nouvelle dépendance
|
||
implementation("androidx.glance:glance-appwidget:1.1.0")
|
||
implementation("androidx.glance:glance-material3:1.1.0")
|
||
```
|
||
|
||
#### A. Widget « Derniers Liens » (4×2)
|
||
|
||
```kotlin
|
||
class RecentLinksWidget : GlanceAppWidget() {
|
||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||
val links = getRecentLinks(context) // Room query directe
|
||
|
||
provideContent {
|
||
GlanceTheme {
|
||
RecentLinksContent(links)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
private fun RecentLinksContent(links: List<WidgetLink>) {
|
||
LazyColumn {
|
||
items(links) { link ->
|
||
Row(
|
||
modifier = GlanceModifier
|
||
.fillMaxWidth()
|
||
.clickable(actionStartActivity<MainActivity>(/* deep link */))
|
||
) {
|
||
Text(link.title, style = TextStyle(fontSize = 14.sp))
|
||
Text(link.siteName ?: "", style = TextStyle(color = ColorProvider(Color.Gray)))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Mockup** :
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ 🔖 ShaarIt — Récents [+] [🔀] │
|
||
├─────────────────────────────────────────────┤
|
||
│ 📄 Kotlin Coroutines Deep Dive │
|
||
│ medium.com · il y a 2h │
|
||
├─────────────────────────────────────────────┤
|
||
│ 📹 Understanding Compose State │
|
||
│ youtube.com · il y a 5h │
|
||
├─────────────────────────────────────────────┤
|
||
│ 🛠️ Architecture hexagonale en pratique │
|
||
│ blog.octo.com · hier │
|
||
└─────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### B. Widget « Quick Stats » (2×1)
|
||
|
||
```
|
||
┌─────────────────────┐
|
||
│ 📊 ShaarIt │
|
||
│ 1,247 liens │
|
||
│ 42 cette semaine │
|
||
│ 📚 3h de lecture │
|
||
└─────────────────────┘
|
||
```
|
||
|
||
#### C. Mise à jour du widget
|
||
|
||
```kotlin
|
||
class WidgetUpdateWorker(
|
||
context: Context,
|
||
params: WorkerParameters
|
||
) : CoroutineWorker(context, params) {
|
||
override suspend fun doWork(): Result {
|
||
RecentLinksWidget().updateAll(applicationContext)
|
||
return Result.success()
|
||
}
|
||
}
|
||
```
|
||
|
||
- Mise à jour automatique toutes les 30 min via WorkManager
|
||
- Mise à jour immédiate après chaque ajout/sync via `SyncManager`
|
||
|
||
### 2.4 Fichiers à Créer
|
||
|
||
```
|
||
presentation/widget/
|
||
├── RecentLinksWidget.kt # Widget liens récents
|
||
├── QuickStatsWidget.kt # Widget statistiques
|
||
├── WidgetReceiver.kt # GlanceAppWidgetReceiver
|
||
├── WidgetDataProvider.kt # Accès Room pour le widget
|
||
└── WidgetUpdateWorker.kt # Mise à jour périodique
|
||
```
|
||
|
||
- Déclaration dans `AndroidManifest.xml` : `<receiver>` + `<appwidget-provider>`
|
||
|
||
### 2.5 Estimation
|
||
|
||
| Tâche | Effort | Priorité |
|
||
|-------|--------|----------|
|
||
| Setup Glance + widget receiver | 2h | Haute |
|
||
| Widget « Derniers Liens » (4×2) | 4h | Haute |
|
||
| Widget « Quick Stats » (2×1) | 2h | Moyenne |
|
||
| Mise à jour auto (WorkManager) | 1h | Haute |
|
||
| Configurations multiples (tailles) | 2h | Basse |
|
||
| **Total** | **~11h** | |
|
||
|
||
---
|
||
|
||
## 3. Partage de Collections entre Utilisateurs
|
||
|
||
### 3.1 Objectif
|
||
|
||
Permettre de partager une collection (liste de liens organisée) avec d'autres utilisateurs Shaarli ou via un lien public.
|
||
|
||
### 3.2 Contexte Existant
|
||
|
||
- Collections manuelles et intelligentes déjà implémentées (`CollectionEntity`, `CollectionDao`, `CollectionLinkCrossRef`)
|
||
- Synchronisation des collections via un bookmark serveur (format JSON dans le champ `description` d'un lien Shaarli privé, tag `shaarit_config`)
|
||
- Import/Export déjà supporté (JSON, CSV, HTML)
|
||
|
||
### 3.3 Modes de Partage
|
||
|
||
#### A. Partage via Lien Public (sans authentification)
|
||
|
||
```kotlin
|
||
data class SharedCollection(
|
||
val id: String, // UUID court (ex: "abc123")
|
||
val name: String,
|
||
val description: String?,
|
||
val links: List<SharedLink>,
|
||
val createdBy: String, // URL de l'instance Shaarli
|
||
val sharedAt: Long,
|
||
val expiresAt: Long? // Optionnel : expiration
|
||
)
|
||
```
|
||
|
||
**Mécanisme** : Stocker la collection partagée comme un bookmark Shaarli **public** avec un tag spécial :
|
||
|
||
```kotlin
|
||
// URL unique pour le partage
|
||
val shareUrl = "https://shaarit.app/shared/$collectionUuid"
|
||
|
||
// Bookmark Shaarli public contenant le JSON de la collection
|
||
val sharedBookmark = CreateLinkDto(
|
||
url = shareUrl,
|
||
title = "[Shared] $collectionName",
|
||
description = sharedCollectionJson, // JSON sérialisé
|
||
tags = listOf("shaarit_shared"),
|
||
isPrivate = false // Public pour être accessible
|
||
)
|
||
```
|
||
|
||
#### B. Partage Inter-Instances (via API Shaarli)
|
||
|
||
```
|
||
Instance A Instance B
|
||
┌──────────┐ ┌──────────┐
|
||
│ Exporter │ ──── JSON ────▶ │ Importer │
|
||
│ Collection│ │ Collection│
|
||
└──────────┘ └──────────┘
|
||
│ │
|
||
└── Bookmark public ──── API fetch ──┘
|
||
```
|
||
|
||
#### C. Partage Local (Share Intent Android)
|
||
|
||
- Export de la collection en JSON/HTML via le système de partage Android
|
||
- Import depuis un fichier JSON reçu (déjà partiellement supporté par `BookmarkImporter`)
|
||
|
||
### 3.4 Interface Utilisateur
|
||
|
||
```
|
||
┌──────────────────────────────────────────┐
|
||
│ 📁 Ma Collection "Dev Android" │
|
||
│ │
|
||
│ [🔗 Copier le lien] [📤 Exporter] │
|
||
│ [👥 Partager via…] [⏰ Expiration: 7j] │
|
||
│ │
|
||
│ ── Liens partagés (12) ── │
|
||
│ ✅ Kotlin Coroutines Guide │
|
||
│ ✅ Compose Navigation Deep Dive │
|
||
│ ✅ Room Database Best Practices │
|
||
│ ... │
|
||
└──────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.5 Fichiers à Créer / Modifier
|
||
|
||
```
|
||
data/share/
|
||
├── CollectionShareManager.kt # Logique de création/lecture de partages
|
||
├── SharedCollectionDto.kt # DTOs pour sérialisation
|
||
domain/model/
|
||
├── SharedCollection.kt # Modèle domaine
|
||
presentation/collections/
|
||
├── ShareCollectionSheet.kt # Bottom sheet de partage
|
||
├── ImportCollectionScreen.kt # Écran d'import
|
||
```
|
||
|
||
- Modifier `CollectionDao` pour ajouter des requêtes liées au partage
|
||
- Modifier `SyncManager` pour synchroniser les collections partagées
|
||
|
||
### 3.6 Estimation
|
||
|
||
| Tâche | Effort | Priorité |
|
||
|-------|--------|----------|
|
||
| `CollectionShareManager` | 4h | Haute |
|
||
| DTOs et sérialisation | 2h | Haute |
|
||
| UI partage (bottom sheet) | 3h | Haute |
|
||
| Import depuis lien/fichier | 3h | Moyenne |
|
||
| Expiration des partages | 2h | Basse |
|
||
| **Total** | **~14h** | |
|
||
|
||
### 3.7 Limitations
|
||
|
||
- L'API Shaarli v1 ne propose pas de mécanisme de partage natif entre instances
|
||
- Le partage repose donc sur des bookmarks publics comme vecteur de transport
|
||
- La collaboration temps réel n'est pas possible sans serveur intermédiaire
|
||
|
||
---
|
||
|
||
## 4. Support Multi-Instances Shaarli
|
||
|
||
### 4.1 Objectif
|
||
|
||
Permettre de gérer plusieurs instances Shaarli (perso, travail, projet) avec switch rapide et vue unifiée optionnelle.
|
||
|
||
### 4.2 Contexte Existant
|
||
|
||
- `TokenManager` stocke un seul jeu de credentials (token, baseUrl, apiSecret)
|
||
- `HostSelectionInterceptor` permet déjà de changer dynamiquement le host de l'API
|
||
- `AuthInterceptor` gère le JWT automatiquement
|
||
- La base Room est unique (une seule `ShaarliDatabase`)
|
||
|
||
### 4.3 Architecture Proposée
|
||
|
||
#### A. Stockage Multi-Comptes
|
||
|
||
```kotlin
|
||
// Nouvelle entité pour stocker les instances
|
||
data class ShaarliInstance(
|
||
val id: String, // UUID
|
||
val name: String, // Ex: "Perso", "Travail"
|
||
val baseUrl: String,
|
||
val username: String,
|
||
val icon: String?, // Emoji ou URL
|
||
val color: Long?, // Couleur d'identification
|
||
val isActive: Boolean,
|
||
val lastSyncAt: Long
|
||
)
|
||
|
||
// Extension de TokenManager
|
||
interface MultiInstanceTokenManager {
|
||
fun getInstances(): List<ShaarliInstance>
|
||
fun getActiveInstance(): ShaarliInstance?
|
||
fun switchToInstance(instanceId: String)
|
||
fun addInstance(instance: ShaarliInstance, token: String, apiSecret: String)
|
||
fun removeInstance(instanceId: String)
|
||
|
||
// Credentials par instance (chiffrées séparément)
|
||
fun getTokenForInstance(instanceId: String): String?
|
||
fun getApiSecretForInstance(instanceId: String): String?
|
||
}
|
||
```
|
||
|
||
#### B. Isolation des Données
|
||
|
||
**Option 1 — Base de données séparée par instance** (recommandé) :
|
||
```kotlin
|
||
fun getDatabaseForInstance(context: Context, instanceId: String): ShaarliDatabase {
|
||
return Room.databaseBuilder(
|
||
context,
|
||
ShaarliDatabase::class.java,
|
||
"shaarli_$instanceId.db" // DB unique par instance
|
||
)
|
||
.addMigrations(MIGRATION_4_5)
|
||
.fallbackToDestructiveMigrationFrom(1, 2, 3)
|
||
.build()
|
||
}
|
||
```
|
||
|
||
**Option 2 — Colonne `instance_id` dans chaque table** :
|
||
```kotlin
|
||
// Plus simple mais requiert migration lourde de toutes les tables
|
||
@ColumnInfo(name = "instance_id")
|
||
val instanceId: String
|
||
```
|
||
|
||
**Recommandation** : Option 1 — DB séparées. Plus propre, pas de migration complexe, isolation parfaite des données.
|
||
|
||
#### C. Switch Rapide
|
||
|
||
```
|
||
┌──────────────────────────────────┐
|
||
│ 🏠 Perso (shaarli.maison.fr) │ ← Active
|
||
│ 💼 Travail (links.corp.com) │
|
||
│ 🧪 Lab (test.shaarli.org) │
|
||
│ │
|
||
│ [+ Ajouter une instance] │
|
||
└──────────────────────────────────┘
|
||
```
|
||
|
||
- Drawer ou bottom sheet accessible depuis le header du feed
|
||
- Indicateur visuel permanent de l'instance active (couleur, icône)
|
||
- Switch = changement de DB + mise à jour du `HostSelectionInterceptor`
|
||
|
||
#### D. Vue Unifiée (optionnelle)
|
||
|
||
```kotlin
|
||
// Agrégation cross-DB en lecture seule
|
||
suspend fun getUnifiedFeed(): Flow<List<ShaarliLink>> {
|
||
val instances = tokenManager.getInstances()
|
||
return instances.map { instance ->
|
||
getDatabaseForInstance(context, instance.id)
|
||
.linkDao()
|
||
.getRecentLinks(limit = 50)
|
||
.map { links -> links.map { it.toDomainModel(instance) } }
|
||
}.merge() // kotlinx.coroutines.flow.merge
|
||
}
|
||
```
|
||
|
||
### 4.4 Impact sur le Code Existant
|
||
|
||
| Fichier | Modification |
|
||
|---------|-------------|
|
||
| `TokenManager.kt` | Refactoring vers `MultiInstanceTokenManager` |
|
||
| `DatabaseModule.kt` | Fournir la DB basée sur l'instance active |
|
||
| `NetworkModule.kt` | `HostSelectionInterceptor` piloté par l'instance active |
|
||
| `SyncManager.kt` | Sync par instance, pas global |
|
||
| `NavGraph.kt` | Nouveau `Screen.InstancePicker` |
|
||
| `FeedScreen.kt` | Afficher l'instance active + switch |
|
||
|
||
### 4.5 Estimation
|
||
|
||
| Tâche | Effort | Priorité |
|
||
|-------|--------|----------|
|
||
| `MultiInstanceTokenManager` | 4h | Haute |
|
||
| DB par instance (`DatabaseModule` refactoring) | 4h | Haute |
|
||
| UI picker d'instances | 3h | Haute |
|
||
| Switch dynamique (network + DB) | 3h | Haute |
|
||
| Vue unifiée cross-DB | 4h | Basse |
|
||
| Migration des données existantes | 2h | Haute |
|
||
| **Total** | **~20h** | |
|
||
|
||
---
|
||
|
||
## 5. Verrouillage Biométrique
|
||
|
||
### 5.1 Objectif
|
||
|
||
Protéger l'accès à l'application par empreinte digitale, reconnaissance faciale ou code PIN, en utilisant l'API BiometricPrompt d'AndroidX.
|
||
|
||
### 5.2 Contexte Existant
|
||
|
||
- L'app utilise déjà `EncryptedSharedPreferences` pour stocker les tokens
|
||
- `TokenManager` gère les secrets sensibles
|
||
- Le minSdk est 24 (Android 7.0) — BiometricPrompt requiert SDK 28+ mais AndroidX fournit un compat
|
||
|
||
### 5.3 Architecture Proposée
|
||
|
||
#### A. Dépendances
|
||
|
||
```kotlin
|
||
implementation("androidx.biometric:biometric:1.2.0-alpha05")
|
||
```
|
||
|
||
#### B. Gestion des Préférences
|
||
|
||
```kotlin
|
||
data class SecurityPreferences(
|
||
val isBiometricEnabled: Boolean = false,
|
||
val lockTimeout: LockTimeout = LockTimeout.IMMEDIATE,
|
||
val requireOnStartup: Boolean = true,
|
||
val requireOnResume: Boolean = false // Après mise en arrière-plan
|
||
)
|
||
|
||
enum class LockTimeout(val displayName: String, val delayMs: Long) {
|
||
IMMEDIATE("Immédiat", 0),
|
||
AFTER_1_MIN("Après 1 minute", 60_000),
|
||
AFTER_5_MIN("Après 5 minutes", 300_000),
|
||
AFTER_15_MIN("Après 15 minutes", 900_000)
|
||
}
|
||
```
|
||
|
||
#### C. Service Biométrique
|
||
|
||
```kotlin
|
||
@Singleton
|
||
class BiometricAuthManager @Inject constructor(
|
||
@ApplicationContext private val context: Context
|
||
) {
|
||
private val biometricManager = BiometricManager.from(context)
|
||
|
||
fun canAuthenticate(): BiometricAvailability {
|
||
return when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
|
||
BiometricManager.BIOMETRIC_SUCCESS -> BiometricAvailability.AVAILABLE
|
||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> BiometricAvailability.NO_HARDWARE
|
||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> BiometricAvailability.NOT_ENROLLED
|
||
else -> BiometricAvailability.UNAVAILABLE
|
||
}
|
||
}
|
||
|
||
fun authenticate(
|
||
activity: FragmentActivity,
|
||
onSuccess: () -> Unit,
|
||
onError: (String) -> Unit
|
||
) {
|
||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||
.setTitle("Déverrouiller ShaarIt")
|
||
.setSubtitle("Utilisez votre empreinte ou votre visage")
|
||
.setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
|
||
.build()
|
||
|
||
val biometricPrompt = BiometricPrompt(activity,
|
||
object : BiometricPrompt.AuthenticationCallback() {
|
||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||
onSuccess()
|
||
}
|
||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||
onError(errString.toString())
|
||
}
|
||
}
|
||
)
|
||
biometricPrompt.authenticate(promptInfo)
|
||
}
|
||
}
|
||
```
|
||
|
||
#### D. Flux d'Authentification
|
||
|
||
```
|
||
App Launch
|
||
│
|
||
├── Biometric disabled? ──▶ Normal flow (Login/Feed)
|
||
│
|
||
└── Biometric enabled?
|
||
│
|
||
├── Show LockScreen
|
||
│ │
|
||
│ ├── Auth success ──▶ Normal flow
|
||
│ ├── Auth failed ──▶ Retry ou message d'erreur
|
||
│ └── Fallback ──▶ PIN/Pattern système
|
||
│
|
||
└── Check timeout (si reprise après background)
|
||
├── Timeout pas expiré ──▶ Normal flow
|
||
└── Timeout expiré ──▶ Show LockScreen
|
||
```
|
||
|
||
#### E. Intégration dans MainActivity
|
||
|
||
```kotlin
|
||
// Dans MainActivity.kt
|
||
override fun onCreate(savedInstanceState: Bundle?) {
|
||
installSplashScreen()
|
||
super.onCreate(savedInstanceState)
|
||
|
||
val needsAuth = securityPreferences.isBiometricEnabled && !isAuthenticated
|
||
|
||
setContent {
|
||
if (needsAuth) {
|
||
LockScreen(onAuthenticated = { isAuthenticated = true })
|
||
} else {
|
||
// AppNavGraph normal...
|
||
}
|
||
}
|
||
}
|
||
|
||
override fun onStop() {
|
||
super.onStop()
|
||
lastBackgroundTime = System.currentTimeMillis()
|
||
}
|
||
|
||
override fun onResume() {
|
||
super.onResume()
|
||
if (securityPreferences.isBiometricEnabled) {
|
||
val elapsed = System.currentTimeMillis() - lastBackgroundTime
|
||
if (elapsed > securityPreferences.lockTimeout.delayMs) {
|
||
isAuthenticated = false
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.4 UI Paramètres
|
||
|
||
```
|
||
┌──────────────────────────────────────────┐
|
||
│ 🔒 Sécurité │
|
||
├──────────────────────────────────────────┤
|
||
│ Verrouillage biométrique [Toggle ON] │
|
||
│ Délai de verrouillage [Immédiat ▾]│
|
||
│ Verrouiller au démarrage [✓] │
|
||
│ Verrouiller en arrière-plan [✓] │
|
||
└──────────────────────────────────────────┘
|
||
```
|
||
|
||
### 5.5 Estimation
|
||
|
||
| Tâche | Effort | Priorité |
|
||
|-------|--------|----------|
|
||
| `BiometricAuthManager` | 2h | Haute |
|
||
| `LockScreen` (Compose) | 2h | Haute |
|
||
| Intégration `MainActivity` (lifecycle) | 2h | Haute |
|
||
| Préférences de sécurité (Settings) | 2h | Moyenne |
|
||
| Gestion du timeout background | 1h | Moyenne |
|
||
| **Total** | **~9h** | |
|
||
|
||
---
|
||
|
||
## 6. Rappels de Lecture (« Lire plus tard »)
|
||
|
||
### 6.1 Objectif
|
||
|
||
Permettre à l'utilisateur de programmer des rappels pour relire des liens sauvegardés, avec notifications push à l'heure souhaitée.
|
||
|
||
### 6.2 Contexte Existant
|
||
|
||
- WorkManager déjà intégré et utilisé (sync, health check)
|
||
- Notifications non implémentées actuellement
|
||
- Le champ `is_pinned` sur `LinkEntity` offre un mécanisme rudimentaire de « favoris »
|
||
|
||
### 6.3 Architecture Proposée
|
||
|
||
#### A. Modèle de Données
|
||
|
||
```kotlin
|
||
// Nouvelle entité Room
|
||
@Entity(
|
||
tableName = "reading_reminders",
|
||
foreignKeys = [ForeignKey(
|
||
entity = LinkEntity::class,
|
||
parentColumns = ["id"],
|
||
childColumns = ["link_id"],
|
||
onDelete = ForeignKey.CASCADE
|
||
)]
|
||
)
|
||
data class ReadingReminderEntity(
|
||
@PrimaryKey(autoGenerate = true)
|
||
val id: Long = 0,
|
||
|
||
@ColumnInfo(name = "link_id")
|
||
val linkId: Int,
|
||
|
||
@ColumnInfo(name = "remind_at")
|
||
val remindAt: Long, // Timestamp du rappel
|
||
|
||
@ColumnInfo(name = "repeat_interval")
|
||
val repeatInterval: RepeatInterval = RepeatInterval.NONE,
|
||
|
||
@ColumnInfo(name = "is_dismissed")
|
||
val isDismissed: Boolean = false,
|
||
|
||
@ColumnInfo(name = "created_at")
|
||
val createdAt: Long = System.currentTimeMillis()
|
||
)
|
||
|
||
enum class RepeatInterval {
|
||
NONE,
|
||
DAILY,
|
||
WEEKLY,
|
||
MONTHLY
|
||
}
|
||
```
|
||
|
||
- Migration Room v6 (ou v7 selon la fonctionnalité Reader Mode) pour ajouter la table
|
||
|
||
#### B. Notification Worker
|
||
|
||
```kotlin
|
||
@HiltWorker
|
||
class ReminderNotificationWorker @AssistedInject constructor(
|
||
@Assisted context: Context,
|
||
@Assisted params: WorkerParameters,
|
||
private val linkDao: LinkDao,
|
||
private val reminderDao: ReminderDao
|
||
) : CoroutineWorker(context, params) {
|
||
|
||
override suspend fun doWork(): Result {
|
||
val reminderId = inputData.getLong("reminder_id", -1)
|
||
val reminder = reminderDao.getById(reminderId) ?: return Result.failure()
|
||
val link = linkDao.getLinkById(reminder.linkId) ?: return Result.failure()
|
||
|
||
showNotification(link, reminder)
|
||
|
||
if (reminder.repeatInterval != RepeatInterval.NONE) {
|
||
scheduleNextReminder(reminder)
|
||
} else {
|
||
reminderDao.markDismissed(reminderId)
|
||
}
|
||
|
||
return Result.success()
|
||
}
|
||
|
||
private fun showNotification(link: LinkEntity, reminder: ReadingReminderEntity) {
|
||
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_REMINDERS)
|
||
.setSmallIcon(R.drawable.ic_bookmark)
|
||
.setContentTitle("📖 Rappel de lecture")
|
||
.setContentText(link.title)
|
||
.setContentIntent(createDeepLinkPendingIntent(link.id))
|
||
.addAction(R.drawable.ic_check, "Lu", createDismissIntent(reminder.id))
|
||
.addAction(R.drawable.ic_snooze, "Rappeler dans 1h", createSnoozeIntent(reminder.id))
|
||
.setAutoCancel(true)
|
||
.build()
|
||
|
||
NotificationManagerCompat.from(applicationContext).notify(reminder.id.toInt(), notification)
|
||
}
|
||
}
|
||
```
|
||
|
||
#### C. Raccourcis Rapides de Rappel
|
||
|
||
```kotlin
|
||
enum class QuickReminder(val displayName: String, val delayMs: Long) {
|
||
IN_1_HOUR("Dans 1 heure", 3_600_000),
|
||
TONIGHT("Ce soir (20h)", /* calculé */),
|
||
TOMORROW("Demain matin (9h)", /* calculé */),
|
||
THIS_WEEKEND("Ce week-end", /* calculé */),
|
||
NEXT_WEEK("La semaine prochaine", /* calculé */),
|
||
CUSTOM("Date personnalisée…", 0)
|
||
}
|
||
```
|
||
|
||
### 6.4 Interface Utilisateur
|
||
|
||
```
|
||
Long press sur un lien dans le feed :
|
||
┌──────────────────────────────────────┐
|
||
│ ⏰ Rappeler de lire │
|
||
├──────────────────────────────────────┤
|
||
│ 🕐 Dans 1 heure │
|
||
│ 🌙 Ce soir (20h00) │
|
||
│ ☀️ Demain matin (9h00) │
|
||
│ 📅 Ce week-end │
|
||
│ 📆 La semaine prochaine │
|
||
│ 📝 Date personnalisée… │
|
||
└──────────────────────────────────────┘
|
||
```
|
||
|
||
- Indicateur visuel sur les liens avec rappel actif (icône ⏰ dans le feed)
|
||
- Section dédiée dans Settings : « Mes rappels » avec liste des rappels planifiés
|
||
|
||
### 6.5 Permissions
|
||
|
||
```xml
|
||
<!-- AndroidManifest.xml -->
|
||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- API 33+ -->
|
||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <!-- API 31+ -->
|
||
```
|
||
|
||
- Demander la permission `POST_NOTIFICATIONS` au runtime sur Android 13+
|
||
|
||
### 6.6 Estimation
|
||
|
||
| Tâche | Effort | Priorité |
|
||
|-------|--------|----------|
|
||
| Entité `ReadingReminderEntity` + DAO + migration | 2h | Haute |
|
||
| `ReminderNotificationWorker` | 3h | Haute |
|
||
| Notification channel + permissions | 1h | Haute |
|
||
| Bottom sheet de sélection rapide | 2h | Haute |
|
||
| Date picker personnalisé | 2h | Moyenne |
|
||
| Écran « Mes rappels » | 3h | Moyenne |
|
||
| Rappels récurrents | 2h | Basse |
|
||
| **Total** | **~15h** | |
|
||
|
||
---
|
||
|
||
## 7. Voice Input (Recherche et Ajout)
|
||
|
||
### 7.1 Objectif
|
||
|
||
Permettre l'ajout de liens et la recherche vocale via `SpeechRecognizer` ou l'API Material Search avec entrée vocale.
|
||
|
||
### 7.2 Contexte Existant
|
||
|
||
- Barre de recherche dans `FeedScreen` (champ texte avec debounce 300ms)
|
||
- Écran `AddLinkScreen` avec champ URL et titres
|
||
- Aucune intégration vocale actuelle
|
||
|
||
### 7.3 Architecture Proposée
|
||
|
||
#### A. Service de Reconnaissance Vocale
|
||
|
||
```kotlin
|
||
@Singleton
|
||
class VoiceInputManager @Inject constructor(
|
||
@ApplicationContext private val context: Context
|
||
) {
|
||
fun isAvailable(): Boolean {
|
||
return SpeechRecognizer.isRecognitionAvailable(context)
|
||
}
|
||
|
||
fun createRecognitionIntent(language: String = "fr-FR"): Intent {
|
||
return Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
||
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
|
||
putExtra(RecognizerIntent.EXTRA_LANGUAGE, language)
|
||
putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true)
|
||
putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 3)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### B. Composable Réutilisable
|
||
|
||
```kotlin
|
||
@Composable
|
||
fun VoiceInputButton(
|
||
onResult: (String) -> Unit,
|
||
modifier: Modifier = Modifier
|
||
) {
|
||
val launcher = rememberLauncherForActivityResult(
|
||
contract = ActivityResultContracts.StartActivityForResult()
|
||
) { result ->
|
||
if (result.resultCode == Activity.RESULT_OK) {
|
||
val matches = result.data
|
||
?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
|
||
matches?.firstOrNull()?.let { onResult(it) }
|
||
}
|
||
}
|
||
|
||
IconButton(
|
||
onClick = { launcher.launch(voiceInputManager.createRecognitionIntent()) },
|
||
modifier = modifier
|
||
) {
|
||
Icon(Icons.Default.Mic, contentDescription = "Recherche vocale")
|
||
}
|
||
}
|
||
```
|
||
|
||
#### C. Points d'Intégration
|
||
|
||
| Écran | Usage | Détail |
|
||
|-------|-------|--------|
|
||
| **FeedScreen** | Recherche vocale | Bouton 🎤 à côté de la barre de recherche |
|
||
| **AddLinkScreen** | Dictée du titre | Bouton 🎤 à côté du champ titre |
|
||
| **AddLinkScreen** | Dictée des tags | Bouton 🎤 — reconnaissance + split par virgule |
|
||
| **AddLinkScreen** | Dictée de la description | Mode dictée pour le champ description |
|
||
|
||
#### D. Intelligence de Parsing
|
||
|
||
```kotlin
|
||
fun parseVoiceInput(text: String): VoiceParsedInput {
|
||
val urlPattern = Regex("https?://\\S+")
|
||
val url = urlPattern.find(text)?.value
|
||
|
||
return if (url != null) {
|
||
VoiceParsedInput.Url(url)
|
||
} else {
|
||
VoiceParsedInput.SearchQuery(text)
|
||
}
|
||
}
|
||
|
||
sealed class VoiceParsedInput {
|
||
data class Url(val url: String) : VoiceParsedInput()
|
||
data class SearchQuery(val query: String) : VoiceParsedInput()
|
||
}
|
||
```
|
||
|
||
### 7.4 Permissions
|
||
|
||
```xml
|
||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||
```
|
||
|
||
- Demander la permission au runtime avant la première utilisation
|
||
- Afficher un message explicatif si refusée
|
||
|
||
### 7.5 Estimation
|
||
|
||
| Tâche | Effort | Priorité |
|
||
|-------|--------|----------|
|
||
| `VoiceInputManager` | 1h | Haute |
|
||
| `VoiceInputButton` composable | 2h | Haute |
|
||
| Intégration FeedScreen (recherche) | 1h | Haute |
|
||
| Intégration AddLinkScreen (titre, tags) | 2h | Moyenne |
|
||
| Parsing intelligent (URL vs texte) | 1h | Moyenne |
|
||
| Gestion des permissions audio | 1h | Haute |
|
||
| **Total** | **~8h** | |
|
||
|
||
---
|
||
|
||
## 8. Adaptive Layouts (Tablettes et Foldables)
|
||
|
||
### 8.1 Objectif
|
||
|
||
Adapter l'interface de ShaarIt pour les tablettes, foldables et écrans larges en utilisant les WindowSizeClass de Material 3 et la navigation adaptative.
|
||
|
||
### 8.2 Contexte Existant
|
||
|
||
- UI actuellement optimisée pour téléphone uniquement (single-pane)
|
||
- Trois modes d'affichage existants : Liste, Grille, Compact (via `ViewStyle`)
|
||
- Navigation Compose avec `NavHost` mono-pane
|
||
- Edge-to-Edge déjà activé
|
||
|
||
### 8.3 Architecture Proposée
|
||
|
||
#### A. Dépendances
|
||
|
||
```kotlin
|
||
implementation("androidx.compose.material3:material3-window-size-class:1.2.0")
|
||
implementation("androidx.compose.material3:material3-adaptive-navigation-suite:1.0.0")
|
||
implementation("androidx.window:window:1.2.0")
|
||
```
|
||
|
||
#### B. Détection de la Taille d'Écran
|
||
|
||
```kotlin
|
||
@Composable
|
||
fun ShaarItAdaptiveLayout() {
|
||
val windowSizeClass = calculateWindowSizeClass(LocalContext.current as Activity)
|
||
|
||
when (windowSizeClass.widthSizeClass) {
|
||
WindowWidthSizeClass.Compact -> {
|
||
// Téléphone : navigation bottom/drawer actuelle
|
||
SinglePaneLayout()
|
||
}
|
||
WindowWidthSizeClass.Medium -> {
|
||
// Tablette portrait / foldable déplié : list-detail
|
||
ListDetailLayout()
|
||
}
|
||
WindowWidthSizeClass.Expanded -> {
|
||
// Tablette paysage / grand écran : three-pane
|
||
ThreePaneLayout()
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### C. Layout List-Detail (Medium)
|
||
|
||
```
|
||
┌─────────────────┬──────────────────────────────┐
|
||
│ │ │
|
||
│ Feed (liste) │ Détail du lien sélectionné │
|
||
│ │ (édition ou Reader Mode) │
|
||
│ ┌────────────┐ │ │
|
||
│ │ Lien 1 │ │ Titre: Kotlin Coroutines │
|
||
│ │ Lien 2 ◀──┼─┤ URL: medium.com/... │
|
||
│ │ Lien 3 │ │ Description: ... │
|
||
│ │ Lien 4 │ │ Tags: [kotlin] [coroutines]│
|
||
│ └────────────┘ │ │
|
||
│ │ │
|
||
└─────────────────┴──────────────────────────────┘
|
||
```
|
||
|
||
#### D. Navigation Adaptative
|
||
|
||
```kotlin
|
||
@Composable
|
||
fun AdaptiveNavigation(
|
||
windowSizeClass: WindowSizeClass,
|
||
content: @Composable () -> Unit
|
||
) {
|
||
when (windowSizeClass.widthSizeClass) {
|
||
WindowWidthSizeClass.Compact -> {
|
||
// Bottom navigation bar
|
||
Scaffold(bottomBar = { BottomNavBar() }) { content() }
|
||
}
|
||
WindowWidthSizeClass.Medium -> {
|
||
// Navigation rail (barre latérale fine)
|
||
Row {
|
||
NavigationRail { /* Feed, Tags, Collections, Settings */ }
|
||
content()
|
||
}
|
||
}
|
||
WindowWidthSizeClass.Expanded -> {
|
||
// Navigation drawer permanent
|
||
PermanentNavigationDrawer(
|
||
drawerContent = { PermanentDrawerSheet { /* Menu complet */ } }
|
||
) { content() }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### E. Grille Adaptative
|
||
|
||
```kotlin
|
||
@Composable
|
||
fun AdaptiveGrid(links: LazyPagingItems<ShaarliLink>) {
|
||
val windowSizeClass = calculateWindowSizeClass(LocalContext.current as Activity)
|
||
val columns = when (windowSizeClass.widthSizeClass) {
|
||
WindowWidthSizeClass.Compact -> 1 // ou 2 en grille
|
||
WindowWidthSizeClass.Medium -> 2
|
||
WindowWidthSizeClass.Expanded -> 3
|
||
else -> 1
|
||
}
|
||
|
||
LazyVerticalStaggeredGrid(
|
||
columns = StaggeredGridCells.Fixed(columns),
|
||
contentPadding = PaddingValues(16.dp)
|
||
) {
|
||
items(links.itemCount) { index ->
|
||
links[index]?.let { LinkCard(it) }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.4 Support Foldables
|
||
|
||
```kotlin
|
||
// Détection de la posture du foldable
|
||
val foldingFeatures = WindowInfoTracker.getOrCreate(context)
|
||
.windowLayoutInfo(context)
|
||
.collectAsState()
|
||
|
||
// Adapter le layout selon la charnière
|
||
foldingFeatures.value.displayFeatures.forEach { feature ->
|
||
if (feature is FoldingFeature) {
|
||
when (feature.state) {
|
||
FoldingFeature.State.HALF_OPENED -> {
|
||
// Mode table-top : contenu en haut, contrôles en bas
|
||
TableTopLayout()
|
||
}
|
||
FoldingFeature.State.FLAT -> {
|
||
// Déplié : utiliser tout l'espace
|
||
ExpandedLayout()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 8.5 Estimation
|
||
|
||
| Tâche | Effort | Priorité |
|
||
|-------|--------|----------|
|
||
| WindowSizeClass + layout adaptatif | 3h | Haute |
|
||
| Navigation adaptative (rail/drawer) | 4h | Haute |
|
||
| Layout List-Detail (tablettes) | 6h | Haute |
|
||
| Grille adaptative multi-colonnes | 2h | Moyenne |
|
||
| Support foldables (posture) | 3h | Basse |
|
||
| Tests sur émulateurs (tablette, fold) | 2h | Haute |
|
||
| **Total** | **~20h** | |
|
||
|
||
---
|
||
|
||
## 9. Thème Clair et Material You (Monet)
|
||
|
||
### 9.1 Objectif
|
||
|
||
Ajouter un thème clair et le support de Material You (couleurs dynamiques basées sur le fond d'écran) sur Android 12+, tout en conservant les 15 thèmes sombres existants.
|
||
|
||
### 9.2 Contexte Existant
|
||
|
||
- 15 thèmes sombres définis dans `Theme.kt` (492 lignes, chacun un `darkColorScheme`)
|
||
- `AppTheme` enum dans `AppTheme.kt` avec `ThemePreferences` (SharedPreferences)
|
||
- `ShaarItTheme` composable applique le thème sélectionné
|
||
- La variable `isEffectivelyDark` est hardcodée à `true`
|
||
- Aucun `lightColorScheme` défini
|
||
|
||
### 9.3 Architecture Proposée
|
||
|
||
#### A. Extension de l'Enum AppTheme
|
||
|
||
```kotlin
|
||
enum class ThemeMode(val displayName: String) {
|
||
DARK("Sombre"),
|
||
LIGHT("Clair"),
|
||
SYSTEM("Système"), // Suit le paramètre Android
|
||
DYNAMIC_DARK("Material You Sombre"), // Android 12+ dynamic dark
|
||
DYNAMIC_LIGHT("Material You Clair"), // Android 12+ dynamic light
|
||
DYNAMIC_SYSTEM("Material You Auto") // Android 12+ dynamic + suit système
|
||
}
|
||
```
|
||
|
||
```kotlin
|
||
// Extension de ThemePreferences
|
||
@Singleton
|
||
class ThemePreferences @Inject constructor(
|
||
@ApplicationContext private val context: Context
|
||
) {
|
||
// Existant
|
||
private val _currentTheme = MutableStateFlow(loadTheme())
|
||
val currentTheme: StateFlow<AppTheme> = _currentTheme.asStateFlow()
|
||
|
||
// Nouveau
|
||
private val _themeMode = MutableStateFlow(loadThemeMode())
|
||
val themeMode: StateFlow<ThemeMode> = _themeMode.asStateFlow()
|
||
|
||
fun setThemeMode(mode: ThemeMode) {
|
||
prefs.edit().putString(KEY_THEME_MODE, mode.name).apply()
|
||
_themeMode.value = mode
|
||
}
|
||
}
|
||
```
|
||
|
||
#### B. Schémas de Couleurs Claires
|
||
|
||
Pour chaque thème sombre existant, créer un pendant clair :
|
||
|
||
```kotlin
|
||
// ── Default Light (ShaarIt) ──
|
||
private val DefaultLightColorScheme = lightColorScheme(
|
||
primary = Color(0xFF006B5A), // CyanPrimary assombri
|
||
onPrimary = Color.White,
|
||
primaryContainer = Color(0xFFB2F5E6),
|
||
onPrimaryContainer = Color(0xFF00201A),
|
||
secondary = Color(0xFF0077B6), // TealSecondary assombri
|
||
onSecondary = Color.White,
|
||
background = Color(0xFFF8FAFA),
|
||
onBackground = Color(0xFF1A1C1E),
|
||
surface = Color(0xFFFFFFFF),
|
||
onSurface = Color(0xFF1A1C1E),
|
||
surfaceVariant = Color(0xFFE7F0EE),
|
||
onSurfaceVariant = Color(0xFF404944),
|
||
outline = Color(0xFF707974),
|
||
error = Color(0xFFBA1A1A),
|
||
onError = Color.White
|
||
)
|
||
|
||
// Générer pour : GitHub Light, Linear Light, Nord Light, etc.
|
||
```
|
||
|
||
#### C. Material You (Dynamic Colors)
|
||
|
||
```kotlin
|
||
@Composable
|
||
fun ShaarItTheme(
|
||
appTheme: AppTheme = AppTheme.DEFAULT,
|
||
themeMode: ThemeMode = ThemeMode.DARK,
|
||
content: @Composable () -> Unit
|
||
) {
|
||
val isDarkTheme = when (themeMode) {
|
||
ThemeMode.DARK, ThemeMode.DYNAMIC_DARK -> true
|
||
ThemeMode.LIGHT, ThemeMode.DYNAMIC_LIGHT -> false
|
||
ThemeMode.SYSTEM, ThemeMode.DYNAMIC_SYSTEM -> isSystemInDarkTheme()
|
||
}
|
||
|
||
val useDynamicColors = themeMode in listOf(
|
||
ThemeMode.DYNAMIC_DARK, ThemeMode.DYNAMIC_LIGHT, ThemeMode.DYNAMIC_SYSTEM
|
||
)
|
||
|
||
val colorScheme = when {
|
||
useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||
if (isDarkTheme) {
|
||
dynamicDarkColorScheme(LocalContext.current)
|
||
} else {
|
||
dynamicLightColorScheme(LocalContext.current)
|
||
}
|
||
}
|
||
isDarkTheme -> getColorSchemeForTheme(appTheme) // Existant
|
||
else -> getLightColorSchemeForTheme(appTheme) // Nouveau
|
||
}
|
||
|
||
val view = LocalView.current
|
||
if (!view.isInEditMode) {
|
||
SideEffect {
|
||
val window = (view.context as Activity).window
|
||
window.statusBarColor = colorScheme.background.toArgb()
|
||
window.navigationBarColor = colorScheme.background.toArgb()
|
||
WindowCompat.getInsetsController(window, view)
|
||
.isAppearanceLightStatusBars = !isDarkTheme
|
||
}
|
||
}
|
||
|
||
MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content)
|
||
}
|
||
```
|
||
|
||
#### D. UI Paramètres (mise à jour)
|
||
|
||
```
|
||
┌──────────────────────────────────────────┐
|
||
│ 🎨 Apparence │
|
||
├──────────────────────────────────────────┤
|
||
│ Mode │
|
||
│ ○ Sombre ○ Clair ○ Système │
|
||
│ │
|
||
│ Material You (Android 12+) [Toggle] │
|
||
│ ↳ Couleurs extraites du fond d'écran │
|
||
│ │
|
||
│ Thème (si Material You désactivé) │
|
||
│ [ShaarIt] [GitHub] [Spotify] [Dracula]… │
|
||
│ │
|
||
│ Prévisualisation │
|
||
│ ┌──────────────────────────────────┐ │
|
||
│ │ Aperçu du thème sélectionné │ │
|
||
│ └──────────────────────────────────┘ │
|
||
└──────────────────────────────────────────┘
|
||
```
|
||
|
||
### 9.4 Impact sur les Composants Existants
|
||
|
||
- **GlassCard** et composants glassmorphism : adapter les effets de transparence pour le mode clair
|
||
- **Skeleton Loading** : les couleurs shimmer doivent être adaptées
|
||
- **TagChip** : les couleurs de fond et texte doivent s'inverser
|
||
- **Icônes de statut** : certaines couleurs (SuccessGreen, WarningAmber) nécessitent un ajustement en mode clair pour le contraste
|
||
|
||
### 9.5 Estimation
|
||
|
||
| Tâche | Effort | Priorité |
|
||
|-------|--------|----------|
|
||
| Créer `lightColorScheme` pour les 15 thèmes | 4h | Haute |
|
||
| `ThemeMode` enum + préférences | 1h | Haute |
|
||
| Material You (dynamic colors API 31+) | 2h | Haute |
|
||
| Modifier `ShaarItTheme` composable | 2h | Haute |
|
||
| Adapter les composants custom (glass, skeleton, etc.) | 4h | Haute |
|
||
| UI paramètres (sélecteur mode + preview) | 3h | Moyenne |
|
||
| Tests visuels (clair/sombre/dynamic × 15 thèmes) | 3h | Haute |
|
||
| **Total** | **~19h** | |
|
||
|
||
---
|
||
|
||
## 10. Roadmap Priorisée et Dépendances
|
||
|
||
### 10.1 Matrice Effort / Impact
|
||
|
||
```
|
||
Impact Utilisateur ▲
|
||
│
|
||
Élevé │ 🏆 Reader Mode 🏆 Material You
|
||
│ 🏆 Biométrique 🏆 Widget Glance
|
||
│
|
||
Moyen │ ★ Rappels Lecture ★ Adaptive Layout
|
||
│ ★ Voice Input
|
||
│
|
||
Faible │ ◆ Multi-Instances ◆ Partage Collections
|
||
│
|
||
└──────────────────────────────────────▶
|
||
Faible Moyen Élevé
|
||
Effort de Dev
|
||
```
|
||
|
||
### 10.2 Graphe de Dépendances
|
||
|
||
```mermaid
|
||
graph TD
|
||
A[Thème Clair / Material You] --> |"Indépendant"| Z[Prêt]
|
||
B[Verrouillage Biométrique] --> |"Indépendant"| Z
|
||
C[Voice Input] --> |"Indépendant"| Z
|
||
D[Widget Glance] --> |"Indépendant"| Z
|
||
E[Mode Lecture] --> |"Migration Room"| F[Rappels de Lecture]
|
||
G[Multi-Instances] --> |"Requis pour"| H[Partage Collections]
|
||
I[Adaptive Layouts] --> |"Améliore"| E
|
||
|
||
style A fill:#81C784
|
||
style B fill:#81C784
|
||
style C fill:#81C784
|
||
style D fill:#81C784
|
||
style E fill:#FFB74D
|
||
style F fill:#FFB74D
|
||
style G fill:#EF5350
|
||
style H fill:#EF5350
|
||
style I fill:#FFB74D
|
||
```
|
||
|
||
### 10.3 Plan de Livraison Recommandé
|
||
|
||
#### Phase 1 — Quick Wins Indépendants (2-3 semaines)
|
||
|
||
| # | Fonctionnalité | Effort | Justification |
|
||
|---|----------------|--------|---------------|
|
||
| 1 | **Verrouillage Biométrique** | ~9h | Indépendant, forte demande sécurité, impact immédiat |
|
||
| 2 | **Voice Input** | ~8h | Indépendant, facile à implémenter, améliore l'accessibilité |
|
||
| 3 | **Thème Clair + Material You** | ~19h | Indépendant, impact visuel majeur, demande fréquente |
|
||
|
||
#### Phase 2 — Expérience Enrichie (3-4 semaines)
|
||
|
||
| # | Fonctionnalité | Effort | Justification |
|
||
|---|----------------|--------|---------------|
|
||
| 4 | **Widget Glance** | ~11h | Visibilité app sur l'écran d'accueil, engagement quotidien |
|
||
| 5 | **Mode Lecture** | ~17h | Différenciateur clé, exploite JSoup/Markdown existants |
|
||
| 6 | **Rappels de Lecture** | ~15h | Complète le Reader Mode, utilise WorkManager existant |
|
||
|
||
#### Phase 3 — Adaptation et Collaboration (4-5 semaines)
|
||
|
||
| # | Fonctionnalité | Effort | Justification |
|
||
|---|----------------|--------|---------------|
|
||
| 7 | **Adaptive Layouts** | ~20h | Ouvre le marché tablettes/foldables, améliore le Reader Mode |
|
||
| 8 | **Multi-Instances** | ~20h | Refactoring profond, prérequis au partage |
|
||
| 9 | **Partage de Collections** | ~14h | Nécessite multi-instances, fonctionnalité sociale |
|
||
|
||
### 10.4 Résumé Global
|
||
|
||
| Fonctionnalité | Effort | Priorité | Dépendances | Migration Room |
|
||
|----------------|--------|----------|-------------|----------------|
|
||
| Verrouillage Biométrique | ~9h | 🔴 Haute | Aucune | Non |
|
||
| Voice Input | ~8h | 🔴 Haute | Aucune | Non |
|
||
| Thème Clair + Material You | ~19h | 🔴 Haute | Aucune | Non |
|
||
| Widget Glance | ~11h | 🔴 Haute | Aucune | Non |
|
||
| Mode Lecture | ~17h | 🟡 Moyenne | Aucune | Oui (v6) |
|
||
| Rappels de Lecture | ~15h | 🟡 Moyenne | Reader Mode (migration) | Oui (v6/v7) |
|
||
| Adaptive Layouts | ~20h | 🟡 Moyenne | Aucune | Non |
|
||
| Multi-Instances | ~20h | 🟠 Élevé | Aucune | Non (DB séparées) |
|
||
| Partage Collections | ~14h | 🟠 Élevé | Multi-Instances | Non |
|
||
|
||
**Effort total estimé** : **~133 heures** (~3.5 semaines à temps plein)
|
||
|
||
---
|
||
|
||
*Document généré le 9 février 2026 — Analyse basée sur le code source ShaarIt v1.0*
|