Laureline's Wiki

Laureline's Wiki

Implémentation

Implémentation

Librarie libVCL

La libVCL à été concue pour fournir un maximum d'abstractions pour la gestion des catalogues pour permettre une forte séparation avec l'implémentation ce qui permettrai éventuellement à d'autre implémentations que celle de base de fournir les même fonctionnalités pour, par exemple, des plateformes mobiles qui n'ont pas été prises en compte lors de ce projet. Cette séparation permet aussi la gestion des différents moteurs de stockage et emplacements de manière complétement transparente pour l'utilisateur.

Design de l'API

L'API publique à été concue pour offrir un maximum de fonctionnalités aux utilisateurs sans compromettre la flexibilité d'implémentation. La seule classe offrant une implémentation concrète étant la CatalogFactory chargée d'instancier les bonnes sous-classes en fonction du type de catalogue.

Si plus d'options de stockage venaient à se présenter (ex. JSON), un refactor de la factory serait très probablement nécéssaire afin de mieux supporter des implémentations arbitraires.

La première partie de l'API est une copie conforme du modèle de domaine en termes d'implémentation. Elle fournit un moyen de parcourir et modifier les données du catalogue de manière simple.

<uml> hide empty methods hide empty fields

class CatalogFactory «Singleton» {

+ load(url, config?): Catalog
+ create(url, config?): Catalog

} CatalogFactory ..> Catalog : creates

abstract class VolumeStore {

+ getVolume(metadata, type)
+ addVolume(metadata, volume)
+ deleteVolume(metadata)
+ editVolume(metadata)

} VolumeStore ..> Volume : loads VolumeStore <.. VolumeMetadata

abstract class Catalog {

+ isRemote(): bool
+ isLocal(): bool

} Catalog –> Category : root

interface CatalogNode {

+ name

} Catalog ← CatalogNode : catalog CatalogNode - VolumeMetadata

CatalogNode <|– Category interface Category { } CatalogNode “*” – “1” Category

CatalogNode <|– Series interface Series { } Series → SeriesMetadata : metadata

interface VolumeMetadata {

+ number
+ title

} VolumeMetadata ← Volume VolumeMetadata –> “0..1” SeriesMetadata

interface SeriesMetadata {

+ authors[*]
+ artists[*]
+ released
+ summary

} SeriesMetadata –> Tag : tags

interface Tag {

+ name

}

interface Volume { } Volume –> “*” Page

interface Page {

+ contents

}

</uml>

La seconde partie de l'API permet d'interragir avec les différents éléments de stockage liés aux données d'un catalogue.

  • CatalogConfig configure les chemins du cache du catalogue. Les différents chemins fournis par cette classe sont passés aux autres composants.
  • INodeFactory permet de créer des classes correspondant au moteur de stockage du catalogue.
  • ICatalogWriter permet de sauvegarder le catalogue en utilisant son moteur de stockage.
  • VolumeStore gère le chargement, sauvegarde, ajout, import et supression de volumes dans le système de fichiers. Cetaines de ces opérations peuvent être indisponnibles selon l'emplacement du catalogue.
  • ThumbnailStore permet de récupérer la miniature associée à un volume. Selon l'implémentation elle sera soit extraite du fichier cbz ou téléchargée et mise en cache.

<uml> hide empty methods hide empty fields

class CatalogFactory «Singleton» {

+ load(url, config?): Catalog
+ create(url, config?): Catalog

} CatalogFactory ..> Catalog : creates CatalogFactory –> CatalogConfig : defaultConfig

class CatalogConfig {

+ cachePath

}

abstract class Catalog {

+ isRemote(): bool
+ isLocal(): bool

} Catalog → CatalogConfig : config Catalog –> INodeFactory Catalog –> ICatalogWriter Catalog –> VolumeStore Catalog –> ThumbnailStore

interface INodeFactory {

+ createCategory(name): Category
+ createSeries(name): Series
+ createVolume(number, name?): VolumeMetadata
+ createSeriesMetadata(): SeriesMetadata
+ createTag(): Tag

}

interface ICatalogWriter {

+ write()
+ writeTo()

}

interface Volume { }

class VolumeFactory «Singleton» {

+ getLoader(type): IVolumeLoader
+ getWriter(): IVolumeWriter

}

VolumeFactory ..> IVolumeLoader : creates VolumeFactory ..> IVolumeWriter : creates

interface IVolumeLoader {

+ load(path, metadata): Volume

} IVolumeLoader ..> Volume : loads

