Tooltip info display — NekStyle extension
// ==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,'&').replace(/</g,'<').replace(/>/g,'>').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();
}
})();