fix(memory): support selected dates and separate prompt titles

This commit is contained in:
Life OS Releases committed 2026-09-27 13:32:17 +00:00
1 parent 80ad1932e1
commit 88edf64e3b
8 files changed
+42 -37

No files matched your search

+3 -3
View File
@@ -27,15 +27,15 @@ Object.assign(ZH,{
});
ZH['Restore window']='恢复窗口';
function browserLanguage(){let ls=navigator.languages?.length?navigator.languages:[navigator.language||'en'];return ls.some(x=>String(x).toLowerCase().startsWith('zh'))?'zh':'en'}function lang(){let s=settings();return s.languageMode==='manual'&&['en','zh'].includes(s.language)?s.language:browserLanguage()}function localizeDynamicText(zh){if(!zh)return;let walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT),nodes=[];while(walker.nextNode())nodes.push(walker.currentNode);nodes.forEach(node=>{if(node.parentElement?.closest('textarea,input,.entry,.snippet,.mtitle,.film-title,.film-copy'))return;let v=node.nodeValue.trim(),r,section=node.parentElement?.closest('section')?.querySelector('.eyebrow')?.textContent||'';if(v==='微光'&&section.includes('外观'))node.nodeValue=node.nodeValue.replace(v,'浅色');else if(v==='Local Storage')node.nodeValue=node.nodeValue.replace(v,'本地存储');else if(v==='Life OS')node.nodeValue=node.nodeValue.replace(v,'人生操作系统');else if(r=v.match(/^(\d+) (?:MEMORY|MEMORIES) ARCHIVED$/))node.nodeValue=node.nodeValue.replace(v,`${r[1]} 条记忆已归档`);else if(r=v.match(/^(\d+) DAYS$/))node.nodeValue=node.nodeValue.replace(v,`${r[1]} 天`);else if(r=v.match(/^Longest streak: (\d+) days$/))node.nodeValue=node.nodeValue.replace(v,`最长连续记录:${r[1]} 天`);else if(r=v.match(/^(\d+) memories · (\d+) preserved$/))node.nodeValue=node.nodeValue.replace(v,`${r[1]} 条记忆 · ${r[2]} 件已珍藏`);else if(r=v.match(/^Exported: (.+)$/))node.nodeValue=node.nodeValue.replace(v,`导出时间:${r[1]}`);else if(r=v.match(/^Memories: (\d+)$/))node.nodeValue=node.nodeValue.replace(v,`记忆:${r[1]}`);else if(r=v.match(/^Museum Items: (\d+)$/))node.nodeValue=node.nodeValue.replace(v,`博物馆藏品:${r[1]}`);else if(r=v.match(/^LAST BACKUP · (.+)$/))node.nodeValue=node.nodeValue.replace(v,`上次备份 · ${r[1]}`);else if(r=v.match(/^(\d+) memories · (\d+) museum items · Approx\. (\d+) KB$/))node.nodeValue=node.nodeValue.replace(v,`${r[1]} 条记忆 · ${r[2]} 件博物馆藏品 · 约 ${r[3]} KB`)})}function applyLanguage(){let zh=lang()==='zh';document.documentElement.lang=zh?'zh-CN':'en';document.title=zh?'Life OS / 人生操作系统':'Life OS';let map=zh?ZH:Object.fromEntries(Object.entries(ZH).map(([a,b])=>[b,a]));let walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);let n=[];while(walker.nextNode())n.push(walker.currentNode);n.forEach(x=>{if(x.parentElement?.closest('textarea,input,.entry,.snippet,.mtitle,.film-title,.film-copy'))return;let v=x.nodeValue.trim();if(map[v])x.nodeValue=x.nodeValue.replace(v,map[v])});document.querySelectorAll('[placeholder]').forEach(x=>{if(zh&&ZH[x.placeholder])x.placeholder=ZH[x.placeholder];if(!zh){let k=Object.entries(ZH).find(([,v])=>v===x.placeholder)?.[0];if(k)x.placeholder=k}});localizeDynamicText(zh)}function setLanguage(v){let s=settings();s.language=v;s.languageMode=v==='auto'?'auto':'manual';save('lifeos_settings',s);renderIcons();renderAll();applyLanguage();toast(v==='auto'?(lang()==='zh'?'已跟随浏览器语言:中文':'Following browser language: English'):v==='zh'?'语言已切换为中文':'Language set to English')}
function i18n(key,fallback=key){return lang()==='zh'?(ZH[key]||key):fallback}function appName(id){return i18n(APPS[id][1])}function moodName(m){return i18n(m.toUpperCase(),cap(m))}function day(){return new Date().toISOString().slice(0,10)}function fmtDate(d){return new Date(d+'T12:00:00').toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'})}function shortDate(d){return new Date(d+'T12:00:00').toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',lang()==='zh'?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function cap(x){return x[0].toUpperCase()+x.slice(1)}function esc(s){return (s||'').replace(/[&<>]/g,x=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[x]))}
function i18n(key,fallback=key){return lang()==='zh'?(ZH[key]||key):fallback}function appName(id){return i18n(APPS[id][1])}function moodName(m){return i18n(m.toUpperCase(),cap(m))}function day(){const d=new Date();return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`}function fmtDate(d){return new Date(d+'T12:00:00').toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'})}function shortDate(d){return new Date(d+'T12:00:00').toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',lang()==='zh'?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function cap(x){return x[0].toUpperCase()+x.slice(1)}function esc(s){return (s||'').replace(/[&<>]/g,x=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[x]))}
function applyWallpaper(w){if(!w?.url)return;desktop.style.backgroundImage=`linear-gradient(120deg,color-mix(in srgb,var(--bg) 68%,transparent),color-mix(in srgb,var(--bg) 36%,transparent)),url('${w.url}')`;desktop.style.backgroundSize='cover';desktop.style.backgroundPosition='center';desktop.title=`Bing daily image: ${w.title||'Daily image'}`}async function loadBingWallpaper(){let s=settings();if(s.wallpaper?.date===day()&&s.wallpaper?.source==='lifeos-proxy'){applyWallpaper(s.wallpaper);return}try{let r=await fetch('/api/bing-wallpaper'),d=await r.json();if(!r.ok||!d.url)throw Error(d.error||'No Bing image');s.wallpaper={date:day(),url:'/api/bing-wallpaper-image?date='+day(),title:d.title||'Bing daily image',source:'lifeos-proxy'};save('lifeos_settings',s);applyWallpaper(s.wallpaper)}catch(e){console.info('Bing wallpaper unavailable; using the calm fallback.',e)}}function init(){let s=settings();document.documentElement.dataset.theme=s.theme==='auto'?(matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'):s.theme;renderIcons();renderTaskbar();loadBingWallpaper();setInterval(clock,1000);clock();setTimeout(applyLanguage,0);setTimeout(()=>boot.remove(),650)}
function desktopIconClick(id,e){if(innerWidth<650){openApp(id);return}selectedIcon=id;document.querySelectorAll('.app-icon').forEach(x=>x.classList.remove('selected'));e.currentTarget.classList.add('selected')}function desktopIconKey(id,e){if(e.key==='Enter'||e.key===' '){e.preventDefault();openApp(id)}}function renderIcons(){icons.innerHTML=Object.entries(APPS).map(([id,[g,n]])=>`<button class="app-icon ${selectedIcon===id?'selected':''}" onclick="desktopIconClick('${id}',event)" ondblclick="openApp('${id}')" onkeydown="desktopIconKey('${id}',event)" oncontextmenu="showContext(event,'${id}')" title="${appName(id)}"><span class="glyph">${g}</span><span>${appName(id)}</span></button>`).join('')}function taskClick(id){let w=windows[id];if(!w){openApp(id);return}if(w.classList.contains('min')){openApp(id);return}if(activeWindow===id){minWin(id);return}focus(w)}function renderTaskbar(){let ids=innerWidth<650?Object.keys(windows):['today','timeline','museum','insights','tasks','calendar'];tasks.innerHTML=ids.map(id=>{let w=windows[id],min=w?.classList.contains('min'),fg=activeWindow===id&&!min;return`<button class="taskapp ${w?'active running':''} ${min?'minimized':''} ${fg?'foreground':''}" title="${min?i18n('Restore window','Restore window'):appName(id)}" aria-label="${min?i18n('Restore window','Restore window'):i18n('Open')} ${appName(id)}" onclick="taskClick('${id}')">${APPS[id][0]}</button>`}).join('')}
function clock(){let d=new Date();document.querySelector('#clock').textContent=d.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});document.querySelector('#date').textContent=d.toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',lang()==='zh'?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function toggleLauncher(){launcher.classList.toggle('hidden');calendar.classList.add('hidden');launcher.innerHTML=`<div class="launchhead"><span>LIFE OS</span><span>${i18n(`GOOD ${new Date().getHours()<12?'MORNING':new Date().getHours()<18?'AFTERNOON':'EVENING'}`)}</span></div><div class="launchgrid">${Object.entries(APPS).map(([id,a])=>`<button class="launchitem" onclick="openApp('${id}');toggleLauncher()">${a[0]}<br><br>${appName(id)}</button>`).join('')}</div><div class="systemline">${i18n('SYSTEM ONLINE · LIFE OS v0.1')}</div>`}function toggleCalendar(){calendar.classList.toggle('hidden');launcher.classList.add('hidden');calendar.innerHTML=`<div class="eyebrow">${i18n('SYSTEM DATE')}</div><h2>${fmtDate(day())}</h2><p class="sub">${lang()==='zh'?`${mems().length} 条记忆 · ${museum().length} 件已珍藏`:`${mems().length} memories · ${museum().length} preserved`}</p>`}function hideContext(){document.querySelector('#life-context')?.remove()}function showContext(e,id){e.preventDefault();e.stopPropagation();hideContext();let zh=lang()==='zh',labels=zh?{open:'打开',min:'最小化',close:'关闭',refresh:'刷新桌面',new:'新建今日记录',system:'系统设置'}:{open:'Open',min:'Minimize',close:'Close',refresh:'Refresh desktop',new:'New today entry',system:'System settings'},actions=id?`<button onclick="openApp('${id}');hideContext()">${labels.open}</button>${windows[id]?`<button onclick="minWin('${id}');hideContext()">${labels.min}</button><button onclick="closeWin('${id}');hideContext()">${labels.close}</button>`:''}`:`<button onclick="renderIcons();hideContext()">${labels.refresh}</button><button onclick="openApp('today');hideContext()">${labels.new}</button><hr><button onclick="openApp('system');hideContext()">${labels.system}</button>`;let m=document.createElement('div');m.id='life-context';m.style.cssText=`position:fixed;z-index:7000;left:${Math.min(e.clientX,innerWidth-190)}px;top:${Math.min(e.clientY,innerHeight-150)}px;width:180px;padding:6px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow)`;m.innerHTML=actions;m.querySelectorAll('button').forEach(b=>b.style.cssText='display:block;width:100%;border:0;background:transparent;color:var(--ink);text-align:left;border-radius:6px;padding:9px 10px;font:12px Manrope');m.querySelectorAll('button').forEach(b=>b.onmouseenter=()=>b.style.background='var(--panel2)');document.body.append(m)}document.addEventListener('contextmenu',e=>{if(e.target.closest('.desktop')&&!e.target.closest('.app-icon')&&!e.target.closest('.window'))showContext(e,null)});document.addEventListener('click',hideContext)
function openApp(id){launcher.classList.add('hidden');let w=windows[id];if(w){w.classList.remove('min');focus(w);return}w=document.createElement('section');w.className='window';w.dataset.id=id;w.style.left=(innerWidth<650?0:90+Object.keys(windows).length*36)+'px';w.style.top=(innerWidth<650?0:72+Object.keys(windows).length*28)+'px';w.innerHTML=`<div class="titlebar" title="Double-click to maximize"><span class="titleicon">${APPS[id][0]}</span><span class="wtitle">${appName(id)}</span><div class="controls"><button data-window-action="min" aria-label="${i18n('Minimize')}">—</button><button data-window-action="max" aria-label="${i18n('Maximize')}">□</button><button data-window-action="close" aria-label="${i18n('Close')}">×</button></div></div><div class="content" id="content-${id}"></div>`;windows[id]=w;document.querySelector('#windows').append(w);let controls=w.querySelector('.controls'),bar=w.querySelector('.titlebar');controls.addEventListener('pointerdown',e=>{e.preventDefault();e.stopPropagation()});controls.addEventListener('click',e=>{let action=e.target.closest('button')?.dataset.windowAction;if(!action)return;e.preventDefault();e.stopPropagation();if(action==='min')minWin(id);if(action==='max')maxWin(id);if(action==='close')closeWin(id)});bar.addEventListener('dblclick',e=>{if(!e.target.closest('.controls'))maxWin(id)});w.addEventListener('pointerdown',()=>focus(w));drag(w);renderApp(id);focus(w)}function focus(w){if(!w)return;activeWindow=w.dataset.id;w.style.zIndex=++z;document.querySelectorAll('.window').forEach(x=>x.classList.toggle('active-window',x===w));renderTaskbar()}function closeWin(id){windows[id]?.remove();delete windows[id];if(activeWindow===id){activeWindow=Object.keys(windows).find(k=>!windows[k].classList.contains('min'))||null;if(activeWindow)focus(windows[activeWindow])}renderTaskbar()}function minWin(id){let w=windows[id];if(!w)return;w.classList.add('min');if(activeWindow===id){activeWindow=Object.keys(windows).find(k=>k!==id&&!windows[k].classList.contains('min'))||null;if(activeWindow)focus(windows[activeWindow])}renderTaskbar();toast(i18n('MINIMIZED — TAP THE APP ICON TO RESTORE'))}function maxWin(id){let w=windows[id];if(!w)return;w.classList.toggle('max');focus(w)}function snapWindow(w){if(innerWidth<650||w.classList.contains('max'))return;let r=w.getBoundingClientRect(),edge=22;if(r.top<edge){w.classList.add('max');return}if(r.left<edge){w.style.left='18px';w.style.top='18px';w.style.width='calc(50vw - 27px)';w.style.height='calc(100vh - 98px)'}else if(r.right>innerWidth-edge){w.style.left='calc(50vw + 9px)';w.style.top='18px';w.style.width='calc(50vw - 27px)';w.style.height='calc(100vh - 98px)'}}function drag(w){let bar=w.querySelector('.titlebar'),x,y,l,t;bar.onpointerdown=e=>{if(e.target.closest('.controls')||innerWidth<650)return;if(w.classList.contains('max'))w.classList.remove('max');x=e.clientX;y=e.clientY;l=w.offsetLeft;t=w.offsetTop;bar.setPointerCapture(e.pointerId);bar.onpointermove=e=>{w.style.left=Math.max(0,l+e.clientX-x)+'px';w.style.top=Math.max(0,t+e.clientY-y)+'px'};bar.onpointerup=()=>{bar.onpointermove=null;snapWindow(w)}}}document.addEventListener('keydown',e=>{if(e.altKey&&e.key==='F4'&&activeWindow){e.preventDefault();closeWin(activeWindow)}if(e.key==='Escape'&&activeWindow&&!document.querySelector('.modal')&&!document.querySelector('#lifeos-search')){let w=windows[activeWindow];if(w?.classList.contains('max')){e.preventDefault();maxWin(activeWindow)}}})
function renderApp(id){let c=document.querySelector('#content-'+id);if(!c)return;if(id==='today')today(c);if(id==='timeline')timeline(c);if(id==='museum')museumApp(c);if(id==='insights')insights(c);if(id==='profile')profileApp(c);if(id==='tasks')tasksApp(c);if(id==='calendar')calendarApp(c);if(id==='system')systemApp(c);setTimeout(applyLanguage,0)}function renderAll(){Object.keys(windows).forEach(renderApp);renderTaskbar();setTimeout(applyLanguage,0)}document.addEventListener('click',e=>{if(e.target.closest('#content-today .chip,#content-today .energy button'))setTimeout(applyLanguage,0)})
let todayDraft=null;function today(c){let m=mems().find(x=>x.date===day());if(todayDraft===null){selectedMood=m?.mood||'cloudy';selectedEnergy=m?.energy||3}let f=m||filmFor(selectedMood,selectedEnergy),text=todayDraft===null?(m?.text||''):todayDraft;c.innerHTML=`<div class="eyebrow">${new Date().toLocaleDateString('en-US',{weekday:'long'}).toUpperCase()}</div><h1>${fmtDate(day()).replace(/^[^,]+, /,'')}</h1><p class="sub">How was your day?</p><div class="today-grid"><div><textarea id="entry" class="entry" placeholder="Write something about today..." oninput="todayDraft=this.value">${esc(text)}</textarea><div class="actions"><button class="primary" onclick="archiveToday()">${m?'UPDATE MEMORY':'ARCHIVE TODAY'}</button><span id="saveStatus" class="status"></span></div></div><aside><div class="eyebrow">TODAY'S WEATHER</div><div class="choices">${['clear','light','cloudy','rain','storm'].map(x=>`<button class="chip ${x===selectedMood?'selected':''}" onclick="setMood('${x}')">${x.toUpperCase()}</button>`).join('')}</div><div class="eyebrow" style="margin-top:22px">ENERGY LEVEL</div><div class="energy">${[1,2,3,4,5].map(x=>`<button class="${x===selectedEnergy?'selected':''}" onclick="setEnergy(${x})">${x}</button>`).join('')}</div><div class="film"><div class="eyebrow">TODAY'S FILM</div><div class="film-title">${f.filmTitle}</div><div class="sub">${f.filmSubtitle}</div><p class="film-copy">“${f.filmLine}”</p></div></aside></div>`}function filmFor(m,e){let n=Math.abs([...m].reduce((a,c)=>a+c.charCodeAt(0),e*7)+new Date().getDate())%films.length;return{filmTitle:films[n][0],filmSubtitle:films[n][1],filmLine:lines[(n*3+e)%lines.length]}}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)}function setMood(x){selectedMood=x;rerenderToday()}function setEnergy(x){selectedEnergy=x;rerenderToday()}function archiveToday(){let text=document.querySelector('#entry').value.trim(),a=mems(),old=a.find(x=>x.date===day()),f=old||filmFor(selectedMood,selectedEnergy),now=new Date().toISOString(),m={id:old?.id||'memory_'+crypto.randomUUID(),date:day(),text,mood:selectedMood,energy:selectedEnergy,filmTitle:f.filmTitle,filmSubtitle:f.filmSubtitle,filmLine:f.filmLine,createdAt:old?.createdAt||now,updatedAt:now};let status=document.querySelector('#saveStatus');status.textContent='ARCHIVING MEMORY...';setTimeout(()=>{save('lifeos_memories',[...a.filter(x=>x.date!==day()),m].sort((x,y)=>y.date.localeCompare(x.date)));todayDraft=null;status.textContent='MEMORY ARCHIVED';renderAll()},360)}
let todayDraft=null,todayTitleDraft=null,todayTargetDate=null;function today(c){let m=mems().find(x=>x.date===day());if(todayDraft===null){selectedMood=m?.mood||'cloudy';selectedEnergy=m?.energy||3}let f=m||filmFor(selectedMood,selectedEnergy),text=todayDraft===null?(m?.text||''):todayDraft;c.innerHTML=`<div class="eyebrow">${new Date().toLocaleDateString('en-US',{weekday:'long'}).toUpperCase()}</div><h1>${fmtDate(day()).replace(/^[^,]+, /,'')}</h1><p class="sub">How was your day?</p><div class="today-grid"><div><textarea id="entry" class="entry" placeholder="Write something about today..." oninput="todayDraft=this.value">${esc(text)}</textarea><div class="actions"><button class="primary" onclick="archiveToday()">${m?'UPDATE MEMORY':'ARCHIVE TODAY'}</button><span id="saveStatus" class="status"></span></div></div><aside><div class="eyebrow">TODAY'S WEATHER</div><div class="choices">${['clear','light','cloudy','rain','storm'].map(x=>`<button class="chip ${x===selectedMood?'selected':''}" onclick="setMood('${x}')">${x.toUpperCase()}</button>`).join('')}</div><div class="eyebrow" style="margin-top:22px">ENERGY LEVEL</div><div class="energy">${[1,2,3,4,5].map(x=>`<button class="${x===selectedEnergy?'selected':''}" onclick="setEnergy(${x})">${x}</button>`).join('')}</div><div class="film"><div class="eyebrow">TODAY'S FILM</div><div class="film-title">${f.filmTitle}</div><div class="sub">${f.filmSubtitle}</div><p class="film-copy">“${f.filmLine}”</p></div></aside></div>`}function filmFor(m,e){let seed=[...`${day()}|${m}|${e}`].reduce((a,c,i)=>a+c.charCodeAt(0)*(i+1),0),n=seed%films.length;return{filmTitle:films[n][0],filmSubtitle:films[n][1],filmLine:lines[(n*3+e)%lines.length]}}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)}function setMood(x){selectedMood=x;rerenderToday()}function setEnergy(x){selectedEnergy=x;rerenderToday()}function archiveToday(){let text=document.querySelector('#entry').value.trim(),a=mems(),old=a.find(x=>x.date===day()),f=old||filmFor(selectedMood,selectedEnergy),now=new Date().toISOString(),m={id:old?.id||'memory_'+crypto.randomUUID(),date:day(),text,mood:selectedMood,energy:selectedEnergy,filmTitle:f.filmTitle,filmSubtitle:f.filmSubtitle,filmLine:f.filmLine,createdAt:old?.createdAt||now,updatedAt:now};let status=document.querySelector('#saveStatus');status.textContent='ARCHIVING MEMORY...';setTimeout(()=>{save('lifeos_memories',[...a.filter(x=>x.date!==day()),m].sort((x,y)=>y.date.localeCompare(x.date)));todayDraft=null;status.textContent='MEMORY ARCHIVED';renderAll()},360)}
function timeline(c){let a=mems();c.innerHTML=`<div class="eyebrow">MEMORY ARCHIVE</div><h1>${a.length} ${a.length===1?'MEMORY':'MEMORIES'} ARCHIVED</h1>${a.length?`<div class="timeline">${a.map(m=>`<article class="memory"><button onclick="detail('${m.id}')"><div class="date">${shortDate(m.date)} · ${m.mood.toUpperCase()} · ENERGY ${m.energy}</div><div class="mtitle">${esc(m.filmTitle)}</div><div class="snippet">${esc(m.text||'No written note.')}</div></button></article>`).join('')}</div>`:'<div class="empty">No memories yet.<br>Open Today when you are ready to keep one.</div>'}`}function detail(id){let m=mems().find(x=>x.id===id);if(!m)return;modal.innerHTML=`<div class="modal"><div class="modalbox"><div class="eyebrow">MEMORY DETAIL</div><h1>${esc(m.filmTitle)}</h1><p class="sub">${fmtDate(m.date)} · ${m.mood.toUpperCase()} · ENERGY ${m.energy}</p><p style="line-height:1.65">${esc(m.text||'No written note.').replace(/\n/g,'<br>')}</p><p class="film-copy">${m.filmSubtitle}</p><div class="actions"><button class="primary" onclick="editMemory('${m.id}')">EDIT</button><button class="secondary" onclick="preserve('${m.id}')">${museum().some(x=>x.memoryId===m.id)?'PRESERVED':'ADD TO MUSEUM'}</button><button class="danger" onclick="confirmDelete('${m.id}')">DELETE</button></div></div></div>`}function editMemory(id){let m=mems().find(x=>x.id===id);modal.innerHTML='';openApp('today');selectedMood=m.mood;selectedEnergy=m.energy;let cur=mems(),ix=cur.findIndex(x=>x.id===id);cur[ix].date=day();save('lifeos_memories',cur);today(document.querySelector('#content-today'));document.querySelector('#entry').value=m.text;toast('EDITING AS TODAY — SAVE TO UPDATE')}function preserve(id){let m=mems().find(x=>x.id===id),a=museum();if(!a.some(x=>x.memoryId===id)){save('lifeos_museum',[...a,{id:'artifact_'+crypto.randomUUID(),memoryId:id,preservedAt:new Date().toISOString()}]);toast('MEMORY PRESERVED')}modal.innerHTML='';renderAll()}function confirmDelete(id){modal.innerHTML=`<div class="modal"><div class="modalbox"><div class="eyebrow">CONFIRM DELETE</div><h2>Remove this memory?</h2><p class="sub">This cannot be undone.</p><div class="actions"><button class="danger" onclick="deleteMemory('${id}')">DELETE MEMORY</button><button class="secondary" onclick="modal.innerHTML=''">CANCEL</button></div></div></div>`}function deleteMemory(id){save('lifeos_memories',mems().filter(x=>x.id!==id));save('lifeos_museum',museum().filter(x=>x.memoryId!==id));modal.innerHTML='';renderAll();toast('MEMORY REMOVED')}
function museumApp(c){let a=museum(),ms=mems();c.innerHTML=`<div class="eyebrow">MUSEUM</div><h1>Preserved moments</h1><p class="sub">Worth keeping for the long view.</p>${a.length?`<div class="museum">${a.map(x=>{let m=ms.find(q=>q.id===x.memoryId);return m?`<button class="artifact" onclick="detail('${m.id}')"><span>COLLECTION #${x.id.slice(-4).toUpperCase()}</span><strong>${esc(m.filmTitle)}</strong><span>${fmtDate(m.date)}<br><br>STATUS: PRESERVED</span></button>`:''}).join('')}</div>`:'<div class="empty">A museum begins with one preserved moment.</div>'}`}
function museumApp(c){let a=museum(),ms=mems();c.innerHTML=`<div class="eyebrow">MUSEUM</div><h1>Preserved moments</h1><p class="sub">Worth keeping for the long view.</p>${a.length?`<div class="museum">${a.map(x=>{let m=ms.find(q=>q.id===x.memoryId);return m?`<button class="artifact" onclick="detail('${m.id}')"><span>COLLECTION #${x.id.slice(-4).toUpperCase()}</span><strong>${esc(m.title||m.text?.trim().split('\n')[0].slice(0,64)||i18n('timeline.untitled'))}</strong><span>${fmtDate(m.date)}<br><br>STATUS: PRESERVED</span></button>`:''}).join('')}</div>`:'<div class="empty">A museum begins with one preserved moment.</div>'}`}
function streak(a){let s=0,d=new Date();while(a.some(m=>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=`<div class="eyebrow">LAST 30 DAYS</div><h1>Insights, quietly.</h1><div class="insights"><section class="metric"><div class="eyebrow">WEATHER</div>${Object.entries(cnt).map(([k,v])=>`<div class="sub">${cap(k)} <b style="float:right;color:var(--ink)">${v} DAYS</b></div>`).join('')}</section><section class="metric"><div class="eyebrow">AVERAGE ENERGY</div><div class="big">${avg}<span style="font-size:16px"> / 5</span></div><div class="bars">${a.slice(0,10).reverse().map(x=>`<i class="bar" style="height:${x.energy*18}%"></i>`).join('')||'<span class="sub">No entries yet</span>'}</div></section><section class="metric"><div class="eyebrow">MOST ACTIVE DAY</div><div class="big" style="font-size:26px">${active}</div></section><section class="metric"><div class="eyebrow">CURRENT STREAK</div><div class="big">${streak(a)}<span style="font-size:16px"> DAYS</span></div><div class="sub">Longest streak: ${longest(a)} days</div></section></div>`}
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=`<div class="eyebrow">SYSTEM PROFILE</div><h1>${p.name?esc(p.name)+' · ':''}LIFE OS ONLINE</h1><div class="profile-grid"><div class="stat"><b>${started}</b><span>DAYS ONLINE</span></div><div class="stat"><b>${a.length}</b><span>MEMORIES</span></div><div class="stat"><b>${museum().length}</b><span>MUSEUM ITEMS</span></div><div class="stat"><b>${streak(a)}</b><span>CURRENT STREAK</span></div><div class="stat"><b>${longest(a)}</b><span>LONGEST STREAK</span></div><div class="stat"><b>${cap(common)}</b><span>COMMON WEATHER</span></div></div><div class="eyebrow">PERSONAL SYSTEM</div><div class="formrow"><label>Display Name</label><input id="pname" value="${esc(p.name)}"></div><div class="formrow"><label>Birthday (optional)</label><input id="pbday" type="date" value="${esc(p.birthday)}"></div><div class="formrow"><label>Life OS Start Date</label><input id="pstart" type="date" value="${esc(p.startDate)}"></div><div class="formrow"><label>Personal Motto</label><input id="pmotto" value="${esc(p.motto)}"></div><div class="actions"><button class="primary" onclick="saveProfile()">SAVE PROFILE</button><span class="status">AVERAGE ENERGY ${avg} / 5</span></div>`}function saveProfile(){save('lifeos_profile',{name:pname.value,birthday:pbday.value,startDate:pstart.value||day(),motto:pmotto.value});renderAll();toast('PROFILE SAVED')}
function today(c){let m=mems().find(x=>x.date===day());if(todayDraft===null){selectedMood=m?.mood||'cloudy';selectedEnergy=m?.energy||3}let f=m||filmFor(selectedMood,selectedEnergy),text=todayDraft===null?(m?.text||''):todayDraft,cWeek=new Date().toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',{weekday:'long'}).toUpperCase();c.innerHTML=`<div class="eyebrow">${cWeek}</div><h1>${fmtDate(day()).replace(/^[^,]+, /,'')}</h1><p class="sub">${i18n('How was your day?')}</p><div class="today-grid"><div><textarea id="entry" class="entry" placeholder="${i18n('Write something about today...')}" oninput="todayDraft=this.value">${esc(text)}</textarea><div class="actions"><button class="primary" onclick="archiveToday()">${i18n(m?'UPDATE MEMORY':'ARCHIVE TODAY')}</button><span id="saveStatus" class="status"></span></div></div><aside><div class="eyebrow">${i18n("TODAY'S WEATHER")}</div><div class="choices">${['clear','light','cloudy','rain','storm'].map(x=>`<button class="chip ${x===selectedMood?'selected':''}" onclick="setMood('${x}')">${moodName(x)}</button>`).join('')}</div><div class="eyebrow" style="margin-top:22px">${i18n('ENERGY LEVEL')}</div><div class="energy">${[1,2,3,4,5].map(x=>`<button class="${x===selectedEnergy?'selected':''}" onclick="setEnergy(${x})">${x}</button>`).join('')}</div><div class="film"><div class="eyebrow">${i18n("TODAY'S FILM")}</div><div class="film-title">${f.filmTitle}</div><div class="sub">${f.filmSubtitle}</div><p class="film-copy">“${f.filmLine}”</p></div></aside></div>`}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)}