Files

239 lines
7.8 KiB
JavaScript

const BADGE_CLASS = 'taskdecay-injected-badge';
function detectTaskManager() {
const host = window.location.host;
if (host.includes('todoist.com'))
return 'todoist';
if (host.includes('ticktick.com'))
return 'ticktick';
if (host.includes('notion.so'))
return 'notion';
return null;
}
function extractTaskIdFromUrl() {
const url = window.location.href;
const platform = detectTaskManager();
if (platform === 'todoist') {
const m = url.match(/task\/(\d+)/);
if (m)
return `todoist-${m[1]}`;
}
if (platform === 'ticktick') {
const m = url.match(/task\/(\w+)/);
if (m)
return `ticktick-${m[1]}`;
}
if (platform === 'notion') {
const m = url.match(/([a-f0-9]{32})/);
if (m)
return `notion-${m[1]}`;
}
return null;
}
function injectStyles() {
if (document.getElementById('taskdecay-styles'))
return;
const style = document.createElement('style');
style.id = 'taskdecay-styles';
style.textContent = `
.taskdecay-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
font-family: system-ui, -apple-system, sans-serif;
margin-left: 8px;
cursor: pointer;
transition: opacity 0.2s;
}
.taskdecay-badge:hover { opacity: 0.8; }
.taskdecay-badge-warm { background: #f39c12; color: #fff; }
.taskdecay-badge-hot { background: #e74c3c; color: #fff; }
.taskdecay-badge-cold { background: #3498db; color: #fff; }
.taskdecay-toast {
position: fixed;
bottom: 20px;
right: 20px;
background: #1a1a1a;
color: #fff;
padding: 12px 16px;
border-radius: 8px;
font-family: system-ui, -apple-system, sans-serif;
font-size: 13px;
z-index: 999999;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
max-width: 320px;
line-height: 1.4;
}
.taskdecay-toast a {
color: #58a6ff;
text-decoration: none;
display: block;
margin-top: 8px;
}
.taskdecay-toast button {
background: #e74c3c;
border: none;
color: #fff;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
margin-top: 8px;
font-size: 12px;
}
`;
document.head.appendChild(style);
}
function showToast(message, action) {
const existing = document.querySelector('.taskdecay-toast');
if (existing)
existing.remove();
const toast = document.createElement('div');
toast.className = 'taskdecay-toast';
toast.textContent = message;
if (action) {
const btn = document.createElement('button');
btn.textContent = action.label;
btn.onclick = () => { action.onClick(); toast.remove(); };
toast.appendChild(btn);
}
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 8000);
}
async function getTaskDecayInfo(taskId) {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ type: 'getReviewTasks' }, (response) => {
if (!response || response.error) {
resolve(null);
return;
}
const match = response.review?.find((r) => r.task.id === taskId);
if (match) {
resolve({
taskId: match.task.id,
score: match.score.score,
ageDays: match.score.ageDays,
explanation: match.score.explanation,
});
}
else {
resolve(null);
}
});
});
}
function addBadgeToTodoist(info) {
const selectors = [
'[data-testid="task-details-modal"] [data-testid="task-title"]',
'[data-testid="task-details-modal"] h1',
'.task_content',
'[data-item-id] .task_content',
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el && !el.parentElement?.querySelector('.taskdecay-badge')) {
const badge = document.createElement('span');
badge.className = `taskdecay-badge ${info.score >= 70 ? 'taskdecay-badge-hot' : info.score >= 40 ? 'taskdecay-badge-warm' : 'taskdecay-badge-cold'}`;
badge.textContent = `${info.score} decay${info.ageDays > 1 ? ` · ${Math.round(info.ageDays)}d` : ''}`;
badge.title = info.explanation;
badge.onclick = (e) => {
e.stopPropagation();
chrome.runtime.sendMessage({ type: 'openReview' });
};
el.parentElement?.insertBefore(badge, el.nextSibling);
break;
}
}
}
function addBadgeToTickTick(info) {
const selectors = [
'.task-title',
'.task-title-text',
'[class*="taskTitle"]',
'h1',
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el && !el.parentElement?.querySelector('.taskdecay-badge')) {
const badge = document.createElement('span');
badge.className = `taskdecay-badge ${info.score >= 70 ? 'taskdecay-badge-hot' : info.score >= 40 ? 'taskdecay-badge-warm' : 'taskdecay-badge-cold'}`;
badge.textContent = `${info.score} decay${info.ageDays > 1 ? ` · ${Math.round(info.ageDays)}d` : ''}`;
badge.title = info.explanation;
badge.onclick = (e) => {
e.stopPropagation();
chrome.runtime.sendMessage({ type: 'openReview' });
};
el.parentElement?.insertBefore(badge, el.nextSibling);
break;
}
}
}
function addBadgeToNotion(info) {
const selectors = [
'[data-testid="page-title"]',
'.notion-page-block',
'h1',
'[class*="title" i]',
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el && !el.parentElement?.querySelector('.taskdecay-badge')) {
const badge = document.createElement('span');
badge.className = `taskdecay-badge ${info.score >= 70 ? 'taskdecay-badge-hot' : info.score >= 40 ? 'taskdecay-badge-warm' : 'taskdecay-badge-cold'}`;
badge.textContent = `${info.score} decay${info.ageDays > 1 ? ` · ${Math.round(info.ageDays)}d` : ''}`;
badge.title = info.explanation;
badge.onclick = (e) => {
e.stopPropagation();
chrome.runtime.sendMessage({ type: 'openReview' });
};
el.parentElement?.insertBefore(badge, el.nextSibling);
break;
}
}
}
async function injectBadge() {
const taskId = extractTaskIdFromUrl();
if (!taskId)
return;
const info = await getTaskDecayInfo(taskId);
if (!info)
return;
const platform = detectTaskManager();
if (platform === 'todoist')
addBadgeToTodoist(info);
else if (platform === 'ticktick')
addBadgeToTickTick(info);
else if (platform === 'notion')
addBadgeToNotion(info);
}
function init() {
injectStyles();
injectBadge();
// Re-inject on URL changes (SPA navigation)
let lastUrl = window.location.href;
const observer = new MutationObserver(() => {
if (window.location.href !== lastUrl) {
lastUrl = window.location.href;
setTimeout(injectBadge, 800);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
}
else {
init();
}
chrome.runtime.onMessage.addListener((message) => {
const msg = message;
if (msg.type === 'showToast') {
showToast(msg.text, msg.action ? { label: msg.action.label, onClick: () => {
chrome.runtime.sendMessage({ type: msg.action.type });
} } : undefined);
}
});
export {};