ditch the awkward periodic service thing, just use window.postMessage to pass info between file:// origins

This commit is contained in:
xenofem 2024-07-08 04:30:18 -04:00
parent fa5e5a1f6a
commit b66e1c5372
7 changed files with 107 additions and 65 deletions

View file

@ -2,7 +2,6 @@
import argparse
import asyncio
from configparser import ConfigParser
import importlib_resources as resources
from io import BytesIO
from pathlib import Path
@ -14,6 +13,7 @@ import readline
import shutil
import sqlite3
import stat
import string
import textwrap
import time
import unicodedata
@ -1219,8 +1219,11 @@ def generate(args):
categorization_template = jenv.get_template("categorization.html")
work_template = jenv.get_template("work.html")
index_template = jenv.get_template("index.html")
store_template = jenv.get_template("store.html")
debug('templates loaded')
store_token = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
debug('opening main database')
con = sqlite3.connect(args.destdir / 'meta.db')
cur = con.cursor()
@ -1313,7 +1316,7 @@ def generate(args):
suggested=[works[suggested] for suggested in cached_suggestions[work['id']]],
))
with open(viewer_dir / 'index.html', 'w') as f:
f.write(viewer_template.render(depth=3, work=work, title=work['title']))
f.write(viewer_template.render(depth=3, work=work, title=work['title'], token=store_token))
count_progress(idx, len(works), 'works processed')
@ -1378,9 +1381,14 @@ def generate(args):
debug('writing index page')
with open(site_dir / 'index.html', 'w') as f:
f.write(index_template.render(depth=0, works=list(works.values())))
f.write(index_template.render(depth=0, works=list(works.values()), token=store_token))
debug('index page written')
debug('writing store iframe page')
with open(site_dir / 'store.html', 'w') as f:
f.write(store_template.render(depth=0, token=store_token))
debug('store iframe page written')
debug('closing cache database')
cache_con.close()
debug('cache database closed')
@ -1390,48 +1398,6 @@ 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,
@ -1621,22 +1587,6 @@ 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()

View file

@ -170,3 +170,8 @@ body {
#suggested-subheader {
text-align: center;
}
#store-iframe {
display: none;
}

View file

@ -28,11 +28,17 @@ function newSeed() {
return Math.floor(Math.random() * LCG_M);
}
let receivedStore = JSON.parse(localStorage.getItem('receivedStore')) || {};
const isFileUrl = (window.location.protocol === 'file:');
function isReading(work) {
return READING_WORKS.indexOf(work.id) !== -1 || !!parseInt(localStorage.getItem(`${work.id}-currentPage`));
const key = `${work.id}-currentPage`;
const value = isFileUrl ? receivedStore[key] : localStorage.getItem(key);
return !!parseInt(value);
}
document.addEventListener('DOMContentLoaded', () => {
function setupIndex() {
const shuffleButton = document.getElementById('shuffle');
const sortButton = document.getElementById('sort');
const readingButton = document.getElementById('reading');
@ -165,4 +171,30 @@ document.addEventListener('DOMContentLoaded', () => {
};
searchBox.oninput = applyView;
});
}
if (isFileUrl) {
let initialized = false;
window.addEventListener('message', (event) => {
if (event.data.token !== TOKEN) {
return;
}
receivedStore = event.data.store;
localStorage.setItem('receivedStore', JSON.stringify(receivedStore));
if (!initialized) {
initialized = true;
setupIndex();
}
});
window.addEventListener('load', () => {
document.getElementById('store-iframe').contentWindow.postMessage({
token: TOKEN,
operation: 'getAll',
}, "*");
});
} else {
document.addEventListener('DOMContentLoaded', setupIndex);
}

View file

@ -5,6 +5,8 @@ const COMMAND_SEQUENCE_MAX_INTERVAL = 400;
const ACCEL_PAGE_MOVEMENT = 10;
const isFileUrl = (window.location.protocol === 'file:');
document.addEventListener('DOMContentLoaded', () => {
const currentImage = document.getElementById('current-image');
const preloadImages = document.getElementById('preload-images');
@ -102,6 +104,14 @@ document.addEventListener('DOMContentLoaded', () => {
currentPage = pageNum;
localStorage.setItem(`${WORK_ID}-currentPage`, currentPage);
if (isFileUrl) {
document.getElementById('store-iframe').contentWindow.postMessage({
token: TOKEN,
operation: 'set',
key: `${WORK_ID}-currentPage`,
value: currentPage,
}, "*");
}
currentImage.replaceChildren(imageSrc(current));
@ -171,6 +181,13 @@ document.addEventListener('DOMContentLoaded', () => {
changeDuration(duration, true);
if (currentPage === IMAGES.length - 1 || currentPage === 0) {
localStorage.removeItem(`${WORK_ID}-currentPage`);
if (isFileUrl) {
document.getElementById('store-iframe').contentWindow.postMessage({
token: TOKEN,
operation: 'remove',
key: `${WORK_ID}-currentPage`,
}, "*");
}
}
window.location.href = `../${INDEX}`;
}

View file

@ -5,8 +5,8 @@
const ROOT = "{{ root() }}";
const INDEX = "{{ index() }}";
const WORKS = {{ works | tojson }};
const TOKEN = "{{ token }}";
</script>
<script src="{{ root() }}/reading.js"></script>
<script src="{{ root() }}/static/index.js"></script>
{% endblock %}
{% block body %}
@ -27,4 +27,5 @@
</div>
<div id="main-listing" class="card-listing">
</div>
<iframe id="store-iframe" src="{{ root() }}/store.html"></iframe>
{% endblock %}

View file

@ -0,0 +1,35 @@
{% from 'utils.html' import root with context -%}
<!DOCTYPE html>
<html>
<head>
<link rel="manifest" href="{{ root() }}/static/manifest.json" crossorigin="use-credentials">
<script type="text/javascript">
const TOKEN = "{{ token }}";
window.addEventListener("message", (event) => {
if (event.data.token !== TOKEN) {
return;
}
switch (event.data.operation) {
case 'set':
localStorage.setItem(event.data.key, event.data.value);
break;
case 'get':
event.source.postMessage({
token: TOKEN,
key: event.data.key,
value: localStorage.getItem(event.data.key),
}, "*");
break;
case 'remove':
localStorage.removeItem(event.data.key);
break;
case 'getAll':
event.source.postMessage({ token: TOKEN, store: {...localStorage} }, "*");
break;
}
});
</script>
</head>
</html>

View file

@ -5,6 +5,7 @@
<script>
const WORK_ID = "{{ work['id'] }}";
const INDEX = "{{ index() }}";
const TOKEN = "{{ token }}";
const IMAGES = [
{% for filename in work['images'] %}
"{{ root() }}/images/{{ work['id'] }}/{{ filename }}",
@ -48,4 +49,5 @@
</div>
<div class="image-container" id="current-image"></div>
<div class="image-container" id="preload-images"></div>
<iframe id="store-iframe" src="{{ root() }}/store.html"></iframe>
{% endblock %}