Getting started

Installation

npm install @prose-reader/core @prose-reader/core-streamer rxjs

You don't absolutely need to have @prose-reader/core-streamer installed to run the reader but the utilities provided are very useful to start quickly. rxjs it a peer-dependency of prose-reader and needs to be installed alongside.

Create your reader

On your html file you need to define a container for the reader. We will be using a full screen reader but you can have any dimensions.

index.html

<!doctype html>
  <html>
  <head>
    <style>
      body, html, #reader {
        height: 100%;
        width: 100%;
        margin: 0;
      }
    </style>
  </head>
  <body>
    <div id="reader"></div>
    <script src="/index.js"></script>
  </body>
</html>

index.js

import { createReader } from '@prose-reader/core'

const reader = createReader({ containerElement: document.getElementById('reader') })

Spin up your book

Once you have a reader instance you can load any book by providing the related manifest. For the sake of the simplicity we are going to render simple text content. We will see later how to stream .epub and other formats.

index.js

import { createReader } from "@prose-reader/core";
import {
  createArchiveFromText,
  getManifestFromArchive,
  getResourceFromArchive
} from "@prose-reader/core-streamer";

/**
 * You can create a unique instance for your entire project
 * or re-create one every time the user visit the reader page.
 * The same instance can be reused for different books.
 */
const reader = createReader();

(async () => {
  /**
   * First we need to convert the book into an archive. This is the format
   * used by the streamer sdk to generate manifest and resources. This is a sort
   * of common format that allow all the functions to works together seamlessly
   */
  const archive = await createArchiveFromText(`
    Lorem Ipsum is simply dummy text of the printing and typesetting 
    industry. Lorem Ipsum has been the industry's standard dummy text 
    ever since the 1500s, when an unknown printer took a galley of type 
    and scrambled it to make a type specimen book. It has survived not 
    only five centuries, but also the leap into electronic typesetting, 
    remaining essentially unchanged. It was popularised in the 1960s 
    with the release of Letraset sheets containing Lorem Ipsum passages, 
    and more recently with desktop publishing software like Aldus 
    PageMaker including versions of Lorem Ipsum.
  `);

  /**
   * getManifestFromArchive returns a Response object because it is highly suggested
   * to be used over HTTP, in a service worker for example.
   */
  const response = await getManifestFromArchive(archive);

  const manifest = await response.json();

  /**
   * Load the book by providing its manifest.
   * We are bypassing the default behavior of the reader for fetching resources.
   * Because we do not have a streamer, we just serve directly from the archive from
   * our script.
   *
   * It works for our example or light documents but is not really recommended.
   * The manifest should describe where to fetch resources and the reader will be
   * using the uri directly. (Usually served in a service worker).
   */
  reader.load(manifest, {
    // specify the container in which you want to display your book.
    container: document.getElementById("reader"),
    fetchResource: (item) => getResourceFromArchive(archive, item.path)
  });
})();

You should now see the book being displayed.

You also already have a quick peak of how to use the streamer sdk to quickly generate manifest and resources. The archive is only relevant when you use the streamer sdk and the manifest is nothing more than a JSON object that contains the information of the book. As explained earlier this part has to be done on your side as in our example.

This part is also better done separately. By default the reader will fetch all resources by HTTP so it's a good practice to offload the work in a service worker or a backend. You can also move the generation of manifest in a service worker, web worker, etc. As a general rule, try to avoid working with big files in the main thread as much as possible.

Adding controls and pagination

The reader sdk exposes various API to control or retrieve information. The two most important one are navigation and pagination. The first one let you control the book and the later gives you information on what is currently displayed.

Letā€™s add two buttons for navigating between pages:

html

<div id=ā€œreaderā€/>
<button id=ā€œreader-turn-left-buttonā€>Turn left</button>
<button id=ā€œreader-turn-right-buttonā€>Turn left</button>

script

document.getElementById(ā€reader-turn-left-buttonā€).addEventListener(ā€œclickā€, () => {
    reader.navigation.turnLeft()
})

