Mattthew commited on
Commit
474474d
1 Parent(s): b0f5ee6

major refactor - attempt to use data cache

Browse files

attempt to use data cache in place of localstorage when the latter is stupidly blocked by chrome. Since cache is async, that required a refactor. meanwhile I made some speed improvements

Files changed (2) hide show
  1. index.html +1 -1
  2. index.js +371 -338
index.html CHANGED
@@ -32,7 +32,7 @@
32
  </ul>
33
  <h3>Hide low-use tags</h3>
34
  <ul>
35
- <li><strong>checked:</strong> hides tags that match less than 3 artists</li>
36
  <li>note that all hidden tags are unchecked</li>
37
  </ul>
38
  <h3>Hide unknown to SDXL</h3>
 
32
  </ul>
33
  <h3>Hide low-use tags</h3>
34
  <ul>
35
+ <li><strong>checked:</strong> hides tags that match less than 4 artists, ~50% of all tags</li>
36
  <li>note that all hidden tags are unchecked</li>
37
  </ul>
38
  <h3>Hide unknown to SDXL</h3>
index.js CHANGED
@@ -15,81 +15,181 @@ var theTime = new Date;
15
  var startUpTime;
16
  var tagsConcatenated = new Set();
17
  var editedArtists = new Set();
18
- var localStorageAccess = false;
19
- var cacheAccess = false;
20
-
21
  //
22
  //
23
  //
 
 
 
 
 
 
 
 
24
  // functions
25
- function startUp() {
26
- checkLocalStorageAccess();
27
- alertNoLocalStorage(2000);
28
- checkCacheAccess();
29
  updateTagsConcatenated();
30
  updateFooter();
31
- loadEditedArtists();
32
  insertArtists();
33
  insertCheckboxesFromArtistsData();
34
  insertCheckboxesFromCategories();
35
- loadCheckboxesState();
36
  showHideCategories();
37
- loadOptionsState();
38
- loadFavoritesState();
39
  hideAllArtists();
40
  unhideBasedOnPermissiveSetting();
41
  updateArtistsCountPerTag('start');
42
  rotatePromptsImages();
43
  sortArtists();
44
  sortTags();
45
- loadMostUsedTags();
46
  updateArtistsCountPerCategory();
47
  showHideLowCountTags();
48
  makeStyleRuleForDrag();
49
  teasePartition();
 
50
  }
51
 
