aboutsummaryrefslogtreecommitdiff
path: root/src/FaunerieAppDisplay.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/FaunerieAppDisplay.ts')
-rwxr-xr-xsrc/FaunerieAppDisplay.ts455
1 files changed, 455 insertions, 0 deletions
diff --git a/src/FaunerieAppDisplay.ts b/src/FaunerieAppDisplay.ts
new file mode 100755
index 0000000..66f592b
--- /dev/null
+++ b/src/FaunerieAppDisplay.ts
@@ -0,0 +1,455 @@
+import {FaunerieApp} from "./FaunerieApp";
+import {FaunerieImageType} from "libfaunerie";
+import * as fs from "node:fs";
+import {FaunerieUtilities} from "./FaunerieUtilities";
+import {shell} from "@electron/remote";
+
+export class FaunerieAppDisplay {
+ private instance: FaunerieApp;
+
+ constructor(app: FaunerieApp) {
+ 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, FaunerieImageType.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(&quot;${thumbnail}&quot;);
+ "
+ 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 + ") — Faunerie (Cached)";
+ } else {
+ document.title = "Searching for " + (document.getElementById("search") as HTMLInputElement).value.trim() + " (page " + _dataStore.page + "/" + totalPages + ") — Faunerie";
+ }
+ } else {
+ if (_dataStore.loadedFromCache) {
+ document.title = "All images (page " + _dataStore.page + "/" + totalPages + ") — Faunerie (Cached)";
+ } else {
+ document.title = "All images (page " + _dataStore.page + "/" + totalPages + ") — Faunerie";
+ }
+ }
+ }
+
+ 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>)"
+ : "") +
+ "" +
+ FaunerieUtilities.getMimeBadge(_dataStore.currentImage.mime_type)
+ }
+
+ displayImageSize() {
+ let _dataStore = this.instance.dataStore;
+ let size = 0;
+ let sizeExplanation = [];
+
+ let file = _dataStore.database.frontend.getImageFile(_dataStore.currentImage, FaunerieImageType.ViewFile);
+ let thumb = _dataStore.database.frontend.getImageFile(_dataStore.currentImage, FaunerieImageType.ThumbnailFile);
+
+ if (file) {
+ let cSize = fs.lstatSync(file).size;
+ size += cSize;
+ sizeExplanation.push("Image: " + FaunerieUtilities.formatSize(cSize));
+ }
+
+ if (thumb) {
+ let cSize = fs.lstatSync(thumb).size;
+ size += cSize;
+ sizeExplanation.push("Thumbnail: " + FaunerieUtilities.formatSize(cSize));
+ }
+
+ let cSize = JSON.stringify(_dataStore.currentImage).length;
+ size += cSize;
+ sizeExplanation.push("Metadata: " + FaunerieUtilities.formatSize(cSize));
+
+ document.getElementById("preview-size").innerHTML =
+ "<span data-bs-toggle='tooltip' data-bs-html='true' title='" + sizeExplanation.join("<br>") + "'>" +
+ FaunerieUtilities.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() + "'>" + FaunerieUtilities.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, FaunerieImageType.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']));
+ }
+}