U2 Magic Form

Automatically configures minimum-cost magic and optionally applies it before downloading torrents.

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         U2 Magic Form
// @namespace    https://u2.dmhy.org/
// @version      2.0
// @description  Automatically configures minimum-cost magic and optionally applies it before downloading torrents.
// @author       LightArrowsEXE
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_registerMenuCommand
// @match        *://u2.dmhy.org/userdetails.php*
// @match        *://u2.dmhy.org/torrents.php*
// @match        *://u2.dmhy.org/details.php*
// @match        *://u2.dmhy.org/promotion.php*
// @icon         https://u2.dmhy.org/favicon.ico
// @license      MIT
// @run-at       document-end
// @noframes
// ==/UserScript==

(function () {
    'use strict';

    const SCRIPT_NAME = 'U2 Magic Form';

    const STORAGE_PREFIX = 'u2-magic-form:';
    const TORRENT_CACHE_PREFIX = `${STORAGE_PREFIX}torrent:`;
    const SETTINGS_KEY = `${STORAGE_PREFIX}settings`;

    const MAGIC_CACHE_MS = hours(23);
    const MAGIC_HOURS = '24';
    const DEFAULT_COST_LIMIT = 250_000;
    const REQUEST_TIMEOUT_MS = 15_000;
    const COST_WAIT_MS = 5_000;

    const DEFAULT_SETTINGS = Object.freeze({
        autoApplyOnDownload: true,
        costLimit: DEFAULT_COST_LIMIT,
        overLimitAction: 'warn', // skip | warn | apply
    });

    const MAGIC_FORM_SELECTORS = Object.freeze({
        hours: 'input[name="hours"]',
        selfRadio: '#s_user_self, input[name="user"][value="SELF"], input[name="user"][value="self"]',
        promotion: 'select[name="promotion"]',
        queryButton: '#btn_query',
        submitButton: '#btn_submit',
        price: '#label_price .ucoin-notation[title], #label_price [title], .ucoin-notation[title]',
        priceLabel: '#label_price',
    });

    const OVER_LIMIT_ACTIONS = new Set(['skip', 'warn', 'apply']);
    const ALLOWED_PATHS = new Set(['/userdetails.php', '/torrents.php', '/details.php', '/promotion.php']);

    const BUTTON_SETS = Object.freeze({
        ok: [
            { value: 'ok', label: 'OK', primary: true },
        ],
        overLimit: [
            { value: 'apply', label: 'Apply magic and download', primary: true },
            { value: 'skip', label: 'Download without magic' },
            { value: 'cancel', label: 'Cancel download' },
        ],
        applyFailed: [
            { value: 'retry', label: 'Retry', primary: true },
            { value: 'download', label: 'Download without magic' },
            { value: 'cancel', label: 'Cancel download' },
        ],
        overLimitSetting: [
            { value: 'warn', label: 'Ask every time' },
            { value: 'skip', label: 'Download without magic' },
            { value: 'apply', label: 'Apply magic anyway' },
            { value: 'cancel', label: 'Cancel' },
        ],
    });

    let settingsCache = null;

    init();

    function init() {
        if (!isAllowedPage()) return;

        injectStyles();
        registerMenuCommands();
        pruneOldCaches();

        if (isMagicPage()) configureVisibleMagicForm();

        document.addEventListener('click', onDownloadClick, true);
    }

    async function onDownloadClick(event) {
        const settings = readSettings();
        if (!settings.autoApplyOnDownload) return;

        const link = event.target?.closest?.('a[href*="download.php?id="]');
        if (!link) return;

        const torrentId = getTorrentIdFromUrl(link.href);
        if (!torrentId || isMagicCached(torrentId)) return;

        const promotionContext = findPromotionContext(link);
        if (promotionContext && hasFreeOrEquivalentPromotion(promotionContext)) {
            writeTorrentCache(torrentId, 'already-free');
            return;
        }

        event.preventDefault();
        event.stopPropagation();

        await applyMagicThenDownload({ torrentId, downloadUrl: link.href, settings });
    }

    async function applyMagicThenDownload({ torrentId, downloadUrl, settings }) {
        while (true) {
            try {
                const result = await applyMagicHidden(torrentId, settings);

                if (result.cacheReason) writeTorrentCache(torrentId, result.cacheReason);
                if (result.continueDownload) window.location.href = downloadUrl;

                return;
            } catch (error) {
                console.error(`[${SCRIPT_NAME}] Failed to apply magic:`, error);

                const choice = await showApplyFailedDialog(error);

                if (choice === 'retry') continue;
                if (choice === 'download') window.location.href = downloadUrl;

                return;
            }
        }
    }

    async function applyMagicHidden(torrentId, settings) {
        const iframe = await loadHiddenMagicFrame(torrentId);

        try {
            const doc = iframe.contentDocument;
            if (!doc?.querySelector('form')) throw new Error('magic form not found');

            configureMagicForm(doc);

            const cost = await calculateMagicCost(doc);
            if (!Number.isFinite(cost)) throw new Error('could not read UCoin cost');

            const decision = await resolveOverLimitDecision(cost, settings);
            if (decision === 'cancel') return { continueDownload: false, cacheReason: null };
            if (decision === 'skip') return { continueDownload: true, cacheReason: 'over-limit-skipped' };

            await submitMagicInFrame(doc);

            return { continueDownload: true, cacheReason: 'magic-applied' };
        } finally {
            iframe.remove();
        }
    }

    async function resolveOverLimitDecision(cost, settings) {
        const limit = Number(settings.costLimit);

        if (!Number.isFinite(limit) || limit <= 0 || cost <= limit) return 'apply';
        if (settings.overLimitAction !== 'warn') return settings.overLimitAction;

        return showChoiceDialog({
            title: 'Magic cost above limit',
            message: [
                `Applying magic would cost ${formatUCoinsWithUnit(cost)}.`,
                `Your configured limit is ${formatUCoinsWithUnit(limit)}.`,
                '',
                'Choose what to do with this download.',
            ],
            buttons: BUTTON_SETS.overLimit,
        });
    }

    function showApplyFailedDialog(error) {
        return showChoiceDialog({
            title: 'Could not apply magic',
            message: [
                'U2 Magic Form could not apply magic automatically.',
                '',
                `Reason: ${errorMessage(error)}`,
                '',
                'Choose what to do with this download.',
            ],
            buttons: BUTTON_SETS.applyFailed,
        });
    }

    function loadHiddenMagicFrame(torrentId) {
        return new Promise((resolve, reject) => {
            const iframe = createNode('iframe', {
                className: 'u2-magic-hidden-frame',
                src: buildMagicUrl(torrentId),
            });

            const fail = error => {
                window.clearTimeout(timeout);
                iframe.remove();
                reject(error);
            };

            const timeout = window.setTimeout(() => {
                fail(new Error('magic page timed out'));
            }, REQUEST_TIMEOUT_MS);

            iframe.addEventListener('load', () => {
                window.clearTimeout(timeout);

                const doc = iframe.contentDocument;
                const text = normaliseText(doc?.body?.textContent || '').toLowerCase();

                if (text.includes('invalid id')) {
                    fail(new Error('U2 returned invalid ID'));
                    return;
                }

                if (!doc?.querySelector('form')) {
                    fail(new Error('magic form not found'));
                    return;
                }

                resolve(iframe);
            }, { once: true });

            document.body.append(iframe);
        });
    }

    function configureVisibleMagicForm() {
        const form = document.querySelector('form');
        if (!form || form.dataset.u2MagicConfigured === '1') return;

        form.dataset.u2MagicConfigured = '1';
        configureMagicForm(document);

        window.setTimeout(() => getMagicElement(document, 'queryButton')?.click(), 100);
    }

    function configureMagicForm(doc) {
        const hoursInput = getMagicElement(doc, 'hours');
        const selfRadio = getMagicElement(doc, 'selfRadio');
        const promotionSelect = getMagicElement(doc, 'promotion');

        if (hoursInput) setFieldValue(hoursInput, MAGIC_HOURS);

        if (selfRadio && !selfRadio.checked) {
            selfRadio.checked = true;
            selfRadio.dispatchEvent(new Event('change', { bubbles: true }));
        }

        if (promotionSelect && promotionSelect.value !== '2') {
            setFieldValue(promotionSelect, '2'); // Free
        }
    }

    async function calculateMagicCost(doc) {
        const before = readMagicCost(doc);
        const queryButton = getMagicElement(doc, 'queryButton');

        if (!queryButton) return before;

        queryButton.click();

        const changed = await waitFor(() => {
            const after = readMagicCost(doc);
            return Number.isFinite(after) && after !== before ? after : null;
        }, COST_WAIT_MS);

        return changed ?? readMagicCost(doc);
    }

    function submitMagicInFrame(doc) {
        return new Promise((resolve, reject) => {
            const submitButton = getMagicElement(doc, 'submitButton');
            const form = submitButton?.closest('form') || doc.querySelector('form');
            const iframe = doc.defaultView?.frameElement;

            if (!submitButton || !form || !iframe) {
                reject(new Error('magic submit button not found'));
                return;
            }

            const timeout = window.setTimeout(() => {
                reject(new Error('magic submit timed out'));
            }, REQUEST_TIMEOUT_MS);

            iframe.addEventListener('load', () => {
                window.clearTimeout(timeout);

                const text = normaliseText(iframe.contentDocument?.body?.textContent || '').toLowerCase();

                if (text.includes('invalid id')) {
                    reject(new Error('U2 returned invalid ID'));
                    return;
                }

                if (looksLikeErrorPage(text)) {
                    reject(new Error('U2 returned an error after submitting magic'));
                    return;
                }

                resolve();
            }, { once: true });

            submitButton.click();
        });
    }

    function looksLikeErrorPage(text) {
        if (!text) return false;

        return (
            text.includes('invalid id') ||
            text.includes('access denied') ||
            text.includes('permission denied') ||
            (
                text.includes('error') &&
                !text.includes('success') &&
                !text.includes('succeed')
            )
        );
    }

    function readMagicCost(doc) {
        const price = getMagicElement(doc, 'price');

        if (price?.getAttribute('title')) {
            return parseUCoins(price.getAttribute('title'));
        }

        return parseUCoins(getMagicElement(doc, 'priceLabel')?.textContent || '');
    }

    function getMagicElement(doc, key) {
        return doc.querySelector(MAGIC_FORM_SELECTORS[key]);
    }

    function parseUCoins(value) {
        const parsed = Number.parseFloat(
            String(value || '')
                .replace(/[^\d.,-]/g, '')
                .replace(/,/g, '')
        );

        return Number.isFinite(parsed) ? parsed : NaN;
    }

    function findPromotionContext(downloadLink) {
        const torrentName = downloadLink.closest('table.torrentname');

        if (torrentName) {
            const row = torrentName.closest('td.rowfollow, td.embedded, td')?.closest('tr');
            if (row) return row;
        }

        return downloadLink.closest('tr') || (location.pathname === '/details.php' ? document.body : null);
    }

    function hasFreeOrEquivalentPromotion(context) {
        const promotionText = normaliseText(getPromotionText(context)).toLowerCase();
        const contextText = normaliseText(context.textContent).toLowerCase();

        if (!promotionText && !contextText) return false;
        if (promotionText.includes('free') || contextText.includes('free')) return true;

        const hasOtherPromotion =
            promotionText.includes('other') ||
            contextText.includes('other') ||
            Boolean(context.querySelector?.('.custompromotion'));

        if (!hasOtherPromotion) return false;

        const hasUploadBonus =
            promotionText.includes('ul ratio') ||
            contextText.includes('ul ratio') ||
            promotionText.includes('upload') ||
            contextText.includes('upload') ||
            Boolean(context.querySelector?.('img.arrowup')) ||
            /\b\d+(?:\.\d+)?x\b/i.test(promotionText);

        return hasUploadBonus && hasNoDownloadRatioCost(context);
    }

    function hasNoDownloadRatioCost(context) {
        const downloadColumnText = getNamedColumnText(context, ['D.L.', 'DL', 'Download', 'Downloaded']);

        if (downloadColumnText !== null) {
            return normaliseText(downloadColumnText).toLowerCase() === '0 b';
        }

        const promotionText = normaliseText(getPromotionText(context)).toLowerCase();

        if (!promotionText) return true;

        if (/\b(?:download|dl)\s*ratio\b/.test(promotionText)) {
            return /(?:download|dl)\s*ratio[^0-9]*0(?:\.0+)?x?/.test(promotionText);
        }

        if (/\[(?:\d+)%\]/.test(promotionText)) {
            return /\[0%\]/.test(promotionText);
        }

        return true;
    }

    function getPromotionText(context) {
        const torrentName = context.querySelector?.('table.torrentname');
        return torrentName?.querySelector('tr:nth-child(2)')?.textContent || context.textContent || '';
    }

    function getNamedColumnText(context, names) {
        const row = context.matches?.('tr') ? context : context.closest?.('tr');
        if (!row) return null;

        const table = row.closest('table');
        const headerRow = table?.querySelector('tr');
        if (!headerRow) return null;

        const normalisedNames = names.map(normaliseColumnName);
        const columnIndex = [...headerRow.children].findIndex(header => {
            const text = header.textContent || header.querySelector('img')?.title || '';
            return normalisedNames.includes(normaliseColumnName(text));
        });

        return columnIndex < 0 ? null : row.children[columnIndex]?.textContent ?? null;
    }

    function registerMenuCommands() {
        if (typeof GM_registerMenuCommand !== 'function') return;

        const settings = readSettings();

        GM_registerMenuCommand(
            `Auto-apply at download: ${settings.autoApplyOnDownload ? 'enabled' : 'disabled'}`,
            toggleAutoApply
        );

        GM_registerMenuCommand(
            `Set ucoin limit: ${settings.costLimit > 0 ? formatUCoins(settings.costLimit) : 'no limit'}`,
            configureCostLimit
        );

        GM_registerMenuCommand(
            `Over-limit behaviour: ${settings.overLimitAction}`,
            configureOverLimitAction
        );
    }

    async function toggleAutoApply() {
        const settings = readSettings();
        const autoApplyOnDownload = !settings.autoApplyOnDownload;

        writeSettings({ ...settings, autoApplyOnDownload });

        await showInfoDialog(
            'Setting updated',
            `Auto-apply at download is now ${autoApplyOnDownload ? 'enabled' : 'disabled'}.`
        );
    }

    function configureCostLimit() {
        const settings = readSettings();

        const value = window.prompt(
            [
                'Enter the maximum UCoin cost for auto-applied magic.',
                '',
                'Use the full UCoin number shown in the tooltip, e.g. 250000 or 250,000.',
                'Enter 0 to disable the limit.',
            ].join('\n'),
            String(settings.costLimit)
        );

        if (value === null) return;

        const costLimit = parseUCoins(value);

        if (!Number.isFinite(costLimit) || costLimit < 0) {
            window.alert('Invalid UCoin limit.');
            return;
        }

        writeSettings({ ...settings, costLimit });
    }

    async function configureOverLimitAction() {
        const settings = readSettings();

        const value = await showChoiceDialog({
            title: 'Set over-limit behaviour',
            message: 'Choose what to do when magic would cost more than your configured limit.',
            buttons: withPrimary(BUTTON_SETS.overLimitSetting, settings.overLimitAction),
        });

        if (!value || value === 'cancel' || !OVER_LIMIT_ACTIONS.has(value)) return;

        writeSettings({ ...settings, overLimitAction: value });
    }

    function readSettings() {
        if (settingsCache) return settingsCache;

        const stable = GM_getValue(SETTINGS_KEY, null);
        const migrated = stable || readLegacySettings();

        settingsCache = normaliseSettings(migrated);
        GM_setValue(SETTINGS_KEY, settingsCache);

        return settingsCache;
    }

    function readLegacySettings() {
        const legacyKeys = [
            'u2-magic-form:v8:settings',
            'u2-magic-form:v7:settings',
            'u2-magic-form:v6:settings',
            'u2-magic-form:v5:settings',
        ];

        for (const key of legacyKeys) {
            const value = GM_getValue(key, null);
            if (value && typeof value === 'object') return value;
        }

        return null;
    }

    function normaliseSettings(settings = {}) {
        const costLimit = Number(settings.costLimit);

        return {
            autoApplyOnDownload: typeof settings.autoApplyOnDownload === 'boolean'
                ? settings.autoApplyOnDownload
                : DEFAULT_SETTINGS.autoApplyOnDownload,
            costLimit: Number.isFinite(costLimit) && costLimit >= 0
                ? costLimit
                : DEFAULT_SETTINGS.costLimit,
            overLimitAction: OVER_LIMIT_ACTIONS.has(settings.overLimitAction)
                ? settings.overLimitAction
                : DEFAULT_SETTINGS.overLimitAction,
        };
    }

    function writeSettings(settings) {
        settingsCache = normaliseSettings(settings);
        GM_setValue(SETTINGS_KEY, settingsCache);
    }

    function showInfoDialog(title, message) {
        return showChoiceDialog({
            title,
            message,
            buttons: BUTTON_SETS.ok,
        });
    }

    function showChoiceDialog({ title, message, buttons }) {
        return new Promise(resolve => {
            document.querySelector('.u2-magic-dialog-backdrop')?.remove();

            const backdrop = createNode('div', { className: 'u2-magic-dialog-backdrop' });
            const dialog = createNode('div', {
                className: 'u2-magic-dialog',
                role: 'dialog',
                ariaModal: 'true',
            });

            const buttonRow = createNode('div', { className: 'u2-magic-dialog-buttons' });

            const close = value => {
                backdrop.remove();
                document.removeEventListener('keydown', onKeyDown, true);
                resolve(value);
            };

            const onKeyDown = event => {
                if (event.key !== 'Escape') return;
                event.preventDefault();
                close('cancel');
            };

            for (const button of buttons) {
                const node = createNode('button', {
                    type: 'button',
                    className: `u2-magic-dialog-button${button.primary ? ' u2-magic-dialog-button-primary' : ''}`,
                    textContent: button.label,
                });

                node.addEventListener('click', () => close(button.value));
                buttonRow.append(node);
            }

            dialog.append(
                createNode('h2', { textContent: title }),
                createMessageBlock(message),
                buttonRow
            );

            backdrop.append(dialog);
            document.body.append(backdrop);
            document.addEventListener('keydown', onKeyDown, true);

            dialog.querySelector('.u2-magic-dialog-button-primary, .u2-magic-dialog-button')?.focus();
        });
    }

    function createMessageBlock(message) {
        const body = createNode('div', { className: 'u2-magic-dialog-body' });
        const lines = Array.isArray(message) ? message : String(message).split('\n');

        for (const line of lines) {
            body.append(createNode('p', { textContent: line || '\u00a0' }));
        }

        return body;
    }

    function createNode(tag, props = {}) {
        const node = document.createElement(tag);

        for (const [key, value] of Object.entries(props)) {
            if (key === 'className') node.className = value;
            else if (key === 'textContent') node.textContent = value;
            else if (key === 'ariaModal') node.setAttribute('aria-modal', value);
            else node[key] = value;
        }

        return node;
    }

    function withPrimary(buttons, primaryValue) {
        return buttons.map(button => ({ ...button, primary: button.value === primaryValue }));
    }

    function buildMagicUrl(torrentId) {
        const url = new URL('/promotion.php', location.origin);

        url.searchParams.set('action', 'magic');
        url.searchParams.set('torrent', torrentId);

        return url.toString();
    }

    function getTorrentIdFromUrl(url) {
        try {
            const id = new URL(url, location.href).searchParams.get('id');
            return id && /^\d+$/.test(id) ? id : null;
        } catch {
            return null;
        }
    }

    function isAllowedPage() {
        const url = new URL(location.href);

        if (!ALLOWED_PATHS.has(url.pathname)) return false;
        return url.pathname !== '/promotion.php' || url.searchParams.get('action') === 'magic';
    }

    function isMagicPage() {
        const url = new URL(location.href);
        return url.pathname === '/promotion.php' && url.searchParams.get('action') === 'magic';
    }

    function isMagicCached(torrentId) {
        return Boolean(readFreshCache(getTorrentKey(torrentId), MAGIC_CACHE_MS));
    }

    function writeTorrentCache(torrentId, reason) {
        writeCache(getTorrentKey(torrentId), { reason, torrentId });
    }

    function readFreshCache(key, maxAgeMs) {
        const cached = GM_getValue(key, null);
        if (!cached?.checkedAt) return null;

        return Date.now() - cached.checkedAt <= maxAgeMs ? cached : null;
    }

    function writeCache(key, value) {
        GM_setValue(key, { ...value, checkedAt: Date.now() });
    }

    function pruneOldCaches() {
        for (const key of safeListValues()) {
            if (!key.startsWith(TORRENT_CACHE_PREFIX)) continue;
            if (!readFreshCache(key, MAGIC_CACHE_MS)) GM_deleteValue(key);
        }
    }

    function safeListValues() {
        try {
            return typeof GM_listValues === 'function' ? GM_listValues() : [];
        } catch (error) {
            console.error(`[${SCRIPT_NAME}] Failed to list userscript storage values:`, error);
            return [];
        }
    }

    function getTorrentKey(torrentId) {
        return `${TORRENT_CACHE_PREFIX}${torrentId}`;
    }

    function setFieldValue(field, value) {
        field.value = value;
        field.dispatchEvent(new Event('input', { bubbles: true }));
        field.dispatchEvent(new Event('change', { bubbles: true }));
    }

    function waitFor(callback, timeoutMs) {
        return new Promise((resolve, reject) => {
            const startedAt = Date.now();

            const tick = () => {
                try {
                    const result = callback();

                    if (result !== null && result !== undefined) {
                        resolve(result);
                        return;
                    }

                    if (Date.now() - startedAt >= timeoutMs) {
                        resolve(null);
                        return;
                    }

                    window.setTimeout(tick, 100);
                } catch (error) {
                    reject(error);
                }
            };

            tick();
        });
    }

    function normaliseColumnName(text) {
        return normaliseText(text)
            .replace(/\s+/g, '')
            .replace(/[::]/g, '')
            .toLowerCase();
    }

    function normaliseText(text) {
        return String(text || '')
            .replace(/\u00a0/g, ' ')
            .replace(/\s+/g, ' ')
            .trim();
    }

    function formatUCoins(value) {
        return Number(value).toLocaleString(undefined, { maximumFractionDigits: 2 });
    }

    function formatUCoinsWithUnit(value) {
        const amount = Number(value);
        return `${formatUCoins(amount)} ${amount === 1 ? 'ucoin' : 'ucoins'}`;
    }

    function errorMessage(error) {
        return error instanceof Error && error.message ? error.message : 'unknown error';
    }

    function injectStyles() {
        if (document.getElementById('u2-magic-form-style')) return;

        const style = createNode('style', { id: 'u2-magic-form-style' });

        style.textContent = `
            .u2-magic-hidden-frame {
                position: fixed !important;
                left: -10000px !important;
                top: -10000px !important;
                width: 1px !important;
                height: 1px !important;
                border: 0 !important;
                opacity: 0 !important;
                pointer-events: none !important;
            }

            .u2-magic-dialog-backdrop {
                position: fixed;
                z-index: 2147483647;
                inset: 0;
                display: flex;
                align-items: center;
                justify-content: center;
                padding: 20px;
                background: rgba(0, 0, 0, 0.55);
            }

            .u2-magic-dialog {
                box-sizing: border-box;
                width: min(520px, 100%);
                max-height: min(720px, 90vh);
                overflow: auto;
                padding: 18px;
                border: 1px solid #555;
                border-radius: 6px;
                background: #f8f8f8;
                color: #222;
                box-shadow: 0 10px 36px rgba(0, 0, 0, 0.35);
                font: 13px/1.45 Arial, Helvetica, sans-serif;
            }

            .u2-magic-dialog h2 {
                margin: 0 0 12px;
                padding: 0;
                border: 0;
                background: transparent;
                color: #111;
                font-size: 17px;
                line-height: 1.3;
            }

            .u2-magic-dialog-body {
                margin: 0 0 16px;
            }

            .u2-magic-dialog-body p {
                margin: 0 0 4px;
            }

            .u2-magic-dialog-buttons {
                display: flex;
                flex-wrap: wrap;
                gap: 8px;
                justify-content: flex-end;
            }

            .u2-magic-dialog-button {
                cursor: pointer;
                padding: 6px 10px;
                border: 1px solid #888;
                border-radius: 4px;
                background: #fff;
                color: #222;
                font: inherit;
            }

            .u2-magic-dialog-button:hover,
            .u2-magic-dialog-button:focus {
                border-color: #444;
                background: #eee;
                outline: none;
            }

            .u2-magic-dialog-button-primary {
                border-color: #24578a;
                background: #2f6fa8;
                color: #fff;
            }

            .u2-magic-dialog-button-primary:hover,
            .u2-magic-dialog-button-primary:focus {
                border-color: #183c5f;
                background: #24578a;
            }
        `;

        document.head.append(style);
    }

    function hours(value) {
        return value * 60 * 60 * 1000;
    }
})();