ecsa

Clip Studio Assets 素材商店強化工具

// ==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();
    });



})();