From 75c81ce498875f514150c13cb944cfc6b2920517 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 24 Sep 2026 08:18:31 +0300 Subject: [PATCH] feat(productivity): add draft recovery and memory search --- README.md | 1 + dist/addons.js | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ dist/index.html | 2 +- 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 dist/addons.js diff --git a/README.md b/README.md index 3d2f66f..12899e5 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - **System**:深浅主题、中文/English 切换、JSON 导入导出与本地存储说明。 - **Bing 每日一图**:桌面壁纸每日自动更新;接口不可用时保留内置背景。 - **Windows 式操作**:桌面端单击选择、双击打开;右键桌面或 App 图标可使用快捷菜单。 +- **快捷键与草稿恢复**:`Ctrl / ⌘ + K` 打开启动器,`Ctrl / ⌘ + F` 搜索记忆,`Ctrl / ⌘ + S` 归档 Today;当天未归档文字会自动恢复。 - **移动端**:App 以全屏方式打开,保留任务栏恢复入口。 ## 数据与备份 diff --git a/dist/addons.js b/dist/addons.js new file mode 100644 index 0000000..cbc8ee3 --- /dev/null +++ b/dist/addons.js @@ -0,0 +1,76 @@ +/* Life OS reliability and productivity add-ons. */ +(() => { + const DRAFT_KEY = 'lifeos_today_draft_v1'; + const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'system']; + const zh = () => (typeof lang === 'function' ? lang() : 'en') === 'zh'; + const copy = (en, cn) => (zh() ? cn : en); + + function readDraft() { + try { return JSON.parse(localStorage.getItem(DRAFT_KEY)); } catch { return null; } + } + function saveDraft(text) { + localStorage.setItem(DRAFT_KEY, JSON.stringify({ date: new Date().toISOString().slice(0, 10), text, updatedAt: new Date().toISOString() })); + } + function restoreDraft(entry) { + const draft = readDraft(); + const today = new Date().toISOString().slice(0, 10); + if (!draft || draft.date !== today || !draft.text || entry.value) return; + entry.value = draft.text; + entry.dispatchEvent(new Event('input', { bubbles: true })); + if (typeof toast === 'function') toast(copy('DRAFT RESTORED', '草稿已恢复')); + } + function wireToday() { + const entry = document.querySelector('#entry'); + if (!entry || entry.dataset.lifeosDraft === 'ready') return; + entry.dataset.lifeosDraft = 'ready'; + restoreDraft(entry); + entry.addEventListener('input', () => saveDraft(entry.value)); + } + function clearDraftAfterSave() { + setTimeout(() => { + const today = new Date().toISOString().slice(0, 10); + try { + const saved = JSON.parse(localStorage.getItem('lifeos_memories') || '[]').some(m => m.date === today); + if (saved) localStorage.removeItem(DRAFT_KEY); + } catch {} + }, 700); + } + + function closeSearch() { document.querySelector('#lifeos-search')?.remove(); } + function openSearch() { + closeSearch(); + const box = document.createElement('div'); + box.id = 'lifeos-search'; + box.innerHTML = `
${copy('SEARCH MEMORY ARCHIVE', '搜索记忆档案')}

${copy('ESC to close · click a result to open it', 'ESC 关闭 · 点击结果即可打开')}

