diff --git a/cry/feed.py b/cry/feed.py index 67b0b76..c965b64 100644 --- a/cry/feed.py +++ b/cry/feed.py @@ -589,6 +589,13 @@ def sort_key(f: Feed) -> int: return -1 +def sort_key_inserted(f: Feed) -> int: + """A sort key for sorting feeds by recency.""" + if len(f.entries) > 0: + return max(e.inserted_at for e in f.entries) + return -1 + + class FeedSearchParser(html.parser.HTMLParser): """An HTML parser that tries to find links to feeds.""" diff --git a/cry/static/event.js b/cry/static/event.js new file mode 100644 index 0000000..74d01e8 --- /dev/null +++ b/cry/static/event.js @@ -0,0 +1,65 @@ +function append_log(txt) { + log = document.getElementById("log"); + log.append(txt + "\n"); + log.scrollTop = log.scrollHeight; +} + +var events = new EventSource(window.location.pathname + "/events"); +events.addEventListener("status", (e) => { + console.log(e); + append_log(e.data); + document.getElementById("status").innerText = e.data; +}); + +events.addEventListener("log", (e) => { + console.log(e); + append_log(e.data); +}); + +events.addEventListener("redirect", (e) => { + console.log(e); + window.location = e.data; +}); + +events.addEventListener("progress", (e) => { + // Grab the progress element being referred to. + const parameters = JSON.parse(e.data); + const progress = document.getElementById(parameters.progressElement); + if (progress) { + progress.value = parameters.progressValue; + } + + // Gather all the progress items into an array. + const progressBarElements = [...document.querySelectorAll('.progress-entry')]; + + // Sort the array by the progress value, but put completed items at the end. + // If both items are completed sort by data-sort-key. + progressBarElements.sort((a, b) => { + const valueA = parseInt(a.querySelector('.progress').getAttribute('value'), 10); + const maxA = parseInt(a.querySelector('.progress').getAttribute('max'), 10); + const keyA = a.getAttribute('data-sort-key') + + const valueB = parseInt(b.querySelector('.progress').getAttribute('value'), 10); + const maxB = parseInt(b.querySelector('.progress').getAttribute('max'), 10); + const keyB = b.getAttribute('data-sort-key') + + if (valueA == maxA) { + if (valueB == maxB) { + return keyA.localeCompare(keyB); + } else { + return 1; // B is not at max, it goes first. + } + } else if (valueB == maxB) { + return -1; // A is not at max, it goes first. + } else { + return valueB - valueA; // Larger values first. + } + + return valueA - valueB; // Sort in ascending order (lowest to highest value) + }); + + const parentContainer = progressBarElements[0].parentNode; + progressBarElements.forEach(element => { + parentContainer.appendChild(element); + }); +}) diff --git a/cry/static/style.css b/cry/static/style.css index 13fcb4b..8fe1df4 100644 --- a/cry/static/style.css +++ b/cry/static/style.css @@ -7,20 +7,12 @@ body { height: 100vh; } -header { - /* padding: 10px; */ -} - .content { flex: 1; /* This makes the content section fill the available space */ overflow-y: auto; /* Allows vertical scrolling */ padding: 10px; } -footer { - padding: 10px; -} - h1 { margin-top: 2rem; margin-bottom: 0.25rem; @@ -134,3 +126,27 @@ li.entry:before { color: inherit; text-decoration: inherit; } + +/* + * STATUS + */ +.status-body { + display: flex; + flex-direction: column; +} + +.status-header { + +} + +.status-log { + flex-grow: 1; + overflow-y: scroll; + padding: 10px; + max-height: 100%; + margin: auto 0 auto 0; +} + +.status-footer { + +} diff --git a/cry/views.py b/cry/views.py index d115939..0ae16b6 100644 --- a/cry/views.py +++ b/cry/views.py @@ -164,3 +164,63 @@ def subscribe_choose_view(candidates: typing.Iterable[tuple[str, str]]) -> tags. ) assert isinstance(document, tags.html) return document + + +def status_view() -> tags.html: + document = tags.html( + _standard_header("Status"), + tags.body( + {"class": "status-body"}, + tags.header( + {"class": "status-header"}, + tags.h1("Status"), + tags.h2("Status: ", tags.span({"id": "status"}, "Starting...")), + ), + tags.pre({"class": "status-log", "id": "log"}), + tags.footer( + {"class": "status-footer"}, + tags.a({"href": "/"}, "Back to feeds"), + ), + tags.script({"src": "/event.js"}), + ), + ) + assert isinstance(document, tags.html) + return document + + +def refresh_view(feeds: list[feed.Feed]) -> tags.html: + document = tags.html( + _standard_header("Refreshing Feeds..."), + tags.body( + {"class": "refresh-body"}, + tags.header( + {"class": "refresh-header"}, + tags.h1("Status"), + tags.h2("Status: ", tags.span({"id": "status"}, "Starting...")), + ), + tags.div( + {"class": "refresh-content"}, + ( + tags.div( + {"class": "progress-entry", "data-sort-key": f.title}, + tags.label( + {"class": "progress-label"}, + f.title, + tags.progress( + {"max": 100, "value": 0, "class": "progress"} + ), + ), + ) + for f in feeds + ), + ), + tags.pre({"class": "refresh-log", "id": "log"}), + tags.footer( + {"class": "status-footer"}, + tags.a({"href": "/"}, "Back to feeds"), + ), + tags.script({"src": "/event.js"}), + ), + ) + assert isinstance(document, tags.html) + return document diff --git a/cry/web.py b/cry/web.py index b1c4b7c..038bec8 100644 --- a/cry/web.py +++ b/cry/web.py @@ -339,6 +339,8 @@ class Handler(http.server.BaseHTTPRequestHandler): return self.serve_feeds() elif self.path == "/style.css": return self.serve_style() + elif self.path == "/event.js": + return self.serve_event_js() elif self.path == "/refresh-status": return self.serve_status() elif self.path == "/subscribe-status": @@ -439,55 +441,9 @@ class Handler(http.server.BaseHTTPRequestHandler): self.wfile.flush() def serve_status(self): - # TODO: FIX STYLES TO BE INLINE FOR SPEED I GUESS - buffer = io.StringIO() - buffer.write( - """ - - - - - Status - - - -
-

Status

-

Status: Starting...

-
+ document = views.status_view() -

-
-            
-            
-            
-            
-            """
-        )
-
-        self.write_html(buffer.getvalue())
+        self.write_html("\n" + document.render())
 
     def serve_feeds(self):
         db = database.Database.local()
@@ -522,6 +478,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
         self.end_headers()
         self.wfile.write(response)
 
+    def serve_event_js(self):
+        self.write_file(
+            pathlib.Path(__file__).parent / "static" / "event.js",
+            content_type="text/javascript",
+        )
+
     def serve_style(self):
         self.write_file(
             pathlib.Path(__file__).parent / "static" / "style.css",