interface IVolumeWriter {

+ load(path): Volume
+ createEmpty(): Volume
+ write(path, volume)
+ createPage(volume): Page
+ setPageName(page)
+ setPageContents(page)

}

abstract class VolumeStore {

+ getVolume(metadata, type): Future<Volume>
+ addVolume(metadata, volume)
+ deleteVolume(metadata)
+ editVolume(metadata): Future<Volume>

} VolumeStore …> IVolumeLoader : uses

abstract class ThumbnailStore {

+ getThumbnail(metadata): Future<byte[]>

} </uml>

 

Moteurs de Stockage

La librarie implémente actuellement deux moteurs de stockage: XML et SQLite. Ces deux moteurs se conforment à l'API définie en implémentant les interfaces fournies.

Moteur XML

Le moteur de stockage XML permet de stocker les métadonnées sous la forme d'un ou plusieurs fichiers XML. Les fichiers de ce moteur peuvent être modifiés à la main par un utilisateur expérimenté. Le moteur de stockage XML se démarque par sa simplicité en terme d'implémentation.

L'avantage de ce moteur de stockage est qu'il peut être utilisé pour aussi bien des catalogues locaux que distants. Il est même possible de simplement transférer un catalogue XML d'un emplacement local sur un serveur HTTP et de le rendre disponnible comme catalogue distant.

Le catalogue est entièrement chargé en mémoire lors de sa lecture depuis le disque. Le parsing s'effectue à l'aide de l'API DOM fournie par java en utilisant principalement des requètes XPath pour facilement trier les défférent types de noeuds.

L'écriture s'effectue à l'aide de la librairie dom4j car l'API DOM java est difficile d'utilisation. Lors de son écriture, un catalogue est complétement ré-écrit sur le disque.

Un exemple de catalogue XML peut être trouvé sur le CD-ROM.

Moteur SQLite

Le moteur SQLite permet d'enregistrer des informations sur les catalogues sur une base de données SQLite.

Il se base sur le framework OrmLite. Voici son modèle UML:

<uml> hide empty fields hide empty methods

class category {

+ id: int {id}
+ parent_id: int {fk}
+ name: string

}

class series {

+ id: int {id}
+ parent_id: int {fk}
+ metadata_id: int {fk}
+ name: string

}

class series_metadata {

+ id: int {id}
+ summary: string
+ released: int
+ author: string
+ artist: string
+ tag: string

}

class volume_metadata {

+ id: int {id}
+ series_parent_id: int {fk}
+ category_parent_id: int {fk}
+ number: int
+ name: string

}

category – “*” category category – “*” series

series → series_metadata

category – “*” volume_metadata series – “*” volume_metadata </uml>

Malheureusement, l'utilisation d'OrmLite n'a pas apporté les résultats souhaités car les objets actifs liés à la base de données fonctionnent de manière particulière, ce qui est incompatible avec le fonctionnement de la libVCL.

Une solution possible est de créer, depuis nos objets actifs SQLite, des objets contenant les informations mais sans liaison à la base de données. Notre application pourrait alors travailler dessus en faisant abstraction de ces problèmes. Puis, lors de la sauvegarde, notre moteur n'aurait plus qu'à parcourir ces objets et les enregistrer.

Cette solution n'a pas été utilisée pour ce projet car elle était jugée plus compliquée que de travailler sur des objets actifs. Cependant, il s'est avéré, après avoir résolu les problèmes liés aux objets actifs, qu'elle était en réalité bien plus simple. Voici ci-dessous une description des problèmes rencontrés et la solution apportée.

Tout d'abord, il est impossible de créer des relations entre des objets qui ne sont pas enregistrés dans la base de données. Par exemple, si on crée un volume qui doit appartenir à une série et que celle-ci n'a pas encore été enregistrée dans la base de données, cela va lever une exception. La solution est qu'à chaque création d'objets, il faut immédiatement l'enregistrer dans la base de données. Ce qui a pour désavantage qu'il n'est pas possible de travailler sur des objets temporaires n'étant pas enregistrés.

Ensuite, une catégorie doit pouvoir contenir à la fois des séries et d'autres catégories. Cependant depuis SQLite, cela se traduit par deux collections distinctes, alors que notre API veut que nous puissions récupérer d'une fois la liste contenant les deux et de pouvoir ajouter un élément depuis celle-ci sans savoir lequel. Il a donc été nécessaire de créer un objet qui émule une liste contenant les deux listes et qui, à l'ajout d'un élément, récupère le type de celui-ci et l'ajoute à la bonne liste.

