Update Site source
This commit is contained in:
1 parent
190fa83ac5
commit
352c3f737c
6 files changed
+62
-6
No files matched your search
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#111317"/><title>Life OS</title><style>
|
<html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#111317"/><meta name="mobile-web-app-capable" content="yes"/><meta name="apple-mobile-web-app-capable" content="yes"/><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/><meta name="apple-mobile-web-app-title" content="Life OS"/><link rel="manifest" href="/manifest.webmanifest"/><link rel="icon" href="/life-os-icon.svg" type="image/svg+xml"/><link rel="apple-touch-icon" href="/life-os-icon.svg"/><title>Life OS</title><style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700&display=swap');
|
||||||
:root{--bg:#101216;--panel:#191c21;--panel2:#20242a;--ink:#f0f2f5;--muted:#939aa5;--line:#30353d;--accent:#c8ff62;--shadow:0 22px 65px #0008} [data-theme="light"]{--bg:#e9ecee;--panel:#f7f8f7;--panel2:#eef0ee;--ink:#14171a;--muted:#66707a;--line:#d5d9da;--accent:#506c00;--shadow:0 22px 65px #7b828733}
|
:root{--bg:#101216;--panel:#191c21;--panel2:#20242a;--ink:#f0f2f5;--muted:#939aa5;--line:#30353d;--accent:#c8ff62;--shadow:0 22px 65px #0008} [data-theme="light"]{--bg:#e9ecee;--panel:#f7f8f7;--panel2:#eef0ee;--ink:#14171a;--muted:#66707a;--line:#d5d9da;--accent:#506c00;--shadow:0 22px 65px #7b828733}
|
||||||
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:Manrope,system-ui;overflow:hidden}button,input,textarea{font:inherit;color:inherit}button{cursor:pointer}.desktop{height:100vh;background:radial-gradient(ellipse at 20% 0%,#3f4a503d,transparent 40%),radial-gradient(ellipse at 93% 65%,#59604224,transparent 42%),var(--bg);position:relative}.brand{position:absolute;top:28px;left:32px;font:500 11px 'DM Mono';letter-spacing:.18em;color:var(--muted)}.desktop-icons{padding:72px 32px;display:grid;grid-template-columns:repeat(3,96px);gap:22px}.app-icon{border:0;background:transparent;color:var(--ink);display:grid;gap:9px;justify-items:center;font:500 10px 'DM Mono';letter-spacing:.12em}.glyph{width:48px;height:48px;background:var(--panel);border:1px solid var(--line);border-radius:14px;display:grid;place-items:center;box-shadow:0 8px 22px #0002;font-size:20px}.app-icon:hover .glyph,.taskapp.active{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 12%,transparent)}.taskbar{position:absolute;bottom:18px;left:50%;transform:translateX(-50%);height:54px;display:flex;align-items:center;gap:5px;padding:6px;background:color-mix(in srgb,var(--panel) 94%,transparent);border:1px solid var(--line);border-radius:17px;box-shadow:var(--shadow);z-index:1000}.life,.taskapp,.clock{height:40px;border:0;border-radius:11px;background:transparent;color:var(--ink);padding:0 12px;font:500 11px 'DM Mono';letter-spacing:.1em}.life{background:var(--accent);color:#151812;font-weight:500}.taskapp{min-width:42px;font-size:17px;padding:0 10px}.clock{border-left:1px solid var(--line);line-height:1.35;text-align:left;font-size:10px}.launcher,.calendar{position:absolute;bottom:82px;background:var(--panel);border:1px solid var(--line);box-shadow:var(--shadow);border-radius:17px;padding:19px;z-index:1200}.launcher{left:calc(50% - 310px);width:300px}.calendar{right:calc(50% - 310px);width:230px}.hidden{display:none!important}.launchhead{display:flex;justify-content:space-between;font:500 11px 'DM Mono';letter-spacing:.12em}.launchgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:18px 0}.launchitem{border:1px solid transparent;background:var(--panel2);border-radius:10px;padding:13px 7px;color:var(--ink);font:500 9px 'DM Mono';letter-spacing:.08em}.launchitem:hover{border-color:var(--accent)}.systemline{border-top:1px solid var(--line);padding-top:12px;color:var(--muted);font:10px 'DM Mono';letter-spacing:.05em}.window{position:absolute;width:min(700px,calc(100vw - 100px));height:min(590px,calc(100vh - 130px));background:var(--panel);border:1px solid var(--line);border-radius:15px;box-shadow:var(--shadow);overflow:hidden;animation:in .18s ease;display:flex;flex-direction:column}.window.max{width:calc(100vw - 36px)!important;height:calc(100vh - 98px)!important;left:18px!important;top:18px!important}.titlebar{height:48px;display:flex;align-items:center;gap:10px;border-bottom:1px solid var(--line);padding:0 13px;cursor:grab;flex:none}.titlebar:active{cursor:grabbing}.titleicon{width:22px;height:22px;border:1px solid var(--line);border-radius:7px;display:grid;place-items:center;font-size:12px}.wtitle{font:500 11px 'DM Mono';letter-spacing:.13em;flex:1}.controls{display:flex;gap:5px}.controls button{width:25px;height:25px;border:0;background:transparent;color:var(--muted);border-radius:6px}.controls button:hover{background:var(--panel2);color:var(--ink)}.content{padding:24px;overflow:auto;height:100%}.eyebrow{font:10px 'DM Mono';letter-spacing:.16em;color:var(--muted);margin-bottom:8px}h1,h2,p{margin-top:0}h1{font-size:28px;letter-spacing:-.05em;margin-bottom:3px}h2{font-size:13px;letter-spacing:.07em}.sub{color:var(--muted);font-size:14px}.today-grid{display:grid;grid-template-columns:1.3fr .7fr;gap:24px}.entry{width:100%;height:190px;resize:none;background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:15px;outline:none;line-height:1.55}.entry:focus{border-color:var(--accent)}.choices{display:flex;gap:7px;flex-wrap:wrap}.chip{border:1px solid var(--line);border-radius:99px;background:transparent;padding:8px 10px;color:var(--muted);font:10px 'DM Mono';letter-spacing:.06em}.chip.selected{background:var(--accent);border-color:var(--accent);color:#171b12}.energy{display:flex;gap:7px}.energy button{width:31px;height:31px;border:1px solid var(--line);background:var(--panel2);color:var(--muted);border-radius:6px}.energy button.selected{background:var(--accent);color:#171b12;border-color:var(--accent)}.film{border-top:1px solid var(--line);margin-top:18px;padding-top:16px}.film-title{font-size:20px;letter-spacing:-.03em}.film-copy{color:var(--muted);font-size:13px;line-height:1.55}.primary,.secondary,.danger{border:0;padding:11px 14px;border-radius:9px;font:500 10px 'DM Mono';letter-spacing:.1em}.primary{background:var(--accent);color:#151812}.secondary{background:var(--panel2);border:1px solid var(--line)}.danger{background:#a94e4e;color:#fff}.actions{dispLine truncated
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:Manrope,system-ui;overflow:hidden}button,input,textarea{font:inherit;color:inherit}button{cursor:pointer}.desktop{height:100vh;background:radial-gradient(ellipse at 20% 0%,#3f4a503d,transparent 40%),radial-gradient(ellipse at 93% 65%,#59604224,transparent 42%),var(--bg);position:relative}.brand{position:absolute;top:28px;left:32px;font:500 11px 'DM Mono';letter-spacing:.18em;color:var(--muted)}.desktop-icons{padding:72px 32px;display:grid;grid-template-columns:repeat(3,96px);gap:22px}.app-icon{border:0;background:transparent;color:var(--ink);display:grid;gap:9px;justify-items:center;font:500 10px 'DM Mono';letter-spacing:.12em}.glyph{width:48px;height:48px;background:var(--panel);border:1px solid var(--line);border-radius:14px;display:grid;place-items:center;box-shadow:0 8px 22px #0002;font-size:20px}.app-icon:hover .glyph,.taskapp.active{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 12%,transparent)}.taskbar{position:absolute;bottom:18px;left:50%;transform:translateX(-50%);height:54px;display:flex;align-items:center;gap:5px;padding:6px;background:color-mix(in srgb,var(--panel) 94%,transparent);border:1px solid var(--line);border-radius:17px;box-shadow:var(--shadow);z-index:1000}.life,.taskapp,.clock{height:40px;border:0;border-radius:11px;background:transparent;color:var(--ink);padding:0 12px;font:500 11px 'DM Mono';letter-spacing:.1em}.life{background:var(--accent);color:#151812;font-weight:500}.taskapp{min-width:42px;font-size:17px;padding:0 10px}.clock{border-left:1px solid var(--line);line-height:1.35;text-align:left;font-size:10px}.launcher,.calendar{position:absolute;bottom:82px;background:var(--panel);border:1px solid var(--line);box-shadow:var(--shadow);border-radius:17px;padding:19px;z-index:1200}.launcher{left:calc(50% - 310px);width:300px}.calendar{right:calc(50% - 310px);width:230px}.hidden{display:none!important}.launchhead{display:flex;justify-content:space-between;font:500 11px 'DM Mono';letter-spacing:.12em}.launchgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:18px 0}.launchitem{border:1px solid transparent;background:var(--panel2);border-radius:10px;padding:13px 7px;color:var(--ink);font:500 9px 'DM Mono';letter-spacing:.08em}.launchitem:hover{border-color:var(--accent)}.systemline{border-top:1px solid var(--line);padding-top:12px;color:var(--muted);font:10px 'DM Mono';letter-spacing:.05em}.window{position:absolute;width:min(700px,calc(100vw - 100px));height:min(590px,calc(100vh - 130px));background:var(--panel);border:1px solid var(--line);border-radius:15px;box-shadow:var(--shadow);overflow:hidden;animation:in .18s ease;display:flex;flex-direction:column}.window.max{width:calc(100vw - 36px)!important;height:calc(100vh - 98px)!important;left:18px!important;top:18px!important}.titlebar{height:48px;display:flex;align-items:center;gap:10px;border-bottom:1px solid var(--line);padding:0 13px;cursor:grab;flex:none}.titlebar:active{cursor:grabbing}.titleicon{width:22px;height:22px;border:1px solid var(--line);border-radius:7px;display:grid;place-items:center;font-size:12px}.wtitle{font:500 11px 'DM Mono';letter-spacing:.13em;flex:1}.controls{display:flex;gap:5px}.controls button{width:25px;height:25px;border:0;background:transparent;color:var(--muted);border-radius:6px}.controls button:hover{background:var(--panel2);color:var(--ink)}.content{padding:24px;overflow:auto;height:100%}.eyebrow{font:10px 'DM Mono';letter-spacing:.16em;color:var(--muted);margin-bottom:8px}h1,h2,p{margin-top:0}h1{font-size:28px;letter-spacing:-.05em;margin-bottom:3px}h2{font-size:13px;letter-spacing:.07em}.sub{color:var(--muted);font-size:14px}.today-grid{display:grid;grid-template-columns:1.3fr .7fr;gap:24px}.entry{width:100%;height:190px;resize:none;background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:15px;outline:none;line-height:1.55}.entry:focus{border-color:var(--accent)}.choices{display:flex;gap:7px;flex-wrap:wrap}.chip{border:1px solid var(--line);border-radius:99px;background:transparent;padding:8px 10px;color:var(--muted);font:10px 'DM Mono';letter-spacing:.06em}.chip.selected{background:var(--accent);border-color:var(--accent);color:#171b12}.energy{display:flex;gap:7px}.energy button{width:31px;height:31px;border:1px solid var(--line);background:var(--panel2);color:var(--muted);border-radius:6px}.energy button.selected{background:var(--accent);color:#171b12;border-color:var(--accent)}.film{border-top:1px solid var(--line);margin-top:18px;padding-top:16px}.film-title{font-size:20px;letter-spacing:-.03em}.film-copy{color:var(--muted);font-size:13px;line-height:1.55}.primary,.secondary,.danger{border:0;padding:11px 14px;border-radius:9px;font:500 10px 'DM Mono';letter-spacing:.1em}.primary{background:var(--accent);color:#151812}.secondary{background:var(--panel2);border:1px solid var(--line)}.danger{background:#a94e4e;color:#fff}.actions{dispLine truncated
|
||||||
@@ -40,4 +40,4 @@ function streak(a){let s=0,d=new Date();while(a.some(m=>m.date===d.toISOString()
|
|||||||
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=`<div class="eyebrow">SYSTEM PROFILE</div><h1>${p.name?esc(p.name)+' · ':''}LIFE OS ONLINE</h1><div class="profile-grid"><div class="stat"><b>${started}</b><span>DAYS ONLINE</span></div><div class="stat"><b>${a.length}</b><span>MEMORIES</span></div><div class="stat"><b>${museum().length}</b><span>MUSEUM ITEMS</span></div><div class="stat"><b>${streak(a)}</b><span>CURRENT STREAK</span></div><div class="stat"><b>${longest(a)}</b><span>LONGEST STREAK</span></div><div class="stat"><b>${cap(common)}</b><span>COMMON WEATHER</span></div></div><div class="eyebrow">PERSONAL SYSTEM</div><div class="formrow"><label>Display Name</label><input id="pname" value="${esc(p.name)}"></div><div class="formrow"><label>Birthday (optional)</label><input id="pbday" type="date" value="${esc(p.birthday)}"></div><div class="formrow"><label>Life OS Start Date</label><input id="pstart" type="date" value="${esc(p.startDate)}"></div><div class="formrow"><label>Personal Motto</label><input id="pmotto" value="${esc(p.motto)}"></div><div class="actions"><button class="primary" onclick="saveProfile()">SAVE PROFILE</button><span class="status">AVERAGE ENERGY ${avg} / 5</span></div>`}function saveProfile(){save('lifeos_profile',{name:pname.value,birthday:pbday.value,startDate:pstart.value||day(),motto:pmotto.value});renderAll();toast('PROFILE SAVED')}
|
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=`<div class="eyebrow">SYSTEM PROFILE</div><h1>${p.name?esc(p.name)+' · ':''}LIFE OS ONLINE</h1><div class="profile-grid"><div class="stat"><b>${started}</b><span>DAYS ONLINE</span></div><div class="stat"><b>${a.length}</b><span>MEMORIES</span></div><div class="stat"><b>${museum().length}</b><span>MUSEUM ITEMS</span></div><div class="stat"><b>${streak(a)}</b><span>CURRENT STREAK</span></div><div class="stat"><b>${longest(a)}</b><span>LONGEST STREAK</span></div><div class="stat"><b>${cap(common)}</b><span>COMMON WEATHER</span></div></div><div class="eyebrow">PERSONAL SYSTEM</div><div class="formrow"><label>Display Name</label><input id="pname" value="${esc(p.name)}"></div><div class="formrow"><label>Birthday (optional)</label><input id="pbday" type="date" value="${esc(p.birthday)}"></div><div class="formrow"><label>Life OS Start Date</label><input id="pstart" type="date" value="${esc(p.startDate)}"></div><div class="formrow"><label>Personal Motto</label><input id="pmotto" value="${esc(p.motto)}"></div><div class="actions"><button class="primary" onclick="saveProfile()">SAVE PROFILE</button><span class="status">AVERAGE ENERGY ${avg} / 5</span></div>`}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=`<div class="eyebrow">${cWeek}</div><h1>${fmtDate(day()).replace(/^[^,]+, /,'')}</h1><p class="sub">${i18n('How was your day?')}</p><div class="today-grid"><div><textarea id="entry" class="entry" placeholder="${i18n('Write something about today...')}" oninput="todayDraft=this.value">${esc(text)}</textarea><div class="actions"><button class="primary" onclick="archiveToday()">${i18n(m?'UPDATE MEMORY':'ARCHIVE TODAY')}</button><span id="saveStatus" class="status"></span></div></div><aside><div class="eyebrow">${i18n("TODAY'S WEATHER")}</div><div class="choices">${['clear','light','cloudy','rain','storm'].map(x=>`<button class="chip ${x===selectedMood?'selected':''}" onclick="setMood('${x}')">${moodName(x)}</button>`).join('')}</div><div class="eyebrow" style="margin-top:22px">${i18n('ENERGY LEVEL')}</div><div class="energy">${[1,2,3,4,5].map(x=>`<button class="${x===selectedEnergy?'selected':''}" onclick="setEnergy(${x})">${x}</button>`).join('')}</div><div class="film"><div class="eyebrow">${i18n("TODAY'S FILM")}</div><div class="film-title">${f.filmTitle}</div><div class="sub">${f.filmSubtitle}</div><p class="film-copy">“${f.filmLine}”</p></div></aside></div>`}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)}
|
function 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=`<div class="eyebrow">${cWeek}</div><h1>${fmtDate(day()).replace(/^[^,]+, /,'')}</h1><p class="sub">${i18n('How was your day?')}</p><div class="today-grid"><div><textarea id="entry" class="entry" placeholder="${i18n('Write something about today...')}" oninput="todayDraft=this.value">${esc(text)}</textarea><div class="actions"><button class="primary" onclick="archiveToday()">${i18n(m?'UPDATE MEMORY':'ARCHIVE TODAY')}</button><span id="saveStatus" class="status"></span></div></div><aside><div class="eyebrow">${i18n("TODAY'S WEATHER")}</div><div class="choices">${['clear','light','cloudy','rain','storm'].map(x=>`<button class="chip ${x===selectedMood?'selected':''}" onclick="setMood('${x}')">${moodName(x)}</button>`).join('')}</div><div class="eyebrow" style="margin-top:22px">${i18n('ENERGY LEVEL')}</div><div class="energy">${[1,2,3,4,5].map(x=>`<button class="${x===selectedEnergy?'selected':''}" onclick="setEnergy(${x})">${x}</button>`).join('')}</div><div class="film"><div class="eyebrow">${i18n("TODAY'S FILM")}</div><div class="film-title">${f.filmTitle}</div><div class="sub">${f.filmSubtitle}</div><p class="film-copy">“${f.filmLine}”</p></div></aside></div>`}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)}
|
||||||
function 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=`<div class="eyebrow">SYSTEM SETTINGS</div><h1>System, local and yours.</h1><div class="settings"><section><div class="eyebrow">APPEARANCE</div><div class="choices">${['dark','light','auto'].map(x=>`<button class="chip ${s.theme===x?'selected':''}" onclick="theme('${x}')">${x.toUpperCase()}</button>`).join('')}</div></section><section><div class="eyebrow">LANGUAGE</div><div class="choices"><button class="chip ${s.language!=='zh'?'selected':''}" onclick="setLanguage('en')">ENGLISH</button><button class="chip ${s.language==='zh'?'selected':''}" onclick="setLanguage('zh')">CHINESE</button></div></section><section><div class="eyebrow">DATA</div><div class="actions"><button class="primary" onclick="exportData()">EXPORT DATA</button><button class="secondary" onclick="document.querySelector('#importer').click()">IMPORT DATA</button><button class="danger" onclick="resetConfirm()">RESET LIFE OS</button><input id="importer" class="hidden" type="file" accept=".json" onchange="readImport(this)"></div></section><section><div class="eyebrow">STORAGE</div><p>Local Storage<br>${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB</p></section><section><div class="eyebrow">DATA STORAGE</div><p>Your data is stored locally in this browser.<br>Create regular backups to prevent data loss.</p><p>${s.lastBackup?'LAST BACKUP · '+fmtDate(s.lastBackup.slice(0,10)):'LAST BACKUP · NEVER'} ${rec?'<br><b style="color:var(--accent)">BACKUP RECOMMENDED</b>':''}</p></section><section><div class="eyebrow">VERSION</div><p>Life OS<br>Version 0.1 · Schema v1</p></section></div>`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:'0.1',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'||d.schemaVersion!==1||!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=`<div class="modal"><div class="modalbox"><div class="eyebrow">LIFE OS BACKUP</div><h2>Ready to restore</h2><p class="sub">Exported: ${fmtDate(d.exportedAt.slice(0,10))}<br>Memories: ${d.memories.length}<br>Museum Items: ${d.museum.length}</p><div class="actions"><button class="primary" onclick="applyImport('merge')">MERGE</button><button class="danger" onclick="replaceConfirm()">REPLACE ALL DATA</button><button class="secondary" onclick="modal.innerHTML=''">CANCEL</button></div></div></div>`}function replaceConfirm(){modal.innerHTML=`<div class="modal"><div class="modalbox"><div class="eyebrow">FINAL CONFIRMATION</div><h2>Replace all Life OS data?</h2><p class="sub">This will replace all current Life OS data.</p><div class="actions"><button class="danger" onclick="applyImport('replace')">REPLACE DATA</button><button class="secondary" onclick="importPreview()">CANCEL</button></div></div></div>`}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=`<div class="modal"><div class="modalbox"><div class="eyebrow">RESET LIFE OS</div><h2>Reset all local data?</h2><p class="sub">Export a backup first if you want to keep it.</p><div class="actions"><button class="danger" onclick="['lifeos_memories','lifeos_museum','lifeos_profile','lifeos_settings','lifeos_system'].forEach(k=>localStorage.removeItem(k));location.reload()">RESET DATA</button><button class="secondary" onclick="modal.innerHTML=''">CANCEL</button></div></div></div>`}function toast(t){let x=document.createElement('div');x.className='toast';x.textContent=t;document.body.append(x);setTimeout(()=>x.remove(),2000)}init();
|
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=`<div class="eyebrow">SYSTEM SETTINGS</div><h1>System, local and yours.</h1><div class="settings"><section><div class="eyebrow">APPEARANCE</div><div class="choices">${['dark','light','auto'].map(x=>`<button class="chip ${s.theme===x?'selected':''}" onclick="theme('${x}')">${x.toUpperCase()}</button>`).join('')}</div></section><section><div class="eyebrow">LANGUAGE</div><div class="choices"><button class="chip ${s.language!=='zh'?'selected':''}" onclick="setLanguage('en')">ENGLISH</button><button class="chip ${s.language==='zh'?'selected':''}" onclick="setLanguage('zh')">CHINESE</button></div></section><section><div class="eyebrow">DATA</div><div class="actions"><button class="primary" onclick="exportData()">EXPORT DATA</button><button class="secondary" onclick="document.querySelector('#importer').click()">IMPORT DATA</button><button class="danger" onclick="resetConfirm()">RESET LIFE OS</button><input id="importer" class="hidden" type="file" accept=".json" onchange="readImport(this)"></div></section><section><div class="eyebrow">STORAGE</div><p>Local Storage<br>${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB</p></section><section><div class="eyebrow">DATA STORAGE</div><p>Your data is stored locally in this browser.<br>Create regular backups to prevent data loss.</p><p>${s.lastBackup?'LAST BACKUP · '+fmtDate(s.lastBackup.slice(0,10)):'LAST BACKUP · NEVER'} ${rec?'<br><b style="color:var(--accent)">BACKUP RECOMMENDED</b>':''}</p></section><section><div class="eyebrow">VERSION</div><p>Life OS<br>Version 0.1 · Schema v1</p></section></div>`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:'0.1',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'||d.schemaVersion!==1||!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=`<div class="modal"><div class="modalbox"><div class="eyebrow">LIFE OS BACKUP</div><h2>Ready to restore</h2><p class="sub">Exported: ${fmtDate(d.exportedAt.slice(0,10))}<br>Memories: ${d.memories.length}<br>Museum Items: ${d.museum.length}</p><div class="actions"><button class="primary" onclick="applyImport('merge')">MERGE</button><button class="danger" onclick="replaceConfirm()">REPLACE ALL DATA</button><button class="secondary" onclick="modal.innerHTML=''">CANCEL</button></div></div></div>`}function replaceConfirm(){modal.innerHTML=`<div class="modal"><div class="modalbox"><div class="eyebrow">FINAL CONFIRMATION</div><h2>Replace all Life OS data?</h2><p class="sub">This will replace all current Life OS data.</p><div class="actions"><button class="danger" onclick="applyImport('replace')">REPLACE DATA</button><button class="secondary" onclick="importPreview()">CANCEL</button></div></div></div>`}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=`<div class="modal"><div class="modalbox"><div class="eyebrow">RESET LIFE OS</div><h2>Reset all local data?</h2><p class="sub">Export a backup first if you want to keep it.</p><div class="actions"><button class="danger" onclick="['lifeos_memories','lifeos_museum','lifeos_profile','lifeos_settings','lifeos_system'].forEach(k=>localStorage.removeItem(k));location.reload()">RESET DATA</button><button class="secondary" onclick="modal.innerHTML=''">CANCEL</button></div></div></div>`}function toast(t){let x=document.createElement('div');x.className='toast';x.textContent=t;document.body.append(x);setTimeout(()=>x.remove(),2000)}init();
|
||||||
</script><script src="boot-guard.js"></script><script src="i18n.js"></script><script src="i18n-renderers.js"></script><script src="addons.js"></script></body></html>
|
</script><script src="boot-guard.js"></script><script src="i18n.js"></script><script src="i18n-renderers.js"></script><script src="addons.js"></script><script>if('serviceWorker' in navigator){window.addEventListener('load',()=>navigator.serviceWorker.register('/service-worker.js').catch(()=>{}))}</script></body></html>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<rect width="512" height="512" rx="112" fill="#101216"/>
|
||||||
|
<rect x="72" y="72" width="368" height="368" rx="86" fill="#191c21" stroke="#c8ff62" stroke-width="14"/>
|
||||||
|
<path d="M174 160v192h164" fill="none" stroke="#c8ff62" stroke-linecap="round" stroke-linejoin="round" stroke-width="38"/>
|
||||||
|
<path d="M250 160v130h88" fill="none" stroke="#f0f2f5" stroke-linecap="round" stroke-linejoin="round" stroke-width="26"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 485 B |
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "Life OS · 人生操作系统",
|
||||||
|
"short_name": "Life OS",
|
||||||
|
"description": "A private personal operating system for memories, reflection, and time.",
|
||||||
|
"id": "/",
|
||||||
|
"start_url": "/",
|
||||||
|
"scope": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#101216",
|
||||||
|
"theme_color": "#111317",
|
||||||
|
"lang": "zh-CN",
|
||||||
|
"icons": [
|
||||||
|
{ "src": "/life-os-icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" }
|
||||||
|
]
|
||||||
|
}
|
||||||
Loaded 3 of 6 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user