// // // // // global variables var timer; var artTypes = ['🎨','🧑','🏞️']; var imgTypeShown = 0; var log = ''; var editMostUsedMode = false; var informationMode = false; var windowWidth = 0; var gutterStartPosX, mouseStartPosX, gutterEndPercentX var style, tempStyle, stylesheet, tempStylesheet, imgHoverRule, teaseRules; var theTime = new Date; var startUpTime; var tagsConcatenated = new Set(); var editedArtists = new Set(); var storingAccessType = 'none'; var start = performance.now(); const lowCountThreshold = 3; // // // // wait for DOM document.addEventListener("DOMContentLoaded", function() { checkStoringAccessType().then(state => { startUp(); }); startUpTime = theTime.getTime(); }); // functions async function startUp() { updateTagsConcatenated(); await loadEditedArtists(); insertArtists(); insertCheckboxesFromArtistsData(); insertCheckboxesFromCategories(); await loadCheckboxesState(); showHideCategories(); await loadOptionsState(); await loadFavoritesState(); hideAllArtists(); unhideBasedOnPermissiveSetting(); updateArtistsCountPerTag('start'); rotatePromptsImages(); sortArtists(); sortTags(); await loadMostUsedTags(); updateArtistsCountPerCategory(); showHideLowCountTags(); makeStyleRuleForDrag(); teasePartition(); addAllListeners(); } function checkStoringAccessType() { return new Promise((resolve, reject) => { try { localStorage.setItem('testKey', 'testValue'); localStorage.removeItem('testKey'); storingAccessType = 'localStorage'; console.log('all settings saved using localStorage'); resolve(); } catch (error) { return caches.open('testCache') .then(cache => { const blob = new Blob([JSON.stringify('test')], { type: 'application/json' }); const responseToCache = new Response(blob); cache.put('testCache', responseToCache).then(response => { storingAccessType = 'dataCache'; console.log('all settings saved using dataCache'); return; }) .catch(error => { console.warn('no settings can be saved; we only have read access to cache: ' + error); resolve(); }); }) .catch(error => { console.warn('no settings can be saved; no access to any storage method: ' + error); resolve(); }); } }).catch(error => { console.warn('had error writing to localStorage: ', error); }); } function loadItemBasedOnAccessType(item) { if(storingAccessType == 'localStorage') { return new Promise((resolve, reject) => { try { const state = JSON.parse(localStorage.getItem(item)); resolve(state || {}); } catch (error) { reject(error); } }).catch(error => { console.warn(item + ' had error loading from localStorage: ', error); return {}; }); } else if(storingAccessType == 'dataCache') { return caches.open('dataCache') .then(cache => { return cache.match(item); }) .then(response => { if(response) { return response.json(); } return {}; }) .catch(error => { console.warn(item + ' had error loading from cache: ', error); }); } else if(storingAccessType == 'none') { return Promise.resolve({}); } } function storeItemBasedOnAccessType(item, stateArray, key, value) { if(storingAccessType == 'localStorage') { try { if(stateArray) { localStorage.setItem(item, JSON.stringify(stateArray)); } else { let state = JSON.parse(localStorage.getItem(item)) || {}; state[key] = value; localStorage.setItem(item, JSON.stringify(state)); } } catch (error) { console.warn(item + ' had error saving localStorage: ', error); } } else if(storingAccessType = 'dataCache') { caches.open('dataCache').then(cache => { if(stateArray) { const blob = new Blob([JSON.stringify(stateArray)], { type: 'application/json' }); const responseToCache = new Response(blob); return cache.put(item, responseToCache); } else { // try to get the item state from the cache cache.match(item).then(response => { let state = {}; if(response) { return response.json().then(cachedData => { state = cachedData || {}; return state; }); } else { return state; } }).then(state => { state[key] = value; // store the updated state back to the cache const blob = new Blob([JSON.stringify(state)], { type: 'application/json' }); const responseToCache = new Response(blob); return cache.put(item, responseToCache); }); } }).catch(error => { console.warn(item + ' had error saving to cache: ', error); }); } else if(storingAccessType == 'none') { alertNoStoringAccess(0); } } async function deleteItemBasedOnAccessType(item) { if(storingAccessType == 'localStorage') { localStorage.removeItem(item); } else if(storingAccessType = 'dataCache') { await caches.delete(item); } else if(storingAccessType == 'none') { // nothing to do } } function alertNoStoringAccess(wait) { window.setTimeout(function(){ let msg = ''; msg += 'My apologies, your browser settings block the ability to save settings and favorites. Suggestions:\n'; msg += '1. Try Firefox, Safari, or Edge\n' msg += '2. Download the app to use offline\n'; msg += '3. On Chrome, enable 3rd-party cookies (not recommended)\n\n'; msg += 'This app doesn\'t use cookies, never sends data to any server, and saves all data locally. But since this app is hosted on Hugging Face, Chrome treats it as a "3rd-party". Other browsers give you more nuanced control of your privacy settings.'; alert(msg); },wait); } function updateTagsConcatenated() { // this set is used for tag editing mode for (var i=0, il=tagCategories.length; i { editedArtists = new Set(Array.from(state)); let proto = window.location.protocol; let anyChanges = false; for (let i=0, il=artistsData.length; i editedA[0] === artist[0] && editedA[1] === artist[1]); if(artistFound) { // check if the edit now matches the original let match = true; for (let j=0, jl=artist.length; j { var last = artist[0]; var first = artist[1]; var tags2 = artist[2].replaceAll('|', ', '); // for display // artists can have a tag in the format of "added-YYYY-MM" // we want that to show up as a filter, but not on the artist card tags2 = tags2.replace(/, added-(\d|-)*/g,''); var itemDiv = document.createElement('div'); itemDiv.className = 'image-item'; itemDiv.dataset.tagList = artist[2].toLowerCase(); if(artist[3]) { itemDiv.dataset.deprecated = true; } var itemHeader = document.createElement('span'); var h3 = document.createElement('h3'); itemHeader.appendChild(h3); var firstN = document.createElement('span'); var lastN = document.createElement('span'); firstN.className = 'firstN'; lastN.className = 'lastN'; firstN.textContent = `${first}`; lastN.textContent = `${last}`; h3.appendChild(firstN); h3.appendChild(lastN); h3.title = 'copy to clipboard'; var h4 = document.createElement('h4'); h4.textContent = tags2; h4.title = 'copy to clipboard'; itemHeader.appendChild(h4); itemDiv.appendChild(itemHeader); var box = document.createElement('div'); var imgTools = document.createElement('div'); imgTools.className = 'imgTools'; var artPrev = document.createElement('div'); artPrev.className = 'art_prev'; var artPrevSpan = document.createElement('span'); artPrevSpan.textContent = '🧑'; artPrev.appendChild(artPrevSpan); imgTools.appendChild(artPrev); var artStar = document.createElement('div'); artStar.className = 'art_star'; var artStarSpan = document.createElement('span'); artStarSpan.textContent = '⭐️'; artStar.appendChild(artStarSpan); imgTools.appendChild(artStar); var artNext = document.createElement('div'); artNext.className = 'art_next'; var artNextSpan = document.createElement('span'); artNextSpan.textContent = '🏞️'; artNext.appendChild(artNextSpan); imgTools.appendChild(artNext); var artEdit = document.createElement('div'); artEdit.className = 'art_edit'; var artEditSpan = document.createElement('span'); artEditSpan.textContent = '✍️'; artEdit.appendChild(artEditSpan); imgTools.appendChild(artEdit); var artSearch = document.createElement('a'); artSearch.className = 'art_search'; artSearch.href = 'https://www.bing.com/images/search?q=' + artist[1].replace(' ','+') + '+' + artist[0].replace(' ','+') + '+artist'; artSearch.target = '_blank'; var artSearchSpan = document.createElement('span'); artSearchSpan.textContent = '🌐'; artSearch.appendChild(artSearchSpan); imgTools.appendChild(artSearch); box.appendChild(imgTools); var imgBox = document.createElement('div'); imgBox.className = 'imgBox'; var imgArtwork = document.createElement('img'); var imgPortrait = document.createElement('img'); var imgLandscape = document.createElement('img'); imgArtwork.alt = `${first} ${last}` + ' - artwork'; imgPortrait.alt = `${first} ${last}` + ' - portrait'; imgLandscape.alt = `${first} ${last}` + ' - landscape'; imgArtwork.className = 'img_artwork'; imgPortrait.className = 'img_portrait hidden'; imgLandscape.className = 'img_landscape hidden'; let src = 'images/SDXL_1_0_thumbs/'; if(first == '') { src += last.replaceAll(' ', '_'); } else { src += first.replaceAll(' ', '_') + '_' + last.replaceAll(' ', '_'); } // files use accented characters and huggingface stores the files with this encoding src = encodeURI(src.normalize("NFD")); imgBox.appendChild(imgArtwork); imgBox.appendChild(imgPortrait); imgBox.appendChild(imgLandscape); box.appendChild(imgBox); itemDiv.appendChild(box); container.appendChild(itemDiv); if(artist[3]) { var deprecatedSpan = document.createElement('span'); deprecatedSpan.textContent = 'this artist is unknown to SDXL - more info in the help ⁉️' deprecatedSpan.className = 'deprecated'; imgBox.appendChild(deprecatedSpan); return Promise.allSettled([ new Promise((resolve, reject) => { imgArtwork.style.display = 'none'; imgArtwork.src = 'images/SDXL_1_0_thumbs/1x1.webp'; }), new Promise((resolve, reject) => { imgPortrait.style.display = 'none'; imgPortrait.src = 'images/SDXL_1_0_thumbs/1x1.webp'; }), new Promise((resolve, reject) => { imgLandscape.style.display = 'none'; imgLandscape.src = 'images/SDXL_1_0_thumbs/1x1.webp'; }) ]); } else { // if not flagged as deprecated return Promise.allSettled([ new Promise((resolve, reject) => { imgArtwork.onload = resolve; imgArtwork.onerror = () => { missingFiles += '
  • ' + first + '_' + last + '-artwork.webp
  • '; reject(); }; imgArtwork.src = src + '-artwork.webp'; }), new Promise((resolve, reject) => { imgPortrait.onload = resolve; imgPortrait.onerror = () => { missingFiles += '
  • ' + first + '_' + last + '-portrait.webp
  • '; reject(); }; imgPortrait.src = src + '-portrait.webp'; }), new Promise((resolve, reject) => { imgLandscape.onload = resolve; imgLandscape.onerror = () => { missingFiles += '
  • ' + first + '_' + last + '-landscape.webp
  • '; reject(); }; imgLandscape.src = src + '-landscape.webp'; }) ]); } }); let report = document.getElementById('missing_images_report'); Promise.allSettled(imagePromises).then(() => { if(missingFiles.indexOf('webp')>0) { report.innerHTML = missingFiles; } else { report.innerHTML = '
  • No thumbnails files are missing! Enlarged images are loaded on hover. If any are missing, they\'ll be listed here at that time.
  • ' } }); } function insertCheckboxesFromArtistsData() { var uniqueTags = new Set(); artistsData.forEach(function(artist) { var tags = artist[2].split('|'); tags.forEach(function(tag) { uniqueTags.add(tag.toLowerCase()); }); }); var uTags = Array.from(uniqueTags); var toggles = document.getElementById('toggles'); for(i=0,il=uTags.length;i 0) { // 👆 shouldn't need to sanitize database, but just in case var label = document.createElement('label'); var el = document.createElement('i'); el.className = 'most_used_indicator'; el.textContent = '+'; var input = document.createElement('input'); input.type = 'checkbox'; input.name = uTags[i]; input.value = uTags[i]; input.checked = true; var span1 = document.createElement('span'); span1.textContent = uTags[i]; var span2 = document.createElement('span'); span2.className = 'count'; label.appendChild(el); label.appendChild(input); label.appendChild(span1); label.appendChild(span2); toggles.appendChild(label); } } } function insertCheckboxesFromCategories() { for(i=0,il=tagCategories.length;i { let allChecked = true; for (let name in state) { if (document.querySelector('input[name="'+name+'"]')) { document.querySelector('input[name="'+name+'"]').checked = state[name]; styleLabelToCheckbox(document.querySelector('input[name="'+name+'"]')); if(name != 'mode' && name != 'use_categories') { if(!state[name]) { allChecked = false; } } } } if(!allChecked) { document.querySelector('input[name="check-all"]').checked = false; } }); } function storeCheckboxState(checkbox) { if(checkbox.name != 'check-all') { storeItemBasedOnAccessType('tagsChecked',false,checkbox.name,checkbox.checked); } } function storeCheckboxStateAll(isChecked) { let checkboxes = document.querySelectorAll('input[type="checkbox"]'); let state = {}; checkboxes.forEach(function(checkbox) { let isTop = checkbox.parentNode.classList.contains('top_control'); if(!isTop || checkbox.name == 'favorite') { // is a tag checkbox, not a setting if(isChecked) { state[checkbox.name] = true; } else { state[checkbox.name] = false; } } }); storeItemBasedOnAccessType('tagsChecked',state,false,false); } async function loadOptionsState() { await loadItemBasedOnAccessType('optionsChecked').then(state => { if(state['prompt']) { let optionsPrompts = document.getElementById('options_prompts'); optionsPrompts.querySelectorAll('.selected')[0].classList.remove('selected'); document.getElementById(state['prompt']).classList.add('selected'); if(state['prompt'] == 'promptA') { imgTypeShown = 0; } else if(state['prompt'] == 'promptP') { imgTypeShown = 1; } else if(state['prompt'] == 'promptL') { imgTypeShown = 2; } } else { // promptA is already highlighted by HTML imgTypeShown = 0; } if(state['artistSort']) { document.getElementById('options_artist_sort').querySelectorAll('.selected')[0].classList.remove('selected'); document.getElementById(state['artistSort']).classList.add('selected'); } else { // sortAR is already highlighted by HTML } if(state['tagSort']) { document.getElementById('options_tag_sort').querySelectorAll('.selected')[0].classList.remove('selected'); document.getElementById(state['tagSort']).classList.add('selected'); } else { // sortTC is already highlighted by HTML } }); } function highlightSelectedOption(selected) { if(selected == 'prev' || selected == 'next') { if(selected == 'prev') { imgTypeShown--; if(imgTypeShown < 0) { imgTypeShown = 2; } } else if(selected == 'next') { imgTypeShown++; if(imgTypeShown > 2) { imgTypeShown = 0; } } let optionsPrompts = document.getElementById('options_prompts'); var links = optionsPrompts.querySelectorAll('.link'); links.forEach(function(link) { link.classList.remove('selected'); }); if(imgTypeShown == 0) { document.getElementById('promptA').classList.add('selected'); doAlert('Showing artwork',0); } else if(imgTypeShown == 1) { document.getElementById('promptP').classList.add('selected'); doAlert('Showing portraits',0); } else if(imgTypeShown == 2) { document.getElementById('promptL').classList.add('selected'); doAlert('Showing landscapes',0); } } else { if(selected == 'promptA') { imgTypeShown = 0; doAlert('Showing artwork',0); } else if(selected == 'promptP') { imgTypeShown = 1; doAlert('Showing portraits',0); } else if(selected == 'promptL') { imgTypeShown = 2; doAlert('Showing landscapes',0); } var links = document.getElementById(selected).parentNode.querySelectorAll('.link'); links.forEach(function(link) { link.classList.remove('selected'); }); document.getElementById(selected).classList.add('selected'); } } function storeOptionsState() { let state = {}; if(document.getElementById('promptA').classList.contains('selected')) { state['prompt'] = 'promptA'; } else if(document.getElementById('promptP').classList.contains('selected')) { state['prompt'] = 'promptP'; } else { state['prompt'] = 'promptL'; } if(document.getElementById('sortAR').classList.contains('selected')) { state['artistSort'] = 'sortAR'; } else { state['artistSort'] = 'sortAA'; } if(document.getElementById('sortTC').classList.contains('selected')) { state['tagSort'] = 'sortTC'; } else { state['tagSort'] = 'sortTA'; } storeItemBasedOnAccessType('optionsChecked',state,false,false); } function rotatePromptsImages() { // hide all images let images = document.querySelectorAll('.imgBox img'); images.forEach(function(image) { image.classList.add('hidden'); }); // unhide images matching highlighted option (imgTypeShown) if(imgTypeShown == 0) { images = document.querySelectorAll('.img_artwork'); } else if(imgTypeShown == 1) { images = document.querySelectorAll('.img_portrait'); } else if(imgTypeShown == 2) { images = document.querySelectorAll('.img_landscape'); } images.forEach(function(image) { image.classList.remove('hidden'); }); // switch prev and next button icons let artIndex = 0; artIndex = imgTypeShown-1; if(artIndex < 0) { artIndex = 2; } let prevButtons = document.querySelectorAll('.art_prev span'); prevButtons.forEach(function(span) { span.textContent = artTypes[artIndex]; }); artIndex = imgTypeShown+1; if(artIndex > 2) { artIndex = 0; } let nextButtons = document.querySelectorAll('.art_next span'); nextButtons.forEach(function(span) { span.textContent = artTypes[artIndex]; }); } function updateArtistsCountPerTag(whoCalled) { if(whoCalled == 'start') { // on page load, we need to add all the counts first updateArtistsCountPerTagSlow(); } timer = setTimeout(function() { // for checkbox, we defer counts because it's slow // delay for 100 to allow CSS animation to complete updateArtistsCountPerTagSlow(); },100); } function updateArtistsCountPerTagSlow() { let permissiveCheckbox = document.querySelector('input[name="mode"]'); let isPermissive = permissiveCheckbox.checked; let deprecatedCheckbox = document.querySelector('input[name="deprecated"]'); let checkboxes = document.querySelectorAll('input[type="checkbox"]'); let divs = document.querySelectorAll('.image-item'); let hiddenDivs = document.querySelectorAll('.image-item.hidden'); let deprecatedDivs = document.querySelectorAll('.image-item[data-deprecated="true"]'); let last = performance.now(); checkboxes.forEach(function(checkbox) { let isTop = checkbox.parentNode.classList.contains('top_control'); if(!isTop) { let matchingDivs; if(isPermissive) { matchingDivs = document.querySelectorAll('.image-item[data-tag-list*="' + checkbox.name + '"]'); } else { // for strict mode, for each checkbox, only count artists with a tags matching all checked checkboxes matchingDivs = document.querySelectorAll('.image-item[data-tag-list*="' + checkbox.name + '"]:not(.hidden)'); } let filteredDivs = Array.from(matchingDivs).filter(mat => { // only includes the artists known to SD return !Array.from(deprecatedDivs).some(dep => dep === mat); }); let count = 0; if(deprecatedCheckbox.checked) { count = filteredDivs.length; } else { count = matchingDivs.length; } if(!count) { count = 0; } checkbox.parentNode.querySelector('.count').textContent = ' - ' + count.toLocaleString(); checkbox.parentNode.classList.remove('no_matches'); checkbox.parentNode.querySelector('input').disabled = false; if(!isPermissive) { if(count == 0) { checkbox.parentNode.classList.add('no_matches'); checkbox.parentNode.querySelector('input').disabled = true; } } } }); updateCountOfAllArtistsShown(divs, hiddenDivs); if(isPermissive) { updateArtistsCountPerCategory(); } } function updateArtistsCountPerCategory() { let imageItems = document.querySelectorAll('.image-item'); let notDeprecatedItems = document.querySelectorAll('.image-item:not([data-deprecated="true"])'); let deprecatedCheckbox = document.querySelector('input[name="deprecated"]'); let countItems = imageItems; if(deprecatedCheckbox.checked) { countItems = notDeprecatedItems; } let counts = []; for(i=0,il=tagCategories.length; i tag.toLowerCase()).some(tag => tagList.includes(tag))) { counts[i]++; } } }); for(i=0,il=tagCategories.length; i