Torn Diary

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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