s0urce.io Target List Sorter

Adds sorting functionality to the target list in s0urce.io

  1. // ==UserScript==
  2. // @name s0urce.io Target List Sorter
  3. // @namespace s0urce.io Target List Sorter
  4. // @version 0.1
  5. // @description Adds sorting functionality to the target list in s0urce.io
  6. // @author NoT BoT
  7. // @match https://s0urce.io/*
  8. // @icon https://s0urce.io/icons/s0urce.svg
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. let currentSort = {
  17. field: null,
  18. direction: 'asc'
  19. };
  20.  
  21. function addSortingControls() {
  22. const existingControls = document.querySelector('.sort-controls');
  23. let style = document.getElementById("STYYYYYLES");
  24. if (!style) {
  25. style = document.createElement('style');
  26. style.id = "STYYYYYLES";
  27. style.innerText = `
  28. #list > .wrapper:not(.npc-premium) {
  29. display: flex;
  30. justify-content: space-between;
  31. align-items: center;
  32. padding-right: 10px
  33. }
  34.  
  35. #list > .wrapper:not(.npc-premium) > div{
  36. order: 4;
  37. }
  38. #list > .wrapper:not(.npc-premium) > .icon.flag{
  39. order: 5;
  40. }
  41. #list > .wrapper:not(.npc-premium) >.badge{
  42. order: 2;
  43. }
  44. #list > .wrapper:not(.npc-premium) > img.icon[src*="premium.svg"] {
  45. order: 3;
  46. margin-left: auto;
  47. }`;
  48. document.head.appendChild(style);
  49. }
  50.  
  51. if (existingControls) return;
  52.  
  53. const sortingControls = `
  54. <div class="sort-controls" style="display: flex; gap: 5px; margin-bottom: 5px; flex-wrap: wrap;">
  55. <div class="sort svelte-1cv9i3z" data-sort="username" style="padding: 4px 8px; border-radius: 2px; background-color: var(--color-darkgrey); cursor: pointer;">Name</div>
  56. <div class="sort svelte-1cv9i3z" data-sort="username-length" style="padding: 4px 8px; border-radius: 2px; background-color: var(--color-darkgrey); cursor: pointer;">Length</div>
  57. <div class="sort svelte-1cv9i3z" data-sort="level" style="padding: 4px 8px; border-radius: 2px; background-color: var(--color-darkgrey); cursor: pointer;">Lvl</div>
  58. <div class="sort svelte-1cv9i3z" data-sort="country" style="padding: 4px 8px; border-radius: 2px; background-color: var(--color-darkgrey); cursor: pointer;">Flag</div>
  59. <div class="sort-reset svelte-1cv9i3z" style="padding: 4px 8px; border-radius: 2px; background-color: var(--color-darkgrey); cursor: pointer;">Reset</div>
  60. </div>
  61. `;
  62.  
  63. const targetWindow = document.querySelector('.window:has([src="icons/targetList.svg"])');
  64. if (!targetWindow) return;
  65.  
  66. const listElement = targetWindow.querySelector('#list');
  67. if (!listElement) return;
  68.  
  69. listElement.insertAdjacentHTML('beforebegin', sortingControls);
  70.  
  71. targetWindow.querySelectorAll('.sort').forEach(button => {
  72. button.addEventListener('click', function () {
  73. const sortBy = this.getAttribute('data-sort');
  74.  
  75. if (currentSort.field === sortBy) {
  76. currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
  77. } else {
  78. currentSort.field = sortBy;
  79. currentSort.direction = 'asc';
  80. }
  81.  
  82. sortTargetList(sortBy, currentSort.direction);
  83.  
  84. localStorage.setItem('lastTargetSort', sortBy);
  85. localStorage.setItem('lastSortDirection', currentSort.direction);
  86.  
  87. targetWindow.querySelectorAll('.sort, .sort-reset').forEach(btn => {
  88. btn.style.backgroundColor = 'var(--color-darkgrey)';
  89. btn.textContent = btn.textContent.replace(' ▲', '').replace(' ▼', '');
  90. });
  91. this.style.backgroundColor = 'var(--color-blue)';
  92.  
  93. const directionIndicator = currentSort.direction === 'asc' ? ' ▲' : ' ▼';
  94. this.textContent = this.textContent.replace(' ▲', '').replace(' ▼', '') + directionIndicator;
  95. });
  96. });
  97.  
  98. const resetButton = targetWindow.querySelector('.sort-reset');
  99. if (resetButton) {
  100. resetButton.addEventListener('click', function () {
  101. currentSort.field = null;
  102. currentSort.direction = 'asc';
  103.  
  104. localStorage.removeItem('lastTargetSort');
  105. localStorage.removeItem('lastSortDirection');
  106.  
  107. resetTargetList(targetWindow);
  108.  
  109. targetWindow.querySelectorAll('.sort, .sort-reset').forEach(btn => {
  110. btn.style.backgroundColor = 'var(--color-darkgrey)';
  111. btn.textContent = btn.textContent.replace(' ▲', '').replace(' ▼', '');
  112. });
  113.  
  114. this.style.backgroundColor = 'var(--color-blue)';
  115. });
  116. }
  117.  
  118. const lastSort = localStorage.getItem('lastTargetSort');
  119. const lastDirection = localStorage.getItem('lastSortDirection') || 'asc';
  120. if (lastSort) {
  121. currentSort.field = lastSort;
  122. currentSort.direction = lastDirection;
  123. sortTargetList(lastSort, lastDirection);
  124.  
  125. const activeButton = targetWindow.querySelector(`.sort[data-sort="${lastSort}"]`);
  126. if (activeButton) {
  127. activeButton.style.backgroundColor = 'var(--color-blue)';
  128. const directionIndicator = lastDirection === 'asc' ? ' ▲' : ' ▼';
  129. activeButton.textContent = activeButton.textContent.replace(' ▲', '').replace(' ▼', '') + directionIndicator;
  130. }
  131. }
  132.  
  133. observeListChanges();
  134. }
  135.  
  136. function resetTargetList(targetWindow) {
  137. const listElement = targetWindow.querySelector('#list');
  138. if (!listElement) return;
  139.  
  140. const items = Array.from(listElement.querySelectorAll('.wrapper'));
  141. items.forEach(item => {
  142. item.style.order = '';
  143. });
  144.  
  145. listElement.style.display = '';
  146. listElement.style.flexDirection = '';
  147. listElement.style.height = '';
  148. listElement.style.maxHeight = '';
  149. listElement.style.overflow = 'auto';
  150. }
  151.  
  152. function sortTargetList(sortBy, direction = 'asc') {
  153. const targetWindow = document.querySelector('.window:has([src="icons/targetList.svg"])');
  154. if (!targetWindow) return;
  155.  
  156. const listElement = targetWindow.querySelector('#list');
  157. if (!listElement) return;
  158.  
  159. const scrollTop = listElement.scrollTop;
  160.  
  161. let all = listElement.querySelectorAll('.wrapper');
  162. for (let i = 0; i < all.length; i++) {
  163. all[i].style.flexShrink = '0';
  164. all[i].style.paddingRight = '10px';
  165. }
  166.  
  167. const items = Array.from(listElement.querySelectorAll('.wrapper:not(.npc, .npc-premium, [style="width: 100%; height: 1px; background-color: var(--color-lightgrey); margin: 15px 0px;"]'));
  168.  
  169. items.forEach(item => {
  170. item.style.order = '';
  171. });
  172.  
  173. const sortedItems = items.map((item, index) => ({
  174. item,
  175. index
  176. }));
  177.  
  178. const measureElement = document.createElement('span');
  179. measureElement.style.visibility = 'hidden';
  180. measureElement.style.position = 'absolute';
  181. measureElement.style.whiteSpace = 'nowrap';
  182. document.body.appendChild(measureElement);
  183.  
  184. function getTextWidth(text, element) {
  185. const usernameElement = element.querySelector('.username');
  186. if (usernameElement) {
  187. const styles = window.getComputedStyle(usernameElement);
  188. measureElement.style.font = styles.font;
  189. measureElement.style.fontSize = styles.fontSize;
  190. measureElement.style.fontFamily = styles.fontFamily;
  191. measureElement.style.fontWeight = styles.fontWeight;
  192. }
  193.  
  194. measureElement.textContent = text;
  195. return measureElement.getBoundingClientRect().width;
  196. }
  197.  
  198. sortedItems.sort((a, b) => {
  199. let result = 0;
  200.  
  201. if (sortBy === 'username') {
  202. const usernameA = a.item.querySelector('.username')?.textContent.trim() || '';
  203. const usernameB = b.item.querySelector('.username')?.textContent.trim() || '';
  204. result = usernameA.localeCompare(usernameB);
  205. }
  206. else if (sortBy === 'username-length') {
  207. const usernameA = a.item.querySelector('.username')?.textContent.trim() || '';
  208. const usernameB = b.item.querySelector('.username')?.textContent.trim() || '';
  209.  
  210. const widthA = getTextWidth(usernameA, a.item);
  211. const widthB = getTextWidth(usernameB, b.item);
  212.  
  213. result = widthA - widthB;
  214. }
  215. else if (sortBy === 'level') {
  216. const levelA = parseInt(a.item.querySelector('div')?.textContent.trim()) || 0;
  217. const levelB = parseInt(b.item.querySelector('div')?.textContent.trim()) || 0;
  218. result = levelB - levelA;
  219. }
  220. else if (sortBy === 'country') {
  221. const flagA = a.item.querySelector('.flag');
  222. const flagB = b.item.querySelector('.flag');
  223. const countryA = flagA ? flagA.getAttribute('alt')?.replace(' Flag', '') || 'ZZ' : 'ZZ';
  224. const countryB = flagB ? flagB.getAttribute('alt')?.replace(' Flag', '') || 'ZZ' : 'ZZ';
  225. result = countryA.localeCompare(countryB);
  226. }
  227.  
  228. if (sortBy === 'level') {
  229. return direction === 'desc' ? -result : result;
  230. } else {
  231. return direction === 'desc' ? -result : result;
  232. }
  233. });
  234.  
  235. document.body.removeChild(measureElement);
  236.  
  237. sortedItems.forEach((item, newIndex) => {
  238. item.item.style.order = newIndex;
  239. });
  240.  
  241. listElement.style.display = 'flex';
  242. listElement.style.flexDirection = 'column';
  243.  
  244. const parentContainer = listElement.parentElement;
  245. if (parentContainer) {
  246. const parentStyle = window.getComputedStyle(parentContainer);
  247. const parentHeight = parentStyle.height;
  248.  
  249. listElement.style.height = parentHeight;
  250. listElement.style.maxHeight = 'none';
  251. listElement.style.overflow = 'auto';
  252.  
  253. items.forEach(item => {
  254. item.style.flexShrink = '0';
  255. });
  256. }
  257.  
  258. listElement.scrollTop = scrollTop;
  259. }
  260.  
  261. function observeListChanges() {
  262. const targetWindow = document.querySelector('.window:has([src="icons/targetList.svg"])');
  263. if (!targetWindow) return;
  264.  
  265. const listElement = targetWindow.querySelector('#list');
  266. if (!listElement) return;
  267.  
  268. const observer = new MutationObserver(() => {
  269. if (currentSort.field) {
  270. sortTargetList(currentSort.field, currentSort.direction);
  271. }
  272. });
  273.  
  274. observer.observe(listElement, { childList: true, subtree: true });
  275. }
  276.  
  277. function checkForTargetList() {
  278. const targetWindow = document.querySelector('.window:has([src="icons/targetList.svg"])');
  279. if (targetWindow) {
  280. addSortingControls();
  281. }
  282. }
  283.  
  284. setInterval(checkForTargetList, 1000);
  285.  
  286. document.addEventListener('click', function (e) {
  287. if (e.target.closest('[src="icons/targetList.svg"]')) {
  288. setTimeout(addSortingControls, 100);
  289. }
  290. });
  291. })();