JanitorAI Enhanced UI

Adds useful UI controls for JanitorAI

目前為 2025-04-09 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name JanitorAI Enhanced UI
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.31
  5. // @description Adds useful UI controls for JanitorAI
  6. // @author Fefnik
  7. // @match https://janitorai.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // Settings
  15. let MIN_TOKENS = localStorage.getItem('janitorAITokenFilter') ? parseInt(localStorage.getItem('janitorAITokenFilter')) : 500;
  16. let isSliderVisible = localStorage.getItem('janitorAISliderVisible') === 'true';
  17. let isSidebarHidden = localStorage.getItem('janitorAISidebarHidden') === 'true';
  18. let isAutoScrollEnabled = localStorage.getItem('janitorAIAutoScroll') !== 'false';
  19.  
  20. // Elements
  21. let sliderElement = null;
  22. let sliderContainer = null;
  23. let controlPanel = null;
  24.  
  25. function isAllowedPage() {
  26. return ['/', '/search', '/my_characters', '/profiles/'].some(path =>
  27. window.location.pathname === path ||
  28. window.location.pathname.startsWith('/search') ||
  29. window.location.pathname.startsWith('/profiles/')
  30. );
  31. }
  32.  
  33. function parseTokens(tokenText) {
  34. try {
  35. let cleanText = tokenText.replace(/<!--[\s\S]*?-->/g, '').replace('tokens', '').trim();
  36. if (cleanText.includes('k')) {
  37. return parseFloat(cleanText.replace('k', '')) * 1000;
  38. }
  39. return parseInt(cleanText, 10) || 0;
  40. } catch (error) {
  41. return 0;
  42. }
  43. }
  44.  
  45. function filterCards() {
  46. const cards = document.querySelectorAll('.chakra-stack.css-1s5evre, .css-1s5evre');
  47. cards.forEach(card => {
  48. const tokenElement = card.querySelector('.chakra-text.css-jccmq6, .css-jccmq6');
  49. if (!tokenElement) return;
  50.  
  51. const tokenCount = parseTokens(tokenElement.textContent);
  52. const parentContainer = card.closest('.css-1sxhvxh, .css-1dbw1r8');
  53. if (parentContainer) {
  54. parentContainer.style.display = tokenCount < MIN_TOKENS ? 'none' : '';
  55. }
  56. });
  57. }
  58.  
  59. function setupPaginationScroll() {
  60. const paginationButtons = document.querySelectorAll('.css-kzd6o0');
  61. paginationButtons.forEach(button => {
  62. button.removeEventListener('click', handlePaginationClick);
  63. button.addEventListener('click', handlePaginationClick);
  64. });
  65. }
  66.  
  67. function handlePaginationClick() {
  68. if (isAutoScrollEnabled) {
  69. setTimeout(() => {
  70. window.scrollTo({
  71. top: 0,
  72. behavior: 'smooth'
  73. });
  74. }, 300);
  75. }
  76. }
  77.  
  78. function toggleSidebar() {
  79. const sidebar = document.querySelector('.css-h988mi');
  80. if (sidebar) {
  81. isSidebarHidden = !isSidebarHidden;
  82. sidebar.style.display = isSidebarHidden ? 'none' : '';
  83. localStorage.setItem('janitorAISidebarHidden', isSidebarHidden);
  84. updateControlIcons();
  85. }
  86. }
  87.  
  88. function toggleAutoScroll() {
  89. isAutoScrollEnabled = !isAutoScrollEnabled;
  90. localStorage.setItem('janitorAIAutoScroll', isAutoScrollEnabled);
  91. updateControlIcons();
  92. }
  93.  
  94. function updateControlIcons() {
  95. const eyeButton = document.getElementById('sidebar-toggle-button');
  96. const scrollButton = document.getElementById('auto-scroll-button');
  97. const settingsButton = document.getElementById('token-filter-toggle');
  98.  
  99. if (eyeButton) {
  100. eyeButton.textContent = isSidebarHidden ? '👁️' : '👁️‍🗨️';
  101. eyeButton.title = isSidebarHidden ? 'Show sidebar' : 'Hide sidebar';
  102. eyeButton.style.backgroundColor = isSidebarHidden ? 'rgba(74, 74, 74, 0.3)' : 'rgba(74, 74, 74, 0.7)';
  103. }
  104.  
  105. if (scrollButton) {
  106. scrollButton.textContent = '⏫';
  107. scrollButton.title = isAutoScrollEnabled ? 'Auto-scroll: ON' : 'Auto-scroll: OFF';
  108. scrollButton.style.backgroundColor = isAutoScrollEnabled ? 'rgba(74, 74, 74, 0.7)' : 'rgba(74, 74, 74, 0.3)';
  109. }
  110.  
  111. if (settingsButton) {
  112. settingsButton.style.backgroundColor = isSliderVisible ? 'rgba(74, 74, 74, 0.7)' : 'rgba(74, 74, 74, 0.3)';
  113. }
  114. }
  115.  
  116. function createControlPanel() {
  117. if (controlPanel) return;
  118.  
  119. // Main control panel (vertical)
  120. controlPanel = document.createElement('div');
  121. controlPanel.id = 'janitor-control-panel';
  122. controlPanel.style.position = 'fixed';
  123. controlPanel.style.top = '75px';
  124. controlPanel.style.left = '10px';
  125. controlPanel.style.zIndex = '100000';
  126. controlPanel.style.display = 'flex';
  127. controlPanel.style.flexDirection = 'column';
  128. controlPanel.style.gap = '5px';
  129. controlPanel.style.alignItems = 'flex-start';
  130.  
  131. // Settings button (gear icon)
  132. const settingsButton = document.createElement('button');
  133. settingsButton.id = 'token-filter-toggle';
  134. settingsButton.textContent = '⚙️';
  135. settingsButton.title = 'Token filter settings';
  136. settingsButton.style.width = '30px';
  137. settingsButton.style.height = '30px';
  138. settingsButton.style.padding = '0';
  139. settingsButton.style.backgroundColor = isSliderVisible ? 'rgba(74, 74, 74, 0.7)' : 'rgba(74, 74, 74, 0.3)';
  140. settingsButton.style.color = '#fff';
  141. settingsButton.style.border = 'none';
  142. settingsButton.style.borderRadius = '5px';
  143. settingsButton.style.cursor = 'pointer';
  144. settingsButton.style.display = 'flex';
  145. settingsButton.style.alignItems = 'center';
  146. settingsButton.style.justifyContent = 'center';
  147. settingsButton.style.fontSize = '16px';
  148. settingsButton.style.transition = 'background-color 0.2s';
  149.  
  150. settingsButton.addEventListener('click', () => {
  151. isSliderVisible = !isSliderVisible;
  152. if (sliderContainer) {
  153. sliderContainer.style.display = isSliderVisible ? 'flex' : 'none';
  154. }
  155. localStorage.setItem('janitorAISliderVisible', isSliderVisible);
  156. updateControlIcons();
  157. });
  158.  
  159. // Eye button (sidebar toggle)
  160. const eyeButton = document.createElement('button');
  161. eyeButton.id = 'sidebar-toggle-button';
  162. eyeButton.style.width = '30px';
  163. eyeButton.style.height = '30px';
  164. eyeButton.style.padding = '0';
  165. eyeButton.style.backgroundColor = isSidebarHidden ? 'rgba(74, 74, 74, 0.3)' : 'rgba(74, 74, 74, 0.7)';
  166. eyeButton.style.color = '#fff';
  167. eyeButton.style.border = 'none';
  168. eyeButton.style.borderRadius = '5px';
  169. eyeButton.style.cursor = 'pointer';
  170. eyeButton.style.display = 'flex';
  171. eyeButton.style.alignItems = 'center';
  172. eyeButton.style.justifyContent = 'center';
  173. eyeButton.style.fontSize = '16px';
  174. eyeButton.style.transition = 'background-color 0.2s';
  175.  
  176. eyeButton.addEventListener('click', toggleSidebar);
  177.  
  178. // Auto-scroll button
  179. const scrollButton = document.createElement('button');
  180. scrollButton.id = 'auto-scroll-button';
  181. scrollButton.style.width = '30px';
  182. scrollButton.style.height = '30px';
  183. scrollButton.style.padding = '0';
  184. scrollButton.style.backgroundColor = isAutoScrollEnabled ? 'rgba(74, 74, 74, 0.7)' : 'rgba(74, 74, 74, 0.3)';
  185. scrollButton.style.color = '#fff';
  186. scrollButton.style.border = 'none';
  187. scrollButton.style.borderRadius = '5px';
  188. scrollButton.style.cursor = 'pointer';
  189. scrollButton.style.display = 'flex';
  190. scrollButton.style.alignItems = 'center';
  191. scrollButton.style.justifyContent = 'center';
  192. scrollButton.style.fontSize = '16px';
  193. scrollButton.style.transition = 'background-color 0.2s';
  194.  
  195. scrollButton.addEventListener('click', toggleAutoScroll);
  196.  
  197. controlPanel.appendChild(settingsButton);
  198. controlPanel.appendChild(eyeButton);
  199. controlPanel.appendChild(scrollButton);
  200. document.body.appendChild(controlPanel);
  201. updateControlIcons();
  202. }
  203.  
  204. function createOrUpdateSlider() {
  205. if (!sliderElement) {
  206. // Slider container (horizontal, appears to the right of control panel)
  207. sliderContainer = document.createElement('div');
  208. sliderContainer.id = 'token-filter-container';
  209. sliderContainer.style.position = 'fixed';
  210. sliderContainer.style.top = '75px';
  211. sliderContainer.style.left = '50px';
  212. sliderContainer.style.zIndex = '99999';
  213. sliderContainer.style.display = 'none';
  214. sliderContainer.style.flexDirection = 'row';
  215. sliderContainer.style.alignItems = 'center';
  216. sliderContainer.style.gap = '10px';
  217. sliderContainer.style.padding = '5px';
  218. sliderContainer.style.backgroundColor = 'rgba(74, 74, 74, 0.7)';
  219. sliderContainer.style.borderRadius = '5px';
  220.  
  221. // Slider element
  222. sliderElement = document.createElement('input');
  223. sliderElement.type = 'range';
  224. sliderElement.id = 'token-filter-slider';
  225. sliderElement.min = '0';
  226. sliderElement.max = '6000';
  227. sliderElement.step = '100';
  228. sliderElement.value = MIN_TOKENS;
  229. sliderElement.style.width = '150px';
  230. sliderElement.style.height = '10px';
  231. sliderElement.style.backgroundColor = '#4a4a4a';
  232. sliderElement.style.cursor = 'pointer';
  233. sliderElement.style.appearance = 'none';
  234. sliderElement.style.outline = 'none';
  235. sliderElement.style.borderRadius = '5px';
  236.  
  237. // Slider thumb style
  238. const style = document.createElement('style');
  239. style.textContent = `
  240. #token-filter-slider::-webkit-slider-thumb {
  241. -webkit-appearance: none;
  242. appearance: none;
  243. width: 16px;
  244. height: 16px;
  245. background: #ffffff;
  246. cursor: pointer;
  247. border-radius: 50%;
  248. border: 2px solid #000;
  249. }
  250. #token-filter-slider::-moz-range-thumb {
  251. width: 16px;
  252. height: 16px;
  253. background: #ffffff;
  254. cursor: pointer;
  255. border-radius: 50%;
  256. border: 2px solid #000;
  257. }
  258. `;
  259. document.head.appendChild(style);
  260.  
  261. // Value label
  262. const label = document.createElement('span');
  263. label.id = 'token-filter-label';
  264. label.style.color = '#fff';
  265. label.style.fontSize = '12px';
  266. label.style.minWidth = '60px';
  267. label.textContent = `${MIN_TOKENS} tokens`;
  268.  
  269. sliderElement.addEventListener('input', (e) => {
  270. MIN_TOKENS = parseInt(e.target.value);
  271. label.textContent = `${MIN_TOKENS} tokens`;
  272. localStorage.setItem('janitorAITokenFilter', MIN_TOKENS);
  273. filterCards();
  274. });
  275.  
  276. sliderContainer.appendChild(sliderElement);
  277. sliderContainer.appendChild(label);
  278. document.body.appendChild(sliderContainer);
  279. }
  280.  
  281. sliderContainer.style.display = isSliderVisible ? 'flex' : 'none';
  282. }
  283.  
  284. function updateElementsVisibility() {
  285. const shouldShow = isAllowedPage();
  286. if (controlPanel) {
  287. controlPanel.style.display = shouldShow ? 'flex' : 'none';
  288. }
  289. if (sliderContainer) {
  290. sliderContainer.style.display = shouldShow && isSliderVisible ? 'flex' : 'none';
  291. }
  292. }
  293.  
  294. function initialize() {
  295. createControlPanel();
  296. createOrUpdateSlider();
  297.  
  298. if (isAllowedPage()) {
  299. filterCards();
  300. setupPaginationScroll();
  301.  
  302. const sidebar = document.querySelector('.css-h988mi');
  303. if (sidebar && isSidebarHidden) {
  304. sidebar.style.display = 'none';
  305. }
  306.  
  307. const observer = new MutationObserver(() => {
  308. filterCards();
  309. setupPaginationScroll();
  310. });
  311. observer.observe(document.body, { childList: true, subtree: true });
  312. }
  313. }
  314.  
  315. const tryInitialize = () => {
  316. if (document.body) {
  317. initialize();
  318.  
  319. let lastPath = window.location.pathname;
  320. setInterval(() => {
  321. if (lastPath !== window.location.pathname) {
  322. lastPath = window.location.pathname;
  323. updateElementsVisibility();
  324. if (isAllowedPage()) {
  325. filterCards();
  326. setupPaginationScroll();
  327. }
  328. }
  329. }, 500);
  330. } else {
  331. setTimeout(tryInitialize, 1000);
  332. }
  333. };
  334.  
  335. tryInitialize();
  336. })();