[fix] login PWA styling, [add] login local link, [add] home local link
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
319
assets/local/index.js
Normal file
319
assets/local/index.js
Normal file
@@ -0,0 +1,319 @@
|
||||
const GET_SW_CACHE = "GET_SW_CACHE";
|
||||
const DEL_SW_CACHE = "DEL_SW_CACHE";
|
||||
|
||||
// ----------------------------------------------------------------------- //
|
||||
// --------------------------- Event Listeners --------------------------- //
|
||||
// ----------------------------------------------------------------------- //
|
||||
|
||||
/**
|
||||
* Initial load handler. Gets called on DOMContentLoaded.
|
||||
**/
|
||||
async function handleLoad() {
|
||||
handleOnlineChange();
|
||||
|
||||
// If SW Redirected
|
||||
if (document.location.pathname !== "/local")
|
||||
window.history.replaceState(null, null, "/local");
|
||||
|
||||
// Create Upload Listener
|
||||
let uploadButton = document.querySelector("button");
|
||||
uploadButton.addEventListener("click", handleFileAdd);
|
||||
|
||||
// Ensure Installed -> Get Cached Items
|
||||
let swCache = await SW.install()
|
||||
// Get Service Worker Cache Books
|
||||
.then(async () => {
|
||||
let swResponse = await SW.send({ type: GET_SW_CACHE });
|
||||
return Promise.all(
|
||||
// Normalize Cached Results
|
||||
swResponse.map(async (item) => {
|
||||
let localCache = await IDB.get("PROGRESS-" + item.id);
|
||||
if (localCache) {
|
||||
item.progress = localCache.progress;
|
||||
item.percentage = Math.round(localCache.percentage * 10000) / 100;
|
||||
}
|
||||
|
||||
// Additional Values
|
||||
item.fileURL = "/documents/" + item.id + "/file";
|
||||
item.coverURL = "/documents/" + item.id + "/cover";
|
||||
item.type = "REMOTE";
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
})
|
||||
// Fail Nicely -> Allows Local Feature
|
||||
.catch((e) => {
|
||||
console.log("[loadContent] Service Worker Cache Error:", e);
|
||||
return [];
|
||||
});
|
||||
|
||||
// Get & Normalize Local Books
|
||||
let localResponse = await IDB.find(/^FILE-.{32}$/, false);
|
||||
let localCache = await Promise.all(localResponse.map(getLocalProgress));
|
||||
|
||||
// Populate DOM with Cache & Local Books
|
||||
populateDOMBooks([...swCache, ...localCache]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update DOM to indicate online status. If no argument is passed, we attempt
|
||||
* to determine online status via `navigator.onLine`.
|
||||
**/
|
||||
function handleOnlineChange(isOnline) {
|
||||
let onlineEl = document.querySelector("#online");
|
||||
isOnline = isOnline == undefined ? navigator.onLine : isOnline;
|
||||
onlineEl.hidden = !isOnline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow deleting local or remote cached files. Deleting remotely cached files
|
||||
* does not remove progress. Progress will still be flushed once online.
|
||||
**/
|
||||
async function handleFileDelete(event, item) {
|
||||
let mainEl =
|
||||
event.target.parentElement.parentElement.parentElement.parentElement
|
||||
.parentElement;
|
||||
|
||||
if (item.type == "LOCAL") {
|
||||
await IDB.del("FILE-" + item.id);
|
||||
await IDB.del("FILE-METADATA-" + item.id);
|
||||
} else if (item.type == "REMOTE") {
|
||||
let swResp = await SW.send({ type: DEL_SW_CACHE, id: item.id });
|
||||
if (swResp != "SUCCESS")
|
||||
throw new Error("[handleFileDelete] Service Worker Error");
|
||||
}
|
||||
|
||||
console.log("[handleFileDelete] Item Deleted");
|
||||
|
||||
mainEl.remove();
|
||||
updateMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow adding file to offline reader. Add to IndexedDB,
|
||||
* and later upload? Add style indicating external file?
|
||||
**/
|
||||
async function handleFileAdd() {
|
||||
const fileInput = document.getElementById("document_file");
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (!file) return console.log("[handleFileAdd] No File");
|
||||
|
||||
function readFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (event) => resolve(event.target.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
function randomID() {
|
||||
return "00000000000000000000000000000000".replace(/[018]/g, (c) =>
|
||||
(
|
||||
c ^
|
||||
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
|
||||
).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
let newID = randomID();
|
||||
|
||||
readFile(file)
|
||||
// Store Blob in IDB
|
||||
.then((fileData) => {
|
||||
if (!isEpubFile(fileData)) throw new Error("Invalid File Type");
|
||||
|
||||
return IDB.set(
|
||||
"FILE-" + newID,
|
||||
new Blob([fileData], { type: "application/octet-binary" })
|
||||
);
|
||||
})
|
||||
// Process File
|
||||
.then(() => getLocalProgress("FILE-" + newID))
|
||||
// Populate in DOM
|
||||
.then((item) => populateDOMBooks([item]))
|
||||
// Hide Add File Button
|
||||
.then(() => {
|
||||
let addButtonEl = document.querySelector("#add-file-button");
|
||||
addButtonEl.checked = false;
|
||||
})
|
||||
// Logging
|
||||
.then(() => console.log("[handleFileAdd] File Add Successfully"))
|
||||
.catch((e) => console.log("[handleFileAdd] File Add Failed:", e));
|
||||
}
|
||||
|
||||
// Add Event Listeners
|
||||
window.addEventListener("DOMContentLoaded", handleLoad);
|
||||
window.addEventListener("online", () => handleOnlineChange(true));
|
||||
window.addEventListener("offline", () => handleOnlineChange(false));
|
||||
|
||||
// ----------------------------------------------------------------------- //
|
||||
// ------------------------------- Helpers ------------------------------- //
|
||||
// ----------------------------------------------------------------------- //
|
||||
|
||||
/**
|
||||
* Update the message element. Called after initial load, on item add or on
|
||||
* item delete.
|
||||
**/
|
||||
function updateMessage() {
|
||||
// Update Loader / No Results Indicator
|
||||
let itemsEl = document.querySelector("#items");
|
||||
let messageEl = document.querySelector("#message");
|
||||
|
||||
if (itemsEl.children.length == 0) {
|
||||
messageEl.innerText = "No Results";
|
||||
messageEl.hidden = false;
|
||||
} else messageEl.hidden = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate DOM with cached documents.
|
||||
**/
|
||||
function populateDOMBooks(data) {
|
||||
let allDocuments = document.querySelector("#items");
|
||||
|
||||
// Create Document Items
|
||||
data.forEach((item) => {
|
||||
// Create Main Element
|
||||
let baseEl = document.querySelector("#item-template").cloneNode(true);
|
||||
baseEl.removeAttribute("id");
|
||||
|
||||
// Get Elements
|
||||
let [titleEl, authorEl, percentageEl] = baseEl.querySelectorAll("p + p");
|
||||
let [svgDivEl, textEl] = baseEl.querySelector("strong").children;
|
||||
let coverEl = baseEl.querySelector("a img");
|
||||
let downloadEl = baseEl.querySelector("svg").parentElement;
|
||||
let deleteInputEl = baseEl.querySelector("#delete-button");
|
||||
let deleteLabelEl = deleteInputEl.previousElementSibling;
|
||||
let deleteTextEl = baseEl.querySelector("input + div span");
|
||||
|
||||
// Set Download Attributes
|
||||
downloadEl.setAttribute("href", item.fileURL);
|
||||
downloadEl.setAttribute(
|
||||
"download",
|
||||
item.title + " - " + item.author + ".epub"
|
||||
);
|
||||
|
||||
// Set Cover Attributes
|
||||
coverEl.setAttribute("src", item.coverURL);
|
||||
coverEl.parentElement.setAttribute(
|
||||
"href",
|
||||
"/reader#id=" + item.id + "&type=" + item.type
|
||||
);
|
||||
|
||||
// Set Additional Metadata Attributes
|
||||
titleEl.textContent = item.title;
|
||||
authorEl.textContent = item.author;
|
||||
percentageEl.textContent = item.percentage + "%";
|
||||
|
||||
// Set Remote / Local Indicator
|
||||
let newSvgEl =
|
||||
item.type == "LOCAL"
|
||||
? document.querySelector("#local-svg-template").cloneNode(true)
|
||||
: document.querySelector("#remote-svg-template").cloneNode(true);
|
||||
svgDivEl.append(newSvgEl);
|
||||
textEl.textContent = item.type;
|
||||
|
||||
// Delete Item
|
||||
deleteInputEl.setAttribute("id", "delete-button-" + item.id);
|
||||
deleteLabelEl.setAttribute("for", "delete-button-" + item.id);
|
||||
deleteTextEl.addEventListener("click", (e) => handleFileDelete(e, item));
|
||||
deleteTextEl.textContent =
|
||||
item.type == "LOCAL" ? "Delete Local" : "Delete Cache";
|
||||
|
||||
allDocuments.append(baseEl);
|
||||
});
|
||||
|
||||
updateMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an item id, generate expected item format from IDB data store.
|
||||
**/
|
||||
async function getLocalProgress(id) {
|
||||
// Get Metadata (Cover Always Needed)
|
||||
let fileBlob = await IDB.get(id);
|
||||
let fileURL = URL.createObjectURL(fileBlob);
|
||||
let metadata = await getMetadata(fileURL);
|
||||
|
||||
// Attempt Cache
|
||||
let documentID = id.replace("FILE-", "");
|
||||
let documentData = await IDB.get("FILE-METADATA-" + documentID);
|
||||
if (documentData)
|
||||
return { ...documentData, fileURL, coverURL: metadata.coverURL };
|
||||
|
||||
// Create Starting Progress
|
||||
let newProgress = {
|
||||
id: documentID,
|
||||
title: metadata.title,
|
||||
author: metadata.author,
|
||||
type: "LOCAL",
|
||||
percentage: 0,
|
||||
progress: "",
|
||||
words: 0,
|
||||
};
|
||||
|
||||
// Update Cache
|
||||
await IDB.set("FILE-METADATA-" + documentID, newProgress);
|
||||
|
||||
// Return Cache + coverURL
|
||||
return { ...newProgress, fileURL, coverURL: metadata.coverURL };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the Title, Author, and CoverURL (blob) for a given file.
|
||||
**/
|
||||
async function getMetadata(fileURL) {
|
||||
let book = ePub(fileURL, { openAs: "epub" });
|
||||
console.log({ book });
|
||||
let coverURL = (await book.coverUrl()) || "/assets/images/no-cover.jpg";
|
||||
let metadata = await book.loaded.metadata;
|
||||
|
||||
let title =
|
||||
metadata.title && metadata.title != "" ? metadata.title : "Unknown";
|
||||
let author =
|
||||
metadata.creator && metadata.creator != "" ? metadata.creator : "Unknown";
|
||||
|
||||
book.destroy();
|
||||
|
||||
return { title, author, coverURL };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate filetype. We check the headers and validate that they are ZIP.
|
||||
* After which we validate contents. This isn't 100% effective, but unless
|
||||
* someone is trying to trick it, it should be fine.
|
||||
**/
|
||||
function isEpubFile(arrayBuffer) {
|
||||
const view = new DataView(arrayBuffer);
|
||||
|
||||
// Too Small
|
||||
if (view.byteLength < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for the ZIP file signature (PK)
|
||||
const littleEndianSignature = view.getUint16(0, true);
|
||||
const bigEndianSignature = view.getUint16(0, false);
|
||||
|
||||
if (littleEndianSignature !== 0x504b && bigEndianSignature !== 0x504b) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Additional Checks (No FP on ZIP)
|
||||
const textDecoder = new TextDecoder();
|
||||
const zipContent = textDecoder.decode(new Uint8Array(arrayBuffer));
|
||||
|
||||
if (
|
||||
zipContent.includes("mimetype") &&
|
||||
zipContent.includes("META-INF/container.xml")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user