From b75bad995a58cd52717bd15bab2df6a2688e20fc Mon Sep 17 00:00:00 2001
From: xenofem <xenofem@xeno.science>
Date: Thu, 25 Jan 2024 04:10:17 -0500
Subject: [PATCH 1/3] rework front page to have shuffle and lazy infinite
 scroll

---
 dlibrary/dlibrary.py                   |   3 +-
 dlibrary/static/dlibrary.css           |  51 ++++++++++-
 dlibrary/static/icons/shuffle.svg      |  12 +++
 dlibrary/static/icons/sort.svg         |   6 ++
 dlibrary/static/index.js               | 117 +++++++++++++++++++++++++
 dlibrary/templates/categorization.html |   2 +-
 dlibrary/templates/index.html          |  25 ++++++
 dlibrary/templates/list.html           |   4 +-
 8 files changed, 214 insertions(+), 6 deletions(-)
 create mode 100644 dlibrary/static/icons/shuffle.svg
 create mode 100644 dlibrary/static/icons/sort.svg
 create mode 100644 dlibrary/static/index.js
 create mode 100644 dlibrary/templates/index.html

diff --git a/dlibrary/dlibrary.py b/dlibrary/dlibrary.py
index 5598c1e..3d49ca0 100755
--- a/dlibrary/dlibrary.py
+++ b/dlibrary/dlibrary.py
@@ -315,6 +315,7 @@ def generate(args):
     list_template = jenv.get_template("list.html")
     categorization_template = jenv.get_template("categorization.html")
     work_template = jenv.get_template("work.html")
+    index_template = jenv.get_template("index.html")
 
     con = sqlite3.connect(args.destdir / 'meta.db')
     cur = con.cursor()
@@ -408,7 +409,7 @@ def generate(args):
         copy_recursive(r / 'static', args.destdir / 'site' / 'static')
 
     with open(args.destdir / 'site' / 'index.html', 'w') as f:
-        f.write(list_template.render(depth=0, works=works))
+        f.write(index_template.render(depth=0, works=works))
 
     con.close()
 
diff --git a/dlibrary/static/dlibrary.css b/dlibrary/static/dlibrary.css
index f82feb5..aaa1b8e 100644
--- a/dlibrary/static/dlibrary.css
+++ b/dlibrary/static/dlibrary.css
@@ -5,9 +5,56 @@ body {
     font-size: 18px;
 }
 
+/* index stuff */
+
+#top {
+    display: flex;
+    justify-content: center;
+    align-items: end;
+    gap: 40px;
+    margin-bottom: 25px;
+}
+
+#top .nav {
+    margin-bottom: 0px;
+}
+
+#top-padding, #controls {
+    flex-grow: 1;
+    flex-basis: 0;
+}
+
+@media all and (max-width: 600px) {
+    #top {
+        flex-direction: column;
+        align-items: center;
+        gap: 0;
+    }
+    #top .nav {
+        margin-bottom: 20px;
+    }
+}
+
+#controls button {
+    position: relative;
+    width: 50px;
+    height: 50px;
+    margin: 5px 2px;
+}
+
+#controls button img {
+    height: 40px;
+    width: 40px;
+
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    margin: -20px 0 0 -20px;
+}
+
 /* listing stuff */
 
-#title, nav {
+#title, .nav {
     text-align: center;
 }
 
@@ -20,7 +67,7 @@ body {
     margin-bottom: 25px;
 }
 
