diff --git a/package.json b/package.json
index f28a32d..75d4374 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "life-os",
- "version": "1.6.0",
+ "version": "1.7.0",
"private": true,
"type": "module",
"scripts": {
diff --git a/public/addons.js b/public/addons.js
index 688472b..46eadc6 100644
--- a/public/addons.js
+++ b/public/addons.js
@@ -3,7 +3,7 @@
const DRAFT_KEY = 'lifeos_today_draft_v1';
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.6.0';
+ const APP_VERSION = '1.7.0';
window.LifeOSVersion = APP_VERSION;
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system'];
const t = (key, values) => LifeI18n.t(key, values);
@@ -68,7 +68,9 @@
document.head.append(style);
function exportData() {
- const data = { app: 'Life OS', version: APP_VERSION, schemaVersion: 2, exportedAt: new Date().toISOString(), profile: profile(), memories: mems(), museum: museum(), settings: settings(), system: sys() };
+ let desktopLayout = null;
+ try { desktopLayout = JSON.parse(localStorage.getItem('lifeos_window_layout_v1') || 'null'); } catch {}
+ const data = { app: 'Life OS', version: APP_VERSION, schemaVersion: 2, exportedAt: new Date().toISOString(), profile: profile(), memories: mems(), museum: museum(), settings: settings(), system: sys(), desktopLayout };
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();
@@ -104,7 +106,7 @@
applyingCloud = true;
Object.entries(CLOUD_KEYS).forEach(([field, key]) => localStorage.setItem(key, JSON.stringify(state[field] ?? cloudFallback[field])));
applyingCloud = false;
- todayDraft = null; window.renderIcons?.(); window.renderAll?.(); window.applyLanguage?.();
+ todayDraft = null; window.renderIcons?.(); window.renderAll?.(); window.applyLanguage?.(); window.applyDesktopPreferences?.();
return mergedLocalBoard || systemNeedsMigration;
}
async function uploadCloudState() {
diff --git a/public/desktop-personalization.js b/public/desktop-personalization.js
new file mode 100644
index 0000000..59907e2
--- /dev/null
+++ b/public/desktop-personalization.js
@@ -0,0 +1,179 @@
+/* Device-aware desktop preferences. The app list and actual records stay in the existing stores. */
+(() => {
+ const APP_IDS = Object.keys(APPS);
+ const LAYOUT_KEY = 'lifeos_window_layout_v1';
+ const cell = { width: 110, height: 98, left: 24, top: 72 };
+ const desktopText = (key, values) => LifeI18n.t(`desktop.${key}`, values);
+ const validApps = value => Array.isArray(value) ? [...new Set(value.filter(id => APP_IDS.includes(id)))] : [];
+ const preference = () => settings();
+ const desktopMode = () => innerWidth > 650;
+ const bounds = () => ({ rows: Math.max(1, Math.floor((innerHeight - 175) / cell.height)), columns: Math.max(1, Math.floor((innerWidth - 48) / cell.width)) });
+ const chosenShortcuts = () => Array.isArray(preference().desktopShortcuts) ? validApps(preference().desktopShortcuts) : APP_IDS;
+ const savedPositions = () => preference().iconPositions || {};
+ const updateSettings = patch => { const state = preference(); Object.assign(state, patch); save('lifeos_settings', state); };
+ const isSlot = (value, limit) => Number.isInteger(value) && value >= 0 && value < limit;
+ let placed = {}, restoring = false, suppressClick = false;
+
+ function arrangeIcons() {
+ const visible = chosenShortcuts(), { rows, columns } = bounds(), occupied = new Set(), result = {};
+ document.querySelectorAll('#icons .app-icon').forEach(node => { if (!visible.includes(node.dataset.app)) node.remove(); });
+ const nodes = [...document.querySelectorAll('#icons .app-icon')];
+ if (!desktopMode()) { nodes.forEach(node => { node.style.left = ''; node.style.top = ''; node.style.position = ''; }); placed = {}; return; }
+ nodes.forEach((node, index) => {
+ const id = node.dataset.app, stored = savedPositions()[id];
+ let col = stored?.col, row = stored?.row;
+ if (!isSlot(col, columns) || !isSlot(row, rows) || occupied.has(`${col}:${row}`)) {
+ const next = Array.from({ length: rows * columns }, (_, slot) => ({ col: Math.floor(slot / rows), row: slot % rows })).find(slot => !occupied.has(`${slot.col}:${slot.row}`));
+ col = next?.col ?? Math.floor(index / rows); row = next?.row ?? index % rows;
+ }
+ occupied.add(`${col}:${row}`); result[id] = { col, row };
+ node.style.position = 'absolute'; node.style.left = `${cell.left + col * cell.width}px`; node.style.top = `${cell.top + row * cell.height}px`;
+ });
+ placed = result;
+ }
+
+ const originalRenderIcons = window.renderIcons;
+ window.renderIcons = () => { originalRenderIcons(); arrangeIcons(); };
+ const icons = document.querySelector('#icons');
+ let dragState = null;
+ icons.addEventListener('pointerdown', event => {
+ const icon = event.target.closest('.app-icon');
+ if (!desktopMode() || !icon || event.button !== 0) return;
+ dragState = { icon, id: icon.dataset.app, x: event.clientX, y: event.clientY, moved: false };
+ icon.setPointerCapture?.(event.pointerId);
+ });
+ icons.addEventListener('pointermove', event => {
+ if (!dragState) return;
+ const dx = event.clientX - dragState.x, dy = event.clientY - dragState.y;
+ if (Math.hypot(dx, dy) > 6) dragState.moved = true;
+ if (dragState.moved) { dragState.icon.classList.add('dragging'); dragState.icon.style.transform = `translate(${dx}px,${dy}px)`; }
+ });
+ icons.addEventListener('pointerup', event => {
+ if (!dragState) return;
+ const { icon, id, moved, x, y } = dragState; dragState = null;
+ icon.classList.remove('dragging'); icon.style.transform = '';
+ if (!moved) return;
+ suppressClick = true;
+ setTimeout(() => { suppressClick = false; }, 0);
+ const { rows, columns } = bounds(), origin = placed[id];
+ const col = Math.max(0, Math.min(columns - 1, Math.round((cell.left + origin.col * cell.width + event.clientX - x - cell.left) / cell.width)));
+ const row = Math.max(0, Math.min(rows - 1, Math.round((cell.top + origin.row * cell.height + event.clientY - y - cell.top) / cell.height)));
+ const other = Object.keys(placed).find(key => key !== id && placed[key].col === col && placed[key].row === row);
+ const positions = { ...savedPositions(), [id]: { col, row } };
+ if (other) positions[other] = origin;
+ updateSettings({ iconPositions: positions }); arrangeIcons();
+ });
+ icons.addEventListener('pointercancel', () => { if (dragState) { dragState.icon.style.transform = ''; dragState.icon.classList.remove('dragging'); dragState = null; } });
+ icons.addEventListener('click', event => { if (suppressClick) { event.preventDefault(); event.stopImmediatePropagation(); } }, true);
+ window.addEventListener('resize', () => { arrangeIcons(); if (desktopMode()) Object.values(windows).forEach(clampWindow); });
+
+ const readLayout = () => { try { return JSON.parse(localStorage.getItem(LAYOUT_KEY) || '{}'); } catch { return {}; } };
+ function clampWindow(node) {
+ if (!desktopMode() || node.classList.contains('max')) return;
+ const width = Math.min(node.offsetWidth || 700, Math.max(360, innerWidth - 20));
+ const height = Math.min(node.offsetHeight || 590, Math.max(280, innerHeight - 85));
+ node.style.width = `${width}px`; node.style.height = `${height}px`;
+ node.style.left = `${Math.max(0, Math.min(parseFloat(node.style.left) || 0, innerWidth - 80))}px`;
+ node.style.top = `${Math.max(0, Math.min(parseFloat(node.style.top) || 0, innerHeight - 95))}px`;
+ }
+ function rememberWindows() {
+ if (restoring || preference().rememberWindowLayout === false || !desktopMode()) return;
+ const entries = Object.values(windows).sort((a, b) => Number(a.style.zIndex || 0) - Number(b.style.zIndex || 0)).map(node => ({
+ id: node.dataset.id, left: parseFloat(node.style.left), top: parseFloat(node.style.top),
+ width: parseFloat(node.style.width) || node.offsetWidth, height: parseFloat(node.style.height) || node.offsetHeight,
+ max: node.classList.contains('max'), min: node.classList.contains('min')
+ }));
+ localStorage.setItem(LAYOUT_KEY, JSON.stringify({ entries }));
+ }
+ function observeWindow(node) {
+ if (node.dataset.layoutObserved) return;
+ node.dataset.layoutObserved = 'true';
+ if (typeof ResizeObserver !== 'undefined') new ResizeObserver(() => rememberWindows()).observe(node);
+ }
+ const originalOpenApp = window.openApp;
+ window.openApp = id => { if (!APP_IDS.includes(id)) return; originalOpenApp(id); observeWindow(windows[id]); rememberWindows(); };
+ const originalFocus = window.focus;
+ window.focus = node => { originalFocus(node); rememberWindows(); };
+ for (const method of ['closeWin', 'minWin', 'maxWin']) {
+ const original = window[method];
+ window[method] = (...args) => { original(...args); rememberWindows(); };
+ }
+ document.querySelector('#windows').addEventListener('pointerup', () => setTimeout(rememberWindows, 0));
+ function restoreWindows() {
+ restoring = true;
+ try {
+ const remembered = desktopMode() && preference().rememberWindowLayout !== false ? readLayout().entries : [];
+ const entries = Array.isArray(remembered) ? remembered.filter(item => APP_IDS.includes(item.id)) : [];
+ const startup = validApps(preference().startupApps);
+ for (const id of [...new Set([...entries.map(item => item.id), ...startup])]) {
+ window.openApp(id);
+ const node = windows[id], position = entries.find(item => item.id === id);
+ if (!position || !desktopMode()) continue;
+ for (const field of ['left', 'top', 'width', 'height']) if (Number.isFinite(position[field]) && position[field] >= 0 && position[field] < 10000) node.style[field] = `${position[field]}px`;
+ node.classList.toggle('max', position.max === true);
+ node.classList.toggle('min', position.min === true);
+ clampWindow(node);
+ }
+ const visible = Object.values(windows).filter(node => !node.classList.contains('min'));
+ if (visible.length) focus(visible.at(-1)); else renderTaskbar();
+ } finally { restoring = false; }
+ rememberWindows();
+ }
+ const originalInit = window.init;
+ window.init = () => { originalInit(); restoreWindows(); };
+ window.applyDesktopPreferences = () => {
+ window.renderIcons(); window.loadBingWallpaper(); window.applyWallpaper();
+ for (const id of validApps(preference().startupApps)) if (!windows[id]) window.openApp(id);
+ };
+
+ const originalWallpaper = window.applyWallpaper;
+ window.applyWallpaper = () => {
+ const state = preference(), mode = state.wallpaperSource || 'bing', desktop = document.querySelector('#desktop');
+ if (mode === 'calm') { desktop.style.backgroundImage = 'radial-gradient(ellipse at 20% 0%,#3f4a503d,transparent 40%),radial-gradient(ellipse at 93% 65%,#59604224,transparent 42%)'; desktop.title = desktopText('calm'); return; }
+ if (mode === 'custom') {
+ try {
+ const url = new URL(state.customWallpaperUrl);
+ if (url.protocol !== 'https:') throw new Error('invalid wallpaper URL');
+ desktop.style.backgroundImage = `linear-gradient(120deg,color-mix(in srgb,var(--bg) 68%,transparent),color-mix(in srgb,var(--bg) 36%,transparent)),url("${url.href.replaceAll('"', '%22')}")`;
+ desktop.style.backgroundSize = 'cover'; desktop.style.backgroundPosition = 'center'; desktop.title = desktopText('custom'); return;
+ } catch { /* Use the built-in background until a valid URL is saved. */ }
+ }
+ if (state.wallpaper?.url) originalWallpaper(state.wallpaper);
+ };
+ const originalLoadBing = window.loadBingWallpaper;
+ window.loadBingWallpaper = () => { if ((preference().wallpaperSource || 'bing') === 'bing') return originalLoadBing(); window.applyWallpaper(); };
+ window.setWallpaperSource = mode => {
+ if (!['bing', 'calm', 'custom'].includes(mode)) return;
+ if (mode === 'custom') {
+ const value = document.querySelector('#desktop-wallpaper-url')?.value.trim() || '';
+ try { const url = new URL(value); if (url.protocol !== 'https:' || value.length > 2048) throw new Error('invalid URL'); }
+ catch { toast(desktopText('invalidUrl')); return; }
+ updateSettings({ wallpaperSource: mode, customWallpaperUrl: value });
+ } else updateSettings({ wallpaperSource: mode });
+ window.loadBingWallpaper(); window.applyWallpaper(); window.renderAll();
+ };
+ window.toggleDesktopApp = (kind, id) => {
+ if (!APP_IDS.includes(id) || !['desktopShortcuts', 'startupApps'].includes(kind)) return;
+ const current = kind === 'desktopShortcuts' ? chosenShortcuts() : validApps(preference().startupApps);
+ const next = current.includes(id) ? current.filter(app => app !== id) : APP_IDS.filter(app => current.includes(app) || app === id);
+ updateSettings({ [kind]: next });
+ if (kind === 'desktopShortcuts') window.renderIcons();
+ window.renderAll();
+ };
+ window.toggleWindowMemory = () => { const enabled = preference().rememberWindowLayout === false; updateSettings({ rememberWindowLayout: enabled }); if (enabled) rememberWindows(); window.renderAll(); };
+ window.resetIconPositions = () => { updateSettings({ iconPositions: {} }); window.renderIcons(); window.renderAll(); };
+ const originalSystemApp = window.systemApp;
+ window.systemApp = container => {
+ originalSystemApp(container);
+ const state = preference(), mode = state.wallpaperSource || 'bing';
+ const appChoices = kind => `
${APP_IDS.map(id => `${appName(id)} `).join('')}
`;
+ const customUrl = String(state.customWallpaperUrl || '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
+ const personalization = `${desktopText('heading')}
${desktopText('wallpaper')} ${['bing','calm'].map(value => `${desktopText(value)} `).join('')}
${desktopText('shortcuts')} ${desktopText('shortcutsHint')}
${appChoices('desktopShortcuts')}${desktopText('resetPositions')} ${desktopText('windowLayout')} ${state.rememberWindowLayout !== false ? desktopText('rememberOn') : desktopText('rememberOff')} ${desktopText('windowHint')}
${desktopText('startup')} ${desktopText('startupHint')}
${appChoices('startupApps')} `;
+ const panel = container.querySelector('.settings');
+ if (panel) panel.insertAdjacentHTML('beforeend', personalization);
+ };
+ const style = document.createElement('style');
+ style.textContent = `.desktop-preferences h2{font-size:14px;margin:18px 0 9px}.desktop-preferences .desktop-app-choices{display:flex;flex-wrap:wrap;gap:8px}.desktop-preferences .chip{font-size:12px;min-height:36px}.desktop-wallpaper-custom{margin-top:13px}.desktop-wallpaper-custom label{display:block;font-size:13px;color:var(--muted);margin-bottom:7px}.desktop-wallpaper-custom>div{display:flex;gap:8px}.desktop-wallpaper-custom input{flex:1;min-width:0;background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:10px;color:var(--ink)}.desktop-wallpaper-custom .secondary{font-size:12px;white-space:nowrap}.desktop-preferences p{font-size:13px;line-height:1.5}.desktop-text-button{margin:10px 0 2px;border:0;background:transparent;padding:5px 0;color:var(--accent);font-size:13px;text-decoration:underline}.app-icon.dragging{z-index:9;cursor:grabbing;touch-action:none}.desktop-icons .app-icon{touch-action:none}@media(max-width:650px){.desktop-icons .app-icon{position:static!important;left:auto!important;top:auto!important;touch-action:auto}.desktop-wallpaper-custom>div{flex-wrap:wrap}.desktop-wallpaper-custom input{flex-basis:100%}}`;
+ document.head.append(style);
+ window.renderIcons();
+})();
diff --git a/public/i18n-renderers.js b/public/i18n-renderers.js
index 5e6ed3b..4c86be1 100644
--- a/public/i18n-renderers.js
+++ b/public/i18n-renderers.js
@@ -110,7 +110,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(), preference = LifeI18n.preference(), 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')} ${t('system.languageEnglish')} ${t('system.languageChinese')}
${t('system.storage')}
${t('system.local')} ${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}
${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')}
${t('system.release',{version:window.LifeOSVersion||'1.6.0'})}
`;
+ 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')} ${t('system.languageEnglish')} ${t('system.languageChinese')}
${t('system.storage')}
${t('system.local')} ${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}
${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')}
${t('system.release',{version:window.LifeOSVersion||'1.7.0'})}
`;
};
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')}
`; };
@@ -118,6 +118,7 @@
const data = importData;
if (mode === 'replace') {
save('lifeos_memories', data.memories); save('lifeos_museum', data.museum); save('lifeos_profile', data.profile || {}); save('lifeos_settings', data.settings || {}); save('lifeos_system', data.system || { schemaVersion: 2 });
+ if (Array.isArray(data.desktopLayout?.entries)) localStorage.setItem('lifeos_window_layout_v1', JSON.stringify({ entries: data.desktopLayout.entries }));
} else {
const memories = mems(), ids = new Set(memories.map(item => item.id)), dates = new Set(memories.map(item => item.date));
save('lifeos_memories', [...memories, ...data.memories.filter(item => !ids.has(item.id) && !dates.has(item.date))]);
@@ -132,7 +133,7 @@
}
rootModal().innerHTML = ''; toast('feedback.restoring'); setTimeout(() => { renderAll(); toast('feedback.restored'); }, 500);
};
- window.resetConfirm = () => { rootModal().innerHTML = `${t('system.reset')}
${t('dialog.resetQuestion')} ${t('dialog.resetWarning')}
${t('dialog.resetData')} ${t('common.cancel')}
`; };
+ window.resetConfirm = () => { rootModal().innerHTML = `${t('system.reset')}
${t('dialog.resetQuestion')} ${t('dialog.resetWarning')}
${t('dialog.resetData')} ${t('common.cancel')}
`; };
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');
diff --git a/public/index.html b/public/index.html
index db1fd1c..3462d67 100644
--- a/public/index.html
+++ b/public/index.html
@@ -12,9 +12,9 @@ const lines=['Nothing remarkable happened. Maybe that was the point.','A small d
function load(k,d){try{return JSON.parse(localStorage.getItem(k))||d}catch{return d}}function save(k,v){localStorage.setItem(k,JSON.stringify(v))}function mems(){return load('lifeos_memories',[])}function museum(){return load('lifeos_museum',[])}function profile(){return load('lifeos_profile',{name:'',birthday:'',startDate:new Date().toISOString().slice(0,10),motto:''})}function settings(){return{theme:'dark',lastBackup:null,language:'auto',languageMode:'auto',...load('lifeos_settings',{})}}function sys(){let value=load('lifeos_system',{schemaVersion:2});if((Number(value.schemaVersion)||1)<2)value.schemaVersion=2;return value}
function browserLanguage(){let ls=navigator.languages?.length?navigator.languages:[navigator.language||navigator.userLanguage||'en'];return ls.some(x=>String(x).toLowerCase().startsWith('zh'))?'zh':'en'}function lang(){return window.LifeI18n?.language()||browserLanguage()}function i18n(key,fallback=key){return 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=LifeI18n.t('shell.wallpaperTitle',{title:w.title||LifeI18n.t('wallpaper.dailyImage')})}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(()=>window.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} ${window.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,name=window.appName(id);return`${APPS[id][0]} `}).join('')}
-function clock(){let d=new Date();document.querySelector('#clock').textContent=d.toLocaleTimeString(LifeI18n.locale(), {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 ${LifeI18n.t(`shell.${new Date().getHours()<12?'morning':new Date().getHours()<18?'afternoon':'evening'}`)}
${Object.entries(APPS).map(([id,a])=>`${a[0]} ${appName(id)} `).join('')}
${LifeI18n.t('shell.systemOnline',{version:window.LifeOSVersion||'1.6.0'})}
`}function toggleCalendar(){calendar.classList.toggle('hidden');launcher.classList.add('hidden');calendar.innerHTML=`${LifeI18n.t('shell.systemDate')}
${fmtDate(day())} ${LifeI18n.t('shell.todaySummary',{memories:mems().length,preserved:museum().length})}
`}function hideContext(){document.querySelector('#life-context')?.remove()}function showContext(e,id){e.preventDefault();e.stopPropagation();hideContext();let labels={open:LifeI18n.t('shell.open'),min:LifeI18n.t('shell.minimize'),close:LifeI18n.t('shell.menuCloseWindow'),refresh:LifeI18n.t('shell.refresh'),new:LifeI18n.t('shell.newToday'),system:LifeI18n.t('shell.systemSettings')},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 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=LifeI18n.t('shell.wallpaperTitle',{title:w.title||LifeI18n.t('wallpaper.dailyImage')})}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=settings();if((s.wallpaperSource||'bing')!=='bing')return;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(()=>window.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} ${window.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,name=window.appName(id);return`${APPS[id][0]} `}).join('')}
+function clock(){let d=new Date();document.querySelector('#clock').textContent=d.toLocaleTimeString(LifeI18n.locale(), {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 ${LifeI18n.t(`shell.${new Date().getHours()<12?'morning':new Date().getHours()<18?'afternoon':'evening'}`)}
${Object.entries(APPS).map(([id,a])=>`${a[0]} ${appName(id)} `).join('')}
${LifeI18n.t('shell.systemOnline',{version:window.LifeOSVersion||'1.7.0'})}
`}function toggleCalendar(){calendar.classList.toggle('hidden');launcher.classList.add('hidden');calendar.innerHTML=`${LifeI18n.t('shell.systemDate')}
${fmtDate(day())} ${LifeI18n.t('shell.todaySummary',{memories:mems().length,preserved:museum().length})}
`}function hideContext(){document.querySelector('#life-context')?.remove()}function showContext(e,id){e.preventDefault();e.stopPropagation();hideContext();let labels={open:LifeI18n.t('shell.open'),min:LifeI18n.t('shell.minimize'),close:LifeI18n.t('shell.menuCloseWindow'),refresh:LifeI18n.t('shell.refresh'),new:LifeI18n.t('shell.newToday'),system:LifeI18n.t('shell.systemSettings')},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')window.today(c);if(id==='timeline')window.timeline(c);if(id==='museum')window.museumApp(c);if(id==='insights')window.insights(c);if(id==='profile')window.profileApp(c);if(id==='tasks')window.tasksApp(c);if(id==='calendar')window.calendarApp(c);if(id==='system')window.systemApp(c);setTimeout(()=>window.applyLanguage?.(),0)}function renderAll(){Object.keys(windows).forEach(renderApp);renderTaskbar();window.renderDesktopGoalCountdowns?.();setTimeout(()=>window.applyLanguage?.(),0)}document.addEventListener('click',e=>{if(e.target.closest('#content-today .chip,#content-today .energy button'))setTimeout(()=>window.applyLanguage?.(),0)})
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?
${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){const dict=LifeI18n.dictionaries[lang()==='zh'?'zh':'en'],seed=[...`${day()}|${m}|${e}`].reduce((sum,char,index)=>sum+char.charCodeAt(0)*(index+1),0),n=seed%dict.prompt.films.length;return{filmTitle:dict.prompt.films[n].title,filmSubtitle:dict.prompt.films[n].subtitle,filmLine:dict.prompt.lines[(n*3+e)%dict.prompt.lines.length]}}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';window.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)}
@@ -23,6 +23,6 @@ function museumApp(c){let a=museum(),ms=mems();c.innerHTML=`m.date===d.toISOString().slice(0,10))){s++;d.setDate(d.getDate()-1)}return s}function longest(a){let ds=a.map(x=>x.date).sort(),best=0,run=0,last='';ds.forEach(d=>{run=last&&((new Date(d)-new Date(last))/864e5===1)?run+1:1;best=Math.max(best,run);last=d});return best}function insights(c){let a=mems(),last=a.filter(m=>(Date.now()-new Date(m.date+'T12:00:00'))<2592e6),cnt={clear:0,light:0,cloudy:0,rain:0,storm:0};last.forEach(x=>cnt[x.mood]++);let avg=a.length?(a.reduce((s,x)=>s+x.energy,0)/a.length).toFixed(1):'—',days={};a.forEach(x=>{let d=new Date(x.date+'T12:00:00').toLocaleDateString('en-US',{weekday:'long'});days[d]=(days[d]||0)+1});let active=Object.entries(days).sort((x,y)=>y[1]-x[1])[0]?.[0]||'—';c.innerHTML=`
LAST 30 DAYS
Insights, quietly. WEATHER
${Object.entries(cnt).map(([k,v])=>`${cap(k)} ${v} DAYS
`).join('')}AVERAGE ENERGY
${avg} / 5
${a.slice(0,10).reverse().map(x=>` `).join('')||'No entries yet '}
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?')}
${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||'';window.today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)}
-function systemApp(c){let s=settings(),bytes=new Blob([JSON.stringify({mems:mems(),museum:museum(),profile:profile()})]).size;let rec=s.lastBackup&&(Date.now()-new Date(s.lastBackup)>12096e5);c.innerHTML=`
SYSTEM SETTINGS
System, local and yours. APPEARANCE
${['dark','light','auto'].map(x=>`${x.toUpperCase()} `).join('')}
STORAGE
Local Storage ${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB
DATA STORAGE
Your data is stored locally in this browser. Create regular backups to prevent data loss.
${s.lastBackup?'LAST BACKUP · '+fmtDate(s.lastBackup.slice(0,10)):'LAST BACKUP · NEVER'} ${rec?'BACKUP RECOMMENDED ':''}
VERSION
Life OS Version ${window.LifeOSVersion||'1.6.0'} · Schema v2
`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:window.LifeOSVersion||'1.6.0',schemaVersion:1,exportedAt:new Date().toISOString(),profile:profile(),memories:mems(),museum:museum(),settings:settings()};let a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));a.download='life-os-backup-'+day()+'.json';a.click();let s=settings();s.lastBackup=new Date().toISOString();save('lifeos_settings',s);renderAll();toast('BACKUP CREATED')}function readImport(el){let f=el.files[0];if(!f)return;let r=new FileReader;r.onload=()=>{try{let d=JSON.parse(r.result);if(d.app!=='Life OS'||![1,2].includes(d.schemaVersion)||!Array.isArray(d.memories)||!Array.isArray(d.museum))throw 0;importData=d;importPreview()}catch{toast('INVALID BACKUP FILE')}};r.readAsText(f)}function importPreview(){let d=importData;modal.innerHTML=`
LIFE OS BACKUP
Ready to restore Exported: ${fmtDate(d.exportedAt.slice(0,10))} Memories: ${d.memories.length} Museum Items: ${d.museum.length}
MERGE REPLACE ALL DATA CANCEL
`}function replaceConfirm(){modal.innerHTML=`
FINAL CONFIRMATION
Replace all Life OS data? This will replace all current Life OS data.
REPLACE DATA CANCEL
`}function applyImport(mode){let d=importData;if(mode==='replace'){save('lifeos_memories',d.memories);save('lifeos_museum',d.museum);save('lifeos_profile',d.profile||{});save('lifeos_settings',d.settings||{})}else{let a=mems(), ids=new Set(a.map(x=>x.id)),dates=new Set(a.map(x=>x.date));save('lifeos_memories',[...a,...d.memories.filter(x=>!ids.has(x.id)&&!dates.has(x.date))]);let mu=museum(),mi=new Set(mu.map(x=>x.id));save('lifeos_museum',[...mu,...d.museum.filter(x=>!mi.has(x.id))])}modal.innerHTML='';toast('RESTORING SYSTEM...');setTimeout(()=>{renderAll();toast('SYSTEM RESTORED')},500)}function resetConfirm(){modal.innerHTML=`
RESET LIFE OS
Reset all local data? Export a backup first if you want to keep it.
RESET DATA CANCEL
`}function toast(t){let x=document.createElement('div');x.className='toast';x.textContent=t;document.body.append(x);setTimeout(()=>x.remove(),2000)}
+function systemApp(c){let s=settings(),bytes=new Blob([JSON.stringify({mems:mems(),museum:museum(),profile:profile()})]).size;let rec=s.lastBackup&&(Date.now()-new Date(s.lastBackup)>12096e5);c.innerHTML=`
SYSTEM SETTINGS
System, local and yours. APPEARANCE
${['dark','light','auto'].map(x=>`${x.toUpperCase()} `).join('')}
STORAGE
Local Storage ${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB
DATA STORAGE
Your data is stored locally in this browser. Create regular backups to prevent data loss.
${s.lastBackup?'LAST BACKUP · '+fmtDate(s.lastBackup.slice(0,10)):'LAST BACKUP · NEVER'} ${rec?'BACKUP RECOMMENDED ':''}
VERSION
Life OS Version ${window.LifeOSVersion||'1.7.0'} · Schema v2
`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:window.LifeOSVersion||'1.7.0',schemaVersion:1,exportedAt:new Date().toISOString(),profile:profile(),memories:mems(),museum:museum(),settings:settings()};let a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));a.download='life-os-backup-'+day()+'.json';a.click();let s=settings();s.lastBackup=new Date().toISOString();save('lifeos_settings',s);renderAll();toast('BACKUP CREATED')}function readImport(el){let f=el.files[0];if(!f)return;let r=new FileReader;r.onload=()=>{try{let d=JSON.parse(r.result);if(d.app!=='Life OS'||![1,2].includes(d.schemaVersion)||!Array.isArray(d.memories)||!Array.isArray(d.museum))throw 0;importData=d;importPreview()}catch{toast('INVALID BACKUP FILE')}};r.readAsText(f)}function importPreview(){let d=importData;modal.innerHTML=`
LIFE OS BACKUP
Ready to restore Exported: ${fmtDate(d.exportedAt.slice(0,10))} Memories: ${d.memories.length} Museum Items: ${d.museum.length}
MERGE REPLACE ALL DATA CANCEL
`}function replaceConfirm(){modal.innerHTML=`
FINAL CONFIRMATION
Replace all Life OS data? This will replace all current Life OS data.
REPLACE DATA CANCEL
`}function applyImport(mode){let d=importData;if(mode==='replace'){save('lifeos_memories',d.memories);save('lifeos_museum',d.museum);save('lifeos_profile',d.profile||{});save('lifeos_settings',d.settings||{})}else{let a=mems(), ids=new Set(a.map(x=>x.id)),dates=new Set(a.map(x=>x.date));save('lifeos_memories',[...a,...d.memories.filter(x=>!ids.has(x.id)&&!dates.has(x.date))]);let mu=museum(),mi=new Set(mu.map(x=>x.id));save('lifeos_museum',[...mu,...d.museum.filter(x=>!mi.has(x.id))])}modal.innerHTML='';toast('RESTORING SYSTEM...');setTimeout(()=>{renderAll();toast('SYSTEM RESTORED')},500)}function resetConfirm(){modal.innerHTML=`
RESET LIFE OS
Reset all local data? Export a backup first if you want to keep it.
RESET DATA CANCEL
`}function toast(t){let x=document.createElement('div');x.className='toast';x.textContent=t;document.body.append(x);setTimeout(()=>x.remove(),2000)}
window.renderIcons=renderIcons;window.renderAll=renderAll;window.toast=toast;
-