ecsa

Clip Studio Assets 素材商店強化工具

  1. // ==UserScript==
  2. // @name ecsa
  3. // @name:en ecsa
  4. // @name:ja ecsa
  5. // @name:ko ecsa
  6. // @namespace https://greasyfork.org/en/scripts/476919-ecsa
  7. // @version 1.13
  8. // @description Clip Studio Assets 素材商店強化工具
  9. // @author Boni
  10. // @match https://assets.clip-studio.com/*/download-list*
  11. // @match https://assets.clip-studio.com/*/starred*
  12. // @match https://assets.clip-studio.com/*
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_addStyle
  16. // @license GPL-3.0-only
  17. // @icon https://www.google.com/s2/favicons?sz=64&domain=clip-studio.com
  18. // @description:zh Clip Studio Assets 素材商店強化工具
  19. // @description:en Enhancements for Clip Studio Assets
  20. // @description:ja Clip Studio Assets 向け拡張機能
  21. // @description:de Erweiterungen für Clip Studio Assets
  22. // @description:es Mejoras para Clip Studio Assets
  23. // @description:fr Améliorations pour Clip Studio Assets
  24. // @description:ko Clip Studio Assets 개선 도구
  25. // ==/UserScript==
  26.  
  27. (function() {
  28. 'use strict';
  29.  
  30. // ================= Settings System =================
  31. const settingsConfig = {
  32. settings: {
  33. openInNewTab: {
  34. type: 'checkbox',
  35. label: 'Open links in new tab',
  36. default: false
  37. },
  38. useSystemFont: {
  39. type: 'checkbox',
  40. label: 'Use system font',
  41. default: false
  42. }
  43. },
  44.  
  45. init() {
  46. this.loadSettings();
  47. this.createPanel();
  48. this.addStyles();
  49. this.setupWrenchButton();
  50. this.applySettings();
  51. },
  52.  
  53. loadSettings() {
  54. this.values = {};
  55. for (const [key, config] of Object.entries(this.settings)) {
  56. this.values[key] = GM_getValue(key, config.default);
  57. }
  58. },
  59.  
  60. saveSetting(key, value) {
  61. GM_setValue(key, value);
  62. this.values[key] = value;
  63. this.applySettings();
  64. },
  65.  
  66. applySettings() {
  67.  
  68. if (this.values.useSystemFont) {
  69. document.body.classList.add('ecsa-system-font');
  70. } else {
  71. document.body.classList.remove('ecsa-system-font');
  72. }
  73. },
  74.  
  75. createPanel() {
  76. // Create overlay
  77. this.overlay = document.createElement('div');
  78. this.overlay.className = 'ecsa-settings-overlay';
  79.  
  80. // Create panel
  81. this.panel = document.createElement('div');
  82. this.panel.className = 'ecsa-settings-panel';
  83. this.panel.innerHTML = `
  84. <div class="ecsa-panel-header">
  85. <h4>${this.getLocalizedText('Script Settings')}</h4>
  86. <button type="button" class="ecsa-close-btn close" aria-label="Close">
  87. <span aria-hidden="true">×</span>
  88. </button>
  89. </div>
  90. <div class="ecsa-panel-content">
  91. ${Object.entries(this.settings).map(([key, config]) => `
  92. <label class="setting-item">
  93. <input type="${config.type}"
  94. data-key="${key}"
  95. ${this.values[key] ? 'checked' : ''}>
  96. ${this.getLocalizedText(config.label)}
  97. </label>
  98. `).join('')}
  99. </div>
  100. `;
  101.  
  102. // Add event listeners
  103. this.panel.querySelector('.ecsa-close-btn').addEventListener('click', () => this.hidePanel());
  104. this.overlay.addEventListener('click', () => this.hidePanel());
  105. document.body.appendChild(this.overlay);
  106. document.body.appendChild(this.panel);
  107.  
  108. // Handle input changes
  109. this.panel.querySelectorAll('input').forEach(input => {
  110. input.addEventListener('change', (e) => {
  111. const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
  112. this.saveSetting(e.target.dataset.key, value);
  113. });
  114. });
  115.  
  116.  
  117. },
  118.  
  119. setupWrenchButton() {
  120. const wrenchButton = document.createElement('a');
  121. wrenchButton.className = 'btn btn-default header__star hidden-xs';
  122. wrenchButton.innerHTML = '<i class="fa fa-cog" aria-hidden="true"></i>';
  123. wrenchButton.title = 'ECSA Settings'
  124.  
  125. const starButton = document.querySelector('.header__star');
  126. if (starButton) {
  127. starButton.insertAdjacentElement('beforebegin', wrenchButton);
  128. wrenchButton.addEventListener('click', (e) => {
  129. e.preventDefault();
  130. this.togglePanel();
  131. });
  132. }
  133. },
  134.  
  135. togglePanel() {
  136. this.panel.style.display = this.panel.style.display === 'block' ? 'none' : 'block';
  137. this.overlay.style.display = this.panel.style.display;
  138. },
  139.  
  140. hidePanel() {
  141. this.panel.style.display = 'none';
  142. this.overlay.style.display = 'none';
  143. },
  144.  
  145. getLocalizedText(textKey) {
  146. const lang = window.location.pathname.split('/')[1] || 'en-us';
  147. const translations = {
  148. "Script Settings": {
  149. "zh-tw": "脚本设置",
  150. "ja-jp": "スクリプト設定",
  151. "en-us": "Script Settings",
  152. "de-de": "Skript-Einstellungen",
  153. "es-es": "Configuración del script",
  154. "fr-fr": "Paramètres du script",
  155. "ko-kr": "스크립트 설정"
  156. },
  157. "Open links in new tab": {
  158. "zh-tw": "在新标签页打开素材链接",
  159. "ja-jp": "素材リンクを新しいタブで開く",
  160. "en-us": "Open asset links in new tab",
  161. "de-de": "Asset-Links in neuem Tab öffnen",
  162. "es-es": "Abrir enlaces de assets en nueva pestaña",
  163. "fr-fr": "Ouvrir les liens d'assets dans un nouvel onglet",
  164. "ko-kr": "에셋 링크를 새 탭에서 열기"
  165. },
  166. "Sort by Category": {
  167. "zh-tw": "按素材类型排序",
  168. "ja-jp": "素材タイプ別に並べ替え",
  169. "en-us": "Sort by Category",
  170. "de-de": "Nach Kategorie sortieren",
  171. "es-es": "Ordenar por categoría",
  172. "fr-fr": "Trier par catégorie",
  173. "ko-kr": "카테고리별 정렬"
  174. },
  175. "Sort in Default Order": {
  176. "zh-tw": "按默认顺序排序",
  177. "ja-jp": "デフォルトの順序別に並べ替え",
  178. "en-us": "Sort in Default Order",
  179. "de-de": "In der Standardreihenfolge sortieren",
  180. "es-es": "Ordenar en el orden predeterminado",
  181. "fr-fr": "Trier dans l'ordre par défaut",
  182. "ko-kr": "기본 순서로 정렬"
  183. },
  184. "Use system font": {
  185. "zh-tw": "使用系统字体",
  186. "ja-jp": "システムフォントを使用",
  187. "en-us": "Use system font",
  188. "de-de": "Systemschriftart verwenden",
  189. "es-es": "Usar fuente del sistema",
  190. "fr-fr": "Utiliser la police système",
  191. "ko-kr": "시스템 글꼴 사용"
  192. },
  193. };
  194.  
  195. // Fallback logic
  196. return translations[textKey]?.[lang] ||
  197. translations[textKey]?.['en-us'] ||
  198. textKey;
  199. },
  200.  
  201. addStyles() {
  202. GM_addStyle(`
  203.  
  204. header .header__notification, header .header__star {
  205. padding: 0px 4px;
  206. }
  207.  
  208. .customDropdown {
  209. min-width:180px !important;
  210. }
  211. /* System font styles */
  212. .ecsa-system-font {
  213. font-family: system-ui, -apple-system, sans-serif !important;
  214. }
  215.  
  216. /* Always use system font for settings panel */
  217. .ecsa-settings-panel {
  218. font-family: system-ui, -apple-system, sans-serif !important;
  219. }
  220. .ecsa-close-btn.close {
  221. font-size: 30px;
  222. }
  223.  
  224. .ecsa-settings-overlay {
  225. position: fixed;
  226. top: 0;
  227. left: 0;
  228. width: 100%;
  229. height: 100%;
  230. background: rgba(0,0,0,0.5);
  231. z-index: 9999;
  232. display: none;
  233. }
  234.  
  235. .ecsa-settings-panel {
  236. position: fixed;
  237. top: 50%;
  238. left: 50%;
  239. transform: translate(-50%, -50%);
  240. background: white;
  241. padding: 8px 16px;
  242. border-radius: 8px;
  243. box-shadow: 0 0 20px rgba(0,0,0,0.2);
  244. z-index: 10000;
  245. width: 80%;
  246. max-width:500px;
  247. display: none;
  248. }
  249.  
  250. .ecsa-panel-header {
  251. display: flex;
  252. justify-content: space-between;
  253. align-items: center;
  254. margin-bottom: 20px;
  255. padding-bottom: 10px;
  256. border-bottom: 1px solid #eee;
  257. }
  258.  
  259.  
  260. .setting-item {
  261. display: block;
  262. margin: 10px 0;
  263. padding: 8px;
  264. border-radius: 4px;
  265. transition: background 0.2s;
  266. }
  267.  
  268. .setting-item:hover {
  269. background: #f8f9fa;
  270. }
  271.  
  272. .header__wrench {
  273. margin-right: 10px;
  274. color: #666;
  275. padding: 6px 12px;
  276. transition: opacity 0.2s;
  277. }
  278.  
  279. .header__wrench:hover {
  280. color: #333;
  281. background-color: #e6e6e6;
  282. }
  283. `);
  284. }
  285. };
  286.  
  287. // Initialize settings system first
  288. settingsConfig.init();
  289.  
  290. document.addEventListener('click', handleClick, true);
  291.  
  292. function handleClick(event) {
  293. if (!settingsConfig.values.openInNewTab) return;
  294.  
  295. let target = event.target.closest('.materialCard__thumbmailBlock');
  296. if (target) {
  297. const link = target.querySelector('a[href]');
  298. if (link) {
  299. event.preventDefault();
  300. window.open(link.href, '_blank');
  301. }
  302. }
  303. }
  304.  
  305.  
  306. const getSortBtnLabel = () => ({
  307. category: settingsConfig.getLocalizedText('Sort by Category'),
  308. time: settingsConfig.getLocalizedText('Sort in Default Order')
  309. });
  310.  
  311.  
  312. // text for "all" option
  313. const getAllText = () => {
  314. if (window.location.href.includes("starred")) {
  315. // Find the first <a> element inside the .btn-group.selectFilter
  316. const selectFilter = document.querySelector('.btn-group.selectFilter');
  317. if (selectFilter) {
  318. const firstOption = selectFilter.querySelector('a');
  319. if (firstOption) {
  320. const firstOptionText = firstOption.textContent.trim();
  321. return firstOptionText
  322. }
  323. }
  324. } else {
  325. // Find the <ul> element inside the .dropdown.selectFilter
  326. const dropdown = document.querySelector('.dropdown.selectFilter');
  327. if (dropdown) {
  328. const ul = dropdown.querySelector('ul.dropdown-menu');
  329. if (ul) {
  330. const firstOption = ul.querySelector('li:first-child a');
  331. if (firstOption) {
  332. const firstOptionText = firstOption.textContent.trim();
  333. return firstOptionText
  334. }
  335. }
  336. }
  337. }
  338. }
  339.  
  340. // Define liElementsByType in the global scope
  341. const liElementsByType = {};
  342. let container = document.querySelector("ul.layput__cardPanel");
  343. if (!container) return
  344. let sortAsset = false;
  345. let orig = container.innerHTML;
  346. let types = []
  347. let allText = getAllText()
  348. let sortBtnText = getSortBtnLabel()
  349. let currentLocation = ''
  350. if (window.location.href.includes("starred")) {
  351. currentLocation = 's'
  352. } else {
  353. currentLocation = 'd'
  354. }
  355.  
  356.  
  357. const toggleSort = (sort) => {
  358. // Set a value in localStorage
  359. localStorage.setItem(currentLocation + 'sorted', sort === true ? 1 : 0);
  360. sortAsset = sort
  361. const sortButton = document.getElementById("sortButton");
  362. sortButton.textContent = sortAsset ? sortBtnText.time : sortBtnText.category;
  363. // sortButton.disabled = type !== allText;
  364. if (sort) {
  365. // Clear the existing content on the page
  366. container.innerHTML = '';
  367. // Sort the <li> elements by type value (custom sorting logic)
  368. const sortedTypes = Object.keys(liElementsByType).sort();
  369. // Reconstruct the sorted list of <li> elements
  370. const sortedLiElements = [];
  371. sortedTypes.forEach((type) => {
  372. sortedLiElements.push(...liElementsByType[type]);
  373. });
  374. // Append the sorted <li> elements back to the container
  375. sortedLiElements.forEach((li) => {
  376. container.appendChild(li);
  377. });
  378. } else {
  379. container.innerHTML = orig;
  380. }
  381. }
  382.  
  383. // Function to sort the <li> elements by type
  384. const preprocessAssets = () => {
  385. const liElements = document.querySelectorAll("li.materialCard");
  386. liElements.forEach((li) => {
  387. const materialTypeLink = li.querySelector("a[href*='/search?type=']");
  388. if (materialTypeLink) {
  389. const type = materialTypeLink.textContent.trim(); // Get the text content of the <a> tag
  390. if (!types.includes(type)) {
  391. types.push(type)
  392. }
  393. if (type) {
  394. if (!liElementsByType[type]) {
  395. liElementsByType[type] = [];
  396. }
  397. liElementsByType[type].push(li);
  398. }
  399. }
  400. });
  401. // Find the existing button element
  402. const existingButton = document.querySelector(".btn.btn-default.operationButton.favoriteButton");
  403. if (existingButton) {
  404. // Create a new button element
  405. const sortButton = document.createElement("button");
  406. sortButton.type = "button";
  407. sortButton.className = "btn btn-primary ";
  408. sortButton.id = "sortButton";
  409. sortButton.textContent = sortBtnText.category;
  410. sortButton.style.marginLeft = '10px'
  411. sortButton.style.marginRight = '10px'
  412. // Add an event listener to the new button if needed
  413. sortButton.addEventListener("click", function() {
  414. // Handle button click event
  415. sortAsset = !sortAsset
  416. sortButton.textContent = sortAsset ? sortBtnText.time : sortBtnText.category;
  417. toggleSort(sortAsset)
  418. });
  419. // Insert the new button after the existing button
  420. existingButton.parentNode.insertBefore(sortButton, existingButton.nextSibling);
  421. const options = [...types];
  422. options.unshift(allText)
  423. const dropdown = createDropdown(options);
  424. existingButton.parentNode.insertBefore(dropdown, sortButton.nextSibling);
  425. }
  426. const filterBtn = document.getElementById("filterButton");
  427. if (filterBtn.textContent === getAllText()) {
  428. // Read a value from localStorage
  429. const sorted = localStorage.getItem(currentLocation + 'sorted');
  430. // Check if the value exists
  431. if (sorted == 1) {
  432. // Use the value
  433. toggleSort(true)
  434. } else {}
  435. }
  436. };
  437.  
  438. // Create a function to generate the dropdown HTML
  439. function createDropdown(types) {
  440. const dropdown = document.createElement("div");
  441. dropdown.className = "dropdown selectFilter ";
  442. dropdown.style.display = 'inline-block'
  443. dropdown.style.marginTop = '10px'
  444.  
  445. const button = document.createElement("button");
  446. button.className = "btn btn-default dropdown-toggle filterButton customFilterButton";
  447. button.id = "filterButton"
  448. button.type = "button";
  449. button.style.width = 'auto';
  450. button.style.paddingRight = '20px';
  451.  
  452. button.setAttribute("data-toggle", "dropdown");
  453. button.setAttribute("aria-haspopup", "true");
  454. button.setAttribute("aria-expanded", "true");
  455. const filterOption = localStorage.getItem(currentLocation + 'filtered');
  456.  
  457. // set sort button text but only allow change when 'all' option is selected
  458. const sorted = localStorage.getItem(currentLocation + 'sorted');
  459.  
  460. if (types.includes(filterOption) && filterOption !== getAllText()) {
  461. const sortButton = document.getElementById("sortButton");
  462. sortButton.disabled = true
  463. button.textContent = filterOption
  464. container.innerHTML = '';
  465. liElementsByType[filterOption].forEach((li) => {
  466. container.appendChild(li);
  467. });
  468. } else {
  469.  
  470. button.textContent = types[0]; // Set the default text
  471.  
  472. }
  473. button.style.borderRadius = '0px'
  474. button.style.textAlign = 'left'
  475. const caret = document.createElement("span");
  476. caret.className = "caret";
  477.  
  478.  
  479. const ul = document.createElement("ul");
  480. ul.className = "dropdown-menu customDropdown";
  481. // Create options from the 'types' array
  482. types.forEach((type) => {
  483. const li = document.createElement("li");
  484. const a = document.createElement("a");
  485. a.textContent = type;
  486. li.appendChild(a);
  487. ul.appendChild(li);
  488. li.addEventListener("click", function(event) {
  489. localStorage.setItem(currentLocation + 'filtered', type);
  490. // Prevent the default behavior of following the link (if it's an anchor)
  491. event.preventDefault();
  492. container.innerHTML = '';
  493. // Enable or disable the new button based on the selected option
  494. const sortButton = document.getElementById("sortButton");
  495. sortButton.disabled = type !== allText;
  496. button.firstChild.textContent = type;
  497. const h4Element = document.querySelector("h4.text-right");
  498. if (type !== allText) {
  499. liElementsByType[type].forEach((li) => {
  500. container.appendChild(li);
  501. });
  502. localStorage.setItem(currentLocation + 'filtered', type);
  503. } else {
  504. container.innerHTML = orig;
  505. const sorted = localStorage.getItem(currentLocation + 'sorted');
  506. // Check if the value exists
  507. if (sorted == 1) {
  508. // Use the value
  509. toggleSort(true)
  510. } else {}
  511. }
  512. });
  513. });
  514. // Append elements to the dropdown
  515. button.appendChild(caret);
  516. dropdown.appendChild(button);
  517. dropdown.appendChild(ul);
  518. return dropdown;
  519. }
  520.  
  521.  
  522. function shouldRunOnThisPage() {
  523. const path = window.location.pathname;
  524. return path.includes('/download-list') || path.includes('/starred');
  525. }
  526.  
  527. function shouldRunFeatureToggle() {
  528. return window.location.pathname.includes('/search');
  529. }
  530.  
  531. // Wait for the page to fully load before executing the sorting function
  532. // Initialize page-specific features
  533. window.addEventListener('load', () => {
  534. if (shouldRunOnThisPage()) preprocessAssets();
  535.  
  536. });
  537.  
  538. // Add ESC key listener
  539. document.addEventListener('keydown', (e) => {
  540. if (e.key === 'Escape') settingsConfig.hidePanel();
  541. });
  542.  
  543.  
  544.  
  545. })();