document.getElementById(ā€reader-turn-right-buttonā€).addEventListener(ā€œclickā€, () => {
    reader.navigation.turnRight()
})

You now have two button which can navigate the book.

Letā€™s now add information about the page being displayed and disable navigation button if the user cannot turn pages anymore (edge of the book).

html

<div>You are at page(s) <span id="pagination-info-page"></span></div> 

Notice how we use page(s) plural possibility. This is because a book can be displayed as spread (two pages). We should display either current page or a range of pages.

script

const leftButton = document.getElementById(ā€reader-turn-left-buttonā€)
const rightButton = document.getElementById(ā€reader-turn-right-buttonā€)
const paginationInfoPageSpan = document.getElementById(ā€pagination-infoā€)

reader.navigation.state$.pipe(
    tap((state) => {
        leftButton.disabled = !state.canTurnLeft
        rightButton.disabled = !state.canTurnRight
    })
).subscribe()

reader.pagination.state$.pipe(
    tap((state) => {
        // `2` or `2 - 3`
        const pageText = state.beginAbsolutePageIndex === state.endAbsolutePageIndex 
            ? state.beginAbsolutePageIndex 
            : `${state.beginAbsolutePageIndex} - ${state.endAbsolutePageIndex}`

        paginationInfoPageSpan.textContent = pageText
    })
).subscribe()

Notice how we use absolute page information, absolute means relative to the entire book. You can also retrieve the page from the current chapter or spine item. In the end it's up to you how you want to display your book and thus what you want to show as information. begin and end are two important concepts. They describe the begining page and the end page visible. We use begin and end instead of left and right to be agnostic of the reading direction. Your code should not change whether the book is reading from left to right, right to left, or vertically.

Handle navigation for ltr, rtl and vertical

Our previous code is good but we did not handle vertical reading. left and right are usefull here to stay agnostic regarding ltr/rtl however they do not make sense in the context of vertical reading. We cannot use begin and end either since it would not work for rtl/ltr anymore. There are unfortunately no unique solution to handle all directions. That being said, prose consider right as right or bottom and left as left or top by convention. The direction taken will be automatically derived from the book settings itself and you don't need to do anything.

This means that your code already work for vertical direction as it is but is not straightforward for developer. Beside, you might also want to change the wording of the button.

Here are two options to make it better for the developer.

html

<div id=ā€œreaderā€/>
<button id=ā€œreader-turn-left-buttonā€>Turn left</button>
<button id=ā€œreader-turn-right-buttonā€>Turn left</button>

script

const leftButton = document.getElementById(ā€reader-turn-left-buttonā€)
const rightButton = document.getElementById(ā€reader-turn-right-buttonā€)

reader.settings.values$
    .pipe(
        tap(({ computedPageTurnDirection }) => {
            leftButton.textContent = computedPageTurnDirection === "vertical"
                ? "Turn top" : "Turn left"
            rightButton.textContent = computedPageTurnDirection === "vertical"
                ? "Turn bottom" : "Turn right"
        })
    )
    .subscribe()

leftButton.addEventListener(ā€œclickā€, () => {
    // alias to turnLeft
    reader.navigation.turnLeftOrTop()
    
    // or more explicit
    if (reader.settings.values.computedPageTurnDirection === "vertical") {
        reader.navigation.turnTop()
    } else {
        reader.navigation.turnLeft()
    }
})

rightButton.addEventListener(ā€œclickā€, () => {
    // alias to turnRight
    reader.navigation.turnRightOrBottom()
    
    // or more explicit
    if (reader.settings.values.computedPageTurnDirection === "vertical") {
        reader.navigation.turnBottom()
    } else {
        reader.navigation.turnRight()
    }
})

What next?

You learnt how to instanciate and load a book together with navigation and pagination concepts. These are the most importants keys you need to dive into prose. There are many more features but you can already make your own basic reader. Keep reading through the documentation to learn more.

Last updated