`; + document.body.append(box); + const input = box.querySelector('input'); + const results = box.querySelector('.lifeos-search-results'); + const render = () => { + let memories = []; + try { memories = JSON.parse(localStorage.getItem('lifeos_memories') || '[]'); } catch {} + const query = input.value.trim().toLowerCase(); + const found = memories.filter(m => !query || [m.filmTitle, m.text, m.mood, m.date].join(' ').toLowerCase().includes(query)).slice(0, 8); + results.innerHTML = found.length ? found.map(m => ``).join('') : `
${copy('No memories found.', '没有找到记忆。')}
`; + results.querySelectorAll('button').forEach(button => button.addEventListener('click', () => { closeSearch(); openApp('timeline'); setTimeout(() => detail(button.dataset.id), 20); })); + }; + input.addEventListener('input', render); + box.querySelector('.lifeos-search-head button').addEventListener('click', closeSearch); + box.querySelector('.lifeos-search-backdrop').addEventListener('click', closeSearch); + render(); input.focus(); + } + + const style = document.createElement('style'); + style.textContent = `.lifeos-search-backdrop{position:fixed;inset:0;background:#0008;z-index:8000}.lifeos-search-panel{position:fixed;z-index:8001;top:18vh;left:50%;transform:translateX(-50%);width:min(560px,calc(100vw - 28px));background:var(--panel);border:1px solid var(--line);border-radius:15px;box-shadow:var(--shadow);padding:15px}.lifeos-search-head{display:flex;justify-content:space-between;align-items:center;font:500 11px 'DM Mono';letter-spacing:.12em;color:var(--muted);margin-bottom:13px}.lifeos-search-head button{border:0;background:transparent;color:var(--ink);font-size:20px}.lifeos-search-panel input{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:12px;outline:none}.lifeos-search-panel input:focus{border-color:var(--accent)}.lifeos-search-results{margin-top:9px;max-height:360px;overflow:auto}.lifeos-search-results button{display:grid;gap:3px;width:100%;text-align:left;border:0;border-radius:9px;padding:11px;background:transparent;color:var(--ink)}.lifeos-search-results button:hover{background:var(--panel2)}.lifeos-search-results small,.lifeos-search-results span,.lifeos-search-hint{color:var(--muted);font-size:12px}.lifeos-search-results strong{font-size:14px}.lifeos-search-results span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lifeos-search-empty{padding:24px;color:var(--muted);text-align:center;font-size:13px}.lifeos-search-hint{margin:11px 0 0;font:10px 'DM Mono';letter-spacing:.04em}`; + document.head.append(style); + new MutationObserver(wireToday).observe(document.body, { childList: true, subtree: true }); + wireToday(); + document.addEventListener('click', e => { if (e.target.closest('#content-today .primary')) clearDraftAfterSave(); }); + document.addEventListener('keydown', e => { + const mod = e.ctrlKey || e.metaKey; + if (mod && e.key.toLowerCase() === 'k') { e.preventDefault(); toggleLauncher(); } + if (mod && e.key.toLowerCase() === 'f') { e.preventDefault(); openSearch(); } + if (mod && e.key.toLowerCase() === 's' && document.querySelector('#entry')) { e.preventDefault(); archiveToday(); } + if (e.altKey && /^[1-6]$/.test(e.key)) { e.preventDefault(); openApp(appIds[Number(e.key) - 1]); } + if (e.key === 'Escape') { closeSearch(); document.querySelector('#launcher')?.classList.add('hidden'); document.querySelector('#calendar')?.classList.add('hidden'); document.querySelector('#life-context')?.remove(); } + }); +})(); diff --git a/dist/index.html b/dist/index.html index dcebd2f..fce0579 100644 --- a/dist/index.html +++ b/dist/index.html @@ -23,4 +23,4 @@ function museumApp(c){let a=museum(),ms=mems();c.innerHTML=`
m.date===d.toISOString().slice(0,10))){s++;d.setDate(d.getDate()-1)}return s}function longest(a){let ds=a.map(x=>x.date).sort(),best=0,run=0,last='';ds.forEach(d=>{run=last&&((new Date(d)-new Date(last))/864e5===1)?run+1:1;best=Math.max(best,run);last=d});return best}function insights(c){let a=mems(),last=a.filter(m=>(Date.now()-new Date(m.date+'T12:00:00'))<2592e6),cnt={clear:0,light:0,cloudy:0,rain:0,storm:0};last.forEach(x=>cnt[x.mood]++);let avg=a.length?(a.reduce((s,x)=>s+x.energy,0)/a.length).toFixed(1):'—',days={};a.forEach(x=>{let d=new Date(x.date+'T12:00:00').toLocaleDateString('en-US',{weekday:'long'});days[d]=(days[d]||0)+1});let active=Object.entries(days).sort((x,y)=>y[1]-x[1])[0]?.[0]||'—';c.innerHTML=`
LAST 30 DAYS

Insights, quietly.

WEATHER
${Object.entries(cnt).map(([k,v])=>`
${cap(k)} ${v} DAYS
`).join('')}
AVERAGE ENERGY
${avg} / 5
${a.slice(0,10).reverse().map(x=>``).join('')||'No entries yet'}
MOST ACTIVE DAY
${active}
CURRENT STREAK
${streak(a)} DAYS
Longest streak: ${longest(a)} days
`} function profileApp(c){let p=profile(),a=mems(),common=Object.entries(a.reduce((o,m)=>(o[m.mood]=(o[m.mood]||0)+1,o),{})).sort((x,y)=>y[1]-x[1])[0]?.[0]||'—',avg=a.length?(a.reduce((x,m)=>x+m.energy,0)/a.length).toFixed(1):'—',started=Math.max(1,Math.floor((Date.now()-new Date(p.startDate))/864e5));c.innerHTML=`
SYSTEM PROFILE

${p.name?esc(p.name)+' · ':''}LIFE OS ONLINE

${started}DAYS ONLINE
${a.length}MEMORIES
${museum().length}MUSEUM ITEMS
${streak(a)}CURRENT STREAK
${longest(a)}LONGEST STREAK
${cap(common)}COMMON WEATHER
PERSONAL SYSTEM
AVERAGE ENERGY ${avg} / 5
`}function saveProfile(){save('lifeos_profile',{name:pname.value,birthday:pbday.value,startDate:pstart.value||day(),motto:pmotto.value});renderAll();toast('PROFILE SAVED')} function systemApp(c){let s=settings(),bytes=new Blob([JSON.stringify({mems:mems(),museum:museum(),profile:profile()})]).size;let rec=s.lastBackup&&(Date.now()-new Date(s.lastBackup)>12096e5);c.innerHTML=`
SYSTEM SETTINGS

System, local and yours.

APPEARANCE
${['dark','light','auto'].map(x=>``).join('')}
LANGUAGE
DATA
STORAGE

Local Storage
${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB

DATA STORAGE

Your data is stored locally in this browser.
Create regular backups to prevent data loss.

${s.lastBackup?'LAST BACKUP · '+fmtDate(s.lastBackup.slice(0,10)):'LAST BACKUP · NEVER'} ${rec?'
BACKUP RECOMMENDED':''}

VERSION

Life OS
Version 0.1 · Schema v1

`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:'0.1',schemaVersion:1,exportedAt:new Date().toISOString(),profile:profile(),memories:mems(),museum:museum(),settings:settings()};let a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));a.download='life-os-backup-'+day()+'.json';a.click();let s=settings();s.lastBackup=new Date().toISOString();save('lifeos_settings',s);renderAll();toast('BACKUP CREATED')}function readImport(el){let f=el.files[0];if(!f)return;let r=new FileReader;r.onload=()=>{try{let d=JSON.parse(r.result);if(d.app!=='Life OS'||d.schemaVersion!==1||!Array.isArray(d.memories)||!Array.isArray(d.museum))throw 0;importData=d;importPreview()}catch{toast('INVALID BACKUP FILE')}};r.readAsText(f)}function importPreview(){let d=importData;modal.innerHTML=``}function replaceConfirm(){modal.innerHTML=``}function applyImport(mode){let d=importData;if(mode==='replace'){save('lifeos_memories',d.memories);save('lifeos_museum',d.museum);save('lifeos_profile',d.profile||{});save('lifeos_settings',d.settings||{})}else{let a=mems(), ids=new Set(a.map(x=>x.id)),dates=new Set(a.map(x=>x.date));save('lifeos_memories',[...a,...d.memories.filter(x=>!ids.has(x.id)&&!dates.has(x.date))]);let mu=museum(),mi=new Set(mu.map(x=>x.id));save('lifeos_museum',[...mu,...d.museum.filter(x=>!mi.has(x.id))])}modal.innerHTML='';toast('RESTORING SYSTEM...');setTimeout(()=>{renderAll();toast('SYSTEM RESTORED')},500)}function resetConfirm(){modal.innerHTML=``}function toast(t){let x=document.createElement('div');x.className='toast';x.textContent=t;document.body.append(x);setTimeout(()=>x.remove(),2000)}init(); - +