Application native multi-plateforme (iOS/iPadOS + Android) pour lire le contenu d'un serveur Kavita : manga (CBZ/CBR), comics, livres (EPUB) et PDF.
Développée par Benjamin Chianese (LORVA LLC).
| Critère | iOS | Android |
|---|---|---|
| Repo | LORVA/Klibri |
LORVA/klibri-android |
| Language | Swift 5.9+ | Kotlin |
| UI framework | SwiftUI | Jetpack Compose |
| Min OS | iOS/iPadOS 16 | Android 8.0 (API 26) |
| Version actuelle | 1.0.5 | 1.0.0 (en dev) |
| Bundle ID | fr.btjp.klibri |
fr.btjp.klibri |
| Formats lus | CBZ, CBR, EPUB, PDF | CBZ, CBR, EPUB, PDF |
| Multi-serveurs | Oui | Oui |
| Offline | Oui (téléchargement + cache) | Oui (téléchargement + cache) |
| Langue UI | FR / EN | FR / EN |
| Composant | Choix |
|---|---|
| Langage | Swift 5.9+ |
| UI | SwiftUI |
| Build system | XcodeGen (project.yml) |
| Min OS | iOS 16 / iPadOS 16 |
| Auth | JWT via API Kavita, stocké dans Keychain (synchronisé iCloud) |
| CBZ/CBR | ZIPFoundation + détection magic bytes |
| EPUB | WKWebView + injection CSS |
| PDFKit natif (rendu offline, lazy loading) | |
| Images serveur | URLSession async/await |
| Cache images | Kingfisher (mémoire + disque, expiry 1j) |
| Réseau | URLSession async/await |
| Persistance progression | JSON dans Documents/ (queue offline + pages locales) |
Dépendances Swift Package Manager :
Kingfisher (>= 7.0.0) — cache images asynchroneZIPFoundation (>= 0.9.0) — décompression CBZSources/Klibri/
├── App/
│ ├── KlibriApp.swift # @main, ThemeManager, LanguageSettings
│ └── Assets.xcassets
├── Core/
│ ├── Network/
│ │ ├── KavitaAPIClient.swift # URLSession, JWT, tous les endpoints
│ │ ├── AuthManager.swift # Login, multi-serveurs, Keychain iCloud sync
│ │ ├── Models.swift # Codable structs (Series, Volume, Chapter…)
│ │ ├── CacheManager.swift # Nettoyage cache au lancement
│ │ └── DataCache.swift # Cache JSON API (TTL)
│ ├── Persistence/
│ │ ├── DownloadManager.swift # Téléchargement épinglé hors-ligne
│ │ └── ProgressSyncManager.swift # Queue offline + retry au foreground
│ └── Readers/
│ ├── CBZ/CBZReaderView.swift # Swipe, zoom, RTL/LTR, webtoon
│ ├── EPUB/EPUBReaderView.swift # WKWebView, thèmes, pagination/scroll
│ ├── PDF/PDFReaderView.swift # PDFKit, lazy, offline
│ └── ServerImage/ServerImageReaderView.swift # Stream images Kavita
├── Features/
│ ├── Auth/ServerSetupView.swift # Connexion + ajout serveur
│ ├── Library/
│ │ ├── MainView.swift # TabView iPhone / NavigationSplitView iPad
│ │ ├── HomeView.swift # Hero card, EN COURS, RÉCEMMENT AJOUTÉS
│ │ ├── LibrariesListView.swift
│ │ └── LibraryView.swift # Grille séries + filtres
│ ├── SeriesDetail/ # Détail série + volumes/chapitres
│ ├── Collections/CollectionsView.swift # Collections Kavita (onglet iPhone, sidebar iPad)
│ ├── ReadingLists/ # Liste + détail + lecture continue
│ ├── Reader/ReaderCoordinatorView.swift # Dispatch format selon extension
│ └── Settings/SettingsView.swift # Serveurs, thème, langue, stockage
└── Shared/
└── Components/ # CoverImageView, PlaceholderView
iPhone (.compact size class) — TabView 5 onglets :
| Onglet | Icône | Contenu |
|---|---|---|
| 0 — Accueil | house.fill |
Hero card + sections EN COURS, LISTES, RÉCEMMENT AJOUTÉS |
| 1 — Bibliothèques | books.vertical.fill |
Grille des librairies Kavita |
| 2 — Collections | rectangle.3.group.fill |
Collections Kavita |
| 3 — Listes | list.bullet |
Reading Lists |
| 4 — Réglages | gear |
Serveurs, thème, langue, cache |
iPad (.regular size class) — NavigationSplitView 2 colonnes :
chevron.up.chevron.down)Indicateur de connectivité : vérification HEAD /api/health toutes les 30 secondes. Point de couleur accent = en ligne, blanc 20% = hors ligne.
Point d'entrée unique. Reçoit un Chapter + liste ordonnée de chapitres. Télécharge le fichier (cache disque ou pinned), détecte l'extension, dispatche vers le bon lecteur. Navigation entre chapitres avec debounce 400ms.
Logique de détection du format (par ordre de priorité) :
Content-Type HTTPContent-Disposition52 61 72 = CBR, 50 4B + META-INF/ = EPUB, 50 4B = CBZTabView(.page)serverId_seriesIdLazyVStack + WebtoonPageView)ServerImageReaderView si l'extraction ZIP échoue (onExtractionFailed)#FFFFFF/#1A1A1A), Sépia (#F5E6C8/#3B2A1A), Sombre (#1A1A1A/#EFEFEF)TabView(.page) pour navigation/api/reader/image?chapterId=X&page=Y&extractPdf=true)Multi-serveurs
kSecAttrSynchronizable = true (sync iCloud entre appareils)Progression sync (3 niveaux)
Démarrage lecteur :
1. GET /api/reader/progress → page serveur
2. localPage (fichier JSON persistant)
3. chapter.pagesRead
→ max des trois
Sauvegarde en temps réel :
1. Écriture locale immédiate
2. POST /api/reader/progress (async)
3. Si erreur → enqueue dans queue.json
4. Retry au retour en foreground (scenePhase == .active)
Collections
/api/image/collection-cover?collectionTagId=X)Téléchargement hors-ligne
Documents/klibri-downloads/<serverId>-<chapterId>.<ext>Caches/chapters/<serverId>-<chapterId>.<ext> (nettoyé par CacheManager)Marquer comme lu/non-lu
Thèmes
preferredColorScheme(.dark))ThemeManager.shared)#0A0A0F (bg), #111118 (surface), #1E1E2A (bordures)Langue UI
LanguageSettings.shared, méthode t(_ en:_ fr:)Problème : SwiftUI ne propage pas les @EnvironmentObject injectés via .environmentObject() vers les vues présentées modalement (fullScreenCover, sheet). L'app crashe avec _EXC_BAD_ACCESS ou "No observable object of type X found".
Solution : Passer les dépendances en paramètre let lors de l'init, ou utiliser des singletons. Exemples dans le code :
ReaderCoordinatorView reçoit authManager: AuthManager via letSettingsView présenté en sheet avec .environmentObject() explicite sur la NavigationStack wrappanteRègle : toujours injecter manuellement dans les modales, ne jamais supposer la propagation automatique.
ZIPFoundation ne peut pas ouvrir les fichiers RAR. Quand Kavita renvoie un CBR avec l'extension .cbz, la détection par magic bytes (52 61 72 21) dans ReaderCoordinatorView.fetchChapter() renomme le fichier en .cbr et le route vers ServerImageReaderView. Le fallback onExtractionFailed dans CBZReaderView gère aussi le cas à l'exécution.
Au lancement, KlibriApp.init() supprime Cache.db, Cache.db-wal, Cache.db-shm pour éviter les corruptions. URLCache.shared est réinitialisé avec 0 disque.
Bug : navigation bloquée après fermeture du lecteur sur iPhone. Corrigé en gérant correctement la capture de closures dans les Task.detached (Swift 6 @MainActor isolation).
Prérequis : Mac avec Xcode 15+, XcodeGen, compte Apple Developer actif.
Setup projet :
brew install xcodegen
cd LORVA/Klibri
xcodegen # génère Klibri.xcodeproj
open Klibri.xcodeproj
Définir le Team ID dans project.yml :
settings:
base:
DEVELOPMENT_TEAM: "XXXXXXXXXX"
Checklist release :
CHANGELOG.md avec les notes EN + FRMARKETING_VERSION dans project.ymlgit tag v1.0.X && git push lorva v1.0.Xpython3 scripts/upload_release_notes.py — affiche le texte prêt à collerBranches : pas de workflow branches. Développement sur master ou branche dev mergée sur master avant le tag.
Remote Git : lorva → gitlab.lorva.dev
| Composant | Choix |
|---|---|
| Langage | Kotlin |
| UI | Jetpack Compose + Material 3 |
| Architecture | MVVM + Hilt DI |
| Min SDK | 26 (Android 8.0 Oreo) |
| Target SDK | 35 |
| Auth | JWT Kavita, stocké dans EncryptedSharedPreferences (AES-256) |
| Images | Coil (Compose) |
| Réseau | Retrofit + OkHttp |
| DI | Hilt + KSP |
| Navigation | Navigation Compose |
| Persistance | DataStore Preferences + FileManager |
| Offline | DownloadManager custom + cache cacheDir |
| Sync progression | ProgressSyncManager (queue JSON + retry) |
app/src/main/java/fr/btjp/klibri/
├── KlibriApplication.kt
├── MainActivity.kt
├── core/
│ ├── AppSettings.kt # accent theme + langue (StateFlow)
│ ├── network/
│ │ ├── AuthManager.kt # multi-serveurs, EncryptedSharedPreferences
│ │ ├── KavitaApiClient.kt # Retrofit service
│ │ └── Models.kt # data classes Kotlin
│ └── persistence/
│ ├── DownloadManager.kt # téléchargement + épinglage hors-ligne
│ ├── ApiCache.kt # cache JSON API
│ └── ProgressSyncManager.kt # queue locale + retry
├── navigation/KlibriNavGraph.kt
├── features/
│ ├── auth/
│ │ ├── ServerSetupScreen.kt
│ │ └── ServerSetupViewModel.kt
│ ├── library/
│ │ ├── HomeScreen.kt + HomeViewModel.kt
│ │ ├── LibrariesListScreen.kt + LibrariesListViewModel.kt
│ │ ├── SeriesListScreen.kt + SeriesListViewModel.kt
│ │ └── SeriesDetailScreen.kt + SeriesDetailViewModel.kt
│ ├── readinglists/
│ │ ├── ReadingListsScreen.kt + ReadingListsViewModel.kt
│ │ └── ReadingListDetailScreen.kt + ReadingListDetailViewModel.kt
│ ├── reader/
│ │ ├── ReaderScreen.kt # lecteur unifié CBZ/EPUB/PDF
│ │ ├── ReaderViewModel.kt
│ │ ├── EpubReader.kt # WebView + injection CSS
│ │ └── ReaderChapterQueue.kt # navigation chapitres
│ └── settings/
│ ├── SettingsScreen.kt
│ └── SettingsViewModel.kt
└── ui/
├── theme/Color.kt + Theme.kt + Type.kt
└── components/SeriesCard.kt
Navigation Compose via KlibriNavGraph. Bottom navigation bar avec 4 onglets :
| Onglet | Contenu |
|---|---|
| Accueil | Hero card, EN COURS, RÉCEMMENT AJOUTÉS |
| Bibliothèques | Liste des librairies + grille séries |
| Listes de lecture | Reading Lists |
| Réglages | Serveurs, thème, langue, cache |
Pas de layout spécifique tablette actuellement (même nav que phone).
ReaderScreen est le composant unique gérant tous les formats. Le ReaderViewModel orchestre le téléchargement et détecte le format.
CBZ/CBR — mode paginé :
HorizontalPager de la librairie ComposeseriesIdrememberTransformableStateCBZ/CBR — mode webtoon :
LazyColumn scroll verticalseriesIdAsyncImage (Coil) + aspectRatio calculé au chargementEPUB :
max-width: 720px, navigation gauche/droite = scrollcolumn-count:1, column-width: colW, navigation par translateX#4AC694PDF :
PdfRenderer Android (natif)ReaderViewModel : fichier CBR/PDF routé vers le reader image si EPUB ne convient pasMulti-serveurs
EncryptedSharedPreferences (AES-256-SIV + AES-256-GCM)Progression sync
chapter.pagesRead)filesDir/klibri_progress/queue.jsonfilesDir/klibri_progress/pages.jsononResumeTéléchargement hors-ligne
filesDir/klibri-downloads/<serverId>-<chapterId>.<ext>cacheDir (7 jours)Thèmes
#0A0A0F, #111118, #1E1E2A)AppSettings.accentTheme)green (#4AC694)Langue UI
AppSettings.currentLanguage StateFlowvalues/strings.xml (EN) + values-fr/strings.xml (FR)Luminosité
Version 1.0.0 en développement actif. Fonctionnel : nav chapitres, webtoon, cache offline, multi-serveur, home refresh au switch serveur, brightness slider, restart depuis la série.
Priorité haute (non implémenté ou bugué) :
/api/search/searchPriorité moyenne :
6. Thèmes couleur 6 accents dans Settings
7. Nettoyage cache auto (> 7 jours) au démarrage
8. Indicateur connectivité HEAD /api/health 30s
9. Direction lecture par série (override persisté)
Priorité basse :
10. Layout tablette dédié
11. Recherche dans la library view
12. Headers Kavita X-Kavita-Client, X-Device-Id
Release Android : aucune release publique à ce jour (versionCode = 1, versionName = "1.0.0"). Distribution prévue via Google Play.
Les deux apps utilisent les mêmes endpoints. Toutes les URLs d'images nécessitent ?apiKey=<KEY>.
| Endpoint | Usage |
|---|---|
POST /api/account/login |
Authentification → token JWT + apiKey |
HEAD /api/health |
Vérification connectivité (30s) |
GET /api/library/libraries |
Liste des librairies |
POST /api/series/v2?pageNumber=X&pageSize=Y |
Séries d'une librairie (FilterV2) |
POST /api/series/on-deck?pageNumber=0&pageSize=30 |
Séries en cours |
POST /api/series/recently-added-v2?pageNumber=0&pageSize=20 |
Récemment ajoutés |
GET /api/series/volumes?seriesId=X |
Volumes d'une série |
GET /api/series/metadata?seriesId=X |
Métadonnées série |
POST /api/readinglist/lists |
Reading Lists |
GET /api/readinglist/items?readingListId=X |
Items d'une Reading List |
GET /api/collection/all-collections |
Collections (tags) |
GET /api/search/search?queryString=X |
Recherche globale |
GET /api/reader/chapter-info?chapterId=X&libraryId=X&seriesId=X |
Infos chapitre |
GET /api/reader/image?chapterId=X&page=Y&extractPdf=true&apiKey=KEY |
Page image (manga/PDF) |
GET /api/download/chapter?chapterId=X |
Téléchargement CBZ/EPUB/PDF |
POST /api/reader/progress |
Sauvegarder progression |
GET /api/reader/progress?chapterId=X&volumeId=Y&seriesId=Z&libraryId=W |
Lire progression |
POST /api/series/mark-read |
Marquer série comme lue |
POST /api/series/mark-unread |
Marquer série comme non-lue |
GET /api/image/series-cover?seriesId=X&apiKey=KEY |
Cover série |
GET /api/image/volume-cover?volumeId=X&apiKey=KEY |
Cover volume |
GET /api/image/chapter-cover?chapterId=X&apiKey=KEY |
Cover chapitre |
GET /api/image/readinglist-cover?readingListId=X&apiKey=KEY |
Cover Reading List |
GET /api/image/collection-cover?collectionTagId=X&apiKey=KEY |
Cover Collection |
| Environnement | URL | Usage |
|---|---|---|
| Kavita demo (App Store screenshots) | klibri-demo.btjp.fr |
Serveur public de démo pour Apple App Store review, apiKey EYzGSzYdywfPtmg2TaXxd |
| Kavita prod LORVA | 10.10.20.10 (interne) |
Kavita de production LORVA |
| Page produit | lorva.dev/klibri |
Site vitrine + release notes dynamiques (depuis GitLab releases) |
| Support | lorva.dev/klibri/support/ |
Lien affiché dans les Réglages iOS |
Repos : gitlab.lorva.dev (remote lorva)
Règles communes :
masterdev possible, mergée sur master avant releaseNommage commits : conventional commits (feat:, fix:, chore:)
Tags iOS : v1.0.X sur master, créés après merge. Après le tag, créer une Release GitLab (obligatoire — lorva.dev lit les releases via l'API GitLab pour afficher les notes de version dynamiquement).
Tags Android : aucun tag à ce jour.
| Version | Date | Points clés |
|---|---|---|
| 1.0.5 | 2026-05-23 | Collections (onglet iPhone + sidebar iPad), Reading Lists en grille, hero section RL/collection, mark read/unread par chapitre/volume/série, badge checkmark séries lues, lien support, fix version Settings |
| 1.0.4 | 2026-05-22 | PDF reader offline (PDFKit), navigation chapitres tous formats, sync progression 3 niveaux, indicateur connectivité, message d'accueil personnalisé, support iPhone TabView natif, iCloud Keychain sync |