Torn Display Case • ABC / Export / Import

Adds ABC, Export, Import buttons to the Display Case manage page next to "Save changes". Never auto-saves or auto-sorts on load.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Torn Display Case • ABC / Export / Import
// @namespace    https://greasyfork.org/en/users/2842410-killercleat
// @version      11.13.2025.22.30
// @description  Adds ABC, Export, Import buttons to the Display Case manage page next to "Save changes". Never auto-saves or auto-sorts on load.
// @author       KillerCleat [2842410]
// @match        https://www.torn.com/displaycase.php*
// @grant        none
// @homepageURL  https://greasyfork.org/en/scripts/555791-torn-display-case-abc-export-import
// @supportURL   https://greasyfork.org/en/scripts/555791-torn-display-case-abc-export-import/feedback
// ==/UserScript==

/*
===============================================================================
NOTES & REQUIREMENTS
-------------------------------------------------------------------------------
Script:   Torn Display Case • ABC / Export / Import
Author:   KillerCleat [2842410]
Version:  11.13.2025.22.30

Purpose:
- On the Display Case manage page (#manage), add three buttons next to
  "SAVE CHANGES":
    [SAVE CHANGES] [ABC] [Export] [Import]  Undo changes
- ABC:   Sorts all items alphabetically by name on screen ONLY.
- Export:Downloads current on-screen item order to a .txt file.
- Import:Reads a previously exported .txt/.json file and reorders items
         on screen to match it.

Important:
- The script NEVER clicks "SAVE CHANGES" or submits any form.
- Torn will only remember the new order if YOU manually press "SAVE CHANGES".
- The script does NOT auto-sort or auto-apply anything on page load.

Requirements:
- Tampermonkey (or compatible userscript manager).
- Torn Display Case "Manage your Display Case" view (#manage).

Behavior:
- Only affects the Display Case manage view.
- No API calls.
- Uses ES6 JavaScript.
- Status text uses Torn-style time: 24-hour clock with "TCT".

Rules Followed:
- Full metadata header included.
- NOTES & REQUIREMENTS with version and author.
- No existing Torn commands are changed.
- Clean, documented ES6 code.
- No emojis or special characters.
===============================================================================
*/

