Torn Diary

Private Torn diary for players, wanted items, notes, reminders and events.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Torn Diary
// @namespace    https://www.torn.com/
// @version      1.1.1
// @description  Private Torn diary for players, wanted items, notes, reminders and events.
// @author       ST4TIC
// @match        https://www.torn.com/profiles.php*
// @grant        none
// @run-at       document-idle
// @license      All Rights Reserved
// ==/UserScript==

(() => {
    'use strict';

    const TD = {
        APP_ID: 'tdiary-app',
        LAUNCHER_ID: 'tdiary-launcher',
        STYLE_ID: 'tdiary-styles',
        STORAGE_KEY: 'tornDiary_v1_entries',
        SETTINGS_KEY: 'tornDiary_v1_settings',
        LAUNCHER_POSITION_KEY: 'tornDiary_v1_launcherPosition',
        LOAD_DELAY: 2000
    };

    const TABS = {
        players: {
            label: 'Players',
            icon: '👤',
            type: 'player'
        },
        items: {
            label: 'Items Wanted',
            icon: '📦',
            type: 'item'
        },
        notes: {
            label: 'Notes',
            icon: '📝',
            type: 'note'
        },
        reminders: {
            label: 'Reminders',
            icon: '🔔',
            type: 'reminder'
        },
        faction: {
            label: 'Faction Events',
            icon: '🛡️',
            type: 'faction'
        },
        torn: {
            label: 'Torn Events',
            icon: '📅',
            type: 'torn'
        },
        settings: {
            label: 'Settings',
            icon: '⚙️',
            type: null
        }
    };

    const DEFAULT_SETTINGS = {
        startTab: 'players',
        dueSoonHours: 24,
        apiKey: ''
    };

    const state = {
        entries: [],
        settings: { ...DEFAULT_SETTINGS },
        activeTab: 'players',
        selectedId: null,
        search: '',
        timer: null
    };

    function createId() {
        return `${Date.now().toString(36)}-${Math.random()
            .toString(36)
            .slice(2, 10)}`;
    }

    function nowISO() {
        return new Date().toISOString();
    }

    function escapeHTML(value = '') {
        return String(value)
            .replaceAll('&', '&')
            .replaceAll('<', '&lt;')
            .replaceAll('>', '&gt;')
            .replaceAll('"', '&quot;')
            .replaceAll("'", '&#039;');
    }

    function linkify(value = '') {
        return escapeHTML(value).replace(
            /(https?:\/\/[^\s<]+)/gi,
            '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>'
        );
    }

    function parseDate(value) {
        if (!value) {
            return null;
        }

        const date = new Date(value);

        return Number.isNaN(date.getTime())
            ? null
            : date;
    }

    function formatDate(value) {
        const date = parseDate(value);

        if (!date) {
            return 'Not set';
        }

        return new Intl.DateTimeFormat('en-AU', {
            dateStyle: 'medium',
            timeStyle: 'short'
        }).format(date);
    }

    function formatCountdown(value) {
        const target = parseDate(value);

        if (!target) {
            return 'No date set';
        }

        const difference = target.getTime() - Date.now();
        const absolute = Math.abs(difference);

        const totalMinutes = Math.floor(absolute / 60000);
        const days = Math.floor(totalMinutes / 1440);
        const hours = Math.floor((totalMinutes % 1440) / 60);
        const minutes = totalMinutes % 60;

        const parts = [];

        if (days > 0) {
            parts.push(`${days}d`);
        }

        if (hours > 0 || days > 0) {
            parts.push(`${hours}h`);
        }

        parts.push(`${minutes}m`);

        return difference >= 0
            ? `${parts.join(' ')} remaining`
            : `${parts.join(' ')} overdue`;
    }

    function loadJSON(key, fallback) {
        try {
            const stored = localStorage.getItem(key);

            if (!stored) {
                return fallback;
            }

            return JSON.parse(stored);
        } catch (error) {
            console.error('[Torn Diary]', error);
            return fallback;
        }
    }

    function loadData() {
        const entries = loadJSON(
            TD.STORAGE_KEY,
            []
        );

        const settings = loadJSON(
            TD.SETTINGS_KEY,
            DEFAULT_SETTINGS
        );

        state.entries = Array.isArray(entries)
            ? entries
            : [];

        state.settings = {
            ...DEFAULT_SETTINGS,
            ...(settings || {})
        };

        state.activeTab = TABS[state.settings.startTab]
            ? state.settings.startTab
            : 'players';
    }

    function saveEntries() {
        localStorage.setItem(
            TD.STORAGE_KEY,
            JSON.stringify(state.entries)
        );

        updateBadge();
    }

    function saveSettings() {
        localStorage.setItem(
            TD.SETTINGS_KEY,
            JSON.stringify(state.settings)
        );
    }

    async function tornApiRequest(path) {
        const apiKey = String(
            state.settings.apiKey || ''
        ).trim();

        if (!apiKey) {
            throw new Error(
                'Add your Torn API key in Settings first.'
            );
        }

        const separator =
            path.includes('?') ? '&' : '?';

        const response = await fetch(
            `https://api.torn.com/v2${path}${separator}key=${encodeURIComponent(apiKey)}`
        );

        const data = await response.json();

        if (!response.ok) {
            throw new Error(
                `Torn API request failed (${response.status}).`
            );
        }

        if (data?.error) {
            throw new Error(
                data.error.error ||
                'Torn API request failed.'
            );
        }

        return data;
    }

    function getEntry(id) {
        return state.entries.find(
            entry => entry.id === id
        );
    }

    function getReminderStatus(entry) {
        if (entry.completed) {
            return 'completed';
        }

        const due = parseDate(entry.reminderAt);

        if (!due) {
            return 'upcoming';
        }

        const hours =
            (due.getTime() - Date.now()) / 3600000;

        if (hours < 0) {
            return 'overdue';
        }

        if (
            hours <=
            Number(state.settings.dueSoonHours || 24)
        ) {
            return 'due-soon';
        }

        return 'upcoming';
    }

    function getEventStatus(entry) {
        const start = parseDate(entry.startAt);
        const end = parseDate(entry.endAt);
        const now = Date.now();

        if (!start) {
            return 'scheduled';
        }

        if (end && now > end.getTime()) {
            return 'ended';
        }

        if (
            end &&
            now >= start.getTime() &&
            now <= end.getTime()
        ) {
            return 'live';
        }

        if (!end && now >= start.getTime()) {
            return 'started';
        }

        const hours =
            (start.getTime() - now) / 3600000;

        if (hours <= 24) {
            return 'today';
        }

        if (hours <= 72) {
            return 'approaching';
        }

        return 'scheduled';
    }

    function getSearchText(entry) {
        return [
            entry.name,
            entry.itemName,
            entry.title,
            entry.note,
            entry.body,
            entry.profileUrl,
            entry.itemUrl,
            entry.link,
            ...(entry.tags || []),
            ...(entry.updates || []).map(
                update => update.text
            )
        ]
            .filter(Boolean)
            .join(' ')
            .toLowerCase();
    }

    function getVisibleEntries() {
        const type = TABS[state.activeTab]?.type;

        if (!type) {
            return [];
        }

        const search =
            state.search.trim().toLowerCase();

        return state.entries
            .filter(entry => entry.type === type)
            .filter(entry => {
                return (
                    !search ||
                    getSearchText(entry).includes(search)
                );
            })
            .sort((a, b) => {
                if (
                    Boolean(a.pinned) !==
                    Boolean(b.pinned)
                ) {
                    return a.pinned ? -1 : 1;
                }

                if (type === 'reminder') {
                    return (
                        (parseDate(a.reminderAt)?.getTime() ||
                            Infinity) -
                        (parseDate(b.reminderAt)?.getTime() ||
                            Infinity)
                    );
                }

                if (
                    type === 'faction' ||
                    type === 'torn'
                ) {
                    return (
                        (parseDate(a.startAt)?.getTime() ||
                            Infinity) -
                        (parseDate(b.startAt)?.getTime() ||
                            Infinity)
                    );
                }

                return (
                    (parseDate(b.updatedAt)?.getTime() || 0) -
                    (parseDate(a.updatedAt)?.getTime() || 0)
                );
            });
    }

    function injectStyles() {
        if (document.getElementById(TD.STYLE_ID)) {
            return;
        }

        const style = document.createElement('style');

        style.id = TD.STYLE_ID;

        style.textContent = `
            #${TD.LAUNCHER_ID},
            #${TD.APP_ID},
            #${TD.APP_ID} * {
                box-sizing: border-box;
            }

            #${TD.LAUNCHER_ID} {
                position: fixed;
                top: 194px;
                right: 121px;
                width: 40px;
                height: 40px;
                padding: 0;
                display: flex;
                align-items: center;
                justify-content: center;
                border: 1px solid #587d91;
                border-radius: 6px;
                background:
                    linear-gradient(
                        180deg,
                        #354e5b,
                        #1c2c34
                    );
                color: #eef7fa;
                cursor: grab;
                touch-action: none;
                user-select: none;
                z-index: 2147482000;
                box-shadow:
                    inset 0 1px 0 rgba(255,255,255,.12),
                    0 0 8px rgba(91,166,207,.25);
                font-size: 18px;
            }

            #${TD.LAUNCHER_ID}:hover {
                filter: brightness(1.15);
            }

            #${TD.LAUNCHER_ID}.tdiary-dragging {
                cursor: grabbing;
            }

            #${TD.LAUNCHER_ID} .tdiary-badge {
                position: absolute;
                top: -7px;
                right: -7px;
                min-width: 17px;
                height: 17px;
                padding: 0 4px;
                border: 1px solid rgba(255,255,255,.55);
                border-radius: 999px;
                background: #bd4848;
                color: #fff;
                font: 700 10px/15px Arial, sans-serif;
            }

            #${TD.APP_ID} {
                position: fixed;
                inset: 0;
                display: none;
                align-items: center;
                justify-content: center;
                padding: 20px;
                background: rgba(5,9,12,.76);
                backdrop-filter: blur(4px);
                z-index: 2147483000;
                font-family: Arial, Helvetica, sans-serif;
            }

            #${TD.APP_ID}.tdiary-open {
                display: flex;
            }

            #${TD.APP_ID} .tdiary-shell {
                width: min(1180px, 96vw);
                height: min(760px, 92vh);
                display: grid;
                grid-template-columns:
                    210px
                    minmax(290px, .9fr)
                    minmax(390px, 1.35fr);
                grid-template-rows: 60px 1fr 32px;
                overflow: hidden;
                border: 1px solid #506e7e;
                border-radius: 9px;
                background: #19252b;
                color: #dbe5ea;
                box-shadow:
                    0 25px 70px rgba(0,0,0,.72);
            }

            #${TD.APP_ID} .tdiary-header {
                grid-column: 1 / -1;
                display: flex;
                align-items: center;
                gap: 12px;
                padding: 0 17px;
                border-bottom: 1px solid #46616e;
                background:
                    linear-gradient(
                        180deg,
                        #263a43,
                        #1b2a31
                    );
            }

            #${TD.APP_ID} .tdiary-logo {
                width: 34px;
                height: 34px;
                display: flex;
                align-items: center;
                justify-content: center;
                border: 1px solid #55788c;
                border-radius: 6px;
                background: #152128;
                font-size: 18px;
            }

            #${TD.APP_ID} .tdiary-brand {
                min-width: 175px;
            }

            #${TD.APP_ID} .tdiary-brand strong {
                display: block;
                color: #eef6f9;
                font-size: 20px;
            }

            #${TD.APP_ID} .tdiary-brand span {
                display: block;
                margin-top: 2px;
                color: #8ca1ab;
                font-size: 10px;
                letter-spacing: .8px;
                text-transform: uppercase;
            }

            #${TD.APP_ID} .tdiary-search-wrap {
                flex: 1;
                display: flex;
                justify-content: center;
            }

            #${TD.APP_ID} .tdiary-search {
                width: min(460px, 100%);
                height: 34px;
                padding: 0 12px;
                border: 1px solid #3d5967;
                border-radius: 5px;
                background: #111b20;
                color: #e4edf1;
                outline: none;
            }

            #${TD.APP_ID} .tdiary-close {
                width: 34px;
                height: 34px;
                padding: 0;
                border: 1px solid #455e6b;
                border-radius: 5px;
                background: #162228;
                color: #aebdc4;
                cursor: pointer;
                font-size: 20px;
            }

            #${TD.APP_ID} .tdiary-sidebar {
                grid-row: 2;
                padding: 13px 9px;
                overflow-y: auto;
                border-right: 1px solid #394f59;
                background: #141f24;
            }

            #${TD.APP_ID} .tdiary-tab {
                width: 100%;
                height: 42px;
                display: flex;
                align-items: center;
                gap: 10px;
                margin-bottom: 6px;
                padding: 0 11px;
                border: 1px solid transparent;
                border-radius: 5px;
                background: transparent;
                color: #aebdc5;
                cursor: pointer;
                font-weight: 700;
                text-align: left;
            }

            #${TD.APP_ID} .tdiary-tab:hover {
                background: #1b2a31;
                color: #e5eef2;
            }

            #${TD.APP_ID} .tdiary-tab.tdiary-active {
                border-color: #50778c;
                background:
                    linear-gradient(
                        90deg,
                        #263d48,
                        #1b2c33
                    );
                color: #f0f7fa;
                box-shadow:
                    inset 3px 0 0 #76a9c3;
            }

            #${TD.APP_ID} .tdiary-tab-icon {
                width: 22px;
                text-align: center;
            }

            #${TD.APP_ID} .tdiary-list-pane {
                grid-row: 2;
                min-width: 0;
                overflow: hidden;
                border-right: 1px solid #394f59;
                background: #1a282e;
            }

            #${TD.APP_ID} .tdiary-detail-pane {
                grid-row: 2;
                min-width: 0;
                overflow-y: auto;
                background: #202e34;
            }

            #${TD.APP_ID} .tdiary-pane-header {
                height: 54px;
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 0 14px;
                border-bottom: 1px solid #374d57;
                background: #1e2c33;
            }

            #${TD.APP_ID} .tdiary-pane-header h2 {
                margin: 0;
                color: #eaf3f7;
                font-size: 16px;
            }

            #${TD.APP_ID} .tdiary-primary,
            #${TD.APP_ID} .tdiary-secondary,
            #${TD.APP_ID} .tdiary-danger {
                height: 32px;
                padding: 0 12px;
                border-radius: 5px;
                cursor: pointer;
                font-weight: 700;
            }

            #${TD.APP_ID} .tdiary-primary {
                border: 1px solid #527d94;
                background: #315f78;
                color: #f2f8fb;
            }

            #${TD.APP_ID} .tdiary-secondary {
                border: 1px solid #455f6c;
                background: #23343d;
                color: #c9d7dd;
            }

            #${TD.APP_ID} .tdiary-danger {
                border: 1px solid #774247;
                background: #4c292c;
                color: #efd9da;
            }

            #${TD.APP_ID} .tdiary-list {
                height: calc(100% - 54px);
                overflow-y: auto;
                padding: 10px;
            }

            #${TD.APP_ID} .tdiary-card {
                width: 100%;
                margin-bottom: 8px;
                padding: 12px;
                border: 1px solid #354d58;
                border-radius: 6px;
                background: #172329;
                color: #c7d4da;
                cursor: pointer;
                text-align: left;
            }

            #${TD.APP_ID} .tdiary-card:hover {
                border-color: #55798c;
                background: #1c2b32;
            }

            #${TD.APP_ID} .tdiary-card.tdiary-selected {
                border-color: #6d9ab2;
                background: #1e3038;
                box-shadow:
                    inset 3px 0 0 #79a9c1;
            }

            #${TD.APP_ID} .tdiary-card-top {
                display: flex;
                align-items: center;
                gap: 7px;
            }

            #${TD.APP_ID} .tdiary-card-title {
                flex: 1;
                min-width: 0;
                overflow: hidden;
                color: #eaf2f5;
                font-size: 14px;
                font-weight: 700;
                text-overflow: ellipsis;
                white-space: nowrap;
            }

            #${TD.APP_ID} .tdiary-card-sub {
                margin-top: 6px;
                overflow: hidden;
                color: #8da2ac;
                font-size: 12px;
                line-height: 1.4;
                text-overflow: ellipsis;
                white-space: nowrap;
            }

            #${TD.APP_ID} .tdiary-pin {
                color: #d5b85e;
            }

            #${TD.APP_ID} .tdiary-status {
                display: inline-flex;
                align-items: center;
                min-height: 20px;
                padding: 0 7px;
                border: 1px solid #4a6571;
                border-radius: 999px;
                background: #22333b;
                color: #bacad2;
                font-size: 10px;
                font-weight: 700;
                text-transform: uppercase;
                white-space: nowrap;
            }

            #${TD.APP_ID} .tdiary-status.overdue,
            #${TD.APP_ID} .tdiary-status.ended {
                border-color: #795052;
                background: #47292c;
                color: #f0c7ca;
            }

            #${TD.APP_ID} .tdiary-status.due-soon,
            #${TD.APP_ID} .tdiary-status.today,
            #${TD.APP_ID} .tdiary-status.approaching {
                border-color: #786a46;
                background: #443b27;
                color: #ebdca7;
            }

            #${TD.APP_ID} .tdiary-status.live,
            #${TD.APP_ID} .tdiary-status.started,
            #${TD.APP_ID} .tdiary-status.completed,
            #${TD.APP_ID} .tdiary-status.obtained {
                border-color: #47715e;
                background: #274336;
                color: #bfe4cf;
            }

            #${TD.APP_ID} .tdiary-detail {
                min-height: 100%;
                padding: 20px;
            }

            #${TD.APP_ID} .tdiary-empty {
                min-height: 230px;
                height: 100%;
                display: flex;
                flex-direction: column;
                align-items: center;
                justify-content: center;
                padding: 28px;
                color: #82969f;
                text-align: center;
            }

            #${TD.APP_ID} .tdiary-empty-icon {
                margin-bottom: 12px;
                font-size: 36px;
            }

            #${TD.APP_ID} .tdiary-detail-title {
                display: flex;
                align-items: flex-start;
                gap: 12px;
                margin-bottom: 12px;
            }

            #${TD.APP_ID} .tdiary-detail-title h2 {
                flex: 1;
                margin: 0;
                color: #eef5f8;
                font-size: 23px;
            }

            #${TD.APP_ID} .tdiary-actions {
                display: flex;
                flex-wrap: wrap;
                gap: 7px;
                margin: 14px 0 18px;
            }

            #${TD.APP_ID} .tdiary-info-grid {
                display: grid;
                grid-template-columns:
                    repeat(2, minmax(0, 1fr));
                gap: 10px;
                margin-bottom: 16px;
            }

            #${TD.APP_ID} .tdiary-info-box {
                padding: 11px;
                border: 1px solid #3e5763;
                border-radius: 6px;
                background: #172329;
            }

            #${TD.APP_ID} .tdiary-info-box span {
                display: block;
                margin-bottom: 5px;
                color: #849aa5;
                font-size: 10px;
                font-weight: 700;
                letter-spacing: .7px;
                text-transform: uppercase;
            }

            #${TD.APP_ID} .tdiary-info-box strong {
                color: #e2ebef;
                font-size: 13px;
            }

            #${TD.APP_ID} .tdiary-body,
            #${TD.APP_ID} .tdiary-timeline {
                padding: 14px;
                border: 1px solid #3b535e;
                border-radius: 6px;
                background: #172329;
                color: #c7d3d9;
                line-height: 1.55;
                white-space: pre-wrap;
                overflow-wrap: anywhere;
            }

            #${TD.APP_ID} a {
                color: #7fb5d1;
            }

            #${TD.APP_ID} .tdiary-section-title {
                margin: 18px 0 8px;
                color: #dfe8ed;
                font-size: 13px;
                letter-spacing: .7px;
                text-transform: uppercase;
            }

            #${TD.APP_ID} .tdiary-timeline-item {
                position: relative;
                padding: 0 0 16px 18px;
                border-left: 1px solid #4a6876;
            }

            #${TD.APP_ID} .tdiary-timeline-item:last-child {
                padding-bottom: 0;
            }

            #${TD.APP_ID} .tdiary-timeline-item::before {
                content: '';
                position: absolute;
                top: 4px;
                left: -5px;
                width: 9px;
                height: 9px;
                border: 2px solid #172329;
                border-radius: 50%;
                background: #79a9c2;
            }

            #${TD.APP_ID} .tdiary-timeline-date {
                margin-bottom: 4px;
                color: #8399a4;
                font-size: 11px;
            }

            #${TD.APP_ID} .tdiary-tags {
                display: flex;
                flex-wrap: wrap;
                gap: 6px;
                margin-top: 12px;
            }

            #${TD.APP_ID} .tdiary-tag {
                padding: 3px 7px;
                border: 1px solid #435f6c;
                border-radius: 999px;
                background: #1e3038;
                color: #9fb3bd;
                font-size: 11px;
            }

            #${TD.APP_ID} .tdiary-progress {
                height: 11px;
                margin-top: 9px;
                overflow: hidden;
                border: 1px solid #344c57;
                border-radius: 999px;
                background: #0f181c;
            }

            #${TD.APP_ID} .tdiary-progress span {
                display: block;
                height: 100%;
                background:
                    linear-gradient(
                        90deg,
                        #416f88,
                        #76a7c0
                    );
            }

            #${TD.APP_ID} .tdiary-footer {
                grid-column: 1 / -1;
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 0 13px;
                border-top: 1px solid #354b55;
                background: #121b20;
                color: #728791;
                font-size: 10px;
            }

            #${TD.APP_ID} .tdiary-modal-layer {
                position: absolute;
                inset: 0;
                display: none;
                align-items: center;
                justify-content: center;
                padding: 20px;
                background: rgba(4,8,10,.8);
                z-index: 20;
            }

            #${TD.APP_ID} .tdiary-modal-layer.tdiary-show {
                display: flex;
            }

            #${TD.APP_ID} .tdiary-modal {
                width: min(620px, 96vw);
                max-height: 88vh;
                overflow-y: auto;
                border: 1px solid #537185;
                border-radius: 8px;
                background: #1b292f;
                color: #dce5ea;
                box-shadow:
                    0 20px 60px rgba(0,0,0,.68);
            }

            #${TD.APP_ID} .tdiary-modal-header {
                min-height: 52px;
                display: flex;
                align-items: center;
                justify-content: space-between;
                padding: 0 15px;
                border-bottom: 1px solid #3d5561;
                background: #22343c;
            }

            #${TD.APP_ID} .tdiary-modal-header h3 {
                margin: 0;
                font-size: 17px;
            }

            #${TD.APP_ID} .tdiary-modal-body {
                padding: 15px;
            }

            #${TD.APP_ID} .tdiary-form-grid {
                display: grid;
                grid-template-columns:
                    repeat(2, minmax(0, 1fr));
                gap: 12px;
            }

            #${TD.APP_ID} .tdiary-field.full {
                grid-column: 1 / -1;
            }

            #${TD.APP_ID} .tdiary-field label {
                display: block;
                margin-bottom: 5px;
                color: #9eb0b9;
                font-size: 11px;
                font-weight: 700;
                text-transform: uppercase;
            }

            #${TD.APP_ID} input,
            #${TD.APP_ID} textarea,
            #${TD.APP_ID} select {
                width: 100%;
                border: 1px solid #405a67;
                border-radius: 5px;
                background: #111b20;
                color: #e3ebef;
                outline: none;
            }

            #${TD.APP_ID} input,
            #${TD.APP_ID} select {
                height: 36px;
                padding: 0 10px;
            }

            #${TD.APP_ID} textarea {
                min-height: 110px;
                padding: 10px;
                resize: vertical;
            }

            #${TD.APP_ID} .tdiary-modal-actions {
                display: flex;
                justify-content: flex-end;
                gap: 8px;
                padding: 0 15px 15px;
            }

            #${TD.APP_ID} .tdiary-settings-card {
                margin-bottom: 12px;
                padding: 14px;
                border: 1px solid #3a525e;
                border-radius: 6px;
                background: #172329;
            }

            #${TD.APP_ID} .tdiary-settings-card h3 {
                margin: 0 0 10px;
                color: #e5eef2;
                font-size: 14px;
            }

            @media (max-width: 900px) {
                #${TD.APP_ID} {
                    padding: 8px;
                }

                #${TD.APP_ID} .tdiary-shell {
                    width: 100%;
                    height: 96vh;
                    grid-template-columns:
                        70px
                        minmax(250px, .8fr)
                        minmax(320px, 1.2fr);
                }

                #${TD.APP_ID} .tdiary-tab {
                    justify-content: center;
                    padding: 0;
                }

                #${TD.APP_ID} .tdiary-tab-label {
                    display: none;
                }
            }
        `;

        document.head.appendChild(style);
    }

    function createLauncher() {
        if (
            document.getElementById(TD.LAUNCHER_ID)
        ) {
            return;
        }

        const launcher = document.createElement('button');

        launcher.id = TD.LAUNCHER_ID;
        launcher.type = 'button';
        launcher.title = 'Open Torn Diary';

        launcher.innerHTML = `
            <span>📘</span>
            <span
                class="tdiary-badge"
                hidden
            >0</span>
        `;

        document.body.appendChild(launcher);

        restoreLauncherPosition(launcher);
        bindLauncherDrag(launcher);

        updateBadge();
    }

    function restoreLauncherPosition(launcher) {
        const position = loadJSON(
            TD.LAUNCHER_POSITION_KEY,
            null
        );

        if (
            !position ||
            !Number.isFinite(position.left) ||
            !Number.isFinite(position.top)
        ) {
            return;
        }

        const maxLeft = Math.max(
            0,
            window.innerWidth - launcher.offsetWidth
        );

        const maxTop = Math.max(
            0,
            window.innerHeight - launcher.offsetHeight
        );

        launcher.style.left =
            `${Math.min(Math.max(0, position.left), maxLeft)}px`;

        launcher.style.top =
            `${Math.min(Math.max(0, position.top), maxTop)}px`;

        launcher.style.right = 'auto';
    }

    function saveLauncherPosition(launcher) {
        localStorage.setItem(
            TD.LAUNCHER_POSITION_KEY,
            JSON.stringify({
                left: parseFloat(launcher.style.left) || 0,
                top: parseFloat(launcher.style.top) || 0
            })
        );
    }

    function bindLauncherDrag(launcher) {
        let dragging = false;
        let moved = false;
        let startX = 0;
        let startY = 0;
        let startLeft = 0;
        let startTop = 0;

        launcher.addEventListener(
            'pointerdown',
            event => {
                if (event.button !== 0) {
                    return;
                }

                const rect =
                    launcher.getBoundingClientRect();

                dragging = true;
                moved = false;
                startX = event.clientX;
                startY = event.clientY;
                startLeft = rect.left;
                startTop = rect.top;

                launcher.style.left =
                    `${rect.left}px`;

                launcher.style.top =
                    `${rect.top}px`;

                launcher.style.right = 'auto';

                launcher.classList.add(
                    'tdiary-dragging'
                );

                launcher.setPointerCapture(
                    event.pointerId
                );
            }
        );

        launcher.addEventListener(
            'pointermove',
            event => {
                if (!dragging) {
                    return;
                }

                const deltaX =
                    event.clientX - startX;

                const deltaY =
                    event.clientY - startY;

                if (
                    Math.abs(deltaX) > 3 ||
                    Math.abs(deltaY) > 3
                ) {
                    moved = true;
                }

                const maxLeft = Math.max(
                    0,
                    window.innerWidth -
                    launcher.offsetWidth
                );

                const maxTop = Math.max(
                    0,
                    window.innerHeight -
                    launcher.offsetHeight
                );

                const left = Math.min(
                    Math.max(0, startLeft + deltaX),
                    maxLeft
                );

                const top = Math.min(
                    Math.max(0, startTop + deltaY),
                    maxTop
                );

                launcher.style.left = `${left}px`;
                launcher.style.top = `${top}px`;
            }
        );

        launcher.addEventListener(
            'pointerup',
            event => {
                if (!dragging) {
                    return;
                }

                dragging = false;

                launcher.classList.remove(
                    'tdiary-dragging'
                );

                if (
                    launcher.hasPointerCapture(
                        event.pointerId
                    )
                ) {
                    launcher.releasePointerCapture(
                        event.pointerId
                    );
                }

                if (moved) {
                    saveLauncherPosition(launcher);
                    return;
                }

                openApp();
            }
        );

        launcher.addEventListener(
            'pointercancel',
            event => {
                dragging = false;

                launcher.classList.remove(
                    'tdiary-dragging'
                );

                if (
                    launcher.hasPointerCapture(
                        event.pointerId
                    )
                ) {
                    launcher.releasePointerCapture(
                        event.pointerId
                    );
                }
            }
        );
    }

    function updateBadge() {
        const badge = document.querySelector(
            `#${TD.LAUNCHER_ID} .tdiary-badge`
        );

        if (!badge) {
            return;
        }

        const count = state.entries.filter(entry => {
            return (
                entry.type === 'reminder' &&
                ['due-soon', 'overdue'].includes(
                    getReminderStatus(entry)
                )
            );
        }).length;

        badge.textContent = String(count);
        badge.hidden = count === 0;
    }

    function createApp() {
        if (document.getElementById(TD.APP_ID)) {
            return;
        }

        const app = document.createElement('div');

        app.id = TD.APP_ID;

        app.innerHTML = `
            <section class="tdiary-shell">
                <header class="tdiary-header">
                    <div class="tdiary-logo">📘</div>

                    <div class="tdiary-brand">
                        <strong>Torn Diary</strong>
                        <span>Private Player Organiser</span>
                    </div>

                    <div class="tdiary-search-wrap">
                        <input
                            class="tdiary-search"
                            type="search"
                            placeholder="Search this tab..."
                        >
                    </div>

                    <button
                        class="tdiary-close"
                        type="button"
                    >×</button>
                </header>

                <aside class="tdiary-sidebar"></aside>

                <section class="tdiary-list-pane">
                    <div class="tdiary-pane-header">
                        <h2 class="tdiary-list-title"></h2>

                        <button
                            class="tdiary-primary tdiary-add"
                            type="button"
                        >
                            + New Entry
                        </button>
                    </div>

                    <div class="tdiary-list"></div>
                </section>

                <section class="tdiary-detail-pane">
                    <div class="tdiary-detail"></div>
                </section>

                <footer class="tdiary-footer">
                    <span>Private local storage</span>

                    <span class="tdiary-entry-count">
                        0 entries
                    </span>
                </footer>

                <div class="tdiary-modal-layer"></div>
            </section>
        `;

        document.body.appendChild(app);

        bindApp();
        renderSidebar();
        renderView();
    }

    function bindApp() {
        const app = document.getElementById(TD.APP_ID);

        app.querySelector('.tdiary-close')
            .addEventListener(
                'click',
                closeApp
            );

        app.querySelector('.tdiary-search')
            .addEventListener(
                'input',
                event => {
                    state.search = event.target.value;
                    state.selectedId = null;

                    renderView();
                }
            );

        app.querySelector('.tdiary-add')
            .addEventListener(
                'click',
                () => {
                    const type =
                        TABS[state.activeTab]?.type;

                    if (type) {
                        openEntryModal(type);
                    }
                }
            );

        app.addEventListener(
            'click',
            event => {
                if (event.target === app) {
                    closeApp();
                }
            }
        );
    }

    function openApp() {
        createApp();

        const app =
            document.getElementById(TD.APP_ID);

        app.classList.add('tdiary-open');

        document.body.style.overflow = 'hidden';

        renderView();

        clearInterval(state.timer);

        state.timer = setInterval(() => {
            updateBadge();

            if (
                app.classList.contains('tdiary-open')
            ) {
                renderView();
            }
        }, 60000);
    }

    function closeApp() {
        const app =
            document.getElementById(TD.APP_ID);

        if (!app) {
            return;
        }

        closeModal();

        app.classList.remove('tdiary-open');

        document.body.style.overflow = '';

        clearInterval(state.timer);
        state.timer = null;
    }

    function renderSidebar() {
        const sidebar = document.querySelector(
            `#${TD.APP_ID} .tdiary-sidebar`
        );

        sidebar.innerHTML = Object.entries(TABS)
            .map(([key, tab]) => `
                <button
                    class="tdiary-tab ${
                        state.activeTab === key
                            ? 'tdiary-active'
                            : ''
                    }"
                    type="button"
                    data-tab="${key}"
                >
                    <span class="tdiary-tab-icon">
                        ${tab.icon}
                    </span>

                    <span class="tdiary-tab-label">
                        ${escapeHTML(tab.label)}
                    </span>
                </button>
            `)
            .join('');

        sidebar.querySelectorAll('.tdiary-tab')
            .forEach(button => {
                button.addEventListener(
                    'click',
                    () => {
                        state.activeTab =
                            button.dataset.tab;

                        state.selectedId = null;
                        state.search = '';

                        const search = document.querySelector(
                            `#${TD.APP_ID} .tdiary-search`
                        );

                        search.value = '';

                        renderSidebar();
                        renderView();
                    }
                );
            });
    }

    function renderView() {
        const app =
            document.getElementById(TD.APP_ID);

        if (!app) {
            return;
        }

        const tab = TABS[state.activeTab];

        app.querySelector('.tdiary-list-title')
            .textContent = tab.label;

        app.querySelector('.tdiary-add')
            .hidden =
            state.activeTab === 'settings';

        app.querySelector('.tdiary-search-wrap')
            .style.visibility =
            state.activeTab === 'settings'
                ? 'hidden'
                : 'visible';

        if (state.activeTab === 'settings') {
            renderSettings();
            updateEntryCount();
            return;
        }

        renderList();
        renderDetail();
        updateEntryCount();
    }

    function updateEntryCount() {
        const element = document.querySelector(
            `#${TD.APP_ID} .tdiary-entry-count`
        );

        if (!element) {
            return;
        }

        element.textContent =
            `${state.entries.length} ${
                state.entries.length === 1
                    ? 'entry'
                    : 'entries'
            }`;
    }

    function renderList() {
        const list = document.querySelector(
            `#${TD.APP_ID} .tdiary-list`
        );

        const entries = getVisibleEntries();

        if (!entries.length) {
            list.innerHTML = `
                <div class="tdiary-empty">
                    <div class="tdiary-empty-icon">
                        ${TABS[state.activeTab].icon}
                    </div>

                    <strong>
                        No ${escapeHTML(
                            TABS[state.activeTab]
                                .label
                                .toLowerCase()
                        )} yet
                    </strong>

                    <span style="margin-top:6px;">
                        Use + New Entry to add one.
                    </span>
                </div>
            `;

            return;
        }

        list.innerHTML =
            entries.map(renderCard).join('');

        list.querySelectorAll('.tdiary-card')
            .forEach(card => {
                card.addEventListener(
                    'click',
                    () => {
                        state.selectedId =
                            card.dataset.id;

                        renderList();
                        renderDetail();
                    }
                );
            });
    }

    function renderCard(entry) {
        const title =
            entry.name ||
            entry.itemName ||
            entry.title ||
            'Untitled';

        let subtitle = '';
        let status = '';

        if (entry.type === 'player') {
            subtitle =
                entry.updates?.at(-1)?.text ||
                entry.note ||
                'No notes yet';

            status =
                `${entry.updates?.length || 0} notes`;
        }

        if (entry.type === 'item') {
            const wanted =
                Number(entry.quantityWanted || 0);

            const have =
                Number(entry.quantityHave || 0);

            subtitle =
                `${have} / ${wanted} obtained`;

            status =
                wanted > 0 && have >= wanted
                    ? 'obtained'
                    : entry.priority || 'normal';
        }

        if (entry.type === 'note') {
            subtitle =
                entry.updates?.at(-1)?.text ||
                entry.body ||
                'No content';

            status =
                `${entry.updates?.length || 0} updates`;
        }

        if (entry.type === 'reminder') {
            subtitle =
                entry.note ||
                formatDate(entry.reminderAt);

            status = getReminderStatus(entry);
        }

        if (
            entry.type === 'faction' ||
            entry.type === 'torn'
        ) {
            subtitle =
                entry.note ||
                formatCountdown(entry.startAt);

            status = getEventStatus(entry);
        }

        return `
            <button
                class="tdiary-card ${
                    state.selectedId === entry.id
                        ? 'tdiary-selected'
                        : ''
                }"
                type="button"
                data-id="${entry.id}"
            >
                <div class="tdiary-card-top">
                    ${
                        entry.pinned
                            ? '<span class="tdiary-pin">★</span>'
                            : ''
                    }

                    <span class="tdiary-card-title">
                        ${escapeHTML(title)}
                    </span>

                    <span class="tdiary-status ${escapeHTML(status)}">
                        ${escapeHTML(status)}
                    </span>
                </div>

                <div class="tdiary-card-sub">
                    ${escapeHTML(subtitle)}
                </div>
            </button>
        `;
    }

    function renderDetail() {
        const detail = document.querySelector(
            `#${TD.APP_ID} .tdiary-detail`
        );

        let entry = getEntry(state.selectedId);

        if (!entry) {
            entry = getVisibleEntries()[0] || null;

            state.selectedId =
                entry?.id || null;

            if (entry) {
                renderList();
            }
        }

        if (!entry) {
            detail.innerHTML = `
                <div class="tdiary-empty">
                    <div class="tdiary-empty-icon">
                        ${TABS[state.activeTab].icon}
                    </div>

                    <strong>
                        Select an entry to view its details.
                    </strong>
                </div>
            `;

            return;
        }

        detail.innerHTML =
            renderEntryDetail(entry);

        bindDetailActions(entry);
    }

    function renderEntryDetail(entry) {
        const title =
            entry.name ||
            entry.itemName ||
            entry.title ||
            'Untitled';

        let html = `
            <div class="tdiary-detail-title">
                <h2>${escapeHTML(title)}</h2>

                ${
                    entry.pinned
                        ? '<span class="tdiary-pin">★</span>'
                        : ''
                }
            </div>

            <div class="tdiary-actions">
                <button
                    class="tdiary-secondary"
                    data-action="edit"
                >
                    Edit
                </button>

                <button
                    class="tdiary-secondary"
                    data-action="pin"
                >
                    ${entry.pinned ? 'Unpin' : 'Pin'}
                </button>

                ${
                    ['player', 'note'].includes(entry.type)
                        ? `
                            <button
                                class="tdiary-primary"
                                data-action="update"
                            >
                                + Add Update
                            </button>
                        `
                        : ''
                }

                ${
                    entry.type === 'reminder' &&
                    !entry.completed
                        ? `
                            <button
                                class="tdiary-primary"
                                data-action="complete"
                            >
                                Complete
                            </button>
                        `
                        : ''
                }

                <button
                    class="tdiary-danger"
                    data-action="delete"
                >
                    Delete
                </button>
            </div>
        `;

        if (entry.type === 'player') {
            html += `
                <div class="tdiary-info-grid">
                    <div class="tdiary-info-box">
                        <span>Profile</span>

                        <strong>
                            ${
                                entry.profileUrl
                                    ? `
                                        <a
                                            href="${escapeHTML(entry.profileUrl)}"
                                            target="_blank"
                                            rel="noopener noreferrer"
                                        >
                                            Open Torn Profile
                                        </a>
                                    `
                                    : 'Not set'
                            }
                        </strong>
                    </div>

                    <div class="tdiary-info-box">
                        <span>Added</span>

                        <strong>
                            ${escapeHTML(
                                formatDate(entry.createdAt)
                            )}
                        </strong>
                    </div>
                </div>

                ${
                    entry.note
                        ? `
                            <div class="tdiary-body">
                                ${linkify(entry.note)}
                            </div>
                        `
                        : ''
                }

                ${renderTimeline(entry)}
            `;
        }

        if (entry.type === 'item') {
            const wanted =
                Math.max(
                    0,
                    Number(entry.quantityWanted || 0)
                );

            const have =
                Math.max(
                    0,
                    Number(entry.quantityHave || 0)
                );

            const percentage =
                wanted > 0
                    ? Math.min(
                        100,
                        Math.round(
                            (have / wanted) * 100
                        )
                    )
                    : 0;

            html += `
                <div class="tdiary-info-grid">
                    <div class="tdiary-info-box">
                        <span>Progress</span>

                        <strong>
                            ${have} / ${wanted}
                        </strong>

                        <div class="tdiary-progress">
                            <span
                                style="width:${percentage}%"
                            ></span>
                        </div>
                    </div>

                    <div class="tdiary-info-box">
                        <span>Priority</span>

                        <strong>
                            ${escapeHTML(
                                entry.priority || 'normal'
                            )}
                        </strong>
                    </div>

                    <div class="tdiary-info-box">
                        <span>Added</span>

                        <strong>
                            ${escapeHTML(
                                formatDate(entry.createdAt)
                            )}
                        </strong>
                    </div>

                    <div class="tdiary-info-box">
                        <span>Item Link</span>

                        <strong>
                            ${
                                entry.itemUrl
                                    ? `
                                        <a
                                            href="${escapeHTML(entry.itemUrl)}"
                                            target="_blank"
                                            rel="noopener noreferrer"
                                        >
                                            Open Link
                                        </a>
                                    `
                                    : 'Not set'
                            }
                        </strong>
                    </div>
                </div>

                <div class="tdiary-actions">
                    <button
                        class="tdiary-secondary"
                        data-action="decrease"
                    >
                        − 1
                    </button>

                    <button
                        class="tdiary-secondary"
                        data-action="increase"
                    >
                        + 1
                    </button>
                </div>

                ${
                    entry.note
                        ? `
                            <div class="tdiary-body">
                                ${linkify(entry.note)}
                            </div>
                        `
                        : ''
                }
            `;
        }

        if (entry.type === 'note') {
            html += `
                <div class="tdiary-info-grid">
                    <div class="tdiary-info-box">
                        <span>Created</span>

                        <strong>
                            ${escapeHTML(
                                formatDate(entry.createdAt)
                            )}
                        </strong>
                    </div>

                    <div class="tdiary-info-box">
                        <span>Last Updated</span>

                        <strong>
                            ${escapeHTML(
                                formatDate(entry.updatedAt)
                            )}
                        </strong>
                    </div>
                </div>

                ${
                    entry.body
                        ? `
                            <div class="tdiary-body">
                                ${linkify(entry.body)}
                            </div>
                        `
                        : ''
                }

                ${renderTimeline(entry)}
            `;
        }

        if (entry.type === 'reminder') {
            const status =
                getReminderStatus(entry);

            html += `
                <div class="tdiary-info-grid">
                    <div class="tdiary-info-box">
                        <span>Status</span>

                        <strong>
                            <span class="tdiary-status ${status}">
                                ${escapeHTML(status)}
                            </span>
                        </strong>
                    </div>

                    <div class="tdiary-info-box">
                        <span>Due</span>

                        <strong>
                            ${escapeHTML(
                                formatDate(entry.reminderAt)
                            )}
                        </strong>
                    </div>

                    <div class="tdiary-info-box">
                        <span>Countdown</span>

                        <strong>
                            ${escapeHTML(
                                formatCountdown(
                                    entry.reminderAt
                                )
                            )}
                        </strong>
                    </div>

                    <div class="tdiary-info-box">
                        <span>Completed</span>

                        <strong>
                            ${
                                entry.completedAt
                                    ? escapeHTML(
                                        formatDate(
                                            entry.completedAt
                                        )
                                    )
                                    : 'No'
                            }
                        </strong>
                    </div>
                </div>

                ${
                    entry.link
                        ? `
                            <div class="tdiary-body">
                                <a
                                    href="${escapeHTML(entry.link)}"
                                    target="_blank"
                                    rel="noopener noreferrer"
                                >
                                    Open Reminder Link
                                </a>
                            </div>
                        `
                        : ''
                }

                ${
                    entry.note
                        ? `
                            <div class="tdiary-section-title">
                                Reminder Notes
                            </div>

                            <div class="tdiary-body">
                                ${linkify(entry.note)}
                            </div>
                        `
                        : ''
                }
            `;
        }

        if (
            entry.type === 'faction' ||
            entry.type === 'torn'
        ) {
            const status =
                getEventStatus(entry);

            html += `
                <div class="tdiary-info-grid">
                    <div class="tdiary-info-box">
                        <span>Status</span>

                        <strong>
                            <span class="tdiary-status ${status}">
                                ${escapeHTML(status)}
                            </span>
                        </strong>
                    </div>

                    <div class="tdiary-info-box">
                        <span>Starts</span>

                        <strong>
                            ${escapeHTML(
                                formatDate(entry.startAt)
                            )}
                        </strong>
                    </div>

                    <div class="tdiary-info-box">
                        <span>Countdown</span>

                        <strong>
                            ${escapeHTML(
                                formatCountdown(entry.startAt)
                            )}
                        </strong>
                    </div>

                    <div class="tdiary-info-box">
                        <span>Ends</span>

                        <strong>
                            ${escapeHTML(
                                formatDate(entry.endAt)
                            )}
                        </strong>
                    </div>
                </div>

                ${
                    entry.link
                        ? `
                            <div class="tdiary-body">
                                <a
                                    href="${escapeHTML(entry.link)}"
                                    target="_blank"
                                    rel="noopener noreferrer"
                                >
                                    Open Event Link
                                </a>
                            </div>
                        `
                        : ''
                }

                ${
                    entry.note
                        ? `
                            <div class="tdiary-section-title">
                                Event Notes
                            </div>

                            <div class="tdiary-body">
                                ${linkify(entry.note)}
                            </div>
                        `
                        : ''
                }
            `;
        }

        if (entry.tags?.length) {
            html += `
                <div class="tdiary-tags">
                    ${entry.tags
                        .map(
                            tag => `
                                <span class="tdiary-tag">
                                    #${escapeHTML(tag)}
                                </span>
                            `
                        )
                        .join('')}
                </div>
            `;
        }

        return html;
    }

    function renderTimeline(entry) {
        const updates =
            Array.isArray(entry.updates)
                ? [...entry.updates].reverse()
                : [];

        if (!updates.length) {
            return `
                <div class="tdiary-section-title">
                    Timeline
                </div>

                <div class="tdiary-body">
                    No timestamped updates yet.
                </div>
            `;
        }

        return `
            <div class="tdiary-section-title">
                Timeline
            </div>

            <div class="tdiary-timeline">
                ${updates
                    .map(
                        update => `
                            <div class="tdiary-timeline-item">
                                <div class="tdiary-timeline-date">
                                    ${escapeHTML(
                                        formatDate(
                                            update.createdAt
                                        )
                                    )}
                                </div>

                                <div>
                                    ${linkify(update.text)}
                                </div>
                            </div>
                        `
                    )
                    .join('')}
            </div>
        `;
    }

    function bindDetailActions(entry) {
        const detail = document.querySelector(
            `#${TD.APP_ID} .tdiary-detail`
        );

        detail.querySelector('[data-action="edit"]')
            ?.addEventListener(
                'click',
                () => openEntryModal(
                    entry.type,
                    entry
                )
            );

        detail.querySelector('[data-action="pin"]')
            ?.addEventListener(
                'click',
                () => {
                    entry.pinned = !entry.pinned;
                    entry.updatedAt = nowISO();

                    saveEntries();
                    renderView();
                }
            );

        detail.querySelector('[data-action="update"]')
            ?.addEventListener(
                'click',
                () => openUpdateModal(entry)
            );

        detail.querySelector('[data-action="complete"]')
            ?.addEventListener(
                'click',
                () => {
                    entry.completed = true;
                    entry.completedAt = nowISO();
                    entry.updatedAt = nowISO();

                    saveEntries();
                    renderView();
                }
            );

        detail.querySelector('[data-action="increase"]')
            ?.addEventListener(
                'click',
                () => {
                    entry.quantityHave =
                        Number(
                            entry.quantityHave || 0
                        ) + 1;

                    entry.updatedAt = nowISO();

                    saveEntries();
                    renderView();
                }
            );

        detail.querySelector('[data-action="decrease"]')
            ?.addEventListener(
                'click',
                () => {
                    entry.quantityHave = Math.max(
                        0,
                        Number(
                            entry.quantityHave || 0
                        ) - 1
                    );

                    entry.updatedAt = nowISO();

                    saveEntries();
                    renderView();
                }
            );

        detail.querySelector('[data-action="delete"]')
            ?.addEventListener(
                'click',
                () => {
                    if (
                        !confirm(
                            'Delete this Torn Diary entry?'
                        )
                    ) {
                        return;
                    }

                    state.entries =
                        state.entries.filter(
                            item => item.id !== entry.id
                        );

                    state.selectedId = null;

                    saveEntries();
                    renderView();
                }
            );
    }

    function toLocalInputValue(value) {
        const date = parseDate(value);

        if (!date) {
            return '';
        }

        const local = new Date(
            date.getTime() -
            date.getTimezoneOffset() * 60000
        );

        return local
            .toISOString()
            .slice(0, 16);
    }

    function normaliseTags(value) {
        return String(value || '')
            .split(',')
            .map(tag =>
                tag.trim().replace(/^#/, '')
            )
            .filter(Boolean);
    }

    function getFormFields(type, entry = {}) {
        const tags =
            (entry.tags || []).join(', ');

        const tagField = `
            <div class="tdiary-field full">
                <label>Tags</label>

                <input
                    name="tags"
                    type="text"
                    value="${escapeHTML(tags)}"
                    placeholder="education, targets, frostbite"
                >
            </div>
        `;

        if (type === 'player') {
            return `
                <div class="tdiary-field">
                    <label>Player ID</label>

                    <input
                        name="playerId"
                        inputmode="numeric"
                        value="${escapeHTML(
                            entry.playerId || ''
                        )}"
                    >
                </div>

                <div class="tdiary-field">
                    <label>API Lookup</label>

                    <button
                        class="tdiary-secondary"
                        type="button"
                        data-player-lookup
                        style="width:100%;height:36px;"
                    >
                        Lookup Player
                    </button>
                </div>

                <div class="tdiary-field">
                    <label>Player Name</label>

                    <input
                        name="name"
                        required
                        value="${escapeHTML(
                            entry.name || ''
                        )}"
                    >
                </div>

                <div class="tdiary-field">
                    <label>Profile Link</label>

                    <input
                        name="profileUrl"
                        type="url"
                        value="${escapeHTML(
                            entry.profileUrl || ''
                        )}"
                    >
                </div>

                <div class="tdiary-field full">
                    <label>General Note</label>

                    <textarea name="note">${escapeHTML(
                        entry.note || ''
                    )}</textarea>
                </div>

                ${tagField}
            `;
        }

        if (type === 'item') {
            return `
                <div class="tdiary-field full">
                    <label>Item Name</label>

                    <input
                        name="itemName"
                        required
                        value="${escapeHTML(
                            entry.itemName || ''
                        )}"
                    >
                </div>

                <div class="tdiary-field">
                    <label>Quantity Wanted</label>

                    <input
                        name="quantityWanted"
                        type="number"
                        min="0"
                        value="${
                            Number(
                                entry.quantityWanted || 1
                            )
                        }"
                    >
                </div>

                <div class="tdiary-field">
                    <label>Quantity Have</label>

                    <input
                        name="quantityHave"
                        type="number"
                        min="0"
                        value="${
                            Number(
                                entry.quantityHave || 0
                            )
                        }"
                    >
                </div>

                <div class="tdiary-field">
                    <label>Priority</label>

                    <select name="priority">
                        <option value="low" ${
                            entry.priority === 'low'
                                ? 'selected'
                                : ''
                        }>Low</option>

                        <option value="normal" ${
                            !entry.priority ||
                            entry.priority === 'normal'
                                ? 'selected'
                                : ''
                        }>Normal</option>

                        <option value="high" ${
                            entry.priority === 'high'
                                ? 'selected'
                                : ''
                        }>High</option>
                    </select>
                </div>

                <div class="tdiary-field">
                    <label>Item Link</label>

                    <input
                        name="itemUrl"
                        type="url"
                        value="${escapeHTML(
                            entry.itemUrl || ''
                        )}"
                    >
                </div>

                <div class="tdiary-field full">
                    <label>Notes</label>

                    <textarea name="note">${escapeHTML(
                        entry.note || ''
                    )}</textarea>
                </div>

                ${tagField}
            `;
        }

        if (type === 'note') {
            return `
                <div class="tdiary-field full">
                    <label>Note Title</label>

                    <input
                        name="title"
                        required
                        value="${escapeHTML(
                            entry.title || ''
                        )}"
                    >
                </div>

                <div class="tdiary-field full">
                    <label>Note</label>

                    <textarea
                        name="body"
                        required
                    >${escapeHTML(
                        entry.body || ''
                    )}</textarea>
                </div>

                ${tagField}
            `;
        }

        if (type === 'reminder') {
            return `
                <div class="tdiary-field full">
                    <label>Reminder Title</label>

                    <input
                        name="title"
                        required
                        value="${escapeHTML(
                            entry.title || ''
                        )}"
                    >
                </div>

                <div class="tdiary-field">
                    <label>Date and Time</label>

                    <input
                        name="reminderAt"
                        type="datetime-local"
                        required
                        value="${toLocalInputValue(
                            entry.reminderAt
                        )}"
                    >
                </div>

                <div class="tdiary-field">
                    <label>Optional Link</label>

                    <input
                        name="link"
                        type="url"
                        value="${escapeHTML(
                            entry.link || ''
                        )}"
                    >
                </div>

                <div class="tdiary-field full">
                    <label>Notes</label>

                    <textarea name="note">${escapeHTML(
                        entry.note || ''
                    )}</textarea>
                </div>

                ${tagField}
            `;
        }

        return `
            <div class="tdiary-field full">
                <label>Event Title</label>

                <input
                    name="title"
                    required
                    value="${escapeHTML(
                        entry.title || ''
                    )}"
                >
            </div>

            <div class="tdiary-field">
                <label>Start Date and Time</label>

                <input
                    name="startAt"
                    type="datetime-local"
                    required
                    value="${toLocalInputValue(
                        entry.startAt
                    )}"
                >
            </div>

            <div class="tdiary-field">
                <label>End Date and Time</label>

                <input
                    name="endAt"
                    type="datetime-local"
                    value="${toLocalInputValue(
                        entry.endAt
                    )}"
                >
            </div>

            <div class="tdiary-field full">
                <label>Optional Link</label>

                <input
                    name="link"
                    type="url"
                    value="${escapeHTML(
                        entry.link || ''
                    )}"
                >
            </div>

            <div class="tdiary-field full">
                <label>Event Notes</label>

                <textarea name="note">${escapeHTML(
                    entry.note || ''
                )}</textarea>
            </div>

            ${tagField}
        `;
    }

    function openEntryModal(
        type,
        existingEntry = null
    ) {
        const layer = document.querySelector(
            `#${TD.APP_ID} .tdiary-modal-layer`
        );

        const editing = Boolean(existingEntry);

        layer.innerHTML = `
            <form class="tdiary-modal">
                <div class="tdiary-modal-header">
                    <h3>
                        ${editing ? 'Edit Entry' : 'New Entry'}
                    </h3>

                    <button
                        class="tdiary-close"
                        type="button"
                        data-close-modal
                    >×</button>
                </div>

                <div class="tdiary-modal-body">
                    <div class="tdiary-form-grid">
                        ${getFormFields(
                            type,
                            existingEntry || {}
                        )}
                    </div>
                </div>

                <div class="tdiary-modal-actions">
                    <button
                        class="tdiary-secondary"
                        type="button"
                        data-close-modal
                    >
                        Cancel
                    </button>

                    <button
                        class="tdiary-primary"
                        type="submit"
                    >
                        ${editing
                            ? 'Save Changes'
                            : 'Save Entry'
                        }
                    </button>
                </div>
            </form>
        `;

        layer.classList.add('tdiary-show');

        layer.querySelectorAll(
            '[data-close-modal]'
        ).forEach(button => {
            button.addEventListener(
                'click',
                closeModal
            );
        });

        layer.querySelector(
            '[data-player-lookup]'
        )?.addEventListener(
            'click',
            async event => {
                const button = event.currentTarget;
                const form = layer.querySelector('form');
                const playerId = String(
                    form.elements.playerId?.value || ''
                ).trim();

                if (!playerId) {
                    alert(
                        'Enter a Torn player ID first.'
                    );

                    return;
                }

                button.disabled = true;
                button.textContent = 'Looking Up...';

                try {
                    const data = await tornApiRequest(
                        `/user/${encodeURIComponent(playerId)}/basic?striptags=true`
                    );

                    const profile = data?.profile;

                    if (
                        !profile ||
                        profile.id == null ||
                        !profile.name
                    ) {
                        throw new Error(
                            'Torn API returned no player profile.'
                        );
                    }

                    form.elements.playerId.value =
                        String(profile.id);

                    form.elements.name.value =
                        String(profile.name);

                    form.elements.profileUrl.value =
                        `https://www.torn.com/profiles.php?XID=${profile.id}`;
                } catch (error) {
                    console.error(
                        '[Torn Diary]',
                        error
                    );

                    alert(error.message);
                } finally {
                    button.disabled = false;
                    button.textContent = 'Lookup Player';
                }
            }
        );

        layer.querySelector('form')
            .addEventListener(
                'submit',
                event => {
                    event.preventDefault();

                    saveEntryFromForm(
                        type,
                        new FormData(event.currentTarget),
                        existingEntry
                    );
                }
            );
    }

    function saveEntryFromForm(
        type,
        formData,
        existingEntry
    ) {
        const timestamp = nowISO();

        const entry =
            existingEntry || {
                id: createId(),
                type,
                createdAt: timestamp,
                updatedAt: timestamp,
                updates: [],
                pinned: false
            };

        entry.updatedAt = timestamp;

        entry.tags = normaliseTags(
            formData.get('tags')
        );

        if (type === 'player') {
            entry.playerId = String(
                formData.get('playerId') || ''
            ).trim();

            entry.name = String(
                formData.get('name') || ''
            ).trim();

            entry.profileUrl = String(
                formData.get('profileUrl') || ''
            ).trim();

            entry.note = String(
                formData.get('note') || ''
            ).trim();
        }

        if (type === 'item') {
            entry.itemName = String(
                formData.get('itemName') || ''
            ).trim();

            entry.quantityWanted = Math.max(
                0,
                Number(
                    formData.get('quantityWanted') || 0
                )
            );

            entry.quantityHave = Math.max(
                0,
                Number(
                    formData.get('quantityHave') || 0
                )
            );

            entry.priority = String(
                formData.get('priority') || 'normal'
            );

            entry.itemUrl = String(
                formData.get('itemUrl') || ''
            ).trim();

            entry.note = String(
                formData.get('note') || ''
            ).trim();
        }

        if (type === 'note') {
            entry.title = String(
                formData.get('title') || ''
            ).trim();

            entry.body = String(
                formData.get('body') || ''
            ).trim();
        }

        if (type === 'reminder') {
            entry.title = String(
                formData.get('title') || ''
            ).trim();

            entry.reminderAt = new Date(
                formData.get('reminderAt')
            ).toISOString();

            entry.link = String(
                formData.get('link') || ''
            ).trim();

            entry.note = String(
                formData.get('note') || ''
            ).trim();

            if (!existingEntry) {
                entry.completed = false;
                entry.completedAt = null;
            }
        }

        if (
            type === 'faction' ||
            type === 'torn'
        ) {
            entry.title = String(
                formData.get('title') || ''
            ).trim();

            entry.startAt = new Date(
                formData.get('startAt')
            ).toISOString();

            const endAt =
                formData.get('endAt');

            entry.endAt = endAt
                ? new Date(endAt).toISOString()
                : '';

            entry.link = String(
                formData.get('link') || ''
            ).trim();

            entry.note = String(
                formData.get('note') || ''
            ).trim();
        }

        if (!existingEntry) {
            state.entries.push(entry);
        }

        state.selectedId = entry.id;

        saveEntries();
        closeModal();
        renderView();
    }

    function openUpdateModal(entry) {
        const layer = document.querySelector(
            `#${TD.APP_ID} .tdiary-modal-layer`
        );

        layer.innerHTML = `
            <form class="tdiary-modal">
                <div class="tdiary-modal-header">
                    <h3>Add Timestamped Update</h3>

                    <button
                        class="tdiary-close"
                        type="button"
                        data-close-modal
                    >×</button>
                </div>

                <div class="tdiary-modal-body">
                    <div class="tdiary-field">
                        <label>Update</label>

                        <textarea
                            name="updateText"
                            required
                        ></textarea>
                    </div>
                </div>

                <div class="tdiary-modal-actions">
                    <button
                        class="tdiary-secondary"
                        type="button"
                        data-close-modal
                    >
                        Cancel
                    </button>

                    <button
                        class="tdiary-primary"
                        type="submit"
                    >
                        Add Update
                    </button>
                </div>
            </form>
        `;

        layer.classList.add('tdiary-show');

        layer.querySelectorAll(
            '[data-close-modal]'
        ).forEach(button => {
            button.addEventListener(
                'click',
                closeModal
            );
        });

        layer.querySelector('form')
            .addEventListener(
                'submit',
                event => {
                    event.preventDefault();

                    const text = String(
                        new FormData(event.currentTarget)
                            .get('updateText') || ''
                    ).trim();

                    if (!text) {
                        return;
                    }

                    entry.updates =
                        Array.isArray(entry.updates)
                            ? entry.updates
                            : [];

                    entry.updates.push({
                        id: createId(),
                        text,
                        createdAt: nowISO()
                    });

                    entry.updatedAt = nowISO();

                    saveEntries();
                    closeModal();
                    renderView();
                }
            );
    }

    function closeModal() {
        const layer = document.querySelector(
            `#${TD.APP_ID} .tdiary-modal-layer`
        );

        if (!layer) {
            return;
        }

        layer.classList.remove('tdiary-show');
        layer.innerHTML = '';
    }

    function renderSettings() {
        const list = document.querySelector(
            `#${TD.APP_ID} .tdiary-list`
        );

        const detail = document.querySelector(
            `#${TD.APP_ID} .tdiary-detail`
        );

        list.innerHTML = `
            <div class="tdiary-empty">
                <div class="tdiary-empty-icon">
                    ⚙️
                </div>

                <strong>
                    Torn Diary Settings
                </strong>
            </div>
        `;

        detail.innerHTML = `
            <div class="tdiary-settings-card">
                <h3>General</h3>

                <div class="tdiary-form-grid">
                    <div class="tdiary-field">
                        <label>Starting Tab</label>

                        <select id="tdiary-start-tab">
                            ${Object.entries(TABS)
                                .filter(
                                    ([key]) =>
                                        key !== 'settings'
                                )
                                .map(
                                    ([key, tab]) => `
                                        <option
                                            value="${key}"
                                            ${
                                                state.settings.startTab === key
                                                    ? 'selected'
                                                    : ''
                                            }
                                        >
                                            ${escapeHTML(tab.label)}
                                        </option>
                                    `
                                )
                                .join('')}
                        </select>
                    </div>

                    <div class="tdiary-field">
                        <label>Due Soon Threshold</label>

                        <select id="tdiary-due-hours">
                            ${[6, 12, 24, 48, 72]
                                .map(
                                    hours => `
                                        <option
                                            value="${hours}"
                                            ${
                                                Number(
                                                    state.settings.dueSoonHours
                                                ) === hours
                                                    ? 'selected'
                                                    : ''
                                            }
                                        >
                                            ${hours} hours
                                        </option>
                                    `
                                )
                                .join('')}
                        </select>
                    </div>
                </div>

                <div class="tdiary-actions">
                    <button
                        class="tdiary-primary"
                        data-save-settings
                    >
                        Save Settings
                    </button>
                </div>
            </div>

            <div class="tdiary-settings-card">
                <h3>Torn API</h3>

                <div class="tdiary-field">
                    <label>API Key</label>

                    <input
                        id="tdiary-api-key"
                        type="password"
                        value="${escapeHTML(
                            state.settings.apiKey || ''
                        )}"
                        autocomplete="off"
                    >
                </div>
            </div>

            <div class="tdiary-settings-card">
                <h3>Backup</h3>

                <div class="tdiary-actions">
                    <button
                        class="tdiary-secondary"
                        data-export
                    >
                        Export Diary
                    </button>

                    <button
                        class="tdiary-secondary"
                        data-import
                    >
                        Import Diary
                    </button>

                    <input
                        id="tdiary-import-file"
                        type="file"
                        accept=".json,application/json"
                        hidden
                    >
                </div>
            </div>

            <div class="tdiary-settings-card">
                <h3>Clear Diary</h3>

                <button
                    class="tdiary-danger"
                    data-clear
                >
                    Delete All Diary Data
                </button>
            </div>
        `;

        detail.querySelector(
            '[data-save-settings]'
        ).addEventListener(
            'click',
            () => {
                state.settings.startTab =
                    detail.querySelector(
                        '#tdiary-start-tab'
                    ).value;

                state.settings.dueSoonHours =
                    Number(
                        detail.querySelector(
                            '#tdiary-due-hours'
                        ).value
                    );

                state.settings.apiKey = String(
                    detail.querySelector(
                        '#tdiary-api-key'
                    ).value || ''
                ).trim();

                saveSettings();
                updateBadge();

                alert(
                    'Torn Diary settings saved.'
                );
            }
        );

        detail.querySelector('[data-export]')
            .addEventListener(
                'click',
                exportDiary
            );

        const importInput =
            detail.querySelector(
                '#tdiary-import-file'
            );

        detail.querySelector('[data-import]')
            .addEventListener(
                'click',
                () => importInput.click()
            );

        importInput.addEventListener(
            'change',
            importDiary
        );

        detail.querySelector('[data-clear]')
            .addEventListener(
                'click',
                () => {
                    if (
                        !confirm(
                            'Delete all Torn Diary data?'
                        )
                    ) {
                        return;
                    }

                    state.entries = [];
                    state.selectedId = null;

                    saveEntries();
                    renderView();
                }
            );
    }

    function exportDiary() {
        const data = {
            application: 'Torn Diary',
            version: 1,
            exportedAt: nowISO(),
            entries: state.entries,
            settings: state.settings
        };

        const blob = new Blob(
            [JSON.stringify(data, null, 2)],
            {
                type: 'application/json'
            }
        );

        const url =
            URL.createObjectURL(blob);

        const link =
            document.createElement('a');

        link.href = url;

        link.download =
            `torn-diary-${new Date()
                .toISOString()
                .slice(0, 10)}.json`;

        document.body.appendChild(link);

        link.click();
        link.remove();

        URL.revokeObjectURL(url);
    }

    function importDiary(event) {
        const file =
            event.target.files?.[0];

        if (!file) {
            return;
        }

        const reader = new FileReader();

        reader.onload = () => {
            try {
                const data =
                    JSON.parse(reader.result);

                if (!Array.isArray(data.entries)) {
                    throw new Error(
                        'Invalid backup'
                    );
                }

                if (
                    !confirm(
                        'Replace current Torn Diary data with this backup?'
                    )
                ) {
                    return;
                }

                state.entries = data.entries;

                state.settings = {
                    ...DEFAULT_SETTINGS,
                    ...(data.settings || {})
                };

                state.selectedId = null;

                saveEntries();
                saveSettings();
                renderView();

                alert(
                    'Torn Diary imported.'
                );
            } catch (error) {
                console.error(
                    '[Torn Diary]',
                    error
                );

                alert(
                    'Invalid Torn Diary backup.'
                );
            } finally {
                event.target.value = '';
            }
        };

        reader.readAsText(file);
    }

    function initialise() {
        loadData();
        injectStyles();

        createLauncher();

        console.info(
            '[Torn Diary] V1.1.1 loaded.'
        );
    }

    setTimeout(
        initialise,
        TD.LOAD_DELAY
    );
})();