Auto Tags Filler for Iromirai & Chichi-pui

Auto-fills tags for Iromirai & Chichi-pui.

// ==UserScript==
// @name         Auto Tags Filler for Iromirai & Chichi-pui
// @name:ja      タグ入力補助 for イロミライ&ちちぷい
// @description    Auto-fills tags for Iromirai & Chichi-pui.
// @description:ja イロミライとちちぷいのタグ入力を補助します。
// @namespace    https://bsky.app/profile/neon-ai.art
// @homepage     https://bsky.app/profile/neon-ai.art
// @icon         data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⛄️</text></svg>
// @version     7.3
// @author       ねおん
// @match        https://iromirai.jp/user/post*
// @match        https://www.chichi-pui.com/posts/upload*
// @grant        GM_getValue
// @grant        GM_setValue
// @require      https://gf.qytechs.cn/scripts/28536-gm-config/code/GM_config.js?version=184529
// @license      CC BY-NC 4.0
// ==/UserScript==

(function() {
    'use strict';

    // --- 設定関連 ---
    // exportFileNameはGM_configには直接関係しないが、エクスポート機能のために残す
    const exportFileName = 'tags_filler.json';

    // DOM要素はサイトごとに動的に設定
    let themeTagInput;
    let addButton;

    const hostname = window.location.hostname;

    if (hostname.includes('iromirai.jp')) {
        themeTagInput = document.getElementById('themeTagText');
        addButton = document.querySelector('.postmedia-mobile-addtag-button');
    } else if (hostname.includes('chichi-pui.com')) {
        themeTagInput = document.querySelector('input.input[placeholder*="タグを入力"]');
        addButton = document.querySelector('button.button.is-dark.ml-2[type="button"]');
    } else {
        console.error('このスクリプトはIromiraiまたはChichi-puiでのみ動作します。');
        return;
    }

    if (!themeTagInput || !addButton) {
        console.error('必要な要素が見つかりません。スクリプトを終了します。');
        return;
    }

    const ENABLE_TEXTFILL_INTEGRATION = false; // TextFill連携機能の有効/無効 (true:有効, false:無効)
    const ENABLE_IN_SCRIPT_TAG_MANAGEMENT = true; // スクリプト内タグ管理機能の有効/無効 (true:有効, false:無効)

    // ユーザー定義テンプレートの初期データ(GM_configにデータがない場合にロードされる)
    const DEFAULT_USER_TEMPLATES = [
        { name: "基本タグ", tags: "イラスト,AI生成,可愛い" },
        { name: "風景タグ", tags: "風景,空,自然" },
        { name: "キャラクター", tags: "女の子,オリジナル,ファンタジー" }
    ];

    let currentTemplates = []; // 現在のテンプレートリスト

    let isProcessing = false; // 二重実行防止フラグ
    let changeTimer = null;
    let isButtonTriggeredTextFill = false; // ボタンからのTextFillトリガー中を示すフラグ
    let isComposing = false; // IME変換中フラグ

    // --- GM_configの初期化 ---
    GM_config.init({
        'id': 'IromiraiChichiPuiConfig', // 設定を保存するためのユニークなID
        'title': 'テーマタグ設定', // 設定パネルのタイトル
        'fields': {
            'userTemplates': {
                'label': 'ユーザーテンプレート',
                'type': 'hidden', // UIには表示しないが、データはここに保存される
                'default': JSON.stringify(DEFAULT_USER_TEMPLATES)
            }
        },
        'events': {
            'save': function() {
                // GM_configが保存されたときに、内部のcurrentTemplatesを更新
                // ただし、このスクリプトはUIから直接GM_configを操作しないため、主にGM_config.set()の後に呼ばれる想定
                loadTemplatesFromGMConfig();
            }
        }
    });

    // --- テンプレートをグローバルに読み込む関数 ---
    function loadTemplatesFromGMConfig() {
        try {
            // グローバルキーを使用してテンプレートを読み込む
            const storedData = GM_getValue('global_user_templates', JSON.stringify(DEFAULT_USER_TEMPLATES));
            const parsedData = JSON.parse(storedData);

            if (Array.isArray(parsedData) && parsedData.every(t => typeof t.name === 'string' && typeof t.tags === 'string')) {
                currentTemplates = parsedData;
            } else {
                console.warn("グローバルストレージのテンプレートデータが不正なため、デフォルトをロードします。");
                currentTemplates = DEFAULT_USER_TEMPLATES;
                saveTemplatesToGMConfig(); // 不正な場合はデフォルトを保存し直す
            }
        } catch (e) {
            console.error("グローバルストレージからのテンプレート読み込みに失敗しました:", e);
            currentTemplates = DEFAULT_USER_TEMPLATES;
            saveTemplatesToGMConfig(); // エラー時はデフォルトを保存
        }
        console.log("ロードされたテンプレート:", currentTemplates);
    }

    // --- テンプレートをグローバルに保存する関数 ---
    function saveTemplatesToGMConfig() {
        try {
            // グローバルキーを使用してテンプレートを保存する
            GM_setValue('global_user_templates', JSON.stringify(currentTemplates));
            console.log("テンプレートをグローバルストレージに保存しました。");
        } catch (e) {
            console.error("グローバルストレージへのテンプレート保存に失敗しました:", e);
        }
    }

    // スクリプト起動時にテンプレートをロード
    if (ENABLE_IN_SCRIPT_TAG_MANAGEMENT) {
        loadTemplatesFromGMConfig();
    }

    // --- タグ追加処理の関数 (自動確定用) ---
    async function processTagsAutomatically() {
        if (isProcessing) {
            console.log('既に処理中です。');
            return;
        }
        isProcessing = true;

        const rawTagsString = themeTagInput.value;
        if (!rawTagsString.trim() || rawTagsString.trim() === '/') {
            console.log('タグ入力フォームが空か、TextFillトリガー文字のみです。自動処理をスキップします。');
            isProcessing = false;
            return;
        }

        let tagsToEnter = rawTagsString.split(/[、,  ]+/).map(tag => tag.trim()).filter(tag => tag !== '');
        if (tagsToEnter.length === 0) {
            console.log('有効なタグが見つかりませんでした。自動処理をスキップします。');
            isProcessing = false;
            return;
        }

        console.log('自動タグ入力処理を開始します。');
        const initialDelayTime = 200;
        console.log('初回処理開始前:遅延 (' + initialDelayTime + 'ms)');
        await new Promise(resolve => setTimeout(resolve, initialDelayTime));
        themeTagInput.value = '';
        themeTagInput.dispatchEvent(new Event('input', { bubbles: true }));
        themeTagInput.dispatchEvent(new Event('change', { bubbles: true }));
        await new Promise(resolve => setTimeout(resolve, 50));
        for (let i = 0; i < tagsToEnter.length; i++) {
            const tag = tagsToEnter[i];
            themeTagInput.value = tag;
            themeTagInput.dispatchEvent(new Event('input', { bubbles: true }));
            themeTagInput.dispatchEvent(new Event('change', { bubbles: true }));

            addButton.click();
            const loopDelayTime = 100;
            console.log('各タグ追加:短めの遅延 (' + loopDelayTime + 'ms)');
            await new Promise(resolve => setTimeout(resolve, loopDelayTime));
        }
        console.log('自動タグ入力処理が完了しました。');
        isProcessing = false;
    }

    // --- UIコンポーネントの作成と配置 ---
    let triggerTextFillButton;
    if (ENABLE_TEXTFILL_INTEGRATION) {
        triggerTextFillButton = document.createElement('button');
        triggerTextFillButton.textContent = 'TextFill';
        triggerTextFillButton.style.cssText = 'position:fixed; bottom:50px; right:10px; z-index:9999; padding:10px; background-color:#800080; color:white; border:none; border-radius:5px; cursor:pointer; font-size: 0.9em;';
        document.body.appendChild(triggerTextFillButton);
    }

    let manageTemplatesButton;
    let managePanel;
    let h3TemplateList;
    let filterInput;
    let templateItemsScrollArea;
    let addFormContainer;
    let addTemplateNameInput;
    let addTemplateTagsInput;
    let addTemplateButton;
    let bottomButtonContainer;
    let addTagButton;
    let exportTagsButton;
    let importTagsButton;

    if (ENABLE_IN_SCRIPT_TAG_MANAGEMENT) {
        manageTemplatesButton = document.createElement('button');
        manageTemplatesButton.textContent = 'タグ管理';
        manageTemplatesButton.style.cssText = 'position:fixed; bottom:95px; right:10px; z-index:9999; padding:10px; background-color:#337ab7; color:white; border:none; border-radius:5px; cursor:pointer; font-size: 0.9em;';
        document.body.appendChild(manageTemplatesButton);

        managePanel = document.createElement('div');
        managePanel.style.cssText = `
            position:fixed; bottom:140px; right:10px; z-index:9999;
            background-color:white; border:1px solid #ccc; border-radius:5px;
            padding:15px; display:none; flex-direction:column; gap:10px;
            width: 350px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
            max-height: calc(100vh - 150px);
            resize: vertical;
            overflow: hidden;
        `;
        document.body.appendChild(managePanel);

        h3TemplateList = document.createElement('h3');
        h3TemplateList.textContent = 'マイテーマタグ';
        managePanel.appendChild(h3TemplateList);

        filterInput = document.createElement('input');
        filterInput.type = 'text';
        filterInput.id = 'templateFilterInput';
        filterInput.placeholder = 'フィルター (名前、タグ)';
        filterInput.style.cssText = 'width:calc(100% - 2px); padding:8px; margin-bottom:10px; border:1px solid #ccc; border-radius:3px;';
        managePanel.appendChild(filterInput);

        templateItemsScrollArea = document.createElement('div');
        templateItemsScrollArea.style.cssText = `
            flex-grow: 1;
            overflow-y: auto;
            border-bottom: 1px solid #eee;
            padding-bottom: 10px;
            margin-bottom: 10px;
            min-height: 50px;
        `;
        managePanel.appendChild(templateItemsScrollArea);

        addFormContainer = document.createElement('div');
        addFormContainer.style.cssText = 'padding-top: 10px; margin-top: 10px; display: none;';
        addFormContainer.innerHTML = `
            <label for="addTemplateNameInput">名前:</label><br>
            <input type="text" id="addTemplateNameInput" style="width:100%; padding:5px; margin-bottom:5px;"><br>
            <label for="addTemplateTagsInput">タグ (カンマ区切り):</label><br>
            <input type="text" id="addTemplateTagsInput" style="width:100%; padding:5px; margin-bottom:10px;"><br>
            <button id="addTemplateButton" style="padding:8px 15px; background-color:#5cb85c; color:white; border:none; border-radius:3px; cursor:pointer;">追加</button>
        `;
        managePanel.appendChild(addFormContainer);

        addTemplateNameInput = addFormContainer.querySelector('#addTemplateNameInput');
        addTemplateTagsInput = addFormContainer.querySelector('#addTemplateTagsInput');
        addTemplateButton = addFormContainer.querySelector('#addTemplateButton');

        bottomButtonContainer = document.createElement('div');
        bottomButtonContainer.style.cssText = `
            border-top: 1px solid #eee; padding-top: 10px; margin-top: 10px;
            display: flex; justify-content: space-between; flex-wrap: wrap;
            gap: 5px;
        `;
        managePanel.appendChild(bottomButtonContainer);

        addTagButton = document.createElement('button');
        addTagButton.textContent = 'タグ追加';
        addTagButton.style.cssText = 'padding:8px 15px; background-color:#6c757d; color:white; border:none; border-radius:3px; cursor:pointer; flex-grow:1; min-width:80px;';
        bottomButtonContainer.appendChild(addTagButton);

        exportTagsButton = document.createElement('button');
        exportTagsButton.textContent = 'エクスポート';
        exportTagsButton.style.cssText = 'padding:8px 15px; background-color:#007bff; color:white; border:none; border-radius:3px; cursor:pointer; margin-right: 5px; flex-grow:1; min-width:80px;';
        bottomButtonContainer.appendChild(exportTagsButton);

        importTagsButton = document.createElement('button');
        importTagsButton.textContent = 'インポート';
        importTagsButton.style.cssText = 'padding:8px 15px; background-color:#ffc107; color:black; border:none; border-radius:3px; cursor:pointer; flex-grow:1; min-width:80px;';
        bottomButtonContainer.appendChild(importTagsButton);

        const importTagsFile = document.createElement('input');
        importTagsFile.type = 'file';
        importTagsFile.id = 'importTagsFile';
        importTagsFile.accept = '.json';
        importTagsFile.style.display = 'none';
        bottomButtonContainer.appendChild(importTagsFile);

        addTagButton.addEventListener('click', () => {
            const isVisible = addFormContainer.style.display === 'block';
            addFormContainer.style.display = isVisible ? 'none' : 'block';
            addTagButton.textContent = isVisible ? 'タグ追加' : 'タグ追加 ▲';
            adjustPanelHeight();
            if (!isVisible) {
                addTemplateNameInput.focus();
            }
        });

        importTagsButton.addEventListener('click', () => {
            importTagsFile.click();
        });

        importTagsFile.addEventListener('change', (event) => {
            const file = event.target.files[0];
            if (!file) {
                return;
            }

            const reader = new FileReader();
            reader.onload = (e) => {
                try {
                    const rawData = JSON.parse(e.target.result);
                    let importedData = [];

                    if (typeof rawData === 'object' && !Array.isArray(rawData) && Object.keys(rawData).every(key => key.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/))) {
                        console.log("TextFill形式のデータを検出しました。");
                        importedData = Object.values(rawData).map(item => {
                            let tags = '';
                            try {
                                if (typeof item.script === 'string') {
                                    let scriptContent;
                                    try {
                                        scriptContent = JSON.parse(item.script);
                                    } catch (innerParseError) {
                                        scriptContent = item.script;
                                    }
                                    if (Array.isArray(scriptContent) && scriptContent.length > 0 && scriptContent[0] && typeof scriptContent[0].insert === 'string') {
                                        tags = scriptContent[0].insert.replace(/\\n/g, ', ').trim();
                                        if (tags.endsWith(',')) tags = tags.slice(0, -1).trim();
                                    } else if (typeof scriptContent === 'string') {
                                        tags = scriptContent.replace(/\\n/g, ', ').trim();
                                        if (tags.endsWith(',')) tags = tags.slice(0, -1).trim();
                                    } else {
                                        tags = item.script;
                                    }
                                } else {
                                    if (Array.isArray(item.script) && item.script.length > 0 && item.script[0] && typeof item.script[0].insert === 'string') {
                                        tags = item.script[0].insert.replace(/\\n/g, ', ').trim();
                                        if (tags.endsWith(',')) tags = tags.slice(0, -1).trim();
                                    } else {
                                        tags = item.script;
                                    }
                                }
                            } catch (parseError) {
                                console.warn(`TextFillスクリプトのパースに失敗しました (${item.label || item.command}):`, parseError);
                                tags = item.script;
                            }
                            return { name: item.label || item.command || '不明なタグ', tags: tags };
                        });
                    } else if (Array.isArray(rawData) && rawData.every(t => typeof t.name === 'string' && typeof t.tags === 'string')) {
                        console.log("iromirai形式のデータを検出しました。");
                        importedData = rawData;
                    } else {
                        throw new Error("対応していないJSON形式です。TextFillのエクスポートデータか、このスクリプトでエクスポートしたJSONファイルを指定してください。");
                    }

                    const newTemplates = [...currentTemplates];
                    let importedCount = 0;
                    let updatedCount = 0;

                    importedData.forEach(importedTemplate => {
                        const existingIndex = newTemplates.findIndex(t => t.name === importedTemplate.name);
                        if (existingIndex !== -1) {
                            newTemplates[existingIndex] = importedTemplate;
                            updatedCount++;
                        } else {
                            newTemplates.push(importedTemplate);
                            importedCount++;
                        }
                    });

                    currentTemplates = newTemplates;
                    saveTemplatesToGMConfig();
                    renderTemplateList();
                    alert(`テーマタグのインポートが完了しました!\n新規追加: ${importedCount}件, 更新: ${updatedCount}件`);
                } catch (e) {
                    alert('ファイルの読み込みまたはパースに失敗しました。\nエラー: ' + e.message);
                    console.error('インポートエラー:', e);
                } finally {
                    event.target.value = '';
                }
            };
            reader.readAsText(file);
        });
    }

    // --- パネルの高さ調整関数 ---
    function adjustPanelHeight() {
        if (!ENABLE_IN_SCRIPT_TAG_MANAGEMENT) return;
        const originalDisplay = managePanel.style.display;
        managePanel.style.display = 'block';
        managePanel.style.height = 'auto';

        let fixedHeight = 0;
        fixedHeight += h3TemplateList.offsetHeight +
                       parseInt(getComputedStyle(h3TemplateList).marginTop) +
                       parseInt(getComputedStyle(h3TemplateList).marginBottom);
        fixedHeight += filterInput.offsetHeight +
                       parseInt(getComputedStyle(filterInput).marginTop) +
                       parseInt(getComputedStyle(filterInput).marginBottom);
        fixedHeight += bottomButtonContainer.offsetHeight +
                       parseInt(getComputedStyle(bottomButtonContainer).marginTop) +
                       parseInt(getComputedStyle(bottomButtonContainer).paddingTop);
        fixedHeight += parseInt(getComputedStyle(managePanel).paddingTop) +
                       parseInt(getComputedStyle(managePanel).paddingBottom);

        if (addFormContainer.style.display === 'block') {
            fixedHeight += addFormContainer.offsetHeight;
        }

        const newTemplateListMaxHeight = `calc(100vh - ${fixedHeight + 140}px)`;
        templateItemsScrollArea.style.maxHeight = newTemplateListMaxHeight;
        templateItemsScrollArea.style.height = 'auto';

        managePanel.style.display = originalDisplay;
    }

    // --- イベントハンドラの定義 ---

    // TextFillボタンクリック処理
    if (ENABLE_TEXTFILL_INTEGRATION) {
        triggerTextFillButton.onclick = () => {
            isButtonTriggeredTextFill = true;
            themeTagInput.value = '/';
            themeTagInput.dispatchEvent(new Event('input', { bubbles: true }));
            themeTagInput.dispatchEvent(new Event('change', { bubbles: true }));

            console.log('TextFillメニューをトリガーしました。手動で選択してください。');
            themeTagInput.focus();

            const tempInputListener = () => {
                if (themeTagInput.value.length > 1 && themeTagInput.value.trim() !== '/') {
                    themeTagInput.removeEventListener('input', tempInputListener);
                    console.log('TextFillがタグの展開を開始しました。自動確定を待機します。');
                    setTimeout(() => {
                        if (!isProcessing) {
                            processTagsAutomatically();
                        }
                        isButtonTriggeredTextFill = false;
                    }, 1000);
                }
            };
            themeTagInput.addEventListener('input', tempInputListener);

            setTimeout(() => {
                if (isButtonTriggeredTextFill && themeTagInput.value.trim() === '/') {
                    console.log("TextFillが使用されなかったため、ボタントリガーフラグをリセットします。");
                    isButtonTriggeredTextFill = false;
                    themeTagInput.removeEventListener('input', tempInputListener);
                }
            }, 5000);
        };
    }

    // タグ管理ボタンクリックでパネル表示/非表示
    if (ENABLE_IN_SCRIPT_TAG_MANAGEMENT) {
        manageTemplatesButton.onclick = () => {
            const isPanelVisible = managePanel.style.display === 'flex';
            managePanel.style.display = isPanelVisible ? 'none' : 'flex';

            if (!isPanelVisible) {
                addFormContainer.style.display = 'none';
                addTagButton.textContent = 'タグ追加';
                // GM_configから最新のデータをロードしてからリストをレンダリング
                loadTemplatesFromGMConfig();
                renderTemplateList();
                resetAddForm();
                adjustPanelHeight();
            }
        };

        function handleFilterInput() {
            if (!isComposing) {
                renderTemplateList();
            }
        }

        function compositionStartHandler() {
            isComposing = true;
        }

        function compositionEndHandler() {
            isComposing = false;
            renderTemplateList();
        }

        filterInput.addEventListener('input', handleFilterInput);
        filterInput.addEventListener('compositionstart', compositionStartHandler);
        filterInput.addEventListener('compositionend', compositionEndHandler);

        // ヘルパー関数:クリックされた要素が特定のコントロールボタンかどうかを判定
        function isControlButtonClicked(targetElement) {
            // 共通のコントロール
            let isCommonControl = targetElement.closest('.save-in-line-edit-btn') ||
                                  targetElement.closest('.cancel-in-line-edit-btn') ||
                                  targetElement.closest('.delete-template-btn') ||
                                  targetElement.closest('#exportTagsButton') ||
                                  targetElement.closest('#importTagsButton') ||
                                  targetElement.closest('.apply-template-btn');

            if (isCommonControl) return true;

            // サイト固有の追加ボタン
            if (hostname.includes('iromirai.jp')) {
                return targetElement.closest('.postmedia-mobile-addtag-button');
            } else if (hostname.includes('chichi-pui.com')) {
                return targetElement.closest('button.button.is-dark.ml-2[type="button"]');
            }
            return false;
        }

        // ドキュメント全体へのクリックリスナー
        document.addEventListener('click', (event) => {
            if (managePanel.style.display === 'flex' &&
                !managePanel.contains(event.target) &&
                event.target !== manageTemplatesButton &&
                event.target !== addTagButton &&
                !addFormContainer.contains(event.target) &&
                !isControlButtonClicked(event.target)) {
                managePanel.style.display = 'none';
            }
        });
    }

    // --- テンプレート一覧をレンダリングする関数 ---
    let draggedItem = null;
    function renderTemplateList() {
        if (!ENABLE_IN_SCRIPT_TAG_MANAGEMENT) return;

        let wasFilterInputFocused = (document.activeElement === filterInput);
        let selectionStart = 0;
        let selectionEnd = 0;

        if (wasFilterInputFocused) {
            selectionStart = filterInput.selectionStart;
            selectionEnd = filterInput.selectionEnd;
        }

        templateItemsScrollArea.innerHTML = '';

        if (wasFilterInputFocused) {
            filterInput.focus();
            filterInput.setSelectionRange(selectionStart, selectionEnd);
        }

        const filterText = filterInput.value;
        const lowerCaseFilter = filterText.toLowerCase();

        const filteredTemplates = currentTemplates.filter(template => {
            return template.name.toLowerCase().includes(lowerCaseFilter) ||
                   template.tags.toLowerCase().includes(lowerCaseFilter);
        });
        if (filteredTemplates.length === 0) {
            const noMatchP = document.createElement('p');
            noMatchP.textContent = '条件に一致するテーマタグが見つかりません。';
            templateItemsScrollArea.appendChild(noMatchP);
            if (currentTemplates.length === 0 && !filterText) {
                const addPromptP = document.createElement('p');
                addPromptP.textContent = '以下の「タグ追加」ボタンから追加してください。';
                templateItemsScrollArea.appendChild(addPromptP);
            }
        } else {
            const ul = document.createElement('ul');
            ul.style.cssText = 'list-style:none; padding:0;';
            filteredTemplates.forEach((template, index) => {
                const originalIndex = currentTemplates.findIndex(t => t.name === template.name && t.tags === template.tags);
                if (originalIndex === -1) return;

                const li = document.createElement('li');
                li.setAttribute('draggable', 'true');
                li.dataset.index = originalIndex;
                li.style.cssText = `
                    margin-bottom:8px; padding:8px; border:1px solid #eee; border-radius:5px;
                    display:flex; flex-direction:column; gap:5px; background-color: #f9f9f9;
                    cursor: grab;
                `;

                const nameDisplay = document.createElement('div');
                nameDisplay.className = 'template-name-display';
                nameDisplay.style.fontWeight = 'bold';
                nameDisplay.textContent = template.name;

                const nameInput = document.createElement('input');
                nameInput.type = 'text';
                nameInput.className = 'template-name-input';
                nameInput.style.cssText = 'width:100%; padding:5px; margin-bottom:5px; display:none;';
                nameInput.value = template.name;

                const tagsDisplay = document.createElement('div');
                tagsDisplay.className = 'template-tags-display';
                tagsDisplay.style.cssText = 'font-size:0.9em; color:#555; display:none;';
                tagsDisplay.textContent = template.tags;

                const tagsInput = document.createElement('input');
                tagsInput.type = 'text';
                tagsInput.className = 'template-tags-input';
                tagsInput.style.cssText = 'width:100%; padding:5px; margin-bottom:10px; display:none;';
                tagsInput.value = template.tags;

                const buttonContainer = document.createElement('div');
                buttonContainer.style.textAlign = 'right';

                const applyButton = document.createElement('button');
                applyButton.textContent = '適用';
                applyButton.className = 'apply-template-btn';
                applyButton.style.cssText = 'padding:5px 10px; background-color:#28a745; color:white; border:none; border-radius:3px; cursor:pointer; font-size:0.8em; margin-right:5px;';
                const editButton = document.createElement('button');
                editButton.textContent = '編集';
                editButton.className = 'edit-template-btn';
                editButton.style.cssText = 'padding:5px 10px; background-color:#0275d8; color:white; border:none; border-radius:3px; cursor:pointer; font-size:0.8em;';
                const saveButton = document.createElement('button');
                saveButton.textContent = '保存';
                saveButton.className = 'save-in-line-edit-btn';
                saveButton.style.cssText = 'padding:5px 10px; background-color:#5cb85c; color:white; border:none; border-radius:3px; cursor:pointer; font-size:0.8em; margin-right:5px; display:none;';

                const cancelButton = document.createElement('button');
                cancelButton.textContent = 'キャンセル';
                cancelButton.className = 'cancel-in-line-edit-btn';
                cancelButton.style.cssText = 'padding:5px 10px; background-color:#f0ad4e; color:white; border:none; border-radius:3px; cursor:pointer; font-size:0.8em; margin-right:5px; display:none;';


                const deleteButton = document.createElement('button');
                deleteButton.textContent = '削除';
                deleteButton.className = 'delete-template-btn';
                deleteButton.style.cssText = 'padding:5px 10px; background-color:#d9534f; color:white; border:none; border-radius:3px; cursor:pointer; font-size:0.8em; margin-left:5px;';

                li.appendChild(nameDisplay);
                li.appendChild(nameInput);
                li.appendChild(tagsDisplay);
                li.appendChild(tagsInput);
                buttonContainer.appendChild(applyButton);
                buttonContainer.appendChild(editButton);
                buttonContainer.appendChild(saveButton);
                buttonContainer.appendChild(cancelButton);
                buttonContainer.appendChild(deleteButton);
                li.appendChild(buttonContainer);

                ul.appendChild(li);
            });
            templateItemsScrollArea.appendChild(ul);
            ul.querySelectorAll('.apply-template-btn').forEach(button => {
                button.onclick = (e) => applyTemplate(parseInt(e.target.closest('li').dataset.index));
            });
            ul.querySelectorAll('.delete-template-btn').forEach(button => {
                button.onclick = (e) => deleteTemplate(parseInt(e.target.closest('li').dataset.index));
            });
            ul.querySelectorAll('.edit-template-btn').forEach(button => {
                button.onclick = (e) => {
                    const li = e.target.closest('li');
                    const index = parseInt(li.dataset.index);
                    const template = currentTemplates[index];

                    li.querySelector('.template-name-display').style.display = 'none';
                    li.querySelector('.template-tags-display').style.display = 'none';
                    li.querySelector('.template-name-input').style.display = 'block';
                    li.querySelector('.template-tags-input').style.display = 'block';

                    li.querySelector('.template-name-input').value = template.name;
                    li.querySelector('.template-tags-input').value = template.tags;

                    li.querySelector('.apply-template-btn').style.display = 'none';
                    li.querySelector('.edit-template-btn').style.display = 'none';
                    li.querySelector('.delete-template-btn').style.display = 'none';
                    li.querySelector('.save-in-line-edit-btn').style.display = 'inline-block';
                    li.querySelector('.cancel-in-line-edit-btn').style.display = 'inline-block';

                    li.querySelector('.template-name-input').focus();
                };
            });
            ul.querySelectorAll('.save-in-line-edit-btn').forEach(button => {
                button.onclick = (e) => {
                    const li = e.target.closest('li');
                    const index = parseInt(li.dataset.index);
                    const newName = li.querySelector('.template-name-input').value.trim();
                    const newTags = li.querySelector('.template-tags-input').value.trim();

                    if (!newName || !newTags) {
                        alert('テーマタグ名とタグを入力してください。');
                        return;
                    }

                    if (currentTemplates.some((t, i) => i !== index && t.name === newName)) {
                        alert('同じテーマタグ名が既に存在します。別の名前を入力してください。');
                        return;
                    }

                    currentTemplates[index] = { name: newName, tags: newTags };
                    saveTemplatesToGMConfig();
                    renderTemplateList();
                };
            });
            ul.querySelectorAll('.cancel-in-line-edit-btn').forEach(button => {
                button.onclick = (e) => {
                    renderTemplateList();
                };
            });
            ul.querySelectorAll('li').forEach(item => {
                item.addEventListener('dragstart', (e) => {
                    if (item.querySelector('.template-name-input').style.display === 'block') {
                        e.preventDefault();
                        return;
                    }
                    draggedItem = item;
                    e.dataTransfer.effectAllowed = 'move';
                    e.dataTransfer.setData('text/html', item.innerHTML);
                    setTimeout(() => {
                        item.style.opacity = '0.5';
                    }, 0);
                });

                item.addEventListener('dragover', (e) => {
                    e.preventDefault();
                    if (item !== draggedItem) {
                        const bounding = item.getBoundingClientRect();
                        const offset = bounding.y + (bounding.height / 2);
                        if (e.clientY > offset) {
                            item.style.borderBottom = '2px solid blue';
                            item.style.borderTop = '';
                        } else {
                            item.style.borderTop = '2px solid blue';
                            item.style.borderBottom = '';
                        }
                    }
                });
                item.addEventListener('dragleave', () => {
                    item.style.borderTop = '';
                    item.style.borderBottom = '';
                });
                item.addEventListener('drop', (e) => {
                    e.preventDefault();
                    if (item !== draggedItem) {
                        const draggedIndex = parseInt(draggedItem.dataset.index);
                        const targetIndex = parseInt(item.dataset.index);

                        const [removed] = currentTemplates.splice(draggedIndex, 1);
                        const bounding = item.getBoundingClientRect();
                        const offset = bounding.y + (bounding.height / 2);

                        let insertIndex;
                        if (e.clientY > offset) {
                            insertIndex = targetIndex + (draggedIndex < targetIndex ? 0 : 1);
                        } else {
                            insertIndex = targetIndex + (draggedIndex < targetIndex ? 0 : 0);
                        }
                        currentTemplates.splice(insertIndex, 0, removed);
                        saveTemplatesToGMConfig();
                        renderTemplateList();
                    }
                });
                item.addEventListener('dragend', () => {
                    ul.querySelectorAll('li').forEach(li => {
                        li.style.opacity = '1';
                        li.style.borderTop = '';
                        li.style.borderBottom = '';
                    });
                    draggedItem = null;
                });
            });
        }
        adjustPanelHeight();
    }

    // --- テンプレートの適用関数 ---
    function applyTemplate(index) {
        if (!ENABLE_IN_SCRIPT_TAG_MANAGEMENT) return;
        const template = currentTemplates[index];
        themeTagInput.value = template.tags;
        if (!isProcessing) {
            processTagsAutomatically();
        }
    }

    // --- 新規テンプレートの追加関数 ---
    if (ENABLE_IN_SCRIPT_TAG_MANAGEMENT) {
        addTemplateButton.onclick = () => {
            const name = addTemplateNameInput.value.trim();
            const tags = addTemplateTagsInput.value.trim();

            if (!name || !tags) {
                alert('テーマタグ名とタグを入力してください。');
                return;
            }

            if (currentTemplates.some(t => t.name === name)) {
                alert('同じテーマタグ名が既に存在します。別の名前を入力してください。');
                return;
            }

            currentTemplates.push({ name, tags });
            console.log("新規テーマタグを追加しました:", { name, tags });

            saveTemplatesToGMConfig();
            renderTemplateList();
            resetAddForm();

            const lastItem = templateItemsScrollArea.querySelector('ul > li:last-child');
            if (lastItem) {
                lastItem.scrollIntoView({ behavior: 'smooth', block: 'end' });
            }
        };
    }

    // --- 追加フォームをリセットする関数 ---
    function resetAddForm() {
        if (!ENABLE_IN_SCRIPT_TAG_MANAGEMENT) return;
        addTemplateNameInput.value = '';
        addTemplateTagsInput.value = '';
    }

    // --- テンプレートを削除する関数 ---
    function deleteTemplate(index) {
        if (!ENABLE_IN_SCRIPT_TAG_MANAGEMENT) return;
        if (confirm(`「${currentTemplates[index].name}」を削除してもよろしいですか?`)) {
            currentTemplates.splice(index, 1);
            saveTemplatesToGMConfig();
            renderTemplateList();
            console.log("テーマタグを削除しました。");
            adjustPanelHeight();
        }
    }

    // --- エクスポート機能 ---
    if (ENABLE_IN_SCRIPT_TAG_MANAGEMENT) {
        exportTagsButton.onclick = () => {
            const dataStr = JSON.stringify(currentTemplates, null, 2);
            const blob = new Blob([dataStr], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = exportFileName;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        };
    }

    // --- その他のイベントリスナー ---

    // themeTagInput の change イベントを監視 (手動入力や、TextFillがchangeを発火した場合用)
    themeTagInput.addEventListener('change', () => {
        if (isButtonTriggeredTextFill) {
            console.log("Changeイベント: ボタンからのTextFillトリガー中のため、changeイベントの自動確定はスキップします。");
            return;
        }

        if (changeTimer) clearTimeout(changeTimer);

        changeTimer = setTimeout(() => {
            if (themeTagInput.value.trim() === '/' || themeTagInput.value.trim() === '') {
                 console.log("Changeイベント: TextFillトリガー文字または空値が検出されました。自動確定はスキップします.");
                 return;
            }
            if (!isProcessing) {
                processTagsAutomatically();
            }
        }, 100);
    });

    // ページロード時に既に値が入っている場合(TextFillが自動で展開するなど)
    if (themeTagInput.value.trim() !== '') {
        setTimeout(() => {
            if (themeTagInput.value.trim() !== '/') {
                if (!isProcessing) {
                    processTagsAutomatically();
                }
            }
        }, 500);
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址