// ==UserScript==
// @name ecsa
// @name:en ecsa
// @name:ja ecsa
// @name:ko ecsa
// @namespace https://greasyfork.org/en/scripts/476919-ecsa
// @version 1.12
// @description Clip Studio Assets 素材商店強化工具
// @author Boni
// @match https://assets.clip-studio.com/*/download-list*
// @match https://assets.clip-studio.com/*/starred*
// @match https://assets.clip-studio.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @license GPL-3.0-only
// @icon https://www.google.com/s2/favicons?sz=64&domain=clip-studio.com
// @description:zh Clip Studio Assets 素材商店強化工具
// @description:en Enhancements for Clip Studio Assets
// @description:ja Clip Studio Assets 向け拡張機能
// @description:de Erweiterungen für Clip Studio Assets
// @description:es Mejoras para Clip Studio Assets
// @description:fr Améliorations pour Clip Studio Assets
// @description:ko Clip Studio Assets 개선 도구
// ==/UserScript==
(function() {
'use strict';
// ================= Settings System =================
const settingsConfig = {
settings: {
openInNewTab: {
type: 'checkbox',
label: 'Open links in new tab',
default: false
},
useSystemFont: {
type: 'checkbox',
label: 'Use system font',
default: false
}
},
init() {
this.loadSettings();
this.createPanel();
this.addStyles();
this.setupWrenchButton();
this.applySettings();
},
loadSettings() {
this.values = {};
for (const [key, config] of Object.entries(this.settings)) {
this.values[key] = GM_getValue(key, config.default);
}
},
saveSetting(key, value) {
GM_setValue(key, value);
this.values[key] = value;
this.applySettings();
},
applySettings() {
if (this.values.useSystemFont) {
document.body.classList.add('ecsa-system-font');
} else {
document.body.classList.remove('ecsa-system-font');
}
},
createPanel() {
// Create overlay
this.overlay = document.createElement('div');
this.overlay.className = 'ecsa-settings-overlay';
// Create panel
this.panel = document.createElement('div');
this.panel.className = 'ecsa-settings-panel';
this.panel.innerHTML = `
<div class="ecsa-panel-header">
<h4>${this.getLocalizedText('Script Settings')}</h4>
<button type="button" class="ecsa-close-btn close" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="ecsa-panel-content">
${Object.entries(this.settings).map(([key, config]) => `
<label class="setting-item">
<input type="${config.type}"
data-key="${key}"
${this.values[key] ? 'checked' : ''}>
${this.getLocalizedText(config.label)}
</label>
`).join('')}
</div>
`;
// Add event listeners
this.panel.querySelector('.ecsa-close-btn').addEventListener('click', () => this.hidePanel());
this.overlay.addEventListener('click', () => this.hidePanel());
document.body.appendChild(this.overlay);
document.body.appendChild(this.panel);
// Handle input changes
this.panel.querySelectorAll('input').forEach(input => {
input.addEventListener('change', (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
this.saveSetting(e.target.dataset.key, value);
});
});
},
setupWrenchButton() {
const wrenchButton = document.createElement('a');
wrenchButton.className = 'btn btn-default header__star hidden-xs';
wrenchButton.innerHTML = '<i class="fa fa-cog" aria-hidden="true"></i>';
wrenchButton.title = 'ECSA Settings'
const starButton = document.querySelector('.header__star');
if (starButton) {
starButton.insertAdjacentElement('beforebegin', wrenchButton);
wrenchButton.addEventListener('click', (e) => {
e.preventDefault();
this.togglePanel();
});
}
},
togglePanel() {
this.panel.style.display = this.panel.style.display === 'block' ? 'none' : 'block';
this.overlay.style.display = this.panel.style.display;
},
hidePanel() {
this.panel.style.display = 'none';
this.overlay.style.display = 'none';
},
getLocalizedText(textKey) {
const lang = window.location.pathname.split('/')[1] || 'en-us';
const translations = {
"Script Settings": {
"zh-tw": "脚本设置",
"ja-jp": "スクリプト設定",
"en-us": "Script Settings",
"de-de": "Skript-Einstellungen",
"es-es": "Configuración del script",
"fr-fr": "Paramètres du script",
"ko-kr": "스크립트 설정"
},
"Open links in new tab": {
"zh-tw": "在新标签页打开素材链接",
"ja-jp": "素材リンクを新しいタブで開く",
"en-us": "Open asset links in new tab",
"de-de": "Asset-Links in neuem Tab öffnen",
"es-es": "Abrir enlaces de assets en nueva pestaña",
"fr-fr": "Ouvrir les liens d'assets dans un nouvel onglet",
"ko-kr": "에셋 링크를 새 탭에서 열기"
},
"Sort by Category": {
"zh-tw": "按素材类型排序",
"ja-jp": "素材タイプ別に並べ替え",
"en-us": "Sort by Category",
"de-de": "Nach Kategorie sortieren",
"es-es": "Ordenar por categoría",
"fr-fr": "Trier par catégorie",
"ko-kr": "카테고리별 정렬"
},
"Sort in Default Order": {
"zh-tw": "按默认顺序排序",
"ja-jp": "デフォルトの順序別に並べ替え",
"en-us": "Sort in Default Order",
"de-de": "In der Standardreihenfolge sortieren",
"es-es": "Ordenar en el orden predeterminado",
"fr-fr": "Trier dans l'ordre par défaut",
"ko-kr": "기본 순서로 정렬"
},
"Use system font": {
"zh-tw": "使用系统字体",
"ja-jp": "システムフォントを使用",
"en-us": "Use system font",
"de-de": "Systemschriftart verwenden",
"es-es": "Usar fuente del sistema",
"fr-fr": "Utiliser la police système",
"ko-kr": "시스템 글꼴 사용"
},
};
// Fallback logic
return translations[textKey]?.[lang] ||
translations[textKey]?.['en-us'] ||
textKey;
},
addStyles() {
GM_addStyle(`
header .header__notification, header .header__star {
padding: 0px 4px;
}
.customDropdown {
min-width:180px !important;
}
.customFilterButton {
min-width:180px !important;
}
/* System font styles */
.ecsa-system-font {
font-family: system-ui, -apple-system, sans-serif !important;
}
/* Always use system font for settings panel */
.ecsa-settings-panel {
font-family: system-ui, -apple-system, sans-serif !important;
}
.ecsa-close-btn.close {
font-size: 30px;
}
.ecsa-settings-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 9999;
display: none;
}
.ecsa-settings-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 8px 16px;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0,0,0,0.2);
z-index: 10000;
width: 80%;
max-width:500px;
display: none;
}
.ecsa-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.setting-item {
display: block;
margin: 10px 0;
padding: 8px;
border-radius: 4px;
transition: background 0.2s;
}
.setting-item:hover {
background: #f8f9fa;
}
.header__wrench {
margin-right: 10px;
color: #666;
padding: 6px 12px;
transition: opacity 0.2s;
}
.header__wrench:hover {
color: #333;
background-color: #e6e6e6;
}
`);
}
};
// Initialize settings system first
settingsConfig.init();
document.addEventListener('click', handleClick, true);
function handleClick(event) {
if (!settingsConfig.values.openInNewTab) return;
let target = event.target.closest('.materialCard__thumbmailBlock');
if (target) {
const link = target.querySelector('a[href]');
if (link) {
event.preventDefault();
window.open(link.href, '_blank');
}
}
}
const getSortBtnLabel = () => ({
category: settingsConfig.getLocalizedText('Sort by Category'),
time: settingsConfig.getLocalizedText('Sort in Default Order')
});
// text for "all" option
const getAllText = () => {
if (window.location.href.includes("starred")) {
// Find the first <a> element inside the .btn-group.selectFilter
const selectFilter = document.querySelector('.btn-group.selectFilter');
if (selectFilter) {
const firstOption = selectFilter.querySelector('a');
if (firstOption) {
const firstOptionText = firstOption.textContent.trim();
return firstOptionText
}
}
} else {
// Find the <ul> element inside the .dropdown.selectFilter
const dropdown = document.querySelector('.dropdown.selectFilter');
if (dropdown) {
const ul = dropdown.querySelector('ul.dropdown-menu');
if (ul) {
const firstOption = ul.querySelector('li:first-child a');
if (firstOption) {
const firstOptionText = firstOption.textContent.trim();
return firstOptionText
}
}
}
}
}
// Define liElementsByType in the global scope
const liElementsByType = {};
let container = document.querySelector("ul.layput__cardPanel");
if (!container) return
let sortAsset = false;
let orig = container.innerHTML;
let types = []
let allText = getAllText()
let sortBtnText = getSortBtnLabel()
let currentLocation = ''
if (window.location.href.includes("starred")) {
currentLocation = 's'
} else {
currentLocation = 'd'
}
const toggleSort = (sort) => {
// Set a value in localStorage
localStorage.setItem(currentLocation + 'sorted', sort === true ? 1 : 0);
sortAsset = sort
const sortButton = document.getElementById("sortButton");
sortButton.textContent = sortAsset ? sortBtnText.time : sortBtnText.category;
// sortButton.disabled = type !== allText;
if (sort) {
// Clear the existing content on the page
container.innerHTML = '';
// Sort the <li> elements by type value (custom sorting logic)
const sortedTypes = Object.keys(liElementsByType).sort();
// Reconstruct the sorted list of <li> elements
const sortedLiElements = [];
sortedTypes.forEach((type) => {
sortedLiElements.push(...liElementsByType[type]);
});
// Append the sorted <li> elements back to the container
sortedLiElements.forEach((li) => {
container.appendChild(li);
});
} else {
container.innerHTML = orig;
}
}
// Function to sort the <li> elements by type
const preprocessAssets = () => {
const liElements = document.querySelectorAll("li.materialCard");
liElements.forEach((li) => {
const materialTypeLink = li.querySelector("a[href*='/search?type=']");
if (materialTypeLink) {
const type = materialTypeLink.textContent.trim(); // Get the text content of the <a> tag
if (!types.includes(type)) {
types.push(type)
}
if (type) {
if (!liElementsByType[type]) {
liElementsByType[type] = [];
}
liElementsByType[type].push(li);
}
}
});
// Find the existing button element
const existingButton = document.querySelector(".btn.btn-default.operationButton.favoriteButton");
if (existingButton) {
// Create a new button element
const sortButton = document.createElement("button");
sortButton.type = "button";
sortButton.className = "btn btn-primary ";
sortButton.id = "sortButton";
sortButton.textContent = sortBtnText.category;
sortButton.style.marginLeft = '10px'
sortButton.style.marginRight = '10px'
// Add an event listener to the new button if needed
sortButton.addEventListener("click", function() {
// Handle button click event
sortAsset = !sortAsset
sortButton.textContent = sortAsset ? sortBtnText.time : sortBtnText.category;
toggleSort(sortAsset)
});
// Insert the new button after the existing button
existingButton.parentNode.insertBefore(sortButton, existingButton.nextSibling);
const options = [...types];
options.unshift(allText)
const dropdown = createDropdown(options);
existingButton.parentNode.insertBefore(dropdown, sortButton.nextSibling);
}
const filterBtn = document.getElementById("filterButton");
if (filterBtn.textContent === getAllText()) {
// Read a value from localStorage
const sorted = localStorage.getItem(currentLocation + 'sorted');
// Check if the value exists
if (sorted == 1) {
// Use the value
toggleSort(true)
} else {}
}
};
// Create a function to generate the dropdown HTML
function createDropdown(types) {
const dropdown = document.createElement("div");
dropdown.className = "dropdown selectFilter ";
dropdown.style.display = 'inline-block'
dropdown.style.marginTop = '10px'
const button = document.createElement("button");
button.className = "btn btn-default dropdown-toggle filterButton customFilterButton";
button.id = "filterButton"
button.type = "button";
button.style.width = 'auto';
button.style.paddingRight = '20px';
button.setAttribute("data-toggle", "dropdown");
button.setAttribute("aria-haspopup", "true");
button.setAttribute("aria-expanded", "true");
const filterOption = localStorage.getItem(currentLocation + 'filtered');
// set sort button text but only allow change when 'all' option is selected
const sorted = localStorage.getItem(currentLocation + 'sorted');
if (types.includes(filterOption) && filterOption !== getAllText()) {
const sortButton = document.getElementById("sortButton");
sortButton.disabled = true
button.textContent = filterOption
container.innerHTML = '';
liElementsByType[filterOption].forEach((li) => {
container.appendChild(li);
});
} else {
button.textContent = types[0]; // Set the default text
}
button.style.borderRadius = '0px'
button.style.textAlign = 'left'
const caret = document.createElement("span");
caret.className = "caret";
const ul = document.createElement("ul");
ul.className = "dropdown-menu customDropdown";
// Create options from the 'types' array
types.forEach((type) => {
const li = document.createElement("li");
const a = document.createElement("a");
a.textContent = type;
li.appendChild(a);
ul.appendChild(li);
li.addEventListener("click", function(event) {
localStorage.setItem(currentLocation + 'filtered', type);
// Prevent the default behavior of following the link (if it's an anchor)
event.preventDefault();
container.innerHTML = '';
// Enable or disable the new button based on the selected option
const sortButton = document.getElementById("sortButton");
sortButton.disabled = type !== allText;
button.firstChild.textContent = type;
const h4Element = document.querySelector("h4.text-right");
if (type !== allText) {
liElementsByType[type].forEach((li) => {
container.appendChild(li);
});
localStorage.setItem(currentLocation + 'filtered', type);
} else {
container.innerHTML = orig;
const sorted = localStorage.getItem(currentLocation + 'sorted');
// Check if the value exists
if (sorted == 1) {
// Use the value
toggleSort(true)
} else {}
}
});
});
// Append elements to the dropdown
button.appendChild(caret);
dropdown.appendChild(button);
dropdown.appendChild(ul);
return dropdown;
}
function shouldRunOnThisPage() {
const path = window.location.pathname;
return path.includes('/download-list') || path.includes('/starred');
}
function shouldRunFeatureToggle() {
return window.location.pathname.includes('/search');
}
// Wait for the page to fully load before executing the sorting function
// Initialize page-specific features
window.addEventListener('load', () => {
if (shouldRunOnThisPage()) preprocessAssets();
});
// Add ESC key listener
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') settingsConfig.hidePanel();
});
})();