Skip to content

Instantly share code, notes, and snippets.

@misaalanshori
Last active August 11, 2023 15:33
Show Gist options
  • Save misaalanshori/4fccf688073480d121905139e437e798 to your computer and use it in GitHub Desktop.
Save misaalanshori/4fccf688073480d121905139e437e798 to your computer and use it in GitHub Desktop.
AO3 Reader - Patched
// ==UserScript==
// @name AO3 Reader - Vite
// @namespace https://misa.pw
// @version 0.0.0
// @author monkey
// @icon https://www.google.com/s2/favicons?sz=64&domain=archiveofourown.org
// @match https://archiveofourown.org/*
// @require https://cdn.jsdelivr.net/npm/preact@10.16.0/dist/preact.min.js
// @connect download.archiveofourown.org
// @grant GM.deleteValue
// @grant GM.getValue
// @grant GM.listValues
// @grant GM.setValue
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-idle
// ==/UserScript==
(i=>{const r=document.createElement("style");r.dataset.source="vite-plugin-monkey",r.textContent=i,document.head.append(r)})(" div.a3r .modal{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#00000080;display:flex;justify-content:center;align-items:center;z-index:1000}div.a3r .modal>.box{background-color:#fff;padding:20px;border-radius:8px;box-shadow:0 0 10px #0003}div.a3r .modal>.box>.contents{display:flex;flex-direction:column}div.a3r .modal>.box>.actions{margin-top:20px;display:flex;flex-direction:row;justify-content:center;gap:4px}div.a3r .modal>.box>.actions>.button{background:initial;color:#fff;padding:8px 16px;border:1px solid #bdbdbd;border-radius:12px;cursor:pointer}div.a3r .modal>.box>.actions>.button:hover{background-color:#0003}div.a3r .workList{display:flex;flex-direction:column;width:100%;height:100%}div.a3r .workItem *{transition:all .1s linear}div.a3r .workItem{display:flex;flex-direction:column;border-bottom:var(--divider-color) 1px solid;margin:8px 0;padding-bottom:16px;align-items:stretch;text-align:start;white-space:pre-line}div.a3r .workItem>*{padding:0 16px}div.a3r .workItem .header{display:flex;flex-direction:row;align-items:center}div.a3r .workItem .header>.title{display:flex;flex-direction:column;font-size:large;padding-top:8px;padding-bottom:8px;width:100%}div.a3r .workItem .header>.title>*{margin:0}div.a3r .workItem .header:hover{background-color:#0000001a}div.a3r .workItem .tags{display:flex;flex-direction:column}div.a3r .workItem .tags:hover{background-color:#0000001a}div.a3r .workItem .tags .main,div.a3r .workItem .tags .expanded{display:flex;flex-direction:column}div.a3r .workItem .tags .main h5{margin:1px}div.a3r .workItem .summary{display:flex;flex-direction:column;padding-bottom:8px}div.a3r .workItem .summary:hover{background-color:#0000001a}div.a3r .workItem .trash{display:flex;flex-direction:column;align-items:center;justify-content:center}div.a3r .workItem .trash>.button{all:initial;display:flex;align-items:center;justify-content:center;height:56px;width:56px;border-radius:64px}div.a3r .workItem .trash>.button:hover{background-color:#0000001a}div.a3r .container{--dark-primary-color: #512da8;--light-primary-color: #d1c4e9;--primary-color: #673ab7;--text-color: #ffffff;--accent-color: #e040fb;--primary-text-color: #212121;--secondary-text-color: #757575;--divider-color: #bdbdbd;--background-color: #f5f5f5}div.a3r .container{display:flex;flex-direction:column;height:100%;color:var(--primary-text-color)}div.a3r .appbar{display:flex;flex-direction:row;height:56px;justify-content:center;background-color:var(--primary-color);color:var(--background-color);align-items:center;filter:drop-shadow(0px 0px 4px rgba(0,0,0,.5))}div.a3r .appbar .left{width:100%;display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-start;align-items:stretch;padding:0 16px}div.a3r .appbar .right{width:100%;display:flex;flex-direction:row;justify-content:flex-end;align-items:flex-end;align-items:stretch;padding:0 16px}div.a3r .appbar h3{margin:0;padding:0;font-size:x-large}div.a3r .contents{display:flex;flex-direction:column;overflow:scroll;height:100%;width:100%}div.a3r .contents *{color:var(--primary-text-color)!important}div.a3r .icons{fill:var(--text-color);width:24px}div.a3r .icons.white{filter:invert(100%)}div.a3r .appbar .button{all:initial;display:flex;justify-content:center;height:56px;width:56px}div.a3r .appbar .button:hover{background-color:#0000001a}div.a3r .workDetails{display:flex;flex-direction:column;padding:16px 32px;gap:16px;border-top:1px solid var(--divider-color);background-color:#fff;filter:drop-shadow(0px 0px 4px rgba(0,0,0,.5))}div.a3r .workDetails .headers{display:flex;flex-direction:column;text-align:left}div.a3r .workDetails .headers>*{margin:0}div.a3r .workDetails .headers>h4{font-size:xx-large}div.a3r .workDetails .headers>h5{font-size:medium}div.a3r .workInfo{padding:8px 16px;display:flex;flex-direction:column;text-align:left;gap:8px;white-space:pre-line}div.a3r .workInfo{display:flex;height:100%}div.a3r .buttons{display:flex;flex-direction:column}div.a3r .buttons>.row{width:100%;display:flex;flex-direction:row;justify-content:center}div.a3r .buttons>.row>a{all:initial;display:block;text-decoration:none;width:100%;height:100%;padding:4px 12px;margin:2px!important;border:none;border-radius:2em!important;text-align:center!important;font-size:medium!important;background-color:var(--primary-color)!important;color:var(--text-color)!important}div.a3r .buttons>.row>a:hover{filter:brightness(85%)!important}div.a3r{all:revert;position:fixed;top:0;left:0;width:100%;height:100%;background-color:#fff;z-index:100}div.a3r *{font-family:Arial,Helvetica,sans-serif!important}div.a3r *:after{all:unset} ");
function addScript(src) {
return new Promise((resolve, reject) => {
const s = document.createElement('script');
s.setAttribute('src', src);
s.addEventListener('load', resolve);
s.addEventListener('error', reject);
document.body.appendChild(s);
});
}
await (async function loadLibrary() {
await addScript("https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js");
await addScript("https://cdn.jsdelivr.net/npm/epubjs@0.3.93/dist/epub.min.js");
})();
(async function (preact, ePub, jszip) {
'use strict';
var _a;
var t, r, u, i, o$1 = 0, f = [], c = [], e = preact.options.__b, a = preact.options.__r, v = preact.options.diffed, l = preact.options.__c, m = preact.options.unmount;
function d(t2, u2) {
preact.options.__h && preact.options.__h(r, t2, o$1 || u2), o$1 = 0;
var i2 = r.__H || (r.__H = { __: [], __h: [] });
return t2 >= i2.__.length && i2.__.push({ __V: c }), i2.__[t2];
}
function h(n) {
return o$1 = 1, s(B, n);
}
function s(n, u2, i2) {
var o2 = d(t++, 2);
if (o2.t = n, !o2.__c && (o2.__ = [i2 ? i2(u2) : B(void 0, u2), function(n2) {
var t2 = o2.__N ? o2.__N[0] : o2.__[0], r2 = o2.t(t2, n2);
t2 !== r2 && (o2.__N = [r2, o2.__[1]], o2.__c.setState({}));
}], o2.__c = r, !r.u)) {
var f2 = function(n2, t2, r2) {
if (!o2.__c.__H)
return true;
var u3 = o2.__c.__H.__.filter(function(n3) {
return n3.__c;
});
if (u3.every(function(n3) {
return !n3.__N;
}))
return !c2 || c2.call(this, n2, t2, r2);
var i3 = false;
return u3.forEach(function(n3) {
if (n3.__N) {
var t3 = n3.__[0];
n3.__ = n3.__N, n3.__N = void 0, t3 !== n3.__[0] && (i3 = true);
}
}), !(!i3 && o2.__c.props === n2) && (!c2 || c2.call(this, n2, t2, r2));
};
r.u = true;
var c2 = r.shouldComponentUpdate, e2 = r.componentWillUpdate;
r.componentWillUpdate = function(n2, t2, r2) {
if (this.__e) {
var u3 = c2;
c2 = void 0, f2(n2, t2, r2), c2 = u3;
}
e2 && e2.call(this, n2, t2, r2);
}, r.shouldComponentUpdate = f2;
}
return o2.__N || o2.__;
}
function p(u2, i2) {
var o2 = d(t++, 3);
!preact.options.__s && z(o2.__H, i2) && (o2.__ = u2, o2.i = i2, r.__H.__h.push(o2));
}
function _$1(n) {
return o$1 = 5, F(function() {
return { current: n };
}, []);
}
function F(n, r2) {
var u2 = d(t++, 7);
return z(u2.__H, r2) ? (u2.__V = n(), u2.i = r2, u2.__h = n, u2.__V) : u2.__;
}
function b() {
for (var t2; t2 = f.shift(); )
if (t2.__P && t2.__H)
try {
t2.__H.__h.forEach(k), t2.__H.__h.forEach(w), t2.__H.__h = [];
} catch (r2) {
t2.__H.__h = [], preact.options.__e(r2, t2.__v);
}
}
preact.options.__b = function(n) {
r = null, e && e(n);
}, preact.options.__r = function(n) {
a && a(n), t = 0;
var i2 = (r = n.__c).__H;
i2 && (u === r ? (i2.__h = [], r.__h = [], i2.__.forEach(function(n2) {
n2.__N && (n2.__ = n2.__N), n2.__V = c, n2.__N = n2.i = void 0;
})) : (i2.__h.forEach(k), i2.__h.forEach(w), i2.__h = [], t = 0)), u = r;
}, preact.options.diffed = function(t2) {
v && v(t2);
var o2 = t2.__c;
o2 && o2.__H && (o2.__H.__h.length && (1 !== f.push(o2) && i === preact.options.requestAnimationFrame || ((i = preact.options.requestAnimationFrame) || j)(b)), o2.__H.__.forEach(function(n) {
n.i && (n.__H = n.i), n.__V !== c && (n.__ = n.__V), n.i = void 0, n.__V = c;
})), u = r = null;
}, preact.options.__c = function(t2, r2) {
r2.some(function(t3) {
try {
t3.__h.forEach(k), t3.__h = t3.__h.filter(function(n) {
return !n.__ || w(n);
});
} catch (u2) {
r2.some(function(n) {
n.__h && (n.__h = []);
}), r2 = [], preact.options.__e(u2, t3.__v);
}
}), l && l(t2, r2);
}, preact.options.unmount = function(t2) {
m && m(t2);
var r2, u2 = t2.__c;
u2 && u2.__H && (u2.__H.__.forEach(function(n) {
try {
k(n);
} catch (n2) {
r2 = n2;
}
}), u2.__H = void 0, r2 && preact.options.__e(r2, u2.__v));
};
var g = "function" == typeof requestAnimationFrame;
function j(n) {
var t2, r2 = function() {
clearTimeout(u2), g && cancelAnimationFrame(t2), setTimeout(n);
}, u2 = setTimeout(r2, 100);
g && (t2 = requestAnimationFrame(r2));
}
function k(n) {
var t2 = r, u2 = n.__c;
"function" == typeof u2 && (n.__c = void 0, u2()), r = t2;
}
function w(n) {
var t2 = r;
n.__c = n.__(), r = t2;
}
function z(n, t2) {
return !n || n.length !== t2.length || t2.some(function(t3, r2) {
return t3 !== n[r2];
});
}
function B(n, t2) {
return "function" == typeof t2 ? t2(n) : t2;
}
var _GM = /* @__PURE__ */ (() => typeof GM != "undefined" ? GM : void 0)();
var _GM_xmlhttpRequest = /* @__PURE__ */ (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)();
var _unsafeWindow = /* @__PURE__ */ (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
var classes;
class StorageWrapper {
/**
* @constructor
* @param {string} storageSpace name of the storage space
*/
constructor(storageSpace) {
this.storageSpace = storageSpace;
}
set(key, value) {
return _GM.setValue(`${this.storageSpace}.${key}`, value);
}
get(key) {
return _GM.getValue(`${this.storageSpace}.${key}`);
}
delete(key) {
return _GM.deleteValue(`${this.storageSpace}.${key}`);
}
list() {
return _GM.listValues().then((values) => {
return values.filter((value) => {
return value.startsWith(this.storageSpace);
}).map((value) => {
return value.slice((this.storageSpace + ".").length);
});
});
}
}
const blobSerializer = {
toBase64: (blobject) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
resolve(reader.result);
};
reader.onerror = reject;
reader.readAsDataURL(blobject);
});
},
toBlob: (base64) => {
return fetch(base64).then((res) => res.blob());
}
};
const objectSerializer = {
datePattern: /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/i,
serialize(classInstance) {
return JSON.stringify(classInstance, (key, value) => {
if (value && typeof value === "object") {
value.__type = value.constructor.name;
}
return value;
});
},
deserialize(jsonString) {
return JSON.parse(jsonString, (key, value) => {
if (value && typeof value === "object" && value.__type) {
const DynamicClass = classes[value.__type] || Object;
value = Object.assign(new DynamicClass(), value);
delete value.__type;
} else if (this.datePattern.test(value)) {
value = new Date(value);
}
return value;
});
}
};
const AO3Parser = {
/**
* @param {HTMLElement} html
* @returns {?AO3Work}
*/
parseWorkPage(html) {
if (!html.querySelector("div.wrapper > dl.work.meta.group")) {
return null;
}
return new AO3Work(html.querySelector("li.share > a").attributes.href.value.split("/")[2].split("?")[0], html.querySelector("h2.title.heading").textContent.trim(), Array.from(html.querySelectorAll("h3.byline.heading > a")).map((e2) => {
return new AO3Author(e2.textContent.trim(), e2.attributes.href.value);
}), Array.from(html.querySelectorAll("dd.rating.tags > ul > li > a")).map((e2) => {
return new AO3Tag(e2.textContent.trim(), e2.attributes.href.value);
}), Array.from(html.querySelectorAll("dd.warning.tags > ul > li > a")).map((e2) => {
return new AO3Tag(e2.textContent.trim(), e2.attributes.href.value);
}), Array.from(html.querySelectorAll("dd.fandom.tags > ul > li > a")).map((e2) => {
return new AO3Tag(e2.textContent.trim(), e2.attributes.href.value);
}), Array.from(html.querySelectorAll("dd.relationship.tags > ul > li > a")).map((e2) => {
return new AO3Tag(e2.textContent.trim(), e2.attributes.href.value);
}), Array.from(html.querySelectorAll("dd.character.tags > ul > li > a")).map((e2) => {
return new AO3Tag(e2.textContent.trim(), e2.attributes.href.value);
}), Array.from(html.querySelectorAll("dd.freeform.tags > ul > li > a")).map((e2) => {
return new AO3Tag(e2.textContent.trim(), e2.attributes.href.value);
}), html.querySelector("div.summary.module > blockquote.userstuff") ? Array.from(html.querySelector("div.summary.module > blockquote.userstuff").childNodes).map((e2) => Array.from(e2.childNodes).map((v2) => v2.nodeName != "BR" ? v2.textContent : "\n").join("")).join("\n\n").trim() : "", parseInt(html.querySelector("dd.chapters").textContent.split("/")[0].trim()), parseInt(html.querySelector("dd.chapters").textContent.split("/")[1].trim()) || null, new Date(html.querySelector("dd.published").textContent.trim()), new Date((html.querySelector("dd.status") || html.querySelector("dd.published")).textContent.trim()), html.querySelector("dd.words") ? parseInt(html.querySelector("dd.words").textContent.trim()) : 0, html.querySelector("dd.comments") ? parseInt(html.querySelector("dd.comments").textContent.trim()) : 0, html.querySelector("dd.kudos") ? parseInt(html.querySelector("dd.kudos").textContent.trim()) : 0, html.querySelector("dd.bookmarks") ? parseInt(html.querySelector("dd.bookmarks").textContent.trim()) : 0, html.querySelector("dd.hits") ? parseInt(html.querySelector("dd.hits").textContent.trim()) : 0, html.querySelector("dd.language") ? html.querySelector("dd.language").textContent.trim() : "", Array.from(html.querySelectorAll("dd.collections > ul > li > a")).map((e2) => {
return new AO3Collection(e2.textContent.trim(), e2.attributes.href.value);
}), html.querySelector("div.series.module") ? new AO3WorkSeries(new AO3Series(html.querySelector("div.series.module > ul > li > span.series > span.position > a").textContent.trim(), html.querySelector("div.series.module > ul > li > span.series > span.position > a").attributes.href.value), parseInt(html.querySelector("div.series.module > ul > li > span.series > span.position").textContent.split(" ")[1]), Array.from(document.body.querySelectorAll("div.series.module > ul > li > span.series > a.next")).map((e2) => e2.attributes.href.value)[0] || "", Array.from(document.body.querySelectorAll("div.series.module > ul > li > span.series > a.previous")).map((e2) => e2.attributes.href.value)[0] || "") : null);
}
};
const AO3AccessObject = {
/**
* fetches the html of the work page for the given id using fetch and calls the parser to parse it.
* set fetch to send all cookies, so it should work for logged in users.
* @param {string} id
* @returns {Promise<AO3Work>}
*/
async getWork(id) {
var response = await fetch(`https://archiveofourown.org/works/${id}?view_adult=true`, {
credentials: "include"
});
var html = await response.text();
return AO3Parser.parseWorkPage(new DOMParser().parseFromString(html, "text/html"));
},
/**
*
* @param {string} id
* @returns {Promise<Blob>} The epub file
*/
getWorkEpub(id) {
return new Promise((resolve, reject) => {
_GM_xmlhttpRequest({
method: "GET",
url: `https://archiveofourown.org/downloads/${id}/book.epub?updated_at=${Math.floor(Date.now() / 1e3)}`,
responseType: "blob",
redirect: "follow",
fetch: true,
onload: (response) => {
resolve(response.response);
},
onerror: (err) => {
reject(err);
}
});
});
}
};
const LibraryStorageObject = {
libraryStore: new StorageWrapper("libraryStore"),
epubStore: new StorageWrapper("epubStore"),
locationStore: new StorageWrapper("locationStore"),
/**
*
* @param {LibraryWork} work
* @returns {Promise<void>}
*/
storeWork(work) {
return this.libraryStore.set(work.id, objectSerializer.serialize(work));
},
/**
*
* @param {string} id
* @returns {Promise<LibraryWork>}
*/
async getWork(id) {
return objectSerializer.deserialize(await this.libraryStore.get(id));
},
/**
*
* @param {LibraryWork | string} work id or object
*/
deleteWork(work) {
if (typeof work === "string") {
return this.libraryStore.delete(work);
} else {
return this.libraryStore.delete(work.id);
}
},
listWorks() {
return this.libraryStore.list();
},
async storeEpub(id, date, blob) {
return this.epubStore.set(`${id}_${date.getTime()}`, await blobSerializer.toBase64(blob));
},
async getEpub(id, date) {
return blobSerializer.toBlob(await this.epubStore.get(`${id}_${date.getTime()}`));
},
deleteEpub(id, date) {
return this.epubStore.delete(`${id}_${date.getTime()}`);
},
async listEpubs(id) {
var epubs = await this.epubStore.list();
if (id) {
return epubs.filter((e2) => e2.startsWith(id));
} else {
return epubs;
}
},
async storeLocation(id, location) {
return this.locationStore.set(id, location);
},
async getLocation(id) {
return this.locationStore.get(id);
},
deleteLocation(id) {
return this.locationStore.delete(id);
}
};
class AO3Tag {
/**
* @param {string} name Name of the tag
* @param {string} url URL of the tag
*/
constructor(name, url) {
this.name = name;
this.url = url;
}
}
class AO3Author {
/**
* @param {string} name Name of the author/pseud
* @param {string} url URL of the author/pseud
*/
constructor(name, url) {
this.name = name;
this.url = url;
}
}
class AO3Collection {
/**
* @param {string} name Title of the collection
* @param {string} url URL of the collection
*/
constructor(name, url) {
this.name = name;
this.url = url;
}
}
class AO3Series {
/**
* @param {string} name Title of the series
* @param {string} url URL of the series
*/
constructor(name, url) {
this.name = name;
this.url = url;
}
}
class AO3WorkSeries {
/**
* @constructor
* @param {AO3Series} series Series of the work
* @param {number} position Position of the work in the series
* @param {string} nextWork URL of the next work in the series
* @param {string} previousWork URL of the previous work in the series
*/
constructor(series, position, nextWork, previousWork) {
this.series = series;
this.position = position;
this.nextWork = nextWork;
this.previousWork = previousWork;
}
}
class AO3Work {
/**
* @constructor
* @param {string} id ID of the work;
* @param {string} title Title of the work
* @param {AO3Author[]} authors Authors of the work
* @param {AO3Tag[]} ratings ratings of the work
* @param {AO3Tag[]} warnings Warnings of the work
* @param {AO3Tag[]} fandoms fandoms of the work
* @param {AO3Tag[]} relationships Relationships of the work
* @param {AO3Tag[]} characters Characters of the work
* @param {AO3Tag[]} tags Tags of the work
* @param {string} summary Summary of the work
* @param {number} chapters Number of chapters
* @param {?number} totalChapters Total number of chapters, undefined if unknown
* @param {date} published Date of work published
* @param {date} updated Date of work updated
* @param {number} wordCount Number of words in work
* @param {number} commentCount Number of comments
* @param {number} kudosCount Number of kudos
* @param {number} bookmarkCount Number of bookmarks
* @param {number} hitCount Number of hits
* @param {string} language Language of the work
* @param {AO3Collection[]} collections Collections the work is in
* @param {?AO3WorkSeries} series Series the work is in
*/
constructor(id, title, author, ratings, warnings, fandoms, relationships, characters, tags, summary, chapters, totalChapters, published, updated, wordCount, commentCount, kudosCount, bookmarkCount, hitCount, language, collections, series) {
this.id = id;
this.title = title;
this.authors = author;
this.ratings = ratings;
this.warnings = warnings;
this.fandoms = fandoms;
this.relationships = relationships;
this.characters = characters;
this.tags = tags;
this.summary = summary;
this.chapterCount = chapters;
this.totalChapters = totalChapters;
this.published = published;
this.updated = updated;
this.wordCount = wordCount;
this.commentCount = commentCount;
this.kudosCount = kudosCount;
this.bookmarkCount = bookmarkCount;
this.hitCount = hitCount;
this.language = language;
this.collections = collections;
this.series = series;
}
getEpub() {
return LibraryStorageObject.getEpub(this.id, this.updated);
}
}
class LibraryWork {
/**
* @constructor
* @param {string} id ID of the work
* @param {date} lastChecked Date and Time of last check
* @param {date} lastAccessed Date and Time of last read/access
* @param {AO3Work[]} works Array of all versions of a work
*/
constructor(id, lastChecked, lastAccessed, works) {
this.id = id;
this.lastChecked = lastChecked;
this.lastAccessed = lastAccessed;
this.works = works || [];
}
/**
* Checks for the latest version of the work and adds it to the works array. Also gets the epub and stores it
* @param {boolean} force force get latest version
* @returns {Promise<boolean>} True if the work was updated, false if not
*/
async updateWork(force = false) {
var newWork;
newWork = await AO3AccessObject.getWork(this.id);
if (this.works.length == 0 || newWork.updated > this.getLatest().updated) {
this.works.push(newWork);
await LibraryStorageObject.storeEpub(this.id, newWork.updated, await AO3AccessObject.getWorkEpub(this.id));
this.lastChecked = /* @__PURE__ */ new Date();
return true;
} else if (force) {
this.works[this.works.length - 1] = newWork;
await LibraryStorageObject.storeEpub(this.id, newWork.updated, await AO3AccessObject.getWorkEpub(this.id));
this.lastChecked = /* @__PURE__ */ new Date();
return true;
}
return false;
}
deleteWorkVersion(date) {
var index2 = this.works.findIndex((work) => work.updated == date);
if (index2 != -1) {
this.works.splice(index2, 1);
LibraryStorageObject.deleteEpub(this.id, date);
}
}
/**
* Removes all versions of the work except the latest
*/
purge() {
while (this.works.length > 1) {
this.deleteWorkVersion(this.works[0].updated);
}
}
/**
* Gets the latest version of the work
* @returns {AO3Work}
*/
getLatest() {
return this.works[this.works.length - 1];
}
updateLastAccessed() {
this.lastAccessed = /* @__PURE__ */ new Date();
}
getReadingLocation() {
return LibraryStorageObject.getLocation(this.id);
}
setReadingLocation(location) {
LibraryStorageObject.storeLocation(this.id, location);
}
resetReadingLocation() {
LibraryStorageObject.deleteLocation(this.id);
}
}
const Library = {
/**@type {string[]} */
ids: [],
/**@type {LibraryWork[]} */
works: [],
/**@type {number} */
page: 0,
/**@type {number} */
pageSize: 20,
async refresh() {
this.ids = await LibraryStorageObject.listWorks();
this.works = await Promise.all(this.ids.slice(this.page * this.pageSize, this.page * this.pageSize + this.pageSize).map((id) => LibraryStorageObject.getWork(id)));
},
setPage(num) {
if (num > 0) {
this.page = num - 1;
}
},
getPage() {
return this.page + 1;
},
setPageSize(size) {
this.pageSize = size;
},
getPageCount() {
return Math.ceil(this.ids.length / this.pageSize);
},
/**
* Creates a new Library work, and retrieves latest work from AO3. Returns the new LibraryWork
* @param {string} id
* @returns {Promise<LibraryWork>}
*/
async addWork(id) {
var work = new LibraryWork(id, null, null, null);
await work.updateWork();
work.lastAccessed = /* @__PURE__ */ new Date(0);
this.updateWork(work);
return work;
},
/**
* Deletes a library work, deletes all the metadata and epub files.
* @param {string} id
* @returns {Promise<void>}
*/
async deleteWork(id) {
var epubs = await LibraryStorageObject.listEpubs(id);
for (var epub of epubs) {
await LibraryStorageObject.deleteEpub(id, new Date(parseInt(epub.split("_")[1])));
}
await LibraryStorageObject.deleteWork(id);
},
async getWorkUpdate(work) {
var newWork = await AO3AccessObject.getWork(work.id);
if (work.getLatest().updated.toString() == newWork.updated.toString()) {
work.works.pop();
}
work.works.push(newWork);
await LibraryStorageObject.storeEpub(work.id, newWork.updated, await AO3AccessObject.getWorkEpub(work.id));
work.lastChecked = /* @__PURE__ */ new Date();
await this.updateWork(work);
},
/**
* Updates the stored library work
* @param {LibraryWork} work
* @returns {Promise<void>}
*/
async updateWork(work) {
await LibraryStorageObject.storeWork(work);
}
};
classes = {
AO3Tag,
AO3Author,
AO3Collection,
AO3Series,
AO3WorkSeries,
AO3Work,
LibraryWork,
StorageWrapper
};
_unsafeWindow.ao3reader = {
class: classes,
objects: {
blobSerializer,
objectSerializer,
AO3Parser,
AO3AccessObject,
LibraryStorageObject,
Library
},
TM: {
GM: _GM,
GM_xmlhttpRequest: _GM_xmlhttpRequest
}
};
var _ = 0;
function o(o2, e2, n, t2, f2, l2) {
var s2, u2, a2 = {};
for (u2 in e2)
"ref" == u2 ? s2 = e2[u2] : a2[u2] = e2[u2];
var i2 = { type: o2, props: a2, key: n, ref: s2, __k: null, __: null, __b: 0, __e: null, __d: void 0, __c: null, __h: null, constructor: void 0, __v: --_, __source: f2, __self: l2 };
if ("function" == typeof o2 && (s2 = o2.defaultProps))
for (u2 in s2)
void 0 === a2[u2] && (a2[u2] = s2[u2]);
return preact.options.vnode && preact.options.vnode(i2), i2;
}
function Modal({
isOpen,
children
}) {
if (!isOpen)
return o(preact.Fragment, {});
return o("div", {
className: "modal",
children: o("div", {
className: "box",
children
})
});
}
function ModalContents({
children
}) {
return o("div", {
className: "contents",
children
});
}
function ModalActions({
children
}) {
return o("div", {
className: "actions",
children
});
}
function ModalButton({
onClick,
children
}) {
return o("button", {
className: "button",
onClick,
children
});
}
const exitIcon = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeD0iMHB4IiB5PSIwcHgiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgd2lkdGg9IjUxMiIgaGVpZ2h0PSI1MTIiPgo8Zz4KCTxwYXRoIGQ9Ik0xNzAuNjk4LDQ0OEg3Mi43NTdjLTQuODE0LTAuMDEyLTguNzE0LTMuOTExLTguNzI1LTguNzI1VjcyLjcyNWMwLjAxMi00LjgxNCwzLjkxMS04LjcxNCw4LjcyNS04LjcyNWg5Ny45NDEgICBjMTcuNjczLDAsMzItMTQuMzI3LDMyLTMycy0xNC4zMjctMzItMzItMzJINzIuNzU3QzMyLjYxMSwwLjA0NywwLjA3OSwzMi41OCwwLjAzMiw3Mi43MjV2MzY2LjU0OSAgIEMwLjA3OSw0NzkuNDIsMzIuNjExLDUxMS45NTMsNzIuNzU3LDUxMmg5Ny45NDFjMTcuNjczLDAsMzItMTQuMzI3LDMyLTMyUzE4OC4zNzEsNDQ4LDE3MC42OTgsNDQ4eiIvPgoJPHBhdGggZD0iTTQ4My45MTQsMTg4LjExN2wtODIuODE2LTgyLjc1MmMtMTIuNTAxLTEyLjQ5NS0zMi43NjQtMTIuNDktNDUuMjU5LDAuMDExcy0xMi40OSwzMi43NjQsMC4wMTEsNDUuMjU5bDcyLjc4OSw3Mi43NjggICBMMTM4LjY5OCwyMjRjLTE3LjY3MywwLTMyLDE0LjMyNy0zMiwzMnMxNC4zMjcsMzIsMzIsMzJsMCwwbDI5MS4xMTUtMC41MzNsLTczLjk2Myw3My45NjMgICBjLTEyLjA0MiwxMi45MzYtMTEuMzE3LDMzLjE4NCwxLjYxOCw0NS4yMjZjMTIuMjk1LDExLjQ0NSwzMS4zNDYsMTEuNDM2LDQzLjYzLTAuMDIxbDgyLjc1Mi04Mi43NTIgICBjMzcuNDkxLTM3LjQ5LDM3LjQ5MS05OC4yNzQsMC4wMDEtMTM1Ljc2NGMwLDAtMC4wMDEtMC4wMDEtMC4wMDEtMC4wMDFMNDgzLjkxNCwxODguMTE3eiIvPgo8L2c+CgoKCgoKCgoKCgoKCgoKCjwvc3ZnPgo=";
const refreshIcon = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeD0iMHB4IiB5PSIwcHgiIHZpZXdCb3g9IjAgMCA1MTMuODA2IDUxMy44MDYiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDUxMy44MDYgNTEzLjgwNjsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIj4KPGc+Cgk8cGF0aCBkPSJNNjYuMDc0LDIyOC43MzFDODEuNTc3LDEyMy4zNzksMTc5LjU0OSw1MC41NDIsMjg0LjkwMSw2Ni4wNDVjMzUuOTQ0LDUuMjg5LDY5LjY2MiwyMC42MjYsOTcuMjcsNDQuMjQ0bC0yNC44NTMsMjQuODUzICAgYy04LjMzLDguMzMyLTguMzI4LDIxLjg0LDAuMDA1LDMwLjE3YzMuOTk5LDMuOTk4LDkuNDIzLDYuMjQ1LDE1LjA3OCw2LjI0Nmg5Ny44MzVjMTEuNzgyLDAsMjEuMzMzLTkuNTUxLDIxLjMzMy0yMS4zMzNWNTIuMzkgICBjLTAuMDAzLTExLjc4Mi05LjU1Ni0yMS4zMzEtMjEuMzM4LTIxLjMyOWMtNS42NTUsMC4wMDEtMTEuMDc5LDIuMjQ4LTE1LjA3OCw2LjI0Nkw0MjcuNDE4LDY1LjA0ICAgQzMyMS42NTgtMjkuMjM1LDE1OS40OTctMTkuOTI1LDY1LjIyMiw4NS44MzVjLTMzLjM5OSwzNy40NjctNTUuMDczLDgzLjkwOS02Mi4zMzcsMTMzLjU3MyAgIGMtMi44NjQsMTcuNjA3LDkuMDg3LDM0LjIwMiwyNi42OTMsMzcuMDY2YzEuNTg2LDAuMjU4LDMuMTg4LDAuMzk3LDQuNzk1LDAuNDE3QzUwLjQ4MSwyNTYuNzE3LDY0LjAwMiwyNDQuNzA2LDY2LjA3NCwyMjguNzMxeiIvPgoJPHBhdGggZD0iTTQ3OS40MjksMjU2Ljg5MWMtMTYuMTA4LDAuMTc0LTI5LjYyOSwxMi4xODUtMzEuNzAxLDI4LjE2QzQzMi4yMjUsMzkwLjQwMywzMzQuMjUzLDQ2My4yNCwyMjguOTAxLDQ0Ny43MzggICBjLTM1Ljk0NC01LjI4OS02OS42NjItMjAuNjI2LTk3LjI3LTQ0LjI0NGwyNC44NTMtMjQuODUzYzguMzMtOC4zMzIsOC4zMjgtMjEuODQtMC4wMDUtMzAuMTcgICBjLTMuOTk5LTMuOTk4LTkuNDIzLTYuMjQ1LTE1LjA3OC02LjI0Nkg0My41NjhjLTExLjc4MiwwLTIxLjMzMyw5LjU1MS0yMS4zMzMsMjEuMzMzdjk3LjgzNSAgIGMwLjAwMywxMS43ODIsOS41NTYsMjEuMzMxLDIxLjMzOCwyMS4zMjljNS42NTUtMC4wMDEsMTEuMDc5LTIuMjQ4LDE1LjA3OC02LjI0NmwyNy43MzMtMjcuNzMzICAgYzEwNS43MzUsOTQuMjg1LDI2Ny44ODQsODUuMDA0LDM2Mi4xNy0yMC43MzJjMzMuNDE3LTM3LjQ3NSw1NS4xMDEtODMuOTMzLDYyLjM2My0xMzMuNjE1ICAgYzIuODc2LTE3LjYwNS05LjA2NC0zNC4yMDgtMjYuNjY4LTM3LjA4NEM0ODIuNjU1LDI1Ny4wNTEsNDgxLjA0NCwyNTYuOTEsNDc5LjQyOSwyNTYuODkxeiIvPgo8L2c+CgoKCgoKCgoKCgoKCgoKCjwvc3ZnPgo=";
const trashIcon = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeD0iMHB4IiB5PSIwcHgiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgd2lkdGg9IjUxMiIgaGVpZ2h0PSI1MTIiPgo8Zz4KCTxwYXRoIGQ9Ik00OTAuNjY3LDk2YzAtMTcuNjczLTE0LjMyNy0zMi0zMi0zMmgtODAuNTU1QzM2NC42MzIsMjUuNzU3LDMyOC41NDksMC4xMywyODgsMGgtNjQgICBjLTQwLjU0OSwwLjEzLTc2LjYzMiwyNS43NTctOTAuMTEyLDY0SDUzLjMzM2MtMTcuNjczLDAtMzIsMTQuMzI3LTMyLDMybDAsMGMwLDE3LjY3MywxNC4zMjcsMzIsMzIsMzJINjR2MjY2LjY2NyAgIEM2NCw0NTkuNDY4LDExNi41MzIsNTEyLDE4MS4zMzMsNTEyaDE0OS4zMzNDMzk1LjQ2OCw1MTIsNDQ4LDQ1OS40NjgsNDQ4LDM5NC42NjdWMTI4aDEwLjY2NyAgIEM0NzYuMzQsMTI4LDQ5MC42NjcsMTEzLjY3Myw0OTAuNjY3LDk2eiBNMzg0LDM5NC42NjdDMzg0LDQyNC4xMjIsMzYwLjEyMiw0NDgsMzMwLjY2Nyw0NDhIMTgxLjMzMyAgIEMxNTEuODc4LDQ0OCwxMjgsNDI0LjEyMiwxMjgsMzk0LjY2N1YxMjhoMjU2VjM5NC42Njd6Ii8+Cgk8cGF0aCBkPSJNMjAyLjY2NywzODRjMTcuNjczLDAsMzItMTQuMzI3LDMyLTMyVjIyNGMwLTE3LjY3My0xNC4zMjctMzItMzItMzJzLTMyLDE0LjMyNy0zMiwzMnYxMjggICBDMTcwLjY2NywzNjkuNjczLDE4NC45OTQsMzg0LDIwMi42NjcsMzg0eiIvPgoJPHBhdGggZD0iTTMwOS4zMzMsMzg0YzE3LjY3MywwLDMyLTE0LjMyNywzMi0zMlYyMjRjMC0xNy42NzMtMTQuMzI3LTMyLTMyLTMycy0zMiwxNC4zMjctMzIsMzJ2MTI4ICAgQzI3Ny4zMzMsMzY5LjY3MywyOTEuNjYsMzg0LDMwOS4zMzMsMzg0eiIvPgo8L2c+CgoKCgoKCgoKCgoKCgoKCjwvc3ZnPgo=";
function WorkItemHeader({
work,
openWork
}) {
return o("div", {
className: "title",
onClick: openWork,
children: [o("h4", {
children: o("strong", {
children: work.getLatest().title
})
}), o("h5", {
children: ["by ", work.getLatest().authors.map((e2) => e2.name).join(" ,")]
})]
});
}
function WorkItemTags({
work
}) {
const [isExpanded, setIsExpanded] = h(false);
function toggleExpanded() {
setIsExpanded(!isExpanded);
}
return o("div", {
className: "tags",
onClick: toggleExpanded,
children: [o("div", {
className: "main",
children: [o("h5", {
children: work.getLatest().ratings.map((e2) => e2.name).join(", ")
}), o("h5", {
children: work.getLatest().fandoms.map((e2) => e2.name).join(", ")
})]
}), o("div", {
className: "expanded",
style: {
display: isExpanded ? "flex" : "none"
},
children: [o("h5", {
children: work.getLatest().warnings.map((e2) => e2.name).join(", ")
}), o("h5", {
children: work.getLatest().relationships.map((e2) => e2.name).join(", ")
}), o("h5", {
children: work.getLatest().characters.map((e2) => e2.name).join(", ")
}), o("h5", {
children: work.getLatest().tags.map((e2) => e2.name).join(", ")
})]
})]
});
}
function WorkItemSummary({
work
}) {
const [isExpanded, setIsExpanded] = h(false);
function toggleExpanded() {
setIsExpanded(!isExpanded);
}
return o("div", {
className: "summary",
onClick: toggleExpanded,
children: [o("h5", {
children: o("strong", {
children: "Summary:"
})
}), o("p", {
children: isExpanded ? work.getLatest().summary : work.getLatest().summary.split("\n")[0].split(" ").slice(0, 32).join(" ") + "..."
})]
});
}
function WorkList({
works,
openWork,
deleteWork
}) {
const [isOpen, setIsOpen] = h(false);
const [workDelete, setWorkDelete] = h(null);
return o("div", {
className: "workList",
children: [works.map((work) => o("div", {
className: "workItem",
children: [o("div", {
className: "header",
children: [o(WorkItemHeader, {
work,
openWork: () => {
openWork(work);
}
}), o("div", {
className: "trash",
children: o("button", {
class: "button",
onClick: () => {
setWorkDelete(work);
setIsOpen(true);
},
children: o("img", {
class: "icons",
src: trashIcon,
alt: "refresh"
})
})
})]
}), o(WorkItemTags, {
work
}), o(WorkItemSummary, {
work
})]
})), o(Modal, {
isOpen,
children: [o(ModalContents, {
children: [o("h3", {
children: "Delete Work?"
}), o("p", {
children: ["This work and its version history will be deleted. ", o("br", {}), "This action is irreversible! "]
})]
}), o(ModalActions, {
children: [o(ModalButton, {
onClick: () => {
deleteWork(workDelete);
setIsOpen(false);
},
children: "Delete"
}), o(ModalButton, {
onClick: () => setIsOpen(false),
children: "Close"
})]
})]
})]
});
}
function App({
closeApp,
setLoadedWork,
setCurrentApp
}) {
const [works, setWorks] = h([]);
async function refresh() {
await Library.refresh();
setWorks(Library.works);
}
function openWork(work) {
setLoadedWork(work);
setCurrentApp("workDetails");
}
async function deleteWork(work) {
await Library.deleteWork(work.id);
refresh();
}
p(() => {
refresh();
}, []);
return o("div", {
class: "container",
children: [o("div", {
className: "appbar",
children: [o("div", {
className: "left",
children: o("h3", {
children: "AO3 Reader"
})
}), o("div", {
className: "right",
children: o("button", {
class: "button",
onClick: closeApp,
children: o("img", {
class: "icons white",
src: exitIcon,
alt: "refresh"
})
})
})]
}), o("div", {
className: "contents",
children: o(WorkList, {
works,
openWork,
deleteWork
})
}), o("div", {
className: "appbar",
children: [o("div", {
className: "left",
children: o("h3", {
children: "Library"
})
}), o("div", {
className: "right",
children: o("button", {
class: "button",
onClick: refresh,
children: o("img", {
class: "icons white",
src: refreshIcon,
alt: "refresh"
})
})
})]
})]
});
}
const arrowLeftIcon = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJCb2xkIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIj48cGF0aCBkPSJNNC45NDMsNS42MDYsMS4wMjQsOS41MjVhMy41ODUsMy41ODUsMCwwLDAsMCw0Ljk1bDMuOTE5LDMuOTE5YTEuNSwxLjUsMCwxLDAsMi4xMjEtMi4xMjFMNC4yODUsMTMuNDkybDE4LjI1LS4wMjNhMS41LDEuNSwwLDAsMCwxLjUtMS41djBhMS41LDEuNSwwLDAsMC0xLjUtMS41TDQuMywxMC40OTIsNy4wNjQsNy43MjdBMS41LDEuNSwwLDAsMCw0Ljk0Myw1LjYwNloiLz48L3N2Zz4K";
function DetailsApp({
closeApp,
work,
setLoadedEpub,
setCurrentApp
}) {
const [isUpdateButtonDisabled, setIsUpdateButtonDisabled] = h(false);
const [updateButtonText, setUpdateButtonText] = h("Update Work");
p(() => {
}, []);
function back() {
setCurrentApp("");
}
async function openWork() {
setLoadedEpub(await work.getLatest().getEpub());
setCurrentApp("workReader");
}
async function updateWork() {
setUpdateButtonText("Updating...");
setIsUpdateButtonDisabled(true);
try {
await Library.getWorkUpdate(work);
setUpdateButtonText("Updated!");
setTimeout(() => {
setUpdateButtonText("Update Work");
setIsUpdateButtonDisabled(false);
}, 1e3);
} catch (error) {
setUpdateButtonText("Update Work Failed!");
setIsUpdateButtonDisabled(false);
}
}
return o("div", {
class: "container",
children: [o("div", {
className: "appbar",
children: [o("div", {
className: "left",
children: o("h3", {
children: "AO3 Reader"
})
}), o("div", {
className: "right",
children: o("button", {
class: "button",
onClick: closeApp,
children: o("img", {
class: "icons white",
src: exitIcon,
alt: "refresh"
})
})
})]
}), o("div", {
className: "contents",
children: [o("div", {
className: "workInfo",
children: [o("h4", {
children: o("strong", {
children: "Summary:"
})
}), o("p", {
children: work.getLatest().summary
}), o("p", {
children: [o("strong", {
children: "Last Updated: "
}), work.getLatest().updated.toString()]
})]
}), o("div", {
className: "workDetails",
children: [o("div", {
className: "headers",
children: [o("h4", {
children: o("strong", {
children: work.getLatest().title
})
}), o("h5", {
children: ["by ", work.getLatest().authors.map((e2) => e2.name).join(" ,")]
})]
}), o("div", {
className: "actions",
children: o("div", {
className: "buttons",
children: [o("div", {
className: "row",
children: [o("a", {
href: "#",
onClick: openWork,
children: "Read in Reader"
}), o("a", {
href: `/works/${work.getLatest().id}`,
target: "_blank",
children: "Read in AO3"
})]
}), o("div", {
className: "row",
children: o("a", {
href: "#",
onClick: isUpdateButtonDisabled ? () => {
} : updateWork,
children: updateButtonText
})
})]
})
}), o("div", {
className: "info"
})]
})]
}), o("div", {
className: "appbar",
children: [o("div", {
className: "left",
children: o("h3", {
children: "Details"
})
}), o("div", {
className: "right",
children: o("button", {
class: "button",
onClick: back,
children: o("img", {
class: "icons white",
src: arrowLeftIcon,
alt: "back"
})
})
})]
})]
});
}
const angleRightIcon = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJCb2xkIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIj48cGF0aCBkPSJNNi4wNzksMjIuNWExLjUsMS41LDAsMCwxLC40NC0xLjA2bDcuNjcyLTcuNjcyYTIuNSwyLjUsMCwwLDAsMC0zLjUzNkw2LjUyOSwyLjU2NUExLjUsMS41LDAsMCwxLDguNjUuNDQ0bDcuNjYyLDcuNjYxYTUuNTA2LDUuNTA2LDAsMCwxLDAsNy43NzlMOC42NCwyMy41NTZBMS41LDEuNSwwLDAsMSw2LjA3OSwyMi41WiIvPjwvc3ZnPgo=";
const angleLeftIcon = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGlkPSJCb2xkIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIj48cGF0aCBkPSJNMTcuOTIxLDEuNTA1YTEuNSwxLjUsMCwwLDEtLjQ0LDEuMDZMOS44MDksMTAuMjM3YTIuNSwyLjUsMCwwLDAsMCwzLjUzNmw3LjY2Miw3LjY2MmExLjUsMS41LDAsMCwxLTIuMTIxLDIuMTIxTDcuNjg4LDE1LjlhNS41MDYsNS41MDYsMCwwLDEsMC03Ljc3OUwxNS4zNi40NDRhMS41LDEuNSwwLDAsMSwyLjU2MSwxLjA2MVoiLz48L3N2Zz4K";
const getChapter = (href, rendition) => {
href = href.replace(" ", "%20");
function flatten(items) {
return [].concat.apply([], items.map((item) => [].concat.apply([item], flatten(item.subitems))));
}
console.log(href, rendition);
var matches = flatten(rendition.book.navigation.toc).filter((item) => rendition.book.canonical(item.href) == rendition.book.canonical(href));
if (matches.length != 1) {
console.log("Unable to get chapter title", event, matches);
return null;
}
return matches[0].label.trim();
};
const EpubViewer = ({
epub,
location,
onLocationChange,
getRendition
}) => {
async function initReader() {
const bookData = await epub.arrayBuffer();
console.log("Epub Loaded, byte length: ", bookData.byteLength);
const book = ePub(bookData);
console.log("Initializing epub viewer");
const rendition = book.renderTo("a3r-epub-viewer", {
width: "100%",
height: "100%"
});
_unsafeWindow.epubrendition = rendition;
rendition.display();
console.log("Epub Displayed");
if (location) {
rendition.display(location);
}
getRendition(rendition);
rendition.on("relocated", (location2) => {
onLocationChange(location2);
});
return () => {
book.destroy();
_unsafeWindow.epubrendition = null;
};
}
p(() => {
initReader();
}, []);
return o("div", {
id: "a3r-epub-viewer",
style: {
width: "100%",
height: "100%"
}
});
};
function ReaderApp({
closeApp,
work,
epub,
setCurrentApp
}) {
const [chapterTitle, setChapterTitle] = h("");
const [epubLocation, setEpubLocation] = h(0);
const renditionRef = _$1(null);
p(() => {
return () => {
};
}, []);
function back() {
setCurrentApp("workDetails");
}
function updateLocation(location) {
work.setReadingLocation(location);
setEpubLocation(location);
}
function nextPage() {
renditionRef.current.next();
}
function prevPage() {
renditionRef.current.prev();
}
function loadRendition(rendition) {
renditionRef.current = rendition;
renditionRef.current.on("relocated", (location) => {
const chapter = getChapter(location.start.href, renditionRef.current) || "";
setChapterTitle(chapter);
});
}
return o("div", {
class: "container",
children: [o("div", {
className: "contents",
children: o(EpubViewer, {
epub,
location: epubLocation,
onLocationChange: updateLocation,
getRendition: loadRendition
})
}), o("div", {
className: "appbar",
children: [o("div", {
className: "left",
children: o("h4", {
children: chapterTitle
})
}), o("div", {
className: "right",
children: [o("button", {
class: "button",
onClick: back,
children: o("img", {
class: "icons white",
src: arrowLeftIcon,
alt: "back"
})
}), o("button", {
class: "button",
onClick: prevPage,
children: o("img", {
class: "icons white",
src: angleLeftIcon,
alt: "back"
})
}), o("button", {
class: "button",
onClick: nextPage,
children: o("img", {
class: "icons white",
src: angleRightIcon,
alt: "back"
})
})]
})]
})]
});
}
console.log("JSLib Version: ", jszip.version);
const AddButton = ({
workID
}) => {
const [buttonText, setButtonText] = h("Add to Library");
const [isButtonDisabled, setButtonDisabled] = h(false);
const handleClick = async () => {
try {
setButtonDisabled(true);
setButtonText("Adding Work...");
await Library.addWork(workID);
setButtonText("Work Added!");
} catch (error) {
console.log("AO3 Reader Error: ", error, workID);
setButtonText("Failed, Retry?");
setButtonDisabled(false);
} finally {
}
};
p(() => {
if (Library.ids.includes(workID)) {
setButtonText("Work Already in Library");
setButtonDisabled(true);
}
}, []);
return o("button", {
onClick: !isButtonDisabled ? handleClick : void 0,
disabled: isButtonDisabled,
type: "button",
class: "action",
style: {
marginTop: "1em"
},
children: buttonText
});
};
function insertButtons() {
for (const button of document.querySelectorAll("div.a3r-actions")) {
button.remove();
}
for (const work of document.querySelectorAll("li.blurb.group")) {
const id = work.querySelector("h4.heading > a").attributes.href.value.split("/")[2];
const header = work.querySelector("div.header.module");
const buttonDiv = document.createElement("div");
buttonDiv.className = "a3r-actions";
header.append(buttonDiv);
preact.render(o(AddButton, {
workID: id
}), buttonDiv);
}
}
const AppController = ({
closeApp
}) => {
const [currentApp, setCurrentApp] = h("");
const [loadedWork, setLoadedWork] = h(null);
const [loadedEpub, setLoadedEpub] = h(null);
switch (currentApp) {
case "workDetails":
if (!loadedWork) {
setCurrentApp("");
}
return o(DetailsApp, {
closeApp,
work: loadedWork,
setLoadedEpub,
setCurrentApp
});
case "workReader":
if (!loadedEpub) {
setCurrentApp("workDetails");
}
return o(ReaderApp, {
closeApp,
work: loadedWork,
epub: loadedEpub,
setCurrentApp
});
default:
return o(App, {
closeApp,
setLoadedWork,
setCurrentApp
});
}
};
const AppWrapper = () => {
const [isAppVisible, setIsAppVisible] = h(false);
const openApp = () => {
document.body.style.overflow = "hidden";
document.body.style.overscrollBehavior = "none";
document.querySelector("html").style.overscrollBehavior = "none";
setIsAppVisible(true);
};
const closeApp = () => {
document.body.style.overflow = "visible";
document.body.style.overscrollBehavior = "";
document.querySelector("html").style.overscrollBehavior = "";
insertButtons();
setIsAppVisible(false);
};
return o(preact.Fragment, {
children: [o("a", {
href: "#",
onClick: openApp,
children: "AO3 Reader"
}), o("div", {
className: "a3r",
style: {
display: isAppVisible ? "block" : "none"
},
children: o(AppController, {
closeApp
})
})]
});
};
try {
console.log("=== AO3 Reader Starting! ===");
_unsafeWindow.library = Library;
(await Library.refresh());
} catch (error) {
console.log("AO3 Reader Error: ", error);
} finally {
insertButtons();
(_a = document.querySelector(".a3r-open")) == null ? void 0 : _a.remove();
const nav = document.querySelector("ul.primary.navigation.actions");
const navItem = document.createElement("li");
navItem.className = "a3r-open dropdown";
nav.append(navItem);
preact.render(o(AppWrapper, {}), navItem);
}
})(preact, unsafeWindow.ePub, unsafeWindow.JSZip);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment