From 22a93a28e2f5bfb6d56056a76f53b518386fad56 Mon Sep 17 00:00:00 2001
From: Ross Williams <ross@ross-williams.net>
Date: Fri, 21 Feb 2025 04:15:37 +0000
Subject: [PATCH] Refactor epub reader progress calculation

The code in epub-progress.js was causing the epub file to be loaded
twice over the network, because it created a second instance of the
epub.js library. I moved all the progress percentage display code into
epub.js. Only the locationchange polyfill code was left in the separate
file, renamed to locationchange-polyfill.js.

I rewrote the percentage progress code to use epub.js events,
giving more succinct and readable code. Also, I added localStorage
caching of the epub location calculations, required for displaying
percentage progress, which can take up to 60-90 seconds to calculate on
longer ebooks. This localStorage caching approach is recommended by
epub.js in the `locations.html` example in their repo.

Signed-off-by: Ross Williams <ross@ross-williams.net>
---
 cps/static/css/main.css                       |  1 +
 cps/static/js/reading/epub-progress.js        | 54 -------------------
 cps/static/js/reading/epub.js                 | 26 +++++++++
 .../js/reading/locationchange-polyfill.js     | 21 ++++++++
 cps/templates/read.html                       |  2 +-
 5 files changed, 49 insertions(+), 55 deletions(-)
 delete mode 100644 cps/static/js/reading/epub-progress.js
 create mode 100644 cps/static/js/reading/locationchange-polyfill.js

diff --git a/cps/static/css/main.css b/cps/static/css/main.css
index 3afaf7a5..75e33943 100644
--- a/cps/static/css/main.css
+++ b/cps/static/css/main.css
@@ -831,4 +831,5 @@ input:-moz-placeholder { color: #454545; }
     bottom: 4rem;
     width: fit-content;
     position: absolute;
+    visibility: hidden;
 }
diff --git a/cps/static/js/reading/epub-progress.js b/cps/static/js/reading/epub-progress.js
deleted file mode 100644
index b8fc11d0..00000000
--- a/cps/static/js/reading/epub-progress.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * waits until queue is finished, meaning the book is done loading
- * @param callback
- */
-function qFinished(callback){
-    let timeout=setInterval(()=>{
-        if(reader.rendition.q.running===undefined)
-            clearInterval(timeout);
-            callback();
-        },300
-    )
-}
-
-function calculateProgress(){
-    let data=reader.rendition.location.end;
-    return Math.round(epub.locations.percentageFromCfi(data.cfi)*100);
-}
-
-// register new event emitter locationchange that fires on urlchange
-// source: https://stackoverflow.com/a/52809105/21941129
-(() => {
-    let oldPushState = history.pushState;
-    history.pushState = function pushState() {
-        let ret = oldPushState.apply(this, arguments);
-        window.dispatchEvent(new Event('locationchange'));
-        return ret;
-    };
-
-    let oldReplaceState = history.replaceState;
-    history.replaceState = function replaceState() {
-        let ret = oldReplaceState.apply(this, arguments);
-        window.dispatchEvent(new Event('locationchange'));
-        return ret;
-    };
-
-    window.addEventListener('popstate', () => {
-        window.dispatchEvent(new Event('locationchange'));
-    });
-})();
-
-window.addEventListener('locationchange',()=>{
-    let newPos=calculateProgress();
-    progressDiv.textContent=newPos+"%";
-});
-
-var epub=ePub(calibre.bookUrl)
-
-let progressDiv=document.getElementById("progress");
-
-qFinished(()=>{
-    epub.locations.generate().then(()=> {
-    window.dispatchEvent(new Event('locationchange'))
-});
-})
diff --git a/cps/static/js/reading/epub.js b/cps/static/js/reading/epub.js
index a552da05..8d299ce8 100644
--- a/cps/static/js/reading/epub.js
+++ b/cps/static/js/reading/epub.js
@@ -52,6 +52,32 @@ var reader;
         }
     });
 
+    // Update progress percentage
+    let progressDiv = document.getElementById("progress");
+    reader.book.ready.then((()=>{
+        let locations_key = reader.book.key()+'-locations';
+        let stored_locations = localStorage.getItem(locations_key);
+        let make_locations, save_locations;
+        if (stored_locations) {
+            make_locations = Promise.resolve(reader.book.locations.load(stored_locations));
+            // No-op because locations are already saved
+            save_locations = ()=>{};
+        } else {
+            make_locations = reader.book.locations.generate();
+            save_locations = ()=>{
+                localStorage.setItem(locations_key, reader.book.locations.save());
+            };
+        }
+        make_locations.then(()=>{
+            reader.rendition.on('relocated', (location)=>{
+                let percentage = Math.round(location.end.percentage*100);
+                progressDiv.textContent=percentage+"%";
+            });
+            reader.rendition.reportLocation();
+            progressDiv.style.visibility = "visible";
+        }).then(save_locations);
+    }));
+
     /**
      * @param {string} action - Add or remove bookmark
      * @param {string|int} location - Location or zero
diff --git a/cps/static/js/reading/locationchange-polyfill.js b/cps/static/js/reading/locationchange-polyfill.js
new file mode 100644
index 00000000..4845ea84
--- /dev/null
+++ b/cps/static/js/reading/locationchange-polyfill.js
@@ -0,0 +1,21 @@
+// register new event emitter locationchange that fires on urlchange
+// source: https://stackoverflow.com/a/52809105/21941129
+(() => {
+    let oldPushState = history.pushState;
+    history.pushState = function pushState() {
+        let ret = oldPushState.apply(this, arguments);
+        window.dispatchEvent(new Event('locationchange'));
+        return ret;
+    };
+
+    let oldReplaceState = history.replaceState;
+    history.replaceState = function replaceState() {
+        let ret = oldReplaceState.apply(this, arguments);
+        window.dispatchEvent(new Event('locationchange'));
+        return ret;
+    };
+
+    window.addEventListener('popstate', () => {
+        window.dispatchEvent(new Event('locationchange'));
+    });
+})();
diff --git a/cps/templates/read.html b/cps/templates/read.html
index bc5bd6a8..496267af 100644
--- a/cps/templates/read.html
+++ b/cps/templates/read.html
@@ -215,6 +215,6 @@
       <script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
       <script src="{{ url_for('static', filename='js/libs/reader.min.js') }}"></script>
       <script src="{{ url_for('static', filename='js/reading/epub.js') }}"></script>
-      <script src="{{ url_for('static', filename='js/reading/epub-progress.js') }}"></script>      
+      <script src="{{ url_for('static', filename='js/reading/locationchange-polyfill.js') }}"></script>      
     </body>
 </html>