[Wallhaven] Subscription Tools

Adds a useful "tool box" to the top of the side bar on the subscriptions page (Filter Subs by name, sort them by amount, toggle on/off visibility of subs with 0 new wallpapers)

  1. // ==UserScript==
  2. // @name [Wallhaven] Subscription Tools
  3. // @namespace NooScripts
  4. // @author NooScripts
  5. // @version 1.6
  6. // @description Adds a useful "tool box" to the top of the side bar on the subscriptions page (Filter Subs by name, sort them by amount, toggle on/off visibility of subs with 0 new wallpapers)
  7. // @license MIT
  8. // @match https://wallhaven.cc/subscription*
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. window.addEventListener('load', () => {
  13. 'use strict';
  14.  
  15. // Create container for search tools
  16. const container = document.createElement('div');
  17. container.id = 'custom-tag-filter';
  18. container.style.display = 'flex';
  19. container.style.flexDirection = 'column';
  20. container.style.alignItems = 'center';
  21. container.style.gap = '8px';
  22. container.style.padding = '10px';
  23. container.style.background = '#111';
  24. container.style.zIndex = '9999';
  25.  
  26. // Search input
  27. const input = document.createElement('input');
  28. input.type = 'text';
  29. input.placeholder = 'Filter tags...';
  30. input.style.padding = '6px';
  31. input.style.width = '100%';
  32. input.style.background = '#222';
  33. input.style.color = '#fff';
  34. input.style.border = '1px solid #555';
  35. input.style.borderRadius = '4px';
  36.  
  37. // Create a container for buttons
  38. const buttonsContainer = document.createElement('div');
  39. buttonsContainer.style.display = 'flex';
  40. buttonsContainer.style.gap = '8px';
  41. buttonsContainer.style.width = '100%';
  42. buttonsContainer.style.margin = 'auto';
  43.  
  44. // Reset button
  45. const resetBtn = document.createElement('button');
  46. resetBtn.textContent = 'Reset';
  47. resetBtn.style.padding = '6px 10px';
  48. resetBtn.style.background = '#333';
  49. resetBtn.style.color = '#fff';
  50. resetBtn.style.border = '1px solid #555';
  51. resetBtn.style.borderRadius = '4px';
  52. resetBtn.style.cursor = 'pointer';
  53. resetBtn.style.flex = '1';
  54.  
  55. // Toggle style button
  56. const toggleBtn = document.createElement('button');
  57. toggleBtn.style.padding = '6px 10px';
  58. toggleBtn.style.background = '#333';
  59. toggleBtn.style.color = '#fff';
  60. toggleBtn.style.border = '1px solid #555';
  61. toggleBtn.style.borderRadius = '4px';
  62. toggleBtn.style.cursor = 'pointer';
  63. toggleBtn.style.flex = '1';
  64.  
  65. // Append buttons to the buttons container
  66. buttonsContainer.appendChild(resetBtn);
  67. buttonsContainer.appendChild(toggleBtn);
  68.  
  69. // Append input and buttons container to the main container
  70. container.appendChild(input);
  71. container.appendChild(buttonsContainer);
  72.  
  73. // Insert above .sidebar-background
  74. const sidebarBg = document.querySelector('.sidebar-background');
  75. if (sidebarBg && sidebarBg.parentNode) {
  76. sidebarBg.parentNode.insertBefore(container, sidebarBg);
  77. } else {
  78. console.warn('⚠️ Could not find sidebar');
  79. }
  80.  
  81. // Initialize styleEnabled from localStorage or default to true
  82. let styleEnabled = localStorage.getItem('styleEnabled') !== null
  83. ? JSON.parse(localStorage.getItem('styleEnabled'))
  84. : true;
  85.  
  86. const styleSheet = document.createElement('style');
  87. document.head.appendChild(styleSheet);
  88.  
  89. // Set initial button text and stylesheet based on styleEnabled
  90. toggleBtn.textContent = styleEnabled ? 'Show Empty' : 'Hide Empty';
  91. styleSheet.textContent = styleEnabled ? `
  92. .blocklist.subscription-list li:is([class=""]) {
  93. display: none !important;
  94. }
  95. ` : '';
  96.  
  97. // Load saved input text from localStorage
  98. input.value = localStorage.getItem('filterText') || '';
  99. if (input.value) performFiltering(); // Apply filter if there's saved text
  100.  
  101. // Add or remove the CSS to hide empty <li> elements
  102. function toggleStyle() {
  103. styleEnabled = !styleEnabled;
  104. localStorage.setItem('styleEnabled', JSON.stringify(styleEnabled)); // Save to localStorage
  105.  
  106. if (styleEnabled) {
  107. styleSheet.textContent = `
  108. .blocklist.subscription-list li:is([class=""]) {
  109. display: none !important;
  110. }
  111. `;
  112. toggleBtn.textContent = 'Show Empty';
  113. } else {
  114. styleSheet.textContent = '';
  115. toggleBtn.textContent = 'Hide Empty';
  116. }
  117. }
  118.  
  119. // Function to get all li elements in blocklist
  120. function getTagItems() {
  121. return Array.from(document.querySelectorAll('ul.blocklist.subscription-list li'));
  122. }
  123.  
  124. // Perform filtering based on input
  125. function performFiltering() {
  126. const term = input.value.toLowerCase().trim();
  127. localStorage.setItem('filterText', input.value); // Save input text to localStorage
  128. const items = getTagItems();
  129. items.forEach(li => {
  130. const tag = li.querySelector('.tagname');
  131. if (!tag) return;
  132. const tagText = tag.textContent.toLowerCase();
  133. li.style.display = tagText.includes(term) ? '' : 'none';
  134. });
  135. }
  136.  
  137. // Reset the filter
  138. function resetFilter() {
  139. const items = getTagItems();
  140. items.forEach(li => li.style.display = '');
  141. input.value = '';
  142. localStorage.setItem('filterText', ''); // Clear saved input text
  143. }
  144.  
  145. // Add event listeners
  146. input.addEventListener('input', performFiltering);
  147. resetBtn.addEventListener('click', resetFilter);
  148. toggleBtn.addEventListener('click', toggleStyle);
  149. });
  150.  
  151.  
  152.  
  153. //Script 2: Sort Subscriptions By Number
  154.  
  155. function sortListItems(descending) {
  156. const container = document.querySelector('[data-storage-id="tagsubscriptions"]');
  157. if (!container) {
  158. console.error('Tag subscriptions container not found.');
  159. return;
  160. }
  161.  
  162. const listItems = Array.from(container.querySelectorAll('.blocklist[class*="subscription-list"] [class*="has-"]'));
  163.  
  164. listItems.sort((a, b) => {
  165. const valueA = parseInt(a.querySelector('small').textContent);
  166. const valueB = parseInt(b.querySelector('small').textContent);
  167.  
  168. return descending ? valueB - valueA : valueA - valueB;
  169. });
  170.  
  171. listItems.forEach((item) => {
  172. const parent = item.parentNode;
  173. parent.appendChild(item);
  174. });
  175. }
  176.  
  177. function createDropdown(labelText, options, onChange) {
  178. const dropdown = document.createElement('select');
  179.  
  180. options.forEach(option => {
  181. const optionElement = document.createElement('option');
  182. optionElement.value = option.value;
  183. optionElement.text = option.text;
  184. dropdown.appendChild(optionElement);
  185. });
  186.  
  187. dropdown.addEventListener('change', onChange);
  188.  
  189. const label = document.createElement('label');
  190. label.style.marginRight = '10px';
  191. label.appendChild(document.createTextNode(labelText + ': '));
  192. label.appendChild(dropdown);
  193.  
  194. return label;
  195. }
  196.  
  197. function handleSortDropdownChange() {
  198. if (this.value === 'default') {
  199. localStorage.removeItem('subscriptionSortOrder');
  200. location.reload();
  201. return;
  202. }
  203.  
  204. const descending = this.value === 'desc';
  205. sortListItems(descending);
  206. localStorage.setItem('subscriptionSortOrder', descending ? 'desc' : 'asc');
  207. }
  208.  
  209. function handleDisplayDropdownChange() {
  210. const existingStyle = document.getElementById('display-filter-style');
  211. if (existingStyle) {
  212. existingStyle.remove();
  213. }
  214.  
  215. let css = '';
  216. switch (this.value) {
  217. case 'sfw':
  218. css = `
  219. [class="has-new "]:has([class="tagname sketchy"]) {display: none!important;}
  220. [class="has-new "]:has([class="tagname nsfw"]) {display: none!important;}
  221. `;
  222. break;
  223. case 'sketch':
  224. css = `
  225. [class="has-new "]:has([class="tagname sfw"]) {display: none!important;}
  226. [class="has-new "]:has([class="tagname nsfw"]) {display: none!important;}
  227. `;
  228. break;
  229. case 'nsfw':
  230. css = `
  231. [class="has-new "]:has([class="tagname sfw"]) {display: none!important;}
  232. [class="has-new "]:has([class="tagname sketchy"]) {display: none!important;}
  233. `;
  234. break;
  235. }
  236.  
  237. if (css) {
  238. const style = document.createElement('style');
  239. style.id = 'display-filter-style';
  240. style.textContent = css;
  241. document.head.appendChild(style);
  242. }
  243.  
  244. localStorage.setItem('subscriptionDisplayFilter', this.value);
  245. }
  246.  
  247. function initializeDropdown() {
  248. const sortDropdownOptions = [
  249. { value: 'default', text: 'Default' },
  250. { value: 'desc', text: 'Most to Least' },
  251. { value: 'asc', text: 'Least To Most' }
  252. ];
  253.  
  254. const displayDropdownOptions = [
  255. { value: 'all', text: 'All' },
  256. { value: 'sfw', text: 'Only SFW' },
  257. { value: 'sketch', text: 'Only Sketch' },
  258. { value: 'nsfw', text: 'Only NSFW' }
  259. ];
  260.  
  261. const sortDropdown = createDropdown('Sort Order', sortDropdownOptions, handleSortDropdownChange);
  262. const displayDropdown = createDropdown('Display', displayDropdownOptions, handleDisplayDropdownChange);
  263.  
  264. const container = document.createElement('div');
  265. container.id = 'sort-dropdown-container';
  266. container.style.padding = '0px 0px 8px 0px';
  267. container.style.marginBottom = '10px';
  268. container.style.backgroundColor = '#111111';
  269. container.style.borderBottom = '1px solid #4a4a4a';
  270. container.style.fontSize = '12px';
  271. container.style.display = 'flex';
  272. container.style.alignItems = 'center';
  273. container.style.gap = '10px';
  274.  
  275. container.appendChild(sortDropdown);
  276. container.appendChild(displayDropdown);
  277.  
  278. const sidebarBackground = document.querySelector('.sidebar-background');
  279. if (sidebarBackground) {
  280. sidebarBackground.insertBefore(container, sidebarBackground.firstChild);
  281. } else {
  282. console.error('.sidebar-background element not found.');
  283. }
  284.  
  285. const sortOrder = localStorage.getItem('subscriptionSortOrder');
  286. if (sortOrder === 'desc') {
  287. sortDropdown.querySelector('select').value = 'desc';
  288. sortListItems(true);
  289. } else if (sortOrder === 'asc') {
  290. sortDropdown.querySelector('select').value = 'asc';
  291. sortListItems(false);
  292. }
  293.  
  294. const displayFilter = localStorage.getItem('subscriptionDisplayFilter');
  295. if (displayFilter) {
  296. displayDropdown.querySelector('select').value = displayFilter;
  297. handleDisplayDropdownChange.call(displayDropdown.querySelector('select'));
  298. }
  299. }
  300.  
  301. initializeDropdown();