sort works by currently-reading
This commit is contained in:
parent
06d782e77a
commit
d689733bbb
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
3
dlibrary/static/icons/bookmark.svg
Normal file
3
dlibrary/static/icons/bookmark.svg
Normal 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 |
|
@ -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;
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in a new issue