Enfin, lorsque l'on modifie des informations sur un enregistrement, il faut appeler une méthode qui met à jour cet élément sur la base de données et l'enregistrement lui-même. Le problème vient de cette dernière opération car quand OrmLite met à jour un élément il change les références vers les objets parents, ce qui a pour conséquence de corrompre la hiérarchie du catalogue.

Le dernier problème n'a pas été résolu car le seul moyen de le résoudre est d'implémenter une autre solution qui ne travaille pas avec des objets actifs. Ce qui reviendrait à recommencer le développement du début, alors que ce problème a été découvert à la fin du projet, après de longues heures de recherche.

Il en résulte que les catalogues SQLite ne sont accessibles qu'en lecture et qu'il faut redémarrer l'application à chaque modification du catalogue.

 

Gestion des Catalogues

Il existe deux types de catalogues: Locaux et Distants. Un catalogue local est disponible en écriture ET en lecture tandis qu'un catalogue distant n'est disponible qu'en lecture. Un catalogue est définit par son URL de base. Deux catalogues différents ne devraient pas tenter de partager une même racine en écriture.

<uml> hide empty members

abstract class Catalog {

+ isRemote(): bool
+ isLocal(): bool

}

Catalog <|– LocalCatalog Catalog <|– RemoteCatalog

LocalCatalog –> LocalVolumeStore RemoteCatalog –> RemoteVolumeStore

LocalCatalog –> LocalThumbnailStore RemoteCatalog –> RemoteThumbnailStore </uml>

Un catalogue local stocke toute ses informations dans sa racine. Le dossier dans lequel se trouve le fichier racine du catalogue détermine l'emplacement de stockage des volumes de ce catalogue. Etant donné que tous les chemins référencés dans un catalogue sont relatifs à son dossier de base, il est possible de déplacer un catalogue sur le système de fichiers sans que les métadonnées ne soient corrompues.

Un catalogue distant est très semblable à un catalogue local, excepté qu'il n'est disponible qu'en lecture seule pour ses utilisateurs. Il est d'ailleurs possible de copier les fichiers d'un catalogue local au format XML sur un point d'accès HTTP et il sera alors disponible en tant que catalogue distant, il sera néanmoins nécessaire de générer les miniatures pour qu'elles soient disponibles.

 

Gestion des Volumes

La gestion des volumes utilise l'api 7zip-JavaBindings. Cette api permet d'interragir avec 7zip, logiciel gratuit, permettant de décompresser et compresser beaucoup de format d'archives actuels, tel que rar, 7zip, zip, bzip2, etc. 7zip est par ailleurs disponible sur tous les Systèmes d'exploitations courants. 7zip-JavaBindings est aussi disponible sur les systèmes d'exploitation courants (windows, linux, mac osx et arm).

Ce choix a été motivé par l'existance même de cette api. En effet, le fait de devoir potentiellement traiter plusieurs types d'archives aurait été compliqué à implémenter en java et pas nécessairement supporté par java de base.

C'était un choix risqué à prendre, car personne dans le groupe n'avait utilisé 7zip-JavaBindings auparavant. Nous ne savions donc pas comment l'api allait réagir, si elle avait des bugs ou pas, etc.

7zip-JavaBindings fonctionne en mode 1-1, c'est à dire que lors de l'extraction d'une archive, l'on doit appeler systématiquement la fonction extractSlow() qui permet d'extraire un seul fichier de l'archive à la fois. Cette fonction est lente, d'où son nom, mais comparé à l'autre solution, qui se comporte comme une approche c++, cette solution parraissait la plus simple. Après quelques tests il s'est avéré que cette fonction n'est pas si lente que cela. A cause de ce comportement de 7zip-Javabindings, chaque archive doit être extraite via une boucle, qui permet de traverser l'entier des fichiers présents dans l'archive.

Afin d'éviter de trop surcharger l'application, qui peut tourner sur un pc plus lent que les nôtres, un système de Lazy loading a été mis en place afin de ne pouvoir charger qu'une seule partie d'un volume et non l'entier. Ce système prend en paramètre une certaine plage de page à charger et permet de charger ces dernières uniquement. Au fur et à mesure de l'avancement dans les pages, les suivantes peuvent être chargées dynamiquement.

L'architecture des volumes se présente comme suit:

