TaskDecay v1.0.0 MVP — smart task review & archive Chrome extension
This commit is contained in:
+238
@@ -0,0 +1,238 @@
|
||||
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 {};
|
||||
Reference in New Issue
Block a user