116 lines
9.3 KiB
JavaScript
116 lines
9.3 KiB
JavaScript
/* Life OS reliability and productivity add-ons. */
|
||
(() => {
|
||
const DRAFT_KEY = 'lifeos_today_draft_v1';
|
||
const APP_VERSION = '1.0.0';
|
||
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'system'];
|
||
const zh = () => (typeof lang === 'function' ? lang() : 'en') === 'zh';
|
||
const copy = (en, cn) => (zh() ? cn : en);
|
||
|
||
function readDraft() {
|
||
try { return JSON.parse(localStorage.getItem(DRAFT_KEY)); } catch { return null; }
|
||
}
|
||
function saveDraft(text) {
|
||
localStorage.setItem(DRAFT_KEY, JSON.stringify({ date: new Date().toISOString().slice(0, 10), text, updatedAt: new Date().toISOString() }));
|
||
}
|
||
function restoreDraft(entry) {
|
||
const draft = readDraft();
|
||
const today = new Date().toISOString().slice(0, 10);
|
||
if (!draft || draft.date !== today || !draft.text || entry.value) return;
|
||
entry.value = draft.text;
|
||
entry.dispatchEvent(new Event('input', { bubbles: true }));
|
||
if (typeof toast === 'function') toast(copy('DRAFT RESTORED', '草稿已恢复'));
|
||
}
|
||
function wireToday() {
|
||
const entry = document.querySelector('#entry');
|
||
if (!entry || entry.dataset.lifeosDraft === 'ready') return;
|
||
entry.dataset.lifeosDraft = 'ready';
|
||
restoreDraft(entry);
|
||
entry.addEventListener('input', () => saveDraft(entry.value));
|
||
}
|
||
function 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';
|
||
section.innerHTML = `<div class="eyebrow">${copy('BROWSER LANGUAGE', '浏览器语言')}</div><p class="sub">${copy(`Detected: ${actual === 'zh' ? 'Chinese' : 'English'}`, `当前识别:${actual === 'zh' ? '中文' : 'English'}`)}</p><div class="choices"><button class="chip ${state.languageMode !== 'manual' ? 'selected' : ''}" data-language="auto">${copy('AUTO', '跟随浏览器')}</button><button class="chip ${state.languageMode === 'manual' && state.language === 'en' ? 'selected' : ''}" data-language="en">ENGLISH</button><button class="chip ${state.languageMode === 'manual' && state.language === 'zh' ? 'selected' : ''}" data-language="zh">中文</button></div>`;
|
||
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 = `<div class="lifeos-search-backdrop"></div><section class="lifeos-search-panel"><div class="lifeos-search-head"><span>${copy('SEARCH MEMORY ARCHIVE', '搜索记忆档案')}</span><button aria-label="Close">×</button></div><input autofocus placeholder="${copy('Search titles, notes, moods…', '搜索标题、记录或情绪…')}"><div class="lifeos-search-results"></div><p class="lifeos-search-hint">${copy('ESC to close · click a result to open it', 'ESC 关闭 · 点击结果即可打开')}</p></section>`;
|
||
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 => `<button data-id="${m.id}"><small>${m.date} · ${(m.mood || '').toUpperCase()}</small><strong>${m.filmTitle || copy('Untitled memory', '未命名记忆')}</strong><span>${(m.text || copy('No written note.', '未写下记录。')).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c]))}</span></button>`).join('') : `<div class="lifeos-search-empty">${copy('No memories found.', '没有找到记忆。')}</div>`;
|
||
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 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', '备份已创建'));
|
||
}
|
||
new MutationObserver(() => { wireToday(); wireLanguageControl(); polishSystemChrome(); }).observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['title'] });
|
||
wireToday();
|
||
wireLanguageControl();
|
||
polishSystemChrome();
|
||
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(); }
|
||
});
|
||
})();
|