<uml> hide empty fields hide empty methods

Interface Volume Volume : getPages(): List<Page> Volume : getMetadata(): VolumeMetadata

Volume <|– LazyVolume LazyVolume : loadPage(int index) LazyVolume : loadPages(int start, int end)

Volume <|– EagerVolume

Volume <|– MutableVolume MutableVolume : createPage(): Page

Volume *- Page :contains

Interface Page Page : getImage(): byte[] Page : setPath() Page : compareTo(Page other): int

Page <|– LazyPage Page <|– EagerPage Page <|– MutablePage MutablePage : getFilename(): String MutablePage : setImage(byte[] i) MutablePage : setFilename(String name)

MutableVolume <.. VolumeWriter :creates

EagerVolume <.. EagerVolumeLoader :creates LazyVolume <.. LazyVolumeLoader :creates

Interface IVolumeLoader IVolumeLoader : load(String path, VolumeMetadata metadata): Volume EagerVolumeLoader –|> IVolumeLoader LazyVolumeLoader –|> IVolumeLoader

Class ImageReader ImageReader : write(byte[] data): int ImageReader : makePage(): Page LazyVolumeLoader …> ImageReader :uses EagerVolumeLoader …> ImageReader :uses

Class “7zip-JavaBindings” as 7jb ImageReader ..> 7jb :uses VolumeWriter …> 7jb :uses LazyVolumeLoader …> 7jb :uses EagerVolumeLoader …> 7jb :uses

Interface IVolumeWriter IVolumeWriter : load(String path): Volume IVolumeWriter : write(String path, Volume volume) IVolumeWriter : createEmpty(): Volume IVolumeWriter : createPage(Volume volume): Page IVolumeWriter : setPageName(Page page, String name) IVolumeWriter : setPageContents(Page page, byte[] content) VolumeWriter –|> IVolumeWriter

</uml>

Comme on peut le voir sur le schéma ci-dessus, les volumes sont composés de pages. Ces dernières sont de type différents selon les différents types de volumes. Un EagerVolume contiendra des EagerPage, un LazyVolume des LazyPage et ainsi de suite. Chaque type de page est légèrement différent d'un autre, car ils ne servent pas les mêmes usages.

Nous pouvons aussi observer que les deux VolumeLoader (Eager et Lazy) utilisent tous-deux la classe ImageReader. Cette dernière fait le lien entre les 7zip-JavaBindings et la librairie. Cette classe est particulièrement utilisée pour récupérer les données de l'archive ouverte. Elle contient notamment la fonction write(byte[] data) qui permet à 7zip d'aller écrire dans ce byte array ce qu'il a lu dans le fichier en extraction (une archive peut évidement contenir plusieurs fichiers). Lors de l'écriture c'est un peu plus compliqué. Le VolumeWriter appèle une fonction 7zip-JavaBindings, qui elle même va appeler une fonction de l'api 7zip. Cette fonction 7zip va ensuite rappeler via un callback la classe StatusCallback (classe privée de VolumeWriter), qui contient les fonctions getStream(int index) et getItemInformation(int index, OutItemFactory<IOutItemZip> outItemFactory). Ces dernières permettent à 7zip de créer le fichier dans l'archive, avec les bonnes permissions, bon chemin, etc.

Petit bémol, en travaillant directement avec l'archive, les ficheirs sont lus dans leur ordre au sein de l'archive. Ceci ne garantit pas du tout le bon ordre des pages d'un volume. En effet si la page 2 est enregistrée avant la 0 et la 1, alors le premier fichier lu sera la page 2 et non la page 0 comme on pourrait s'y attendre. Ceci a dû être traité lors du chargement du volume. Le EagerVolumeLoader chargeant toutes les pages d'un coup, il n'a pas été difficile de trier ses pages une fois chargées en mémoire. Le LazyVolumeLoader permettant de ne charger qu'un certain nombre de pages, ne permet pas de trier aussi facilement les pages. En effet, il suffit que la page 0 soit la dernière, alors les 10 premières pages peuvent être les pages 1 à 10, mais devraient être les pages 0 à 9. Une solution simple serait de charger, trier et réécrire le volume, mais c'est très peu propre et non-performant. Ceci posant un sérieux problème lors du chargement du volume, le chargement en mode Lazy n'a pas été implémenté dans la liseuse (qui devrait dans ce mode demander les pages suivantes à chaque changement de page affichée).

 

Crawler

