ecsa

Clip Studio Assets 素材商店強化工具

当前为 2025-03-21 提交的版本,查看 最新版本

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