Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions Build/src/book.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ const tocContainer = document.getElementById('toc-container');
const tocContent = document.getElementById('toc-content');
const viewer = document.getElementById('viewer');

/***** Book Opening functions *****/
/**
* Open an EPUB file selected via a file input and load it into the viewer.
*
* Validates the selected file is an EPUB, shows a loading indicator, reads the file
* as an ArrayBuffer, and calls loadBook with the file data. On read/load errors
* it hides the loading indicator and displays an error message.
*
* @param {Event} e - Change event from a file input; the function reads e.target.files[0].
*/
export function openBook(e) {
const file = e.target.files[0];
if (!file) return;
Expand All @@ -50,7 +58,16 @@ export function openBook(e) {
}

// Immediately close library on click so the user sees the main viewer
// (and the loading spinner) right away
/**
* Open and load an EPUB from a library entry, managing the library UI and loading spinner.
*
* Reads the file from the given library entry (object with an async `getFile()` method), converts it to an ArrayBuffer,
* and delegates to `loadBook` to render the book. Closes the library and shows a loading indicator while loading.
* If an error occurs, the library is reopened and an error message is shown; the function always hides the loading indicator before returning.
*
* @param {Object} entry - Library entry providing an async `getFile()` method that returns a `File`/Blob.
* @return {Promise<void>} Resolves once loading has finished or an error has been handled.
*/
export async function openBookFromEntry(entry) {
// Close library right away
toggleLibrary(false);
Expand All @@ -68,6 +85,19 @@ export async function openBookFromEntry(entry) {
}
}

/**
* Load and render an EPUB into the viewer, initialise navigation, and wire up UI and event handlers.
*
* This replaces any currently loaded book, creates a new ePub instance and rendition rendered into the
* viewer element, generates the table of contents and location map, enables navigation controls,
* and registers relocation and keyboard listeners. The relocation handler updates the global
* `currentLocation` and the page input when location data exists. Also attempts to set the visible
* book title from metadata (with fallbacks).
*
* @param {ArrayBuffer|Uint8Array|Blob|string} bookData - EPUB data or URL accepted by epubjs.
* @param {string} [startLocation] - Optional initial location (CFI or href) to display.
* @returns {Promise} A promise that resolves when the rendition's initial display operation completes.
*/
async function loadBook(bookData, startLocation) {
if (book) {
book = null;
Expand Down Expand Up @@ -109,6 +139,14 @@ async function loadBook(bookData, startLocation) {
return displayed;
}

/**
* Generate the book's virtual pagination (locations) and update the UI with the total page count.
*
* This async function returns early if no book is loaded. It calls the EPUB book's
* locations.generate(1000) to build location data, stores the resulting locations in the
* module-level `locations` variable, and updates `totalPagesSpan.textContent` with the
* computed number of locations. Errors are caught and logged; the function does not throw.
*/
async function generateLocations() {
if (!book) return;
try {
Expand All @@ -120,6 +158,18 @@ async function generateLocations() {
}
}

/**
* Build and render the book's table of contents (TOC) into the UI.
*
* If a book is loaded, asynchronously reads the book's navigation TOC, clears the
* existing TOC container, and creates a clickable entry for each TOC item.
* Clicking an entry displays that location in the rendition and closes the TOC overlay.
*
* Does nothing if no book is loaded. Errors encountered while retrieving or
* rendering the TOC are caught and logged to the console.
*
* @returns {Promise<void>} Resolves when the TOC has been generated and appended to the DOM.
*/
async function generateToc() {
if (!book) return;
try {
Expand All @@ -140,14 +190,35 @@ async function generateToc() {
}
}

/**
* Navigate the viewer to the previous page.
*
* If a rendition is active, calls its `prev()` method; otherwise does nothing.
*/
export function prevPage() {
if (rendition) rendition.prev();
}

/**
* Advance the current rendition to the next page/location.
*
* This is a no-op if no rendition is initialized.
*/
export function nextPage() {
if (rendition) rendition.next();
}

/**
* Navigate the viewer to the page number entered in the page input field.
*
* Reads a 1-based page number from `currentPageInput.value`, converts it to a
* 0-based location index, validates it against the book's generated locations,
* converts that location index to a CFI using `book.locations.cfiFromLocation`,
* and displays it in the rendition.
*
* No action is taken if there is no loaded book or location data, or if the
* entered page number is out of range or not a valid integer.
*/
export function goToPage() {
if (!book || !locations) return;
const pageNumber = parseInt(currentPageInput.value, 10) - 1;
Expand All @@ -157,17 +228,31 @@ export function goToPage() {
}
}

/**
* Handle keyboard navigation: left/right arrow keys move to the previous/next page.
* @param {KeyboardEvent} e - Keyboard event; listens for 'ArrowLeft' to go to the previous page and 'ArrowRight' to go to the next page.
*/
function handleKeyEvents(e) {
if (!book || !rendition) return;
if (e.key === 'ArrowLeft') prevPage();
if (e.key === 'ArrowRight') nextPage();
}

/**
* Toggle the visibility of the table of contents overlay.
*
* Adds or removes the 'open' class on the TOC container and the overlay element to show or hide the table of contents.
*/
export function toggleToc() {
tocContainer.classList.toggle('open');
overlay.classList.toggle('open');
}

/**
* Close the table of contents overlay.
*
* Removes the 'open' class from the TOC container and the page overlay, hiding the table of contents.
*/
export function closeToc() {
tocContainer.classList.remove('open');
overlay.classList.remove('open');
Expand Down
24 changes: 24 additions & 0 deletions Build/src/indexedDB.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
/**
* Open (or create) the IndexedDB database "htmlreader-db" (version 1) and ensure the "handles" object store exists.
*
* Returns a promise that resolves to the opened IDBDatabase instance. During upgrade, an object store named
* "handles" with keyPath "name" is created if missing. The promise rejects with the underlying IndexedDB error
* if opening the database fails.
*
* @return {Promise<IDBDatabase>} Promise resolving to the opened IndexedDB database.
*/
function getDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open("htmlreader-db", 1);
Expand All @@ -9,6 +18,12 @@ function getDB() {
request.onerror = e => reject(e.target.error);
});
}
/**
* Persistently stores the provided library handle in the IndexedDB "handles" object store under the key "library".
*
* @param {*} handle - The library handle to store (e.g., a FileSystem handle or other serializable handle).
* @returns {Promise<void>} Resolves when the handle has been written to the database. Rejects with the underlying error if the database write fails.
*/
export async function storeLibraryHandle(handle) {
const db = await getDB();
return new Promise((resolve, reject) => {
Expand All @@ -19,6 +34,15 @@ export async function storeLibraryHandle(handle) {
req.onerror = e => reject(e.target.error);
});
}
/**
* Retrieve the stored library handle from the "handles" object store.
*
* Returns a Promise that resolves to the stored handle (if present) or null
* when no entry named "library" exists. The Promise rejects with the
* underlying IndexedDB error if the request fails.
*
* @return {Promise<any|null>} The stored library handle or null.
*/
export async function getStoredLibraryHandle() {
const db = await getDB();
return new Promise((resolve, reject) => {
Expand Down
41 changes: 40 additions & 1 deletion Build/src/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ const libraryContainer = document.getElementById('library-container');
const libraryContent = document.getElementById('library-content');
const overlay = document.getElementById('overlay');

/**
* Open the user's EPUB library: get or prompt for a directory, scan for .epub files, display them, and open the library UI.
*
* Attempts to use a previously stored directory handle; if none is available, prompts the user to pick a directory and stores the handle.
* Scans the directory for files whose names end with ".epub", passes those entries to the library grid renderer, and opens the library overlay.
* On failure, reports a user-facing error via showError; the function catches errors and does not rethrow.
*
* @returns {Promise<void>} Resolves after the library grid is displayed or after an error has been reported.
*/
export async function openLibrary() {
try {
// Try to retrieve stored library directory handle
Expand All @@ -29,13 +38,26 @@ export async function openLibrary() {
showError('Failed to open library: ' + err.message);
}
}
// Fallback for multiple file selection if directory picker is not available
/**
* Handle a file-input change by displaying selected EPUB files in the library and opening the library UI.
* @param {Event} e - Change event from a file input (`<input type="file" multiple>`); selected File objects are read and shown in the library grid.
*/
export function handleLibraryFiles(e) {
const files = Array.from(e.target.files);
displayLibraryGrid(files);
toggleLibrary(true);
}

/**
* Render a grid of EPUB items into the library UI.
*
* Clears the library content area and, for each entry in `fileEntries`, creates
* a library item (cover + title) and appends it to the grid. If `fileEntries`
* is empty, shows a "No EPUB files found." message instead.
*
* @param {Array<import('./types').FileEntry|File>} fileEntries - Array of file entries to display. Each entry may be a File, FileSystemFileHandle, or similar object accepted by createLibraryItem.
* @return {Promise<void>}
*/
async function displayLibraryGrid(fileEntries) {
libraryContent.innerHTML = '';
if (fileEntries.length === 0) {
Expand All @@ -50,6 +72,18 @@ async function displayLibraryGrid(fileEntries) {
}
}

/**
* Create a DOM element representing an EPUB library item (cover + title) for the given file entry.
*
* The function accepts either a File object or a FileSystemFileHandle (from the File System Access API),
* reads the EPUB to extract a cover image and metadata title when available, and falls back to the
* file name and a generic placeholder cover if not. It attaches a click handler that opens the book
* via openBookFromEntry(fileEntry). Errors while loading cover/metadata are caught and logged; they
* do not prevent the returned element from being used.
*
* @param {File|FileSystemFileHandle} fileEntry - The EPUB file or a handle for the EPUB file.
* @return {HTMLElement} A '.library-item' element containing an image ('.library-cover') and title ('.library-title').
*/
async function createLibraryItem(fileEntry) {
const item = document.createElement('div');
item.className = 'library-item';
Expand Down Expand Up @@ -93,6 +127,11 @@ async function createLibraryItem(fileEntry) {
return item;
}

/**
* Open, close, or toggle the library UI.
*
* @param {boolean|undefined} forceOpen - If true, ensures the library is opened; if false, ensures it is closed; if omitted, toggles the current state.
*/
export function toggleLibrary(forceOpen) {
if (forceOpen === true) {
libraryContainer.classList.add('open');
Expand Down
23 changes: 22 additions & 1 deletion Build/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,38 @@ closeErrorButton.addEventListener('click', hideError);
// Fallback: multiple file input for library import
libraryInput.addEventListener('change', handleLibraryFiles);

/***** Message Functions *****/
/**
* Show the global loading message/overlay.
*
* Makes the loadingMessage element visible by adding the CSS `show` class.
*/
export function showLoading() {
loadingMessage.classList.add('show');
}
/**
* Hide the global loading indicator.
*
* Removes the 'show' CSS class from the loading message element to hide the loading UI.
*/
export function hideLoading() {
loadingMessage.classList.remove('show');
}
/**
* Display an error message in the UI.
*
* Sets the visible error panel's text to `message` and makes the panel visible by adding the `show` class.
*
* @param {string} message - The error text to display to the user.
*/
export function showError(message) {
errorText.textContent = message;
errorMessage.classList.add('show');
}
/**
* Hide the visible error message UI.
*
* Removes the 'show' class from the error message element so the error overlay is hidden.
*/
export function hideError() {
errorMessage.classList.remove('show');
}
Loading