52
- function checkLocalStorageAccess() {
53
- try {
54
- localStorage.setItem('testKey', 'testValue');
55
- localStorage.removeItem('testKey');
56
- localStorageAccess = true;
57
- } catch (error) {
58
- localStorageAccess = false;
59
- alertNoLocalStorage();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
  }
62
 
63
- async function checkCacheAccess() {
64
- try {
65
- // open or create test cache
66
- const cache = await caches.open('testCache');
67
- cacheAccess = true;
68
- await caches.delete('testCache');
69
- console.log("Cache API Access:", cacheAccess);
70
- } catch (error) {
71
- console.error('Cache API Access Error: ', error);
72
- cacheAccess = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
  }
75
 
76
- function alertNoLocalStorage(wait) {
77
- if(!localStorageAccess) {
78
- window.setTimeout(function(){
79
- let msg = '';
80
- msg += 'My apologies, your browser settings block the ability to save settings and favorites. If you want those features, you have 3 options:\n';
81
- msg += '1. Use a different browser than Chrome\n'
82
- msg += '2. Change Chrome settings\n';
83
- msg += '3. Download the app to use offline\n\n';
84
- msg += 'This app doesn\'t use cookies and instead saves all settings locally so that no data is ever sent to any server. But when you set to Chrome to block third-party cookies (which you should), it stupidly also blocks local storage. That\'s because Google wants you to feel pain for blocking their ad-based revenue model. To change this setting in Chrome (not recommended):\n';
85
- msg += '1. In settings, click "Privacy and security"\n';
86
- msg += '2. Click "Third-party cookies" and set it to "Allow third-party cookies"\n';
87
- msg += '3. Unfortunately, that will allow all 3rd-party cookies on all sites, which is exactly what Google wants.\n';
88
- alert(msg);
89
- },wait);
90
  }
91
  }
92
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  function updateTagsConcatenated() {
94
  // this set is used for tag editing mode
95
  for (var i=0, il=tagCategories.length; i<il; i++) {
@@ -117,10 +217,9 @@ function updateFooter() {
117
  }
118
  }
119
 
120
- function loadEditedArtists() {
121
- if(localStorageAccess) {
122
- const arr = JSON.parse(localStorage.getItem('editedArtists')) || [];
123
- editedArtists = new Set(arr);
124
  let proto = window.location.protocol;
125
  let anyChanges = false;
126
  for (let i=0, il=artistsData.length; i<il; i++) {
@@ -147,9 +246,9 @@ function loadEditedArtists() {
147
  }
148
  }
149
  if(anyChanges) {
150
- localStorage.setItem('editedArtists', JSON.stringify(Array.from(editedArtists)));
151
  }
152
- }
153
  }
154
 
155
  function insertArtists() {
@@ -159,16 +258,13 @@ function insertArtists() {
159
  let imagePromises = artistsData.map((artist) => {
160
  var last = artist[0];
161
  var first = artist[1];
162
- var tags1 = artist[2].replaceAll('|', ' ').toLowerCase(); // for classes
163
  var tags2 = artist[2].replaceAll('|', ', '); // for display
164
- // class names can't start with a number, but some tags do
165
- // in these cases we prepend the class with 'qqqq-'
166
- tags1 = tags1.replace(/(^|\s)(\d)/g, '$1qqqq-$2');
167
  // artists can have a tag in the format of "added-YYYY-MM"
168
  // we want that to show up as a filter, but not on the artist card
169
  tags2 = tags2.replace(/, added-(\d|-)*/g,'');
170
  var itemDiv = document.createElement('div');
171
- itemDiv.className = 'image-item ' + tags1;
 
172
  if(artist[3]) {
173
  itemDiv.dataset.deprecated = true;
174
  }
@@ -359,9 +455,8 @@ function insertCheckboxesFromCategories() {
359
  }
360
  }
361
 
362
- function loadCheckboxesState() {
363
- if(localStorageAccess) {
364
- let state = JSON.parse(localStorage.getItem('tagsChecked')) || {};
365
  let allChecked = true;
366
  for (let name in state) {
367
  if (document.querySelector('input[name="'+name+'"]')) {
@@ -376,39 +471,32 @@ function loadCheckboxesState() {
376
  if(!allChecked) {
377
  document.querySelector('input[name="check-all"]').checked = false;
378
  }
379
- }
380
  }
381
 
382
  function storeCheckboxState(checkbox) {
383
- if(localStorageAccess) {
384
- let state = JSON.parse(localStorage.getItem('tagsChecked')) || {};
385
- state[checkbox.name] = checkbox.checked;
386
- localStorage.setItem('tagsChecked', JSON.stringify(state));
387
- }
388
  }
389
 
390
  function storeCheckboxStateAll(isChecked) {
391
- if(localStorageAccess) {
392
- let state = {};
393
- var checkboxes = document.querySelectorAll('input[type="checkbox"]');
394
- checkboxes.forEach(function(checkbox) {
395
- let isTop = checkbox.parentNode.classList.contains('top_control');
396
- if(!isTop || checkbox.name == 'favorite') {
397
- // is a tag checkbox, not a setting
398
- if(isChecked) {
399
- state[checkbox.name] = true;
400
- } else {
401
- state[checkbox.name] = false;
402
- }
403
  }
404
- });
405
- localStorage.setItem('tagsChecked', JSON.stringify(state));
406
- }
407
  }
408
 
409
- function loadOptionsState() {
410
- if(localStorageAccess) {
411
- let state = JSON.parse(localStorage.getItem('tagsChecked')) || {};
412
  if(state['prompt']) {
413
  document.getElementById('options_prompts').querySelectorAll('.selected')[0].classList.remove('selected');
414
  document.getElementById(state['prompt']).classList.add('selected');
@@ -435,7 +523,7 @@ function loadOptionsState() {
435
  } else {
436
  // sortTC is already highlighted by HTML
437
  }
438
- }
439
  }
440
 
441
  function highlightSelectedOption(selected) {
@@ -481,27 +569,25 @@ function highlightSelectedOption(selected) {
481
  }
482
 
483
  function storeOptionsState() {
484
- if(localStorageAccess) {
485
- let state = JSON.parse(localStorage.getItem('tagsChecked')) || {};
486
- if(document.getElementById('promptA').classList.contains('selected')) {
487
- state['prompt'] = 'promptA';
488
- } else if(document.getElementById('promptP').classList.contains('selected')) {
489
- state['prompt'] = 'promptP';
490
- } else {
491
- state['prompt'] = 'promptL';
492
- }
493
- if(document.getElementById('sortAR').classList.contains('selected')) {
494
- state['artistSort'] = 'sortAR';
495
- } else {
496
- state['artistSort'] = 'sortAA';
497
- }
498
- if(document.getElementById('sortTC').classList.contains('selected')) {
499
- state['tagSort'] = 'sortTC';
500
- } else {
501
- state['tagSort'] = 'sortTA';
502
- }
503
- localStorage.setItem('tagsChecked', JSON.stringify(state));
504
  }
 
505
  }
506
 
507
  function rotatePromptsImages() {
@@ -538,72 +624,60 @@ function rotatePromptsImages() {
538
  }
539
 
540
  function updateArtistsCountPerTag(whoCalled) {
541
- window.setTimeout(function() {
542
- var permissiveCheckbox = document.querySelector('input[name="mode"]');
543
- var deprecatedCheckbox = document.querySelector('input[name="deprecated"]');
544
- var checkboxes = document.querySelectorAll('input[type="checkbox"]');
545
- var divs = document.querySelectorAll('.image-item');
546
- var hiddenDivs = document.querySelectorAll('.image-item.hidden');
547
- var deprecatedDivs = document.querySelectorAll('.image-item[data-deprecated="true"]');
548
- var count = 0;
549
- if(permissiveCheckbox.checked || whoCalled == 'start') {
550
- // on page load, we need to add all the counts first
551
- checkboxes.forEach(function(checkbox) {
552
- let isTop = checkbox.parentNode.classList.contains('top_control');
553
- if(!isTop) {
554
- var theClass = checkbox.name.replace(/(^|\s)(\d)/g, '$1qqqq-$2');
555
- var matchingDivs = document.querySelectorAll('.image-item.' + theClass);
556
- let filteredDivs = Array.from(matchingDivs).filter(mat => {
557
- return !Array.from(deprecatedDivs).some(dep => dep === mat);
558
- });
559
- if(deprecatedCheckbox.checked) {
560
- count = filteredDivs.length;
561
- } else {
562
- count = matchingDivs.length;
563
- }
564
- checkbox.parentNode.classList.remove('no_matches');
565
- checkbox.parentNode.querySelector('input').disabled = false;
566
- // count null when tag/checkbox exists, but the artist is hidden
567
- if(count) {
568
- checkbox.parentNode.querySelector('.count').textContent = ' - ' + count.toLocaleString();
569
- } else {
570
- checkbox.parentNode.querySelector('.count').textContent = ' - ' + '0';
571
- }
572
- }
573
  });
574
- updateArtistsCountPerCategory();
575
- }
576
- if(!permissiveCheckbox.checked) {
577
- checkboxes.forEach(function(checkbox) {
578
- let isTop = checkbox.parentNode.classList.contains('top_control');
579
- if(!isTop) {
580
- count = 0;
581
- // class names can't start with a number, but some tags do
582
- // in these cases prepending with 'qqqq-'
583
- var theClass = checkbox.name.replace(/(^|\s)(\d)/g, '$1qqqq-$2');
584
- // for strict mode, for each checkbox, only count artists with a classes matching all checked checkboxes
585
- var matchingDivs = document.querySelectorAll('.image-item.' + theClass + ':not(.hidden)');
586
- let filteredDivs = Array.from(matchingDivs).filter(mat => {
587
- return !Array.from(deprecatedDivs).some(dep => dep === mat);
588
- });
589
- if(deprecatedCheckbox.checked) {
590
- count = filteredDivs.length;
591
- } else {
592
- count = matchingDivs.length;
593
- }
594
- if(count == 0) {
595
- checkbox.parentNode.classList.add('no_matches');
596
- checkbox.parentNode.querySelector('input').disabled = true;
597
- } else {
598
- checkbox.parentNode.classList.remove('no_matches');
599
- checkbox.parentNode.querySelector('input').disabled = false;
600
- }
601
- checkbox.parentNode.querySelector('.count').textContent = ' - ' + count.toLocaleString();
602
  }
603
- });
604
  }
605
- updateCountOfAllArtistsShown(divs, hiddenDivs);
606
- },0);
 
 
 
607
  }
608
 
609
  function updateArtistsCountPerCategory() {
@@ -613,14 +687,9 @@ function updateArtistsCountPerCategory() {
613
  counts[i] = 0;
614
  }
615
  imageItems.forEach(function(imageItem) {
616
- var classes = Array.from(imageItem.classList).map(className => {
617
- // class names can't start with a number,
618
- // so some classes were prepending with 'qqqq-'
619
- // which must be ignored
620
- return className.replace(/^qqqq-/, '');
621
- });
622
  for(i=0,il=tagCategories.length; i<il; i++) {
623
- if(tagCategories[i].map(c => c.toLowerCase()).some(c => classes.includes(c))) {
624
  counts[i]++;
625
  }
626
  }
@@ -713,20 +782,18 @@ function unhideBasedOnPermissiveSetting() {
713
 
714
  function unhideArtistsPermissive() {
715
  // permissive mode unhides images that match ANY checked tag
716
- // the set of checkboxes is derived from the unique tags within the imageItem (Artists) classes
717
  var imageItems = document.querySelectorAll('.image-item');
718
  var checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"]'))
719
  .filter(cb => !cb.parentNode.classList.contains("top_control"));
720
  checkboxes.push(document.querySelector('input[name="favorite"]'));
721
  var checked = checkboxes.filter(cb => cb.checked).map(cb => cb.name);
722
  imageItems.forEach(function(imageItem) {
723
- var classes = Array.from(imageItem.classList).map(className => {
724
- // class names can't start with a number,
725
- // so some classes were prepending with 'qqqq-'
726
- // which must be ignored
727
- return className.replace(/^qqqq-/, '');
728
- });
729
- if(checked.some(c => classes.includes(c))) {
730
  imageItem.classList.remove('hidden');
731
  }
732
  });
@@ -735,7 +802,7 @@ function unhideArtistsPermissive() {
735
 
736
  function unhideArtistsStrict() {
737
  // strict mode unhides images that match ALL checked tags
738
- // the set of checkboxes is derived from the unique tags within the imageItem (Artists) classes
739
  var imageItems = document.querySelectorAll('.image-item');
740
  var checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"]'))
741
  .filter(cb => !cb.parentNode.classList.contains("top_control"));
@@ -743,13 +810,11 @@ function unhideArtistsStrict() {
743
  var checked = checkboxes.filter(cb => cb.checked).map(cb => cb.name);
744
  if(checked.length > 0) {
745
  imageItems.forEach(function(imageItem, index) {
746
- var classes = Array.from(imageItem.classList).map(className => {
747
- // class names can't start with a number,
748
- // so some classes were prepending with 'qqqq-'
749
- // which must be ignored
750
- return className.replace(/^qqqq-/, '');
751
- });
752
- if(checked.every(c => classes.includes(c))) {
753
  imageItem.classList.remove('hidden');
754
  }
755
  });
@@ -766,7 +831,7 @@ function unhideArtistsStrict() {
766
  function unhideAristsExact() {
767
  // exact mode isn't currently used because almost no two artists have the same set of tags
768
  // exact mode unhides images that match ALL checked tags and NO unchecked tags
769
- // the set of checkboxes is derived from the unique tags within the imageItem (Artists) classes
770
  var imageItems = document.querySelectorAll('.image-item');
771
  var checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"]'))
772
  .filter(cb => !cb.parentNode.classList.contains("top_control"));
@@ -775,9 +840,12 @@ function unhideAristsExact() {
775
  var unchecked = checkboxes.filter(cb => !cb.checked).map(cb => cb.name);
776
  if(checked.length > 0) {
777
  imageItems.forEach(function(imageItem, index) {
778
- var classes = Array.from(imageItem.classList);
779
- if(checked.every(c => classes.includes(c))) {
780
- if(unchecked.every(c => !classes.includes(c))) {
 
 
 
781
  imageItem.classList.remove('hidden');
782
  }
783
  }
@@ -837,26 +905,25 @@ function showExport() {
837
  // favorites
838
  var textareaF = document.getElementById('export_favorites_list');
839
  var favoritedArtists = false;
840
- if(localStorageAccess) {
841
- var favorites = localStorage.getItem('favoritedArtists');
842
  var value = '';
843
- if(favorites) {
844
  value += 'You have favorited these artists:\r\n';
845
- for (let key in JSON.parse(favorites)) {
846
- if (JSON.parse(favorites)[key] === true) {
847
  let names = key.split("|");
848
  if(!names[0]) { names[0] = '(no first name)'; }
849
  value += '•' + names[0] + ',' + names[1] + '\r\n';
850
  }
851
  }
852
- value += '\r\n\r\nTo import these favorites later, click "copy to clipboard" and save to any file. Then paste the text from that file into this text box, and click "import". The imported text must contain the JSON string below (the curly brackets and what\'s between them). It must not contain any other more than one set of curly brackets.\r\n\r\n' + favorites;
853
  textareaF.value = value;
854
  } else {
855
  value += 'You haven\'t favorited any artists yet.\r\n\r\n';
856
  value += 'To import favorites that you exported earlier, paste the text into this text box, and click "import".';
857
  textareaF.value = value;
858
  }
859
- }
860
  // edits
861
  var textareaE = document.getElementById('export_edits_list');
862
  let editedArtistsArr = Array.from(editedArtists);
@@ -899,44 +966,42 @@ function exportTextarea(type) {
899
  }
900
 
901
  function importFavorites() {
902
- if(localStorageAccess) {
903
- let el = document.getElementById('export_favorites_list');
904
- let favorites = el.value;
905
- let startCount = (favorites.match(/{/g) || []).length;
906
- let endCount = (favorites.match(/}/g) || []).length;
907
- if (startCount > 1 || endCount > 1) {
908
- el.value = 'That text can\'t be imported because it contains multiple curly brackets {}.'
909
- return null;
910
- }
911
- let start = favorites.indexOf('{');
912
- let end = favorites.lastIndexOf('}');
913
- if (start === -1 || end === -1) {
914
- el.value = 'That text can\'t be imported because it contains zero curly brackets {}.'
915
- return null;
916
- }
917
- let jsonString = favorites.substring(start, end + 1);
918
- try {
919
- let jsonObject = JSON.parse(jsonString);
920
- // Check structure of each key-value pair in jsonObject
921
- for (let key in jsonObject) {
922
- let value = jsonObject[key];
923
- if (!key.includes('|') || typeof value !== 'boolean') {
924
- el.value = 'That text can\'t be imported because the JSON string it contains doesn\'t contain a valid list of artists.'
925
- return null;
926
- }
927
- }
928
- if(confirm('This will overwrite any saved favorites. Are you sure?')) {
929
- localStorage.setItem('favoritedArtists', jsonString);
930
- alert('Favorites were imported!');
931
- loadFavoritesState();
932
- } else {
933
- alert('Okay, you have cancelled the import.');
934
  return null;
935
  }
936
- } catch(e) {
937
- el.value = 'That text can\'t be imported because it doesn\'t contain a valid JSON sting.'
 
 
 
 
 
938
  return null;
939
  }
 
 
 
940
  }
941
  }
942
 
@@ -1112,9 +1177,8 @@ function sortTagsByCount() {
1112
  }
1113
  }
1114
 
1115
- function loadMostUsedTags() {
1116
- if(localStorageAccess) {
1117
- let state = JSON.parse(localStorage.getItem('mustUsedTags')) || {};
1118
  let mostUsedCategory = document.querySelector('[data-category-name="important"]');
1119
  for(let tag in state) {
1120
  if (state[tag]) {
@@ -1127,8 +1191,8 @@ function loadMostUsedTags() {
1127
  updateTagArrayToMatchMostUsed(true,label,tag);
1128
  }
1129
  }
1130
- };
1131
- }
1132
  }
1133
 
1134
  function updateTagArrayToMatchMostUsed(isAdding,label,tag) {
@@ -1160,16 +1224,14 @@ function updateTagArrayToMatchMostUsed(isAdding,label,tag) {
1160
  }
1161
 
1162
  function storeMostUsedState(label) {
1163
- if(localStorageAccess) {
1164
- var name = label.querySelector('input').name;
1165
- let state = JSON.parse(localStorage.getItem('mustUsedTags')) || {};
1166
- state[name] = label.classList.contains('is_most_used');
1167
- localStorage.setItem('mustUsedTags', JSON.stringify(state));
1168
- }
1169
  }
1170
 
1171
  function enterExitEditMostUsedMode(doExit) {
1172
- if(localStorageAccess) {
 
 
1173
  let inputs = Array.from(document.querySelectorAll('input'));
1174
  if(editMostUsedMode || doExit) {
1175
  // exit edit mode
@@ -1200,8 +1262,6 @@ function enterExitEditMostUsedMode(doExit) {
1200
  document.getElementById('gutter').style.left = '';
1201
  document.getElementById('image-container').style.marginLeft = '';
1202
  }
1203
- } else {
1204
- alertNoLocalStorage(0);
1205
  }
1206
  }
1207
 
@@ -1277,9 +1337,8 @@ function addOrRemoveFavorite(artist) {
1277
  }
1278
  }
1279
 
1280
- function loadFavoritesState() {
1281
- if(localStorageAccess) {
1282
- let state = JSON.parse(localStorage.getItem('favoritedArtists')) || {};
1283
  let artists = document.getElementsByClassName('image-item');
1284
  for(let artist of artists) {
1285
  let artistName = artist.getElementsByClassName('firstN')[0].textContent + '|' + artist.getElementsByClassName('lastN')[0].textContent;
@@ -1290,18 +1349,16 @@ function loadFavoritesState() {
1290
  }
1291
  }
1292
  updateFavoritesCount();
1293
- }
1294
  }
1295
 
1296
  function storeFavoriteState(artist) {
1297
- if(localStorageAccess) {
 
 
1298
  var artistName = artist.getElementsByClassName('firstN')[0].textContent + '|' + artist.getElementsByClassName('lastN')[0].textContent;
1299
  var isFavorited = artist.classList.contains('favorite');
1300
- let state = JSON.parse(localStorage.getItem('favoritedArtists')) || {};
1301
- state[artistName] = isFavorited;
1302
- localStorage.setItem('favoritedArtists', JSON.stringify(state));
1303
- } else {
1304
- alertNoLocalStorage(0);
1305
  }
1306
  }
1307
 
@@ -1414,7 +1471,7 @@ function showHideLowCountTags() {
1414
  // skip hide
1415
  } else {
1416
  let count = parseInt(checkbox.parentNode.querySelector('.count').textContent.replace(/,/g, '').trim().substring(2),10);
1417
- if(count < 3) {
1418
  checkbox.checked = false;
1419
  checkbox.parentNode.classList.add('hidden');
1420
  }
@@ -1422,8 +1479,8 @@ function showHideLowCountTags() {
1422
  } else {
1423
  checkbox.parentNode.classList.remove('hidden');
1424
  }
1425
- showHideCategories();
1426
  });
 
1427
  }
1428
 
1429
  function loadLargerImages(imageItem) {
@@ -1561,7 +1618,9 @@ function teasePartition() {
1561
  }
1562
 
1563
  function editTagsClicked(clickedImageItem) {
1564
- if(localStorageAccess) {
 
 
1565
  let indicatorEl = clickedImageItem.querySelector('.art_edit span');
1566
  if(indicatorEl.textContent == '✍️') {
1567
  let artistWasInEditMode = editTagsFindArtistInEditMode(clickedImageItem);
@@ -1572,8 +1631,6 @@ function editTagsClicked(clickedImageItem) {
1572
  } else {
1573
  editTagsFindArtistInEditMode();
1574
  }
1575
- } else {
1576
- alertNoLocalStorage(0);
1577
  }
1578
  }
1579
 
@@ -1765,66 +1822,64 @@ function focusInput(input) {
1765
  }
1766
 
1767
  function saveTagsForArtist(tagArea) {
1768
- if(localStorageAccess) {
1769
- // get new tags
1770
- let tagLabels = tagArea.querySelectorAll('label');
1771
- let newTagsArr = [];
1772
- let artistKnown = true;
1773
- tagLabels.forEach(function(label) {
1774
- let input = label.querySelector('input');
1775
- if(input.value == 'known') {
1776
- artistKnown = input.checked;
1777
- } else {
1778
- if(input.checked) {
1779
- newTagsArr.push(input.value);
1780
- }
1781
  }
1782
- });
1783
- // find match in artistsData
1784
- let firstN = tagArea.closest('.image-item').querySelector('.firstN').textContent;
1785
- let lastN = tagArea.closest('.image-item').querySelector('.lastN').textContent;
1786
- let edit = [];
1787
- for (let i=0, il=artistsData.length; i<il; i++) {
1788
- let artist = artistsData[i];
1789
- if(artist[0] == lastN && artist[1] == firstN) {
1790
- // artists can have a tag in the format of "added-YYYY-MM-DD"
1791
- // this was stripped earlier, so we need to add it back in
1792
- let oldTagsArr = artist[2].split('|');
1793
- for (let j=oldTagsArr.length-1; j>=0; j--) {
1794
- // loop backwards because it should be at the end
1795
- if(oldTagsArr[j].match(/added-(\d|-)*/)) {
1796
- newTagsArr.push(oldTagsArr[j]);
1797
- }
1798
- }
1799
- let newTagsStr = newTagsArr.join('|');
1800
- artist[2] = newTagsStr;
1801
- // in db, true = hide unknown, but here true = known
1802
- if(artistKnown) {
1803
- artist[3] = false;
1804
- } else {
1805
- artist[3] = true;
1806
  }
1807
- edit = artist;
1808
- break;
1809
  }
1810
- }
1811
- // replace old edits with new edits
1812
- for (let i=0, il=editedArtists.length; i<il; i++) {
1813
- let oldEdit = editedArtists[i];
1814
- if(edit[0] == oldEdit[0] && edit[1] == oldEdit[1]) {
1815
- editedArtists.delete(oldEdit);
 
1816
  }
 
 
1817
  }
1818
- editedArtists.add(edit)
1819
- // save edited artists locally
1820
- localStorage.setItem('editedArtists', JSON.stringify(Array.from(editedArtists)));
1821
  }
 
 
 
 
 
 
 
 
 
 
1822
  }
1823
 
1824
  function deleteAllEdits() {
1825
- if(localStorageAccess) {
1826
  if(confirm('This will delete all of your edits. Are you sure?')) {
1827
- localStorage.removeItem('editedArtists');
1828
  alert('official database restored! this page will reload...');
1829
  location.reload();
1830
  } else {
@@ -1832,30 +1887,8 @@ function deleteAllEdits() {
1832
  }
1833
  }
1834
  }
1835
- //
1836
- //
1837
- //
1838
- //
1839
- //
1840
- //
1841
- //
1842
- //
1843
- //
1844
- //
1845
- //
1846
- //
1847
- //
1848
- //
1849
- //
1850
- //
1851
- // content loaded function
1852
- document.addEventListener("DOMContentLoaded", function() {
1853
- //
1854
- //
1855
- startUp();
1856
- startUpTime = theTime.getTime();
1857
- //
1858
- //
1859
  // add checkbox event listeners
1860
  var checkboxes = document.querySelectorAll('input[type="checkbox"]');
1861
  checkboxes.forEach(function(checkbox) {
@@ -2075,4 +2108,4 @@ document.addEventListener("DOMContentLoaded", function() {
2075
  closeFooter.addEventListener('click', function(e) {
2076
  document.getElementById('layout').classList.add('footerHidden');
2077
  });
2078
- });
 
15
  var startUpTime;
16
  var tagsConcatenated = new Set();
17
  var editedArtists = new Set();
18
+ var storingAccessType = 'none';
19
+ const lowCountThreshold = 3;
 
20
  //
21
  //
22
  //
23
+ // wait for DOM
24
+ document.addEventListener("DOMContentLoaded", function() {
25
+ checkStoringAccessType().then(state => {
26
+ startUp();
27
+ });
28
+ startUpTime = theTime.getTime();
29
+ });
30
+
31
  // functions
32
+ async function startUp() {
 
 
 
33
  updateTagsConcatenated();
34
  updateFooter();
35
+ await loadEditedArtists();
36
  insertArtists();
37
  insertCheckboxesFromArtistsData();
38
  insertCheckboxesFromCategories();
39
+ await loadCheckboxesState();
40
  showHideCategories();
41
+ await loadOptionsState();
42
+ await loadFavoritesState();
43
  hideAllArtists();
44
  unhideBasedOnPermissiveSetting();
45
  updateArtistsCountPerTag('start');
46
  rotatePromptsImages();
47
  sortArtists();
48
  sortTags();
49
+ await loadMostUsedTags();
50
  updateArtistsCountPerCategory();
51
  showHideLowCountTags();
52
  makeStyleRuleForDrag();
53
  teasePartition();
54
+ addAllListeners();
55
  }
56
 
57
+ function checkStoringAccessType() {
58
+ return new Promise((resolve, reject) => {
59
+ try {
60
+ localStorage.setItem('testKey', 'testValue');
61
+ localStorage.removeItem('testKey');
62
+ storingAccessType = 'localStorage';
63
+ console.log('all settings saved using localStorage');
64
+ resolve();
65
+ } catch (error) {
66
+ return caches.open('testCache')
67
+ .then(cache => {
68
+ const blob = new Blob([JSON.stringify('test')], { type: 'application/json' });
69
+ const responseToCache = new Response(blob);
70
+ cache.put('testCache', responseToCache).then(response => {
71
+ storingAccessType = 'dataCache';
72
+ console.log('all settings saved using dataCache');
73
+ return;
74
+ })
75
+ .catch(error => {
76
+ console.warn('no settings can be saved; we only have read access to cache: ' + error);
77
+ alertNoStoringAccess(2000);
78
+ resolve();
79
+ });
80
+ })
81
+ .catch(error => {
82
+ console.warn('no settings can be saved; no access to any storage method: ' + error);
83
+ alertNoStoringAccess(2000);
84
+ resolve();
85
+ });
86
+ }
87
+ }).catch(error => {
88
+ console.warn('had error writing to localStorage: ', error);
89
+ });
90
+ }
91
+
92
+ function loadItemBasedOnAccessType(item) {
93
+ if(storingAccessType == 'localStorage') {
94
+ return new Promise((resolve, reject) => {
95
+ try {
96
+ const state = JSON.parse(localStorage.getItem(item));
97
+ resolve(state || {});
98
+ } catch (error) {
99
+ reject(error);
100
+ }
101
+ }).catch(error => {
102
+ console.warn(item + ' had error loading from localStorage: ', error);
103
+ return {};
104
+ });
105
+ } else if(storingAccessType == 'dataCache') {
106
+ return caches.open('dataCache')
107
+ .then(cache => {
108
+ return cache.match(item);
109
+ })
110
+ .then(response => {
111
+ if(response) {
112
+ return response.json();
113
+ }
114
+ return {};
115
+ })
116
+ .catch(error => {
117
+ console.warn(item + ' had error loading from cache: ', error);
118
+ });
119
+ } else if(storingAccessType == 'none') {
120
+ return Promise.resolve({});
121
  }
122
  }
123
 
124
+ function storeItemBasedOnAccessType(item, stateArray, key, value) {
125
+ if(storingAccessType == 'localStorage') {
126
+ try {
127
+ if(stateArray) {
128
+ localStorage.setItem(item, JSON.stringify(stateArray));
129
+ } else {
130
+ let state = JSON.parse(localStorage.getItem(item)) || {};
131
+ state[key] = value;
132
+ localStorage.setItem(item, JSON.stringify(state));
133
+ }
134
+ } catch (error) {
135
+ console.warn(item + ' had error saving localStorage: ', error);
136
+ }
137
+ } else if(storingAccessType = 'dataCache') {
138
+ caches.open('dataCache').then(cache => {
139
+ if(stateArray) {
140
+ const blob = new Blob([JSON.stringify(stateArray)], { type: 'application/json' });
141
+ const responseToCache = new Response(blob);
142
+ return cache.put(item, responseToCache);
143
+ } else {
144
+ // try to get the item state from the cache
145
+ cache.match(item).then(response => {
146
+ let state = {};
147
+ if(response) {
148
+ return response.json().then(cachedData => {
149
+ state = cachedData || {};
150
+ return state;
151
+ });
152
+ } else {
153
+ return state;
154
+ }
155
+ }).then(state => {
156
+ state[key] = value;
157
+ // store the updated state back to the cache
158
+ const blob = new Blob([JSON.stringify(state)], { type: 'application/json' });
159
+ const responseToCache = new Response(blob);
160
+ return cache.put(item, responseToCache);
161
+ });
162
+ }
163
+ }).catch(error => {
164
+ console.warn(item + ' had error saving to cache: ', error);
165
+ });
166
+ } else if(storingAccessType == 'none') {
167
+ alertNoStoringAccess(0);
168
  }
169
  }
170
 
171
+ async function deleteItemBasedOnAccessType(item) {
172
+ if(storingAccessType == 'localStorage') {
173
+ localStorage.removeItem(item);
174
+ } else if(storingAccessType = 'dataCache') {
175
+ await caches.delete(item);
176
+ } else if(storingAccessType == 'none') {
177
+ // nothing to do
 
 
 
 
 
 
 
178
  }
179
  }
180
 
181
+ function alertNoStoringAccess(wait) {
182
+ window.setTimeout(function(){
183
+ let msg = '';
184
+ msg += 'My apologies, your browser settings block the ability to save settings and favorites. This was working on Firefox, Safari, and Chrome as of Sep. 2023. Suggestions:\n';
185
+ msg += '1. Try a different browser\n'
186
+ msg += '2. Check browser privacy settings\n';
187
+ msg += '3. Download the app to use offline\n\n';
188
+ msg += 'This app doesn\'t use cookies, saves all settings locally, and never sends data to any server. However, some privacy settings may block all data storage.'
189
+ alert(msg);
190
+ },wait);
191
+ }
192
+
193
  function updateTagsConcatenated() {
194
  // this set is used for tag editing mode
195
  for (var i=0, il=tagCategories.length; i<il; i++) {
 
217
  }
218
  }
219
 
220
+ async function loadEditedArtists() {
221
+ await loadItemBasedOnAccessType('editedArtists').then(state => {
222
+ editedArtists = new Set(Array.from(state));
 
223
  let proto = window.location.protocol;
224
  let anyChanges = false;
225
  for (let i=0, il=artistsData.length; i<il; i++) {
 
246
  }
247
  }
248
  if(anyChanges) {
249
+ storeItemBasedOnAccessType('editedArtists',editedArtists,false,false);
250
  }
251
+ });
252
  }
253
 
254
  function insertArtists() {
 
258
  let imagePromises = artistsData.map((artist) => {
259
  var last = artist[0];
260
  var first = artist[1];
 
261
  var tags2 = artist[2].replaceAll('|', ', '); // for display
 
 
 
262
  // artists can have a tag in the format of "added-YYYY-MM"
263
  // we want that to show up as a filter, but not on the artist card
264
  tags2 = tags2.replace(/, added-(\d|-)*/g,'');
265
  var itemDiv = document.createElement('div');
266
+ itemDiv.className = 'image-item';
267
+ itemDiv.dataset.tagList = artist[2].toLowerCase();
268
  if(artist[3]) {
269
  itemDiv.dataset.deprecated = true;
270
  }
 
455
  }
456
  }
457
 
458
+ async function loadCheckboxesState() {
459
+ await loadItemBasedOnAccessType('tagsChecked').then(state => {
 
460
  let allChecked = true;
461
  for (let name in state) {
462
  if (document.querySelector('input[name="'+name+'"]')) {
 
471
  if(!allChecked) {
472
  document.querySelector('input[name="check-all"]').checked = false;
473
  }
474
+ });
475
  }
476
 
477
  function storeCheckboxState(checkbox) {
478
+ storeItemBasedOnAccessType('tagsChecked',false,checkbox.name,checkbox.checked);
 
 
 
 
479
  }
480
 
481
  function storeCheckboxStateAll(isChecked) {
482
+ let checkboxes = document.querySelectorAll('input[type="checkbox"]');
483
+ let state = {};
484
+ checkboxes.forEach(function(checkbox) {
485
+ let isTop = checkbox.parentNode.classList.contains('top_control');
486
+ if(!isTop || checkbox.name == 'favorite') {
487
+ // is a tag checkbox, not a setting
488
+ if(isChecked) {
489
+ state[checkbox.name] = true;
490
+ } else {
491
+ state[checkbox.name] = false;
 
 
492
  }
493
+ }
494
+ });
495
+ storeItemBasedOnAccessType('tagsChecked',state,false,false);
496
  }
497
 
498
+ async function loadOptionsState() {
499
+ await loadItemBasedOnAccessType('optionsChecked').then(state => {
 
500
  if(state['prompt']) {
501
  document.getElementById('options_prompts').querySelectorAll('.selected')[0].classList.remove('selected');
502
  document.getElementById(state['prompt']).classList.add('selected');
 
523
  } else {
524
  // sortTC is already highlighted by HTML
525
  }
526
+ });
527
  }
528
 
529
  function highlightSelectedOption(selected) {
 
569
  }
570
 
571
  function storeOptionsState() {
572
+ let state = {};
573
+ if(document.getElementById('promptA').classList.contains('selected')) {
574
+ state['prompt'] = 'promptA';
575
+ } else if(document.getElementById('promptP').classList.contains('selected')) {
576
+ state['prompt'] = 'promptP';
577
+ } else {
578
+ state['prompt'] = 'promptL';
579
+ }
580
+ if(document.getElementById('sortAR').classList.contains('selected')) {
581
+ state['artistSort'] = 'sortAR';
582
+ } else {
583
+ state['artistSort'] = 'sortAA';
584
+ }
585
+ if(document.getElementById('sortTC').classList.contains('selected')) {
586
+ state['tagSort'] = 'sortTC';
587
+ } else {
588
+ state['tagSort'] = 'sortTA';
 
 
 
589
  }
590
+ storeItemBasedOnAccessType('optionsChecked',state,false,false);
591
  }
592
 
593
  function rotatePromptsImages() {
 
624
  }
625
 
626
  function updateArtistsCountPerTag(whoCalled) {
627
+ if(whoCalled == 'start') {
628
+ // on page load, we need to add all the counts first
629
+ updateArtistsCountPerTagSlow();
630
+ }
631
+ timer = setTimeout(function() {
632
+ // for checkbox, we defer counts because it's slow
633
+ updateArtistsCountPerTagSlow();
634
+ },0);
635
+ }
636
+
637
+ function updateArtistsCountPerTagSlow() {
638
+ let permissiveCheckbox = document.querySelector('input[name="mode"]');
639
+ let isPermissive = permissiveCheckbox.checked;
640
+ let deprecatedCheckbox = document.querySelector('input[name="deprecated"]');
641
+ let checkboxes = document.querySelectorAll('input[type="checkbox"]');
642
+ let divs = document.querySelectorAll('.image-item');
643
+ let hiddenDivs = document.querySelectorAll('.image-item.hidden');
644
+ let deprecatedDivs = document.querySelectorAll('.image-item[data-deprecated="true"]');
645
+ checkboxes.forEach(function(checkbox) {
646
+ let isTop = checkbox.parentNode.classList.contains('top_control');
647
+ if(!isTop) {
648
+ let matchingDivs;
649
+ if(isPermissive) {
650
+ matchingDivs = document.querySelectorAll('.image-item[data-tag-list*="' + checkbox.name + '"]');
651
+ } else {
652
+ // for strict mode, for each checkbox, only count artists with a tags matching all checked checkboxes
653
+ matchingDivs = document.querySelectorAll('.image-item[data-tag-list*="' + checkbox.name + '"]:not(.hidden)');
654
+ }
655
+ let filteredDivs = Array.from(matchingDivs).filter(mat => {
656
+ // only includes the artists known to SD
657
+ return !Array.from(deprecatedDivs).some(dep => dep === mat);
 
658
  });
659
+ let count = 0;
660
+ if(deprecatedCheckbox.checked) {
661
+ count = filteredDivs.length;
662
+ } else {
663
+ count = matchingDivs.length;
664
+ }
665
+ if(!count) { count = 0; }
666
+ checkbox.parentNode.querySelector('.count').textContent = ' - ' + count.toLocaleString();
667
+ checkbox.parentNode.classList.remove('no_matches');
668
+ checkbox.parentNode.querySelector('input').disabled = false;
669
+ if(!isPermissive) {
670
+ if(count == 0) {
671
+ checkbox.parentNode.classList.add('no_matches');
672
+ checkbox.parentNode.querySelector('input').disabled = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
  }
674
+ }
675
  }
676
+ });
677
+ updateCountOfAllArtistsShown(divs, hiddenDivs);
678
+ if(isPermissive) {
679
+ updateArtistsCountPerCategory();
680
+ }
681
  }
682
 
683
  function updateArtistsCountPerCategory() {
 
687
  counts[i] = 0;
688
  }
689
  imageItems.forEach(function(imageItem) {
690
+ let tagList = imageItem.dataset.tagList.split('|');
 
 
 
 
 
691
  for(i=0,il=tagCategories.length; i<il; i++) {
692
+ if(tagCategories[i].map(tag => tag.toLowerCase()).some(tag => tagList.includes(tag))) {
693
  counts[i]++;
694
  }
695
  }
 
782
 
783
  function unhideArtistsPermissive() {
784
  // permissive mode unhides images that match ANY checked tag
785
+ // the set of checkboxes is derived from the unique tags within the imageItem (Artists) tagList dataSet
786
  var imageItems = document.querySelectorAll('.image-item');
787
  var checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"]'))
788
  .filter(cb => !cb.parentNode.classList.contains("top_control"));
789
  checkboxes.push(document.querySelector('input[name="favorite"]'));
790
  var checked = checkboxes.filter(cb => cb.checked).map(cb => cb.name);
791
  imageItems.forEach(function(imageItem) {
792
+ let tagList = imageItem.dataset.tagList.split('|');
793
+ if(imageItem.classList.contains('favorite')) {
794
+ tagList.push('favorite');
795
+ }
796
+ if(checked.some(tag => tagList.includes(tag))) {
 
 
797
  imageItem.classList.remove('hidden');
798
  }
799
  });
 
802
 
803
  function unhideArtistsStrict() {
804
  // strict mode unhides images that match ALL checked tags
805
+ // the set of checkboxes is derived from the unique tags within the imageItem (Artists) tagList dataSet
806
  var imageItems = document.querySelectorAll('.image-item');
807
  var checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"]'))
808
  .filter(cb => !cb.parentNode.classList.contains("top_control"));
 
810
  var checked = checkboxes.filter(cb => cb.checked).map(cb => cb.name);
811
  if(checked.length > 0) {
812
  imageItems.forEach(function(imageItem, index) {
813
+ let tagList = imageItem.dataset.tagList.split('|');
814
+ if(imageItem.classList.contains('favorite')) {
815
+ tagList.push('favorite');
816
+ }
817
+ if(checked.every(tag => tagList.includes(tag))) {
 
 
818
  imageItem.classList.remove('hidden');
819
  }
820
  });
 
831
  function unhideAristsExact() {
832
  // exact mode isn't currently used because almost no two artists have the same set of tags
833
  // exact mode unhides images that match ALL checked tags and NO unchecked tags
834
+ // the set of checkboxes is derived from the unique tags within the imageItem (Artists) tagList dataSet
835
  var imageItems = document.querySelectorAll('.image-item');
836
  var checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"]'))
837
  .filter(cb => !cb.parentNode.classList.contains("top_control"));
 
840
  var unchecked = checkboxes.filter(cb => !cb.checked).map(cb => cb.name);
841
  if(checked.length > 0) {
842
  imageItems.forEach(function(imageItem, index) {
843
+ let tagList = imageItem.dataset.tagList.split('|');
844
+ if(imageItem.classList.contains('favorite')) {
845
+ tagList.push('favorite');
846
+ }
847
+ if(checked.every(tag => tagList.includes(tag))) {
848
+ if(unchecked.every(tag => !tagList.includes(tag))) {
849
  imageItem.classList.remove('hidden');
850
  }
851
  }
 
905
  // favorites
906
  var textareaF = document.getElementById('export_favorites_list');
907
  var favoritedArtists = false;
908
+ loadItemBasedOnAccessType('favoritedArtists').then(state => {
 
909
  var value = '';
910
+ if(state) {
911
  value += 'You have favorited these artists:\r\n';
912
+ for (let key in state) {
913
+ if (state[key] === true) {
914
  let names = key.split("|");
915
  if(!names[0]) { names[0] = '(no first name)'; }
916
  value += '•' + names[0] + ',' + names[1] + '\r\n';
917
  }
918
  }
919
+ value += '\r\n\r\nTo import these favorites later, click "copy to clipboard" and save to any file. Then paste the text from that file into this text box, and click "import". The imported text must contain the JSON string below (the curly brackets and what\'s between them). It must not contain any other more than one set of curly brackets.\r\n\r\n' + state;
920
  textareaF.value = value;
921
  } else {
922
  value += 'You haven\'t favorited any artists yet.\r\n\r\n';
923
  value += 'To import favorites that you exported earlier, paste the text into this text box, and click "import".';
924
  textareaF.value = value;
925
  }
926
+ });
927
  // edits
928
  var textareaE = document.getElementById('export_edits_list');
929
  let editedArtistsArr = Array.from(editedArtists);
 
966
  }
967
 
968
  function importFavorites() {
969
+ let el = document.getElementById('export_favorites_list');
970
+ let favorites = el.value;
971
+ let startCount = (favorites.match(/{/g) || []).length;
972
+ let endCount = (favorites.match(/}/g) || []).length;
973
+ if (startCount > 1 || endCount > 1) {
974
+ el.value = 'That text can\'t be imported because it contains multiple curly brackets {}.'
975
+ return null;
976
+ }
977
+ let start = favorites.indexOf('{');
978
+ let end = favorites.lastIndexOf('}');
979
+ if (start === -1 || end === -1) {
980
+ el.value = 'That text can\'t be imported because it contains zero curly brackets {}.'
981
+ return null;
982
+ }
983
+ let jsonString = favorites.substring(start, end + 1);
984
+ try {
985
+ let favoritesObject = JSON.parse(jsonString);
986
+ // check structure of each key-value pair in favoritesObject
987
+ for (let key in favoritesObject) {
988
+ let value = favoritesObject[key];
989
+ if (!key.includes('|') || typeof value !== 'boolean') {
990
+ el.value = 'That text can\'t be imported because the JSON string it contains doesn\'t contain a valid list of artists.'
 
 
 
 
 
 
 
 
 
 
991
  return null;
992
  }
993
+ }
994
+ if(confirm('This will overwrite any saved favorites. Are you sure?')) {
995
+ storeItemBasedOnAccessType('favoritedArtists',favoritesObject,false,false);
996
+ alert('Favorites were imported!');
997
+ loadFavoritesState();
998
+ } else {
999
+ alert('Okay, you have cancelled the import.');
1000
  return null;
1001
  }
1002
+ } catch(e) {
1003
+ el.value = 'That text can\'t be imported because it doesn\'t contain a valid JSON sting.'
1004
+ return null;
1005
  }
1006
  }
1007
 
 
1177
  }
1178
  }
1179
 
1180
+ async function loadMostUsedTags() {
1181
+ await loadItemBasedOnAccessType('mustUsedTags').then(state => {
 
1182
  let mostUsedCategory = document.querySelector('[data-category-name="important"]');
1183
  for(let tag in state) {
1184
  if (state[tag]) {
 
1191
  updateTagArrayToMatchMostUsed(true,label,tag);
1192
  }
1193
  }
1194
+ }
1195
+ });
1196
  }
1197
 
1198
  function updateTagArrayToMatchMostUsed(isAdding,label,tag) {
 
1224
  }
1225
 
1226
  function storeMostUsedState(label) {
1227
+ var name = label.querySelector('input').name;
1228
+ storeItemBasedOnAccessType('mustUsedTags',false,name,label.classList.contains('is_most_used'));
 
 
 
 
1229
  }
1230
 
1231
  function enterExitEditMostUsedMode(doExit) {
1232
+ if(storingAccessType == 'none') {
1233
+ alertNoStoringAccess(0)
1234
+ } else {
1235
  let inputs = Array.from(document.querySelectorAll('input'));
1236
  if(editMostUsedMode || doExit) {
1237
  // exit edit mode
 
1262
  document.getElementById('gutter').style.left = '';
1263
  document.getElementById('image-container').style.marginLeft = '';
1264
  }
 
 
1265
  }
1266
  }
1267
 
 
1337
  }
1338
  }
1339
 
1340
+ async function loadFavoritesState() {
1341
+ await loadItemBasedOnAccessType('favoritedArtists').then(state => {
 
1342
  let artists = document.getElementsByClassName('image-item');
1343
  for(let artist of artists) {
1344
  let artistName = artist.getElementsByClassName('firstN')[0].textContent + '|' + artist.getElementsByClassName('lastN')[0].textContent;
 
1349
  }
1350
  }
1351
  updateFavoritesCount();
1352
+ });
1353
  }
1354
 
1355
  function storeFavoriteState(artist) {
1356
+ if(storingAccessType == 'none') {
1357
+ alertNoStoringAccess(0)
1358
+ } else {
1359
  var artistName = artist.getElementsByClassName('firstN')[0].textContent + '|' + artist.getElementsByClassName('lastN')[0].textContent;
1360
  var isFavorited = artist.classList.contains('favorite');
1361
+ storeItemBasedOnAccessType('favoritedArtists',false,artistName,isFavorited);
 
 
 
 
1362
  }
1363
  }
1364
 
 
1471
  // skip hide
1472
  } else {
1473
  let count = parseInt(checkbox.parentNode.querySelector('.count').textContent.replace(/,/g, '').trim().substring(2),10);
1474
+ if(count <= lowCountThreshold) {
1475
  checkbox.checked = false;
1476
  checkbox.parentNode.classList.add('hidden');
1477
  }
 
1479
  } else {
1480
  checkbox.parentNode.classList.remove('hidden');
1481
  }
 
1482
  });
1483
+ showHideCategories();
1484
  }
1485
 
1486
  function loadLargerImages(imageItem) {
 
1618
  }
1619
 
1620
  function editTagsClicked(clickedImageItem) {
1621
+ if(storingAccessType == 'none') {
1622
+ alertNoStoringAccess(0);
1623
+ } else {
1624
  let indicatorEl = clickedImageItem.querySelector('.art_edit span');
1625
  if(indicatorEl.textContent == '✍️') {
1626
  let artistWasInEditMode = editTagsFindArtistInEditMode(clickedImageItem);
 
1631
  } else {
1632
  editTagsFindArtistInEditMode();
1633
  }
 
 
1634
  }
1635
  }
1636
 
 
1822
  }
1823
 
1824
  function saveTagsForArtist(tagArea) {
1825
+ // get new tags
1826
+ let tagLabels = tagArea.querySelectorAll('label');
1827
+ let newTagsArr = [];
1828
+ let artistKnown = true;
1829
+ tagLabels.forEach(function(label) {
1830
+ let input = label.querySelector('input');
1831
+ if(input.value == 'known') {
1832
+ artistKnown = input.checked;
1833
+ } else {
1834
+ if(input.checked) {
1835
+ newTagsArr.push(input.value);
 
 
1836
  }
1837
+ }
1838
+ });
1839
+ // find match in artistsData
1840
+ let firstN = tagArea.closest('.image-item').querySelector('.firstN').textContent;
1841
+ let lastN = tagArea.closest('.image-item').querySelector('.lastN').textContent;
1842
+ let edit = [];
1843
+ for (let i=0, il=artistsData.length; i<il; i++) {
1844
+ let artist = artistsData[i];
1845
+ if(artist[0] == lastN && artist[1] == firstN) {
1846
+ // artists can have a tag in the format of "added-YYYY-MM-DD"
1847
+ // this was stripped earlier, so we need to add it back in
1848
+ let oldTagsArr = artist[2].split('|');
1849
+ for (let j=oldTagsArr.length-1; j>=0; j--) {
1850
+ // loop backwards because it should be at the end
1851
+ if(oldTagsArr[j].match(/added-(\d|-)*/)) {
1852
+ newTagsArr.push(oldTagsArr[j]);
 
 
 
 
 
 
 
 
1853
  }
 
 
1854
  }
1855
+ let newTagsStr = newTagsArr.join('|');
1856
+ artist[2] = newTagsStr;
1857
+ // in db, true = hide unknown, but here true = known
1858
+ if(artistKnown) {
1859
+ artist[3] = false;
1860
+ } else {
1861
+ artist[3] = true;
1862
  }
1863
+ edit = artist;
1864
+ break;
1865
  }
 
 
 
1866
  }
1867
+ // replace old edits with new edits
1868
+ for (let i=0, il=editedArtists.length; i<il; i++) {
1869
+ let oldEdit = editedArtists[i];
1870
+ if(edit[0] == oldEdit[0] && edit[1] == oldEdit[1]) {
1871
+ editedArtists.delete(oldEdit);
1872
+ }
1873
+ }
1874
+ editedArtists.add(edit)
1875
+ // save edited artists locally
1876
+ storeItemBasedOnAccessType('editedArtists',Array.from(editedArtists),false,false);
1877
  }
1878
 
1879
  function deleteAllEdits() {
1880
+ if(storingAccessType != 'none') {
1881
  if(confirm('This will delete all of your edits. Are you sure?')) {
1882
+ deleteItemBasedOnAccessType('editedArtists');
1883
  alert('official database restored! this page will reload...');
1884
  location.reload();
1885
  } else {
 
1887
  }
1888
  }
1889
  }
1890
+
1891
+ function addAllListeners() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1892
  // add checkbox event listeners
1893
  var checkboxes = document.querySelectorAll('input[type="checkbox"]');
1894
  checkboxes.forEach(function(checkbox) {
 
2108
  closeFooter.addEventListener('click', function(e) {
2109
  document.getElementById('layout').classList.add('footerHidden');
2110
  });
2111
+ }