diff --git a/CHANGELOG.md b/CHANGELOG.md index 91770cf..7a302de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ 本项目按 `v主版本.次版本.修订版本` 标记发布。下列内容以对应 Git tag 和已提交功能为准。 +## v1.16.0 — 2026-09-28 + +- 系统设置改为分类导航:首页、个性化、通知、语言、数据与备份、关于,桌面端使用侧栏与独立内容区,移动端使用横向分类栏。 +- 原有主题、壁纸、图标、小部件、通知、语言和备份控制保留在对应分类;切换语言或修改设置时保持当前分类。 + ## v1.15.0 — 2026-09-28 - 系统设置新增浏览器通知:今日未写记忆、到期未完成待办、进行中目标倒计时,可分别开启并选择本地通知时间。 diff --git a/README.md b/README.md index 9ba5ce6..976195e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 以桌面操作系统为交互方式的个人记录网站。用 App 记录日常、管理目标与待办,并沿时间线回看已经留下的内容。 -在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.15.0** · [更新日志](CHANGELOG.md) +在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.16.0** · [更新日志](CHANGELOG.md) ## 功能 @@ -18,7 +18,7 @@ | 档案 | 查看记录概况并维护个人资料。 | | 浏览器 | 在桌面窗口里打开网页或使用 Google 搜索,支持后退、前进、刷新和外部打开。默认打开 Google;第三方网站可能禁止 iframe 嵌入。 | | 终端 | 逐项创建记忆、目标与待办;选择并确认删除记录,管理完成状态,查询数据和打开其他 App。 | -| 系统设置 | 选择语言、主题、壁纸、桌面快捷方式和开机 App,管理备份;分别设置今日记忆、本日待办和目标倒计时的系统通知与时间。 | +| 系统设置 | 按首页、个性化、通知、语言、数据与备份、关于分类浏览;选择主题、壁纸、桌面快捷方式和开机 App,管理备份与通知。 | 通知权限需手动授予,开关和时间仅保存在当前浏览器。网页打开时检查提醒;关闭网站后无法保证定时送达,如需关闭网页后的可靠提醒,需要另行接入推送服务。 diff --git a/package.json b/package.json index 13e266d..39338d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "life-os", - "version": "1.15.0", + "version": "1.16.0", "private": true, "type": "module", "scripts": { diff --git a/public/addons.js b/public/addons.js index e7d9199..52904d6 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.15.0'; + const APP_VERSION = '1.16.0'; window.LifeOSVersion = APP_VERSION; const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system', 'browser', 'terminal', 'database']; const t = (key, values) => LifeI18n.t(key, values); diff --git a/public/desktop-personalization.js b/public/desktop-personalization.js index 4fbcf0b..799a586 100644 --- a/public/desktop-personalization.js +++ b/public/desktop-personalization.js @@ -187,7 +187,7 @@ const state = preference(), mode = state.wallpaperSource || 'bing'; const appChoices = kind => `
${APP_IDS.map(id => ``).join('')}
`; const customUrl = String(state.customWallpaperUrl || '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); - const personalization = `
${desktopText('heading')}

${desktopText('wallpaper')}

${['bing','calm'].map(value => ``).join('')}

${desktopText('urlHint')}

${desktopText('shortcuts')}

${desktopText('shortcutsHint')}

${appChoices('desktopShortcuts')}

${desktopText('windowLayout')}

${desktopText('windowHint')}

${desktopText('startup')}

${desktopText('startupHint')}

${appChoices('startupApps')}
`; + const personalization = `
${desktopText('heading')}

${desktopText('wallpaper')}

${['bing','calm'].map(value => ``).join('')}

${desktopText('urlHint')}

${desktopText('shortcuts')}

${desktopText('shortcutsHint')}

${appChoices('desktopShortcuts')}

${desktopText('windowLayout')}

${desktopText('windowHint')}

${desktopText('startup')}

${desktopText('startupHint')}

${appChoices('startupApps')}
`; const panel = container.querySelector('.settings'); if (panel) panel.insertAdjacentHTML('beforeend', personalization); }; diff --git a/public/desktop-widgets.js b/public/desktop-widgets.js index 1998d3a..52b1bc0 100644 --- a/public/desktop-widgets.js +++ b/public/desktop-widgets.js @@ -137,7 +137,7 @@ originalSystemApp(container); const panel = container.querySelector('.settings'); if (!panel) return; - panel.insertAdjacentHTML('beforeend', `
${label('heading')}

