[fix] handle sw unsupported, [fix] sw install / upgrade, [add] local file upload / delete
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:
parent
0917172d1c
commit
5880d3beb6
@ -12,7 +12,7 @@ const SW = (function () {
|
||||
let swInstance = null;
|
||||
let outstandingMessages = {};
|
||||
|
||||
navigator.serviceWorker.addEventListener("message", ({ data }) => {
|
||||
navigator.serviceWorker?.addEventListener("message", ({ data }) => {
|
||||
let { id } = data;
|
||||
data = data.data;
|
||||
|
||||
@ -25,6 +25,9 @@ const SW = (function () {
|
||||
});
|
||||
|
||||
async function install() {
|
||||
if (!navigator.serviceWorker)
|
||||
throw new Error("Service Worker Not Supported");
|
||||
|
||||
// Register Service Worker
|
||||
swInstance = await navigator.serviceWorker.register("/sw.js");
|
||||
swInstance.onupdatefound = (data) =>
|
||||
@ -38,9 +41,11 @@ const SW = (function () {
|
||||
await new Promise((resolve) => {
|
||||
serviceWorker.onstatechange = (data) => {
|
||||
console.log("[SW.install] State Change:", serviceWorker.state);
|
||||
if (serviceWorker.state == "activated") resolve();
|
||||
if (["installed", "activated"].includes(serviceWorker.state)) resolve();
|
||||
};
|
||||
if (serviceWorker.state == "activated") resolve();
|
||||
|
||||
console.log("[SW.install] Current State:", serviceWorker.state);
|
||||
if (["installed", "activated"].includes(serviceWorker.state)) resolve();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,8 @@
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href="/assets/style.css" />
|
||||
|
||||
<script src="/assets/lib/jszip.min.js"></script>
|
||||
<script src="/assets/lib/epub.min.js"></script>
|
||||
<script src="/assets/lib/idb-keyval.js"></script>
|
||||
<script src="/assets/lib/sw-helper.js"></script>
|
||||
<script src="/assets/index.js"></script>
|
||||
@ -96,7 +98,7 @@
|
||||
>
|
||||
<div
|
||||
id="online"
|
||||
class="rounded text-white bg-white dark:bg-gray-700 text-center p-3 mb-4"
|
||||
class="rounded text-black dark:text-white bg-white dark:bg-gray-700 text-center p-3 mb-4"
|
||||
>
|
||||
You're Online:
|
||||
<a
|
||||
@ -107,7 +109,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="loading"
|
||||
id="message"
|
||||
class="rounded text-white bg-white dark:bg-gray-700 text-center p-3 mb-4"
|
||||
>
|
||||
Loading...
|
||||
@ -175,5 +177,102 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Template HTML Elements -->
|
||||
<div class="hidden">
|
||||
<svg id="local-svg-template" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 22H10C6.22876 22 4.34315 22 3.17157 20.8284C2 19.6569 2 17.7712 2 14V10C2 6.22876 2 4.34315 3.17157 3.17157C4.34315 2 6.23869 2 10.0298 2C10.6358 2 11.1214 2 11.53 2.01666C11.5166 2.09659 11.5095 2.17813 11.5092 2.26057L11.5 5.09497C11.4999 6.19207 11.4998 7.16164 11.6049 7.94316C11.7188 8.79028 11.9803 9.63726 12.6716 10.3285C13.3628 11.0198 14.2098 11.2813 15.0569 11.3952C15.8385 11.5003 16.808 11.5002 17.9051 11.5001L18 11.5001H21.9574C22 12.0344 22 12.6901 22 13.5629V14C22 17.7712 22 19.6569 20.8284 20.8284C19.6569 22 17.7712 22 14 22Z" />
|
||||
<path d="M19.3517 7.61665L15.3929 4.05375C14.2651 3.03868 13.7012 2.53114 13.0092 2.26562L13 5.00011C13 7.35713 13 8.53564 13.7322 9.26787C14.4645 10.0001 15.643 10.0001 18 10.0001H21.5801C21.2175 9.29588 20.5684 8.71164 19.3517 7.61665Z" />
|
||||
</svg>
|
||||
|
||||
<svg id="remote-svg-template" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.3517 7.61665L15.3929 4.05375C14.2651 3.03868 13.7012 2.53114 13.0092 2.26562L13 5.00011C13 7.35713 13 8.53564 13.7322 9.26787C14.4645 10.0001 15.643 10.0001 18 10.0001H21.5801C21.2175 9.29588 20.5684 8.71164 19.3517 7.61665Z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 22H14C17.7712 22 19.6569 22 20.8284 20.8284C22 19.6569 22 17.7712 22 14V13.5629C22 12.6901 22 12.0344 21.9574 11.5001H18L17.9051 11.5001C16.808 11.5002 15.8385 11.5003 15.0569 11.3952C14.2098 11.2813 13.3628 11.0198 12.6716 10.3285C11.9803 9.63726 11.7188 8.79028 11.6049 7.94316C11.4998 7.16164 11.4999 6.19207 11.5 5.09497L11.5092 2.26057C11.5095 2.17813 11.5166 2.09659 11.53 2.01666C11.1214 2 10.6358 2 10.0298 2C6.23869 2 4.34315 2 3.17157 3.17157C2 4.34315 2 6.22876 2 10V14C2 17.7712 2 19.6569 3.17157 20.8284C4.34315 22 6.22876 22 10 22ZM11 18C12.1046 18 13 17.2099 13 16.2353C13 15.4629 12.4375 14.8063 11.6543 14.5672C11.543 13.6855 10.6956 13 9.66667 13C8.5621 13 7.66667 13.7901 7.66667 14.7647C7.66667 14.9803 7.71047 15.1868 7.79066 15.3778C7.69662 15.3615 7.59944 15.3529 7.5 15.3529C6.67157 15.3529 6 15.9455 6 16.6765C6 17.4074 6.67157 18 7.5 18H11Z"/>
|
||||
</svg>
|
||||
|
||||
<div id="item-template" class="w-full relative">
|
||||
<div class="flex gap-4 w-full h-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<div class="min-w-fit my-auto h-48 relative">
|
||||
<a href="#">
|
||||
<img class="rounded object-cover h-full" src="/assets/images/no-cover.jpg"></img>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Title</p>
|
||||
<p class="font-medium">
|
||||
N/A
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Author</p>
|
||||
<p class="font-medium">
|
||||
N/A
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Progress</p>
|
||||
<p class="font-medium">
|
||||
0%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute flex flex-col gap-2 right-4 bottom-4 text-gray-500 dark:text-gray-400">
|
||||
<div class="relative">
|
||||
<label for="delete-button">
|
||||
<svg
|
||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3 6.52381C3 6.12932 3.32671 5.80952 3.72973 5.80952H8.51787C8.52437 4.9683 8.61554 3.81504 9.45037 3.01668C10.1074 2.38839 11.0081 2 12 2C12.9919 2 13.8926 2.38839 14.5496 3.01668C15.3844 3.81504 15.4756 4.9683 15.4821 5.80952H20.2703C20.6733 5.80952 21 6.12932 21 6.52381C21 6.9183 20.6733 7.2381 20.2703 7.2381H3.72973C3.32671 7.2381 3 6.9183 3 6.52381Z"
|
||||
/>
|
||||
<path
|
||||
d="M11.6066 22H12.3935C15.101 22 16.4547 22 17.3349 21.1368C18.2151 20.2736 18.3052 18.8576 18.4853 16.0257L18.7448 11.9452C18.8425 10.4086 18.8913 9.64037 18.4498 9.15352C18.0082 8.66667 17.2625 8.66667 15.7712 8.66667H8.22884C6.7375 8.66667 5.99183 8.66667 5.55026 9.15352C5.1087 9.64037 5.15756 10.4086 5.25528 11.9452L5.51479 16.0257C5.69489 18.8576 5.78494 20.2736 6.66513 21.1368C7.54532 22 8.89906 22 11.6066 22Z"
|
||||
/>
|
||||
</svg>
|
||||
</label>
|
||||
<input type="checkbox" id="delete-button" class="hidden css-button"/>
|
||||
<div class="absolute z-30 bottom-7 right-0 p-3 transition-all duration-200 bg-gray-200 rounded shadow-lg shadow-gray-500 dark:shadow-gray-900 dark:bg-gray-600">
|
||||
<span
|
||||
class="block cursor-pointer font-medium text-sm text-center w-32 px-2 py-1 text-white bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100"
|
||||
>Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="#">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V12.1893L14.4697 10.4697C14.7626 10.1768 15.2374 10.1768 15.5303 10.4697C15.8232 10.7626 15.8232 11.2374 15.5303 11.5303L12.5303 14.5303C12.3897 14.671 12.1989 14.75 12 14.75C11.8011 14.75 11.6103 14.671 11.4697 14.5303L8.46967 11.5303C8.17678 11.2374 8.17678 10.7626 8.46967 10.4697C8.76256 10.1768 9.23744 10.1768 9.53033 10.4697L11.25 12.1893V7C11.25 6.58579 11.5858 6.25 12 6.25ZM8 16.25C7.58579 16.25 7.25 16.5858 7.25 17C7.25 17.4142 7.58579 17.75 8 17.75H16C16.4142 17.75 16.75 17.4142 16.75 17C16.75 16.5858 16.4142 16.25 16 16.25H8Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute top-0 right-0">
|
||||
<strong class="bg-blue-100 text-blue-700 inline-flex items-center gap-1 rounded-tr rounded-bl p-1">
|
||||
<div class="w-4 h-4"></div>
|
||||
<span class="text-xs font-medium">REMOTE</span>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,142 +1,319 @@
|
||||
/**
|
||||
* TODO:
|
||||
* - Offling / Online Checker
|
||||
* - Flush oustanding read activity & progress
|
||||
* - No files cached
|
||||
* - Upload Files
|
||||
**/
|
||||
|
||||
const BASE_ITEM = `
|
||||
<div class="w-full relative">
|
||||
<div class="flex gap-4 w-full h-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||||
<div class="min-w-fit my-auto h-48 relative">
|
||||
<a href="#">
|
||||
<img class="rounded object-cover h-full" src="/assets/images/no-cover.jpg"></img>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Title</p>
|
||||
<p class="font-medium">
|
||||
N/A
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Author</p>
|
||||
<p class="font-medium">
|
||||
N/A
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex shrink-0 items-center">
|
||||
<div>
|
||||
<p class="text-gray-400">Progress</p>
|
||||
<p class="font-medium">
|
||||
0%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute flex flex-col gap-2 right-4 bottom-4 text-gray-500 dark:text-gray-400">
|
||||
<a href="#">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
class="cursor-pointer hover:text-gray-800 dark:hover:text-gray-100"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V12.1893L14.4697 10.4697C14.7626 10.1768 15.2374 10.1768 15.5303 10.4697C15.8232 10.7626 15.8232 11.2374 15.5303 11.5303L12.5303 14.5303C12.3897 14.671 12.1989 14.75 12 14.75C11.8011 14.75 11.6103 14.671 11.4697 14.5303L8.46967 11.5303C8.17678 11.2374 8.17678 10.7626 8.46967 10.4697C8.76256 10.1768 9.23744 10.1768 9.53033 10.4697L11.25 12.1893V7C11.25 6.58579 11.5858 6.25 12 6.25ZM8 16.25C7.58579 16.25 7.25 16.5858 7.25 17C7.25 17.4142 7.58579 17.75 8 17.75H16C16.4142 17.75 16.75 17.4142 16.75 17C16.75 16.5858 16.4142 16.25 16 16.25H8Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const GET_SW_CACHE = "GET_SW_CACHE";
|
||||
const DEL_SW_CACHE = "DEL_SW_CACHE";
|
||||
|
||||
async function initOffline() {
|
||||
updateOnlineIndicator();
|
||||
// ----------------------------------------------------------------------- //
|
||||
// --------------------------- Event Listeners --------------------------- //
|
||||
// ----------------------------------------------------------------------- //
|
||||
|
||||
/**
|
||||
* Initial load handler. Gets called on DOMContentLoaded.
|
||||
**/
|
||||
async function handleLoad() {
|
||||
handleOnlineChange();
|
||||
|
||||
// If SW Redirected
|
||||
if (document.location.pathname !== "/offline")
|
||||
window.history.replaceState(null, null, "/offline");
|
||||
|
||||
// Ensure Installed
|
||||
await SW.install();
|
||||
// Create Upload Listener
|
||||
let uploadButton = document.querySelector("button");
|
||||
uploadButton.addEventListener("click", handleFileAdd);
|
||||
|
||||
// Get Service Worker Cache & Local Cache - Override Local
|
||||
let swCache = await SW.send({ type: GET_SW_CACHE });
|
||||
let allCache = await Promise.all(
|
||||
swCache.map(async (item) => {
|
||||
// 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 [];
|
||||
});
|
||||
|
||||
populateDOM(allCache);
|
||||
// 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]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate DOM with cached documents.
|
||||
* Update DOM to indicate online status. If no argument is passed, we attempt
|
||||
* to determine online status via `navigator.onLine`.
|
||||
**/
|
||||
function populateDOM(data) {
|
||||
let allDocuments = document.querySelector("#items");
|
||||
function handleOnlineChange(isOnline) {
|
||||
let onlineEl = document.querySelector("#online");
|
||||
isOnline = isOnline == undefined ? navigator.onLine : isOnline;
|
||||
onlineEl.hidden = !isOnline;
|
||||
}
|
||||
|
||||
// Update Loader / No Results Indicator
|
||||
let loadingEl = document.querySelector("#loading");
|
||||
if (data.length == 0) loadingEl.innerText = "No Results";
|
||||
else loadingEl.remove();
|
||||
/**
|
||||
* 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;
|
||||
|
||||
data.forEach((item) => {
|
||||
// Create Main Element
|
||||
let baseEl = document.createElement("div");
|
||||
baseEl.innerHTML = BASE_ITEM;
|
||||
baseEl = baseEl.firstElementChild;
|
||||
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");
|
||||
}
|
||||
|
||||
// Get Elements
|
||||
let coverEl = baseEl.querySelector("a img");
|
||||
let [titleEl, authorEl, percentageEl] = baseEl.querySelectorAll("p + p");
|
||||
let downloadEl = baseEl.querySelector("svg").parentElement;
|
||||
console.log("[handleFileDelete] Item Deleted");
|
||||
|
||||
// Set Variables
|
||||
downloadEl.setAttribute("href", "/documents/" + item.id + "/file");
|
||||
coverEl.setAttribute("src", "/documents/" + item.id + "/cover");
|
||||
coverEl.parentElement.setAttribute("href", "/reader#id=" + item.id);
|
||||
titleEl.textContent = item.title;
|
||||
authorEl.textContent = item.author;
|
||||
percentageEl.textContent = item.percentage + "%";
|
||||
|
||||
allDocuments.append(baseEl);
|
||||
});
|
||||
mainEl.remove();
|
||||
updateMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow adding file to offline reader. Add to IndexedDB,
|
||||
* and later upload? Add style indicating external file?
|
||||
**/
|
||||
function handleFileAdd() {}
|
||||
async function handleFileAdd() {
|
||||
const fileInput = document.getElementById("document_file");
|
||||
const file = fileInput.files[0];
|
||||
|
||||
function updateOnlineIndicator(isOnline) {
|
||||
let onlineEl = document.querySelector("#online");
|
||||
isOnline = isOnline == undefined ? navigator.onLine : isOnline;
|
||||
onlineEl.hidden = !isOnline;
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize
|
||||
window.addEventListener("DOMContentLoaded", initOffline);
|
||||
window.addEventListener("online", () => updateOnlineIndicator(true));
|
||||
window.addEventListener("offline", () => updateOnlineIndicator(false));
|
||||
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;
|
||||
}
|
||||
|
@ -1,20 +1,26 @@
|
||||
const THEMES = ["light", "tan", "blue", "gray", "black"];
|
||||
const THEME_FILE = "/assets/reader/readerThemes.css";
|
||||
|
||||
/**
|
||||
* Initial load handler. Gets called on DOMContentLoaded. Responsible for
|
||||
* normalizing the documentData depending on type (REMOTE or LOCAL), and
|
||||
* populating the metadata of the book into the DOM.
|
||||
**/
|
||||
async function initReader() {
|
||||
let documentData;
|
||||
let filePath;
|
||||
|
||||
// Get Document ID & Type
|
||||
const urlParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
const documentID = urlParams.get("id");
|
||||
const localID = urlParams.get("local");
|
||||
const documentType = urlParams.get("type");
|
||||
|
||||
if (documentID) {
|
||||
if (documentType == "REMOTE") {
|
||||
// Get Server / Cached Document
|
||||
let progressResp = await fetch("/documents/" + documentID + "/progress");
|
||||
documentData = await progressResp.json();
|
||||
|
||||
// Update Local Cache
|
||||
// Update With Local Cache
|
||||
let localCache = await IDB.get("PROGRESS-" + documentID);
|
||||
if (localCache) {
|
||||
documentData.progress = localCache.progress;
|
||||
@ -22,25 +28,31 @@ async function initReader() {
|
||||
}
|
||||
|
||||
filePath = "/documents/" + documentID + "/file";
|
||||
} else if (localID) {
|
||||
// Get Local Document
|
||||
// TODO:
|
||||
// - IDB FileID
|
||||
// - IDB Metadata
|
||||
} else if (documentType == "LOCAL") {
|
||||
documentData = await IDB.get("FILE-METADATA-" + documentID);
|
||||
let fileBlob = await IDB.get("FILE-" + documentID);
|
||||
filePath = URL.createObjectURL(fileBlob);
|
||||
} else {
|
||||
throw new Error("Invalid");
|
||||
throw new Error("Invalid Type");
|
||||
}
|
||||
|
||||
populateMetadata(documentData);
|
||||
// Update Type
|
||||
documentData.type = documentType;
|
||||
|
||||
// Populate Metadata & Create Reader
|
||||
window.currentReader = new EBookReader(filePath, documentData);
|
||||
populateMetadata(documentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates metadata into the DOM. Specifically for the top "drop" down.
|
||||
**/
|
||||
function populateMetadata(data) {
|
||||
let documentLocation = data.id.startsWith("local-")
|
||||
? "/offline"
|
||||
: "/documents/" + data.id;
|
||||
let documentLocation =
|
||||
data.type == "LOCAL" ? "/offline" : "/documents/" + data.id;
|
||||
|
||||
let documentCoverLocation = data.id.startsWith("local-")
|
||||
let documentCoverLocation =
|
||||
data.type == "LOCAL"
|
||||
? "/assets/images/no-cover.jpg"
|
||||
: "/documents/" + data.id + "/cover";
|
||||
|
||||
@ -54,6 +66,11 @@ function populateMetadata(data) {
|
||||
authorEl.innerText = data.author;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the main reader class. All functionality is wrapped in this class.
|
||||
* Responsible for handling gesture / clicks, flushing progress & activity,
|
||||
* storing and processing themes, etc.
|
||||
**/
|
||||
class EBookReader {
|
||||
bookState = {
|
||||
currentWord: 0,
|
||||
@ -734,7 +751,10 @@ class EBookReader {
|
||||
],
|
||||
};
|
||||
|
||||
// Flush -> Offline Cache IDB
|
||||
// Local Files
|
||||
if (this.bookState.type == "LOCAL") return;
|
||||
|
||||
// Remote Flush -> Offline Cache IDB
|
||||
this.flushActivity(activityEvent).catch(async (e) => {
|
||||
console.error("[createActivity] Activity Flush Failed:", {
|
||||
error: e,
|
||||
@ -790,7 +810,18 @@ class EBookReader {
|
||||
progress: this.bookState.progress,
|
||||
};
|
||||
|
||||
// Flush -> Offline Cache IDB
|
||||
// Update Local Metadata
|
||||
if (this.bookState.type == "LOCAL") {
|
||||
let currentMetadata = await IDB.get("FILE-METADATA-" + this.bookState.id);
|
||||
return IDB.set("FILE-METADATA-" + this.bookState.id, {
|
||||
...currentMetadata,
|
||||
progress: progressEvent.progress,
|
||||
percentage: Math.round(progressEvent.percentage * 10000) / 100,
|
||||
words: this.bookState.words,
|
||||
});
|
||||
}
|
||||
|
||||
// Remote Flush -> Offline Cache IDB
|
||||
this.flushProgress(progressEvent).catch(async (e) => {
|
||||
console.error("[createProgress] Progress Flush Failed:", {
|
||||
error: e,
|
||||
|
@ -829,6 +829,10 @@ video {
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
.h-4 {
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.h-48 {
|
||||
height: 12rem;
|
||||
}
|
||||
@ -885,6 +889,10 @@ video {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.w-40 {
|
||||
width: 10rem;
|
||||
}
|
||||
@ -1157,6 +1165,14 @@ video {
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.rounded-bl {
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.rounded-tr {
|
||||
border-top-right-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
@ -1236,6 +1252,11 @@ video {
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-700 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
@ -1553,6 +1574,11 @@ video {
|
||||
color: rgb(0 0 0 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(29 78 216 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-gray-200 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity));
|
||||
|
12
assets/sw.js
12
assets/sw.js
@ -200,7 +200,17 @@ function handleMessage(event) {
|
||||
event.source.postMessage({ id, data: cachedDocuments });
|
||||
});
|
||||
} else if (data.type === DEL_SW_CACHE) {
|
||||
// TODO
|
||||
let basePath = "/documents/" + data.id;
|
||||
caches
|
||||
.open(SW_CACHE_NAME)
|
||||
.then((cache) =>
|
||||
Promise.all([
|
||||
cache.delete(basePath + "/file"),
|
||||
cache.delete(basePath + "/progress"),
|
||||
])
|
||||
)
|
||||
.then(() => event.source.postMessage({ id, data: "SUCCESS" }))
|
||||
.catch(() => event.source.postMessage({ id, data: "FAILURE" }));
|
||||
} else {
|
||||
event.source.postMessage({ id, data: { pong: 1 } });
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
{{ if .Data.Filepath }}
|
||||
<a
|
||||
href="/reader#id={{ .Data.ID }}"
|
||||
href="/reader#id={{ .Data.ID }}&type=REMOTE"
|
||||
class="z-10 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded text-sm text-center py-1 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>Read</a>
|
||||
{{ end }}
|
||||
|
Loading…
Reference in New Issue
Block a user