Le crawler est un module supplémentaire fournit par la librairie qui permet de télécharger une série à partir d'un site web. Ceci permet la lecture “on the go” de séries sans la nécessité de posséder une connexion internet.

Le fonctionnement et la structure du crawler est une ré-implémentation du projet MangaArchive qui a inspiré la création de cette application.

<uml> hide empty fields hide empty methods

class CrawlerFactory {

+ register(crawler)
+ registerAll()
+ forUrl(url): Optional<ICrawler>
+ getExecutor(): Executor
+ setExecutor(executor)

} CrawlerFactory ..> ICrawler : provides

interface ICrawler {

+ canCrawl(url): boolean
+ crawl(url): WebSeries
+ crawlAsync(url): ProgressFuture<WebSeries>

} ICrawler .> WebSeries : creates

class Builder {

+ build(webSeries): boolean
+ buildAsync(webSries): ProgressFuture<boolean>

} WebSeries .> Builder

WebSeries –> WebVolume WebVolume –> WebChapter WebChapter –> WebPage

</uml>

Le module fonctionne en deux temps: Crawling et Building. La phase de crawling construit un descripteur à partir des informations disponibles sur la page de la série. Un descripteur contient les métadonnées de la série construite en “best effort” et la référence vers toutes les images (pages) nécessaires à la construction de la série. Les images sont organisées en chapitres eux mêmes organisés en volumes. L'inclusion des chapitres dans la structure permet à certaines implémentations de mieux gérer la récupération des URLs des pages. Une fois la création du descripteur complet il est possible de le sauvegarder au format JSON pour une utilisation future, par exemple si le processus de construiction est interrompu. La phase de building utilise le descripteur créé lors de la phase précédente, effectue le téléchargement de toutes les images et les ajoute à un nouveau volume qui est alors ajouté à la série. Une fois le processus terminé, le série est ajoutée à la racine du catalogue et peut être modifiée par l'utilisateur.

Chacun des deux modules fournit deux modes de fonctionnement: Synchrone et Asynchrone. Le mode synchrone fontionne en appelant le mode Asynchrone et en bloquant sur le résultat. Le mode asynchrone détache son fonctionnement en arrière-plan dans l'executor fournit par la CrawlerFactory et retourne un ProgressFuture. L'objet ProgressFuture hérite de CompletableFuture et permet en plus de reporter l'avancement de la tâche aux objets s'étant enregistrés pour recevoir les notifications de progrès.

Gestion de Progression

La gestion de la progression se fait au travers d'une classe héritant de CompletableFuture. Les Promesses et Futures sont déjà fortement utilisées dans d'autre parties de l'application ou les opérations asynchrones sont nécessaires, par exemple, lors du chargement d'un volume. Dans le cas courant, en plus de fournir la promesse d'une réponse, étant donné que l'opération peut durer plusieurs minutes, elle doit aussi fournir à son appelant un moyen de surveiller l'avancement de l'opération. Une option aurait été de créer une classe spécialisée pour cette tâche, mais cela aurait nécessité à l'appelant de devoir gérer un objet supplémentaire. L'option choisie à été de créer une classe ProgressFuture qui dérive de CompletableFuture et qui fournit la possibilité à la tâche de communiquer son avancement à travers un canal asynchrone à son appelant.

<uml> hide empty fields hide empty methods

class CompletableFuture<TValue> {

+ whenComplete(consumer)
+ whenCompleteAsync(consumer, executor)

}

CompletableFuture <|– ProgressFuture class ProgressFuture<TValue> {

+ whenProgress(consumer)
+ whenProgressAsync(consumer, executor)
+ whenMessage(consumer)
+ whenMessageAsync(consumer, executor)

}

</uml>

 
 

Application Desktop

La partie graphique de l'application à été réalisée sous la forme d'une application JavaFX. Nous avons choisi le design “single-window” afin de fournir une application avec une interface unie. Les composant communiquent entre eux à l'aide d'un Message Bus pour permettre un certain découplage des responsabilités entre, par exemple, le composant demandant l'ouverture d'un onglet pour un volume et le composant qui crée les onglets dans sa vue.

Annecdote de développement Durant les tests d'utilisation, nous avons remarqué que l'application accumulait beaucoup de mémoire qu'elle ne libérait jamais. Après une analyse avec un profiler Laureline à découvert que le message bus Google Guava gardait des références fortes sur tous les objets enregistrés (implementation) et ne les relachais jamais. Nous avons donc changé de librarie pour MBassador ce qui à résolu les problèmes de fuite mémoire.