(function () {
    "use strict";

    /** Get Torn-style time string "HH:MM TCT". */
    function getTornStyleTime() {
        const selectors = [
            "#bar-time",
            "#tct-time",
            "#clock",
            ".time",
            ".header .time"
        ];

        for (const sel of selectors) {
            const el = document.querySelector(sel);
            if (el) {
                const text = el.textContent.trim();
                const match = text.match(/\b\d{1,2}:\d{2}(?::\d{2})?\b/);
                if (match) {
                    const parts = match[0].split(":");
                    const hh = parts[0].padStart(2, "0");
                    const mm = parts[1].padStart(2, "0");
                    return hh + ":" + mm + " TCT";
                }
            }
        }

        const now = new Date();
        const hh = String(now.getHours()).padStart(2, "0");
        const mm = String(now.getMinutes()).padStart(2, "0");
        return hh + ":" + mm + " TCT";
    }

    /** Find the "SAVE CHANGES" button so we can attach our buttons next to it. */
    function findSaveButton() {
        const buttons = Array.from(document.querySelectorAll("button, input[type='button'], input[type='submit']"));
        for (const btn of buttons) {
            const text = (btn.value || btn.textContent || "").trim().toLowerCase();
            if (text === "save changes") {
                return btn;
            }
        }
        return null;
    }

    /** Get all display item rows and build an array of {el, name}. */
    function getItemRows() {
        const nameBlocks = Array.from(document.querySelectorAll(".name.flex .desc .bold"));
        if (nameBlocks.length === 0) {
            return { parent: null, rows: [] };
        }

        const rows = [];
        nameBlocks.forEach(span => {
            const row = span.closest("li") || span.closest(".name.flex");
            if (row && !rows.includes(row)) {
                rows.push(row);
            }
        });

        const parent = rows.length > 0 ? rows[0].parentElement : null;
        return { parent, rows };
    }

    /** Sort items alphabetically A–Z on screen only. */
    function sortItemsABC() {
        const { parent, rows } = getItemRows();
        if (!parent || rows.length === 0) {
            console.warn("KC DisplayCase ABC: No items found to sort.");
            return false;
        }

        const sortable = rows.map(el => {
            const nameEl = el.querySelector(".desc .bold");
            const name = nameEl ? nameEl.textContent.trim().toLowerCase() : "";
            return { el, name };
        });

        sortable.sort((a, b) => a.name.localeCompare(b.name));

        sortable.forEach(item => parent.appendChild(item.el));
        return true;
    }

    /** Export current visible order to a downloadable file. */
    function exportOrderToFile() {
        const { rows } = getItemRows();
        if (rows.length === 0) {
            console.warn("KC DisplayCase Export: No items found.");
            return false;
        }

        const names = rows.map(el => {
            const nameEl = el.querySelector(".desc .bold");
            return nameEl ? nameEl.textContent.trim() : "";
        });

        const dataStr = JSON.stringify(names, null, 0);

        const blob = new Blob([dataStr], { type: "text/plain;charset=utf-8" });
        const url = URL.createObjectURL(blob);

        const now = new Date();
        const stamp =
            String(now.getFullYear()) +
            String(now.getMonth() + 1).padStart(2, "0") +
            String(now.getDate()).padStart(2, "0") +
            "_" +
            String(now.getHours()).padStart(2, "0") +
            String(now.getMinutes()).padStart(2, "0");

        const a = document.createElement("a");
        a.href = url;
        a.download = "displaycase_order_" + stamp + ".txt";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);

        return true;
    }

    /** Import order from a selected file and reorder items on screen. */
    function importOrderFromFile(file, onDone) {
        const reader = new FileReader();
        reader.onload = function (e) {
            try {
                const text = String(e.target.result || "").trim();
                if (!text) {
                    console.warn("KC DisplayCase Import: File is empty.");
                    onDone(false);
                    return;
                }

                let list;
                try {
                    list = JSON.parse(text);
                } catch (err) {
                    // Allow comma-separated plain text as fallback
                    list = text.split(",").map(s => s.trim()).filter(Boolean);
                }

                if (!Array.isArray(list) || list.length === 0) {
                    console.warn("KC DisplayCase Import: Parsed list is empty.");
                    onDone(false);
                    return;
                }

                const { parent, rows } = getItemRows();
                if (!parent || rows.length === 0) {
                    console.warn("KC DisplayCase Import: No display rows found.");
                    onDone(false);
                    return;
                }

                // Map current rows by exact name text
                const nameToRows = new Map();
                rows.forEach(el => {
                    const nameEl = el.querySelector(".desc .bold");
                    const name = nameEl ? nameEl.textContent.trim() : "";
                    if (!nameToRows.has(name)) {
                        nameToRows.set(name, []);
                    }
                    nameToRows.get(name).push(el);
                });

                const used = new Set();

                // First, append rows in the order from the imported list
                list.forEach(importName => {
                    const arr = nameToRows.get(importName);
                    if (arr && arr.length > 0) {
                        const row = arr.shift();
                        used.add(row);
                        parent.appendChild(row);
                    }
                });

                // Then append any remaining rows (not present in the file)
                rows.forEach(row => {
                    if (!used.has(row)) {
                        parent.appendChild(row);
                    }
                });

                onDone(true);
            } catch (err) {
                console.error("KC DisplayCase Import error:", err);
                onDone(false);
            }
        };

        reader.onerror = function () {
            console.error("KC DisplayCase Import: File read error.");
            onDone(false);
        };

        reader.readAsText(file);
    }

    /** Insert the ABC / Export / Import buttons next to SAVE CHANGES. */
    function insertButtons() {
        const url = window.location.href;
        if (!url.includes("/displaycase.php") || !url.includes("#manage")) {
            return;
        }

        // Avoid duplicates
        if (document.querySelector(".kc-dc-abc-btn")) {
            return;
        }

        const saveBtn = findSaveButton();
        if (!saveBtn || !saveBtn.parentNode) {
            return;
        }

        const parent = saveBtn.parentNode;

        // Status text
        const statusSpan = document.createElement("span");
        statusSpan.className = "kc-dc-status";
        statusSpan.style.marginLeft = "8px";
        statusSpan.style.fontSize = "11px";
        statusSpan.style.opacity = "0.8";

        function setStatus(msg) {
            statusSpan.textContent = msg + " at " + getTornStyleTime() + " (not saved)";
        }

        // Helper to clone save button style
        function makeButton(label, extraClass) {
            const btn = document.createElement("button");
            btn.type = "button";
            btn.textContent = label;
            btn.className = (saveBtn.className || "") + " " + extraClass;
            btn.style.marginLeft = "8px";
            return btn;
        }

        const abcBtn = makeButton("ABC", "kc-dc-abc-btn");
        const exportBtn = makeButton("Export", "kc-dc-export-btn");
        const importBtn = makeButton("Import", "kc-dc-import-btn");

        // Put them directly after SAVE CHANGES, in this order: ABC, Export, Import
        parent.insertBefore(abcBtn, saveBtn.nextSibling);
        parent.insertBefore(exportBtn, abcBtn.nextSibling);
        parent.insertBefore(importBtn, exportBtn.nextSibling);
        parent.insertBefore(statusSpan, importBtn.nextSibling);

        // Hidden file input for Import
        const fileInput = document.createElement("input");
        fileInput.type = "file";
        fileInput.accept = ".txt,.json";
        fileInput.style.display = "none";
        document.body.appendChild(fileInput);

        // Button handlers — NONE of these save anything to Torn.
        abcBtn.addEventListener("click", () => {
            const ok = sortItemsABC();
            if (ok) setStatus("ABC sort applied");
        });

        exportBtn.addEventListener("click", () => {
            const ok = exportOrderToFile();
            if (ok) setStatus("Order exported");
        });

        importBtn.addEventListener("click", () => {
            fileInput.value = "";
            fileInput.click();
        });

        fileInput.addEventListener("change", () => {
            const file = fileInput.files && fileInput.files[0];
            if (!file) return;
            importOrderFromFile(file, success => {
                if (success) {
                    setStatus("Order imported");
                } else {
                    statusSpan.textContent = "Import failed (not saved)";
                }
            });
        });
    }

    /** Observe DOM so we can attach when the manage section appears. */
    function setupObserver() {
        const observer = new MutationObserver(() => {
            insertButtons();
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        insertButtons();
    }

    setupObserver();
})();