/* Life OS reliability and productivity add-ons. */
(() => {
const DRAFT_KEY = 'lifeos_today_draft_v1';
const APP_VERSION = '1.1.0';
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'system'];
const zh = () => (typeof lang === 'function' ? lang() : 'en') === 'zh';
// Add-on strings share the same catalogue as the desktop shell. Keeping
// this mapping here makes a new feature impossible to ship with a separate
// ad-hoc language mechanism.
const EXTRA_ZH = {
'DRAFT RESTORED':'草稿已恢复','BROWSER LANGUAGE':'浏览器语言',
'Detected: Chinese':'当前识别:中文','Detected: English':'当前识别:英语',
'AUTO':'跟随浏览器','SEARCH MEMORY ARCHIVE':'搜索记忆档案',
'Search titles, notes, moods…':'搜索标题、记录或天气…',
'ESC to close · click a result to open it':'按 Esc 关闭 · 点击结果即可打开',
'Untitled memory':'未命名记忆','No written note.':'尚未写下记录。',
'No memories found.':'没有找到记忆。','CLOUD SYNC PAUSED — CHANGES STAY ON THIS DEVICE':'云端同步暂停——更改仍保留在此设备',
'CLOUD SYNC READY':'云端同步已就绪','Move this browser’s Life OS to the cloud?':'将此浏览器的 Life OS 迁移到云端?',
'Your existing local records will become the first synced copy for this ChatGPT account.':'现有本地记录将成为这个 ChatGPT 账号的第一份云端同步数据。',
'MIGRATE & SYNC':'迁移并同步','START FRESH':'从空白开始','LOCAL LIFE OS MIGRATED':'本地 Life OS 已迁移',
'CLOUD LIFE OS RESTORED':'云端 Life OS 已恢复','CLOUD STORAGE UNAVAILABLE — LOCAL MODE ACTIVE':'云端存储暂不可用——已使用本地模式'
};
if (typeof ZH !== 'undefined') Object.assign(ZH, EXTRA_ZH);
const copy = (en, cn) => (typeof i18n === 'function' ? i18n(en, en) : (zh() ? cn : en));
function readDraft() {
try { return JSON.parse(localStorage.getItem(DRAFT_KEY)); } catch { return null; }
}
function saveDraft(text) {
localStorage.setItem(DRAFT_KEY, JSON.stringify({ date: new Date().toISOString().slice(0, 10), text, updatedAt: new Date().toISOString() }));
}
function restoreDraft(entry) {
const draft = readDraft();
const today = new Date().toISOString().slice(0, 10);
if (!draft || draft.date !== today || !draft.text || entry.value) return;
entry.value = draft.text;
entry.dispatchEvent(new Event('input', { bubbles: true }));
if (typeof toast === 'function') toast(copy('DRAFT RESTORED', '草稿已恢复'));
}
function wireToday() {
const entry = document.querySelector('#entry');
if (!entry || entry.dataset.lifeosDraft === 'ready') return;
entry.dataset.lifeosDraft = 'ready';
restoreDraft(entry);
entry.addEventListener('input', () => saveDraft(entry.value));
}
function wireLanguageControl() {
const settingsPanel = document.querySelector('#content-system .settings');
if (!settingsPanel || settingsPanel.querySelector('#lifeos-browser-language')) return;
const state = settings();
const actual = typeof browserLanguage === 'function' ? browserLanguage() : 'en';
const section = document.createElement('section');
section.id = 'lifeos-browser-language';
const detected = actual === 'zh' ? copy('Detected: Chinese', '当前识别:中文') : copy('Detected: English', '当前识别:英语');
section.innerHTML = `
${copy('BROWSER LANGUAGE', '浏览器语言')}
${detected}
`;
section.querySelectorAll('button').forEach(button => button.addEventListener('click', () => setLanguage(button.dataset.language)));
[...settingsPanel.children].forEach(node => {
const label = node.querySelector?.('.eyebrow')?.textContent.trim();
if (label === 'LANGUAGE' || label === '语言') node.remove();
});
settingsPanel.prepend(section);
}
function clearDraftAfterSave() {
setTimeout(() => {
const today = new Date().toISOString().slice(0, 10);
try {
const saved = JSON.parse(localStorage.getItem('lifeos_memories') || '[]').some(m => m.date === today);
if (saved) localStorage.removeItem(DRAFT_KEY);
} catch {}
}, 700);
}
function closeSearch() { document.querySelector('#lifeos-search')?.remove(); }
function openSearch() {
closeSearch();
const box = document.createElement('div');
box.id = 'lifeos-search';
box.innerHTML = `${copy('SEARCH MEMORY ARCHIVE', '搜索记忆档案')}
${copy('ESC to close · click a result to open it', 'ESC 关闭 · 点击结果即可打开')}
`;
document.body.append(box);
const input = box.querySelector('input');
const results = box.querySelector('.lifeos-search-results');
const render = () => {
let memories = [];
try { memories = JSON.parse(localStorage.getItem('lifeos_memories') || '[]'); } catch {}
const query = input.value.trim().toLowerCase();
const found = memories.filter(m => !query || [m.filmTitle, m.text, m.mood, m.date].join(' ').toLowerCase().includes(query)).slice(0, 8);
results.innerHTML = found.length ? found.map(m => ``).join('') : `${copy('No memories found.', '没有找到记忆。')}
`;
results.querySelectorAll('button').forEach(button => button.addEventListener('click', () => { closeSearch(); openApp('timeline'); setTimeout(() => detail(button.dataset.id), 20); }));
};
input.addEventListener('input', render);
box.querySelector('.lifeos-search-head button').addEventListener('click', closeSearch);
box.querySelector('.lifeos-search-backdrop').addEventListener('click', closeSearch);
render(); input.focus();
}
const style = document.createElement('style');
style.textContent = `.lifeos-search-backdrop{position:fixed;inset:0;background:#0008;z-index:8000}.lifeos-search-panel{position:fixed;z-index:8001;top:18vh;left:50%;transform:translateX(-50%);width:min(560px,calc(100vw - 28px));background:var(--panel);border:1px solid var(--line);border-radius:15px;box-shadow:var(--shadow);padding:15px}.lifeos-search-head{display:flex;justify-content:space-between;align-items:center;font:500 11px 'DM Mono';letter-spacing:.12em;color:var(--muted);margin-bottom:13px}.lifeos-search-head button{border:0;background:transparent;color:var(--ink);font-size:20px}.lifeos-search-panel input{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:12px;outline:none}.lifeos-search-panel input:focus{border-color:var(--accent)}.lifeos-search-results{margin-top:9px;max-height:360px;overflow:auto}.lifeos-search-results button{display:grid;gap:3px;width:100%;text-align:left;border:0;border-radius:9px;padding:11px;background:transparent;color:var(--ink)}.lifeos-search-results button:hover{background:var(--panel2)}.lifeos-search-results small,.lifeos-search-results span,.lifeos-search-hint{color:var(--muted);font-size:12px}.lifeos-search-results strong{font-size:14px}.lifeos-search-results span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.lifeos-search-empty{padding:24px;color:var(--muted);text-align:center;font-size:13px}.lifeos-search-hint{margin:11px 0 0;font:10px 'DM Mono';letter-spacing:.04em}.launchitem svg{width:28px;height:28px;display:block;margin:auto;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.launchitem:hover svg{color:var(--accent)}`;
document.head.append(style);
function polishSystemChrome() {
document.querySelector('#desktop')?.removeAttribute('title');
document.querySelectorAll('.systemline').forEach(node => {
const next = node.textContent.replace(/LIFE OS v0\.1/g, `LIFE OS v${APP_VERSION}`);
if (next !== node.textContent) node.textContent = next;
});
document.querySelectorAll('#content-system .settings section:last-child p').forEach(node => {
const next = node.innerHTML.replace(/Version 0\.1/g, `Version ${APP_VERSION}`).replace(/版本 0\.1/g, `版本 ${APP_VERSION}`);
if (next !== node.innerHTML) node.innerHTML = next;
});
}
function polishLocaleChrome() {
const panel = document.querySelector('#content-system .settings');
if (!panel) return;
const appearance = [...panel.querySelectorAll('section')].find(section => {
const label = section.querySelector('.eyebrow')?.textContent.trim();
return label === 'APPEARANCE' || label === '外观';
});
if (!appearance) return;
const labels = zh() ? ['深色', '浅色', '跟随系统'] : ['DARK', 'LIGHT', 'AUTO'];
appearance.querySelectorAll('.chip').forEach((button, index) => {
if (labels[index] && button.textContent !== labels[index]) button.textContent = labels[index];
});
}
function exportData() {
const data = { app: 'Life OS', version: APP_VERSION, schemaVersion: 1, exportedAt: new Date().toISOString(), profile: profile(), memories: mems(), museum: museum(), settings: settings() };
const link = document.createElement('a');
link.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
link.download = `life-os-backup-${day()}.json`; link.click();
const state = settings(); state.lastBackup = new Date().toISOString(); save('lifeos_settings', state); renderAll(); toast(copy('BACKUP CREATED', '备份已创建'));
}
const CLOUD_KEYS = { profile: 'lifeos_profile', memories: 'lifeos_memories', museum: 'lifeos_museum', settings: 'lifeos_settings', system: 'lifeos_system' };
let cloudReady = false, applyingCloud = false, syncTimer = null, syncErrorShown = false;
const cloudFallback = { profile: {}, memories: [], museum: [], settings: {}, system: {} };
function cloudSnapshot() {
return Object.fromEntries(Object.entries(CLOUD_KEYS).map(([field, key]) => {
try { return [field, JSON.parse(localStorage.getItem(key) || JSON.stringify(cloudFallback[field]))]; } catch { return [field, cloudFallback[field]]; }
}));
}
function hasLocalLife(state) { return state.memories.length || state.museum.length || Object.keys(state.profile).length || Object.keys(state.settings).length > 2; }
function applyCloudState(state) {
applyingCloud = true;
Object.entries(CLOUD_KEYS).forEach(([field, key]) => localStorage.setItem(key, JSON.stringify(state[field] ?? cloudFallback[field])));
applyingCloud = false;
todayDraft = null; renderIcons(); renderAll(); applyLanguage();
}
async function uploadCloudState() {
if (!cloudReady) return;
const response = await fetch('/api/lifeos-state', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ state: cloudSnapshot() }) });
if (!response.ok) throw new Error('sync failed');
}
function scheduleCloudSync() {
if (!cloudReady || applyingCloud) return;
clearTimeout(syncTimer);
syncTimer = setTimeout(() => uploadCloudState().catch(() => {
if (!syncErrorShown) { syncErrorShown = true; toast(copy('CLOUD SYNC PAUSED — CHANGES STAY ON THIS DEVICE', '云端同步暂停——更改仍保存在此设备')); }
}), 450);
}
function migrationPrompt(state) {
const dialog = document.createElement('div');
dialog.id = 'lifeos-cloud-migration';
dialog.innerHTML = `${copy('CLOUD SYNC READY', '云端同步已就绪')}
${copy('Move this browser’s Life OS to the cloud?', '将此浏览器的 Life OS 迁移到云端?')}
${copy('Your existing local records will become the first synced copy for this ChatGPT account.', '现有本地记录将成为这个 ChatGPT 账号的第一份云端同步数据。')}
`;
document.body.append(dialog);
dialog.querySelector('[data-cloud="move"]').addEventListener('click', async () => { dialog.remove(); cloudReady = true; await uploadCloudState(); toast(copy('LOCAL LIFE OS MIGRATED', '本地 Life OS 已迁移')); });
dialog.querySelector('[data-cloud="empty"]').addEventListener('click', async () => { dialog.remove(); applyCloudState(cloudFallback); cloudReady = true; await uploadCloudState(); });
}
async function startCloudSync() {
try {
const response = await fetch('/api/lifeos-state', { cache: 'no-store' });
if (!response.ok) throw new Error('cloud unavailable');
const { state } = await response.json();
const local = cloudSnapshot();
if (state) {
if (hasLocalLife(local)) localStorage.setItem('lifeos_precloud_backup_v1', JSON.stringify({ exportedAt: new Date().toISOString(), state: local }));
applyCloudState(state); cloudReady = true; toast(copy('CLOUD LIFE OS RESTORED', '云端 Life OS 已恢复'));
} else if (hasLocalLife(local)) {
migrationPrompt(local);
} else {
cloudReady = true; await uploadCloudState();
}
} catch {
if (!syncErrorShown) { syncErrorShown = true; toast(copy('CLOUD STORAGE UNAVAILABLE — LOCAL MODE ACTIVE', '云端存储暂不可用——已使用本地模式')); }
}
}
const originalSetItem = Storage.prototype.setItem;
Storage.prototype.setItem = function(key, value) {
originalSetItem.call(this, key, value);
if (this === localStorage && Object.values(CLOUD_KEYS).includes(key)) scheduleCloudSync();
};
const cloudStyle = document.createElement('style');
cloudStyle.textContent = `#lifeos-cloud-migration{position:fixed;inset:0;z-index:9000;background:#071008b8;display:grid;place-items:center;padding:20px}.lifeos-cloud-card{width:min(470px,100%);background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:25px;box-shadow:var(--shadow)}.lifeos-cloud-card h2{margin:10px 0;font-size:25px;letter-spacing:-.04em}.lifeos-cloud-card p{color:var(--muted);line-height:1.65;margin:0 0 20px}`;
document.head.append(cloudStyle);
startCloudSync();
new MutationObserver(() => { wireToday(); wireLanguageControl(); polishSystemChrome(); polishLocaleChrome(); }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['title'] });
wireToday();
wireLanguageControl();
polishSystemChrome();
polishLocaleChrome();
document.addEventListener('click', e => { if (e.target.closest('#content-today .primary')) clearDraftAfterSave(); });
document.addEventListener('click', e => {
if (e.target.closest('.launchitem')) document.querySelector('#launcher')?.classList.add('hidden');
});
document.addEventListener('keydown', e => {
const mod = e.ctrlKey || e.metaKey;
if (mod && e.key.toLowerCase() === 'k') { e.preventDefault(); toggleLauncher(); }
if (mod && e.key.toLowerCase() === 'f') { e.preventDefault(); openSearch(); }
if (mod && e.key.toLowerCase() === 's' && document.querySelector('#entry')) { e.preventDefault(); archiveToday(); }
if (e.altKey && /^[1-6]$/.test(e.key)) { e.preventDefault(); openApp(appIds[Number(e.key) - 1]); }
if (e.key === 'Escape') { closeSearch(); document.querySelector('#launcher')?.classList.add('hidden'); document.querySelector('#calendar')?.classList.add('hidden'); document.querySelector('#life-context')?.remove(); }
});
})();