-.card-listing {
+#card-listing {
     display: flex;
     flex-wrap: wrap;
     justify-content: center;
diff --git a/dlibrary/static/icons/shuffle.svg b/dlibrary/static/icons/shuffle.svg
new file mode 100644
index 0000000..074e7f9
--- /dev/null
+++ b/dlibrary/static/icons/shuffle.svg
@@ -0,0 +1,12 @@
+<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='180' height='180' viewBox='-40 -40 180 180'>
+  <defs>
+    <clipPath id="crossing">
+      <path d='M -30 40 L -10 40 C 65 40 25 140 100 140 L 120 140 L -30 140 Z'/>
+      <path d='M -20 -40 L -10 -40 C 65 -40 25 60 100 60 L 130 60 L 130 -40 Z'/>
+    </clipPath>
+  </defs>
+  <path d='M -20 0 L -10 0 C 65 0 25 100 100 100 L 120 100' fill='none' stroke='#ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='10'/>
+  <path d='M -20 100 L -10 100 C 65 100 25 0 100 0 L 120 0' fill='none' clip-path='url(#crossing)' stroke='#ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='10'/>
+  <path d='M 105 85 L 120 100 L 105 115' fill='none' stroke='#ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='10'/>
+  <path d='M 105 -15 L 120 0 L 105 15' fill='none' stroke='#ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='10'/>
+</svg>
diff --git a/dlibrary/static/icons/sort.svg b/dlibrary/static/icons/sort.svg
new file mode 100644
index 0000000..a66a68d
--- /dev/null
+++ b/dlibrary/static/icons/sort.svg
@@ -0,0 +1,6 @@
+<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='180' height='180' viewBox='-20 -20 180 180'>
+  <path d='M 30 0 L 30 140' fill='none' stroke='#ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='10'/>
+  <path d='M 0 110 L 30 140 L 60 110' fill='none' stroke='#ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='10'/>
+  <path d='M 110 0 L 110 140' fill='none' stroke='#ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='10'/>
+  <path d='M 80 30 L 110 0 L 140 30' fill='none' stroke='#ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='10'/>
+</svg>
diff --git a/dlibrary/static/index.js b/dlibrary/static/index.js
new file mode 100644
index 0000000..409e537
--- /dev/null
+++ b/dlibrary/static/index.js
@@ -0,0 +1,117 @@
+const LCG_M = Math.pow(2, 32);
+const LCG_A = 0xd9f5;
+const LCG_C = 69;
+
+function lcg(seed) {
+    let value = seed % LCG_M;
+
+    return (n) => {
+        value = (LCG_A * value + LCG_C) % LCG_M;
+        return Math.floor(n * value / LCG_M);
+    };
+}
+
+function seedableShuffledCopy(list, seed) {
+    const gen = lcg(seed);
+    const l = [...list];
+
+    for (let i = 0; i < l.length - 1; ++i) {
+        j = i + gen(l.length - i);
+        const tmp = l[i];
+        l[i] = l[j];
+        l[j] = tmp;
+    }
+    return l;
+}
+
+function newSeed() {
+    return Math.floor(Math.random() * LCG_M);
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+    const shuffleButton = document.getElementById('shuffle');
+    const sortButton = document.getElementById('sort');
+    const listContainer = document.getElementById('card-listing');
+    let ordering = localStorage.getItem('indexOrdering') || 'dateDesc';
+
+    let orderedWorks;
+
+    function scrollHandler() {
+        while (orderedWorks.length > 0 && listContainer.clientHeight - window.scrollY < 5000) {
+            const work = orderedWorks.shift();
+
+            const card = document.createElement('div');
+            card.className = 'card';
+
+            const link = document.createElement('a');
+            link.href = `${ROOT}/works/${work.id}/${INDEX}`;
+            card.appendChild(link);
+
+            const thumb = document.createElement('img');
+            thumb.src = `${ROOT}/thumbnails/${work.id}.jpg`;
+            link.appendChild(thumb);
+
+            const creators = document.createElement('div');
+            creators.className = 'card-creators';
+            let creatorsInfo = `[${work.circle || ''}`;
+            if (work.authors) {
+                let authorList = work.authors[0];
+                for (let i = 1; i < work.authors.length; ++i) {
+                    authorList += `, ${work.authors[i]}`;
+                }
+                creatorsInfo += (work.circle ? ` (${authorList})]` : `${authorList}]`);
+            }
+            creators.textContent = creatorsInfo;
+            link.appendChild(creators);
+
+            const title = document.createElement('div');
+            title.className = 'card-title';
+            title.textContent = work.title;
+            link.appendChild(title);
+
+            listContainer.appendChild(card);
+        }
+    }
+
+    function applyOrdering() {
+        listContainer.replaceChildren();
+        scrollHandler();
+    }
+
+    switch (ordering) {
+    case 'shuffle':
+        let seed = parseInt(localStorage.getItem('shuffleSeed')) || newSeed();
+        orderedWorks = seedableShuffledCopy(WORKS, seed);
+        break;
+    case 'dateAsc':
+        orderedWorks = WORKS.toReversed();
+        break;
+    default:
+        orderedWorks = [...WORKS];
+        break;
+    }
+    applyOrdering();
+
+    window.addEventListener('scroll', scrollHandler);
+
+    document.getElementById('shuffle').onclick = () => {
+        let seed = newSeed();
+        localStorage.setItem('shuffleSeed', seed);
+        ordering = 'shuffle';
+        localStorage.setItem('indexOrdering', ordering);
+
+        orderedWorks = seedableShuffledCopy(WORKS, seed);
+        applyOrdering();
+    };
+    document.getElementById('sort').onclick = () => {
+        if (ordering === 'dateDesc') {
+            ordering = 'dateAsc';
+            orderedWorks = WORKS.toReversed();
+        } else {
+            ordering = 'dateDesc';
+            orderedWorks = [...WORKS];
+        }
+        localStorage.setItem('indexOrdering', ordering);
+        applyOrdering();
+    };
+});
diff --git a/dlibrary/templates/categorization.html b/dlibrary/templates/categorization.html
index 0039442..f903b2f 100644
--- a/dlibrary/templates/categorization.html
+++ b/dlibrary/templates/categorization.html
@@ -4,7 +4,7 @@
 {% from 'utils.html' import urlcat, index, root with context %}
 <h1 id="title"><a href="{{ root() }}/{{ index() }}">DLibrary</a> &gt; {{ categorization.capitalize() }}</h1>
 {% include 'nav.html' %}