Bibliothèque

Bibliothèque

La bibliothèque est implémentée à l'aide de plusieurs contrôleurs. Un contrôleur principal qui gère la TreeView qui affiche les catalogues et un contrôleur secondaire qui se charge de l'affichage des volumes. Chaque volume possède aussi son propre contrôleur afin de pouvoir gérer l'affichage et les actions d'un volume de manière isolée.

Affichage des Catalogues

L'affichage des catalogues utilise une TreeView pour afficher les catalogues. Pour pouvoir appliquer des filtres au contenu de la TreeView, les classes RootCatalogItem et CatalogItem on été créées. La classe RootCatalogItem se place comme élément racine (La TreeView est configurée pour ne pas afficher son élément racine mais uniquement ses enfants) et expose comme enfants les catégories racines des catalogues enregistrés dans le système sous la forme de CatalogItems qui eux mêmes possèdent plusieurs CatalogItems enfants. Ceci forme un arbe parallèle à la structure des catalogues sous-jacents. Ces objets peuvent être considérés comme des “proxy” vers la vértiable structure.

<uml> hide empty fields hide empty methods

TreeView → “1” RootCatalogItem RootCatalogItem → “*” CatalogItem CatalogItem –> “*” CatalogItem CatalogItem –> CatalogNode RootCatalogItem –> “1” Catalog Catalog → “1” Category CatalogNode <|– Category CatalogNode <|– Series </uml>

Ces objets proxy permettent d'implémenter un système de filtre sur l'arborecence sans qu'il soit nécessaire de recréer toute la structure. La classe CatalogItem expose une méthode refresh() qui synchronise ses enfants avec l'objet sous-jacent et conserve l'ordre des enfants.

Filtrer un Catalogue

