Laureline's Wiki

Laureline's Wiki

Application Desktop

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 (desktop) 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.