Greasy Fork is available in English.

Sonauto Multi-Delete

Multi-select and bulk delete songs on Sonauto editor

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Sonauto Multi-Delete
// @namespace    https://greasyfork.org/en/users/10118-drhouse
// @version      1.3
// @description  Multi-select and bulk delete songs on Sonauto editor
// @match        https://sonauto.ai/*
// @grant        none
// @require      https://code.jquery.com/jquery-3.7.1.min.js
// @author       drhouse
// @license      CC-BY-NC-SA-4.0
// @icon https://www.google.com/s2/favicons?sz=64&domain=sonauto.ai
// ==/UserScript==
(function () {
    'use strict';
    const $ = jQuery.noConflict(true);
    // --- Simulate a real click (required for Radix UI components) ---
    function realClick(el) {
        const rect = el.getBoundingClientRect();
        const x = rect.left + rect.width / 2;
        const y = rect.top + rect.height / 2;
        const opts = { bubbles: true, cancelable: true, clientX: x, clientY: y, pointerId: 1 };
        el.dispatchEvent(new PointerEvent('pointerdown', opts));
        el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: x, clientY: y }));
        el.dispatchEvent(new PointerEvent('pointerup', opts));
        el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, clientX: x, clientY: y }));
        el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, clientX: x, clientY: y }));
    }
    // --- Resilient selectors (ID can be #editor-track-list OR #editor-track-list-and-details) ---
    function getTrackListRoot() {
        return document.querySelector('[id^="editor-track-list"]');
    }
    function getTrackContainer() {
        const root = getTrackListRoot();
        if (!root) return null;
        // Find the visible section with song rows
        const sections = root.querySelectorAll('section.contents');
        for (const s of sections) {
            const gc = s.querySelector('div.flex.flex-col.gap-2');
            if (gc && gc.children.length > 0 && gc.querySelector('h3')) {
                return gc;
            }
        }
        return null;
    }
    // --- Styles ---
    const style = document.createElement('style');
    style.textContent = `
        .sm-checkbox {
            width: 18px;
            height: 18px;
            min-width: 18px;
            accent-color: #ef4444;
            cursor: pointer;
            margin-right: 6px;
            z-index: 10;
            position: relative;
        }
        .sm-toolbar {
            position: fixed;
            bottom: 80px;
            right: 30px;
            z-index: 99999;
            display: flex;
            gap: 8px;
            align-items: center;
            background: #1a1a2e;
            border: 1px solid #333;
            border-radius: 12px;
            padding: 8px 14px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.5);
            font-family: system-ui, sans-serif;
        }
        .sm-toolbar button {
            padding: 6px 14px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 13px;
            font-weight: 600;
            transition: opacity 0.15s;
        }
        .sm-toolbar button:hover { opacity: 0.85; }
        .sm-delete-btn {
            background: #ef4444;
            color: white;
        }
        .sm-delete-btn:disabled {
            background: #555;
            cursor: not-allowed;
            opacity: 0.5 !important;
        }
        .sm-select-all-btn {
            background: #3b82f6;
            color: white;
        }
        .sm-deselect-btn {
            background: #555;
            color: #ccc;
        }
        .sm-count {
            color: #ccc;
            font-size: 13px;
            min-width: 80px;
            text-align: center;
        }
        .sm-progress {
            color: #facc15;
            font-size: 12px;
        }
        .sm-row-checked {
            outline: 2px solid #ef4444 !important;
            outline-offset: -2px;
        }
    `;
    document.head.appendChild(style);
    // --- Toolbar ---
    const $toolbar = $(`
        <div class="sm-toolbar">
            <button class="sm-select-all-btn" id="sm-select-all">Select All</button>
            <button class="sm-deselect-btn" id="sm-deselect">Deselect All</button>
            <span class="sm-count" id="sm-count">0 selected</span>
            <button class="sm-delete-btn" id="sm-delete-selected" disabled>Delete Selected</button>
            <span class="sm-progress" id="sm-progress"></span>
        </div>
    `);
    $('body').append($toolbar);
    // --- Helpers ---
    function getSongRows() {
        const container = getTrackContainer();
        return container ? Array.from(container.children) : [];
    }
    function updateCount() {
        const checked = $('input.sm-checkbox:checked').length;
        $('#sm-count').text(checked + ' selected');
        $('#sm-delete-selected').prop('disabled', checked === 0);
    }
    // --- Inject checkboxes ---
    function injectCheckboxes() {
        const rows = getSongRows();
        rows.forEach((row) => {
            if (row.querySelector('.sm-checkbox')) return;
            const h3 = row.querySelector('h3');
            if (!h3) return;
            const cb = document.createElement('input');
            cb.type = 'checkbox';
            cb.className = 'sm-checkbox';
            cb.addEventListener('change', function () {
                const container = this.closest('div.flex.flex-col.gap-2 > div');
                if (container) {
                    container.classList.toggle('sm-row-checked', this.checked);
                }
                updateCount();
            });
            const flexGrow = h3.closest('.flex-grow');
            if (flexGrow) {
                flexGrow.parentElement.insertBefore(cb, flexGrow);
            } else {
                const titleContainer = h3.parentElement;
                titleContainer.insertBefore(cb, titleContainer.firstChild);
            }
        });
    }
    // --- Delete logic ---
    function sleep(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
    }
    function findEllipsisButton(row) {
        const buttons = row.querySelectorAll('button');
        for (const btn of buttons) {
            const svg = btn.querySelector('svg.lucide-ellipsis-vertical');
            if (svg && btn.parentElement && btn.parentElement.className.includes('@lg:contents')) {
                return btn;
            }
        }
        for (const btn of buttons) {
            if (btn.querySelector('svg.lucide-ellipsis-vertical')) {
                return btn;
            }
        }
        return null;
    }
    function findDeleteMenuItem() {
        const menuItems = document.querySelectorAll('[role="menuitem"]');
        for (const item of menuItems) {
            if (item.textContent.trim() === 'Delete' && item.classList.contains('text-red-500')) {
                return item;
            }
        }
        return null;
    }
    function findDeleteConfirmButton() {
        const dialog = document.querySelector('[role="dialog"]');
        if (!dialog) return null;
        const buttons = dialog.querySelectorAll('button');
        for (const btn of buttons) {
            if (btn.textContent.trim() === 'Delete') {
                return btn;
            }
        }
        return null;
    }
    async function waitForElement(finder, timeout = 3000) {
        const start = Date.now();
        while (Date.now() - start < timeout) {
            const el = finder();
            if (el) return el;
            await sleep(100);
        }
        return null;
    }
    async function waitForElementGone(finder, timeout = 3000) {
        const start = Date.now();
        while (Date.now() - start < timeout) {
            const el = finder();
            if (!el) return true;
            await sleep(100);
        }
        return false;
    }
    async function deleteSong(row) {
        row.scrollIntoView({ behavior: 'instant', block: 'center' });
        await sleep(200);
        const ellipsisBtn = findEllipsisButton(row);
        if (!ellipsisBtn) {
            console.warn('SM: No ellipsis button found');
            return false;
        }
        realClick(ellipsisBtn);
        const deleteMenuItem = await waitForElement(findDeleteMenuItem, 2000);
        if (!deleteMenuItem) {
            console.warn('SM: No Delete menu item appeared');
            document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
            return false;
        }
        await sleep(150);
        realClick(deleteMenuItem);
        const confirmBtn = await waitForElement(findDeleteConfirmButton, 2000);
        if (!confirmBtn) {
            console.warn('SM: No confirmation dialog appeared');
            return false;
        }
        await sleep(150);
        realClick(confirmBtn);
        // Wait for dialog to close (confirms deletion went through)
        await waitForElementGone(() => document.querySelector('[role="dialog"]'), 3000);
        await sleep(400);
        return true;
    }
    async function deleteSelected() {
        const checked = document.querySelectorAll('input.sm-checkbox:checked');
        if (checked.length === 0) return;
        const total = checked.length;
        const confirmMsg = `Are you sure you want to delete ${total} song(s)? This cannot be undone.`;
        if (!confirm(confirmMsg)) return;
        $('#sm-delete-selected').prop('disabled', true);
        $('#sm-select-all').prop('disabled', true);
        let deleted = 0;
        let failed = 0;
        for (let i = 0; i < total; i++) {
            const remaining = document.querySelector('input.sm-checkbox:checked');
            if (!remaining) break;
            const row = remaining.closest('div.flex.flex-col.gap-2 > div');
            if (!row) {
                remaining.checked = false;
                failed++;
                continue;
            }
            const title = row.querySelector('h3')?.textContent || 'Unknown';
            $('#sm-progress').text(`Deleting ${i + 1}/${total}: ${title}...`);
            const success = await deleteSong(row);
            if (success) {
                deleted++;
            } else {
                failed++;
                remaining.checked = false;
            }
            await sleep(300);
        }
        $('#sm-progress').text(`Done! Deleted ${deleted}${failed > 0 ? `, ${failed} failed` : ''}`);
        setTimeout(() => $('#sm-progress').text(''), 4000);
        $('#sm-select-all').prop('disabled', false);
        updateCount();
    }
    // --- Event handlers ---
    $('#sm-select-all').on('click', function () {
        $('input.sm-checkbox').prop('checked', true).each(function () {
            $(this).closest('div.flex.flex-col.gap-2 > div').addClass('sm-row-checked');
        });
        updateCount();
    });
    $('#sm-deselect').on('click', function () {
        $('input.sm-checkbox').prop('checked', false).each(function () {
            $(this).closest('div.flex.flex-col.gap-2 > div').removeClass('sm-row-checked');
        });
        updateCount();
    });
    $('#sm-delete-selected').on('click', function () {
        deleteSelected();
    });
    // --- MutationObserver to inject checkboxes on new/changed rows ---
    const observer = new MutationObserver(() => {
        injectCheckboxes();
    });
    function startObserving() {
        const container = getTrackContainer();
        if (container) {
            observer.observe(container, { childList: true, subtree: true });
            injectCheckboxes();
        } else {
            setTimeout(startObserving, 1000);
        }
    }
    // Observe the whole body for structural changes (panel open/close changes the ID)
    const bodyObserver = new MutationObserver(() => {
        const container = getTrackContainer();
        if (container) {
            injectCheckboxes();
            observer.disconnect();
            observer.observe(container, { childList: true, subtree: true });
        }
    });
    bodyObserver.observe(document.body, { childList: true, subtree: true });
    startObserving();
    setInterval(injectCheckboxes, 3000);
    console.log('Sonauto Multi-Delete v1.2 loaded!');
})();