diff options
author | RaindropsSys <raindrops@equestria.dev> | 2024-06-13 15:46:03 +0200 |
---|---|---|
committer | RaindropsSys <raindrops@equestria.dev> | 2024-06-13 15:46:03 +0200 |
commit | e44e2fe070484e06d384a31ef2699c3a2d5d474e (patch) | |
tree | 2d5eb5d1b01646270d18cf1f2d94519966d6e7de /src | |
download | faunerie-e44e2fe070484e06d384a31ef2699c3a2d5d474e.tar.gz faunerie-e44e2fe070484e06d384a31ef2699c3a2d5d474e.tar.bz2 faunerie-e44e2fe070484e06d384a31ef2699c3a2d5d474e.zip |
GitHub migration
Diffstat (limited to 'src')
-rwxr-xr-x | src/PrisbeamAI.ts | 224 | ||||
-rwxr-xr-x | src/PrisbeamActions.ts | 13 | ||||
-rwxr-xr-x | src/PrisbeamApp.ts | 129 | ||||
-rwxr-xr-x | src/PrisbeamAppDisplay.ts | 455 | ||||
-rwxr-xr-x | src/PrisbeamDataStore.ts | 47 | ||||
-rwxr-xr-x | src/PrisbeamDerpibooru.ts | 293 | ||||
-rwxr-xr-x | src/PrisbeamLoader.ts | 261 | ||||
-rwxr-xr-x | src/PrisbeamSearch.ts | 130 | ||||
-rwxr-xr-x | src/PrisbeamSettings.ts | 290 | ||||
-rwxr-xr-x | src/PrisbeamUtilities.ts | 75 | ||||
-rwxr-xr-x | src/index.ts | 41 | ||||
-rwxr-xr-x | src/tsconfig.json | 12 |
12 files changed, 1970 insertions, 0 deletions
diff --git a/src/PrisbeamAI.ts b/src/PrisbeamAI.ts new file mode 100755 index 0000000..a9fc4d4 --- /dev/null +++ b/src/PrisbeamAI.ts @@ -0,0 +1,224 @@ +import {PrisbeamApp} from "./PrisbeamApp"; +import {ChildProcess} from "node:child_process"; +import {PrisbeamImageType} from "libprisbeam"; +import * as fs from "node:fs"; +import * as cp from "node:child_process"; + +export class PrisbeamAI { + instance: PrisbeamApp; + aiProcess: ChildProcess; + aiStarting: boolean; + + constructor(instance: PrisbeamApp) { + this.instance = instance; + } + + get aiRunning() { + return (!!this.aiProcess && this.aiProcess.exitCode === null) || this.aiStarting; + } + + findPythonExecutable() { + try { + if (process.platform !== "darwin") throw new Error("Not running on macOS"); + cp.execFileSync("/usr/local/bin/python3.11", ["--version"], {cwd: __dirname + "/../ai"}).toString().trim(); + return "/usr/local/bin/python3.11"; + } catch (e) { + try { + cp.execFileSync("py", ["--version"], {cwd: __dirname + "/../ai"}).toString().trim(); + return "py"; + } catch (e) { + try { + cp.execFileSync("python3.11", ["--version"], {cwd: __dirname + "/../ai"}).toString().trim(); + return "python3.11"; + } catch (e) { + try { + cp.execFileSync("python3", ["--version"], {cwd: __dirname + "/../ai"}).toString().trim(); + return "python3"; + } catch (e) { + cp.execFileSync("python", ["--version"], {cwd: __dirname + "/../ai"}).toString().trim(); + return "python"; + } + } + } + } + } + + getPythonVersion() { + return cp.execFileSync(this.findPythonExecutable(), ["--version"], {cwd: __dirname + "/../ai"}).toString().trim(); + } + + updateDependencies() { + return new Promise<void>((res) => { + this.aiProcess = cp.execFile(this.findPythonExecutable(), ["-m", "pip", "install", "-U", "torch", "ultralytics", "Pillow", "requests", "pandas", "opencv-python", "flask", "gitpython", "setuptools>=65.5.1"], {cwd: __dirname + "/../ai"}); + + this.aiProcess.stdout.on('data', (d) => { + console.debug(d.toString()); + }); + + this.aiProcess.stderr.on('data', (d) => { + console.debug(d.toString()); + }); + + this.aiProcess.on('exit', () => { + res(); + }); + }); + } + + triggerEngineStart() { + this.aiProcess = cp.execFile(this.findPythonExecutable(), ["server.py"], {cwd: __dirname + "/../ai"}); + + this.aiProcess.stdout.on('data', (d) => { + console.debug(d.toString()); + }); + + this.aiProcess.stderr.on('data', (d) => { + console.debug(d.toString()); + }); + + this.aiProcess.on('exit', () => { + this.aiProcess = null; + }); + } + + waitForEngine() { + return new Promise<void>((res) => { + let aiInterval = setInterval(async () => { + try { + if ("data" in (await (await fetch("http://127.0.0.1:25091/status")).json())) { + clearInterval(aiInterval); + this.aiStarting = false; + res(); + } + } catch (e) { + } + }, 1000); + }) + } + + validatePythonVersion() { + let pyVersion: string = this.getPythonVersion(); + + if (!pyVersion) { + console.log("Unable to find a Python executable"); + throw new Error("Python not found"); + } else if (!pyVersion.startsWith("Python 3.11.") && pyVersion !== "Python 3.11") { + console.log("Invalid Python version: " + pyVersion); + throw new Error("Python not found"); + } + } + + async startAI() { + if (this.aiRunning) return; + + this.aiStarting = true; + this.validatePythonVersion(); + await this.updateDependencies(); + this.triggerEngineStart(); + await this.waitForEngine(); + } + + async getClasses() { + let _dataStore = this.instance.dataStore; + let protectedDecode = (b: Buffer) => { + return require('zlib').inflateRawSync(b); + } + + await this.startAI(); + let url = _dataStore.database.frontend.getImageFile(_dataStore.currentImage, PrisbeamImageType.ViewURL); + let data: any; + + if (url.startsWith("blob:") || url.startsWith("pbip:")) { + fs.writeFileSync(_dataStore.appData + "/.temp", protectedDecode(fs.readFileSync(_dataStore.database.frontend.getImageFile(_dataStore.currentImage, PrisbeamImageType.ViewFile)))); + url = "file://" + (_dataStore.appData + "/.temp").replaceAll("\\", "/"); + } + + if (_dataStore.currentImage.tags.includes("safe")) { + data = await (await fetch("http://127.0.0.1:25091/safe?url=" + encodeURIComponent(url.replace("file://", "")))).json(); + } else { + data = await (await fetch("http://127.0.0.1:25091/explicit?url=" + encodeURIComponent(url.replace("file://", "")))).json(); + } + + if (fs.existsSync(_dataStore.appData + "/.temp")) fs.unlinkSync(_dataStore.appData + "/.temp"); + return data; + } + + unload() { + if (this.instance.dataStore.appData && fs.existsSync(this.instance.dataStore.appData + "/.temp")) fs.unlinkSync(this.instance.dataStore.appData + "/.temp"); + + if (this.aiProcess) { + console.log("Engine did not stop before quitting, forcefully killing it"); + this.aiProcess.kill("SIGKILL"); + } + } + + makeClassesHTML(c: any) { + let _dataStore = this.instance.dataStore; + + window.onresize = () => { + document.getElementById("preview-zones").style.top = document.getElementById("preview-content-inner").offsetTop + "px"; + document.getElementById("preview-zones").style.left = document.getElementById("preview-content-inner").offsetLeft + "px"; + document.getElementById("preview-zones").style.width = document.getElementById("preview-content-inner").clientWidth + "px"; + document.getElementById("preview-zones").style.height = document.getElementById("preview-content-inner").clientHeight + "px"; + } + + window.onresize(null); + _dataStore.currentImageClasses = c.data || []; + + let properClasses = _dataStore.currentImageClasses.filter(i => i.confidence > 0.5).map(i => { + return { + name: i.name, + top: (i.ymin / _dataStore.currentImage.height) * 100, + left: (i.xmin / _dataStore.currentImage.width) * 100, + width: ((i.xmax - i.xmin) / _dataStore.currentImage.width) * 100, + height: ((i.ymax - i.ymin) / _dataStore.currentImage.height) * 100 + } + }); + document.getElementById("preview-zones").innerHTML = properClasses.map((i, j) => ` + <div onmouseenter="instance.display.highlightZone(${j}, true);" onmouseleave="instance.display.highlightZone(${j}, false);" class="preview-zone" id="preview-zone-${j}" style="opacity: 0; transition: opacity 100ms; border-radius: 1%; background-color: rgba(255, 255, 255, .25); border: 1px solid rgba(255, 255, 255, .5); width: ${i.width}%; height: ${i.height}%; top: ${i.top}%; left: ${i.left}%; position: absolute;"></div> + `).join(""); + + this.displayClassesList(properClasses); + } + + displayClassesList(properClasses: any) { + if (properClasses.length === 0) { + document.getElementById("preview-parts-loader").style.display = "none"; + document.getElementById("preview-parts-none").style.display = ""; + document.getElementById("preview-parts-unsupported").style.display = "none"; + document.getElementById("preview-parts-list").style.display = "none"; + } else { + document.getElementById("preview-parts-loader").style.display = "none"; + document.getElementById("preview-parts-none").style.display = "none"; + document.getElementById("preview-parts-unsupported").style.display = "none"; + document.getElementById("preview-parts-list").style.display = ""; + document.getElementById("preview-parts-list").innerHTML = properClasses.map((i, j) => ` + <a onmouseenter="instance.display.highlightZone(${j}, true);" onmouseleave="instance.display.highlightZone(${j}, false);" id="preview-tag-zone-${j}" class='preview-tag preview-tag-zone' href='#'>${i.name}</a> + `).join(""); + } + } + + displayClasses(id: string) { + let _dataStore = this.instance.dataStore; + + if (_dataStore.currentImage.mime_type.startsWith("image/")) { + if (_dataStore.currentImage.tags.includes("screencap") || !_dataStore.currentImage.tags.includes("safe")) { + this.instance.ai.getClasses().then((c: any) => { + if (_dataStore.currentImage.id === parseInt(id)) { + this.makeClassesHTML(c); + } + }); + } else { + document.getElementById("preview-parts-loader").style.display = "none"; + document.getElementById("preview-parts-none").style.display = ""; + document.getElementById("preview-parts-unsupported").style.display = "none"; + document.getElementById("preview-parts-list").style.display = "none"; + } + } else { + document.getElementById("preview-parts-loader").style.display = "none"; + document.getElementById("preview-parts-none").style.display = "none"; + document.getElementById("preview-parts-unsupported").style.display = ""; + document.getElementById("preview-parts-list").style.display = "none"; + } + } +} diff --git a/src/PrisbeamActions.ts b/src/PrisbeamActions.ts new file mode 100755 index 0000000..06e69fe --- /dev/null +++ b/src/PrisbeamActions.ts @@ -0,0 +1,13 @@ +import {PrisbeamApp} from "./PrisbeamApp"; + +export class PrisbeamActions { + instance: PrisbeamApp; + + constructor(instance: PrisbeamApp) { + this.instance = instance; + } + + goHome() { + this.instance.search.startTagSearch(""); + } +} diff --git a/src/PrisbeamApp.ts b/src/PrisbeamApp.ts new file mode 100755 index 0000000..a44e1e4 --- /dev/null +++ b/src/PrisbeamApp.ts @@ -0,0 +1,129 @@ +import {PrisbeamDataStore} from "./PrisbeamDataStore"; +import {PrisbeamListType} from "libprisbeam"; +import {PrisbeamAppDisplay} from "./PrisbeamAppDisplay"; +import {PrisbeamSettings} from "./PrisbeamSettings"; +import {PrisbeamSearch} from "./PrisbeamSearch"; +import {PrisbeamActions} from "./PrisbeamActions"; +import {PrisbeamAI} from "./PrisbeamAI"; +import {PrisbeamLoader} from "./PrisbeamLoader"; +import {PrisbeamPropertyStore} from "libprisbeam/src/PrisbeamPropertyStore"; +import {PrisbeamDerpibooru} from "./PrisbeamDerpibooru"; + +export class PrisbeamApp { + dataStore: PrisbeamDataStore; + bootstrap: any; + display: PrisbeamAppDisplay; + settings: PrisbeamSettings; + search: PrisbeamSearch; + actions: PrisbeamActions; + ai: PrisbeamAI; + loader: PrisbeamLoader; + propertyStore: PrisbeamPropertyStore; + derpibooru: PrisbeamDerpibooru; + + constructor(bootstrap: any) { + this.bootstrap = bootstrap; + this.dataStore = new PrisbeamDataStore(this); + this.display = new PrisbeamAppDisplay(this); + this.settings = new PrisbeamSettings(this); + this.search = new PrisbeamSearch(this); + this.actions = new PrisbeamActions(this); + this.ai = new PrisbeamAI(this); + this.loader = new PrisbeamLoader(this); + this.derpibooru = new PrisbeamDerpibooru(this); + + this.search.loadSearchModule(); + } + + createPropertyStore() { + this.propertyStore = this.dataStore.database.propertyStore; + } + + async finishLoading() { + document.getElementById("loading-btn").classList.add("disabled"); + + document.getElementById("load").innerText = "Loading interface..."; + document.getElementById("progress").classList.add("progress-bar-striped"); + document.getElementById("progress").style.width = "100%"; + + this.dataStore.loaded = true; + this.dataStore.page = 1; + this.dataStore.currentView = await this.dataStore.database.frontend.getAllImages(PrisbeamListType.Array) as any[]; + this.display.updateDisplay(); + + this.dataStore.loader.hide(); + document.getElementById("app").classList.remove("disabled"); + } + + loadingError(msg: string) { + let li = document.createElement("li"); + li.classList.add("list-group-item"); + li.classList.add("list-group-item-warning"); + li.innerHTML = msg; + + document.getElementById("loader-errors-list").append(li); + document.getElementById("loader-errors").style.display = ""; + this.dataStore.hadErrorsLoading = true; + } + + async loadApp() { + if (this.dataStore.loaded) return; + + document.getElementById("load").innerText = "Waiting for application..."; + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("progress").style.width = "0%"; + + await this.loader.findDatabase(); + this.loader.checkBusyUpdating(); + await this.loader.initializeDatabase(); + await this.loader.updateCache(); + await this.loader.completeLoading(); + await this.derpibooru.initialize(); + } + + safeUnload() { + let modal = new this.bootstrap.Modal(document.getElementById("close-dialog")); + modal.show(); + + return new Promise<void>((res) => { + this.ai.unload(); + + if (!this.derpibooru.window?.isDestroyed()) { + this.derpibooru.window.destroy(); + } + + if (this.dataStore.database && !this.dataStore.unloaded) { + (async () => { + try { + await this.dataStore.database.close(); + } catch (e) { + console.error(e); + } + + this.dataStore.unloaded = true; + + res(); + })(); + } else { + res(); + } + }); + } + + async safeReload() { + await this.safeUnload(); + location.reload(); + } + + // noinspection JSUnusedGlobalSymbols + async safeClose() { + await this.safeUnload(); + window.close(); + } + + bootstrapTooltips() { + const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); + //@ts-ignore + [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); + } +} diff --git a/src/PrisbeamAppDisplay.ts b/src/PrisbeamAppDisplay.ts new file mode 100755 index 0000000..e3a9ee6 --- /dev/null +++ b/src/PrisbeamAppDisplay.ts @@ -0,0 +1,455 @@ +import {PrisbeamApp} from "./PrisbeamApp"; +import {PrisbeamImageType} from "libprisbeam"; +import * as fs from "node:fs"; +import {PrisbeamUtilities} from "./PrisbeamUtilities"; +import {shell} from "@electron/remote"; + +export class PrisbeamAppDisplay { + private instance: PrisbeamApp; + + constructor(app: PrisbeamApp) { + this.instance = app; + } + + runUserSort(a: any, b: any) { + if ((document.getElementById("sorting") as HTMLInputElement).value === "new") { + return new Date(b.first_seen_at).getTime() - new Date(a.first_seen_at).getTime(); + } else if ((document.getElementById("sorting") as HTMLInputElement).value === "id") { + return b.id - a.id; + } else if ((document.getElementById("sorting") as HTMLInputElement).value === "popular") { + return b.score - a.score; + } else if ((document.getElementById("sorting") as HTMLInputElement).value === "size") { + return b.size - a.size; + } else if ((document.getElementById("sorting") as HTMLInputElement).value === "duration") { + return b.duration - a.duration; + } else if ((document.getElementById("sorting") as HTMLInputElement).value === "resolution") { + return (b.height * b.width) - (a.height * a.width); + } else if ((document.getElementById("sorting") as HTMLInputElement).value === "relevant") { + return b.wilson_score - a.wilson_score; + } + } + + displayNoResults() { + document.getElementById("search-warning").style.display = ""; + document.getElementById("pages").style.display = "none"; + document.getElementById("search-warning").innerHTML = "<b>There are no images matching your request.</b> Make sure the selected rating, filters and search query are correct and try again."; + } + + getThumbnailHTML(i: any) { + if (i['representations']['thumb'].endsWith(".webm")) i['representations']['thumb'] = i['representations']['thumb'].substring(0, i['representations']['thumb'].length - 5) + ".gif"; + let thumbnail = this.instance.dataStore.database.frontend.getImageFile(i, PrisbeamImageType.ThumbnailURL); + + // noinspection CssUnknownTarget + return ` + <div + id="image-${i.id}" + class=" + image${i.tags.includes('safe') ? ' image-safe' : ''} + ${i.tags.includes('questionable') ? ' image-questionable' : ''} + ${i.tags.includes('suggestive') ? ' image-suggestive' : ''} + ${i.tags.includes('explicit') ? ' image-explicit' : ''} + ${i.tags.includes('grimdark') ? ' image-grimdark' : ''} + " + style=" + aspect-ratio: 1; + background-color: #333; + border-radius: 10px; + background-position: center; + background-size: contain; + background-repeat: no-repeat; + cursor: pointer; + background-image: url("${thumbnail}"); + " + onclick="instance.display.openImage('${i.id}')" + ></div> + `; + } + + filterEnabledRatings(i: any) { + return (((document.getElementById("rating-safe") as HTMLInputElement).checked && i.tags.includes("safe")) || + ((document.getElementById("rating-suggestive") as HTMLInputElement).checked && i.tags.includes("suggestive")) || + ((document.getElementById("rating-questionable") as HTMLInputElement).checked && i.tags.includes("questionable")) || + ((document.getElementById("rating-explicit") as HTMLInputElement).checked && i.tags.includes("explicit")) || + ((document.getElementById("rating-grimdark") as HTMLInputElement).checked && i.tags.includes("grimdark"))) + } + + getPageNumbers() { + let _dataStore = this.instance.dataStore; + let items = _dataStore.currentViewItems; + let totalPages = Math.ceil(items.length / 50) + 1; + let pageNumbers = []; + + if (_dataStore.page - 4 > 0) { + pageNumbers.push(_dataStore.page - 4); + pageNumbers.push(_dataStore.page - 3); + pageNumbers.push(_dataStore.page - 2); + pageNumbers.push(_dataStore.page - 1); + } else if (_dataStore.page - 3 > 0) { + pageNumbers.push(_dataStore.page - 3); + pageNumbers.push(_dataStore.page - 2); + pageNumbers.push(_dataStore.page - 1); + } else if (_dataStore.page - 2 > 0) { + pageNumbers.push(_dataStore.page - 2); + pageNumbers.push(_dataStore.page - 1); + } else if (_dataStore.page - 1 > 0) { + pageNumbers.push(_dataStore.page - 1); + } + + pageNumbers.push(_dataStore.page); + let i = 0; + + while (pageNumbers.length < 9) { + if (_dataStore.page + i + 1 <= totalPages) { + pageNumbers.push(_dataStore.page + i + 1); + } else { + break; + } + + i++; + } + + return pageNumbers; + } + + getPageHTML(page: number) { + let length = (Math.ceil(this.instance.dataStore.currentViewItems.length / 50) + 1).toString().length; + return "<code>" + ("0".repeat(length).substring(0, length - page.toString().length) + page.toString()) + "</code>"; + } + + getPageListHTML(pageNumbers: number[]) { + let _dataStore = this.instance.dataStore; + let items = _dataStore.currentViewItems; + let totalPages = Math.ceil(items.length / 50) + 1; + + return ` + <div class="btn-group"> + <span tabindex="-1" onclick="instance.search.viewPage(1);" class="btn btn-primary ${_dataStore.page < 2 ? 'disabled' : ''}">«</span> + <span tabindex="-1" onclick="instance.search.viewPage(${_dataStore.page - 1});" class="btn btn-primary ${_dataStore.page === 1 ? 'disabled' : ''}">‹</span> + </div> + <div class="btn-group"> + ${totalPages > 5 ? pageNumbers.map(i => ` + <span tabindex="-1" onclick="instance.search.viewPage(${i});" class="btn btn${i !== _dataStore.page ? '-outline' : ''}-primary">${this.getPageHTML(i)}</span> + `).join("") : Array(totalPages).fill(null).map((_, i) => i + 1).map(i => ` + <span tabindex="-1" onclick="instance.search.viewPage(${i});" class="btn btn${i !== _dataStore.page ? '-outline' : ''}-primary">${this.getPageHTML(i)}</span> + `).join("")} + </div> + <div class="btn-group"> + <span tabindex="-1" onclick="instance.search.viewPage(${_dataStore.page + 1});" class="btn btn-primary ${_dataStore.page === totalPages ? 'disabled' : ''}">›</span> + <span tabindex="-1" onclick="instance.search.viewPage(${totalPages as number});" class="btn btn-primary ${_dataStore.page >= totalPages - 1 ? 'disabled' : ''}">»</span> + </div> + `; + } + + updateDisplay() { + let _dataStore = this.instance.dataStore; + document.getElementById("search-warning").style.display = "none"; + + if (document.getElementById("search-error").style.display === "none") { + document.getElementById("pages").style.display = ""; + } else { + document.getElementById("pages").style.display = "none"; + } + + let items = _dataStore.currentViewItems = [..._dataStore.currentView] + .sort(this.runUserSort) + .filter(this.filterEnabledRatings) + .map((i) => this.getThumbnailHTML(i)); + + if (items.length === 0 && document.getElementById("search-error").style.display === "none") this.displayNoResults(); + + if ((document.getElementById("order") as HTMLInputElement).value === "up") { + items = items.reverse(); + } + + document.getElementById("images").innerHTML = items.splice((_dataStore.page - 1) * 50, 50).join(""); + document.getElementById("pages").innerHTML = this.getPageListHTML(this.getPageNumbers()); + + this.updateTitle(); + this.instance.bootstrapTooltips(); + } + + updateTitle() { + let _dataStore = this.instance.dataStore; + let totalPages = Math.ceil(_dataStore.currentViewItems.length / 50) + 1; + + if (_dataStore.searching) { + if (_dataStore.loadedFromCache) { + document.title = "Searching for " + (document.getElementById("search") as HTMLInputElement).value.trim() + " (page " + _dataStore.page + "/" + totalPages + ") — Prisbeam (Cached)"; + } else { + document.title = "Searching for " + (document.getElementById("search") as HTMLInputElement).value.trim() + " (page " + _dataStore.page + "/" + totalPages + ") — Prisbeam"; + } + } else { + if (_dataStore.loadedFromCache) { + document.title = "All images (page " + _dataStore.page + "/" + totalPages + ") — Prisbeam (Cached)"; + } else { + document.title = "All images (page " + _dataStore.page + "/" + totalPages + ") — Prisbeam"; + } + } + } + + setRating(rating: string) { + Array.from(document.querySelectorAll(".tooltip.bs-tooltip-auto")).map(i => i.outerHTML = ""); + + (document.getElementById("rating-safe") as HTMLInputElement).checked = false; + (document.getElementById("rating-questionable") as HTMLInputElement).checked = false; + (document.getElementById("rating-suggestive") as HTMLInputElement).checked = false; + (document.getElementById("rating-explicit") as HTMLInputElement).checked = false; + (document.getElementById("rating-grimdark") as HTMLInputElement).checked = false; + + let code = 0; + if (rating === "safe") code = 1; + if (rating === "questionable") code = 2; + if (rating === "suggestive") code = 3; + if (rating === "explicit") code = 4; + if (rating === "grimdark") code = 5; + + if (code >= 1) (document.getElementById("rating-safe") as HTMLInputElement).checked = true; + if (code >= 2) (document.getElementById("rating-questionable") as HTMLInputElement).checked = true; + if (code >= 3) (document.getElementById("rating-suggestive") as HTMLInputElement).checked = true; + if (code >= 4) (document.getElementById("rating-explicit") as HTMLInputElement).checked = true; + if (code >= 5) (document.getElementById("rating-grimdark") as HTMLInputElement).checked = true; + } + + highlightZone(id: string, state: boolean) { + if (state) { + document.getElementById("preview-zone-" + id).classList.add("hover"); + document.getElementById("preview-tag-zone-" + id).classList.add("hover"); + } else { + document.getElementById("preview-zone-" + id).classList.remove("hover"); + document.getElementById("preview-tag-zone-" + id).classList.remove("hover"); + } + } + + buildImageTitle() { + let _dataStore = this.instance.dataStore; + + return "Image #<span class='selectable'>" + + (_dataStore.currentImage.source_id ?? _dataStore.currentImage.id) + + (_dataStore.currentImage.source_name + ? "</span> (<span class='selectable'>" + _dataStore.currentImage.source_name + "</span>)" + : "") + + "" + + PrisbeamUtilities.getMimeBadge(_dataStore.currentImage.mime_type) + } + + displayImageSize() { + let _dataStore = this.instance.dataStore; + let size = 0; + let sizeExplanation = []; + + let file = _dataStore.database.frontend.getImageFile(_dataStore.currentImage, PrisbeamImageType.ViewFile); + let thumb = _dataStore.database.frontend.getImageFile(_dataStore.currentImage, PrisbeamImageType.ThumbnailFile); + + if (file) { + let cSize = fs.lstatSync(file).size; + size += cSize; + sizeExplanation.push("Image: " + PrisbeamUtilities.formatSize(cSize)); + } + + if (thumb) { + let cSize = fs.lstatSync(thumb).size; + size += cSize; + sizeExplanation.push("Thumbnail: " + PrisbeamUtilities.formatSize(cSize)); + } + + let cSize = JSON.stringify(_dataStore.currentImage).length; + size += cSize; + sizeExplanation.push("Metadata: " + PrisbeamUtilities.formatSize(cSize)); + + document.getElementById("preview-size").innerHTML = + "<span data-bs-toggle='tooltip' data-bs-html='true' title='" + sizeExplanation.join("<br>") + "'>" + + PrisbeamUtilities.formatSize(size) + "</span>"; + } + + categorySortingNumber(cat: string) { + if (cat === "error") return 0; + if (cat === "rating") return 1; + if (cat === "origin") return 2; + if (cat === "character") return 3; + if (cat === "oc") return 4; + if (cat === "species") return 5; + if (cat === "body-type") return 6; + if (cat === "content-official") return 7; + if (cat === "content-fanmade") return 8; + if (cat === "spoiler") return 9; + return 999; + } + + async getImpliedTags() { + let _dataStore = this.instance.dataStore; + + let categories = {}; + let implied = []; + let impliedNames = []; + + for (let id of _dataStore.currentImage.tag_ids) { + implied.push(...(await _dataStore.database.frontend.getImpliedTagIdsFromId(id)).filter(i => !_dataStore.currentImage.tag_ids.includes(i))); + impliedNames.push(...(await _dataStore.database.frontend.getImpliedTagNamesFromId(id)).filter(i => !_dataStore.currentImage.tags.includes(i))); + } + + for (let id of implied) { + categories[id] = (await _dataStore.database._sql("SELECT category FROM tags WHERE id=" + id))[0]['category']; + } + + implied = [...new Set(implied)]; + impliedNames = [...new Set(impliedNames)]; + + let impliedTags = impliedNames.map((i: string, j: number) => [i, implied[j], categories[implied[j]]]); + + return impliedTags.sort((a, b) => { + return a[0].localeCompare(b[0]); + }).sort((a, b) => { + return this.categorySortingNumber(a[2]) - this.categorySortingNumber(b[2]); + }); + } + + async getTags() { + let _dataStore = this.instance.dataStore; + let categories = {}; + + for (let id of _dataStore.currentImage.tag_ids) { + categories[id] = (await _dataStore.database._sql("SELECT category FROM tags WHERE id=" + id))[0]['category']; + } + + let tags = _dataStore.currentImage.tags.map((i: string, j: number) => [i, _dataStore.currentImage.tag_ids[j], categories[_dataStore.currentImage.tag_ids[j]]]); + + return tags.sort((a: string, b: string) => { + return a[0].localeCompare(b[0]); + }).sort((a: string, b: string) => { + return this.categorySortingNumber(a[2]) - this.categorySortingNumber(b[2]); + }); + } + + async displayTags() { + let tags = await this.getTags(); + let impliedTags = await this.getImpliedTags(); + + document.getElementById("preview-tags").innerHTML = tags.map((i: any[]) => + "<a class='preview-tag preview-tag-" + + (i[2] ?? "unknown") + + "' onclick='instance.search.startTagSearch(decodeURIComponent(atob(\"" + btoa(encodeURIComponent(i[0])) + "\")))'>" + + i[0] + "</a>" + ).join("<wbr>") + + impliedTags.map(i => + "<a class='preview-tag preview-tag-implied preview-tag-" + + (i[2] ?? "unknown") + + "' onclick='searchForTag(decodeURIComponent(atob(\"" + btoa(encodeURIComponent(i[0])) + "\")))'>" + + i[0] + "</a>" + ).join("<wbr>"); + } + + async initializeImageUI(id: string) { + let _dataStore = this.instance.dataStore; + + _dataStore.modal.show(); + _dataStore.currentImage = await _dataStore.database.frontend.getImage(id); + _dataStore.currentImageClasses = []; + document.getElementById("preview-zones").innerHTML = ""; + + document.getElementById("preview-parts-loader").style.display = ""; + document.getElementById("preview-parts-none").style.display = "none"; + document.getElementById("preview-parts-unsupported").style.display = "none"; + document.getElementById("preview-parts-list").style.display = "none"; + document.title = "Viewing image #" + (_dataStore.currentImage.source_id ?? id) + " — " + document.title; + document.getElementById("preview-title").innerHTML = this.buildImageTitle(); + document.getElementById("preview-date").innerHTML = "Uploaded <span data-bs-toggle='tooltip' title='" + new Date(_dataStore.currentImage.created_at * 1000).toString() + "'>" + PrisbeamUtilities.timeAgo(_dataStore.currentImage.created_at * 1000) + "</span>"; + document.getElementById("preview-resolution").innerText = _dataStore.currentImage.width + " × " + _dataStore.currentImage.height; + document.getElementById("preview-source-cta").innerText = "View on " + _dataStore.currentImage.source_name ?? "Derpibooru"; + } + + getCreditDescriptors() { + let descriptors = { + artists: [], editors: [], generators: [], photographers: [], prompters: [], colorists: [] + }; + let finalDescriptors = []; + + for (let tag of this.instance.dataStore.currentImage.tags) { + if (tag.startsWith("artist:")) descriptors.artists.push(tag.substring(7)); + if (tag.startsWith("editor:")) descriptors.editors.push(tag.substring(7)); + if (tag.startsWith("generator:")) descriptors.generators.push(tag.substring(10)); + if (tag.startsWith("photographer:")) descriptors.photographers.push(tag.substring(13)); + if (tag.startsWith("prompter:")) descriptors.prompters.push(tag.substring(9)); + if (tag.startsWith("colorist:")) descriptors.colorists.push(tag.substring(9)); + } + + if (descriptors.artists.length > 0) finalDescriptors.push("artist" + (descriptors.artists.length > 1 ? "s" : "") + ": " + descriptors.artists.join("")); + if (descriptors.editors.length > 0) finalDescriptors.push("editor" + (descriptors.editors.length > 1 ? "s" : "") + ": " + descriptors.editors.join("")); + if (descriptors.generators.length > 0) finalDescriptors.push("generator" + (descriptors.generators.length > 1 ? "s" : "") + ": " + descriptors.generators.join("")); + if (descriptors.photographers.length > 0) finalDescriptors.push("photographer" + (descriptors.photographers.length > 1 ? "s" : "") + ": " + descriptors.photographers.join("")); + if (descriptors.prompters.length > 0) finalDescriptors.push("prompter" + (descriptors.prompters.length > 1 ? "s" : "") + ": " + descriptors.prompters.join("")); + if (descriptors.colorists.length > 0) finalDescriptors.push("colorist" + (descriptors.colorists.length > 1 ? "s" : "") + ": " + descriptors.colorists.join("")); + + if (finalDescriptors[0]) finalDescriptors[0] = finalDescriptors[0].substring(0, 1).toUpperCase() + finalDescriptors[0].substring(1); + + return finalDescriptors; + } + + displayCredits() { + let _dataStore = this.instance.dataStore; + let artist = "Anonymous artist"; + let finalDescriptors = this.getCreditDescriptors(); + if (finalDescriptors.length > 0) artist = finalDescriptors.join("; "); + + if (_dataStore.currentImage.tags.includes("official")) { + if (finalDescriptors.length > 0) { + artist = finalDescriptors.join("; ") + " (official content)"; + } else { + artist = "Official Hasbro content"; + } + } + + if (_dataStore.currentImage.tags.includes("artist needed")) { + artist = "Unknown artist"; + } + + document.getElementById("preview-artist").innerText = artist; + } + + displayScore() { + let _dataStore = this.instance.dataStore; + + if (_dataStore.currentImage.score !== 0) { + try { document.getElementById("preview-score").outerHTML = ""; } catch (e) {} + document.getElementById("preview-statistics").insertAdjacentHTML("afterbegin", `<div class="list-group-item" id="preview-score" style="font-weight: bold;"></div>`); + + if (_dataStore.currentImage.faves > 0) { + document.getElementById("preview-score").innerText = _dataStore.currentImage.faves + " favorites · " + _dataStore.currentImage.score + " points (" + _dataStore.currentImage.upvotes + " up, " + _dataStore.currentImage.downvotes + " down)"; + } else { + document.getElementById("preview-score").innerText = _dataStore.currentImage.score + " points (" + _dataStore.currentImage.upvotes + " up, " + _dataStore.currentImage.downvotes + " down)"; + } + } else { + try { document.getElementById("preview-score").outerHTML = ""; } catch (e) {} + } + } + + displayViewer() { + let _dataStore = this.instance.dataStore; + let url = _dataStore.database.frontend.getImageFile(_dataStore.currentImage, PrisbeamImageType.ViewURL) + + if (_dataStore.currentImage.mime_type.startsWith("video/")) { + document.getElementById("preview-content").innerHTML = ` + <video id="preview-content-inner" loop muted autoplay controls src="${url}" style="max-width: 100%; max-height: 80vh; margin-left: auto; margin-right: auto; display: block;"></video> + `; + } else { + document.getElementById("preview-content").innerHTML = ` + <img alt="" id="preview-content-inner" src="${url}" style="max-width: 100%; max-height: calc((100vh - 1.75rem * 2) - 71px); margin-left: auto; margin-right: auto; display: block;"> + `; + } + } + + async openImage(id: string) { + await this.initializeImageUI(id); + this.displayImageSize(); + await this.displayTags(); + this.displayCredits(); + this.displayScore(); + this.displayViewer(); + this.instance.ai.displayClasses(id); + this.instance.bootstrapTooltips(); + } + + openImageOnSource() { + let image = this.instance.dataStore.currentImage; + shell.openExternal(image['source'].replace("%s", image['source_id'])); + } +} diff --git a/src/PrisbeamDataStore.ts b/src/PrisbeamDataStore.ts new file mode 100755 index 0000000..cf910f0 --- /dev/null +++ b/src/PrisbeamDataStore.ts @@ -0,0 +1,47 @@ +import {PrisbeamApp} from "./PrisbeamApp"; +import {Prisbeam} from "libprisbeam"; + +export class PrisbeamDataStore { + public loaded: boolean; + public loadedFromCache: boolean; + public hadErrorsLoading: boolean; + public searching: boolean; + public source: string; + public appData: string; + public database: Prisbeam; + public db: object; + public tags: any[][]; + public tagsHashed: object; + public currentView: any[]; + public currentViewItems: any[]; + public page: number; + public modal: any; + public loader: any; + public login: any; + public unloaded: boolean; + public currentImage: any; + public currentImageClasses: any[]; + public needUpdate: boolean; + public lastQuery: string; + public lastPress: number; + public close: boolean; + + constructor(instance: PrisbeamApp) { + let bootstrap = instance.bootstrap; + + this.page = 1; + this.searching = false; + this.unloaded = false; + // @ts-ignore + this.modal = new bootstrap.Modal(document.getElementById("preview")); + // @ts-ignore + this.loader = new bootstrap.Modal(document.getElementById("loader")); + this.login = new bootstrap.Modal(document.getElementById("login")); + this.loader.show(); + this.currentImage = null; + this.currentImageClasses = []; + this.hadErrorsLoading = false; + this.lastQuery = ""; + this.loaded = false; + } +} diff --git a/src/PrisbeamDerpibooru.ts b/src/PrisbeamDerpibooru.ts new file mode 100755 index 0000000..0680f13 --- /dev/null +++ b/src/PrisbeamDerpibooru.ts @@ -0,0 +1,293 @@ +import {PrisbeamApp} from "./PrisbeamApp"; +import {BrowserWindow, shell} from "@electron/remote"; +import {BrowserWindow as TBrowserWindow, SafeStorage} from "electron"; + +interface DerpibooruJSDataStore { + fancyTagEdit: boolean; + fancyTagUpload: boolean; + filterId: number; + hiddenFilter: string; + hiddenTagList: number[]; + ignoredTagList: number[]; + interactions: any[]; + spoilerType: string; + spoileredFilter: string; + spoileredTagList: number[]; + userCanEditFilter: boolean; + userIsSignedIn: boolean; + watchedTagList: number[]; +} + +export class PrisbeamDerpibooru { + instance: PrisbeamApp; + enabled: boolean; + window: TBrowserWindow; + dataStore: DerpibooruJSDataStore; + + constructor(instance: PrisbeamApp) { + this.instance = instance; + this.enabled = null; + } + + async getDataStore() { + if (await this.window.webContents.executeJavaScript("!!document.getElementsByClassName(\"js-datastore\")[0]")) { + let attributes = await this.window.webContents.executeJavaScript("document.getElementsByClassName(\"js-datastore\")[0].getAttributeNames().filter(i => i.startsWith(\"data\"))"); + let obj = {}; + + for (let name of attributes) { + let data = await this.window.webContents.executeJavaScript("document.getElementsByClassName(\"js-datastore\")[0].getAttribute(\"" + name + "\")"); + + name = name.substring(5).toLowerCase().replace(/-(.)/g, function(_: string, g: string) { + return g.toUpperCase(); + }); + + try { + obj[name] = JSON.parse(data); + } catch (e) { + obj[name] = data; + } + } + + return obj; + } else { + return {}; + } + } + + startLoginFlow() { + (document.getElementById("derpibooru-email") as HTMLInputElement).value = ""; + (document.getElementById("derpibooru-password") as HTMLInputElement).value = ""; + (document.getElementById("derpibooru-2fa") as HTMLInputElement).value = ""; + document.getElementById("derpibooru-login-2fa").style.display = "none"; + document.getElementById("derpibooru-login-initial").style.display = ""; + + this.setFormLock(false); + this.instance.dataStore.login.show(); + } + + validateEmail(email: string) { + return /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + .test(email.toLowerCase()); + } + + validateCredentials(email: string, password: string) { + if (email.trim() === "") { + alert("Please enter an email address."); + document.getElementById("derpibooru-email").focus(); + return false; + } + + if (password.trim() === "") { + alert("Please enter a password."); + document.getElementById("derpibooru-password").focus(); + return false; + } + + if (!this.validateEmail(email)) { + alert("Please enter a valid email address."); + document.getElementById("derpibooru-email").focus(); + return false; + } + + if (password.length < 12) { + alert("Your password must be at least 12 characters."); + document.getElementById("derpibooru-password").focus(); + return false; + } + + return true; + } + + setFormLock(lock: boolean) { + document.getElementById("login").style['pointerEvents'] = lock ? "none" : ""; + (document.getElementById("derpibooru-email") as HTMLInputElement).disabled = lock; + (document.getElementById("derpibooru-password") as HTMLInputElement).disabled = lock; + (document.getElementById("derpibooru-2fa") as HTMLInputElement).disabled = lock; + document.getElementById("derpibooru-confirm-btn").classList[lock ? "add" : "remove"]("disabled"); + document.getElementById("derpibooru-confirm-btn2").classList[lock ? "add" : "remove"]("disabled"); + } + + async handleLoginPage(email?: string, password?: string) { + if (this.window.webContents.getURL().endsWith("/sessions/new") || + this.window.webContents.getURL().endsWith("/sessions/new/") || + this.window.webContents.getURL().endsWith("/sessions") || + this.window.webContents.getURL().endsWith("/sessions/")) { + await this.requestCredentials(email, password); + } else if (this.window.webContents.getURL().endsWith("/sessions/totp/new") || this.window.webContents.getURL().endsWith("/sessions/totp/new/")) { + await this.request2fa(); + } else { + await this.regularLoginItem(); + } + } + + async requestCredentials(email?: string, password?: string) { + let msg = await this.window.webContents.executeJavaScript('document.querySelector(".alert.alert-danger")?.innerText'); + + if (msg) { + alert(msg); + this.setFormLock(false); + document.getElementById("derpibooru-email").focus(); + } else { + this.window.webContents.once("did-stop-loading", () => this.handleLoginPage(email, password)); + + await this.window.webContents.executeJavaScript("document.getElementById('user_email').value = \"" + email.trim().replaceAll('\\', '\\\\').replaceAll('"', '\\"') + "\";"); + await this.window.webContents.executeJavaScript("document.getElementById('user_password').value = \"" + password.trim().replaceAll('\\', '\\\\').replaceAll('"', '\\"') + "\";"); + await this.window.webContents.executeJavaScript("document.getElementById('user_remember_me').checked = true;"); + await this.window.webContents.executeJavaScript('document.querySelector("[action=\\"/sessions\\"] [type=\\"submit\\"]").click();'); + } + } + + async request2fa() { + document.getElementById("derpibooru-login-2fa").style.display = ""; + document.getElementById("derpibooru-login-initial").style.display = "none"; + document.getElementById("derpibooru-2fa").focus(); + this.setFormLock(false); + } + + async loadUserData() { + if (!this.dataStore.userIsSignedIn) { + document.getElementById("derpibooru-login-btn").classList.remove("disabled"); + document.getElementById("derpibooru-login").style.display = ""; + Array.from(document.getElementsByClassName("derpibooru-when-logged-in")) + .map((i: HTMLElement) => i.style.display = "none"); + } else { + document.getElementById("derpibooru-login-btn").classList.add("disabled"); + document.getElementById("derpibooru-login").style.display = "none"; + Array.from(document.getElementsByClassName("derpibooru-when-logged-in")) + .map((i: HTMLElement) => i.style.display = ""); + + let avatar = await this.window.webContents + .executeJavaScript('document.querySelector("[src^=\\"https://derpicdn.net/avatars/\\"]").src'); + await this.instance.propertyStore.setItem("pba_derpibooru_avatar", avatar); + (document.getElementById("avatar") as HTMLImageElement).src = avatar; + + this.window.webContents.on('did-stop-loading', async () => { + let apiKey = await this.window.webContents.executeJavaScript('document.getElementById("api-key").innerText.trim();'); + let apiKeyToStore = apiKey; + let safeStorage: SafeStorage = require('@electron/remote').safeStorage; + + if (safeStorage.isEncryptionAvailable()) { + apiKeyToStore = safeStorage.encryptString(apiKey).toString("base64"); + } + + await this.instance.propertyStore.setItem("pba_derpibooru_key_encrypted", apiKeyToStore); + + let userName = await this.window.webContents.executeJavaScript('document.querySelector("[class=\\"header__link\\"][href^=\\"/profiles/\\"]").innerText'); + await this.instance.propertyStore.setItem("pba_derpibooru_user_name", userName); + document.getElementById("user-name").innerText = "Logged in as: " + userName; + }); + + await this.window.loadURL("https://derpibooru.org/registrations/edit"); + } + } + + openManager() { + shell.openExternal("https://derpibooru.org/registrations/edit"); + } + + async logOut() { + if (confirm("Are you sure you want to log out from Derpibooru? Any features using Derpibooru will stop working and your Derpibooru user data will be removed from Prisbeam.")) { + await this.instance.propertyStore.removeItem("pba_derpibooru_user_name"); + await this.instance.propertyStore.removeItem("pba_derpibooru_key_encrypted"); + await this.instance.propertyStore.removeItem("pba_derpibooru_avatar"); + await this.window.webContents.executeJavaScript('document.querySelector("[data-method=\\"delete\\"][href=\\"/sessions\\"]").click();'); + location.reload(); + } + } + + async regularLoginItem() { + if ((await this.getDataStore() as DerpibooruJSDataStore).userIsSignedIn) { + this.setFormLock(false); + this.instance.dataStore.login.hide(); + location.reload(); + } else { + if (document.getElementById("derpibooru-login-2fa").style.display !== "none") { + alert("An invalid two-factor authentication code was entered. Please try again."); + (document.getElementById("derpibooru-2fa") as HTMLInputElement).value = ""; + (document.getElementById("derpibooru-email") as HTMLInputElement).value = ""; + (document.getElementById("derpibooru-password") as HTMLInputElement).value = ""; + this.setFormLock(false); + } + + document.getElementById("derpibooru-login-2fa").style.display = "none"; + document.getElementById("derpibooru-login-initial").style.display = ""; + } + } + + submitLogin() { + let email = (document.getElementById("derpibooru-email") as HTMLInputElement).value; + let password = (document.getElementById("derpibooru-password") as HTMLInputElement).value; + if (!this.validateCredentials(email, password)) return; + + this.setFormLock(true); + this.window.webContents.once("did-stop-loading", () => this.handleLoginPage(email, password)); + + this.window.loadURL("https://derpibooru.org/sessions/new"); + } + + async submit2fa() { + let mfa = parseInt((document.getElementById("derpibooru-2fa") as HTMLInputElement).value).toString(); + if (mfa.length !== 6) { + alert("A two-factor authentication code contains 6 digits, but you entered " + mfa.length + "."); + document.getElementById("derpibooru-2fa").focus(); + return; + } + + this.setFormLock(true); + this.window.webContents.once("did-stop-loading", () => this.handleLoginPage(null, null)); + await this.window.webContents.executeJavaScript("document.getElementById('user_twofactor_token').value = \"" + mfa.trim().replaceAll('\\', '\\\\').replaceAll('"', '\\"') + "\";"); + await this.window.webContents.executeJavaScript("document.getElementById('user_remember_me').checked = true;"); + await this.window.webContents.executeJavaScript('document.querySelector("[action=\\"/sessions/totp\\"] [type=\\"submit\\"]").click();'); + } + + initialize() { + document.getElementById("login").addEventListener("shown.bs.modal", () => { + if (this.window.webContents.getURL().endsWith("/sessions/totp/new") || this.window.webContents.getURL().endsWith("/sessions/totp/new/")) { + document.getElementById("derpibooru-login-2fa").style.display = ""; + document.getElementById("derpibooru-login-initial").style.display = "none"; + document.getElementById("derpibooru-2fa").focus(); + } else { + document.getElementById("derpibooru-login-2fa").style.display = "none"; + document.getElementById("derpibooru-login-initial").style.display = ""; + document.getElementById("derpibooru-email").focus(); + } + }); + + document.getElementById("derpibooru-email").onkeydown = document.getElementById("derpibooru-password").onkeydown = (event) => { + if (event.key === "Enter") { + this.submitLogin(); + } + } + + document.getElementById("derpibooru-2fa").onkeydown = (event) => { + if (event.key === "Enter") { + this.submit2fa(); + } + } + + document.getElementById("avatar").onerror = () => { + (document.getElementById("avatar") as HTMLImageElement).src = "../logo/placeholder.jpg"; + } + + return new Promise<void>(async (res) => { + this.enabled = false; + + this.window = new BrowserWindow({ + show: false + }); + + await this.window.loadURL("https://derpibooru.org"); + + this.dataStore = await this.getDataStore() as DerpibooruJSDataStore; + await this.loadUserData(); + this.enabled = true; + + this.window.webContents.on('did-stop-loading', async () => { + this.dataStore = await this.getDataStore() as DerpibooruJSDataStore; + if (!this.dataStore.userIsSignedIn) await this.loadUserData(); + }); + + res(); + }) + } +} diff --git a/src/PrisbeamLoader.ts b/src/PrisbeamLoader.ts new file mode 100755 index 0000000..935b003 --- /dev/null +++ b/src/PrisbeamLoader.ts @@ -0,0 +1,261 @@ +import {PrisbeamApp} from "./PrisbeamApp"; +import {ipcRenderer} from "electron"; +import * as si from "systeminformation"; +import fs from "fs"; +import {Prisbeam, PrisbeamListType} from "libprisbeam"; + +export class PrisbeamLoader { + instance: PrisbeamApp; + dataPath: string; + + constructor(instance: PrisbeamApp) { + this.instance = instance; + } + + async requestDatabaseOpen() { + if (localStorage.getItem("path") === null) { + let path = await ipcRenderer.invoke("openFolder"); + if (typeof path !== "object" && typeof path !== "string") { + alert("Please select a folder and try again."); + this.instance.dataStore.unloaded = true; + window.close(); + return; + } + + if (path instanceof Array) { + localStorage.setItem("path", path[0]); + } else { + localStorage.setItem("path", path); + } + + if (!require('fs').existsSync(localStorage.getItem("path") + "/instance.pbmk")) require('fs').writeFileSync(localStorage.getItem("path") + "/instance.pbmk", ""); + } + } + + async getPossibleSources() { + let mounts = (await si.fsSize()).map(i => i.mount); + let list = [localStorage.getItem("path").replaceAll("\\", "/")]; + let parts = localStorage.getItem("path").replaceAll("\\", "/").split("/").filter(i => i.trim() !== ""); + + for (let i = 1; i <= parts.length; i++) { + for (let mount of mounts) { + list.push(mount + "/" + parts.slice(0, i).join("/")); + } + } + + for (let i = 1; i <= parts.length; i++) { + for (let mount of mounts) { + let j = i + 1; + + while (j <= parts.length) { + list.push(mount + "/" + parts.slice(i, j).join("/")); + j++; + } + } + } + + for (let i = 1; i <= parts.length; i++) { + for (let mount of mounts) { + list.push(mount + "/" + parts.slice(i, i + 1).join("/")); + } + } + + return [...new Set(list.map(i => i.replaceAll("//", "/").replaceAll("\\", "/")))]; + } + + async filterValidSources(list: string[]) { + let validSources = []; + let statFolders = ["images", "thumbnails"]; + let statFiles = ["current.pbdb", "instance.pbmk"]; + + for (let item of list) { + let validFolders = 0; + let validFiles = 0; + + for (let folder of statFolders) { + let valid = false; + + try { + valid = (await fs.promises.lstat(item + "/" + folder)).isDirectory(); + } catch (e) { + } + + if (valid) validFolders++; + } + + for (let file of statFiles) { + let valid = false; + + try { + valid = (await fs.promises.lstat(item + "/" + file)).isFile(); + } catch (e) { + } + + if (valid) validFiles++; + } + + if (validFolders > 0 || validFiles > 0) { + validSources.push(item); + } + } + + return validSources; + } + + openFirstSource(validSources: string[]) { + let selectedSource: string; + + if (validSources.filter(i => i === localStorage.getItem("path").replaceAll("\\", "/")).length > 0) { + selectedSource = validSources.filter(i => i === localStorage.getItem("path").replaceAll("\\", "/"))[0]; + } else { + selectedSource = validSources[0]; + this.instance.loadingError("Database was read from " + selectedSource + " as the normal path is not available"); + } + + return selectedSource; + } + + async triggerInvalidSource(list: string[]) { + alert("Unable to load images as no valid image source could be found.\n\nTried: " + list.join(", ")); + + let path = await ipcRenderer.invoke("openFolder"); + if (typeof path !== "object" && typeof path !== "string") { + alert("Please select a folder and try again."); + this.instance.dataStore.unloaded = true; + window.close(); + return; + } + + if (path instanceof Array) { + localStorage.setItem("path", path[0]); + } else { + localStorage.setItem("path", path); + } + + if (!require('fs').existsSync(localStorage.getItem("path") + "/instance.pbmk")) require('fs').writeFileSync(localStorage.getItem("path") + "/instance.pbmk", ""); + + location.reload(); + } + + async loadFromCache() { + let _dataStore = this.instance.dataStore; + let valid = false; + + try { + valid = (await fs.promises.lstat(_dataStore.appData + "/PrisbeamCache/current.pbdb")).isFile(); + } catch (e) { + valid = false; + } + + if (valid) { + this.instance.loadingError("No valid image source found, images will be downloaded from their source"); + _dataStore.loadedFromCache = true; + return _dataStore.appData + "/PrisbeamCache"; + } else { + alert("Unable to load images from cache as the cache is empty or corrupted."); + this.instance.dataStore.unloaded = true; + window.close(); + } + } + + async loadAppropriateSource() { + let _dataStore = this.instance.dataStore; + + _dataStore.loadedFromCache = false; + + let list = await this.getPossibleSources(); + let validSources = await this.filterValidSources(list); + + if (validSources.length > 0) { + return this.openFirstSource(validSources); + } else { + if (fs.existsSync(_dataStore.appData + "/PrisbeamCache")) { + return await this.loadFromCache(); + } else { + await this.triggerInvalidSource(list); + } + } + } + + async findDatabase(): Promise<string> { + document.getElementById("load").innerText = "Finding database..."; + + await this.requestDatabaseOpen(); + + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("progress").style.width = "0%"; + + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("progress").style.width = "0%"; + + this.instance.dataStore.source = this.dataPath = await this.loadAppropriateSource(); + return this.dataPath; + } + + checkBusyUpdating() { + if (require('fs').existsSync(this.dataPath + "/updater.pbmk")) { + let pid = parseInt(require('fs').readFileSync(this.dataPath + "/updater.pbmk").toString().trim()); + let isUpdating = false; + + try { + process.kill(pid, 0); + isUpdating = true; + } catch (e) { + isUpdating = false; + } + + if (isUpdating) { + alert("This database is locked because an external Prisbeam Updater is updating its content. Please try again later."); + this.instance.dataStore.unloaded = true; + window.close(); + } + } + } + + async initializeDatabase() { + let _dataStore = this.instance.dataStore; + + _dataStore.database = new Prisbeam({ + database: this.dataPath, + cachePath: _dataStore.appData, + sqlitePath: process.platform === "darwin" ? "../../../sql/mac" : "../../../sql/win", + readOnly: false, + sensitiveImageProtocol: true + }); + + await _dataStore.database.initialize(true); + this.instance.createPropertyStore(); + } + + async updateCache() { + let _dataStore = this.instance.dataStore; + + if (!_dataStore.loadedFromCache) { + document.getElementById("load").innerText = "Updating cache..."; + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("progress").style.width = "0%"; + + if (!fs.existsSync(_dataStore.appData + "/PrisbeamCache")) await fs.promises.mkdir(_dataStore.appData + "/PrisbeamCache"); + await fs.promises.copyFile(this.dataPath + "/current.pbdb", _dataStore.appData + "/PrisbeamCache/current.pbdb"); + + document.getElementById("progress").style.width = "100%"; + } + } + + async completeLoading() { + let _dataStore = this.instance.dataStore; + + _dataStore.db = await _dataStore.database.frontend.getAllImages(PrisbeamListType.Object); + _dataStore.tags = _dataStore.database.frontend.tags; + _dataStore.tagsHashed = _dataStore.database.frontend.tagsHashed; + + if (_dataStore.hadErrorsLoading) { + document.getElementById("load").innerText = "Finished loading with warnings."; + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("progress").style.width = "100%"; + document.getElementById("loading-btn").classList.remove("disabled"); + } else { + this.instance.finishLoading(); + } + } +} diff --git a/src/PrisbeamSearch.ts b/src/PrisbeamSearch.ts new file mode 100755 index 0000000..daffde4 --- /dev/null +++ b/src/PrisbeamSearch.ts @@ -0,0 +1,130 @@ +import {PrisbeamApp} from "./PrisbeamApp"; +import {PrisbeamListType} from "libprisbeam"; + +export class PrisbeamSearch { + instance: PrisbeamApp; + + constructor(instance: PrisbeamApp) { + this.instance = instance; + } + + startTagSearch(tag: string) { + this.instance.dataStore.modal.hide(); + (document.getElementById("search") as HTMLInputElement).value = tag; + this.updateSearch(); + this.instance.display.updateDisplay(); + } + + searchError(e: Error, query: string, sql: string = null) { + this.instance.dataStore.currentView = []; + + document.getElementById("search-error").style.display = ""; + document.getElementById("pages").style.display = "none"; + document.getElementById("search-error").innerHTML = ` + <p><b>${this.resolveError(e)}</b></p> + <p>Prisbeam uses the same search query format as Derpibooru/Philomena. To make sure you make correct use of the search syntax, you can try to search for the same query on Derpibooru directly (if possible). Please keep in mind that fuzzy matching, boosting, escaping, the <code>faved_by</code> and <code>uploader</code> fields as well as the <code>my:hidden</code> are not supported.</code></p> + <p>If you think something is wrong with Prisbeam, please send a bug report, so we can fix it.</p> + <hr> + <details> + <summary>Show technical information</summary> + <pre style='margin-bottom: 0;'>Query: ${query.substring(0, 1024)}${sql ? `\n\nSQL: ${sql.substring(0, 1024)}` : ""}\n\nError dump:\n${e.stack}</pre> + </details> + `; + } + + async displayAll() { + this.instance.dataStore.page = 1; + this.instance.dataStore.searching = false; + this.instance.dataStore.currentView = await this.instance.dataStore.database.frontend.getAllImages(PrisbeamListType.Array) as any[]; + this.instance.display.updateDisplay(); + } + + async updateSearch() { + document.getElementById("images").classList.add("searching"); + document.getElementById("search-error").style.display = "none"; + document.getElementById("search-warning").style.display = "none"; + document.getElementById("pages").style.display = ""; + + if ((document.getElementById("search") as HTMLInputElement).value.trim() !== "") { + this.instance.dataStore.searching = true; + this.instance.dataStore.page = 1; + + try { + this.instance.dataStore.currentView = await this.initiateSearch((document.getElementById("search") as HTMLInputElement).value.trim()); + } catch (e) { + console.error(e); + let query = (document.getElementById("search") as HTMLInputElement).value.trim(); + let sql: string; + + try { + sql = this.instance.dataStore.database.frontend.searchEngine.buildQueryV2(query, false); + } catch (e) {} + + this.searchError(e, query, sql); + } + + this.instance.display.updateDisplay(); + } else { + this.displayAll(); + } + + document.getElementById("images").classList.remove("searching"); + (document.getElementById('search') as HTMLInputElement).disabled = false; + document.getElementById('filter-bar').classList.remove("disabled"); + } + + resolveError(e: Error) { + if (e.name === "SyntaxError") { + if (e.message === "Unexpected end of input") { + return "Unclosed parenthesis or quote, or trailing operator"; + } + + if (e.message === "Unexpected token ')'") { + return "Empty parenthesis statement or trailing close parenthesis"; + } + } + + if (e.name === "SearchError" || e.stack.startsWith("SearchError: ")) { + return e.message; + } + + return "An error has occurred while processing your search query"; + } + + initiateSearch(query: string): Promise<any> { + return new Promise(async (res, rej) => { + try { + res(await this.instance.dataStore.database.frontend.search(query)); + } catch (e) { + rej(e); + } + }); + } + + loadSearchModule() { + setInterval(() => { + let _dataStore = this.instance.dataStore; + + if (new Date().getTime() - _dataStore.lastPress > 1000 && _dataStore.needUpdate && _dataStore.lastQuery.trim().toLowerCase() !== (document.getElementById('search') as HTMLInputElement).value.trim().toLowerCase()) { + _dataStore.needUpdate = false; + _dataStore.lastQuery = (document.getElementById('search') as HTMLInputElement).value; + (document.getElementById('search') as HTMLInputElement).disabled = true; + document.getElementById('filter-bar').classList.add("disabled"); + this.updateSearch(); + } + }, 50); + + document.getElementById("search").onkeydown = (event) => { + if (event.key === "Enter") { + this.instance.dataStore.lastPress = new Date().getTime() - 5000; this.instance.dataStore.needUpdate = true; + } else { + this.instance.dataStore.lastPress = new Date().getTime(); this.instance.dataStore.needUpdate = true; + } + } + } + + async viewPage(n: number) { + this.instance.dataStore.page = n; + this.instance.display.updateDisplay(); + } +} diff --git a/src/PrisbeamSettings.ts b/src/PrisbeamSettings.ts new file mode 100755 index 0000000..7ec0b2b --- /dev/null +++ b/src/PrisbeamSettings.ts @@ -0,0 +1,290 @@ +import {PrisbeamApp} from "./PrisbeamApp"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import {PrisbeamImageType, PrisbeamListType} from "libprisbeam"; + +export class PrisbeamSettings { + instance: PrisbeamApp; + + constructor(instance: PrisbeamApp) { + this.instance = instance; + } + + async getThumbnailFiles() { + let _dataStore = this.instance.dataStore; + let files = []; + let filesPre = await fs.promises.readdir(_dataStore.source + "/thumbnails"); + + for (let i of filesPre) { + if ((await fs.promises.lstat(_dataStore.source + "/thumbnails/" + i)).isDirectory()) { + let list = await fs.promises.readdir(_dataStore.source + "/thumbnails/" + i); + + for (let j of list) { + if ((await fs.promises.lstat(_dataStore.source + "/thumbnails/" + i + "/" + j)).isDirectory()) { + let list2 = await fs.promises.readdir(_dataStore.source + "/thumbnails/" + i + "/" + j); + + for (let k of list2) { + if ((await fs.promises.lstat(_dataStore.source + "/thumbnails/" + i + "/" + j + "/" + k)).isDirectory()) { + let list3 = await fs.promises.readdir(_dataStore.source + "/thumbnails/" + i + "/" + j + "/" + k); + + for (let l of list3) { + files.push(_dataStore.source + "/thumbnails/" + i + "/" + j + "/" + k + "/" + l); + } + } + } + } + } + } + } + + return files; + } + + async getImageFiles() { + let _dataStore = this.instance.dataStore; + let files = []; + let filesPre = await fs.promises.readdir(_dataStore.source + "/images"); + + for (let i of filesPre) { + if ((await fs.promises.lstat(_dataStore.source + "/images/" + i)).isDirectory()) { + let list = await fs.promises.readdir(_dataStore.source + "/images/" + i); + + for (let j of list) { + if ((await fs.promises.lstat(_dataStore.source + "/images/" + i + "/" + j)).isDirectory()) { + let list2 = await fs.promises.readdir(_dataStore.source + "/images/" + i + "/" + j); + + for (let k of list2) { + if ((await fs.promises.lstat(_dataStore.source + "/images/" + i + "/" + j + "/" + k)).isDirectory()) { + let list3 = await fs.promises.readdir(_dataStore.source + "/images/" + i + "/" + j + "/" + k); + + for (let l of list3) { + files.push(_dataStore.source + "/images/" + i + "/" + j + "/" + k + "/" + l); + } + } + } + } + } + } + } + + return files; + } + + async processList(files: string[]) { + let _dataStore = this.instance.dataStore; + + let total = files.length; + let index = 0; + let orphans = []; + + for (let file of files) { + document.getElementById("load").innerText = "Looking for orphan files... " + file; + + if (!(await _dataStore.database.frontend.getImage(path.basename(file, path.extname(file))))) { + orphans.push(file); + } + + index++; + document.getElementById("progress").style.width = ((index / total) * 100) + "%"; + } + + return orphans; + } + + async processOrphans(orphans: string[]): Promise<boolean> { + let hadErrors = false; + + document.getElementById("load").innerText = "Removing orphan files..."; + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("progress").style.width = "0%"; + + let index = 0; + + for (let orphan of orphans) { + document.getElementById("load").innerText = "Removing orphan files... " + orphan; + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("progress").style.width = ((index / orphans.length) * 100) + "%"; + + try { + await fs.promises.unlink(orphan); + } catch (e) { + console.error(e); + this.instance.loadingError("Failed to remove " + orphan); + hadErrors = true; + } + + index++; + } + + return hadErrors; + } + + async removeOrphans() { + let _dataStore = this.instance.dataStore; + if (_dataStore.loadedFromCache) return; + + _dataStore.loader.show(); + _dataStore.hadErrorsLoading = false; + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("loader-errors-list").innerHTML = ""; + document.getElementById("loader-errors").style.display = "none"; + + document.getElementById("load").innerText = "Looking for orphan files..."; + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("progress").style.width = "0%"; + + let files = [ + ...(await this.getThumbnailFiles()), + ...(await this.getImageFiles()) + ]; + + let hadErrors = false; + let orphans = await this.processList(files); + + if (orphans.length > 0) { + hadErrors = await this.processOrphans(orphans); + } + + document.getElementById("load").innerText = hadErrors ? "This operation completed with some errors." : "This operation completed successfully."; + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("progress").style.width = "100%"; + document.getElementById("loading-btn").classList.remove("disabled"); + document.getElementById("loader-errors").style.display = ""; + } + + private protectedEncode(b: string | ArrayBuffer) { + return require('zlib').deflateRawSync(b, {level: 9}); + } + + checkImageForCorruptions(image: any): Promise<boolean> { + let i = image; + let instance = this.instance; + let _dataStore = this.instance.dataStore; + + return new Promise<boolean>(async (res) => { + function next() { + let imageIsCorrupted: Function; + let img = i['mime_type'].startsWith("image/") ? new Image() : document.createElement("video"); + img.onload = img.oncanplaythrough = () => { + img.remove(); + if ("srcObject" in img) img.srcObject = null; + // noinspection HttpUrlsUsage + if (img.src.startsWith("https://") || img.src.startsWith("http://")) imageIsCorrupted(); + res(false); + } + img.onerror = imageIsCorrupted = () => { + instance.loadingError("Image " + i.id + " is corrupted"); + res(true); + } + img.src = _dataStore.database.frontend.getImageFile(i, PrisbeamImageType.ViewURL); + } + + let imageIsCorrupted: Function; + let img = new Image(); + img.onload = img.oncanplaythrough = () => { + img.remove(); + if ("srcObject" in img) img.srcObject = null; + // noinspection HttpUrlsUsage + if (img.src.startsWith("https://") || img.src.startsWith("http://")) imageIsCorrupted(); + next(); + } + img.onerror = imageIsCorrupted = () => { + instance.loadingError("Image " + i.id + " is corrupted"); + res(true); + } + img.src = _dataStore.database.frontend.getImageFile(i, PrisbeamImageType.ThumbnailURL); + }); + } + + async repairCorruptedImage(i: any) { + let _dataStore = this.instance.dataStore; + + try { + await fs.promises.writeFile(_dataStore.source + "/images/" + (i['sha512_hash'] ?? i['orig_sha512_hash'] ?? "0000000").substring(0, 1) + "/" + (i['sha512_hash'] ?? i['orig_sha512_hash'] ?? "0000000").substring(0, 2) + "/" + i.id + ".bin", this.protectedEncode(Buffer.from(await (await fetch(i['view_url'])).arrayBuffer()))); + await fs.promises.writeFile(_dataStore.source + "/thumbnails/" + (i['sha512_hash'] ?? i['orig_sha512_hash'] ?? "0000000").substring(0, 1) + "/" + (i['sha512_hash'] ?? i['orig_sha512_hash'] ?? "0000000").substring(0, 2) + "/" + i.id + ".bin", this.protectedEncode(Buffer.from(await (await fetch(i['representations']['thumb'])).arrayBuffer()))); + return true; + } catch (e) { + console.error(e); + this.instance.loadingError("Failed to repair image " + i.id); + return false; + } + } + + finishRepairing(hadErrorsFixing: boolean) { + if (this.instance.dataStore.hadErrorsLoading) { + if (hadErrorsFixing) { + document.getElementById("load").innerText = "Corrupted files found and could not be repaired."; + } else { + document.getElementById("load").innerText = "Corrupted files found and repaired successfully."; + } + + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("progress").style.width = "100%"; + document.getElementById("loading-btn").classList.remove("disabled"); + } else { + document.getElementById("load").innerText = "No corrupted files found."; + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("progress").style.width = "100%"; + document.getElementById("loading-btn").classList.remove("disabled"); + document.getElementById("loader-errors").style.display = ""; + } + } + + async repairScanForCorruptions() { + let corrupted = []; + let _dataStore = this.instance.dataStore; + + _dataStore.loader.show(); + _dataStore.hadErrorsLoading = false; + document.getElementById("progress").classList.remove("progress-bar-striped"); + document.getElementById("loader-errors-list").innerHTML = ""; + document.getElementById("loader-errors").style.display = "none"; + document.getElementById("load").innerText = "Checking for corrupted images..."; + + let total = await _dataStore.database.frontend.countImages(); + let index = 0; + + document.getElementById("progress").style.width = "0%"; + + for (let image of await _dataStore.database.frontend.getAllImages(PrisbeamListType.Array) as any[]) { + document.getElementById("load").innerText = "Checking for corrupted images... " + Math.round(((index / total) * 100)) + "% (" + image.id + ")"; + if (await this.checkImageForCorruptions(image)) { + corrupted.push(image); + } + + index++; + document.getElementById("progress").style.width = ((index / total) * 100) + "%"; + } + + document.getElementById("progress").style.width = "0%"; + document.getElementById("load").innerText = "Repairing corrupted images..."; + index = 0; + + return corrupted; + } + + async repairProcessCorruptions(corrupted: any[]) { + let hadErrorsFixing = false; + let index = 0; + + for (let file of corrupted) { + hadErrorsFixing = hadErrorsFixing && await this.repairCorruptedImage(file); + + document.getElementById("progress").style.width = ((index / corrupted.length) * 100) + "%"; + document.getElementById("load").innerText = "Repairing corrupted images... " + Math.round(((index / corrupted.length) * 100)) + "% (" + file.id + ")"; + index++; + } + + return hadErrorsFixing; + } + + async repairCorruptions() { + if (this.instance.dataStore.loadedFromCache) return; + + this.finishRepairing( + await this.repairProcessCorruptions( + await this.repairScanForCorruptions() + ) + ) + } +} diff --git a/src/PrisbeamUtilities.ts b/src/PrisbeamUtilities.ts new file mode 100755 index 0000000..455a4d7 --- /dev/null +++ b/src/PrisbeamUtilities.ts @@ -0,0 +1,75 @@ +export class PrisbeamUtilities { + static timeAgo(time: number | Date | string) { + if (!isNaN(parseInt(time as string))) { + time = new Date(time).getTime(); + } + + let periods = ["second", "minute", "hour", "day", "week", "month", "year", "age"]; + + let lengths = [60, 60, 24, 7, 4.35, 12, 100]; + + let now = new Date().getTime(); + + let difference = Math.round((now - (time as number)) / 1000); + let tense: string; + let period: string; + + if (difference <= 10 && difference >= 0) { + return "now"; + } else if (difference > 0) { + tense = "ago"; + } else { + tense = "later"; + } + + let j: number; + + for (j = 0; difference >= lengths[j] && j < lengths.length - 1; j++) { + difference /= lengths[j]; + } + + difference = Math.round(difference); + + period = periods[j]; + + return `${difference} ${period}${difference > 1 ? "s" : ""} ${tense}`; + } + + static formatSize(size: number) { + let sizeString: string; + + if (size > 1024 ** 3) { + sizeString = (size / 1024 ** 3).toFixed(1) + " GB"; + } else if (size > 1024 ** 2) { + sizeString = (size / 1024 ** 2).toFixed(1) + " MB"; + } else if (size > 1024) { + sizeString = (size / 1024).toFixed(0) + " KB"; + } else { + sizeString = size + " B"; + } + + return sizeString ?? size; + } + + static getMimeBadge(type: string) { + switch (type) { + case "image/gif": + return `<span style='float: right; margin-right: 10px; font-size: 1rem; margin-top: 5px;' class='badge bg-danger'>GIF</span>`; + + case "image/jpeg": + return `<span style='float: right; margin-right: 10px; font-size: 1rem; margin-top: 5px;' class='badge bg-warning'>JPEG</span>`; + + case "image/png": + return `<span style='float: right; margin-right: 10px; font-size: 1rem; margin-top: 5px;' class='badge bg-primary'>PNG</span>`; + + case "image/svg+xml": + return `<span style='float: right; margin-right: 10px; font-size: 1rem; margin-top: 5px;' class='badge bg-light'>SVG</span>`; + + case "video/webm": + return `<span style='float: right; margin-right: 10px; font-size: 1rem; margin-top: 5px;' class='badge bg-info'>WebM</span>`; + + default: + return `<span style='float: right; margin-right: 10px; font-size: 1rem; margin-top: 5px;' class='badge bg-secondary'>Unknown</span>`; + } + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100755 index 0000000..f6d526f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,41 @@ +import {ipcRenderer} from "electron"; +import {PrisbeamApp} from "./PrisbeamApp"; + +let loaded = false; + +window.onload = () => { + loaded = true; +} + +// noinspection JSUnusedGlobalSymbols +export function runApp(bootstrap: any) { + ipcRenderer.on('path', (_, appDataPath) => { + let instance = window['instance'] = new PrisbeamApp(bootstrap); + instance.dataStore.appData = appDataPath; + + let loadInterval = setInterval(() => { + if (loaded) { + clearInterval(loadInterval); + instance.loadApp(); + } + }); + + window.onclose = () => { + instance.dataStore.close = true; + } + + window.onbeforeunload = (e: DOMEvent) => { + if (instance.dataStore.database && !instance.dataStore.unloaded) { + e.preventDefault(); + instance.safeReload(); + } + }; + + document.getElementById("preview").addEventListener('hide.bs.modal', () => { + document.getElementById("preview-content").innerHTML = ""; + instance.display.updateTitle(); + }); + + instance.bootstrapTooltips(); + }); +} diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100755 index 0000000..e397c5f --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "baseUrl": "../node_modules", + "module": "commonjs", + "target": "es2021", + "sourceMap": true, + "esModuleInterop": true + }, + "exclude": [ + "node_modules" + ], +} |