sort works by currently-reading

This commit is contained in:
xenofem 2024-07-07 01:05:27 -04:00
parent 06d782e77a
commit d689733bbb
5 changed files with 102 additions and 9 deletions

View file

@ -2,6 +2,7 @@
import argparse import argparse
import asyncio import asyncio
from configparser import ConfigParser
import importlib_resources as resources import importlib_resources as resources
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
@ -1389,6 +1390,48 @@ def generate(args):
debug('main database closed') 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( argparser = argparse.ArgumentParser(
prog='dlibrary', prog='dlibrary',
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
@ -1578,6 +1621,22 @@ parser_generate = subparsers.add_parser(
) )
parser_generate.set_defaults(func=generate) 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(): def main():
args = argparser.parse_args() args = argparser.parse_args()

View file

@ -98,6 +98,10 @@ body {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.card.reading {
background: #522;
}
.card img { .card img {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;

View file

@ -0,0 +1,3 @@
<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 L 70 100 L 110 140 L 110 0 Z' fill='none' stroke='#ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='10'/>
</svg>

After

Width:  |  Height:  |  Size: 332 B

View file

@ -28,9 +28,14 @@ function newSeed() {
return Math.floor(Math.random() * LCG_M); 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', () => { document.addEventListener('DOMContentLoaded', () => {
const shuffleButton = document.getElementById('shuffle'); const shuffleButton = document.getElementById('shuffle');
const sortButton = document.getElementById('sort'); const sortButton = document.getElementById('sort');
const readingButton = document.getElementById('reading');
const searchBox = document.getElementById('search'); const searchBox = document.getElementById('search');
const listContainer = document.getElementById('main-listing'); const listContainer = document.getElementById('main-listing');
@ -45,6 +50,9 @@ document.addEventListener('DOMContentLoaded', () => {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'card'; card.className = 'card';
if (isReading(work)) {
card.classList.add('reading');
}
const link = document.createElement('a'); const link = document.createElement('a');
link.href = `${ROOT}/works/${work.id}/${INDEX}`; link.href = `${ROOT}/works/${work.id}/${INDEX}`;
@ -87,6 +95,20 @@ document.addEventListener('DOMContentLoaded', () => {
case 'dateAsc': case 'dateAsc':
orderedWorks = WORKS.toReversed(); orderedWorks = WORKS.toReversed();
break; 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: default:
orderedWorks = [...WORKS]; orderedWorks = [...WORKS];
break; break;
@ -120,23 +142,26 @@ document.addEventListener('DOMContentLoaded', () => {
window.addEventListener('scroll', scrollHandler); window.addEventListener('scroll', scrollHandler);
function updateOrdering(o) {
ordering = o;
localStorage.setItem('indexOrdering', ordering);
applyView();
}
shuffleButton.onclick = () => { shuffleButton.onclick = () => {
shuffleSeed = newSeed(); shuffleSeed = newSeed();
localStorage.setItem('shuffleSeed', shuffleSeed); localStorage.setItem('shuffleSeed', shuffleSeed);
ordering = 'shuffle'; updateOrdering('shuffle');
localStorage.setItem('indexOrdering', ordering);
applyView();
}; };
sortButton.onclick = () => { sortButton.onclick = () => {
if (ordering === 'dateDesc') { if (ordering === 'dateDesc') {
ordering = 'dateAsc'; updateOrdering('dateAsc');
} else { } else {
ordering = 'dateDesc'; updateOrdering('dateDesc');
} }
localStorage.setItem('indexOrdering', ordering); };
readingButton.onclick = () => {
applyView(); updateOrdering('reading');
}; };
searchBox.oninput = applyView; searchBox.oninput = applyView;

View file

@ -6,6 +6,7 @@
const INDEX = "{{ index() }}"; const INDEX = "{{ index() }}";
const WORKS = {{ works | tojson }}; const WORKS = {{ works | tojson }};
</script> </script>
<script src="{{ root() }}/reading.js"></script>
<script src="{{ root() }}/static/index.js"></script> <script src="{{ root() }}/static/index.js"></script>
{% endblock %} {% endblock %}
{% block body %} {% block body %}
@ -18,6 +19,7 @@
<div id="controls"> <div id="controls">
<button id="shuffle" name="Shuffle"><img src="{{ root() }}/static/icons/shuffle.svg"/></button> <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> <button id="sort" name="Sort"><img src="{{ root() }}/static/icons/sort.svg"/></button>
<button id="reading" name="Reading"><img src="{{ root() }}/static/icons/bookmark.svg"/></button>
</div> </div>
</div> </div>
<div id="search-container"> <div id="search-container">