Skip to content
Permalink
 
 
Cannot retrieve contributors at this time
import * as Sentry from "@sentry/browser"
class KFWProxy {
constructor(base = "https://kfwproxy.geek1011.net") {
this.base = base
}
async latestVersion(id, affiliate = "kobo") {
let version = "0.0"
if (id == "00000000-0000-0000-0000-000000000381")
version = "4.7.10364" // required to receive the next update
if (id == "00000000-0000-0000-0000-000000000387")
version = "4.28.17826" // required to receive the next update (17826 -> 17925)
return await this.UpgradeCheck(id, affiliate, version)
}
async UpgradeCheck(id, affiliate = "kobo", version = "0.0", serial = "N0") {
return await this.KoboAPI(`1.0/UpgradeCheck/Device/${id}/${affiliate}/${version}/${serial}`, true, KFWProxy.transformUpgrade)
}
static transformUpgrade(obj) {
obj.UpgradeVersion = KFWProxy.#transformUpgradeVersion(obj.UpgradeURL)
obj.UpgradeDate = KFWProxy.#transformUpgradeDate(obj.UpgradeURL, obj.UpgradeVersion)
if (false) // debug
if (Math.random() > .5)
obj.UpgradeVersion = "1.2.3"
return obj
}
static #transformUpgradeVersion(url) {
// fallback
if (!url)
return "0.0"
// overrides
url = url.split("?")[0]
if (url.endsWith("/kobo7/April2018/kobo-update-4.8.zip")) return "4.8.10956"
if (url.endsWith("/kobo7/May2018/kobo-update-4.8.zip")) return "4.8.11090"
if (url.endsWith("/kobo4/May2018/kobo-update-4.9.11311.zip")) return "4.9.11314"
if (url.endsWith("/kobo5/May2018/kobo-update-4.9.11311.zip")) return "4.9.11314"
if (url.endsWith("/kobo6/Aug2018/kobo-update-4.10.zip")) return "4.10.11591"
if (url.endsWith("/kobo7/Aug2018/kobo-update-4.10.zip")) return "4.10.11591"
// filename
const m = url.match(/\/kobo-update-([0-9.]+(?:-s)?)(?:-TF[0-9]+|-TouchFW-[0-9]+)?\.zip/)
if (!m || m.length != 2)
throw new Error(`Failed to extract version from "${url}"`)
return m[1]
}
static #transformUpgradeDate(url, version) {
// fallback
if (!url || !version || version == "0.0")
return "Unknown"
// overrides
switch (version) {
case "3.4.1": return "May 2014"
case "4.6.9960": return "October 2017"
case "4.6.10075": return "November 2017"
case "4.9.11311": return "June 2018"
case "4.9.11314": return "June 2018"
case "4.28.17623": return "June 2021" // it was indeed built in May as it is in the URL, but we don't want to confuse people
}
// date folder
const m = url.match(/\/firmwares\/kobo[0-9]\/(.+?)\//)
if (!m || m.length != 2)
throw new Error(`Failed to extract date from "${url}" and "${version}"`)
// month expansion
let [month, year] = m[1].replace(/([0-9]+)/, " $1").split(" ")
switch ((month = month.charAt(0).toUpperCase() + month.slice(1))) {
case "Jan": month = "January"; break
case "Feb": month = "February"; break
case "Mar": month = "March"; break
case "Apr": month = "April"; break
case "Jun": month = "June"; break
case "Jul": month = "July"; break
case "Aug": month = "August"; break
case "Sep": month = "September"; break
case "Sept": month = "September"; break
case "Oct": month = "October"; break
case "Nov": month = "November"; break
case "Dec": month = "December"; break
case "Rakuten-": month = "Unknown"; break
}
return [month, year].join(" ").trim()
}
// -1[a<b] 0[a=b] 1[a>b]
static versionCompare(a, b) {
// fallback
if (!a) return -1;
if (!b) return 1;
// April 2019 -s version suffix.
if (a.endsWith("-s") && b == a.substring(0, a.length - 2)) return 1
if (b.endsWith("-s") && a == b.substring(0, b.length - 2)) return -1
// version parts
const as = a.split(".").map(x => parseInt(x, 10))
const bs = b.split(".").map(x => parseInt(x, 10))
for (let i = 0; i < Math.max(as.length, bs.length); i++) {
if (as[i] == null) return bs[i] == 0 ? 0 : -1
if (bs[i] == null) return as[i] == 0 ? 0 : 1
if (as[i] < bs[i]) return -1
if (as[i] > bs[i]) return 1
}
return 0
}
async ReleaseNotes(n = 1) {
if (typeof n === "string") {
const m = n.match(/\/1\.0\/ReleaseNotes\/([0-9]+)/)
if (!m || m.length != 2)
throw new Error(`Failed to extract release note number from "${n}"`)
n = m[1]
}
return await this.KoboAPI(`1.0/ReleaseNotes/${n}`, false, KFWProxy.transformNotes)
}
static transformNotes(html) {
const css = `p{margin:0}`
try {
const doc = new DOMParser().parseFromString(html, "text/html")
doc.head.appendChild(doc.createElement("style")).textContent = css
return doc.documentElement.innerHTML
} catch (ex) {
// this happens on the Kobo Web Browser for some reason
console.warn(`tranform notes failed ${ex}, falling back to innerHTML method`)
const doc = document.createElement("div")
doc.innerHTML = html
return `<html><head><title></title><style>${css}</style></head><body>${doc.innerHTML}</body></html>`
}
}
async KoboAPI(path, json = false, transform = null) {
return await this.#request(`${this.base}/api.kobobooks.com/${path}`, json, transform)
}
// request batching
#pending = null
#PendingRequest = class {
constructor(url, json = false, transform = null) {
this.url = url
this.transform = transform
this.json = json
this.promise = new Promise((a, b) => {
this.resolve = a
this.reject = b
})
}
}
batch() {
if (!this.#pending)
this.#pending = []
}
endBatch() {
if (this.#pending) {
this.#doBatch(...this.#pending)
this.#pending = null
}
}
async #request(url, json = false, transform = null) {
const req = new this.#PendingRequest(url, json, transform)
if (this.#pending && url.startsWith(`${this.base}/api.kobobooks.com/`)) {
this.#pending.push(req)
} else {
this.#do(req)
}
return req.promise
}
async #do(req) {
const xhr = new XMLHttpRequest()
xhr.addEventListener("load", ev => this.#doResponse(req, xhr.responseText, xhr.status, xhr.statusText, xhr.getResponseHeader("X-KFWProxy-Request-Id")))
xhr.addEventListener("error", ev => req.reject(new Error(`Request to "${req.url}" failed`)))
xhr.addEventListener("abort", ev => req.reject(new Error(`Request to "${req.url}" aborted`)))
xhr.open("GET", req.url)
xhr.send()
return await req.promise
}
async #doBatch(...req) {
if (req.length == 0) {
return
}
const xhr = new XMLHttpRequest()
xhr.addEventListener("load", ev => {
const requestID = xhr.getResponseHeader("X-KFWProxy-Request-Id")
try {
if (xhr.status != 200)
throw `${xhr.status} ${xhr.statusText}`
const rsp = JSON.parse(xhr.responseText)
if (!Array.isArray(rsp))
throw `response not an array`
if (rsp.length != req.length)
throw `found ${rsp.length} responses, but expected ${req.length}`
for (let i = 0; i < req.length; i++)
this.#doResponse(req[i], rsp[i].body, rsp[i].status, "", requestID)
} catch (ex) {
for (const r of req)
r.reject(new Error(`Batch request including "${r.url}" failed: ${ex}`))
Sentry.captureException(new Error(`Batch request failed: ${ex}`), {
extra: {requestID},
})
return
}
})
xhr.addEventListener("error", ev => req.forEach(r => r.reject(new Error(`Batch request including "${r.url}" failed`))))
xhr.addEventListener("abort", ev => req.forEach(r => r.reject(new Error(`Batch request including "${r.url}" aborted`))))
xhr.open("GET", `${this.base}/api.kobobooks.com?h=1&x=${req.map(r => encodeURIComponent(r.url.replace(`${this.base}/api.kobobooks.com/`, ""))).join("&x=")}`)
xhr.send()
return await Promise.all(req.map(r => r.promise))
}
#doResponse(req, body, status, statusText = "", requestID = null) {
if (status != 200) {
req.reject(new Error(`Request to "${req.url}" failed: ${status} ${statusText}`))
Sentry.captureException(new Error(`Request to "${req.url}" failed: ${status} ${statusText}`), {
extra: {requestID},
})
return
}
let obj = body
if (req.json) {
try {
obj = JSON.parse(obj)
} catch (ex) {
req.reject(new Error(`Request to "${req.url}" failed: ${ex}`))
return
}
}
if (req.transform) {
try {
obj = req.transform(obj)
} catch (ex) {
req.reject(new Error(`Request to "${req.url}" failed: transform error: ${ex}`))
return
}
}
req.resolve(obj)
}
}
// so we can load it deferred directly from the webpage
const KoboFirmwareOldVersionsData = window.KoboFirmwareOldVersionsData = (() => {
let resolve, reject
let promise = new Promise((a, b) => {
resolve = a
reject = b
})
let timeout = window.setTimeout(() => {
if ("KoboFirmwareOldVersionsDataRaw" in window) {
resolve(KoboFirmwareOldVersionsDataRaw)
console.warn("async old versions load failed, but still found the data afterwards...")
return
}
reject(new Error("Failed to load old versions data: Timed out"))
console.error("old versions load failed")
Sentry.captureException(new Error("Failed to load old versions data: Timed out"))
}, 10000)
promise.resolve = data => {
window.clearTimeout(timeout)
resolve(data)
}
return promise
})()
class KoboFirmwareDB {
#db
constructor(db = KoboFirmwareOldVersionsData) {
this.#db = db // hardware, id, version, date, download
}
#__db
async #_db() {
if (this.#__db)
return this.#__db
this.#__db = (await this.#db)
.sort(([,aid,aversion,,], [,bid,bversion,,]) => {
const a = KFWProxy.versionCompare(aversion, bversion)
return a == 0 ? aid.localeCompare(bid) : a
})
return this.#__db
}
async versionsForDevice(id) {
return (await this.#_db())
.filter(([,did,,,]) => did == id)
.reverse() // #_db sorts it asc, we want it desc
.map(([,,version,date,download]) => ({version, date, download}))
}
async versionsByDevice() {
let tmp = (await this.#_db())
.reduce((acc, [,id,version,date,]) => {
(acc[id] = acc[id] || {})[version] = date
return acc
}, {})
for (const id in tmp) {
let earliest
for (const version in tmp[id])
if (!earliest || KFWProxy.versionCompare(earliest, version) == 1)
earliest = version
tmp[id].earliest = earliest
}
return tmp
}
async versions() {
return Object.keys((await this.#_db())
.reduce((acc, [,,version,,]) => {
acc[version] = null
return acc
}, {}))
.reverse() // #_db sorts it asc, we want it desc
}
async downloadsByVersion() {
let tmp = []
let cur, curv
let devs = {}, devsh = {}
for (const [hardware,id,version,date,download] of await this.#_db()) {
if (curv != version) {
if (cur)
tmp.push(cur)
cur = {version, date, for: {}, download: {[hardware]: download}}
curv = version
}
if (hardware in cur.download) {
if (cur.download[hardware] != download)
cur.download[hardware] = "varies" // maybe make this add all by device id if needed later?
} else {
cur.download[hardware] = download
}
cur.for[id] = true
if (!(id in devs)) {
{devs[id] = hardware}
(devsh[hardware] = devsh[hardware] || {})[id] = null
}
}
tmp.push(cur)
for (let i = 0; i < tmp.length; i++) {
tmp[i].notfor = []
for (const hardware in tmp[i].download)
for (const id in devsh[hardware])
if (!(id in tmp[i].for))
tmp[i].notfor.push(id)
tmp[i].for = Object.keys(tmp[i].for)
}
return tmp
}
}
class KoboFirmware {
#kfw
#db
#affiliates
#devices
get debug() {
return {
KFWProxy, KoboFirmwareDB, KoboFirmwareOldVersionsData, KoboFirmware,
kfw: this.#kfw,
db: this.#db,
affiliates: this.#affiliates,
devices: this.#devices,
req: this.#req,
}
}
constructor(
kfw = new KFWProxy(),
db = new KoboFirmwareDB(),
affiliates = [
"kobo",
"bestbuyca",
"fnac",
"beta",
"rakutenbooks",
"walmartca",
],
devices = [
["kobo3", "00000000-0000-0000-0000-000000000310", "Kobo Touch A/B"],
["kobo4", "00000000-0000-0000-0000-000000000320", "Kobo Touch C"],
["kobo4", "00000000-0000-0000-0000-000000000340", "Kobo Mini"],
["kobo4", "00000000-0000-0000-0000-000000000330", "Kobo Glo"],
["kobo6", "00000000-0000-0000-0000-000000000371", "Kobo Glo HD"],
["kobo6", "00000000-0000-0000-0000-000000000372", "Kobo Touch 2.0"],
["kobo5", "00000000-0000-0000-0000-000000000360", "Kobo Aura"],
["kobo4", "00000000-0000-0000-0000-000000000350", "Kobo Aura HD"],
["kobo5", "00000000-0000-0000-0000-000000000370", "Kobo Aura H2O"],
["kobo6", "00000000-0000-0000-0000-000000000374", "Kobo Aura H2O Edition 2 v1"],
["kobo7", "00000000-0000-0000-0000-000000000378", "Kobo Aura H2O Edition 2 v2"],
["kobo6", "00000000-0000-0000-0000-000000000373", "Kobo Aura ONE"],
["kobo6", "00000000-0000-0000-0000-000000000381", "Kobo Aura ONE Limited Edition"],
["kobo6", "00000000-0000-0000-0000-000000000375", "Kobo Aura Edition 2 v1"],
["kobo7", "00000000-0000-0000-0000-000000000379", "Kobo Aura Edition 2 v2"],
["kobo7", "00000000-0000-0000-0000-000000000382", "Kobo Nia"],
["kobo7", "00000000-0000-0000-0000-000000000376", "Kobo Clara HD"],
["kobo7", "00000000-0000-0000-0000-000000000380", "Kobo Forma"],
["kobo7", "00000000-0000-0000-0000-000000000384", "Kobo Libra H2O"],
["kobo8", "00000000-0000-0000-0000-000000000387", "Kobo Elipsa"],
["kobo8", "00000000-0000-0000-0000-000000000383", "Kobo Sage"],
["kobo9", "00000000-0000-0000-0000-000000000388", "Kobo Libra 2"],
],
) {
this.#kfw = kfw
this.#db = db
this.#affiliates = affiliates
this.#devices = []
for (const device of devices) {
if (!Array.isArray(device) || device.length != 3)
throw new TypeError(`Invalid device ${JSON.stringify(device)}: incorrect length`)
const [hardware, id, name] = device
if (!hardware || typeof hardware !== "string" || !hardware.startsWith("kobo"))
throw new TypeError(`Invalid device ${JSON.stringify(device)}: bad hardware`)
if (!id || typeof id !== "string" || id.length != 36) {
throw new TypeError(`Invalid device ${JSON.stringify(device)}: bad id`) }
if (!name || typeof name !== "string" || name.length == 0)
throw new TypeError(`Invalid device ${JSON.stringify(device)}: bad device`)
this.#devices.push({hardware, id, name})
}
this.#load()
}
#req // [device][affiliate]Promise<UpgradeCheckResult>
#load() {
this.#req = {}
for (const device of this.#devices) {
this.#kfw.batch()
this.#req[device.id] = {}
for (const affiliate of this.#affiliates)
this.#req[device.id][affiliate] = this.#kfw.latestVersion(device.id, affiliate) // note that a Promise's result/rejection can be used multiple times
this.#kfw.endBatch()
}
}
#ctr(link, ...x) {
if (window.goatcounter) {
const fn = () => {
for (const [evt, name] of x) {
const ct = window.goatcounter.url({
event: true,
path: `kfw-${evt}`,
title: `${name}`,
})
if (!ct)
continue
navigator.sendBeacon(ct)
}
}
link.addEventListener("click", fn, false)
link.addEventListener("auxclick", fn, false)
}
}
async renderLatest(table) {
const ths = document.createDocumentFragment()
KoboFirmware.#el(ths, "th", "Model", ["kfw-latest__model"])
KoboFirmware.#el(ths, "th", "Hardware", ["kfw-latest__hardware"])
KoboFirmware.#el(ths, "th", "Version", ["kfw-latest__version"])
KoboFirmware.#el(ths, "th", "Date", ["kfw-latest__date"])
KoboFirmware.#el(ths, "th", "Links", ["kfw-latest__links"])
table.querySelector("thead tr").appendChild(ths)
// load the initial data
const trm = {}
const trs = document.createDocumentFragment()
for (const device of this.#devices) {
const tr = KoboFirmware.#el(trs, "tr")
trm[device.id] = {
model: KoboFirmware.#el(tr, "td", device.name, ["kfw-latest__model"], {title: `ID: ${device.id}`}),
hardware: KoboFirmware.#el(tr, "td", device.hardware, ["kfw-latest__hardware"]),
version: KoboFirmware.#el(tr, "td", "Loading", ["kfw-latest__version"]),
date: KoboFirmware.#el(tr, "td", null, ["kfw-latest__date"]),
linksw: KoboFirmware.#el(tr, "td", null, ["kfw-latest__links"]),
}
const linksw1 = KoboFirmware.#el(trm[device.id].linksw, "span", null, [], {style: "display: inline-block; vertical-align: top;"}) // to control wrapping
const linksw2 = KoboFirmware.#el(trm[device.id].linksw, "span", null, [], {style: "display: inline-block; vertical-align: top;"}) // to control wrapping
trm[device.id].links = {
download: KoboFirmware.#el(linksw1, "a", "Download", ["kfw-latest__links__download"], {style: "display: none;", rel: "noopener"}),
notes: KoboFirmware.#el(linksw1, "a", "Notes", ["kfw-latest__links__notes"], {style: "display: none;", rel: "noopener", target: "_blank"}),
affiliates: KoboFirmware.#el(linksw2, "button", "Other Affiliates", ["kfw-latest__links__affiliates"], {style: "display: none;"}),
versions: KoboFirmware.#el(linksw2, "button", "Other Versions", ["kfw-latest__links__versions"], {}),
}
trm[device.id].links.versions.addEventListener("click", ev => {
KoboFirmware.#modal(`Other versions for ${device.name}`, async () => {
const frag = document.createDocumentFragment()
for (const version of await this.#db.versionsForDevice(device.id)) {
if (!version.download.includes(device.hardware))
console.warn("possible hardware mismatch", device, version)
const el = KoboFirmware.#el(frag, "div", `${version.version} - ${version.date} - <a href="${version.download}" rel="noopener">Download</a> - ${device.hardware}`, [], {}, true)
// stats
this.#ctr(el.querySelector("a"),
[`dl`, "Firmware"],
[`dl-version-${version.version}`, `Firmware ${version.version}`],
[`dl-device-${device.id.replace(/^[0-]+/, "")}`, `${device.hardware} / ${device.name}`],
)
}
return frag
})
ev.preventDefault()
})
}
table.querySelector("tbody").appendChild(trs)
// load each row async as data becomes available
await Promise.all(this.#devices.map(device => (async device => {
try {
// load the latest update info and the affiliates which have it
let latest, latests = []
for (const affiliate of this.#affiliates) {
let info
try {
info = await this.#req[device.id][affiliate]
} catch (ex) {
// retry once more
this.#req[device.id][affiliate] = this.#kfw.latestVersion(device.id, affiliate)
info = await this.#req[device.id][affiliate]
}
switch (KFWProxy.versionCompare(latest?.UpgradeVersion, info.UpgradeVersion)) {
case -1: // !latest || latest < info
latest = info
latests = [affiliate]
break
case 0: // latest == info
latests.push(affiliate)
break
}
}
// table cells
trm[device.id].version.textContent = latest.UpgradeVersion
trm[device.id].date.textContent = latest.UpgradeDate
trm[device.id].version.setAttribute("title", `Affiliates with version: ${latests.join(", ")}`)
// download
trm[device.id].links.download.href = latest.UpgradeURL
trm[device.id].links.download.style.removeProperty("display")
// release notes
trm[device.id].links.notes.href = latest.ReleaseNoteURL
trm[device.id].links.notes.addEventListener("click", ev => {
if (navigator.userAgent.includes("MSIE") || navigator.userAgent.includes("Trident/")) {
return // doesn't support srcdoc
}
KoboFirmware.#modal(`Release notes for ${latest.UpgradeVersion}`, async () => {
const content = await this.#kfw.ReleaseNotes(latest.ReleaseNoteURL)
const frame = document.createElement("iframe")
frame.srcdoc = content
frame.setAttribute("sandbox", "")
return frame
})
ev.preventDefault()
});
trm[device.id].links.notes.style.removeProperty("display")
// affiliates
trm[device.id].links.affiliates.addEventListener("click", ev => {
KoboFirmware.#modal(`Versions of other affiliates for ${device.name}`, async () => {
const frag = document.createDocumentFragment()
for (const affiliate in this.#req[device.id]) {
const info = await this.#req[device.id][affiliate]
const a = KoboFirmware.#el(frag, "div", `${affiliate} - ${info.UpgradeVersion}${info.UpgradeURL ? ` - <a href="${info.UpgradeURL}" rel="noopener">Download</a>` : ``}`, [], {}, true)
// stats
this.#ctr(a,
[`dl`, "Firmware"],
[`dl-version-${info.UpgradeVersion}`, `Firmware ${info.UpgradeVersion}`],
[`dl-device-${device.id.replace(/^[0-]+/, "")}`, `${device.hardware} / ${device.name}`],
)
}
return frag
}, ["thin"])
ev.preventDefault()
});
trm[device.id].links.affiliates.style.removeProperty("display")
// stats
this.#ctr(trm[device.id].links.download,
[`dl`, "Firmware"],
[`dl-version-${latest.UpgradeVersion}`, `Firmware ${latest.UpgradeVersion}`],
[`dl-device-${device.id.replace(/^[0-]+/, "")}`, `${device.hardware} / ${device.name}`],
)
} catch (ex) {
trm[device.id].version.textContent = "Error"
trm[device.id].version.setAttribute("title", `Error: ${ex}`)
Sentry.captureException(new Error(`Table row load failed for "${device.name}" in latest versions table: ${ex}`))
}
})(device)))
}
async renderMatrix(table) {
// this one is built horizontally
let rows = {}
const tb = table.querySelector("tbody")
rows.header = table.querySelector("thead tr")
// add a column for the device names
KoboFirmware.#el(rows.header, "th", "Device", ["kfw-matrix__device"])
for (const device of this.#devices) {
rows[device.id] = KoboFirmware.#el(tb, "tr")
KoboFirmware.#el(rows[device.id], "td", device.name, ["kfw-matrix__device"])
}
// add the version columns all at once (since there are a lot of them)
let cols = {}
const availability = await this.#db.versionsByDevice()
for (const version of await this.#db.versions()) {
KoboFirmware.#el((cols.header = cols.header || document.createDocumentFragment()), "th", version, ["kfw-matrix__version"])
for (const device of this.#devices) {
const date = availability[device.id][version]
let t, c, a
if (date) {
t = "✓"
c = "kfw-matrix__version--yes"
a = {title: date}
} else if (KFWProxy.versionCompare(availability[device.id].earliest, version) == 1) {
// TODO: make this check more efficient by doing it separately going through the versions backwards
t = "-"
c = "kfw-matrix__version--none"
a = {}
} else {
t = "✗"
c = "kfw-matrix__version--no"
a = {}
}
KoboFirmware.#el((cols[device.id] = cols[device.id] || document.createDocumentFragment()), "td", t, ["kfw-matrix__version", c], a)
}
}
for (const row in cols)
rows[row].appendChild(cols[row])
// add a column for the device names
KoboFirmware.#el(rows.header, "th", "Device", ["kfw-matrix__device"])
for (const device of this.#devices)
KoboFirmware.#el(rows[device.id], "td", device.name, ["kfw-matrix__device"])
}
async renderAffiliates(table) {
const ths = document.createDocumentFragment()
KoboFirmware.#el(ths, "th", "Device", ["kfw-affiliates__device"])
for (const affiliate of this.#affiliates)
KoboFirmware.#el(ths, "th", affiliate, ["kfw-affiliates__affiliate"])
table.querySelector("thead tr").appendChild(ths)
// load the initial data
const trm = {}
const trs = document.createDocumentFragment()
for (const device of this.#devices) {
const tr = KoboFirmware.#el(trs, "tr")
trm[device.id] = {}
KoboFirmware.#el(tr, "td", device.name, ["kfw-affiliates__device"])
for (const affiliate of this.#affiliates)
trm[device.id][affiliate] = KoboFirmware.#el(tr, "td", "Loading", ["kfw-affiliates__affiliate"])
}
table.querySelector("tbody").appendChild(trs)
// load each cell async as data becomes available (we use traditional
// promise chaining here to improve performance by reducing the number
// of pending async functions at once)
return Promise.all(this.#devices.map(device =>
Promise.all(this.#affiliates.map(affiliate =>
this.#req[device.id][affiliate]
.then(obj => {
trm[device.id][affiliate].textContent = obj.UpgradeVersion == "0.0"
? "-"
: obj.UpgradeVersion
return Promise.resolve([affiliate, obj])
}).catch(ex => {
trm[device.id][affiliate].textContent = "Error"
trm[device.id][affiliate].title = `Error: ${ex}`
return Promise.reject(ex)
})
)).then(affiliates => {
let latest
for (const [, info] of affiliates)
if (KFWProxy.versionCompare(latest?.UpgradeVersion, info.UpgradeVersion) == -1)
latest = info
for (const [affiliate, info] of affiliates)
trm[device.id][affiliate].classList.add(
latest.UpgradeVersion == info.UpgradeVersion
? "kfw-affiliates__affiliate--latest"
: info.UpgradeVersion == "0.0"
? "kfw-affiliates__affiliate--none"
: "kfw-affiliates__affiliate--outdated")
})
))
}
async renderVersions(table) {
let hardware = {}
let name = {}
let idhardware = {}
for (const device of this.#devices) {
hardware[device.hardware] = null
name[device.id] = device.name
idhardware[device.id] = device.hardware
}
hardware = Object.keys(hardware).sort((a, b) => a.localeCompare(b))
const ths = document.createDocumentFragment()
KoboFirmware.#el(ths, "th", "Date", ["kfw-versions__date"])
KoboFirmware.#el(ths, "th", "Version", ["kfw-versions__version"])
for (const hw of hardware)
KoboFirmware.#el(ths, "th", hw, ["kfw-versions__hardware"])
KoboFirmware.#el(ths, "th", "Notes", ["kfw-versions__notes"])
table.querySelector("thead tr").appendChild(ths)
const rows = document.createDocumentFragment()
for (const version of await this.#db.downloadsByVersion()) {
const row = KoboFirmware.#el(rows, "tr")
KoboFirmware.#el(row, "td", version.date, ["kfw-versions__date"])
KoboFirmware.#el(row, "td", version.version, ["kfw-versions__version"])
for (const hw of hardware) {
const td = KoboFirmware.#el(row, "td", version.download[hw] ? "" : "-", ["kfw-versions__hardware"])
if (version.download[hw]) {
const a = KoboFirmware.#el(td, "a", "Download", [], {rel: "noopener", href: version.download[hw], title: KoboFirmware.#listify(version.for.filter(id => idhardware[id] == hw).map(id => name[id].replace(/Kobo /, "")))})
// stats
this.#ctr(a,
[`dl`, "Firmware"],
[`dl-version-${version.version}`, `Firmware ${version.version}`],
)
}
}
const x = version.notfor.length
? version.for.length > version.notfor.length
? `Not for ${KoboFirmware.#listify(version.notfor.map(id => name[id].replace(/Kobo /, "")))}.`
: `Only for ${KoboFirmware.#listify(version.for.map(id => name[id].replace(/Kobo /, "")))}.`
: ""
KoboFirmware.#el(row, "td", x, ["kfw-versions__notes"])
}
table.querySelector("tbody").appendChild(rows)
}
static #listify(arr) {
if (!arr || arr.length == 0)
return ""
if (arr.length == 1)
return arr[0].toString()
if (arr.length == 2)
return arr.join(" and ")
return `${arr.slice(0, arr.length-1).join(", ")}, and ${arr[arr.length-1]}`
}
static #modal(title, fn, classes = []) {
const modal = KoboFirmware.#el(null, "aside", null, ["modal-wrapper"])
const inner = KoboFirmware.#el(modal, "div", null, ["modal", ...classes])
const bar = KoboFirmware.#el(inner, "div", null, ["titlebar"])
const close = KoboFirmware.#el(bar, "button", "Close", ["close"])
const text = KoboFirmware.#el(bar, "div", title, ["title"])
const body = KoboFirmware.#el(inner, "div", "Loading", ["body"])
const cfn = ev => {
if (ev) {
if (ev.target != close && ev.target != modal && ev.keyCode != 27) {
return // !close && !back && !esc
}
ev.preventDefault()
}
document.body.removeChild(modal)
document.body.removeEventListener("keydown", cfn, true)
}
modal.addEventListener("click", cfn, true)
document.body.addEventListener("keydown", cfn, true)
document.body.appendChild(modal)
{(async () => {
try {
let res = fn()
if (res instanceof Promise)
res = await res
if (typeof res === "string") {
body.innerHTML = res
} else if (res instanceof HTMLIFrameElement) {
res.classList.add("body")
res.style.border = "none"
res.style.width = "100%"
res.style.height = "100%"
res.style.padding = 0
inner.removeChild(body)
inner.appendChild(res)
} else {
body.innerHTML = ""
body.appendChild(res)
}
} catch (ex) {
body.innerHTML = `Error: Load failed: ${ex}`
console.error("modal load failed", ex)
Sentry.captureException(new Error(`Modal load failed for "${title}": ${ex}`))
}
})()}
return {title: text, body, close: () => cfn(null)}
}
static #el(parent, tag, inner = null, classes = [], attrs = {}, raw = false) {
const el = document.createElement(tag)
for (const c of classes)
el.classList.add(c)
for (const a in attrs)
el.setAttribute(a, attrs[a])
if (inner)
el[raw ? "innerHTML" : "textContent"] = inner.toString()
if (parent)
parent.appendChild(el)
return el
}
}
Sentry.init({
dsn: "https://696a706ba440443eb3e19094ebd72f74@o143001.ingest.sentry.io/1104793",
maxBreadcrumbs: 100,
attachStacktrace: true,
})
const app = window.app = new KoboFirmware()
app.renderLatest(document.getElementById("kfw-latest"))
.catch(ex => Sentry.captureException(new Error(`Table load failed for #kfw-latest`)))
app.renderMatrix(document.getElementById("kfw-matrix"))
.catch(ex => Sentry.captureException(new Error(`Table load failed for #kfw-matrix`)))
app.renderAffiliates(document.getElementById("kfw-affiliates"))
.catch(ex => Sentry.captureException(new Error(`Table load failed for #kfw-affiliates`)))
app.renderVersions(document.getElementById("kfw-versions"))
.catch(ex => Sentry.captureException(new Error(`Table load failed for #kfw-versions`)))
document.getElementById("error").className = "error hidden";