diff --git a/package.json b/package.json
index 328673e..e151b17 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "life-os",
- "version": "1.4.2",
+ "version": "1.4.3",
"private": true,
"type": "module",
"scripts": {
diff --git a/public/addons.js b/public/addons.js
index c2d366d..3099c56 100644
--- a/public/addons.js
+++ b/public/addons.js
@@ -1,7 +1,9 @@
/* Life OS reliability and productivity add-ons. */
(() => {
const DRAFT_KEY = 'lifeos_today_draft_v1';
- const APP_VERSION = '1.4.2';
+ const localDay = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; };
+ const escapeHtml = value => String(value || '').replace(/[&<>]/g, char => ({ '&': '&', '<': '<', '>': '>' }[char]));
+ const APP_VERSION = '1.4.3';
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system'];
const t = (key, values) => LifeI18n.t(key, values);
@@ -9,11 +11,11 @@
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() }));
+ localStorage.setItem(DRAFT_KEY, JSON.stringify({ date: (typeof todayTargetDate !== 'undefined' && todayTargetDate) || localDay(), text, updatedAt: new Date().toISOString() }));
}
function restoreDraft(entry) {
const draft = readDraft();
- const today = new Date().toISOString().slice(0, 10);
+ const today = (typeof todayTargetDate !== 'undefined' && todayTargetDate) || localDay();
if (!draft || draft.date !== today || !draft.text || entry.value) return;
entry.value = draft.text;
entry.dispatchEvent(new Event('input', { bubbles: true }));
@@ -29,7 +31,7 @@
function wireLanguageControl() { /* The System renderer owns this control. */ }
function clearDraftAfterSave() {
setTimeout(() => {
- const today = new Date().toISOString().slice(0, 10);
+ const today = (typeof todayTargetDate !== 'undefined' && todayTargetDate) || localDay();
try {
const saved = JSON.parse(localStorage.getItem('lifeos_memories') || '[]').some(m => m.date === today);
if (saved) localStorage.removeItem(DRAFT_KEY);
@@ -50,8 +52,8 @@
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 => `${m.date} · ${t(`weather.${m.mood || 'cloudy'}`).toUpperCase()} ${m.filmTitle || t('addon.untitled')} ${(m.text || t('timeline.noNote')).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c]))} `).join('') : `
${t('addon.noResults')}
`;
+ const found = memories.filter(m => !query || [m.title, m.filmTitle, m.text, m.mood, m.date].join(' ').toLowerCase().includes(query)).slice(0, 8);
+ results.innerHTML = found.length ? found.map(m => `${m.date} · ${t(`weather.${m.mood || 'cloudy'}`).toUpperCase()} ${escapeHtml(m.title || m.text?.trim().split('\n')[0].slice(0, 64) || t('timeline.untitled'))} ${(m.text || t('timeline.noNote')).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c]))} `).join('') : `${t('addon.noResults')}
`;
results.querySelectorAll('button').forEach(button => button.addEventListener('click', () => { closeSearch(); openApp('timeline'); setTimeout(() => detail(button.dataset.id), 20); }));
};
input.addEventListener('input', render);
diff --git a/public/calendar.js b/public/calendar.js
index 6c77e3a..23a0d47 100644
--- a/public/calendar.js
+++ b/public/calendar.js
@@ -13,7 +13,7 @@
function eventsFor(key) {
const events = [];
- for (const memory of mems()) if (memory.date === key) events.push({ kind: 'memory', rank: 0, title: memory.filmTitle || t('memory'), detail: memory.text || LifeI18n.t('timeline.noNote'), time: '', id: memory.id, mood: memory.mood, energy: memory.energy });
+ for (const memory of mems()) if (memory.date === key) events.push({ kind: 'memory', rank: 0, title: memory.title || memory.text?.trim().split('\n')[0].slice(0, 64) || t('timeline.untitled'), detail: memory.text || LifeI18n.t('timeline.noNote'), time: '', id: memory.id, mood: memory.mood, energy: memory.energy });
const board = system().tasksBoard || { goals: [], tasks: [] };
for (const task of board.tasks || []) {
const due = task.dueDate === key, completed = localDatePart(task.completedAt) === key;
@@ -51,7 +51,7 @@
function selectedDayMarkup() {
const items = eventsFor(selected);
- return `${t('dayAtGlance')}
${esc(formatDate(selected))} ${items.length} ${items.length ? `${items.map(eventMarkup).join('')}
` : `${t('emptyDay')}
${t('openToday')} ${t('openTasks')}
`} `;
+ return `${t('dayAtGlance')}
${esc(formatDate(selected))} ${items.length} ${t('writeMemory')}
${items.length ? `${items.map(eventMarkup).join('')}
` : `${t('emptyDay')}
${t('openTasks')}
`} `;
}
function render(container) {
@@ -71,6 +71,6 @@
window.moveCalendarDay = delta => { const date = fromKey(selected); date.setDate(date.getDate() + delta); selected = localKey(date); month = fromKey(selected); renderAll(); };
const style = document.createElement('style');
- style.textContent = `.cal-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:16px 0}.cal-title,.cal-toolbar-actions,.cal-view-switch{display:flex;align-items:center}.cal-title{gap:8px}.cal-title h1{font-size:22px;min-width:130px;text-align:center;margin:0}.cal-nav{border:1px solid var(--line);background:var(--panel2);color:var(--ink);border-radius:8px;width:32px;height:32px;font-size:21px;line-height:1}.cal-toolbar-actions{gap:8px}.cal-view-switch{padding:3px;background:var(--panel2);border:1px solid var(--line);border-radius:9px}.cal-view-switch button{border:0;background:transparent;color:var(--muted);border-radius:6px;padding:7px 10px;font:10px 'DM Mono'}.cal-view-switch button.active{background:var(--panel);color:var(--ink);box-shadow:0 1px 4px #0002}.cal-today{padding:9px 11px;font-size:9px}.cal-month-layout{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(220px,.85fr);gap:15px;align-items:start}.cal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr))}.cal-weekdays{margin-bottom:5px}.cal-weekday{text-align:center;padding:5px 0;color:var(--muted);font:9px 'DM Mono';letter-spacing:.05em}.cal-weekday.weekend{color:var(--accent)}.cal-days{gap:3px}.cal-cell,.cal-blank{min-width:0;min-height:57px}.cal-cell{position:relative;display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:7px;border:1px solid transparent;border-radius:9px;background:transparent;color:var(--ink);text-align:left}.cal-cell:hover{background:var(--panel2)}.cal-cell.selected{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 9%,var(--panel))}.cal-cell.today .cal-number{color:var(--accent);font-weight:700}.cal-number{font-size:12px}.cal-dots{display:flex;gap:3px}.cal-dots i,.cal-legend i{display:block;width:5px;height:5px;border-radius:50%;background:var(--muted)}.cal-dots i.memory,.cal-legend i.memory{background:var(--accent)}.cal-dots i.task,.cal-legend i.task{background:#78a7d7}.cal-dots i.goal,.cal-legend i.goal{background:#c38dd3}.cal-legend{display:flex;gap:12px;flex-wrap:wrap;padding:10px 2px 0;color:var(--muted);font-size:9px}.cal-legend span{display:flex;align-items:center;gap:5px}.cal-day-card{min-width:0;padding:14px;border:1px solid var(--line);border-radius:12px;background:color-mix(in srgb,var(--panel2) 45%,transparent)}.cal-day-head{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:9px}.cal-day-head .eyebrow{margin-bottom:5px}.cal-day-head h2{font-size:15px;line-height:1.35;margin:0}.cal-day-count{display:grid;place-items:center;min-width:25px;height:25px;padding:0 5px;border-radius:99px;background:var(--panel);color:var(--muted);font:10px 'DM Mono'}.cal-events{display:grid;gap:5px}.cal-event{display:flex;align-items:flex-start;gap:8px;width:100%;padding:8px;border:1px solid transparent;border-radius:8px;background:transparent;color:var(--ink);text-align:left}.cal-event:hover{background:var(--panel);border-color:var(--line)}.cal-event-mark{width:18px;flex:none;text-align:center;color:var(--accent);font-size:13px}.cal-event.task .cal-event-mark{color:#78a7d7}.cal-event.goal .cal-event-mark{color:#c38dd3}.cal-event-body{display:grid;gap:3px;min-width:0;flex:1}.cal-event-meta{color:var(--muted);font:8px 'DM Mono';letter-spacing:.06em;text-transform:uppercase}.cal-event-body strong{font-size:11px;line-height:1.4;overflow-wrap:anywhere}.cal-event-detail{color:var(--muted);font-size:10px;line-height:1.4;overflow-wrap:anywhere}.cal-event.complete .cal-event-body strong{text-decoration:line-through;color:var(--muted)}.cal-event-arrow{color:var(--muted);font-size:11px}.cal-empty{padding:20px 4px 7px;text-align:center;color:var(--muted);font-size:11px;line-height:1.6}.cal-empty-actions{display:flex;justify-content:center;gap:6px;margin-top:10px}.cal-empty-actions .secondary,.cal-day-navigate .secondary{font-size:9px;padding:8px}.cal-single-day{max-width:580px;margin:10px auto}.cal-single-day .cal-day-card{padding:19px}.cal-single-day .cal-day-head h2{font-size:19px}.cal-single-day .cal-event{padding:11px}.cal-single-day .cal-event-body strong{font-size:13px}.cal-day-navigate{display:flex;justify-content:space-between;margin-top:10px}@media(max-width:700px){.cal-month-layout{grid-template-columns:1fr}.cal-toolbar{align-items:flex-start;flex-wrap:wrap}.cal-title{gap:5px}.cal-title h1{font-size:18px;min-width:112px}.cal-toolbar-actions{gap:6px}.cal-cell,.cal-blank{min-height:46px}.cal-cell{padding:5px 4px;gap:6px}.cal-number{font-size:11px}.cal-legend{gap:9px}.cal-day-card{padding:12px}.cal-event{padding:8px 5px}}`;
+ style.textContent = `.cal-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:16px 0}.cal-title,.cal-toolbar-actions,.cal-view-switch{display:flex;align-items:center}.cal-title{gap:8px}.cal-title h1{font-size:22px;min-width:130px;text-align:center;margin:0}.cal-nav{border:1px solid var(--line);background:var(--panel2);color:var(--ink);border-radius:8px;width:32px;height:32px;font-size:21px;line-height:1}.cal-toolbar-actions{gap:8px}.cal-view-switch{padding:3px;background:var(--panel2);border:1px solid var(--line);border-radius:9px}.cal-view-switch button{border:0;background:transparent;color:var(--muted);border-radius:6px;padding:7px 10px;font:10px 'DM Mono'}.cal-view-switch button.active{background:var(--panel);color:var(--ink);box-shadow:0 1px 4px #0002}.cal-today{padding:9px 11px;font-size:9px}.cal-month-layout{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(220px,.85fr);gap:15px;align-items:start}.cal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr))}.cal-weekdays{margin-bottom:5px}.cal-weekday{text-align:center;padding:5px 0;color:var(--muted);font:9px 'DM Mono';letter-spacing:.05em}.cal-weekday.weekend{color:var(--accent)}.cal-days{gap:3px}.cal-cell,.cal-blank{min-width:0;min-height:57px}.cal-cell{position:relative;display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:7px;border:1px solid transparent;border-radius:9px;background:transparent;color:var(--ink);text-align:left}.cal-cell:hover{background:var(--panel2)}.cal-cell.selected{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 9%,var(--panel))}.cal-cell.today .cal-number{color:var(--accent);font-weight:700}.cal-number{font-size:12px}.cal-dots{display:flex;gap:3px}.cal-dots i,.cal-legend i{display:block;width:5px;height:5px;border-radius:50%;background:var(--muted)}.cal-dots i.memory,.cal-legend i.memory{background:var(--accent)}.cal-dots i.task,.cal-legend i.task{background:#78a7d7}.cal-dots i.goal,.cal-legend i.goal{background:#c38dd3}.cal-legend{display:flex;gap:12px;flex-wrap:wrap;padding:10px 2px 0;color:var(--muted);font-size:9px}.cal-legend span{display:flex;align-items:center;gap:5px}.cal-day-card{min-width:0;padding:14px;border:1px solid var(--line);border-radius:12px;background:color-mix(in srgb,var(--panel2) 45%,transparent)}.cal-day-head{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:9px}.cal-day-head .eyebrow{margin-bottom:5px}.cal-day-head h2{font-size:15px;line-height:1.35;margin:0}.cal-day-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap;justify-content:flex-end}.cal-add-memory{font-size:9px;padding:7px 8px}.cal-day-count{display:grid;place-items:center;min-width:25px;height:25px;padding:0 5px;border-radius:99px;background:var(--panel);color:var(--muted);font:10px 'DM Mono'}.cal-events{display:grid;gap:5px}.cal-event{display:flex;align-items:flex-start;gap:8px;width:100%;padding:8px;border:1px solid transparent;border-radius:8px;background:transparent;color:var(--ink);text-align:left}.cal-event:hover{background:var(--panel);border-color:var(--line)}.cal-event-mark{width:18px;flex:none;text-align:center;color:var(--accent);font-size:13px}.cal-event.task .cal-event-mark{color:#78a7d7}.cal-event.goal .cal-event-mark{color:#c38dd3}.cal-event-body{display:grid;gap:3px;min-width:0;flex:1}.cal-event-meta{color:var(--muted);font:8px 'DM Mono';letter-spacing:.06em;text-transform:uppercase}.cal-event-body strong{font-size:11px;line-height:1.4;overflow-wrap:anywhere}.cal-event-detail{color:var(--muted);font-size:10px;line-height:1.4;overflow-wrap:anywhere}.cal-event.complete .cal-event-body strong{text-decoration:line-through;color:var(--muted)}.cal-event-arrow{color:var(--muted);font-size:11px}.cal-empty{padding:20px 4px 7px;text-align:center;color:var(--muted);font-size:11px;line-height:1.6}.cal-empty-actions{display:flex;justify-content:center;gap:6px;margin-top:10px}.cal-empty-actions .secondary,.cal-day-navigate .secondary{font-size:9px;padding:8px}.cal-single-day{max-width:580px;margin:10px auto}.cal-single-day .cal-day-card{padding:19px}.cal-single-day .cal-day-head h2{font-size:19px}.cal-single-day .cal-event{padding:11px}.cal-single-day .cal-event-body strong{font-size:13px}.cal-day-navigate{display:flex;justify-content:space-between;margin-top:10px}@media(max-width:700px){.cal-month-layout{grid-template-columns:1fr}.cal-toolbar{align-items:flex-start;flex-wrap:wrap}.cal-title{gap:5px}.cal-title h1{font-size:18px;min-width:112px}.cal-toolbar-actions{gap:6px}.cal-cell,.cal-blank{min-height:46px}.cal-cell{padding:5px 4px;gap:6px}.cal-number{font-size:11px}.cal-legend{gap:9px}.cal-day-card{padding:12px}.cal-event{padding:8px 5px}}`;
document.head.append(style);
})();
diff --git a/public/i18n-renderers.js b/public/i18n-renderers.js
index d0d0beb..cce8090 100644
--- a/public/i18n-renderers.js
+++ b/public/i18n-renderers.js
@@ -8,6 +8,8 @@
const weather = mood => t(`weather.${mood}`);
const rootModal = () => document.getElementById('modal');
const linkedTasks = () => sys().tasksBoard?.tasks || [];
+ const escAttr = value => esc(value).replace(/\"/g, '"');
+ const titleOf = memory => memory.title?.trim() || memory.text?.trim().split('\n')[0].slice(0, 64) || t('timeline.untitled');
const tagList = value => [...new Set(String(value || '').split(/[,,]/).map(tag => tag.trim()).filter(Boolean))].slice(0, 12);
const memoryMetaFields = memory => `${t('memory.tags')} ${t('memory.place')} ${t('memory.linkTask')}${t('memory.noTask')} ${linkedTasks().map(task => `${esc(task.title)} `).join('')}
`;
@@ -15,6 +17,7 @@
window.moodName = weather;
window.fmtDate = uiDate;
window.shortDate = short;
+ window.addMemoryForDate = date => { todayTargetDate = date; todayDraft = null; todayTitleDraft = null; openApp('today'); };
window.closeModal = () => { rootModal().innerHTML = ''; };
window.applyLanguage = () => { document.documentElement.lang = lang() === 'zh' ? 'zh-CN' : 'en'; document.title = lang() === 'zh' ? 'Life OS / 人生操作系统' : 'Life OS'; };
window.setLanguage = value => {
@@ -23,37 +26,37 @@
toast(value === 'auto' ? t(lang() === 'zh' ? 'feedback.followingZh' : 'feedback.followingEn') : t(value === 'zh' ? 'feedback.languageZh' : 'feedback.languageEn'));
};
window.today = container => {
- const memory = mems().find(item => item.date === day());
+ const entryDate = todayTargetDate || day(), memory = mems().find(item => item.date === entryDate);
if (todayDraft === null) { selectedMood = memory?.mood || 'cloudy'; selectedEnergy = memory?.energy || 3; }
- const film = memory || filmFor(selectedMood, selectedEnergy), text = todayDraft === null ? (memory?.text || '') : todayDraft;
- container.innerHTML = `${weekday(day()).toUpperCase()}
${uiDate(day())} ${t('today.prompt')}
${memoryMetaFields(memory)}
${t(memory ? 'today.update' : 'today.archive')}
${t('today.weather')}
${['clear','light','cloudy','rain','storm'].map(item => `${weather(item)} `).join('')}
${t('today.energy')}
${[1,2,3,4,5].map(item => `${item} `).join('')}
${t('today.film')}
${film.filmTitle}
${film.filmSubtitle}
“${film.filmLine}”
`;
+ const film = filmFor(selectedMood, selectedEnergy), text = todayDraft === null ? (memory?.text || '') : todayDraft;
+ container.innerHTML = `${weekday(entryDate).toUpperCase()}
${uiDate(entryDate)} ${t('today.prompt')} ${entryDate !== day() ? `${t('today.backToToday')} ` : ''}
${t('today.weather')}
${['clear','light','cloudy','rain','storm'].map(item => `${weather(item)} `).join('')}
${t('today.energy')}
${[1,2,3,4,5].map(item => `${item} `).join('')}
${t('today.film')}
${film.filmTitle}
${film.filmSubtitle}
“${film.filmLine}”
`;
};
window.archiveToday = () => {
- const text = document.querySelector('#entry')?.value.trim() || '', old = mems().find(item => item.date === day()), film = old || filmFor(selectedMood, selectedEnergy), now = new Date().toISOString();
- const memory = { ...old, id: old?.id || `memory_${crypto.randomUUID()}`, date: day(), text, mood: selectedMood, energy: selectedEnergy, tags: tagList(document.querySelector('#memory-tags')?.value), location: document.querySelector('#memory-place')?.value.trim() || '', taskId: document.querySelector('#memory-task')?.value || '', filmTitle: film.filmTitle, filmSubtitle: film.filmSubtitle, filmLine: film.filmLine, createdAt: old?.createdAt || now, updatedAt: now };
+ const entryDate = todayTargetDate || day(), text = document.querySelector('#entry')?.value.trim() || '', old = mems().find(item => item.date === entryDate), film = filmFor(selectedMood, selectedEnergy), now = new Date().toISOString();
+ const memory = { ...old, id: old?.id || `memory_${crypto.randomUUID()}`, date: entryDate, text, title: document.querySelector('#memory-title')?.value.trim() || '', mood: selectedMood, energy: selectedEnergy, tags: tagList(document.querySelector('#memory-tags')?.value), location: document.querySelector('#memory-place')?.value.trim() || '', taskId: document.querySelector('#memory-task')?.value || '', filmTitle: film.filmTitle, filmSubtitle: film.filmSubtitle, filmLine: film.filmLine, createdAt: old?.createdAt || now, updatedAt: now };
const status = document.querySelector('#saveStatus'); status.textContent = t('today.archiving');
- setTimeout(() => { save('lifeos_memories', [...mems().filter(item => item.date !== day()), memory].sort((a, b) => b.date.localeCompare(a.date))); todayDraft = null; status.textContent = t('today.archived'); renderAll(); }, 360);
+ setTimeout(() => { save('lifeos_memories', [...mems().filter(item => item.date !== entryDate), memory].sort((a, b) => b.date.localeCompare(a.date))); todayDraft = null; todayTitleDraft = null; status.textContent = t('today.archived'); renderAll(); }, 360);
};
window.timeline = container => {
const all = mems(), tags = [...new Set(all.flatMap(item => item.tags || []))].sort((a,b) => a.localeCompare(b));
const query = window.timelineQuery || '', selectedTag = window.timelineTag || '';
const filtered = all.filter(memory => {
const task = linkedTasks().find(item => item.id === memory.taskId);
- const haystack = [memory.filmTitle, memory.text, memory.location, ...(memory.tags || []), task?.title].join(' ').toLocaleLowerCase();
+ const haystack = [memory.title, memory.filmTitle, memory.text, memory.location, ...(memory.tags || []), task?.title].join(' ').toLocaleLowerCase();
return (!query || haystack.includes(query.toLocaleLowerCase())) && (!selectedTag || (memory.tags || []).includes(selectedTag));
});
const isSaved = memory => museum().some(item => item.memoryId === memory.id);
- container.innerHTML = `${t('timeline.eyebrow')}
${t('timeline.heading',{count:filtered.length,total:all.length})} ${t('timeline.search')} ${t('timeline.filterTag')} ${t('timeline.allTags')} ${tags.map(tag => `${esc(tag)} `).join('')}
${filtered.length ? `${filtered.map(memory => { const task = linkedTasks().find(item => item.id === memory.taskId); return `
${short(memory.date)} · ${weather(memory.mood).toUpperCase()} · ${t('common.energy')} ${memory.energy}${isSaved(memory) ? ` · ★ ${t('timeline.favorite')}` : ''}
${esc(memory.filmTitle)}
${esc(memory.text || t('timeline.noNote'))}
${memory.location ? `⌖ ${esc(memory.location)} ` : ''}${task ? `↗ ${esc(task.title)} ` : ''}${(memory.tags || []).map(tag => `# ${esc(tag)} `).join('')}
`; }).join('')}
` : `${all.length ? t('timeline.noMatches') : `${t('timeline.emptyTitle')} ${t('timeline.emptyBody')}`}
`}`;
+ container.innerHTML = `${t('timeline.eyebrow')}
${t('timeline.heading',{count:filtered.length,total:all.length})} ${t('timeline.search')} ${t('timeline.filterTag')} ${t('timeline.allTags')} ${tags.map(tag => `${esc(tag)} `).join('')}
${filtered.length ? `${filtered.map(memory => { const task = linkedTasks().find(item => item.id === memory.taskId); return `
${short(memory.date)} · ${weather(memory.mood).toUpperCase()} · ${t('common.energy')} ${memory.energy}${isSaved(memory) ? ` · ★ ${t('timeline.favorite')}` : ''}
${esc(titleOf(memory))}
${esc(memory.text || t('timeline.noNote'))}
${memory.location ? `⌖ ${esc(memory.location)} ` : ''}${task ? `↗ ${esc(task.title)} ` : ''}${(memory.tags || []).map(tag => `# ${esc(tag)} `).join('')}
`; }).join('')}
` : `${all.length ? t('timeline.noMatches') : `${t('timeline.emptyTitle')} ${t('timeline.emptyBody')}`}
`}`;
};
window.setTimelineQuery = value => { window.timelineQuery = value; const field = document.querySelector('#content-timeline input[type=search]'); const start = field?.selectionStart; timeline(document.querySelector('#content-timeline')); const next = document.querySelector('#content-timeline input[type=search]'); next?.focus(); if (start !== undefined) next?.setSelectionRange(start,start); };
window.setTimelineTag = value => { window.timelineTag = value; timeline(document.querySelector('#content-timeline')); };
window.detail = id => {
const memory = mems().find(item => item.id === id); if (!memory) return;
const task = linkedTasks().find(item => item.id === memory.taskId), favorite = museum().some(item => item.memoryId === memory.id);
- rootModal().innerHTML = `× ${t('timeline.detail')}
${esc(memory.filmTitle)} ${uiDate(memory.date)} · ${weather(memory.mood)} · ${t('common.energy')} ${memory.energy}
${esc(memory.text || t('timeline.noNote')).replace(/\n/g,' ')}
${memory.location ? `
⌖ ${esc(memory.location)}
` : ''}${task ? `
↗ ${esc(t('memory.relatedTask'))}: ${esc(task.title)}
` : ''}${(memory.tags || []).length ? `
${memory.tags.map(tag => `# ${esc(tag)} `).join('')}
` : ''}
${esc(memory.filmSubtitle || '')}
${t('common.edit')} ${favorite ? t('timeline.preserved') : t('memory.favorite')} ${t('common.delete')}
`;
+ rootModal().innerHTML = `× ${t('timeline.detail')}
${esc(titleOf(memory))} ${uiDate(memory.date)} · ${weather(memory.mood)} · ${t('common.energy')} ${memory.energy}
${esc(memory.text || t('timeline.noNote')).replace(/\n/g,' ')}
${memory.location ? `
⌖ ${esc(memory.location)}
` : ''}${task ? `
↗ ${esc(t('memory.relatedTask'))}: ${esc(task.title)}
` : ''}${(memory.tags || []).length ? `
${memory.tags.map(tag => `# ${esc(tag)} `).join('')}
` : ''}
${t('today.film')}: ${esc(memory.filmTitle || '')} ${esc(memory.filmSubtitle || '')}
${t('common.edit')} ${favorite ? t('timeline.preserved') : t('memory.favorite')} ${t('common.delete')}
`;
};
- window.editMemory = id => { const memory = mems().find(item => item.id === id); if (!memory) return; rootModal().innerHTML = ``; };
- window.saveMemoryEdit = (event,id) => { event.preventDefault(); const all=mems(), memory=all.find(item=>item.id===id); if(!memory)return; memory.text=document.querySelector('#memory-edit-note').value.trim(); memory.tags=tagList(document.querySelector('#memory-tags').value); memory.location=document.querySelector('#memory-place').value.trim(); memory.taskId=document.querySelector('#memory-task').value||''; memory.updatedAt=new Date().toISOString(); save('lifeos_memories',all); closeModal(); renderAll(); detail(id); };
+ window.editMemory = id => { const memory = mems().find(item => item.id === id); if (!memory) return; rootModal().innerHTML = `× ${t('timeline.detail')}
${t('memory.edit')} ${uiDate(memory.date)}
${t('memory.title')} ${t('memory.note')}${esc(memory.text || '')} ${memoryMetaFields(memory)}${t('common.save')} ${t('common.cancel')}
`; };
+ window.saveMemoryEdit = (event,id) => { event.preventDefault(); const all=mems(), memory=all.find(item=>item.id===id); if(!memory)return; memory.title=document.querySelector('#memory-edit-title').value.trim(); memory.text=document.querySelector('#memory-edit-note').value.trim(); memory.tags=tagList(document.querySelector('#memory-tags').value); memory.location=document.querySelector('#memory-place').value.trim(); memory.taskId=document.querySelector('#memory-task').value||''; memory.updatedAt=new Date().toISOString(); save('lifeos_memories',all); closeModal(); renderAll(); detail(id); };
window.preserve = id => { const all = museum(), alreadyPreserved = all.some(item => item.memoryId === id); if (alreadyPreserved) { save('lifeos_museum', all.filter(item => item.memoryId !== id)); toast(t('timeline.unpreserved')); } else { save('lifeos_museum', [...all, { id: `artifact_${crypto.randomUUID()}`, memoryId: id, preservedAt: new Date().toISOString() }]); toast(t('timeline.added')); } closeModal(); renderAll(); };
window.confirmDelete = id => { rootModal().innerHTML = `${t('timeline.confirmDelete')}
${t('timeline.remove')} ${t('timeline.irreversible')}
${t('timeline.deleteMemory')} ${t('common.cancel')}
`; };
window.deleteMemory = id => { save('lifeos_memories', mems().filter(item => item.id !== id)); save('lifeos_museum', museum().filter(item => item.memoryId !== id)); rootModal().innerHTML = ''; renderAll(); toast(t('timeline.removed')); };
@@ -69,7 +72,7 @@
window.saveProfile = () => { save('lifeos_profile', { name: pname.value, birthday: pbday.value, startDate: pstart.value || day(), motto: pmotto.value }); renderAll(); toast(t('profile.saved')); };
window.systemApp = container => {
const state = settings(), bytes = new Blob([JSON.stringify({ mems: mems(), museum: museum(), profile: profile(), system: sys() })]).size, oldBackup = state.lastBackup && Date.now() - new Date(state.lastBackup) > 12096e5, detected = browserLanguage() === 'zh' ? t('system.detectedZh') : t('system.detectedEn');
- container.innerHTML = `${t('system.eyebrow')}
${t('system.title')} ${t('system.appearance')}
${['dark','light','auto'].map(item => `${t(`system.theme.${item}`)} `).join('')}
${t('system.language')}
${detected}
${t('system.auto')} ENGLISH 中文
${t('system.storage')}
${t('system.local')} ${lang() === 'zh' ? `${mems().length} 条记忆 · ${museum().length} 件博物馆藏品 · ${sys().tasksBoard?.tasks?.length || 0} 项待办 · 约 ${Math.ceil(bytes/1024)} KB` : `${mems().length} memories · ${museum().length} museum items · ${sys().tasksBoard?.tasks?.length || 0} tasks · Approx. ${Math.ceil(bytes/1024)} KB`}
${t('system.dataStorage')}
${t('system.stored')} ${t('system.backups')}
${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `${t('system.recommended')} ` : ''}
${t('system.version')}
${lang() === 'zh' ? 'Life OS 版本 1.4.2 · 备份格式 v2 · 数据库结构 v1' : 'Life OS Version 1.4.2 · Backup schema v2 · D1 schema v1'}
`;
+ container.innerHTML = `${t('system.eyebrow')}
${t('system.title')} ${t('system.appearance')}
${['dark','light','auto'].map(item => `${t(`system.theme.${item}`)} `).join('')}
${t('system.language')}
${detected}
${t('system.auto')} ENGLISH 中文
${t('system.storage')}
${t('system.local')} ${lang() === 'zh' ? `${mems().length} 条记忆 · ${museum().length} 件博物馆藏品 · ${sys().tasksBoard?.tasks?.length || 0} 项待办 · 约 ${Math.ceil(bytes/1024)} KB` : `${mems().length} memories · ${museum().length} museum items · ${sys().tasksBoard?.tasks?.length || 0} tasks · Approx. ${Math.ceil(bytes/1024)} KB`}
${t('system.dataStorage')}
${t('system.stored')} ${t('system.backups')}
${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `${t('system.recommended')} ` : ''}
${t('system.version')}
${lang() === 'zh' ? 'Life OS 版本 1.4.3 · 备份格式 v2 · 数据库结构 v1' : 'Life OS Version 1.4.3 · Backup schema v2 · D1 schema v1'}
`;
};
window.importPreview = () => { const data = importData, board = data.system?.tasksBoard || {}; rootModal().innerHTML = `${t('dialog.backup')}
${t('dialog.ready')} ${t('dialog.exported',{date:short(data.exportedAt.slice(0,10))})} ${t('dialog.memories',{count:data.memories.length})} ${t('dialog.museumItems',{count:data.museum.length})} ${t('dialog.goals',{count:board.goals?.length || 0})} ${t('dialog.tasks',{count:board.tasks?.length || 0})}
${t('dialog.merge')} ${t('dialog.replaceAll')} ${t('common.cancel')}
`; };
window.replaceConfirm = () => { rootModal().innerHTML = `${t('dialog.final')}
${t('dialog.replaceQuestion')} ${t('dialog.replaceWarning')}
${t('dialog.replace')} ${t('common.cancel')}
`; };
@@ -95,7 +98,7 @@
window.toast = key => { const message = key.includes('.') ? t(key) : key; const node = document.createElement('div'); node.className = 'toast'; node.textContent = message; document.body.append(node); setTimeout(() => node.remove(), 2000); };
window.minWin = id => { const windowNode = windows[id]; if (!windowNode) return; windowNode.classList.add('min'); if (activeWindow === id) { activeWindow = Object.keys(windows).find(key => key !== id && !windows[key].classList.contains('min')) || null; if (activeWindow) focus(windows[activeWindow]); } renderTaskbar(); toast('feedback.minimized'); };
const memoryStyle = document.createElement('style');
- memoryStyle.textContent = `.memory-meta-form{display:grid;gap:9px;margin-top:12px}.memory-meta-form label,.memory-edit-form>label{display:grid;gap:6px;color:var(--muted);font-size:12px}.memory-meta-form input,.memory-meta-form select,.memory-edit-form textarea{width:100%;min-width:0;border:1px solid var(--line);background:var(--panel2);border-radius:8px;padding:10px 11px;color:var(--ink);outline:none;font:inherit}.memory-meta-form input:focus,.memory-meta-form select:focus,.memory-edit-form textarea:focus,.timeline-tools input:focus,.timeline-tools select:focus{border-color:var(--accent)}.memory-edit-form{display:grid;gap:12px;max-height:min(80vh,720px);overflow:auto}.memory-edit-form h2{font-size:21px;margin:0}.memory-edit-form textarea{resize:vertical;line-height:1.55}.memory-edit-form .memory-meta-form{margin:0}.memory-edit-form .actions{margin:0}.timeline-tools{display:grid;grid-template-columns:minmax(0,1fr) 190px;gap:8px;margin:17px 0 11px}.timeline-tools input,.timeline-tools select{width:100%;min-width:0;height:40px;border:1px solid var(--line);border-radius:8px;background:var(--panel2);color:var(--ink);padding:0 11px;outline:none;font:13px Manrope,system-ui}.timeline-tools input::placeholder{color:var(--muted)}.memory-open{display:block!important;text-align:left}.memory-badges{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px}.memory-badges span,.memory-detail-meta{color:var(--muted);font-size:12px}.memory-badges span{border:1px solid var(--line);background:var(--panel2);border-radius:99px;padding:4px 8px}.detail-badges{margin:0 0 14px}.text-link{border:0;background:none;padding:0;color:var(--accent);text-decoration:underline}.memory-detail-meta{margin:8px 0}.memory-edit-form .sub{margin:0}@media(max-width:600px){.timeline-tools{grid-template-columns:1fr 130px}.memory-badges span{font-size:11px}.memory-meta-form label,.memory-edit-form>label{font-size:13px}}`;
+ memoryStyle.textContent = `.memory-meta-form{display:grid;gap:9px;margin-top:12px}.memory-title-field{display:grid;gap:6px;margin-top:10px;color:var(--muted);font-size:12px}.memory-title-field input,.memory-edit-form input{width:100%;min-width:0;border:1px solid var(--line);background:var(--panel2);border-radius:8px;padding:10px 11px;color:var(--ink);outline:none;font:inherit}.memory-title-field input:focus,.memory-edit-form input:focus{border-color:var(--accent)}.memory-meta-form label,.memory-edit-form>label{display:grid;gap:6px;color:var(--muted);font-size:12px}.memory-meta-form input,.memory-meta-form select,.memory-edit-form input,.memory-edit-form textarea{width:100%;min-width:0;border:1px solid var(--line);background:var(--panel2);border-radius:8px;padding:10px 11px;color:var(--ink);outline:none;font:inherit}.memory-meta-form input:focus,.memory-meta-form select:focus,.memory-edit-form input:focus,.memory-edit-form textarea:focus,.timeline-tools input:focus,.timeline-tools select:focus{border-color:var(--accent)}.memory-edit-form{display:grid;gap:12px;max-height:min(80vh,720px);overflow:auto}.memory-edit-form h2{font-size:21px;margin:0}.memory-edit-form textarea{resize:vertical;line-height:1.55}.memory-edit-form .memory-meta-form{margin:0}.memory-edit-form .actions{margin:0}.timeline-tools{display:grid;grid-template-columns:minmax(0,1fr) 190px;gap:8px;margin:17px 0 11px}.timeline-tools input,.timeline-tools select{width:100%;min-width:0;height:40px;border:1px solid var(--line);border-radius:8px;background:var(--panel2);color:var(--ink);padding:0 11px;outline:none;font:13px Manrope,system-ui}.timeline-tools input::placeholder{color:var(--muted)}.memory-open{display:block!important;text-align:left}.memory-badges{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px}.memory-badges span,.memory-detail-meta{color:var(--muted);font-size:12px}.memory-badges span{border:1px solid var(--line);background:var(--panel2);border-radius:99px;padding:4px 8px}.detail-badges{margin:0 0 14px}.text-link{border:0;background:none;padding:0;color:var(--accent);text-decoration:underline}.memory-detail-meta{margin:8px 0}.memory-edit-form .sub{margin:0}@media(max-width:600px){.timeline-tools{grid-template-columns:1fr 130px}.memory-badges span{font-size:11px}.memory-meta-form label,.memory-edit-form>label,.memory-title-field{font-size:13px}}`;
document.head.append(memoryStyle);
// Render every surface again after the overrides are installed; no DOM walking is involved.
renderIcons(); renderAll(); applyLanguage();
diff --git a/public/i18n.js b/public/i18n.js
index 4e721bf..0715c19 100644
--- a/public/i18n.js
+++ b/public/i18n.js
@@ -3,10 +3,10 @@
const en = {
app: { today: 'TODAY', timeline: 'TIMELINE', museum: 'MUSEUM', insights: 'INSIGHTS', profile: 'PROFILE', tasks: 'TASKS', calendar: 'CALENDAR', system: 'SYSTEM' },
common: { save: 'SAVE', cancel: 'CANCEL', edit: 'EDIT', delete: 'DELETE', close: 'Close', open: 'Open', minimize: 'Minimize', maximize: 'Maximize', restore: 'Restore window', energy: 'ENERGY', days: 'DAYS', never: 'NEVER' },
- today: { prompt: 'How was your day?', placeholder: 'Write something about today...', archive: 'ARCHIVE TODAY', update: 'UPDATE MEMORY', weather: "TODAY'S WEATHER", energy: 'ENERGY LEVEL', film: "TODAY'S FILM", archiving: 'ARCHIVING MEMORY...', archived: 'MEMORY ARCHIVED' },
+ today: { prompt: 'How was your day?', placeholder: 'Write something about today...', archive: 'ARCHIVE TODAY', update: 'UPDATE MEMORY', archiveDate: 'SAVE MEMORY', updateDate: 'UPDATE MEMORY', backToToday: 'Back to today', weather: "TODAY'S WEATHER", energy: 'ENERGY LEVEL', film: 'DAILY PROMPT', archiving: 'ARCHIVING MEMORY...', archived: 'MEMORY ARCHIVED' },
weather: { clear: 'Clear', light: 'Light', cloudy: 'Cloudy', rain: 'Rain', storm: 'Storm' },
- timeline: { eyebrow: 'MEMORY ARCHIVE', heading: '{count} OF {total} MEMORIES', search: 'Search memories', searchPlaceholder: 'Search notes, tags, places, tasks…', filterTag: 'Filter by tag', allTags: 'ALL TAGS', noMatches: 'No memories match these filters.', favorite: 'FAVORITE', emptyTitle: 'No memories yet.', emptyBody: 'Open Today when you are ready to keep one.', detail: 'MEMORY DETAIL', noNote: 'No written note.', editing: 'EDITING AS TODAY — SAVE TO UPDATE', preserved: 'FAVORITED', addMuseum: 'ADD TO MUSEUM', confirmDelete: 'CONFIRM DELETE', remove: 'Remove this memory?', irreversible: 'This cannot be undone.', deleteMemory: 'DELETE MEMORY', removed: 'MEMORY REMOVED', added: 'MEMORY FAVORITED', unpreserved: 'REMOVED FROM FAVORITES' },
- memory: { tags: 'TAGS', tagsPlaceholder: 'Separate tags with commas', place: 'PLACE / SCENE (OPTIONAL)', placePlaceholder: 'Where or what was the setting?', linkTask: 'RELATED TASK (OPTIONAL)', noTask: 'NO RELATED TASK', relatedTask: 'Related task', favorite: 'ADD TO FAVORITES', edit: 'EDIT MEMORY DETAILS', note: 'NOTE' },
+ timeline: { eyebrow: 'MEMORY ARCHIVE', heading: '{count} OF {total} MEMORIES', search: 'Search memories', searchPlaceholder: 'Search notes, tags, places, tasks…', filterTag: 'Filter by tag', allTags: 'ALL TAGS', noMatches: 'No memories match these filters.', favorite: 'FAVORITE', emptyTitle: 'No memories yet.', emptyBody: 'Open Today when you are ready to keep one.', detail: 'MEMORY DETAIL', untitled: 'Untitled memory', noNote: 'No written note.', editing: 'EDITING AS TODAY — SAVE TO UPDATE', preserved: 'FAVORITED', addMuseum: 'ADD TO MUSEUM', confirmDelete: 'CONFIRM DELETE', remove: 'Remove this memory?', irreversible: 'This cannot be undone.', deleteMemory: 'DELETE MEMORY', removed: 'MEMORY REMOVED', added: 'MEMORY FAVORITED', unpreserved: 'REMOVED FROM FAVORITES' },
+ memory: { tags: 'TAGS', tagsPlaceholder: 'Separate tags with commas', place: 'PLACE / SCENE (OPTIONAL)', placePlaceholder: 'Where or what was the setting?', linkTask: 'RELATED TASK (OPTIONAL)', noTask: 'NO RELATED TASK', relatedTask: 'Related task', favorite: 'ADD TO FAVORITES', edit: 'EDIT MEMORY DETAILS', note: 'NOTE', title: 'TITLE (OPTIONAL)', titlePlaceholder: 'Give this memory a title' },
museum: { eyebrow: 'MUSEUM', title: 'Preserved moments', subtitle: 'Worth keeping for the long view.', empty: 'A museum begins with one preserved moment.', preserved: 'STATUS: PRESERVED' },
insights: { eyebrow: 'LAST 30 DAYS', title: 'Insights, quietly.', weather: 'WEATHER', average: 'AVERAGE ENERGY', active: 'MOST ACTIVE DAY', current: 'CURRENT STREAK', longest: 'Longest streak: {count} days', noEntries: 'No entries yet' },
profile: { eyebrow: 'SYSTEM PROFILE', online: 'LIFE OS ONLINE', daysOnline: 'DAYS ONLINE', memories: 'MEMORIES', museumItems: 'MUSEUM ITEMS', streak: 'CURRENT STREAK', longest: 'LONGEST STREAK', commonWeather: 'COMMON WEATHER', personal: 'PERSONAL SYSTEM', name: 'Display Name', birthday: 'Birthday (optional)', start: 'Life OS Start Date', motto: 'Personal Motto', save: 'SAVE PROFILE', saved: 'PROFILE SAVED', average: 'AVERAGE ENERGY {value} / 5' },
@@ -14,16 +14,16 @@
dialog: { backup: 'LIFE OS BACKUP', ready: 'Ready to restore', exported: 'Exported: {date}', memories: 'Memories: {count}', museumItems: 'Museum Items: {count}', goals: 'Goals: {count}', tasks: 'Tasks: {count}', merge: 'MERGE', replaceAll: 'REPLACE ALL DATA', final: 'FINAL CONFIRMATION', replaceQuestion: 'Replace all Life OS data?', replaceWarning: 'This will replace all current Life OS data.', replace: 'REPLACE DATA', resetQuestion: 'Reset all local data?', resetWarning: 'Export a backup first if you want to keep it.', resetData: 'RESET DATA' },
feedback: { backup: 'BACKUP CREATED', invalid: 'INVALID BACKUP FILE', restored: 'SYSTEM RESTORED', restoring: 'RESTORING SYSTEM...', minimized: 'MINIMIZED — TAP THE APP ICON TO RESTORE', languageZh: 'Language set to Chinese', languageEn: 'Language set to English', followingZh: 'Following browser language: Chinese', followingEn: 'Following browser language: English' },
addon: { draftRestored: 'DRAFT RESTORED', searchTitle: 'SEARCH MEMORY ARCHIVE', searchPlaceholder: 'Search titles, notes, moods…', searchHint: 'ESC to close · click a result to open it', untitled: 'Untitled memory', noResults: 'No memories found.', syncPaused: 'CLOUD SYNC PAUSED — CHANGES STAY ON THIS DEVICE', syncReady: 'CLOUD SYNC READY', migrateQuestion: 'Move this browser’s Life OS to the cloud?', migrateBody: 'Your existing local records will become the first synced copy for this ChatGPT account.', migrate: 'MIGRATE & SYNC', fresh: 'START FRESH', migrated: 'LOCAL LIFE OS MIGRATED', cloudRestored: 'CLOUD LIFE OS RESTORED', cloudUnavailable: 'CLOUD STORAGE UNAVAILABLE — LOCAL MODE ACTIVE' },
- calendar: { eyebrow: 'MEMORIES & MILESTONES', month: 'MONTH', day: 'DAY', today: 'TODAY', mon: 'MON', tue: 'TUE', wed: 'WED', thu: 'THU', fri: 'FRI', sat: 'SAT', sun: 'SUN', previous: 'Previous month', next: 'Next month', previousDay: 'Previous day', nextDay: 'Next day', dayAtGlance: 'THAT DAY', memory: 'MEMORY', task: 'TASK', goal: 'GOAL', items: 'items', taskDue: 'TASK DUE', taskDone: 'TASK COMPLETED', goalCreated: 'GOAL CREATED', goalTarget: 'GOAL TARGET DATE', goalDone: 'GOAL COMPLETED', emptyDay: 'No memories or milestones recorded for this day.', openToday: 'Write a memory', openTasks: 'Open tasks' },
+ calendar: { eyebrow: 'MEMORIES & MILESTONES', month: 'MONTH', day: 'DAY', today: 'TODAY', mon: 'MON', tue: 'TUE', wed: 'WED', thu: 'THU', fri: 'FRI', sat: 'SAT', sun: 'SUN', previous: 'Previous month', next: 'Next month', previousDay: 'Previous day', nextDay: 'Next day', dayAtGlance: 'THAT DAY', memory: 'MEMORY', task: 'TASK', goal: 'GOAL', items: 'items', taskDue: 'TASK DUE', taskDone: 'TASK COMPLETED', goalCreated: 'GOAL CREATED', goalTarget: 'GOAL TARGET DATE', goalDone: 'GOAL COMPLETED', emptyDay: 'No memories or milestones recorded for this day.', openToday: 'Write a memory', writeMemory: 'Add memory', openTasks: 'Open tasks' },
tasks: { eyebrow: 'GOALS & TASKS', title: 'Make room for what matters.', subtitle: 'A small, steady plan for today and the days ahead.', addGoal: 'NEW GOAL', addTask: 'ADD TASK', taskPlaceholder: 'What needs doing?', goalPlaceholder: 'What do you want to move toward?', goalDescription: 'A note about this goal (optional)', finishedGoals: 'COMPLETED GOALS', goalComplete: 'COMPLETE GOAL', goalReopen: 'REOPEN GOAL', all: 'ALL', today: 'TODAY', upcoming: 'UPCOMING', done: 'DONE', activeGoals: 'ACTIVE GOALS', noGoals: 'Start with one goal worth moving toward.', noTasks: 'Nothing on this list yet.', due: 'DUE {date}', noDate: 'NO DUE DATE', priority: { low: 'LOW', normal: 'NORMAL', high: 'HIGH' }, goal: 'GOAL', anyGoal: 'NO GOAL', save: 'SAVE', cancel: 'CANCEL', complete: 'MARK COMPLETE', reopen: 'REOPEN', delete: 'DELETE', goalTitle: 'New goal', taskTitle: 'New task', goalCreated: 'GOAL CREATED', taskAdded: 'TASK ADDED', taskRemoved: 'TASK REMOVED', goalRemoved: 'GOAL REMOVED', confirmGoalDelete: 'Delete this goal?', confirmGoalDeleteBody: 'Tasks linked to it will be kept without a goal.', target: 'TARGET DATE', completed: 'COMPLETED', progress: '{done} OF {total} DONE', todayHeading: 'Focus for today', openCount: '{count} OPEN TASKS', overdue: 'OVERDUE' }
};
const zh = {
app: { today: '今日', timeline: '时间线', museum: '人生博物馆', insights: '洞察', profile: '个人资料', tasks: '目标与待办', calendar: '日历', system: '系统' },
common: { save: '保存', cancel: '取消', edit: '编辑', delete: '删除', close: '关闭', open: '打开', minimize: '最小化', maximize: '最大化', restore: '恢复窗口', energy: '能量', days: '天', never: '从未' },
- today: { prompt: '今天过得怎么样?', placeholder: '写下今天的片段…', archive: '归档今日', update: '更新记忆', weather: '今日天气', energy: '能量等级', film: '今日电影', archiving: '正在归档…', archived: '记忆已归档' },
+ today: { prompt: '今天过得怎么样?', placeholder: '写下今天的片段…', archive: '归档今日', update: '更新记忆', archiveDate: '保存记忆', updateDate: '更新记忆', backToToday: '返回今天', weather: '今日天气', energy: '能量等级', film: '今日灵感', archiving: '正在归档…', archived: '记忆已归档' },
weather: { clear: '晴朗', light: '微光', cloudy: '多云', rain: '雨天', storm: '风暴' },
- timeline: { eyebrow: '记忆档案', heading: '{count} / {total} 条记忆', search: '搜索记忆', searchPlaceholder: '搜索记录、标签、地点或待办…', filterTag: '按标签筛选', allTags: '全部标签', noMatches: '没有符合筛选条件的记忆。', favorite: '已收藏', emptyTitle: '还没有记忆。', emptyBody: '准备好记录时,打开“今日”。', detail: '记忆详情', noNote: '尚未写下记录。', editing: '正在以今日记录编辑 — 保存即可更新', preserved: '已收藏', addMuseum: '加入收藏', confirmDelete: '确认删除', remove: '删除这条记忆?', irreversible: '此操作无法撤销。', deleteMemory: '删除记忆', removed: '记忆已删除', added: '记忆已收藏', unpreserved: '已取消收藏' },
- memory: { tags: '标签', tagsPlaceholder: '多个标签请用逗号分隔', place: '地点 / 场景(选填)', placePlaceholder: '在哪里,或当时是什么场景?', linkTask: '关联待办(选填)', noTask: '不关联待办', relatedTask: '关联待办', favorite: '加入收藏', edit: '编辑记忆详情', note: '记录内容' },
+ timeline: { eyebrow: '记忆档案', heading: '{count} / {total} 条记忆', search: '搜索记忆', searchPlaceholder: '搜索记录、标签、地点或待办…', filterTag: '按标签筛选', allTags: '全部标签', noMatches: '没有符合筛选条件的记忆。', favorite: '已收藏', emptyTitle: '还没有记忆。', emptyBody: '准备好记录时,打开“今日”。', detail: '记忆详情', untitled: '当天记录', noNote: '尚未写下记录。', editing: '正在以今日记录编辑 — 保存即可更新', preserved: '已收藏', addMuseum: '加入收藏', confirmDelete: '确认删除', remove: '删除这条记忆?', irreversible: '此操作无法撤销。', deleteMemory: '删除记忆', removed: '记忆已删除', added: '记忆已收藏', unpreserved: '已取消收藏' },
+ memory: { tags: '标签', tagsPlaceholder: '多个标签请用逗号分隔', place: '地点 / 场景(选填)', placePlaceholder: '在哪里,或当时是什么场景?', linkTask: '关联待办(选填)', noTask: '不关联待办', relatedTask: '关联待办', favorite: '加入收藏', edit: '编辑记忆详情', note: '记录内容', title: '标题(选填)', titlePlaceholder: '为这条记忆取个标题' },
museum: { eyebrow: '人生博物馆', title: '被珍藏的瞬间', subtitle: '值得留给漫长回望的片段。', empty: '从一份被珍藏的记忆开始。', preserved: '状态:已珍藏' },
insights: { eyebrow: '最近 30 天', title: '安静地看见时间。', weather: '天气', average: '平均能量', active: '最活跃日', current: '当前连续记录', longest: '最长连续记录:{count} 天', noEntries: '暂无记录' },
profile: { eyebrow: '系统档案', online: 'LIFE OS 在线', daysOnline: '系统运行天数', memories: '记忆', museumItems: '博物馆藏品', streak: '当前连续记录', longest: '最长连续记录', commonWeather: '常见天气', personal: '个人系统', name: '显示名称', birthday: '生日(选填)', start: 'Life OS 启动日期', motto: '个人格言', save: '保存资料', saved: '资料已保存', average: '平均能量 {value} / 5' },
@@ -31,7 +31,7 @@
dialog: { backup: 'Life OS 备份', ready: '准备恢复', exported: '导出时间:{date}', memories: '记忆:{count}', museumItems: '博物馆藏品:{count}', goals: '目标:{count}', tasks: '待办:{count}', merge: '合并', replaceAll: '替换全部数据', final: '最终确认', replaceQuestion: '替换全部 Life OS 数据?', replaceWarning: '这将覆盖当前所有 Life OS 数据。', replace: '替换数据', resetQuestion: '重置所有本地数据?', resetWarning: '如需保留数据,请先导出备份。', resetData: '重置数据' },
feedback: { backup: '备份已创建', invalid: '无效备份文件', restored: '系统已恢复', restoring: '正在恢复系统…', minimized: '已最小化 — 点击任务栏图标即可恢复', languageZh: '语言已切换为中文', languageEn: 'Language set to English', followingZh: '已跟随浏览器语言:中文', followingEn: 'Following browser language: English' },
addon: { draftRestored: '草稿已恢复', searchTitle: '搜索记忆档案', searchPlaceholder: '搜索标题、记录或天气…', searchHint: '按 Esc 关闭 · 点击结果即可打开', untitled: '未命名记忆', noResults: '没有找到记忆。', syncPaused: '云端同步暂停——更改仍保留在此设备', syncReady: '云端同步已就绪', migrateQuestion: '将此浏览器的 Life OS 迁移到云端?', migrateBody: '现有本地记录将成为这个 ChatGPT 账号的第一份云端同步数据。', migrate: '迁移并同步', fresh: '从空白开始', migrated: '本地 Life OS 已迁移', cloudRestored: '云端 Life OS 已恢复', cloudUnavailable: '云端存储暂不可用——已使用本地模式' },
- calendar: { eyebrow: '记忆与生活节点', month: '月', day: '日', today: '今天', mon: '一', tue: '二', wed: '三', thu: '四', fri: '五', sat: '六', sun: '日', previous: '上个月', next: '下个月', previousDay: '前一天', nextDay: '后一天', dayAtGlance: '这一天', memory: '记忆', task: '待办', goal: '目标', items: '项', taskDue: '待办截止', taskDone: '完成待办', goalCreated: '创建目标', goalTarget: '目标日期', goalDone: '完成目标', emptyDay: '这一天还没有记忆或目标节点。', openToday: '写下一段记忆', openTasks: '打开待办' },
+ calendar: { eyebrow: '记忆与生活节点', month: '月', day: '日', today: '今天', mon: '一', tue: '二', wed: '三', thu: '四', fri: '五', sat: '六', sun: '日', previous: '上个月', next: '下个月', previousDay: '前一天', nextDay: '后一天', dayAtGlance: '这一天', memory: '记忆', task: '待办', goal: '目标', items: '项', taskDue: '待办截止', taskDone: '完成待办', goalCreated: '创建目标', goalTarget: '目标日期', goalDone: '完成目标', emptyDay: '这一天还没有记忆或目标节点。', openToday: '写下一段记忆', writeMemory: '新增记忆', openTasks: '打开待办' },
tasks: { eyebrow: '目标与待办', title: '把心力留给重要的事。', subtitle: '为今天和接下来的日子,排一个轻量、踏实的计划。', addGoal: '新建目标', addTask: '添加待办', taskPlaceholder: '接下来要做什么?', goalPlaceholder: '你想朝什么方向前进?', goalDescription: '关于这个目标的备注(选填)', finishedGoals: '已完成的目标', goalComplete: '完成目标', goalReopen: '重新开启目标', all: '全部', today: '今天', upcoming: '即将开始', done: '已完成', activeGoals: '进行中的目标', noGoals: '从一个值得继续靠近的目标开始。', noTasks: '这里暂时没有待办。', due: '截止 {date}', noDate: '无截止日期', priority: { low: '低', normal: '普通', high: '高' }, goal: '目标', anyGoal: '不关联目标', save: '保存', cancel: '取消', goalTitle: '新建目标', taskTitle: '新建待办', goalCreated: '目标已创建', taskAdded: '待办已添加', taskRemoved: '待办已删除', goalRemoved: '目标已删除', confirmGoalDelete: '删除这个目标?', confirmGoalDeleteBody: '关联的待办会保留,但不再关联此目标。', target: '目标日期', completed: '已完成', complete: '标记完成', reopen: '重新打开', delete: '删除', progress: '已完成 {done} / {total}', todayHeading: '今天要推进的事', openCount: '还有 {count} 项待办', overdue: '已逾期' }
};
const get = (object, key) => key.split('.').reduce((value, part) => value?.[part], object);
diff --git a/public/index.html b/public/index.html
index 407947f..3aae0d2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -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==='微光'&§ion.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=>({'&':'&','<':'<','>':'>'}[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=>({'&':'&','<':'<','>':'>'}[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]])=>`${g} ${appName(id)} `).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`${APPS[id][0]} `}).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=`LIFE OS ${i18n(`GOOD ${new Date().getHours()<12?'MORNING':new Date().getHours()<18?'AFTERNOON':'EVENING'}`)}
${Object.entries(APPS).map(([id,a])=>`${a[0]} ${appName(id)} `).join('')}
${i18n('SYSTEM ONLINE · LIFE OS v0.1')}
`}function toggleCalendar(){calendar.classList.toggle('hidden');launcher.classList.add('hidden');calendar.innerHTML=`${i18n('SYSTEM DATE')}
${fmtDate(day())} ${lang()==='zh'?`${mems().length} 条记忆 · ${museum().length} 件已珍藏`:`${mems().length} memories · ${museum().length} preserved`}
`}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?`${labels.open} ${windows[id]?`${labels.min} ${labels.close} `:''}`:`${labels.refresh} ${labels.new} ${labels.system} `;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=`${APPS[id][0]} ${appName(id)} — □ ×
`;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.topinnerWidth-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=`${new Date().toLocaleDateString('en-US',{weekday:'long'}).toUpperCase()}
${fmtDate(day()).replace(/^[^,]+, /,'')} How was your day?
${esc(text)} ${m?'UPDATE MEMORY':'ARCHIVE TODAY'}
TODAY'S WEATHER
${['clear','light','cloudy','rain','storm'].map(x=>`${x.toUpperCase()} `).join('')}
ENERGY LEVEL
${[1,2,3,4,5].map(x=>`${x} `).join('')}
TODAY'S FILM
${f.filmTitle}
${f.filmSubtitle}
“${f.filmLine}”
`}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=`${new Date().toLocaleDateString('en-US',{weekday:'long'}).toUpperCase()}
${fmtDate(day()).replace(/^[^,]+, /,'')} How was your day?
${esc(text)} ${m?'UPDATE MEMORY':'ARCHIVE TODAY'}
TODAY'S WEATHER
${['clear','light','cloudy','rain','storm'].map(x=>`${x.toUpperCase()} `).join('')}
ENERGY LEVEL
${[1,2,3,4,5].map(x=>`${x} `).join('')}
TODAY'S FILM
${f.filmTitle}
${f.filmSubtitle}
“${f.filmLine}”
`}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=`MEMORY ARCHIVE
${a.length} ${a.length===1?'MEMORY':'MEMORIES'} ARCHIVED ${a.length?`${a.map(m=>`
${shortDate(m.date)} · ${m.mood.toUpperCase()} · ENERGY ${m.energy}
${esc(m.filmTitle)}
${esc(m.text||'No written note.')}
`).join('')}
`:'No memories yet. Open Today when you are ready to keep one.
'}`}function detail(id){let m=mems().find(x=>x.id===id);if(!m)return;modal.innerHTML=`MEMORY DETAIL
${esc(m.filmTitle)} ${fmtDate(m.date)} · ${m.mood.toUpperCase()} · ENERGY ${m.energy}
${esc(m.text||'No written note.').replace(/\n/g,' ')}
${m.filmSubtitle}
EDIT ${museum().some(x=>x.memoryId===m.id)?'PRESERVED':'ADD TO MUSEUM'} DELETE
`}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=`CONFIRM DELETE
Remove this memory? This cannot be undone.
DELETE MEMORY CANCEL
`}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=`MUSEUM
Preserved moments Worth keeping for the long view.
${a.length?`${a.map(x=>{let m=ms.find(q=>q.id===x.memoryId);return m?`COLLECTION #${x.id.slice(-4).toUpperCase()} ${esc(m.filmTitle)} ${fmtDate(m.date)} STATUS: PRESERVED `:''}).join('')}
`:'A museum begins with one preserved moment.
'}`}
+function museumApp(c){let a=museum(),ms=mems();c.innerHTML=`MUSEUM
Preserved moments Worth keeping for the long view.
${a.length?`${a.map(x=>{let m=ms.find(q=>q.id===x.memoryId);return m?`COLLECTION #${x.id.slice(-4).toUpperCase()} ${esc(m.title||m.text?.trim().split('\n')[0].slice(0,64)||i18n('timeline.untitled'))} ${fmtDate(m.date)} STATUS: PRESERVED `:''}).join('')}
`:'A museum begins with one preserved moment.
'}`}
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=`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 '}
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
Display Name
Birthday (optional)
Life OS Start Date
Personal Motto
SAVE PROFILE 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 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=`${cWeek}
${fmtDate(day()).replace(/^[^,]+, /,'')} ${i18n('How was your day?')}
${esc(text)} ${i18n(m?'UPDATE MEMORY':'ARCHIVE TODAY')}
${i18n("TODAY'S WEATHER")}
${['clear','light','cloudy','rain','storm'].map(x=>`${moodName(x)} `).join('')}
${i18n('ENERGY LEVEL')}
${[1,2,3,4,5].map(x=>`${x} `).join('')}
${i18n("TODAY'S FILM")}
${f.filmTitle}
${f.filmSubtitle}
“${f.filmLine}”
`}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)}
diff --git a/public/service-worker.js b/public/service-worker.js
index a3cbaab..7e252a7 100644
--- a/public/service-worker.js
+++ b/public/service-worker.js
@@ -1,7 +1,7 @@
-const CACHE = 'life-os-shell-v1.4.2';
+const CACHE = 'life-os-shell-v1.4.3';
const SHELL = [
'/', '/index.html', '/manifest.webmanifest', '/life-os-icon.svg',
- '/boot-guard.js', '/i18n.js', '/tasks.js?v=1.4.2', '/calendar.js?v=1.4.2', '/i18n-renderers.js', '/addons.js?v=1.4.2'
+ '/boot-guard.js', '/i18n.js', '/tasks.js?v=1.4.3', '/calendar.js?v=1.4.3', '/i18n-renderers.js', '/addons.js?v=1.4.3'
];
self.addEventListener('install', event => {
diff --git a/scripts/build.mjs b/scripts/build.mjs
index b21572f..c7603c3 100644
--- a/scripts/build.mjs
+++ b/scripts/build.mjs
@@ -8,7 +8,7 @@ const meta = new URL("../dist/.openai/", import.meta.url);
await rm(dist, { recursive: true, force: true });
await Promise.all([mkdir(server, { recursive: true }), mkdir(meta, { recursive: true })]);
const [rawIndexHtml, addonsJs, bootGuardJs, i18nJs, tasksJs, calendarJs, i18nRenderersJs, manifest, serviceWorker, icon] = await Promise.all([text("index.html"), text("addons.js"), text("boot-guard.js"), text("i18n.js"), text("tasks.js"), text("calendar.js"), text("i18n-renderers.js"), text("manifest.webmanifest"), text("service-worker.js"), text("life-os-icon.svg")]);
-const release = "1.4.2";
+const release = "1.4.3";
// The original single-file prototype carried an obsolete English-to-Chinese
// DOM replacement block. Runtime localisation now lives in i18n.js and
// i18n-renderers.js, so exclude that dead block from the deployed document.