// ==UserScript==
// @name DeviantArt Search Galleries and Favorites
// @namespace http://tampermonkey.net/
// @version 0.3.4
// @description Creates a search function that works on artist galleries and favorites collections. Search by deviation title and artist name. Numerous sorting options.
// @author corepower
// @match https://www.deviantart.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=deviantart.com
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
/////////////////////////////////////////////////////////
////// Default User Preference Variables //////
// Maximum search return: Maximum number of results that will tile after a search. This can be set as large as you like, but the page may slow down.
// Possibly even consider lowering it if things are laggy while searching.
let maximumsearchreturn = 1000;
// Sort by: based on setting, search results will be sorted based on description in the value. The "sortoptions" array holds the possible options for sorting.
// The "Default" option is based on date added to the collection. Matches DeviantArt default.
const sortoptions = ["Default", "Post Date(Desc)", "Post Date(Asc)", "Title(Desc)", "Title(Asc)", "Artist(Desc)", "Artist(Asc)", "Favorites(Desc)", "Favorites(Asc)", "Views(Desc)", "Views(Asc)"];
let sortby = sortoptions[0];
// Pagination rate limit: the amount of time, in seconds to delay downloading gallery/collection page results during search indexing.
// Can be set to 0 but if lots of large galleries are indexed, DeviantArt may automatically temp ban your account/IP.
// Note: Multiple searches on a single page will not rerun the index, therefore unlimited searches may be run on a single page without consequence as long as the page is not navigated or refreshed.
let paginationratelimit = 1.0;
// Rate limit warning: If true, puts a warning message in the search output text while indexing about rate limits if the rate limit is under two seconds on a large search.
// Set to false to disable warning.
let ratelimitwarning = true;
// Pivotal row height: height, in pixels, that the dynamic deviation tiles will be based around. Tiles will be this tall or larger.
let pivotalrowheight = 280;
//// End Default User Preference Variables //////
/////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////
////// Desired Features List - NOT YET IMPLEMENTED //////
// These features may come in future versions.
// * Navigate within DeviantArt and reset search capability based on new page (partially implemented)
// * Dynamic tile visibility so that any number of search results can be efficiently displayed without slowing down the page.
// * Dynamic re-tiling on viewport size change
// * React framework tie-ins for things like element creation, event listeners, and deviation favoriting. This may not be feasible. I don't know React.
////// End Desired Features List //////
/////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////
////// Other Constants //////
//Define constants for text style toggles
const regularblacktextcolor = "black";
const regularwhitetextcolor = "rgb(242, 242, 242)";
const warningtextcolor = "orange";
const errortextcolor = "red";
// Preferred size: this is a named identifier for a size of DeviantArt generated thumbnails. Larger numbers are generally higher quality.
// Set this variable based on your internet speed or personal preference.
// Choose a value from amongst the following list: ["92S", "150", "200H", "300W", "375W", "414W", "250T", "350T","400T", "preview", "social_preview"]
const preferredsize = "350T";
// Tile margin: margin between deviation tiles, in pixels. DeviantArt uses 4 pixels, don't know why you would change this but you can.
const tilemargin = 4;
// Debug: debug flag turns on various console outputs for debugging. Outputs may or may not make sense, they were added as needed.
const debug = false;
////// End Other Constants //////
/////////////////////////////////////////////////////////
//Special event listener for React-style internal navigation
//Credit to Raja Osama: https://rajaosama.me/blogs/detect-react-route-change-in-vanilla-js
//Re-run entire script after this type of navigation
let monitored_url = location.href;
document.body.addEventListener('click', ()=>{
requestAnimationFrame(()=>{
if(monitored_url!==location.href){
let old_url = monitored_url;
monitored_url = location.href
//DEBUG
if(debug && false) console.log('url changed to: ', monitored_url);
//Check for specific case where navigation is between Galleries and Favorites on a single artist
let old_url_parts = old_url.split("/");
let monitored_url_parts = monitored_url.split("/");
//Disregard these base path type of /artist
if(old_url_parts.length < 5 || monitored_url_parts.length < 5)
return;
//Check for if navigations from gallery -> favourite or favourite -> gallery
let is_old_url_favourite = old_url_parts[4] == "favourites";
let is_new_url_favourite = monitored_url_parts[4] == "favourites";
if( is_old_url_favourite ? !is_new_url_favourite : is_new_url_favourite ) {
return;
//TODO modify main script execution for this type of internal navigation then enable observer by uncommenting code below
//set observer on this element to load
// waitForElm('._3h7d3').then((elm) => {
// if(debug && true) console.log('Element ._3h7d3 is ready');
// //executeScript();
// });
}
else { //not a problem navigation, run script regularly
executeScript();
}
}
});
}, true);
//DEBUG
if(debug && false) console.log("Entrypoint ready state: ", document.readyState);
if(debug && false) document.addEventListener('readystatechange', () => console.log("Ready state change: ", document.readyState));
//Page needs to be fully loaded for script to work
if(document.readyState == "loading" || document.readyState == "interactive")
window.addEventListener('load',executeScript);
else
executeScript();
//Function wrapper for the entire script to enable document readiness check above, as well as for re-running after React-style navigation events
function executeScript() {
//DEBUG
if(debug && false) console.log("Entered script execution.");
if(debug && false) console.log("Internal entrypoint ready state: ", document.readyState);
//Pull in existing user preference settings to globals, re-store to confirm all of them exist in localStorage. Use global variables for get() actions, update globals and localStorage with set() actions.
getUserPreferencesFromLocalStorage();
//Only execute script on gallery pages, otherwise return immediately
let pathparts = window.location.pathname.split("/");
if(pathparts.length < 3) {
return;
}
if(pathparts[2] != "gallery" && pathparts[2] != "favourites") {
return;
}
//Creating and adding this search element structure to the DOM
//<div class="_1hkGk DRK5r _1bp4v">
// <span class="DLN_H">
// <span class="z8jNZ _1yoxj _2F1i2">
// <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
// <path d="M19 11a8 8 0 10-3.095 6.32l3.63 3.63.095.083a1 1 0 001.32-1.498l-3.63-3.63A7.965 7.965 0 0019 11zM5 11a6 6 0 1112 0 6 6 0 01-12 0z"></path>
// </svg>
// </span>
// </span>
// <input type="text" class="aFKMF _3kAA3 _2KZ9p" aria-invalid="false" id="search-gallery" autocomplete="off" placeholder="Search Gallery" value="">
//</div>
let searchparentparent = document.getElementsByClassName("_3h7d3")[0];
let searchparent = document.createElement("div");
searchparent.className = "_1hkGk DRK5r _1bp4v";
searchparent.style.width = "350px";
let searchiconparentparent = document.createElement("span");
searchiconparentparent.className = "DLN_H";
let searchiconparent = document.createElement("span");
searchiconparent.className = "z8jNZ _1yoxj _2F1i2";
var searchicon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
searchicon.setAttribute("viewBox", "0 0 24 24");
var searchiconpath = document.createElementNS("http://www.w3.org/2000/svg", "path");
searchiconpath.setAttribute("d", "M19 11a8 8 0 10-3.095 6.32l3.63 3.63.095.083a1 1 0 001.32-1.498l-3.63-3.63A7.965 7.965 0 0019 11zM5 11a6 6 0 1112 0 6 6 0 01-12 0z");
let searchelement = document.createElement('input');
searchelement.type = 'text';
searchelement.className = 'aFKMF _3kAA3 _2KZ9p';
searchelement.setAttribute('aria-invalid', 'false');
searchelement.id = 'search-gallery';
searchelement.autocomplete = 'off';
searchelement.placeholder = 'Search Gallery';
searchelement.value = '';
searchicon.appendChild(searchiconpath);
searchiconparent.appendChild(searchicon);
searchiconparentparent.appendChild(searchiconparent);
searchparent.appendChild(searchiconparentparent);
searchparent.appendChild(searchelement);
//Create search results text output and put it next to the search box
let searchoutputtext = document.createElement("span");
searchoutputtext.style.margin = "8px";
searchoutputtext.style.maxWidth = "1000px";
searchparentparent.appendChild(searchoutputtext);
searchparentparent.appendChild(searchparent);
//Create Settings Cog. Minified because we don't need access to any of the inner elements.
let settingscogdiv = document.createElement("div");
settingscogdiv.style.position = "relative";
settingscogdiv.id = "settingscog";
settingscogdiv.innerHTML = '<span class="DLN_H"><span class="z8jNZ _1yoxj _2F1i2"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="width: 26px;height: 26px;padding-left: 8px;color: #d9d9d9;padding-right: 4px;" xml:space="preserve"><g><g><path d="M451.528,198.531c-4.088-13.938-9.657-27.369-16.645-40.14l42.774-42.773l-81.273-81.274l-42.774,42.773c-12.771-6.987-26.201-12.557-40.14-16.644V0H198.531v60.472c-13.939,4.088-27.369,9.657-40.14,16.644l-42.774-42.773l-81.273,81.274l42.774,42.773c-6.988,12.771-12.558,26.202-16.645,40.14H0v114.939h60.472c4.088,13.938,9.657,27.369,16.645,40.14l-42.774,42.773l81.273,81.274l42.774-42.773c12.771,6.987,26.201,12.557,40.14,16.644V512h114.939v-60.472c13.939-4.088,27.369-9.657,40.14-16.644l42.774,42.773l81.273-81.274l-42.774-42.773c6.988-12.771,12.558-26.202,16.645-40.14H512v0V198.531H451.528z M480.653,282.122h-53.755l-2.769,12.204c-4.301,18.952-11.756,36.932-22.158,53.441l-6.672,10.589l38.026,38.025l-36.942,36.942l-38.027-38.026l-10.589,6.672c-16.507,10.402-34.487,17.856-53.44,22.157l-12.204,2.771v53.755h-52.245v-53.755l-12.205-2.77c-18.953-4.301-36.933-11.755-53.44-22.157l-10.589-6.672l-38.027,38.026l-36.941-36.943l38.027-38.026l-6.672-10.589c-10.402-16.508-17.857-34.489-22.158-53.441l-2.77-12.203H31.347v-52.245h53.755l2.769-12.204c4.301-18.952,11.756-36.932,22.158-53.441l6.672-10.589l-38.026-38.025l36.942-36.942l38.027,38.026l10.589-6.672c16.507-10.402,34.487-17.856,53.44-22.157l12.204-2.771V31.347h52.245v53.755l12.205,2.77c18.953,4.301,36.933,11.755,53.44,22.157l10.589,6.672l38.027-38.026l36.942,36.942L395.3,153.643l6.672,10.589c10.402,16.508,17.857,34.489,22.158,53.441l2.769,12.204h53.755V282.122z"></path></g></g><g><g><path d="M256,135.837c-66.258,0-120.163,53.905-120.163,120.163c0,66.258,53.905,120.163,120.163,120.163c66.258,0,120.163-53.905,120.163-120.163S322.258,135.837,256,135.837z M256,344.816c-48.973,0-88.816-39.843-88.816-88.816s39.843-88.816,88.816-88.816s88.816,39.843,88.816,88.816S304.973,344.816,256,344.816z"></path></g></g></svg></span></span>'
//Create toggleable settings menu
let settingsmenudiv = createSettingsMenu();
searchparentparent.appendChild(settingscogdiv);
//Add event listeners
searchelement.addEventListener("focus", buildIndex, {once : true});
searchelement.addEventListener("keyup", search);
settingscogdiv.firstChild.addEventListener("click", toggleSettingsElement);
document.addEventListener("click", hideSettingsElement);
//define persistent globals for search indexing
const csrftoken = window.__CSRF_TOKEN__;
const artist_friendly_id = pathparts[1];//from the early URL path deconstruction
let deviations;//this will be objects to index search with
let matchingdeviations;//this will be objects that match the search
let initialstatescripttext;
let initialstatematch;
let i = 0;
while(initialstatematch == null) {
initialstatescripttext = document.getElementsByTagName("script")[i].innerHTML;
initialstatematch = initialstatescripttext.match(/\.__INITIAL_STATE__\s*=\s*JSON\.parse\(("[^\n]+")/);
i++;
}
if(initialstatematch.length < 2) {
console.error("DeviantArt Gallery Search Userscript Error: Could not parse script tag for __INITIAL_STATE__")
searchelement.placeholder = 'Script Error';
return;
}
let initialstateJSON = JSON.parse(eval(initialstatematch[1]));
//console.log(initialstateJSON);
let isgallery = pathparts[2] == "gallery";
let collectionorgalleryfoldername = isgallery ? "galleryFolder" : "collectionFolder";
let foldersJSON = initialstateJSON["@@entities"][collectionorgalleryfoldername];
//console.log(foldersJSON);
let isallfolder = false;
if(pathparts.length >= 4 && pathparts[3] == "all") {
isallfolder = true;
}
let folderid = "-1";// folder id of "-1" corresponds to "All" gallery
let gallerysize = 0;
if(!isallfolder) {
//Get folder id from current url as most up-to-date source, else default assigned folder id from initial state
//TODO this is imperfect. Fails if internal React navigation between galleries and favorites is initiated.
if(pathparts.length >= 4) {
folderid = pathparts[3];
}
else {//Featured or imperfect navigation, for now just handle featured
for(let folder in foldersJSON) {
if(foldersJSON[folder].name == "Featured"){
folderid = folder;
break;
}
}
//folderid = initialstateJSON["gallectionSection"]["currentlyViewedFolderId"];
}
}
gallerysize = foldersJSON[folderid]["totalItemCount"];
//construct reuseable gallery pagination api string pieces
let paginationstring_base = "https://www.deviantart.com/_napi/da-user-profile/api/"
+ (isgallery ? "gallery" : "collection")
+ "/contents?username=" + artist_friendly_id
+ "&csrf_token=" + csrftoken
+ (isallfolder ? "&all_folder=true" : "&folderid=" + folderid)
+ "&limit=60&offset=";
//Get remaining UI elements needed for reference
let itemscontainer = document.getElementsByClassName("RMUi2")[0];
let searchitemscontainer = itemscontainer.cloneNode();
itemscontainer.parentNode.appendChild(searchitemscontainer);
let viewportwidth;
searchelement.addEventListener("focus", setViewportWidth);
//Create cloneable deviation elements, one for each type. Clone operations are faster?
var cloneableimagedeviationelement = createCloneableImageDeviationElement();
var cloneableliteraturedeviationelement = createCloneableLiteratureDeviationElement();
var cloneablejournaldeviationelement = createCloneableJournalDeviationElement();
//This function indexes the gallery to be searched on textbox focus
async function buildIndex() {
searchelement.placeholder = 'Building Search Index...';
searchelement.blur();
searchelement.setAttribute('readonly', 'readonly');
//DEBUG indexing performance
let starttime;
if(debug && true) {
starttime = performance.now();
}
let currentoffset = 0;
let paginated_urls = [];
while(currentoffset < gallerysize) {
let currentpaginationurl = paginationstring_base + currentoffset;
paginated_urls.push(currentpaginationurl);
currentoffset += 60;//60 is maximum pagination
}
//DEBUG
if(debug && false) console.log(paginated_urls);
await concatenateJson(paginated_urls)
.then(concatenatedJson => {
//prepare indexable items by making them lowercase
let lowercasejson = concatenatedJson;
for(let i=0; i< lowercasejson.length; i++) {
let deviation = lowercasejson[i].deviation;
compileGSVariables(deviation, i);
trimFat(deviation);
}
deviations = lowercasejson;
sort();
});
searchelement.removeAttribute('readonly');
searchelement.focus();
searchelement.placeholder = 'Search Gallery';
//DEBUG indexing performance
let endtime;
if(debug && true) {
endtime = performance.now();
console.log("Indexing took " + (endtime - starttime) + " milliseconds for " + paginated_urls.length + " page" + (paginated_urls.length == 1 ? "." : "s."))
}
}
//Basic JSON fetch from URL function
async function fetchJson(url) {
// Use the fetch API to download the URL
const response = await fetch(url);
// Parse the JSON result from the response
const json = await response.json();
return json;
}
//Fetch all jsons then concatenate
async function concatenateJson(urls) {
let results = [];
//Convert user preference on rate limit to useable millisecond value
let waitmilliseconds = Math.ceil(paginationratelimit * 1000);
let pagedownloadestimate = paginationratelimit + 0.306;// 306 milliseconds based on average download test
let friendlytimeremaing = pagedownloadestimate * urls.length;
if(urls.length > 30 && paginationratelimit < 2.0 && ratelimitwarning) {
searchoutputtext.style.color = warningtextcolor;
searchoutputtext.innerText = "This is a large gallery to search. If you run searches like this frequently, "
+ "consider raising the rate limit to 2 seconds or longer to avoid DeviantArt temp bans. "
+ "Rate limits can be changed and this warning can be turned off in the settings next to the search box.";
}
// Loop through the URLs
for (let i=0; i<urls.length; i++) {
//Show rough indexing time remaining to the user. The bulk of the indexing time comes from this rate limited function
if(urls.length != 1) {
searchelement.placeholder = 'Building Search Index... (' + Math.ceil(friendlytimeremaing) + " seconds remaining)";
//DEBUG
if(debug && false) console.log("Time remaining: " + friendlytimeremaing);
friendlytimeremaing -= pagedownloadestimate;
}
//Set wait time between JSON fetches, only if there is a "between" to begin with. Also only do this if rate limit is not 0.
if(i > 0 && waitmilliseconds != 0) {
//DEBUG
if(debug && false) console.log("Waiting " + waitmilliseconds + " milliseconds.");
await new Promise(resolve => setTimeout(resolve, waitmilliseconds));
}
//DEBUG
if(debug && false) console.log("Download round " + i);
// Download the current URL and concatenate the JSON result
let response = await fetchJson(urls[i]);
let json = response.results;
results = results.concat(json);
}
//Reset output text after possible rate limit warning.
searchoutputtext.style.color = regularwhitetextcolor;
searchoutputtext.innerText = "";
return results;
}
//Perform onetime calculations on deviation objects for indexing, searching, and rendering tasks
function compileGSVariables(deviation, index)
{
deviation.gs_username = deviation.author.username.toLowerCase();
deviation.gs_title = deviation.title.toLowerCase().trim();
deviation.gs_default_order = index;
deviation.gs_favorites = deviation.stats.favourites;
deviation.gs_views = deviation.stats.views;
//Convert datetime to unix milliseconds and store
const pubDate = new Date(deviation.publishedTime )
deviation.gs_published_time = pubDate.getTime();
//Collect static thumbnail meta info. easier to do once
for(let i=0; i<deviation.media.types.length; i++) {
if(deviation.media.types[i].t == preferredsize) {
deviation.gs_thumb_path_string = deviation.media.types[i].c;
deviation.gs_thumb_width = deviation.media.types[i].w;
deviation.gs_thumb_height = deviation.media.types[i].h;
break;
}
}
//Set text-based deviations to have square tiles
if(deviation.type == "journal" || deviation.type == "literature" || deviation.type == "status") {
deviation.gs_thumb_width = 300;
deviation.gs_thumb_height = 300;
}
}
//Reduce deviation object memory usage, hopefully speeds up searches
function trimFat(deviation)
{
delete deviation.author.isGroup;
delete deviation.author.isNewDeviant;
delete deviation.author.isSubscribed;
delete deviation.author.isWatching;
delete deviation.author.type;
delete deviation.author.userId;
delete deviation.author.useridUuid;
delete deviation.blockReasons;
delete deviation.deviationId;
delete deviation.hasNft;
delete deviation.hasPrivateComments;
delete deviation.isAdoptable;
delete deviation.isAiUseDisallowed;
delete deviation.isAntisocial;
delete deviation.isBackgroundEditable;
delete deviation.isBlocked;
delete deviation.isCommentable;
delete deviation.isDailyDeviation;
delete deviation.isDeleted;
delete deviation.isDownloadable;
delete deviation.isDreamsofart;
delete deviation.isFavouritable;
delete deviation.isFavourited;
delete deviation.isJournal;
delete deviation.isMature;//TODO add to filter option section?
delete deviation.isNsfg;
delete deviation.isPublished;
delete deviation.isShareable;
delete deviation.isTextEditable;
delete deviation.isVideo;
delete deviation.legacyTextEditUrl;
delete deviation.matureLevel;
//delete deviation.media.
delete deviation.printId;
}
//Searches through deviations array on keyup event
function search() {
//DEBUG indexing performance
let starttime;
if(debug && true) {
starttime = performance.now();
}
let currentsearchtext = searchelement.value.toLowerCase();
//DEBUG
if(debug && true) console.log("Searching: " + currentsearchtext);
//Restore gallery visibility and abort if search goes inactive
if(currentsearchtext == "") {
itemscontainer.style.display = "";
searchitemscontainer.style.display = "none";
searchoutputtext.innerText = "";
return;
}
//hide original gallery, show custom search gallery
itemscontainer.style.display = "none";
searchitemscontainer.style.display = "";
//search
let resultsarray = [];
for(let i=0; i< deviations.length; i++) {
let deviation = deviations[i].deviation;
if(deviation.gs_username.includes(currentsearchtext)) {
resultsarray.push(deviation);
continue;
}
if(deviation.gs_title.includes(currentsearchtext)) {
resultsarray.push(deviation);
continue;
}
}
matchingdeviations = resultsarray;
//Now display based on results
if(resultsarray.length == 0) {
searchoutputtext.innerText = "No results.";
return;
}
//DEBUG
if(debug && false) console.log("Results:", matchingdeviations);
if(resultsarray.length > maximumsearchreturn) {
searchoutputtext.innerText = resultsarray.length + " results. Too many for display. (Maximum is set to " + maximumsearchreturn + " results)";
return;
}
searchoutputtext.innerText = resultsarray.length + " result" + (resultsarray.length == 1 ? "." : "s.");
assignTileDimensions();
tileElements();
//DEBUG indexing performance
let endtime;
if(debug && true) {
endtime = performance.now();
console.log("Searching and rendering took " + (endtime - starttime) + " milliseconds for " + deviations.length + " items.");
}
//DEBUG sort order
if(debug && false) {
console.log("-----------")
for(let i=0; i<matchingdeviations.length; i++) {
console.log(matchingdeviations[i].gs_default_order);
}
}
}
//Sorts all deviations in the collection based on "sortby" option
var lastsort;
function sort() {
//avoid re-sorting when list is already sorted for a given sorting option
if(sortby == lastsort)
return;
lastsort = sortby;
if(sortby == "Default") {
deviations = sortDeviationsByKey(deviations, "gs_default_order", true);
}
else if(sortby == "Post Date(Desc)") {
deviations = sortDeviationsByKey(deviations, "gs_published_time", false);
}
else if(sortby == "Post Date(Asc)") {
deviations = sortDeviationsByKey(deviations, "gs_published_time", true);
}
else if(sortby == "Title(Desc)") {
deviations = sortDeviationsByKey(deviations, "gs_title", false);
}
else if(sortby == "Title(Asc)") {
deviations = sortDeviationsByKey(deviations, "gs_title", true);
}
else if(sortby == "Artist(Desc)") {
deviations = sortDeviationsByKey(deviations, "gs_username", false);
}
else if(sortby == "Artist(Asc)") {
deviations = sortDeviationsByKey(deviations, "gs_username", true);
}
else if(sortby == "Favorites(Desc)") {
deviations = sortDeviationsByKey(deviations, "gs_favorites", false);
}
else if(sortby == "Favorites(Asc)") {
deviations = sortDeviationsByKey(deviations, "gs_favorites", true);
}
else if(sortby == "Views(Desc)") {
deviations = sortDeviationsByKey(deviations, "gs_views", false);
}
else if(sortby == "Views(Asc)") {
deviations = sortDeviationsByKey(deviations, "gs_views", true);
}
else {
console.error("Unknown sort by option used.")
}
}
//This function matches row widths, row height preference, and image aspect ratios to tile items evenly in every row.
function assignTileDimensions() {
//let viewportwidth = parseFloat(window.getComputedStyle(itemscontainer).width);
//DEBUG
if(debug && false) console.log("Viewport width: ", viewportwidth);
//begin tiling rows
let useditemscount = 0;
while(useditemscount < matchingdeviations.length)
{
let currentindex = useditemscount;
//discover row item count based on pivotal row height
let rowitemcount = 0;
let currenttotalaspectedwidth = 0;
let isfinalrow = false;
while(currentindex < matchingdeviations.length)
{
let croppedwidth = matchingdeviations[currentindex].gs_thumb_width;
let croppedheight = matchingdeviations[currentindex].gs_thumb_height;
let aspectedwidth = getAspectedWidth(croppedwidth, croppedheight, pivotalrowheight);
currenttotalaspectedwidth += aspectedwidth + (tilemargin*2);
if(currenttotalaspectedwidth >= viewportwidth)
{
if(rowitemcount == 0)//Handles case where the first image in the row has a greater aspected width than the viewport, previously caused infinite loops
{
if(debug && true) console.log(currentindex);
rowitemcount++;
currentindex++;
}
break;
}
else
{
rowitemcount++;
currentindex++;
}
//last row case detector
if(currentindex == matchingdeviations.length)
{
isfinalrow = true;
}
}
//now we have row item count, size to fill usable viewport width and then aspect for final row height
//console.log("Row item count: ", rowitemcount);
var useableviewportwidth = viewportwidth - (rowitemcount*tilemargin*2);
//console.log("Useable viewport width: ", useableviewportwidth);
currentindex = useditemscount;//reset to beginning of row index
if(!isfinalrow)//perform sizing calculations on every row but the last
{
//get width ratio denominator
var sumwidthratios = 0;
for(let i=0; i<rowitemcount; i++)
{
let croppedwidth = matchingdeviations[currentindex + i].gs_thumb_width;
let croppedheight = matchingdeviations[currentindex + i].gs_thumb_height;
sumwidthratios += croppedwidth/croppedheight;
}
//set final widths using width ratio percentage of total summed width ratios, set final height one time off of final width
let finalheight = 0;
let remainder = 0;//pass on unused pixel space to the next element
for(let i=0; i<rowitemcount; i++)
{
let currenttiledeviation = matchingdeviations[currentindex + i];
let croppedwidth = matchingdeviations[currentindex + i].gs_thumb_width;
let croppedheight = matchingdeviations[currentindex + i].gs_thumb_height;
let currentwidthratio = croppedwidth/croppedheight;
let exactfinalwidth = (currentwidthratio * useableviewportwidth / sumwidthratios) + remainder;
let finalwidth = Math.floor(exactfinalwidth);
remainder = exactfinalwidth - finalwidth;
if(i == 0)
{
finalheight = getAspectedHeight(croppedwidth, croppedheight, finalwidth);
}
//The current tile widths will change every time the viewport size changes or the search results change
currenttiledeviation.gs_tile_width = finalwidth + "px";
currenttiledeviation.gs_tile_height = finalheight + "px";
}
useditemscount += rowitemcount;
}
else//final row, size based off of simple pivotal row height
{
let remainder = 0;//pass on unused pixel space to the next element
let finalheight = 0;
for(let i=0; i<rowitemcount; i++)
{
let currenttiledeviation = matchingdeviations[currentindex + i];
let croppedwidth = matchingdeviations[currentindex + i].gs_thumb_width;
let croppedheight = matchingdeviations[currentindex + i].gs_thumb_height;
let exactfinalwidth = getAspectedWidth(croppedwidth, croppedheight, pivotalrowheight);
let finalwidth = Math.floor(exactfinalwidth);
remainder = exactfinalwidth - finalwidth;
if(i == 0)
{
finalheight = getAspectedHeight(croppedwidth, croppedheight, finalwidth);
}
//The current tile widths will change every time the viewport size changes or the search results change
currenttiledeviation.gs_tile_width= finalwidth + "px";
currenttiledeviation.gs_tile_height= finalheight + "px";
}
useditemscount += rowitemcount;
}
//DEBUG
//break;
}
}
//This function orders the creation the tiles for each deviation based on search results
function tileElements() {
//Reset search items container
searchitemscontainer.innerText = "";
for(let i=0; i<matchingdeviations.length; i++) {
let currenttileelement = createDeviationElement(matchingdeviations[i])
searchitemscontainer.appendChild(currenttileelement);
}
}
//This function differentiates the deviation tiles and calls the appropriate createElement() function
function createDeviationElement(deviation) {
if(deviation.type == "literature") {
return createLiteratureDeviationElement(deviation);
}
else if(deviation.type == "journal" || deviation.type == "status") {
return createJournalDeviationElement(deviation);
}
else {// image type and other types
//DEBUG
if(debug && true) {
if(deviation.type != "image" && deviation.type != "pdf" && deviation.type != "film")
console.log(deviation.type);
}
return createImageDeviationElement(deviation);
}
}
function createImageDeviationElement(deviation)
{
let newtileelement = cloneableimagedeviationelement.cloneNode(true);
newtileelement.style.width = deviation.gs_tile_width;
newtileelement.style.height = deviation.gs_tile_height;
//construct and set thumbnail url
let thumbpathstring = deviation.gs_thumb_path_string;
thumbpathstring = thumbpathstring.replace("<prettyName>", deviation.media.prettyName);
let tokenstring = deviation.media.token == null ? "" : "?token=" + deviation.media.token[0];
let thumburl = deviation.media.baseUri + thumbpathstring + tokenstring;
//Dive the cloned DOM for these assignments
let draggablecontainer = newtileelement.children[0];
let artlinkelement = draggablecontainer.children[0];
let outermousediv = draggablecontainer.children[1];//_2jPGh _3Cax3
let imgelement = artlinkelement.children[0].children[0];
let blackfadeouterdiv = outermousediv.children[1].children[0].children[0];//_1mmGw _31MCr
let iconartistcontainer2 = blackfadeouterdiv.children[1].children[0];//_2o1Q1
let arttitlelinkelement = blackfadeouterdiv.children[0];
let artisticonlinkelement = iconartistcontainer2.children[0].children[0];
let artisticonelement = artisticonlinkelement.children[0];
let artistnamelinkelement = iconartistcontainer2.children[1].children[0];
let artistnametextelement = artistnamelinkelement.children[0];
let commentlinkelement = outermousediv.children[1].children[0].children[1].children[0];//_1-Wh7 x48yz
let commentcountspan = commentlinkelement.children[1];
imgelement.src = thumburl;
imgelement.alt = deviation.title;
artlinkelement.href = deviation.url;
arttitlelinkelement.href = deviation.url;
let arttitleelement = outermousediv.children[1].children[0].children[0].children[0].children[0];
arttitleelement.innerText = deviation.title;
//mouseover events attach to link parent
draggablecontainer.addEventListener("mouseover", (event) => { outermousediv.style.visibility = ""; });
draggablecontainer.addEventListener("mouseout", (event) => { outermousediv.style.visibility = "hidden"; });
artisticonlinkelement.href = "https://www.deviantart.com/" + deviation.gs_username;
artisticonelement.alt = deviation.author.username + "'s avatar";
artisticonelement.src = deviation.author.usericon;
artistnamelinkelement.href = "https://www.deviantart.com/" + deviation.gs_username;
artistnametextelement.innerText = deviation.author.username;
commentlinkelement.href = deviation.url + "#comments";
commentcountspan.innerText = deviation.stats.comments;
return newtileelement;
}
function createLiteratureDeviationElement(deviation)
{
let newtileelement = cloneableliteraturedeviationelement.cloneNode(true);
newtileelement.style.width = deviation.gs_tile_width;
newtileelement.style.height = deviation.gs_tile_height;
//Dive the cloned DOM for these assignments
let draggablecontainer = newtileelement.children[0];
let sectionelement = draggablecontainer.children[0];
let literaturepreviewtitle = sectionelement.children[2];//_2mwJN
let literaturepreviewtext = sectionelement.children[3];//heXvc
let deviationlink = draggablecontainer.children[1];//_1vRyy
let outermousediv = draggablecontainer.children[2];//_2jPGh _3Cax3
let iconartistcontainer2 = outermousediv.children[1].children[0].children[0].children[0].children[0];//_2o1Q1
let artisticonlinkelement = iconartistcontainer2.children[0].children[0];//user-link _2f0dA _23x0l
let artisticonelement = artisticonlinkelement.children[0];//_1IDJa
let artistnamelinkelement = iconartistcontainer2.children[1].children[0];//user-link _2f0dA
let artistnametextelement = artistnamelinkelement.children[0];//_2UI2c
let commentlinkelement = outermousediv.children[1].children[0].children[1].children[0]//_1-Wh7 x48yz
let commentcountspan = commentlinkelement.children[1];
literaturepreviewtitle.innerText = deviation.title;
literaturepreviewtext.innerText = deviation.textContent.excerpt;
deviationlink.href = deviation.url;
//mouseover events attach to link parent
draggablecontainer.addEventListener("mouseover", (event) => { outermousediv.style.visibility = ""; });
draggablecontainer.addEventListener("mouseout", (event) => { outermousediv.style.visibility = "hidden"; });
artisticonlinkelement.href = "https://www.deviantart.com/" + deviation.gs_username;
artisticonelement.alt = deviation.author.username + "'s avatar";
artisticonelement.src = deviation.author.usericon;
artistnamelinkelement.href = "https://www.deviantart.com/" + deviation.gs_username;
artistnametextelement.innerText = deviation.author.username;
commentlinkelement.href = deviation.url + "#comments";
commentcountspan.innerText = deviation.stats.comments;
return newtileelement;
}
function createJournalDeviationElement(deviation)
{
let newtileelement = cloneablejournaldeviationelement.cloneNode(true);
newtileelement.style.width = deviation.gs_tile_width;
newtileelement.style.height = deviation.gs_tile_height;
//Dive the cloned DOM for these assignments
let draggablecontainer = newtileelement.children[0];
let sectionelement = draggablecontainer.children[0];//_1C7DQ _1L6MH
let journaltitle = sectionelement.children[0].children[0];//mhmhR
let journaldtcontainer = sectionelement.children[1];//_2Hfrr
let journaldt = journaldtcontainer.children[0].children[0];
let journalexcerptdiv = journaldtcontainer.children[1];//legacy-journal _2HUtS
let deviationlink = draggablecontainer.children[1];//_1vRyy
let outermousediv = draggablecontainer.children[2];//_2jPGh _3Cax3
let iconartistcontainer2 = outermousediv.children[1].children[0].children[0].children[0].children[0];//_2o1Q1
let artisticonlinkelement = iconartistcontainer2.children[0].children[0];//user-link _2f0dA _23x0l
let artisticonelement = artisticonlinkelement.children[0];//_1IDJa
let artistnamelinkelement = iconartistcontainer2.children[1].children[0];//user-link _2f0dA
let artistnametextelement = artistnamelinkelement.children[0];//_2UI2c
let commentlinkelement = outermousediv.children[1].children[0].children[1].children[0]//_1-Wh7 x48yz
let commentcountspan = commentlinkelement.children[1];
journaltitle.innerText = deviation.title;
journaldt.dateTime = deviation.publishedTime;
let date = new Date(deviation.publishedTime);
let formattedDate = date.toLocaleDateString('en-US', {month: 'short', day: 'numeric', year: 'numeric'});
journaldt.innerText = formattedDate;
journalexcerptdiv.innerText = deviation.textContent.excerpt;
deviationlink.href = deviation.url;
//mouseover events attach to link parent
draggablecontainer.addEventListener("mouseover", (event) => { outermousediv.style.visibility = ""; });
draggablecontainer.addEventListener("mouseout", (event) => { outermousediv.style.visibility = "hidden"; });
artisticonlinkelement.href = "https://www.deviantart.com/" + deviation.gs_username;
artisticonelement.alt = deviation.author.username + "'s avatar";
artisticonelement.src = deviation.author.usericon;
artistnamelinkelement.href = "https://www.deviantart.com/" + deviation.gs_username;
artistnametextelement.innerText = deviation.author.username;
commentlinkelement.href = deviation.url + "#comments";
commentcountspan.innerText = deviation.stats.comments;
return newtileelement;
}
function createCloneableImageDeviationElement()
{
let outermostdiv = document.createElement("div");
outermostdiv.style.display = "inline-block";
outermostdiv.style.float = "left";
outermostdiv.style.position = "relative";
outermostdiv.style.margin = tilemargin + "px";
//Create Image DONE
let imgelement = document.createElement("img");
imgelement.style.width = "100%";
imgelement.style.height = "100%";
imgelement.style.objectFit = "cover";
imgelement.style.objectPosition = "50% 100%";
//Image container Done
let imgcontainer = document.createElement("div")
imgcontainer.className = "_24Wda";
imgcontainer.style.width = "100%";
imgcontainer.style.height = "100%";
//Create link to deviation page Done
let artlinkelement = document.createElement("a");
//Draggable div container, also contains mouseover event assigned later
let draggablecontainer = document.createElement("div")
draggablecontainer.style.width = "100%";
draggablecontainer.style.height = "100%";
draggablecontainer.className = "_1xcj5 _1QdgI";
draggablecontainer.draggable = "true";
imgcontainer.appendChild(imgelement);
artlinkelement.appendChild(imgcontainer);
draggablecontainer.appendChild(artlinkelement);
outermostdiv.appendChild(draggablecontainer);
//////////////////////////////////
//Mouseover elements section
//////////////////////////////////
//Outer mouseover element
let outermousediv = document.createElement("div");
outermousediv.style.visibility = "hidden";
outermousediv.style.width = "100%";
outermousediv.style.height = "100%";
outermousediv.className = "_2jPGh _3Cax3";
//Divs for slight black fade on hover
let blackfadeouterdiv = document.createElement("div");
blackfadeouterdiv.className = "_1mmGw";
let blackfadeinnerdiv = document.createElement("div");
blackfadeinnerdiv.className = "cjZ9o _2QZ8F _3b-i8";
blackfadeouterdiv.appendChild(blackfadeinnerdiv);
outermousediv.appendChild(blackfadeouterdiv);
//Next inward div
let innermousediv = document.createElement("div");
innermousediv.style.width = "100%";
innermousediv.style.height = "100%";
innermousediv.className = "_2ehf4 YpNhf";
//Div for all all meta elements container (title, artist, icon, comments)
let metaelementscontainer = document.createElement("div");
metaelementscontainer.className = "_5Xty_";
//Div to contain title and artist elements (title, artist, icon)
let titleartistcontainer = document.createElement("div");
titleartistcontainer.className = "_1mmGw _31MCr";
//Link with Title
let arttitlelinkelement = document.createElement("a")
arttitlelinkelement.className = "KoW6A";
//Title
let arttitleelement = document.createElement("h2")
arttitleelement.className = "_1lmpZ";
arttitlelinkelement.appendChild(arttitleelement);
titleartistcontainer.appendChild(arttitlelinkelement);
metaelementscontainer.appendChild(titleartistcontainer);
innermousediv.appendChild(metaelementscontainer);
outermousediv.appendChild(innermousediv);
draggablecontainer.appendChild(outermousediv);
//Subsection for artist icon and name
//Divs to contain title and artist elements (title, artist, icon)
let iconartistcontainer = document.createElement("div");
iconartistcontainer.className = "_13y-9";
let iconartistcontainer2 = document.createElement("div");
iconartistcontainer2.className = "_2o1Q1";
//Div to contain artist icon
let iconcontainer = document.createElement("div");
iconcontainer.className = "_3CR67 _1I9Ar";
//Link for artist that surrounds icon
let artisticonlinkelement = document.createElement("a")
artisticonlinkelement.className = "user-link _2f0dA _23x0l";
//Artist icon element
let artisticonelement = document.createElement("img");
artisticonelement.style.width = "24px";
artisticonelement.style.height = "24px";
artisticonelement.loading = "lazy";
artisticonelement.className = "_1IDJa";
artisticonlinkelement.appendChild(artisticonelement);
iconcontainer.appendChild(artisticonlinkelement);
//Div to contain artist name
let artistnamecontainer = document.createElement("div");
artistnamecontainer.className = "_3CR67 k4CiA";
//Link for artist that surrounds icon
let artistnamelinkelement = document.createElement("a")
artistnamelinkelement.className = "user-link _2f0dA";
//Artist name text span
let artistnametextelement = document.createElement("span");
artistnametextelement.className = "_2UI2c";
//Artist cursor span
let artistcursorelement = document.createElement("span");
artistcursorelement.className = "_3LUMH _1NhtS G0rcN";
artistcursorelement.style.cursor = "pointer";
artistcursorelement.role = "img";
artistnamelinkelement.appendChild(artistnametextelement);
artistnamelinkelement.appendChild(artistcursorelement);
artistnamecontainer.appendChild(artistnamelinkelement);
iconartistcontainer2.appendChild(iconcontainer);
iconartistcontainer2.appendChild(artistnamecontainer);
iconartistcontainer.appendChild(iconartistcontainer2);
titleartistcontainer.appendChild(iconartistcontainer);
//Subsection for comments icon and link elements
//Div to contain comment icon and link elements
let commenticonlinkcontainer = document.createElement("div");
commenticonlinkcontainer.className = "_1mmGw _2WpJA _6oiPd";
//Link for comment section
let commentlinkelement = document.createElement("a");
commentlinkelement.className = "_1-Wh7 x48yz";
//Comment icon span
let commenticonspan = document.createElement("span");
commenticonspan.className = "z8jNZ _1yoxj _38kc5";
//Comment icon (SVG)
let commenticonSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
commenticonSVG.setAttribute("viewBox", "0 0 24 24");
commenticonSVG.setAttribute("version", "1.1");
commenticonSVG.setAttribute("xlmns:xlink", "http://www.w3.org/1999/xlink");
//Comment icon (SVG PATH)
let commenticonpath = document.createElementNS("http://www.w3.org/2000/svg", "path");
commenticonpath.setAttribute("fill-rule", "evenodd");
commenticonpath.setAttribute("d", "M20 3a1 1 0 01.993.883L21 4v9.586a1 1 0 01-.206.608l-.087.099-2.414 2.414a1 1 0 01-.576.284l-.131.009H13l-2.7 3.6a1 1 0"
+ " 01-.683.393L9.5 21H8a1 1 0 01-.993-.883L7 20v-3H4a1 1 0 01-.993-.883L3 16V6.414a1 1 0 01.206-.608l.087-.099 2.414-2.414a1 1 0 01.576-.284L6.414"
+ " 3H20zm-1 2H6.828L5 6.828V15h4v4l3-4h5.17L19 13.17V5z");
//Comment count span
let commentcountspan = document.createElement("span");
commenticonSVG.appendChild(commenticonpath);
commenticonspan.appendChild(commenticonSVG);
commentlinkelement.appendChild(commenticonspan);
commentlinkelement.appendChild(commentcountspan);
commenticonlinkcontainer.appendChild(commentlinkelement);
metaelementscontainer.appendChild(commenticonlinkcontainer);
return outermostdiv;
}
function createCloneableLiteratureDeviationElement() {
let outermostdiv = document.createElement("div");
outermostdiv.style.display = "inline-block";
outermostdiv.style.float = "left";
outermostdiv.style.position = "relative";
outermostdiv.style.margin = tilemargin + "px";
//Draggable div container, also contains mouseover event assigned later
let draggablecontainer = document.createElement("div")
draggablecontainer.style.width = "100%";
draggablecontainer.style.height = "100%";
draggablecontainer.className = "_1xcj5 _1QdgI";
draggablecontainer.draggable = "true";
//Section element that contains literature display elements
let sectionelement = document.createElement("section");
sectionelement.className = "_33VtO _3rqZq";
sectionelement.style.width = "100%";
sectionelement.style.height = "100%";
//Fancy literature deviation background element container
let literaturebgcontainer = document.createElement("div")
literaturebgcontainer.className = "xPxyA LXVwg";
//Fancy literature deviation background (SVG)
let litbgSVG = document.createElement("svg");
litbgSVG.setAttribute("viewBox", "0 0 15 12");
litbgSVG.setAttribute("height", "100%");
litbgSVG.setAttribute("preserveAspectRatio", "xMidYMin slice");
litbgSVG.setAttribute("fill-rule", "evenodd");
//Fancy literature deviation background (lineargradient)
let litbglineargradient = document.createElement("linearGradient");
litbglineargradient.setAttribute("x1", "87.8481761%");
litbglineargradient.setAttribute("y1", "16.3690766%");
litbglineargradient.setAttribute("x2", "45.4107524%");
litbglineargradient.setAttribute("y2", "71.4898596%");
//Fancy literature deviation background (stop color)s
let litbgstop1 = document.createElement("stop");
litbgstop1.setAttribute("stop-color", "#00FF62");
litbgstop1.setAttribute("offset", "0%");
let litbgstop2 = document.createElement("stop");
litbgstop2.setAttribute("stop-color", "#3197EF");
litbgstop2.setAttribute("offset", "100%");
litbgstop2.setAttribute("stop-opacity", "0");
litbglineargradient.appendChild(litbgstop1);
litbglineargradient.appendChild(litbgstop2);
litbgSVG.appendChild(litbglineargradient);
literaturebgcontainer.appendChild(litbgSVG);
sectionelement.appendChild(literaturebgcontainer);
draggablecontainer.appendChild(sectionelement);
outermostdiv.appendChild(draggablecontainer);
//Literature Preview Subsection
//Fancy literature deviation background element container
let literaturecategorydiv = document.createElement("div")
literaturecategorydiv.className = "_3hLq8";
literaturecategorydiv.innerText = "Literature";
//Literature Preview Title
let literaturepreviewtitle = document.createElement("h2")
literaturepreviewtitle.className = "_2mwJN";
//Literature Preview Text
let literaturepreviewtext = document.createElement("h2")
literaturepreviewtext.className = "heXvc";
sectionelement.appendChild(literaturecategorydiv);
sectionelement.appendChild(literaturepreviewtitle);
sectionelement.appendChild(literaturepreviewtext);
//Deviation link section
let deviationlink = document.createElement("a");
deviationlink.className = "_1vRyy";
draggablecontainer.appendChild(deviationlink);
//////////////////////////////////
//Mouseover elements section
//////////////////////////////////
//Outer mouseover element
let outermousediv = document.createElement("div");
outermousediv.style.visibility = "hidden";
outermousediv.style.width = "100%";
outermousediv.style.height = "100%";
outermousediv.className = "_2jPGh _3Cax3";
//Divs for slight black fade on hover
let blackfadeouterdiv = document.createElement("div");
blackfadeouterdiv.className = "_1mmGw";
let blackfadeinnerdiv = document.createElement("div");
blackfadeinnerdiv.className = "cjZ9o _2QZ8F _3b-i8";
blackfadeouterdiv.appendChild(blackfadeinnerdiv);
outermousediv.appendChild(blackfadeouterdiv);
//Next inward div
let innermousediv = document.createElement("div");
innermousediv.style.width = "100%";
innermousediv.style.height = "100%";
innermousediv.className = "_2ehf4 YpNhf";
//Div for all all meta elements container (title, artist, icon, comments)
let metaelementscontainer = document.createElement("div");
metaelementscontainer.className = "_5Xty_";
//Div to contain title and artist elements (title, artist, icon)
let titleartistcontainer = document.createElement("div");
titleartistcontainer.className = "_1mmGw _31MCr";
// //Link with Title
// let arttitlelinkelement = document.createElement("a")
// arttitlelinkelement.href = deviation.url;
// arttitlelinkelement.className = "KoW6A";
// //Title
// let arttitleelement = document.createElement("h2")
// arttitleelement.className = "_1lmpZ";
// arttitleelement.innerText = deviation.title;
// arttitlelinkelement.appendChild(arttitleelement);
//titleartistcontainer.appendChild(arttitlelinkelement);
metaelementscontainer.appendChild(titleartistcontainer);
innermousediv.appendChild(metaelementscontainer);
outermousediv.appendChild(innermousediv);
draggablecontainer.appendChild(outermousediv);
//Subsection for artist icon and name
//Divs to contain title and artist elements (title, artist, icon)
let iconartistcontainer = document.createElement("div");
iconartistcontainer.className = "_13y-9";
let iconartistcontainer2 = document.createElement("div");
iconartistcontainer2.className = "_2o1Q1";
//Div to contain artist icon
let iconcontainer = document.createElement("div");
iconcontainer.className = "_3CR67 _1I9Ar";
//Link for artist that surrounds icon
let artisticonlinkelement = document.createElement("a")
artisticonlinkelement.className = "user-link _2f0dA _23x0l";
//Artist icon element
let artisticonelement = document.createElement("img");
artisticonelement.style.width = "24px";
artisticonelement.style.height = "24px";
artisticonelement.loading = "lazy";
artisticonelement.className = "_1IDJa";
artisticonlinkelement.appendChild(artisticonelement);
iconcontainer.appendChild(artisticonlinkelement);
//Div to contain artist name
let artistnamecontainer = document.createElement("div");
artistnamecontainer.className = "_3CR67 k4CiA";
//Link for artist that surrounds icon
let artistnamelinkelement = document.createElement("a");
artistnamelinkelement.className = "user-link _2f0dA";
//Artist name text span
let artistnametextelement = document.createElement("span")
artistnametextelement.className = "_2UI2c";
//Artist cursor span
let artistcursorelement = document.createElement("span")
artistcursorelement.className = "_3LUMH _1NhtS G0rcN";
artistcursorelement.style.cursor = "pointer";
artistcursorelement.role = "img";
artistnamelinkelement.appendChild(artistnametextelement);
artistnamelinkelement.appendChild(artistcursorelement);
artistnamecontainer.appendChild(artistnamelinkelement);
iconartistcontainer2.appendChild(iconcontainer);
iconartistcontainer2.appendChild(artistnamecontainer);
iconartistcontainer.appendChild(iconartistcontainer2);
titleartistcontainer.appendChild(iconartistcontainer);
//Subsection for comments icon and link elements
//Div to contain comment icon and link elements
let commenticonlinkcontainer = document.createElement("div");
commenticonlinkcontainer.className = "_1mmGw _2WpJA _6oiPd";
//Link for comment section
let commentlinkelement = document.createElement("a")
commentlinkelement.className = "_1-Wh7 x48yz";
//Comment icon span
let commenticonspan = document.createElement("span")
commenticonspan.className = "z8jNZ _1yoxj _38kc5";
//Comment icon (SVG)
let commenticonSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
commenticonSVG.setAttribute("viewBox", "0 0 24 24");
commenticonSVG.setAttribute("version", "1.1");
commenticonSVG.setAttribute("xlmns:xlink", "http://www.w3.org/1999/xlink");
//Comment icon (SVG PATH)
let commenticonpath = document.createElementNS("http://www.w3.org/2000/svg", "path");
commenticonpath.setAttribute("fill-rule", "evenodd");
commenticonpath.setAttribute("d", "M20 3a1 1 0 01.993.883L21 4v9.586a1 1 0 01-.206.608l-.087.099-2.414 2.414a1 1 0 01-.576.284l-.131.009H13l-2.7 3.6a1 1 0"
+ " 01-.683.393L9.5 21H8a1 1 0 01-.993-.883L7 20v-3H4a1 1 0 01-.993-.883L3 16V6.414a1 1 0 01.206-.608l.087-.099 2.414-2.414a1 1 0 01.576-.284L6.414"
+ " 3H20zm-1 2H6.828L5 6.828V15h4v4l3-4h5.17L19 13.17V5z");
//Comment count span
let commentcountspan = document.createElement("span")
commenticonSVG.appendChild(commenticonpath);
commenticonspan.appendChild(commenticonSVG);
commentlinkelement.appendChild(commenticonspan);
commentlinkelement.appendChild(commentcountspan);
commenticonlinkcontainer.appendChild(commentlinkelement);
metaelementscontainer.appendChild(commenticonlinkcontainer);
return outermostdiv;
}
function createCloneableJournalDeviationElement() {
let outermostdiv = document.createElement("div");
outermostdiv.style.display = "inline-block";
outermostdiv.style.float = "left";
outermostdiv.style.position = "relative";
outermostdiv.style.margin = tilemargin + "px";
//Draggable div container, also contains mouseover event assigned later
let draggablecontainer = document.createElement("div")
draggablecontainer.style.width = "100%";
draggablecontainer.style.height = "100%";
draggablecontainer.className = "_1xcj5 _1QdgI";
draggablecontainer.draggable = "true";
//Section element that contains journal display elements
let sectionelement = document.createElement("section");
sectionelement.className = "_1C7DQ _1L6MH";
sectionelement.style.width = "100%";
sectionelement.style.height = "100%";
//Journal title container
let journaltitlecontainer = document.createElement("div")
journaltitlecontainer.className = "_1i4Yb";
//Journal Title
let journaltitle = document.createElement("h2")
journaltitle.className = "mhmhR";
journaltitlecontainer.appendChild(journaltitle);
sectionelement.appendChild(journaltitlecontainer);
draggablecontainer.appendChild(sectionelement);
outermostdiv.appendChild(draggablecontainer);
//Journal Excerpt subsection
//Journal preview container
let journalpreviewcontainer = document.createElement("div")
journalpreviewcontainer.className = "_2Hfrr";
//Journal datetime container
let journaldtcontainer = document.createElement("div")
journaldtcontainer.className = "uBAbQ";
//Journal datetime, also get the correct date format. Example: Dec 14, 2015
let journaldt = document.createElement("time")
//Journal excerpt div
let journalexcerptdiv = document.createElement("div")
journalexcerptdiv.className = "legacy-journal _2HUtS";
journaldtcontainer.appendChild(journaldt);
journalpreviewcontainer.appendChild(journaldtcontainer);
journalpreviewcontainer.appendChild(journalexcerptdiv);
sectionelement.appendChild(journalpreviewcontainer);
//Deviation link section
let deviationlink = document.createElement("a");
deviationlink.className = "_1vRyy";
draggablecontainer.appendChild(deviationlink);
//////////////////////////////////
//Mouseover elements section
//////////////////////////////////
//Outer mouseover element
let outermousediv = document.createElement("div");
outermousediv.style.visibility = "hidden";
outermousediv.style.width = "100%";
outermousediv.style.height = "100%";
outermousediv.className = "_2jPGh _3Cax3";
//Divs for slight black fade on hover
let blackfadeouterdiv = document.createElement("div");
blackfadeouterdiv.className = "_1mmGw";
let blackfadeinnerdiv = document.createElement("div");
blackfadeinnerdiv.className = "cjZ9o _2QZ8F _3b-i8";
blackfadeouterdiv.appendChild(blackfadeinnerdiv);
outermousediv.appendChild(blackfadeouterdiv);
//Next inward div
let innermousediv = document.createElement("div");
innermousediv.style.width = "100%";
innermousediv.style.height = "100%";
innermousediv.className = "_2ehf4 YpNhf";
//Div for all all meta elements container (title, artist, icon, comments)
let metaelementscontainer = document.createElement("div");
metaelementscontainer.className = "_5Xty_";
//Div to contain title and artist elements (title, artist, icon)
let titleartistcontainer = document.createElement("div");
titleartistcontainer.className = "_1mmGw _31MCr";
// arttitlelinkelement.appendChild(arttitleelement);
//titleartistcontainer.appendChild(arttitlelinkelement);
metaelementscontainer.appendChild(titleartistcontainer);
innermousediv.appendChild(metaelementscontainer);
outermousediv.appendChild(innermousediv);
draggablecontainer.appendChild(outermousediv);
//Subsection for artist icon and name
//Divs to contain title and artist elements (title, artist, icon)
let iconartistcontainer = document.createElement("div");
iconartistcontainer.className = "_13y-9";
let iconartistcontainer2 = document.createElement("div");
iconartistcontainer2.className = "_2o1Q1";
//Div to contain artist icon
let iconcontainer = document.createElement("div");
iconcontainer.className = "_3CR67 _1I9Ar";
//Link for artist that surrounds icon
let artisticonlinkelement = document.createElement("a")
artisticonlinkelement.className = "user-link _2f0dA _23x0l";
//Artist icon element
let artisticonelement = document.createElement("img");
artisticonelement.style.width = "24px";
artisticonelement.style.height = "24px";
artisticonelement.loading = "lazy";
artisticonelement.className = "_1IDJa";
artisticonlinkelement.appendChild(artisticonelement);
iconcontainer.appendChild(artisticonlinkelement);
//Div to contain artist name
let artistnamecontainer = document.createElement("div");
artistnamecontainer.className = "_3CR67 k4CiA";
//Link for artist that surrounds icon
let artistnamelinkelement = document.createElement("a")
artistnamelinkelement.className = "user-link _2f0dA";
//Artist name text span
let artistnametextelement = document.createElement("span")
artistnametextelement.className = "_2UI2c";
//Artist cursor span
let artistcursorelement = document.createElement("span")
artistcursorelement.className = "_3LUMH _1NhtS G0rcN";
artistcursorelement.style.cursor = "pointer";
artistcursorelement.role = "img";
artistnamelinkelement.appendChild(artistnametextelement);
artistnamelinkelement.appendChild(artistcursorelement);
artistnamecontainer.appendChild(artistnamelinkelement);
iconartistcontainer2.appendChild(iconcontainer);
iconartistcontainer2.appendChild(artistnamecontainer);
iconartistcontainer.appendChild(iconartistcontainer2);
titleartistcontainer.appendChild(iconartistcontainer);
//Subsection for comments icon and link elements
//Div to contain comment icon and link elements
let commenticonlinkcontainer = document.createElement("div");
commenticonlinkcontainer.className = "_1mmGw _2WpJA _6oiPd";
//Link for comment section
let commentlinkelement = document.createElement("a")
commentlinkelement.className = "_1-Wh7 x48yz";
//Comment icon span
let commenticonspan = document.createElement("span")
commenticonspan.className = "z8jNZ _1yoxj _38kc5";
//Comment icon (SVG)
let commenticonSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
commenticonSVG.setAttribute("viewBox", "0 0 24 24");
commenticonSVG.setAttribute("version", "1.1");
commenticonSVG.setAttribute("xlmns:xlink", "http://www.w3.org/1999/xlink");
//Comment icon (SVG PATH)
let commenticonpath = document.createElementNS("http://www.w3.org/2000/svg", "path");
commenticonpath.setAttribute("fill-rule", "evenodd");
commenticonpath.setAttribute("d", "M20 3a1 1 0 01.993.883L21 4v9.586a1 1 0 01-.206.608l-.087.099-2.414 2.414a1 1 0 01-.576.284l-.131.009H13l-2.7 3.6a1 1 0"
+ " 01-.683.393L9.5 21H8a1 1 0 01-.993-.883L7 20v-3H4a1 1 0 01-.993-.883L3 16V6.414a1 1 0 01.206-.608l.087-.099 2.414-2.414a1 1 0 01.576-.284L6.414"
+ " 3H20zm-1 2H6.828L5 6.828V15h4v4l3-4h5.17L19 13.17V5z");
//Comment count span
let commentcountspan = document.createElement("span")
commenticonSVG.appendChild(commenticonpath);
commenticonspan.appendChild(commenticonSVG);
commentlinkelement.appendChild(commenticonspan);
commentlinkelement.appendChild(commentcountspan);
commenticonlinkcontainer.appendChild(commentlinkelement);
metaelementscontainer.appendChild(commenticonlinkcontainer);
return outermostdiv;
}
function toggleSettingsElement() {
if(settingsmenudiv.style.display == "none") {
settingsmenudiv.style.display = "";
settingsmenudiv.focus();
}
else {
settingsmenudiv.style.display = "none";
}
}
function hideSettingsElement(event) {
if(!settingscogdiv.contains(event.target)){
settingsmenudiv.style.display = 'none';
}
}
function createSettingsMenu() {
let settingscontainer = document.createElement("div");
settingscontainer.id = "settingsmenu";
settingscontainer.style.background = "#06070d";
settingscontainer.style.border = "1px solid #262830";
settingscontainer.style.borderRadius = "5px solid #262830";
settingscontainer.style.position = "absolute";
settingscontainer.style.right = "-20px";
settingscontainer.style.top = "40px";
settingscontainer.style.zIndex = "1";
settingscontainer.style.zIndex = "1";
settingscontainer.style.display = "none";
let settingstable = document.createElement("table");
settingstable.style.width = "300px";
settingstable.style.margin = "5px";
let tr1 = document.createElement("tr");
let tr2 = document.createElement("tr");
let tr3 = document.createElement("tr");
let tr4 = document.createElement("tr");
let t2tr1 = document.createElement("tr");
let maxsearchresultsleft = document.createElement("td");
maxsearchresultsleft.style.textAlign = "left";
maxsearchresultsleft.style.padding = "5px";
maxsearchresultsleft.innerText = "Max Search Results:";
let maxsearchresultsright = document.createElement("td");
maxsearchresultsright.style.textAlign = "right";
maxsearchresultsright.style.padding = "5px";
let maxsearchresultsinput = document.createElement("input");
maxsearchresultsinput.id = "maximumsearchreturn";
maxsearchresultsinput.type = "text";
maxsearchresultsinput.style.width = "40px";
maxsearchresultsinput.value = maximumsearchreturn;
maxsearchresultsright.appendChild(maxsearchresultsinput);
tr1.appendChild(maxsearchresultsleft);
tr1.appendChild(maxsearchresultsright);
let paginationrateleft = document.createElement("td");
paginationrateleft.style.textAlign = "left";
paginationrateleft.style.padding = "5px";
paginationrateleft.innerText = "Index Rate Limit (in seconds):";
let paginationrateright = document.createElement("td");
paginationrateright.style.textAlign = "right";
paginationrateright.style.padding = "5px";
let paginationrateinput = document.createElement("input");
paginationrateinput.id = "paginationratelimit";
paginationrateinput.type = "text";
paginationrateinput.style.width = "40px";
paginationrateinput.value = paginationratelimit.toPrecision(2);
paginationrateright.appendChild(paginationrateinput);
tr2.appendChild(paginationrateleft);
tr2.appendChild(paginationrateright);
let ratewarningleft = document.createElement("td");
ratewarningleft.style.textAlign = "left";
ratewarningleft.style.padding = "5px";
ratewarningleft.innerText = "Rate Limit Warning:";
let ratewarningright = document.createElement("td");
ratewarningright.style.textAlign = "right";
ratewarningright.style.padding = "5px";
let ratewarningcheckbox = document.createElement("input");
ratewarningcheckbox.id = "ratelimitwarning";
ratewarningcheckbox.type = "checkbox";
ratewarningcheckbox.style.accentColor = "green";
ratewarningcheckbox.checked = ratelimitwarning;
ratewarningright.appendChild(ratewarningcheckbox);
tr3.appendChild(ratewarningleft);
tr3.appendChild(ratewarningright);
let tileheightleft = document.createElement("td");
tileheightleft.style.textAlign = "left";
tileheightleft.style.padding = "5px";
tileheightleft.innerText = "Rough Tiling Height (in pixels):";
let tileheightright = document.createElement("td");
tileheightright.style.textAlign = "right";
tileheightright.style.padding = "5px";
let tileheightinput = document.createElement("input");
tileheightinput.id = "pivotalrowheight";
tileheightinput.type = "text";
tileheightinput.style.width = "40px";
tileheightinput.value = pivotalrowheight;
tileheightright.appendChild(tileheightinput);
tr4.appendChild(tileheightleft);
tr4.appendChild(tileheightright);
settingstable.appendChild(tr1);
settingstable.appendChild(tr2);
settingstable.appendChild(tr3);
settingstable.appendChild(tr4);
settingscontainer.appendChild(settingstable);
let settingstable2 = document.createElement("table");
settingstable2.style.width = "300px";
settingstable2.style.margin = "5px";
let sortbyleft = document.createElement("td");
sortbyleft.style.textAlign = "left";
sortbyleft.style.padding = "5px";
sortbyleft.innerText = "Sort By:";
let sortbyright = document.createElement("td");
sortbyright.style.textAlign = "right";
let sortbycontainer = document.createElement("div");
sortbycontainer.style.width = "180px";
sortbycontainer.style.color = regularwhitetextcolor;
sortbycontainer.style.marginLeft = "auto";
sortbycontainer.style.display = "flex";
let leftsort = document.createElement("span");
leftsort.id = "leftsort";
leftsort.style.cursor = "pointer";
leftsort.style.margin = "5px auto";
leftsort.style.background = "green";
leftsort.style.padding = "5px";
leftsort.style.borderTopLeftRadius = "5px";
leftsort.style.borderBottomLeftRadius = "5px";
leftsort.innerText = "<";
let sorttext = document.createElement("span");
sorttext.id = "sortby";
sorttext.style.margin = "5px auto";
sorttext.style.background = "green";
sorttext.style.padding = "5px";
sorttext.style.width = "120px";
sorttext.style.textAlign = "center";
sorttext.innerText = sortby;
let rightsort = document.createElement("span");
rightsort.id = "rightsort";
rightsort.style.cursor = "pointer";
rightsort.style.margin = "5px auto";
rightsort.style.background = "green";
rightsort.style.padding = "5px";
rightsort.style.borderTopRightRadius = "5px";
rightsort.style.borderBottomRightRadius = "5px";
rightsort.innerText = ">";
sortbycontainer.appendChild(leftsort);
sortbycontainer.appendChild(sorttext);
sortbycontainer.appendChild(rightsort);
sortbyright.appendChild(sortbycontainer);
t2tr1.appendChild(sortbyleft);
t2tr1.appendChild(sortbyright);
settingstable2.appendChild(t2tr1);
settingscontainer.appendChild(settingstable2);
settingscogdiv.appendChild(settingscontainer);
//Add button animations
leftsort.addEventListener("mouseover", hoverFade);
leftsort.addEventListener("mouseout", hoverFadeCancel);
rightsort.addEventListener("mouseover", hoverFade);
rightsort.addEventListener("mouseout", hoverFadeCancel);
//Add user settings change listeners
maxsearchresultsinput.addEventListener("keyup", updateUserNumberPreference);
paginationrateinput.addEventListener("keyup", updateUserNumberPreference);
ratewarningcheckbox.addEventListener("click", updateUserCheckboxPreference);
tileheightinput.addEventListener("keyup", updateUserNumberPreference);
leftsort.addEventListener("click", updateSortByPreference);
rightsort.addEventListener("click", updateSortByPreference);
return settingscontainer;
function hoverFade(event) {
event.target.style.opacity = "50%";
}
function hoverFadeCancel(event) {
event.target.style.opacity = "100%";
}
function updateUserNumberPreference(event) {
let key = event.target.id;
let value = event.target.value;
let updatedCorrectly = setUserPreference(key, value);
if(updatedCorrectly) {
event.target.style.color = regularblacktextcolor;
searchoutputtext.style.color = regularwhitetextcolor;
if(searchoutputtext.innerText.includes("Error"))
searchoutputtext.innerText = "";
}
else {
event.target.style.color = errortextcolor;
searchoutputtext.style.color = errortextcolor;
let typeproblemstring = (key == "paginationratelimit" ? "decimal number. (Example: 2.5)" : "integer. (Example: 300)");
searchoutputtext.innerText = "Error: '" + value + "' is not a " + typeproblemstring;
}
//Add an immediate tiling update if the desired row height is changed, handled within the search function
if(key == "pivotalrowheight") {
search();
}
}
function updateUserCheckboxPreference(event) {
let key = event.target.id;
let value = event.target.checked;
setUserPreference(key, value);
}
function updateSortByPreference(event) {
let isleft = event.target.id == "leftsort";
//Get and set new sort option based on click direction
let sortoptionindex = 0;
for(let i=0; i<sortoptions.length; i++) {
if(sortoptions[i] == sortby){
sortoptionindex = i;
break;
}
}
if(isleft)
sortoptionindex -= 1;
else
sortoptionindex += 1;
if(sortoptionindex == -1)//loop the options backwards
sortoptionindex = sortoptions.length-1;
if(sortoptionindex == sortoptions.length)//loop the options forwards
sortoptionindex = 0;
setUserPreference("sortby", sortoptions[sortoptionindex]);
//Set the html element
sorttext.innerText = sortby;
//Now re-sort and re-display search results based on new order
sort();
search();
}
}
function getUserPreferencesFromLocalStorage() {
//For all user preference variables, get them from localStorage if they exist and re-store them so the script can start with a consistent state in global variables and localStorage.
let newmaximumsearchreturn = localStorage.getItem("maximumsearchreturn");
if(newmaximumsearchreturn != null)
setUserPreference("maximumsearchreturn", newmaximumsearchreturn)
let newpaginationratelimit = localStorage.getItem("paginationratelimit");
if(newpaginationratelimit != null)
setUserPreference("paginationratelimit", newpaginationratelimit)
let newratelimitwarning = localStorage.getItem("ratelimitwarning");
if(newratelimitwarning != null)
setUserPreference("ratelimitwarning", newratelimitwarning)
let newpivotalrowheight = localStorage.getItem("pivotalrowheight");
if(newpivotalrowheight != null)
setUserPreference("pivotalrowheight", newpivotalrowheight)
let newsortby = localStorage.getItem("sortby");
if(newsortby != null)
setUserPreference("sortby", newsortby)
}
function setUserPreference(key, value) {
//Sets the user preference value based on new input from user. Returns true if setting was a success, returns false if there was a parsing error.
//Use this for all set() actions. Keeps the state consistent between the gloabl variables and localStorage.
//This handles string inputs (ex: maximumsearchreturn -> "1000" ) and direct inputs (ex: maximumsearchreturn -> 1000 )
if(key == "maximumsearchreturn") {
let newmaximumsearchreturn = parseInt(value);
if(isNaN(newmaximumsearchreturn))
return false;
maximumsearchreturn = newmaximumsearchreturn;
localStorage.setItem("maximumsearchreturn", maximumsearchreturn);
}
else if(key == "paginationratelimit") {
let newpaginationratelimit = parseFloat(value);
if(isNaN(newpaginationratelimit))
return false;
paginationratelimit = newpaginationratelimit;
localStorage.setItem("paginationratelimit", paginationratelimit);
}
else if(key == "ratelimitwarning") {
let newratelimitwarning = (value == true || value == "true");
ratelimitwarning = newratelimitwarning;
localStorage.setItem("ratelimitwarning", ratelimitwarning);
}
else if(key == "pivotalrowheight") {
let newpivotalrowheight = parseInt(value);
if(isNaN(newpivotalrowheight))
return false;
pivotalrowheight = newpivotalrowheight;
localStorage.setItem("pivotalrowheight", pivotalrowheight);
}
else if(key == "sortby") {
sortby = value;
localStorage.setItem("sortby", sortby);
}
else {
return false;
}
return true;
}
/////////////////////////////////////////////////////////////////////////////////////
///// Other Helper Functions /////
/////////////////////////////////////////////////////////////////////////////////////
function getAspectedWidth(origwidth, origheight, sizedheight)
{
return (origwidth * sizedheight) / origheight;
}
function getAspectedHeight(origwidth, origheight, sizedwidth)
{
return (origheight * sizedwidth) / origwidth;
}
function setViewportWidth()
{
viewportwidth = parseFloat(window.getComputedStyle(itemscontainer).width);
}
//Function to sort deviationJSON based on attribute provided
function sortDeviationsByKey(array, key, ascending) {
//spot check for the variable type
let spotcheck = typeof array[0].deviation[key];
if(spotcheck == "string")
{
return array.sort(function(a, b) {
let x = a.deviation[key]; let y = b.deviation[key];
if(ascending)
return x.localeCompare(y);
else
return y.localeCompare(x);
});
}
else if(spotcheck == "number") {
return array.sort(function(a, b) {
let x = a.deviation[key]; let y = b.deviation[key];
if(x == y)
return 0;
let returnval;
if(x > y)
returnval = 1;
else
returnval = -1;
if(ascending)
return returnval;
else
return -returnval;
});
}
else {
console.error("Unhandled type for sort key: " + key);
}
}
}
//DOM Element Mutation Observer
//Credit to Yong Wang on StackOverflow: https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
function waitForElm(selector) {
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector));
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
})();