diff --git a/dlibrary/dlibrary.py b/dlibrary/dlibrary.py index c963ee1..d26dba9 100755 --- a/dlibrary/dlibrary.py +++ b/dlibrary/dlibrary.py @@ -2,6 +2,7 @@ import argparse import asyncio +from configparser import ConfigParser import importlib_resources as resources from io import BytesIO from pathlib import Path @@ -1389,6 +1390,48 @@ def generate(args): debug('main database closed') +def update_reading(args): + debug('updating list of currently-reading works') + + firefox_data_dir = Path.home() / '.mozilla' / 'firefox' + debug('finding firefox profiles') + firefox_profiles_parser = ConfigParser() + firefox_profiles_parser.read(firefox_data_dir / 'profiles.ini') + default_profile = next( + section['Path'] + for section in firefox_profiles_parser.values() + if section.get('Default') == '1' + ) + debug(f'selecting default profile {default_profile}') + storage_path = firefox_data_dir / default_profile / 'storage' / 'default' + dlibrary_site_works_path = args.destdir / 'site' / 'works' + origin_storage_glob = ( + 'file+++' + + str(dlibrary_site_works_path.absolute()).replace('/', '+') + + '*+view+index.html/ls/data.sqlite' + ) + debug(f'searching for local storage matching glob {origin_storage_glob}') + + reading_works = [] + for local_storage_db in storage_path.glob(origin_storage_glob): + debug(f'reading db {local_storage_db}') + try: + with sqlite3.connect(local_storage_db, timeout=0.1) as con: + cur = con.cursor() + for (key,) in cur.execute("SELECT key FROM data WHERE key LIKE '%-currentPage' AND VALUE != CAST('0' AS BLOB)"): + work_id = key[:-len('-currentPage')] + reading_works.append(work_id) + except sqlite3.OperationalError: + debug(f'database {local_storage_db} locked, skipping') + + debug(f'reading works: {reading_works}') + output_file = args.destdir / 'site' / 'reading.js' + with open(output_file, 'w') as f: + f.write('const READING_WORKS = [\n') + for work_id in reading_works: + f.write(f' "{work_id}",\n') + f.write('];') + argparser = argparse.ArgumentParser( prog='dlibrary', formatter_class=argparse.RawDescriptionHelpFormatter, @@ -1578,6 +1621,22 @@ parser_generate = subparsers.add_parser( ) parser_generate.set_defaults(func=generate) +parser_update_reading = subparsers.add_parser( + 'update-reading', + help='update list of currently-reading works (firefox-only)', + formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent("""\ + If accessing dlibrary via file:// URLs, every distinct filepath is + its own opaque origin, so individual works can't share + localStorage with the main overview page, making it impossible to + sort works by currently-reading status. This subcommand can be run + periodically to work around the same-origin limitations by + directly accessing data from firefox's localStorage and putting a + list of currently-reading works in a JS file that the main page can read. + """), +) +parser_update_reading.set_defaults(func=update_reading) + def main(): args = argparser.parse_args() diff --git a/dlibrary/static/dlibrary.css b/dlibrary/static/dlibrary.css index 52f3696..0ee5bcf 100644 --- a/dlibrary/static/dlibrary.css +++ b/dlibrary/static/dlibrary.css @@ -98,6 +98,10 @@ body { overflow-wrap: anywhere; } +.card.reading { + background: #522; +} + .card img { max-width: 100%; max-height: 100%; diff --git a/dlibrary/static/icons/bookmark.svg b/dlibrary/static/icons/bookmark.svg new file mode 100644 index 0000000..e81be0a --- /dev/null +++ b/dlibrary/static/icons/bookmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/dlibrary/static/index.js b/dlibrary/static/index.js index f818247..cd5bfb5 100644 --- a/dlibrary/static/index.js +++ b/dlibrary/static/index.js @@ -28,9 +28,14 @@ function newSeed() { return Math.floor(Math.random() * LCG_M); } +function isReading(work) { + return READING_WORKS.indexOf(work.id) !== -1 || !!localStorage.getItem(`${work.id}-currentPage`); +} + document.addEventListener('DOMContentLoaded', () => { const shuffleButton = document.getElementById('shuffle'); const sortButton = document.getElementById('sort'); + const readingButton = document.getElementById('reading'); const searchBox = document.getElementById('search'); const listContainer = document.getElementById('main-listing'); @@ -45,6 +50,9 @@ document.addEventListener('DOMContentLoaded', () => { const card = document.createElement('div'); card.className = 'card'; + if (isReading(work)) { + card.classList.add('reading'); + } const link = document.createElement('a'); link.href = `${ROOT}/works/${work.id}/${INDEX}`; @@ -87,6 +95,20 @@ document.addEventListener('DOMContentLoaded', () => { case 'dateAsc': orderedWorks = WORKS.toReversed(); break; + case 'reading': + orderedWorks = WORKS.toSorted((a, b) => { + const aReading = isReading(a); + const bReading = isReading(b); + + if (aReading && !bReading) { + return -1; + } + if (bReading && !aReading) { + return 1; + } + return 0; + }) + break; default: orderedWorks = [...WORKS]; break; @@ -120,23 +142,26 @@ document.addEventListener('DOMContentLoaded', () => { window.addEventListener('scroll', scrollHandler); + function updateOrdering(o) { + ordering = o; + localStorage.setItem('indexOrdering', ordering); + applyView(); + } + shuffleButton.onclick = () => { shuffleSeed = newSeed(); localStorage.setItem('shuffleSeed', shuffleSeed); - ordering = 'shuffle'; - localStorage.setItem('indexOrdering', ordering); - - applyView(); + updateOrdering('shuffle'); }; sortButton.onclick = () => { if (ordering === 'dateDesc') { - ordering = 'dateAsc'; + updateOrdering('dateAsc'); } else { - ordering = 'dateDesc'; + updateOrdering('dateDesc'); } - localStorage.setItem('indexOrdering', ordering); - - applyView(); + }; + readingButton.onclick = () => { + updateOrdering('reading'); }; searchBox.oninput = applyView; diff --git a/dlibrary/templates/index.html b/dlibrary/templates/index.html index fa432a9..fa632f8 100644 --- a/dlibrary/templates/index.html +++ b/dlibrary/templates/index.html @@ -6,6 +6,7 @@ const INDEX = "{{ index() }}"; const WORKS = {{ works | tojson }}; + {% endblock %} {% block body %} @@ -18,6 +19,7 @@
+