DeepCo NekTooltip

Tooltip info display — NekStyle extension

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.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

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!)

// ==UserScript==
// @name         DeepCo NekTooltip
// @namespace    http://tampermonkey.net/
// @version      1.5.2
// @description  Tooltip info display — NekStyle extension
// @match        https://deepco.app/*
// @license      MIT
// @grant        none
// @icon         https://www.google.com/s2/favicons?sz=64&domain=deepco.app
// ==/UserScript==

(function () {
    'use strict';

    const EXTENSION_ID = 'nek-tooltip';
    const EXTENSION_LABEL = 'NekTooltip';
    const EXTENSION_VERSION = GM?.info?.script?.version || '1';
    const BUTTON_COLOR = '#55adda';
    const STORAGE_KEY = 'NekTooltipOptions';
    const CACHE_KEY = 'NekTooltipProfileCache';
    const CACHE_TTL = 5 * 60 * 1000; // 5 min
    const NOTES_KEY = 'NekTooltipNotes';
    const HOVER_DELAY_MS = 500;
    const INJECT_TARGET = 'footer.footer>nav';

    const OPTIONS = [
        {
            id:    'tooltip-tenure',
            label: 'Show Tenure',
            alt:   'Display Tenure',
            fieldsetLabel: 'main',
            innerLabel:    'tenure',
            displayLabel:  'Tenure',
            format: (val) => val.replace('Tenure ','')
        },
        {
            id:    'tooltip-cycle-duration',
            label: 'Show Cycle Duration',
            alt:   'Display current-cycle duration',
            fieldsetLabel: 'Current Cycle',
            innerLabel:    'Cycle Duration',
            displayLabel:  'Cycle',
            format: (val) => `<span class="text-accent">${val.split(' ').slice(0,2).join(' ')}</span>`
        },
        {
            id:    'tooltip-recurse-credits',
            label: 'Show Recursive Credits',
            alt:   'Display lifetime Recursive Credits Acquired in worker tooltips',
            fieldsetLabel: 'Lifetime Output',
            innerLabel:    'Recursive Credits Acquired',
            displayLabel:  'Recurse Credits',
            format: (val) => {
                const num = parseFloat(val.replace(/,/g, ''));
                const fmt = isNaN(num)
                ? val
                : num >= 100000 ? `${Math.floor(num / 1000)}k`
                    : num >= 10000 ? `${Math.floor(num / 100)/10}k`
                    : num >= 1000 ? `${Math.floor(num)}`
                    : num >= 100 ? `${Math.floor(num*10)/10}`
                    : val;
                return `<span class="text-secondary">${fmt}</span>`;

                `<span class="text-secondary">${val}</span>`
            }
        },
        {
            id:    'tooltip-departement',
            label: 'Show Departement',
            alt:   'Display current departement',
            fieldsetLabel: 'main',
            innerLabel:    'department',
            displayLabel:  'Department',
        },
        {
            id:    'tooltip-presence-today',
            label: 'Show Presence Today',
            alt:   'Display Presence Log today duration',
            fieldsetLabel: 'Presence Log',
            innerLabel:    'Today',
            displayLabel:  'Presence Today',
            format: (val) => val.split(' ').slice(0,2).join(' ')
        },
        {
            id:    'tooltip-teamwork-processes',
            label: 'Show Teamwork Processes',
            alt:   'Display current-cycle Teamwork Processes',
            fieldsetLabel: 'Current Cycle',
            innerLabel:    'Teamwork Processes',
            displayLabel:  'Teamwork',
            format: (val) => {
                const num = parseInt(val.replace(/[\.,\s]/g,''));
                return isNaN(num)
                    ? val
                : num >= 300
                    ? `<span class="text-s">✔️</span> ${val}`
                      : `<span class="text-s">❌</span> ${val}`;
            }
        },
    ];

    const PREFERENCES = [
        { id: 'pref-enable-colors', label: 'Enable Colors', alt: 'Enable colors' },
        { id: 'pref-name-not-colored', label: 'Do not color name', alt: 'Disable player custom color' },
    ];

    function loadSettings() {
        try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; }
        catch { return {}; }
    }

    function saveSettings(s) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
    }

    function loadNotes() {
        try { return JSON.parse(localStorage.getItem(NOTES_KEY)) || {}; }
        catch { return {}; }
    }

    function saveNote(profileUrl, note) {
        const notes = loadNotes();
        const clean = note.trim();
        if (clean === '') delete notes[profileUrl];
        else notes[profileUrl] = clean;
        localStorage.setItem(NOTES_KEY, JSON.stringify(notes));
    }

    function getNote(profileUrl) {
        return loadNotes()[profileUrl] || '';
    }

    function loadCache() {
        try { return JSON.parse(localStorage.getItem(CACHE_KEY)) || {}; }
        catch { return {}; }
    }

    function saveCache(cache) {
        try { localStorage.setItem(CACHE_KEY, JSON.stringify(cache)); }
        catch { /* ignore */ }
    }

    function getCachedProfile(profileUrl) {
        const cache = loadCache();
        const entry = cache[profileUrl];
        if (!entry) return null;
        if (Date.now() - entry.ts > CACHE_TTL) return null;
        return {...entry.data, ts: entry.ts};
    }

    function setCachedProfile(profileUrl, data) {
        const cache = loadCache();
        cache[profileUrl] = { ts: Date.now(), data };
        for (const [k, v] of Object.entries(cache)) {
            if (Date.now() - v.ts > CACHE_TTL) delete cache[k];
        }
        saveCache(cache);
    }

    function extractProfileData(doc) {
        const panel = doc.getElementById('main-panel')
        ?.getElementsByTagName('main')[0]
        ?.getElementsByClassName('settings-panel')[0];

        if (!panel) return null;
        const data = {};

        panel.querySelectorAll('fieldset').forEach(fieldset => {
            const legend = fieldset.querySelector('legend');
            if (!legend) return;
            const sectionLabel = legend.textContent.trim();

            fieldset.querySelectorAll('div.text-xs').forEach(labelDiv => {
                const innerLabel = labelDiv.textContent.trim();
                const container = labelDiv.closest('.rounded-box');
                const valueDiv = container?.querySelector('.text-2xl') ?? container?.querySelector('.text-lg.font-semibold');
                if (valueDiv) {
                    data[`${sectionLabel}::${innerLabel}`] = valueDiv.textContent.trim();
                }
            });
        });

        const boxes = doc.querySelectorAll('.rounded-box');
        boxes.forEach(box => {
            const label = box.querySelector('.text-xs')?.textContent.trim();
            if (!label) return;
            switch (label) {
                case 'Name': {
                    const nameSpan = box.querySelector('.text-lg span.truncate');
                    if (nameSpan) {
                        data['main::name'] = nameSpan.textContent.trim();
                        data['main::color'] = nameSpan.style.color || null;
                    }
                    const specBadge = box.querySelector('.badge-outline');
                    if (specBadge) {
                        data['main::specialisation'] = specBadge.textContent.trim();
                    }
                    break;
                }
                case 'Department': {
                    const value = box.querySelector('.text-lg span');
                    if (value) data['main::department'] = value.textContent.trim();
                    break;
                }
                case 'Status': {
                    const badge = box.querySelector('.badge');
                    if (badge) data['main::online'] = badge.textContent.trim();
                    break;
                }
                case 'Enlisted': {
                    const badge = box.querySelector('.badge');
                    if (badge) data['main::tenure'] = badge.textContent.trim();
                    break;
                }
            }
        });

        return Object.keys(data).length > 0 ? data : null;
    }

    async function fetchProfileData(profileUrl) {
        const cached = getCachedProfile(profileUrl);
        if (cached) return cached;

        const res = await fetch(profileUrl);
        const text = await res.text();
        const doc = new DOMParser().parseFromString(text, 'text/html');
        const data = extractProfileData(doc);

        if (data) setCachedProfile(profileUrl, data);
        return data ? {...data, ts: Date.now()} : null;
    }

    const tooltipEl = (() => {
        const el = document.createElement('div');
        el.classList.add("bg-base-200","card","border","border-base-300");
        el.style.cssText = [
            'display: none',
            'position: fixed',
            'z-index: 99999',
            'pointer-events: none',
            'padding: 6px 10px',
            'font-size: 11px',
            'line-height: 1.5',
            'max-width: 240px',
            'box-shadow: 0 4px 12px rgba(0,0,0,0.25)',
        ].join('; ');
        document.body.appendChild(el);
        return el;
    })();

    function hideOriginalTooltip(anchorEl){
        if(!window.NekStyle?.enabledOptions?.includes("presence-tooltip")){
            const p = anchorEl.parentElement
            if(p.classList.contains('tooltip')){
                p.classList.remove("tooltip");
                p.classList.add("nek-tooltip-removed");
            }
        }
    }

    function showOriginalTooltip(anchorEl){
        if(anchorEl && !window.NekStyle?.enabledOptions?.includes("presence-tooltip")){
            const p = anchorEl.parentElement
            if(p.classList.contains('nek-tooltip-removed')){
                p.classList.add("tooltip");
                p.classList.remove("nek-tooltip-removed");
            }
        }
    }


    function showTooltip(anchorEl, lines, datas, settings, profileUrl = null) {
        hideOriginalTooltip(anchorEl)
        const refreshTime = datas?.ts ? ( Date.now() - datas.ts ) / 60000 : 0;
        const values = lines
        .map(({ label, value }) =>
             `<div><span style="opacity:0.6;margin-right:4px;">${label}:</span>${value}</div>`
            );

        const note = profileUrl ? getNote(profileUrl) : '';
        const trucated_note = note.split("\n").slice(0,2).join("\n");
        const noteHtml = trucated_note
        ? `<div class="text-info" style="
                opacity:0.75;
                font-family: cursive;
                word-break: break-word;
              ">${trucated_note.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>')}</div>`
            : '';

        if(datas){
            const online = datas['main::online'] === 'Online';
            let spec = datas['main::specialisation'].split(" ").pop();

            tooltipEl.innerHTML = `
                <div class="flex">
                  <div class="flex-shrink-0 mr-1">${online ? '🟢':'⚫'}</div>
                  <div class="flex-1 truncate" ${settings['pref-name-not-colored']?'':'style="color:'+datas['main::color']+'"'}>${datas['main::name']}</div>
                  ${spec === 'Unspecialized' ? '' : `<div class="flex-shrink-0 ml-2">${spec}</div>`}
                </div>
                ${noteHtml}
                ${values.join('')}
                ${online ? '' : `
                  <br>
                  <div style="opacity:0.6"><span style="opacity:0.6;margin-right:4px;">Last Signal:</span>${datas["Presence Log::Last Signal"].split(' ').slice(0,2).join(' ')}</div>
                `}
            `;
        } else {
            tooltipEl.innerHTML = values.join('');
        }
        tooltipEl.style.display = 'block';
        positionTooltip(anchorEl);
    }

    function positionTooltip(anchorEl) {
        const rect = anchorEl.getBoundingClientRect();
        const gap = 8;
        let top = rect.top - tooltipEl.offsetHeight - gap;
        let left = rect.left;
        if (top < 4) top = rect.bottom + gap;
        const maxLeft = window.innerWidth - tooltipEl.offsetWidth - 4;
        if (left > maxLeft) left = maxLeft;
        tooltipEl.style.top = `${Math.max(4, top)}px`;
        tooltipEl.style.left = `${Math.max(4, left)}px`;
    }

    function hideTooltip(anchorEl) {
        tooltipEl.style.display = 'none';
        showOriginalTooltip(anchorEl);
    }

    let hoverTimer = null;
    let abortCtrl = null;
    let currentAnchor = null;

    function cancelPending() {
        clearTimeout(hoverTimer);
        hoverTimer = null;
        if (abortCtrl) { abortCtrl.abort(); abortCtrl = null; }
    }

    function buildTooltipLines(data, settings) {
        return OPTIONS
            .filter(f => settings[f.id])
            .map(f => {
            const key = `${f.fieldsetLabel}::${f.innerLabel}`;
            const raw = data[key];
            let value = raw !== undefined ? raw : '—';
            if (settings['pref-enable-colors'] && f.format && raw !== undefined) {
                value = f.format(raw);
            }
            return { label: f.displayLabel ?? f.innerLabel, value };
        });
    }

    function onEnter(anchorEl, profileUrl) {
        cancelPending();
        currentAnchor = anchorEl;

        hoverTimer = setTimeout(async () => {
            const settings = loadSettings();
            if (OPTIONS.filter(f => settings[f.id]).length === 0) return;

            showTooltip(anchorEl, [{ label: '⏳', value: 'Loading…' }], null, settings);
            abortCtrl = new AbortController();
            try {
                const data = await fetchProfileData(profileUrl);
                if (!data) {
                    showTooltip(anchorEl, [{ label: '⚠', value: 'No data found' }], null, settings);
                    return;
                }
                const lines = buildTooltipLines(data, settings);
                if (lines.length === 0) hideTooltip(anchorEl);
                else showTooltip(anchorEl, lines, data, settings, profileUrl);
            } catch (e) {
                console.error(e);
                if (e.name !== 'AbortError') {
                    showTooltip(anchorEl, [{ label: '⚠', value: 'Fetch failed' }], null, settings);
                }
            }
        }, HOVER_DELAY_MS);
    }

    function showProfileNoteTooltip(anchorEl, profileUrl) {
        const settings = loadSettings();
        const data = extractProfileData(document);
        if (!data) return;
        data.ts = data.ts ?? Date.now(); // ensure ts for display
        const lines = buildTooltipLines(data, settings);
        showTooltip(anchorEl, lines, data, settings, profileUrl);
    }

    function injectNoteFieldset() {
        if (document.getElementById('nek-note-fieldset')) return;
        const nameBox = [...document.querySelectorAll('div.rounded-box')].find(box =>
                                                                               box.querySelector('.text-xs')?.textContent.trim() === 'Name'
                                                                              );
        if (!nameBox) {
            return;
        }

        const profileUrl = window.location.href;
        const currentNote = getNote(profileUrl);

        const wrapper = document.createElement('div');
        wrapper.id = 'nek-note-fieldset';
        wrapper.className = 'rounded-box border border-base-300 bg-base-100/60 p-2';

        wrapper.innerHTML = `
            <div class="text-xs uppercase tracking-widest text-base-content/60 flex" style="margin-bottom:4px;">
                <div class="flex-1">Note</div><div class="flex-shrink-0" style="opacity:0.4;font-size:9px;text-transform:none;letter-spacing:0;">NekTooltip</div>
            </div>
            <textarea
                id="nek-note-textarea"
                class="textarea textarea-bordered w-full resize-none"
                placeholder="Personal notes…"
                rows="1"
                style="font-size:12px;line-height:1.3;min-height: auto;font-family: cursive;"
            ></textarea>
        `;

        nameBox.insertAdjacentElement('afterend', wrapper);

        const textarea = wrapper.querySelector('#nek-note-textarea');
        textarea.value = currentNote;
        textarea.style.height = textarea.scrollHeight + 1 + "px";

        textarea.addEventListener('focus', () => {
            showProfileNoteTooltip(textarea, profileUrl);
        });

        textarea.addEventListener('input', () => {
            saveNote(profileUrl, textarea.value);
            showProfileNoteTooltip(textarea, profileUrl);
            textarea.style.height = "auto";
            textarea.style.height = textarea.scrollHeight + 1 + "px";
        });

        textarea.addEventListener('blur', () => {
            hideTooltip();
        });

        document.addEventListener('scroll', () => {hideTooltip()}, true);
    }

    function startProfileObserver() {
        function tryInjectNote() {
            if (document.querySelector('.settings-panel')) {
                injectNoteFieldset();
            }
        }
        tryInjectNote();
        const obs = new MutationObserver(tryInjectNote);
        obs.observe(document.documentElement, { childList: true, subtree: true });
    }

    function startLinkObserver() {
        document.addEventListener('mouseover', (e) => {
            const anchor = e.target.closest('a[href^="/workers/"]');
            if (anchor && !anchor.href.endsWith("/online") && anchor !== currentAnchor) {
                anchor.setAttribute('title', '');
                onEnter(anchor, anchor.href);
            }
        }, true);

        document.addEventListener('mouseout', (e) => {
            if (currentAnchor) {
                const anchor = e.target.closest('a[href^="/workers/"]');
                if (anchor === currentAnchor) {
                    const related = e.relatedTarget;
                    if (!related || !anchor.contains(related)) {
                        cancelPending();
                        hideTooltip(currentAnchor);
                        currentAnchor = null;
                    }
                }
            }
        }, true);

        document.addEventListener('mousemove', () => {
            if (tooltipEl.style.display !== 'none' && currentAnchor) {
                positionTooltip(currentAnchor);
            }
        }, true);
    }

    function buildOptionRow(opt, settings) {
        const row = document.createElement('label');
        row.style.cssText = 'display: flex; gap: 8px; padding: 5px 10px; cursor: pointer; align-items: center; user-select: none;';
        if (opt.alt) {
            row.setAttribute('data-tip', opt.alt);
            row.classList.add('tooltip', 'tooltip-right');
        }
        const cb = document.createElement('input');
        cb.type = 'checkbox';
        cb.checked = !!settings[opt.id];
        cb.classList.add('checkbox', 'checkbox-xs');
        cb.addEventListener('change', () => {
            settings[opt.id] = cb.checked;
            saveSettings(settings);
        });
        const span = document.createElement('span');
        span.textContent = opt.label;
        row.append(cb, span);
        return row;
    }

    function createMenuContent() {
        const settings = loadSettings();
        const container = document.createElement('div');
        container.style.cssText = 'padding: 2px 0;';
        OPTIONS.forEach(opt => container.appendChild(buildOptionRow(opt, settings)));
        const hr = document.createElement('div');
        hr.style.cssText = 'height: 1px; background: #000; opacity: 0.1; margin: 4px 0;';
        container.appendChild(hr);
        PREFERENCES.forEach(opt => container.appendChild(buildOptionRow(opt, settings)));

        const versionNote = document.createElement('div');
        versionNote.textContent = `${EXTENSION_LABEL} v${EXTENSION_VERSION}`;
        versionNote.style.cssText = 'font-size: 10px; color: #777; text-align: right; padding: 4px 10px 2px; pointer-events: none;';
        container.appendChild(versionNote);
        return container;
    }

    function createStandaloneWidget() {
        const wrapper = document.createElement('div');
        wrapper.style.cssText = 'position: relative; display: inline-block;';

        const btn = document.createElement('button');
        btn.textContent = EXTENSION_LABEL;
        btn.style.color = BUTTON_COLOR;
        btn.classList.add('btn', 'btn-ghost', 'btn-sm', 'text-primary');

        const popup = document.createElement('div');
        popup.style.cssText = [
            'display: none',
            'position: absolute',
            'bottom: calc(100% + 8px)',
            'left: 0',
            'min-width: 220px',
            'z-index: 1000',
            'text-align: left',
        ].join('; ');
        popup.classList.add('card', 'bg-base-100', 'border', 'border-base-300', 'p-2', 'text-xs', 'shadow-xl');

        popup.appendChild(createMenuContent());

        btn.addEventListener('click', e => {
            e.stopPropagation();
            popup.style.display = popup.style.display === 'none' ? 'block' : 'none';
        });
        document.addEventListener('click', e => {
            if (!wrapper.contains(e.target)) popup.style.display = 'none';
        });

        wrapper.append(btn, popup);
        return wrapper;
    }

    function injectStandaloneButton() {
        const widget = createStandaloneWidget();

        function tryInject() {
            const target = document.querySelector(INJECT_TARGET);
            if (target && widget.parentElement !== target) {
                target.insertBefore(widget, target.firstChild);
            }
            if (!document.body.contains(tooltipEl)) {
                document.body.appendChild(tooltipEl);
            }
            if (currentAnchor && !document.body.contains(currentAnchor)) {
                cancelPending();
                hideTooltip(currentAnchor);
                currentAnchor = null;
            }
        }

        tryInject();
        const obs = new MutationObserver(tryInject);
        obs.observe(document.documentElement, { childList: true, subtree: true });
    }

    function registerWithNekStyle() {
        window.NekStyle.registerExtension({
            id: EXTENSION_ID,
            label: EXTENSION_LABEL,
            color: BUTTON_COLOR,
            createContent: createMenuContent,
        });
    }

    function init() {
        startLinkObserver();
        startProfileObserver();

        if (typeof window.NekStyle?.registerExtension === 'function') {
            window.NekStyle.registerExtension({
                id: EXTENSION_ID,
                label: EXTENSION_LABEL,
                color: BUTTON_COLOR,
                createContent: createMenuContent,
            });
            const obs = new MutationObserver(() => {
                if (!document.body.contains(tooltipEl)) document.body.appendChild(tooltipEl);
                if (currentAnchor && !document.body.contains(currentAnchor)) {
                    cancelPending();
                    hideTooltip(currentAnchor);
                    currentAnchor = null;
                }
            });
            obs.observe(document.documentElement, { childList: true, subtree: true });
            return;
        }

        let registered = false;
        window.__NekStyleExtensions = window.__NekStyleExtensions || [];
        window.__NekStyleExtensions.push(() => {
            registered = true;
            registerWithNekStyle();
        });

        setTimeout(() => {
            if (!registered) injectStandaloneButton();
        }, 0);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();