${label('hint')}

${widgetIds.map(id => ``).join('')}
`); + panel.insertAdjacentHTML('beforeend', `
${label('heading')}

${label('hint')}

${widgetIds.map(id => ``).join('')}
`); }; setInterval(renderWidgets, 60000); window.addEventListener('resize', () => wrappers.forEach((_, id) => place(id))); diff --git a/public/i18n-renderers.js b/public/i18n-renderers.js index 2189eb9..b873dfc 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 => ``).join('')}
${t('system.language')}

${detected}

${window.LifeOSNotifications?.markup() || ''}
${t('system.data')}
${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.15.0'})}

`; + container.innerHTML = `
${t('system.eyebrow')}

${t('system.title')}

${t('system.appearance')}
${['dark','light','auto'].map(item => ``).join('')}
${t('system.language')}

${detected}

${window.LifeOSNotifications?.markup() || ''}
${t('system.data')}
${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.16.0'})}

`; }; window.importPreview = () => { const data = importData, board = data.system?.tasksBoard || {}; rootModal().innerHTML = ``; }; window.replaceConfirm = () => { rootModal().innerHTML = ``; }; diff --git a/public/index.html b/public/index.html index d7aea6c..126ddbb 100644 --- a/public/index.html +++ b/public/index.html @@ -14,7 +14,7 @@ function browserLanguage(){let ls=navigator.languages?.length?navigator.language 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=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]])=>``).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','browser','terminal','database'];tasks.innerHTML=ids.map(id=>{let w=windows[id],min=w?.classList.contains('min'),fg=activeWindow===id&&!min,name=window.appName(id);return``}).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])=>``).join('')}
${LifeI18n.t('shell.systemOnline',{version:window.LifeOSVersion||'1.15.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?`${windows[id]?``:''}`:`
`;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 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])=>``).join('')}
${LifeI18n.t('shell.systemOnline',{version:window.LifeOSVersion||'1.16.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?`${windows[id]?``:''}`:`
`;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);if(id==='browser')window.browserApp(c);if(id==='terminal')window.terminalApp(c);if(id==='database')window.databaseApp(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?

`}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'}
MOST ACTIVE DAY
${active}
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
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?')}

`}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=>``).join('')}
LANGUAGE
DATA
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.15.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.15.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=``}function replaceConfirm(){modal.innerHTML=``}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=``}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=>``).join('')}
LANGUAGE
DATA
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.16.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.16.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=``}function replaceConfirm(){modal.innerHTML=``}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=``}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; - + diff --git a/public/locales/en.json b/public/locales/en.json index a3e4d8e..1599e5c 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -215,6 +215,25 @@ "goals": { "label": "Goal countdown", "description": "Show the nearest active goal with a target date.", "title": "Goal countdown", "days": "{name}: {count} days left.", "today": "{name}: target date is today.", "overdue": "{name}: {count} days past the target date." }, "test": { "title": "Life OS notifications are ready", "body": "Your browser can show reminders from Life OS.", "failed": "Could not show the test notification." } }, + "systemNav": { + "settings": "SETTINGS", + "navigation": "Settings categories", + "home": "Home", + "appearance": "Personalization", + "notifications": "Notifications", + "language": "Language", + "data": "Data & backup", + "about": "About", + "homeHint": "Choose a category to manage your Life OS preferences.", + "device": "This browser", + "deviceHint": "Your personal Life OS workspace", + "version": "v{version}", + "quickSettings": "Settings at a glance", + "appearanceHint": "Theme, wallpaper, desktop icons and widgets", + "notificationsHint": "Memory, tasks and goal reminders", + "languageHint": "Browser language and manual selection", + "dataHint": "Import, export and local storage" + }, "system": { "eyebrow": "SYSTEM SETTINGS", "title": "System, local and yours.", diff --git a/public/locales/zh-CN.json b/public/locales/zh-CN.json index 5aae3bb..0440150 100644 --- a/public/locales/zh-CN.json +++ b/public/locales/zh-CN.json @@ -215,6 +215,25 @@ "goals": { "label": "目标倒计时", "description": "显示最近一个有目标日期的进行中目标。", "title": "目标倒计时", "days": "{name}:还有 {count} 天。", "today": "{name}:目标日期就是今天。", "overdue": "{name}:已超过目标日期 {count} 天。" }, "test": { "title": "Life OS 通知已就绪", "body": "浏览器可以显示来自 Life OS 的提醒。", "failed": "测试通知发送失败。" } }, + "systemNav": { + "settings": "设置", + "navigation": "设置分类", + "home": "主页", + "appearance": "个性化", + "notifications": "通知", + "language": "语言", + "data": "数据与备份", + "about": "关于", + "homeHint": "选择分类,管理你的 Life OS 设置。", + "device": "此浏览器", + "deviceHint": "你的 Life OS 个人空间", + "version": "v{version}", + "quickSettings": "常用设置", + "appearanceHint": "主题、壁纸、桌面图标和小部件", + "notificationsHint": "记忆、待办和目标提醒", + "languageHint": "浏览器语言与手动选择", + "dataHint": "导入、导出与本地存储" + }, "system": { "eyebrow": "系统设置", "title": "你的个人系统。", diff --git a/public/service-worker.js b/public/service-worker.js index f8901ba..4d9b2fd 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const CACHE = 'life-os-shell-v1.15.0'; +const CACHE = 'life-os-shell-v1.16.0'; const SHELL = [ '/', '/index.html', '/manifest.webmanifest', '/life-os-icon.svg' ]; diff --git a/public/system-navigation.js b/public/system-navigation.js new file mode 100644 index 0000000..346dc63 --- /dev/null +++ b/public/system-navigation.js @@ -0,0 +1,43 @@ +/* Windows-inspired categories around the existing settings controls. */ +(() => { + const tabs = ['home', 'appearance', 'notifications', 'language', 'data', 'about']; + const symbols = { home: '⌂', appearance: '◈', notifications: '♧', language: '文', data: '▤', about: 'ⓘ' }; + const t = (key, values) => LifeI18n.t(`systemNav.${key}`, values); + let active = 'home'; + + const navigationButton = (id, compact = false) => ``; + window.setSystemCategory = id => { + if (!tabs.includes(id)) return; + active = id; + const container = document.querySelector('#content-system'); + if (!container) return; + window.systemApp(container); + container.querySelector('.system-settings-main h1')?.focus({ preventScroll: true }); + }; + + const previous = window.systemApp; + window.systemApp = container => { + previous(container); + const panel = container.querySelector('.settings'); + if (!panel?.querySelectorAll || !container.replaceChildren) return; + const sections = [...panel.querySelectorAll(':scope > section[data-settings-group]')]; + const shell = document.createElement('div'); + shell.className = 'system-settings-shell'; + shell.innerHTML = `
${t('settings')}

${t(active)}

${active === 'home' ? `

${t('homeHint')}

` : ''}
`; + const view = shell.querySelector('.system-settings-view'); + if (active === 'home') { + view.innerHTML = `
${t('device')}

${t('deviceHint')}

${t('version', { version: window.LifeOSVersion || '1.16.0' })}
${t('quickSettings')}
${['appearance', 'notifications', 'language', 'data'].map(id => `
${navigationButton(id, true)}

${t(`${id}Hint`)}

`).join('')}
`; + } else { + const group = document.createElement('div'); + group.className = 'settings system-settings-group'; + sections.filter(section => section.dataset.settingsGroup === active).forEach(section => group.append(section)); + view.append(group); + } + container.replaceChildren(shell); + shell.querySelectorAll('[data-category]').forEach(button => button.addEventListener('click', () => window.setSystemCategory(button.dataset.category))); + }; + + const style = document.createElement('style'); + style.textContent = `.window[data-id="system"]{width:min(900px,calc(100vw - 100px));height:min(650px,calc(100vh - 120px))}#content-system{padding:0!important;height:100%;min-height:0}.system-settings-shell{display:grid;grid-template-columns:210px minmax(0,1fr);min-height:100%;background:var(--panel)}.system-settings-sidebar{padding:22px 10px;background:color-mix(in srgb,var(--panel2) 78%,var(--panel));border-right:1px solid var(--line)}.system-settings-brand{padding:0 13px 22px;color:var(--ink);font:600 12px 'DM Mono',monospace;letter-spacing:.09em}.system-settings-brand span{color:var(--muted);font-weight:400}.system-settings-nav{display:grid;gap:3px}.system-nav-item{position:relative;display:flex;align-items:center;gap:12px;width:100%;min-height:43px;padding:8px 13px;border:0;border-radius:8px;background:transparent;color:var(--ink);text-align:left;font:500 13px Manrope,system-ui;cursor:pointer}.system-nav-item:hover,.system-nav-item:focus-visible{background:var(--panel2)}.system-nav-item.selected{background:color-mix(in srgb,var(--accent) 12%,var(--panel2))}.system-nav-item.selected:before{content:'';position:absolute;left:0;top:11px;bottom:11px;width:3px;border-radius:3px;background:var(--accent)}.system-nav-symbol{width:22px;flex:none;text-align:center;color:var(--accent);font-size:19px;line-height:1}.system-settings-main{min-width:0;padding:28px clamp(22px,4vw,42px) 45px}.system-settings-header{margin-bottom:22px}.system-settings-header h1{font-size:clamp(25px,3vw,34px);margin:7px 0 0}.system-settings-header h1:focus{outline:none}.system-settings-header .sub{margin:8px 0 0}.system-settings-group{display:block}.system-settings-group>section{padding:18px 20px!important;margin-bottom:12px;border:1px solid var(--line)!important;border-radius:12px;background:var(--panel2)}.system-settings-group>section:first-child{border-top:1px solid var(--line)}.system-settings-group .notification-row{background:var(--panel)}.system-settings-group .notification-list{grid-template-columns:repeat(2,minmax(0,1fr))}.system-home-status{display:flex;align-items:center;gap:16px;padding:18px 20px;margin-bottom:24px;border:1px solid var(--line);border-radius:13px;background:var(--panel2)}.system-device-mark{display:grid;place-items:center;width:58px;height:58px;flex:none;border-radius:10px;background:var(--bg);color:var(--accent);font-size:30px}.system-home-status strong{font-size:15px}.system-home-status p{margin:4px 0 0;color:var(--muted);font-size:12px}.system-home-status>span{margin-left:auto;white-space:nowrap;color:var(--muted);font:11px 'DM Mono',monospace}.system-home-heading{margin-bottom:12px;font-size:17px;font-weight:600}.system-home-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.system-home-card{border:1px solid var(--line);border-radius:12px;background:var(--panel2);padding:9px 10px 12px}.system-home-card:hover{border-color:var(--accent)}.system-home-link{font-size:14px;font-weight:600}.system-home-link:before{display:none}.system-chevron{margin-left:auto;color:var(--muted);font-size:20px}.system-home-card p{padding:0 13px 0 47px;margin:0;color:var(--muted);font-size:12px;line-height:1.5}@media(max-width:760px){.system-settings-shell{grid-template-columns:1fr}.system-settings-sidebar{padding:12px 14px 7px;border-right:0;border-bottom:1px solid var(--line)}.system-settings-brand{padding:3px 4px 10px}.system-settings-nav{display:flex;overflow-x:auto;gap:5px;scrollbar-width:thin}.system-settings-nav .system-nav-item{width:auto;white-space:nowrap;flex:none;padding:9px 12px}.system-settings-nav .system-nav-item.selected:before{left:12px;right:12px;top:auto;bottom:0;width:auto;height:2px}.system-settings-main{padding:23px 18px 90px}.system-home-grid{grid-template-columns:1fr}.system-settings-group .notification-list{grid-template-columns:1fr}.system-home-status{flex-wrap:wrap}.system-home-status>span{margin-left:74px}}@media(max-width:650px){.window[data-id="system"]{width:100%;height:100%}}`; + document.head.append(style); +})(); diff --git a/public/terminal-app.js b/public/terminal-app.js index 5de77bc..7f57043 100644 --- a/public/terminal-app.js +++ b/public/terminal-app.js @@ -150,7 +150,7 @@ return { message: new Intl.DateTimeFormat(lang() === 'zh' ? 'zh-CN' : 'en-US', options).format(new Date()) }; } if (kind === 'uptime') { if (verb) throw new Error('usage'); const seconds = Math.floor((Date.now() - startedAt) / 1000); return { message: t('uptime', { minutes: Math.floor(seconds / 60), seconds: seconds % 60 }) }; } - if (kind === 'version') { if (verb) throw new Error('usage'); return { message: `Life OS v${window.LifeOSVersion || '1.15.0'}` }; } + if (kind === 'version') { if (verb) throw new Error('usage'); return { message: `Life OS v${window.LifeOSVersion || '1.16.0'}` }; } if (kind === 'status') { if (verb) throw new Error('usage'); const data = board(); return { message: t('status', { memories: mems().length, goals: data.goals.length, tasks: data.tasks.length }) }; } if (kind === 'apps') { if (verb) throw new Error('usage'); return { message: Object.keys(APPS).map(id => `${id} — ${LifeI18n.t(`app.${id}`)}`).join('\n') }; } if (kind === 'history') { if (verb) throw new Error('usage'); return { message: history.slice(-20).map((entry,index) => `${index + 1}. ${entry}`).join('\n') || t('empty') }; } @@ -235,7 +235,7 @@ }; window.terminalApp = node => { if (node.querySelector('.terminal-shell') && node.dataset.terminalLocale === lang()) { output(node); return; } - if (!welcomed) { lines.unshift({ value: t('welcome', { version: window.LifeOSVersion || '1.15.0' }), kind: 'system' }); welcomed = true; } + if (!welcomed) { lines.unshift({ value: t('welcome', { version: window.LifeOSVersion || '1.16.0' }), kind: 'system' }); welcomed = true; } node.dataset.terminalLocale = lang(); node.innerHTML = `
Life OS / ${t('title')}${t('hint')}
`; output(node); composer(node, Boolean(compose)); diff --git a/scripts/build.mjs b/scripts/build.mjs index dd27d9a..cde6868 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -7,21 +7,21 @@ 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, notificationsJs, i18nRenderersJs, desktopPersonalizationJs, desktopWidgetsJs, browserAppJs, dataActionsJs, terminalAppJs, dbAdminJs, manifest, serviceWorker, icon, localeEn, localeZh] = await Promise.all([text("index.html"), text("addons.js"), text("boot-guard.js"), text("i18n.js"), text("tasks.js"), text("calendar.js"), text("notifications.js"), text("i18n-renderers.js"), text("desktop-personalization.js"), text("desktop-widgets.js"), text("browser-app.js"), text("data-actions.js"), text("terminal-app.js"), text("db-admin.js"), text("manifest.webmanifest"), text("service-worker.js"), text("life-os-icon.svg"), readFile(new URL("../public/locales/en.json", import.meta.url), "utf8"), readFile(new URL("../public/locales/zh-CN.json", import.meta.url), "utf8")]); -const release = "1.15.0"; +const [rawIndexHtml, addonsJs, bootGuardJs, i18nJs, tasksJs, calendarJs, notificationsJs, i18nRenderersJs, desktopPersonalizationJs, desktopWidgetsJs, systemNavigationJs, browserAppJs, dataActionsJs, terminalAppJs, dbAdminJs, manifest, serviceWorker, icon, localeEn, localeZh] = await Promise.all([text("index.html"), text("addons.js"), text("boot-guard.js"), text("i18n.js"), text("tasks.js"), text("calendar.js"), text("notifications.js"), text("i18n-renderers.js"), text("desktop-personalization.js"), text("desktop-widgets.js"), text("system-navigation.js"), text("browser-app.js"), text("data-actions.js"), text("terminal-app.js"), text("db-admin.js"), text("manifest.webmanifest"), text("service-worker.js"), text("life-os-icon.svg"), readFile(new URL("../public/locales/en.json", import.meta.url), "utf8"), readFile(new URL("../public/locales/zh-CN.json", import.meta.url), "utf8")]); +const release = "1.16.0"; const localeEnJs = `window.LifeOSLocales=window.LifeOSLocales||{};window.LifeOSLocales.en=${localeEn};`; const localeZhJs = `window.LifeOSLocales=window.LifeOSLocales||{};window.LifeOSLocales['zh-CN']=${localeZh};`; let indexHtml = rawIndexHtml.replace('src="addons.js"', `src="addons.js?v=${release}"`).replace('src="tasks.js"', `src="tasks.js?v=${release}"`).replace('src="calendar.js"', `src="calendar.js?v=${release}"`); for (const [src, code] of [ ['boot-guard.js', bootGuardJs], ['locales/en.js', localeEnJs], ['locales/zh-CN.js', localeZhJs], ['i18n.js', i18nJs], ['tasks.js', tasksJs], ['calendar.js', calendarJs], - ['addons.js', addonsJs], ['notifications.js', notificationsJs], ['i18n-renderers.js', i18nRenderersJs], ['desktop-personalization.js', desktopPersonalizationJs], ['desktop-widgets.js', desktopWidgetsJs], ['browser-app.js', browserAppJs], ['data-actions.js', dataActionsJs], ['terminal-app.js', terminalAppJs], ['db-admin.js', dbAdminJs] + ['addons.js', addonsJs], ['notifications.js', notificationsJs], ['i18n-renderers.js', i18nRenderersJs], ['desktop-personalization.js', desktopPersonalizationJs], ['desktop-widgets.js', desktopWidgetsJs], ['system-navigation.js', systemNavigationJs], ['browser-app.js', browserAppJs], ['data-actions.js', dataActionsJs], ['terminal-app.js', terminalAppJs], ['db-admin.js', dbAdminJs] ]) { const tag = ``; if (!indexHtml.includes(tag)) throw new Error(`Missing Life OS script: ${src}`); indexHtml = indexHtml.replace(tag, ``); } -await writeFile(new URL("assets.js", server), `export const indexHtml=${JSON.stringify(indexHtml)};\nexport const addonsJs=${JSON.stringify(addonsJs)};\nexport const bootGuardJs=${JSON.stringify(bootGuardJs)};\nexport const i18nJs=${JSON.stringify(i18nJs)};\nexport const localeEn=JSON.parse(${JSON.stringify(localeEn)});\nexport const localeZh=JSON.parse(${JSON.stringify(localeZh)});\nexport const localeEnJs=${JSON.stringify(localeEnJs)};\nexport const localeZhJs=${JSON.stringify(localeZhJs)};\nexport const tasksJs=${JSON.stringify(tasksJs)};\nexport const calendarJs=${JSON.stringify(calendarJs)};\nexport const notificationsJs=${JSON.stringify(notificationsJs)};\nexport const i18nRenderersJs=${JSON.stringify(i18nRenderersJs)};\nexport const desktopPersonalizationJs=${JSON.stringify(desktopPersonalizationJs)};\nexport const desktopWidgetsJs=${JSON.stringify(desktopWidgetsJs)};\nexport const browserAppJs=${JSON.stringify(browserAppJs)};\nexport const dataActionsJs=${JSON.stringify(dataActionsJs)};\nexport const terminalAppJs=${JSON.stringify(terminalAppJs)};\nexport const dbAdminJs=${JSON.stringify(dbAdminJs)};\nexport const manifest=${JSON.stringify(manifest)};\nexport const serviceWorker=${JSON.stringify(serviceWorker)};\nexport const icon=${JSON.stringify(icon)};\n`); +await writeFile(new URL("assets.js", server), `export const indexHtml=${JSON.stringify(indexHtml)};\nexport const addonsJs=${JSON.stringify(addonsJs)};\nexport const bootGuardJs=${JSON.stringify(bootGuardJs)};\nexport const i18nJs=${JSON.stringify(i18nJs)};\nexport const localeEn=JSON.parse(${JSON.stringify(localeEn)});\nexport const localeZh=JSON.parse(${JSON.stringify(localeZh)});\nexport const localeEnJs=${JSON.stringify(localeEnJs)};\nexport const localeZhJs=${JSON.stringify(localeZhJs)};\nexport const tasksJs=${JSON.stringify(tasksJs)};\nexport const calendarJs=${JSON.stringify(calendarJs)};\nexport const notificationsJs=${JSON.stringify(notificationsJs)};\nexport const i18nRenderersJs=${JSON.stringify(i18nRenderersJs)};\nexport const desktopPersonalizationJs=${JSON.stringify(desktopPersonalizationJs)};\nexport const desktopWidgetsJs=${JSON.stringify(desktopWidgetsJs)};\nexport const systemNavigationJs=${JSON.stringify(systemNavigationJs)};\nexport const browserAppJs=${JSON.stringify(browserAppJs)};\nexport const dataActionsJs=${JSON.stringify(dataActionsJs)};\nexport const terminalAppJs=${JSON.stringify(terminalAppJs)};\nexport const dbAdminJs=${JSON.stringify(dbAdminJs)};\nexport const manifest=${JSON.stringify(manifest)};\nexport const serviceWorker=${JSON.stringify(serviceWorker)};\nexport const icon=${JSON.stringify(icon)};\n`); await copyFile(new URL("../worker/index.js", import.meta.url), new URL("index.js", server)); await copyFile(new URL("../worker/cloud-state.js", import.meta.url), new URL("cloud-state.js", server)); await copyFile(new URL("../worker/db-admin.js", import.meta.url), new URL("db-admin.js", server)); diff --git a/scripts/validate.mjs b/scripts/validate.mjs index 568c11e..1881ca4 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -18,6 +18,8 @@ assert.match(assets.addonsJs, /api\/lifeos-sync/); assert.match(assets.i18nRenderersJs, /dialog\.goals/); assert.match(assets.i18nRenderersJs, /shell\.wallpaperTitle/); assert.match(assets.i18nRenderersJs, /window\.museumApp/); +assert.ok(assets.indexHtml.indexOf('window.systemApp = container =>') < assets.indexHtml.indexOf('system-settings-shell'), 'category layout wraps existing settings renderers'); +for (const group of ['appearance', 'notifications', 'language', 'data', 'about']) assert.match(assets.i18nRenderersJs, new RegExp(`data-settings-group="${group}"`)); assert.equal(assets.manifest.includes('"description"'), false); const flatten = (value, prefix = "") => Object.entries(value).flatMap(([key, child]) => child && typeof child === "object" && !Array.isArray(child) ? flatten(child, prefix ? `${prefix}.${key}` : key) : [prefix ? `${prefix}.${key}` : key]).sort(); const enKeys = flatten(assets.localeEn), zhKeys = flatten(assets.localeZh); @@ -50,11 +52,11 @@ assert.doesNotMatch(assets.indexHtml, /setTimeout\(applyLanguage\s*,/); assert.doesNotMatch(assets.addonsJs, /(?([\s\S]*?)<\/script>/g)].map(match => match[1]); -assert.equal(shippedScripts.length, 17, "the complete desktop script chain is present in the served HTML"); +assert.equal(shippedScripts.length, 18, "the complete desktop script chain is present in the served HTML"); assert.doesNotMatch(assets.indexHtml, / [kind, { enabled: true, time: '12:00' }])))); const reminderContext = vm.createContext({ @@ -203,7 +205,7 @@ assert.ok(command('date').message.length > 8); assert.ok(command('time').message.includes(':')); assert.ok(command('datetime').message.length > 12); assert.equal(command('uptime').message, 'terminal.uptime'); -assert.equal(command('version').message, 'Life OS v1.15.0'); +assert.equal(command('version').message, 'Life OS v1.16.0'); assert.equal(command('status').message, 'terminal.status'); assert.match(command('apps').message, /calendar/); assert.equal(command('echo "hello Life OS"').message, 'hello Life OS'); diff --git a/worker/index.js b/worker/index.js index d83bbc5..c88b230 100644 --- a/worker/index.js +++ b/worker/index.js @@ -1,4 +1,4 @@ -import { addonsJs, bootGuardJs, i18nJs, localeEn, localeZh, localeEnJs, localeZhJs, tasksJs, calendarJs, notificationsJs, i18nRenderersJs, browserAppJs, dataActionsJs, terminalAppJs, dbAdminJs, indexHtml, manifest, serviceWorker, icon } from "./assets.js"; +import { addonsJs, bootGuardJs, i18nJs, localeEn, localeZh, localeEnJs, localeZhJs, tasksJs, calendarJs, notificationsJs, i18nRenderersJs, systemNavigationJs, browserAppJs, dataActionsJs, terminalAppJs, dbAdminJs, indexHtml, manifest, serviceWorker, icon } from "./assets.js"; import { cloudApi } from "./cloud-state.js"; import { dbAdminApi } from "./db-admin.js"; @@ -53,6 +53,7 @@ export default { if (url.pathname === "/terminal-app.js") return new Response(terminalAppJs, { headers: headers("application/javascript; charset=utf-8", "no-cache") }); if (url.pathname === "/db-admin.js") return new Response(dbAdminJs, { headers: headers("application/javascript; charset=utf-8", "no-cache") }); if (url.pathname === "/notifications.js") return new Response(notificationsJs, { headers: headers("application/javascript; charset=utf-8", "no-cache") }); + if (url.pathname === "/system-navigation.js") return new Response(systemNavigationJs, { headers: headers("application/javascript; charset=utf-8", "no-cache") }); if (url.pathname === "/browser-app.js") return new Response(browserAppJs, { headers: headers("application/javascript; charset=utf-8", "no-cache") }); if (url.pathname === "/i18n-renderers.js") return new Response(i18nRenderersJs, { headers: headers("application/javascript; charset=utf-8", "no-cache") }); if (url.pathname === "/" || url.pathname === "/index.html") {