Une méthode applyFilter(filter) est aussi disponible sur les CatalogItems. Cette méthode permet de restreindre quels noeuds sont affichés. Le filtre est appliqué de manière récursive aux enfants. Le noeud est considéré comme ayant une correspondance si il remplit les critères du filtre ou si un de ses enfants remplit les critères du filtre. Si un enfant ne correspond pas au filtre il est retiré de la liste des enfants du noeud (si il y figure), de même si un enfant correspond aux critères il sera ajouté à la liste des enfants (si il n'y figure pas). La liste des enfants est ensuite retriée en fonction de l'ordre d'origine.

Ce fonctionnement permet d'afficher des noeuds qui correspondent au filtre même si leur parents ne le sont pas.

Modification du Catalogue

Modification d'une série En plus d'afficher la structure d'un catalogue, la bibliothèque permet aussi d'en modifier le contenu si il est local.

La bibliothèque présente des dialogues lors de la modification ou la création d'éléments. Un système pour gérer les dialogues “complexes” a été mis en place.

<uml> hide empty fields hide empty methods

abstract class Controller

Controller <|– DialogController abstract class DialogController<TResult> {

+ getTitle()
+ getButtons()
+ getResult(button)

}

DialogController <|– ModalController abstract class ModalController<TValue> {

+ setValue(value)
+ getValue()
+ commit()

}

</uml>

Le DialogController permet de présenter un dialogue en mode modal et renvoie un résultat. Ce type de dialogue permet de renvoyer un resultat en fonction du bouton qui a été appuyé. Le résultat est transformé en optional en fonction de sa valeur (en utilisant Optional.ofNullable()).

Le ModalController présente un dialogue “Ok, Cancel” en mode modal avec une valeur existante et permet au contrôleur de la modifier lorsque l'utilisateur sélectionne “Ok” via la méthode commit(). Si l'utilisateur annule l'action, la méthode n'est pas appelée et l'objet ne sera donc pas modifié.

Les différents contrôleurs d'ajout et de modification des objets du catalogues héritent de ces deux classes afin de faciliter leur implémentation.

Affichage des Volumes

Les volumes sont affichés dans une TilePane. Chaque volume est un tile qui affiche sa couverture (obtenue à l'aide du ThumbnailStore de son catalogue) ainsi que son titre.

 

Liseuse

Liseuse

La liseuse permet de lire les différents volumes de la bibliothèque. Elle est à part de le bibliothèque et ne fait que prendre un volume en entrée. Elle offre à l'utilisateur plusieurs option de navigation telle que:

  • Changer le sens de lecture: Cela permet, pour certains mangas, de lire de droite à gauche plutôt que de gauche à droite. Cela est surtout pratique quand on utilise les doubles pages.
  • Simple ou double pages: Cela gère l'affichage et définit si une seule page à la fois ou deux pages en même temps doivent être affichées. L'affichage de deux pages simultanées simule l'expérience qu'un utilisateur pourrait avoir si il lisait un livre physique.
  • Redimmensionnement de la page: Il est possible de définir le redimmensionnement automatique de la page sois sur la hauteur, sois sur la largeur de la fenêtre.

L'implémentation de la logique de la liseuse est faites grâce à un Navigator implémenté dans le package navigation. La classe Navigator étant abstraite, elle donne la possibilité de créer des Navigator pour un nombre de pages voulu. Nous avons décidé de ne l'implémenter que pour les pages simple (SimpleNavigator) et pour les pages doubles, c'est à dire 2 pages simples (DoubleNavigator) car cela n'a pas de sens dans notre contexte de l'utiliser pour plus de pages.

Chaque Navigator contient un volume et une page courante qui est affichée. Ces pages sont définie au tant que DisplayPage, qui est aussi une classe abstraite permettant d'implémenter des classes définies sur un nombre de pages voulu. Nous avons implémenté les classes pour des pages simples (SimplePage) et des pages double, c'est à dire 2 pages simples (DoublePage). Une fois encore, nous n'avons pas implémenté d'autre classe, cela n'ayant pas de sens pour nous mais cela serait tout à fait possible.

En résumé, le Navigator permet d'implémenter la logique par laquelle les pages doivent être parcourues alors que les DisplayPage permettent simplement de contenir une page de manière contrôlée et définie dans le programme.

Voici un diagramme de classe pour le Navigator:

<uml> hide empty fields hide empty methods

abstract class Navigator{

  1. vol: Volume
  2. direction: Direction
  3. currentPage: DisplayPage

+setDirection(sens: Direction) : void

+setCurrentPage(page: DisplayPage) : void
+getVolume() : Volume
+getDirection(): Direction
+getCurrentPage(): DisplayPage

} class SimpleNavigator {

+SimpleNavigator(volume: Volume, direction: Direction, beginIndex: Int)
+SimpleNavigator(nav: DoubleNavigator)
+nextPages(): DisplayPage
+previousPages(): DisplayPage
+goTo(index: int): DisplayPage

} class DoubleNavigator {

+DoubleNavigator(volume: Volume, direction: Direction, beginIndex: Int)
+DoubleNavigator(nav: SimpleNavigator)
+update(): void
+nextPages(): DisplayPage
+previousPages(): DisplayPage
+goTo(index: int): DisplayPage

} interface INavigator {

+nextPages() : DisplayPage
+previousPages() : DisplayPage
+goTo(index: int): DisplayPage
+setDirection(sens: Direction) : void
+setCurrentPage(page: DisplayPage) : void
+getVolume() : Volume
+getDirection(): Direction
+getCurrentPage(): DisplayPage

}

enum Direction{

LTR,RTL

}

Navigator –> SimpleNavigator Navigator –> DoubleNavigator INavigator –> Navigator </uml>

On peut voir que l'on utilise une interface INavigator qui oblige les implémentations de définir les méthodes essentielles à la logique d'un navigateur. Le Navigator implémente partiellement certaines méthodes qui sont communes à n'importe quel navigateur.

Voici un diagramme de classe pour le DisplayPage: <uml> hide empty fields hide empty methods

abstract class DisplayPage{

  1. imageIndexes: ArrayList<Integer>
  2. imageReferences: ArrayList<Image>
  3. direction: Direction

+DisplayPage(indexes: ArrayList<Integer>, images: ArrayList<Image>, dir: Direction)

+getImageIndexes(): ArrayList<Integer>
+getImageReferences(): ArrayList<Image>
+getDirection(): Direction

} class SinglePage {

+SinglePage(index: int, image: Image, dir: Direction)
+getImage(): Image
+getIndex(): int

} class DoublePage {

+DoublePage(index1: int, index2: int, image1: Image, image2: Image, dir: Direction)
+getFirstImage(): Image
+getSecondeImage(): Image
+getFirstIndex(): int
+getSecondIndex(): int

} interface IDisplayPage {

+getImageIndexes(): ArrayList<Integer>
+getImageReferences(): ArrayList<Image>
+getDirection(): Direction

}

DisplayPage –> SinglePage DisplayPage –> DoublePage IDisplayPage –> DisplayPage </uml>

Ici, la logique est la même que pour le navigateur. Une interface définissant les méthodes obligatoires et une classe DisplayPage qui permet de faire une implémentation partielle des pages.

Une subtilité réside dans le DoubleNavigator. En effet, il est possible que des page doubles, c'est à dire des pages qui, dans un livre physique sont en paysage, se trouvent dans le volume. Ces pages ont été mises par les gens qui ont numérisé le livre comme une grande page qui est plus large que haute. Cela a été fait pour conserver le travail de l'auteur et ne pas dénaturer ces images en les séparant sur deux pages différentes. Dans ce cas, si jamais le DoubleNavigator voit qu'il y'a une double page, il va uniquement afficher la prochaine page et non les 2 prochaines pages. Cela à pour effet que la page courante dans le navigateur sera une SimplePage et non une DoublePage comme on pourrait s'y attendre. Avec ce système, la vue n'a pas à ce soucier du type de navigateur qu'elle utilise, étant donné qu'elle reçoit une DisplayPage et de ce fait, il lui suffit de faire un contrôle du type de page qu'elle a pour donner le bon affichage.

Exemple d'image simple

Exemple d'image double

Redimensionnement

La liseuse offre trois options de redimensionnement: Complet, Horizontal, Vertical. Le mode complet affiche la/les page(s) courante(s) au complet sur la fenêtre. Le redimensionnement Horizontal agrandi l'image jusqu'à ce que sa largeur soit celle de la fenêtre, l'utilisateur peut alors faire défiler l'image verticalement. De même, le mode vertical effectue un redimensionnement par rapport à la hauteur de l'image.

La logique est implémentée dans la fonction ReaderController#refreshScaling() qui prend en charge tout les modes ainsi que le mode simple et double page. Cette fonction est appellée quand l'utilisateur change les dimensions de la fenêtre et lors d'un changement de page.

 

Editeur

Editeur

L'editeur à été implémenté de manière très simple. Il présente la liste des images présentes dans l'archive avec une aperçu lorsqu'elle est sélectionnée.

Il est possible d'ajouter une ou plusieurs images à l'aide du bouton “Add” et supprimmer une page à l'aide du bouton “Delete” ou de la touche backspace. Le bouton “Save” écrit le volume sur le disque à l'aide du IVolumeWriter.

Cette présentation à été préférée à celle présentée dans le cahier de charges car elle est beaucoup plus simple à gérer au niveau backend sans sacrifier la facilité d'utilisation. Il avait été prévu d'implémenter un Drag & Drop pour permettre le réagencement des pages, mais étant donné que les fichiers sont triés par nom avant la lecture, il est facile de réordonner les fichier en changeant leur noms.

 

Crawler

Crawler

Le crawler est implémenté comme un module “standalone” qui peut être lancé depuis le menu principal de l'application. Il implémente une interface pour intéragir avec le module de l'API.

Les boutons sont mis à jour en fonction de l'état du système en utilisant les bindings javafx afin de gérer facilement les mises à jour.

Cette interface utilise les procédures asynchrones fournies par l'API. Ceci permet à l'interface de reporter à l'utilisateur la progression des différentes étapes de manière fluide.

ProgressFuture<WebSeries> future = crawler
  .crawlAsync(url)
  .whenProgressAsync(this::onCrawlProgress, PlatformExecutor.instance)
  .whenMessageAsync(this::onCrawlMessage, PlatformExecutor.instance)
  .whenCompleteAsync(this::onCrawlComplete, PlatformExecutor.instance);
  
private void onCrawlProgress(ProgressFuture<WebSeries> future) {
  // Met à jour la barre de progression
  crawlProgress.setProgress(future.getProgress());
  crawlCount.setText(String.format("%d/%d", future.getCurrent(), future.getMaximum()));
}
    
private void onCrawlMessage(ProgressFuture<WebSeries> future) {
  // Affiche le message dans le log
  log.appendText(future.getMessage() + "\n");
}

L'utilisation des méthodes de rapport asynchrones (whenCompleteAsync, whenProgressAsync, …) en conjonction avec un Executor spécial permettant aux méthodes d'être executées dans le contexte de l'UI permet une gestion très simple du rapport de progression.

L'interface permet aussi de charger et sauvegarder des descripteurs dans le pipeline afin de permettre aux utilisateurs de conserver les métadonnées pour un téléchargement ultérieur.