-<div class="card-listing">
+<div id="card-listing">
   {% for cat in categories %}
   <div class="card {% if not work_style_cards %}category{% endif %}">
     <a href="{{ urlcat(cat) }}/{{ index() }}">
diff --git a/dlibrary/templates/index.html b/dlibrary/templates/index.html
new file mode 100644
index 0000000..37aaa4f
--- /dev/null
+++ b/dlibrary/templates/index.html
@@ -0,0 +1,25 @@
+{% extends 'base.html' %}
+{% from 'utils.html' import index, root with context %}
+{% block head %}
+<script>
+  const ROOT = "{{ root() }}";
+  const INDEX = "{{ index() }}";
+  const WORKS = {{ works | tojson }};
+</script>
+<script src="{{ root() }}/static/index.js"></script>
+{% endblock %}
+{% block body %}
+<div id="top">
+  <div id="top-padding"></div>
+  <div id="header">
+    <h1 id="title"><a href="{{ root() }}/{{ index() }}">DLibrary</a></h1>
+    {% include 'nav.html' %}
+  </div>
+  <div id="controls">
+    <button id="shuffle" name="Shuffle"><img src="{{ root() }}/static/icons/shuffle.svg"/></button>
+    <button id="sort" name="Sort"><img src="{{ root() }}/static/icons/sort.svg"/></button>
+  </div>
+</div>
+<div id="card-listing">
+</div>
+{% endblock %}
diff --git a/dlibrary/templates/list.html b/dlibrary/templates/list.html
index d0e1139..ec7165d 100644
--- a/dlibrary/templates/list.html
+++ b/dlibrary/templates/list.html
@@ -3,12 +3,12 @@
 {% from 'utils.html' import index, root with context %}
 <h1 id="title"><a href="{{ root() }}/{{ index() }}">DLibrary</a>{% if categorization %} &gt; <a href="{{ root() }}/{{ categorization }}/{{ index() }}">{{ categorization.capitalize() }}</a>{% endif %}{% if title %} &gt; {{ title }}{% endif %}</h1>
 {% include 'nav.html' %}
-<div class="card-listing">
+<div id="card-listing">
   {% for work in works %}
   <div class="card">
     <a href="{{ root() }}/works/{{ work['id'] }}/{{ index() }}">
       <img src="{{ root() }}/thumbnails/{{ work['id'] }}.jpg">
-      <div class="card-authors">
+      <div class="card-creators">
         [{% if work['circle'] %}{{ work['circle'] }}{% endif %}{% if work['circle'] and work['authors'] %} ({% endif %}{{ ', '.join(work['authors']) }}{% if work['circle'] and work['authors'] %}){% endif %}]
       </div>
       <div class="card-title">

From 61049f0d11e19b8b0b47a95d69390e1b9725f3b4 Mon Sep 17 00:00:00 2001
From: xenofem <xenofem@xeno.science>
Date: Thu, 25 Jan 2024 04:15:11 -0500
Subject: [PATCH 2/3] forgot empty arrays are truthy

---
 dlibrary/static/index.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dlibrary/static/index.js b/dlibrary/static/index.js
index 409e537..813b33a 100644
--- a/dlibrary/static/index.js
+++ b/dlibrary/static/index.js
@@ -54,7 +54,7 @@ document.addEventListener('DOMContentLoaded', () => {
             const creators = document.createElement('div');
             creators.className = 'card-creators';
             let creatorsInfo = `[${work.circle || ''}`;
-            if (work.authors) {
+            if (work.authors.length > 0) {
                 let authorList = work.authors[0];
                 for (let i = 1; i < work.authors.length; ++i) {
                     authorList += `, ${work.authors[i]}`;

From cf23ca6bbe925a1e0628690b245395922e82f2ae Mon Sep 17 00:00:00 2001
From: xenofem <xenofem@xeno.science>
Date: Thu, 25 Jan 2024 04:16:26 -0500
Subject: [PATCH 3/3] fix case with no authors

---
 dlibrary/static/index.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/dlibrary/static/index.js b/dlibrary/static/index.js
index 813b33a..8d48595 100644
--- a/dlibrary/static/index.js
+++ b/dlibrary/static/index.js
@@ -59,8 +59,9 @@ document.addEventListener('DOMContentLoaded', () => {
                 for (let i = 1; i < work.authors.length; ++i) {
                     authorList += `, ${work.authors[i]}`;
                 }
-                creatorsInfo += (work.circle ? ` (${authorList})]` : `${authorList}]`);
+                creatorsInfo += (work.circle ? ` (${authorList})` : `${authorList}`);
             }
+            creatorsInfo += ']';
             creators.textContent = creatorsInfo;
             link.appendChild(creators);