From 533ca2d3a41eef36c42e8ddefe4ac35992b053ae Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 28 Sep 2026 20:37:24 +0800 Subject: [PATCH] feat(calendar): show regional holidays and observances --- CHANGELOG.md | 6 ++++ README.md | 4 +-- package.json | 2 +- public/addons.js | 2 +- public/calendar.js | 71 ++++++++++++++++++++++++++++++++++--- public/i18n-renderers.js | 2 +- public/index.html | 6 ++-- public/locales/ar.json | 42 +++++++++++++++++++++- public/locales/en.json | 42 +++++++++++++++++++++- public/locales/es.json | 42 +++++++++++++++++++++- public/locales/fr.json | 42 +++++++++++++++++++++- public/locales/ja.json | 42 +++++++++++++++++++++- public/locales/ru.json | 42 +++++++++++++++++++++- public/locales/zh-CN.json | 42 +++++++++++++++++++++- public/locales/zh-HK.json | 42 +++++++++++++++++++++- public/locales/zh-MO.json | 42 +++++++++++++++++++++- public/locales/zh-TW.json | 42 +++++++++++++++++++++- public/service-worker.js | 2 +- public/system-navigation.js | 2 +- public/terminal-app.js | 4 +-- scripts/build.mjs | 2 +- scripts/validate.mjs | 40 ++++++++++++++++++++- worker/index.js | 29 +++++++++++++++ 23 files changed, 563 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c11043f..8d9bec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 本项目按 `v主版本.次版本.修订版本` 标记应用版本;按需创建 Git tag。下列内容以对应提交和已发布功能为准。 +## v1.18.0 — 2026-09-28 + +- 日历按语言地区和浏览器区域推断国家或地区,并提供独立选择;选择仅保存在当前设备,可随时切回自动模式。 +- 在日、月视图展示公共假期、补休、补班与部分常见纪念日,支持 2024–2030 年资料;附来源和官方公告提醒,网络故障时显示重试,不把加载失败当作无假期。 +- 假期数据使用 OpenWorkdays(CC BY 4.0),服务端按国家与年份缓存;此次更新线上站点,不创建 Git tag。 + ## v1.17.0 — 2026-09-28 - 界面扩展至联合国六种工作语文,并加入日语和繁体中文(香港、澳门、台湾),覆盖设置、记忆、目标待办、日历、通知、数据库管理、终端及每日提示。 diff --git a/README.md b/README.md index e4ff8f7..3c31525 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 以桌面操作系统为交互方式的个人记录网站。用 App 记录日常、管理目标与待办,并沿时间线回看已经留下的内容。 -在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.17.0** · [更新日志](CHANGELOG.md) +在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.18.0** · [更新日志](CHANGELOG.md) ## 功能 @@ -12,7 +12,7 @@ | 时间线 | 按日期浏览记忆,使用关键词或标签筛选,编辑、删除和收藏。 | | 博物馆 | 回看收藏的记忆。 | | 目标与待办 | 创建目标和任务,设置截止日期、优先级和完成状态;目标倒计时可选择显示在桌面。 | -| 日历 | 在月视图与日视图中查看记忆、任务和目标节点,并为选定日期新增记忆。 | +| 日历 | 在月视图与日视图中查看记忆、任务、目标、节日和公共假期;按界面语言地区自动选择日历国家,也可手动切换,为选定日期新增记忆。 | | 数据库管理器 | 在再次登录并确认风险后,按表查看、创建、编辑和删除当前账号的云端记录。 | | 洞察 | 周报和月报展示记录频率、完成任务数、常见天气、能量记录及收藏或标记高光的记忆;仅依据已保存的数据统计。 | | 档案 | 查看记录概况并维护个人资料。 | diff --git a/package.json b/package.json index ad90550..5e4e727 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "life-os", - "version": "1.17.0", + "version": "1.18.0", "private": true, "type": "module", "scripts": { diff --git a/public/addons.js b/public/addons.js index 03e0391..f18ac36 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.17.0'; + const APP_VERSION = '1.18.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/calendar.js b/public/calendar.js index cb17219..2f64b68 100644 --- a/public/calendar.js +++ b/public/calendar.js @@ -10,6 +10,58 @@ const formatDate = (key, options = { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }) => new Intl.DateTimeFormat(LifeI18n.locale(), options).format(fromKey(key)); const localDatePart = value => value ? localKey(new Date(value)) : ''; const system = () => { try { return JSON.parse(localStorage.getItem('lifeos_system') || '{}'); } catch { return {}; } }; + const countryKey = 'lifeos_holiday_country_v1'; + const defaults = { 'zh-CN': 'CN', 'zh-HK': 'HK', 'zh-MO': 'MO', 'zh-TW': 'TW', fr: 'FR', ru: 'RU', es: 'ES', ja: 'JP', ar: 'EG' }; + const fallbackCountries = ['CN','HK','MO','TW','US','GB','CA','AU','IN','FR','RU','ES','JP','EG','AE','SA']; + let countries = fallbackCountries, countriesRequested = false; + const feeds = new Map(); + const browserCountry = () => { + const language = LifeI18n.language(); + const match = (navigator.languages || [navigator.language || '']).find(code => code.toLowerCase().startsWith(language.toLowerCase().split('-')[0] + '-')) || navigator.language || ''; + const region = match.match(/-(?:[A-Za-z]{4}-)?([A-Za-z]{2})(?:-|$)/)?.[1]?.toUpperCase(); + return region && /^[A-Z]{2}$/.test(region) ? region : (defaults[language] || 'US'); + }; + const preference = () => { try { return localStorage.getItem(countryKey) || 'auto'; } catch { return 'auto'; } }; + const country = () => preference() === 'auto' ? (defaults[LifeI18n.language()] || browserCountry()) : preference(); + const countryName = code => { try { return new Intl.DisplayNames([LifeI18n.locale()], { type: 'region' }).of(code) || code; } catch { return code; } }; + const feedKey = (code, year) => `${code}/${year}`; + function loadHolidays(code, year, retry = false) { + if (year < 2024 || year > 2030) return; + const key = feedKey(code, year), current = feeds.get(key); + if (current && !retry) return; + feeds.set(key, { status: 'loading', holidays: [], workingDays: [] }); + fetch(`/api/holidays?country=${encodeURIComponent(code)}&year=${year}`) + .then(async response => { if (!response.ok) throw new Error('holiday feed'); return response.json(); }) + .then(data => { feeds.set(key, { status: 'ready', holidays: data.holidays || [], workingDays: data.workingDays || [] }); renderAll(); }) + .catch(() => { feeds.set(key, { status: 'error', holidays: [], workingDays: [] }); renderAll(); }); + } + function loadCountries() { + if (countriesRequested) return; + countriesRequested = true; + fetch('/api/holiday-countries').then(async response => { if (!response.ok) throw new Error('countries'); return response.json(); }) + .then(data => { countries = [...new Set(data.countries.map(item => item.code).concat(fallbackCountries))]; renderAll(); }) + .catch(() => { /* Keep the basic choices if the directory is temporarily unavailable. */ }); + } + const currentFeed = () => feeds.get(feedKey(country(), month.getFullYear())); + const holidayName = name => { + const translations = LifeI18n.dictionaries[LifeI18n.language()]?.calendar?.holidayNames || {}; + return typeof translations === 'object' ? translations[name] || name : name; + }; + const observances = { + '02-14': { key: 'valentine', countries: ['US','GB','CA','AU','FR','ES','JP','CN','HK','MO','TW'] }, + '04-22': { key: 'earth', countries: null }, + '10-31': { key: 'halloween', countries: ['US','GB','CA','AU'] }, + '12-24': { key: 'christmasEve', countries: ['US','GB','CA','AU','FR','ES'] }, + '09-10': { key: 'teachers', countries: ['CN'] } + }; + function holidayEvents(key) { + const feed = currentFeed(); + const publicItems = feed?.status === 'ready' ? feed.holidays.filter(item => item.date === key).map(item => ({ kind: 'holiday', rank: 6, title: holidayName(item.name), detail: item.estimated ? t('estimated') : '', time: item.observed ? t('observed') : t('publicHoliday') })) : []; + const working = feed?.status === 'ready' && feed.workingDays.includes(key) ? [{ kind: 'workday', rank: 7, title: t('workingWeekend'), detail: '', time: t('workingDay') }] : []; + const festival = observances[key.slice(5)]; + const cultural = festival && (!festival.countries || festival.countries.includes(country())) && !publicItems.length ? [{ kind: 'festival', rank: 8, title: t(`festivals.${festival.key}`), detail: '', time: t('festival') }] : []; + return [...publicItems, ...working, ...cultural]; + } function eventsFor(key) { const events = []; @@ -25,7 +77,7 @@ if (goal.targetDate === key) events.push({ kind: 'goal', rank: 4, title: goal.title, detail: goal.description || '', time: t('goalTarget'), id: goal.id }); if (localDatePart(goal.completedAt) === key) events.push({ kind: 'goal', rank: 5, title: goal.title, detail: goal.description || '', time: t('goalDone'), id: goal.id, done: true }); } - return events.sort((a, b) => a.rank - b.rank || a.title.localeCompare(b.title)); + return events.concat(holidayEvents(key)).sort((a, b) => a.rank - b.rank || a.title.localeCompare(b.title)); } function monthCells() { @@ -37,7 +89,7 @@ const number = index - firstOffset + 1, inMonth = number > 0 && number <= days; if (!inMonth) return ''; const key = `${year}-${pad(monthIndex + 1)}-${pad(number)}`, events = eventsFor(key), types = [...new Set(events.map(event => event.kind))]; - return ``; + return ``; }).join(''); } @@ -46,7 +98,8 @@ const action = event.kind === 'memory' ? `openApp('timeline');setTimeout(()=>detail('${esc(event.id)}'),20)` : `openApp('tasks');setTaskFilter('${event.done ? 'done' : 'all'}')`; - return ``; + const informational = ['holiday','festival','workday'].includes(event.kind); + return `<${informational ? 'div' : 'button'} class="cal-event ${event.kind} ${event.done ? 'complete' : ''}" ${informational ? '' : `onclick="${action}"`}>${esc(badge || t('festival'))}${esc(event.title)}${event.detail ? `${esc(event.detail)}` : ''}${informational ? '' : ''}`; } function selectedDayMarkup() { @@ -55,12 +108,20 @@ } function render(container) { + loadCountries(); + const code = country(), year = month.getFullYear(); + loadHolidays(code, year); + const feed = currentFeed(); + const availability = year < 2024 || year > 2030 ? t('outsideRange') : feed?.status === 'loading' ? t('loadingHolidays') : feed?.status === 'error' ? `${t('holidayUnavailable')} ` : t('holidaySource'); + const options = [...new Set([code, ...countries])].sort((a, b) => countryName(a).localeCompare(countryName(b), LifeI18n.locale())); const title = new Intl.DateTimeFormat(LifeI18n.locale(), { month: 'long', year: 'numeric' }).format(month); const heading = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'].map((key, index) => `${t(key)}`).join(''); - container.innerHTML = `
${t('eyebrow')}

${esc(title)}

${view === 'month' ? `
${heading}
${monthCells()}
${t('memory')}${t('task')}${t('goal')}
${selectedDayMarkup()}
` : `
${selectedDayMarkup()}
`}`; + container.innerHTML = `
${t('eyebrow')}

${esc(title)}

${availability}${feed?.status === 'ready' ? ` · ${t('holidayCaveat')} · OpenWorkdays` : ''}
${view === 'month' ? `
${heading}
${monthCells()}
${t('memory')}${t('task')}${t('goal')}${t('publicHoliday')}${t('festival')}${t('workingDay')}
${selectedDayMarkup()}
` : `
${selectedDayMarkup()}
`}`; } window.calendarApp = render; + window.setCalendarHolidayCountry = value => { if (value !== 'auto' && !/^[A-Z]{2}$/.test(value)) return; try { localStorage.setItem(countryKey, value); } catch {} renderAll(); }; + window.retryCalendarHolidays = () => loadHolidays(country(), month.getFullYear(), true); window.selectCalendarDate = key => { selected = key; month = fromKey(key); renderAll(); }; window.setCalendarView = value => { view = value; renderAll(); }; window.moveCalendarMonth = delta => { @@ -71,6 +132,6 @@ window.moveCalendarDay = delta => { const date = fromKey(selected); date.setDate(date.getDate() + delta); selected = localKey(date); month = fromKey(selected); renderAll(); }; const style = document.createElement('style'); - style.textContent = `.cal-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:16px 0}.cal-title,.cal-toolbar-actions,.cal-view-switch{display:flex;align-items:center}.cal-title{gap:8px}.cal-title h1{font-size:22px;min-width:130px;text-align:center;margin:0}.cal-nav{border:1px solid var(--line);background:var(--panel2);color:var(--ink);border-radius:8px;width:32px;height:32px;font-size:21px;line-height:1}.cal-toolbar-actions{gap:8px}.cal-view-switch{padding:3px;background:var(--panel2);border:1px solid var(--line);border-radius:9px}.cal-view-switch button{border:0;background:transparent;color:var(--muted);border-radius:6px;padding:7px 10px;font:10px 'DM Mono'}.cal-view-switch button.active{background:var(--panel);color:var(--ink);box-shadow:0 1px 4px #0002}.cal-today{padding:9px 11px;font-size:9px}.cal-month-layout{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(220px,.85fr);gap:15px;align-items:start}.cal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr))}.cal-weekdays{margin-bottom:5px}.cal-weekday{text-align:center;padding:5px 0;color:var(--muted);font:9px 'DM Mono';letter-spacing:.05em}.cal-weekday.weekend{color:var(--accent)}.cal-days{gap:3px}.cal-cell,.cal-blank{min-width:0;min-height:57px}.cal-cell{position:relative;display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:7px;border:1px solid transparent;border-radius:9px;background:transparent;color:var(--ink);text-align:left}.cal-cell:hover{background:var(--panel2)}.cal-cell.selected{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 9%,var(--panel))}.cal-cell.today .cal-number{color:var(--accent);font-weight:700}.cal-number{font-size:12px}.cal-dots{display:flex;gap:3px}.cal-dots i,.cal-legend i{display:block;width:5px;height:5px;border-radius:50%;background:var(--muted)}.cal-dots i.memory,.cal-legend i.memory{background:var(--accent)}.cal-dots i.task,.cal-legend i.task{background:#78a7d7}.cal-dots i.goal,.cal-legend i.goal{background:#c38dd3}.cal-legend{display:flex;gap:12px;flex-wrap:wrap;padding:10px 2px 0;color:var(--muted);font-size:9px}.cal-legend span{display:flex;align-items:center;gap:5px}.cal-day-card{min-width:0;padding:14px;border:1px solid var(--line);border-radius:12px;background:color-mix(in srgb,var(--panel2) 45%,transparent)}.cal-day-head{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:9px}.cal-day-head .eyebrow{margin-bottom:5px}.cal-day-head h2{font-size:15px;line-height:1.35;margin:0}.cal-day-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap;justify-content:flex-end}.cal-add-memory{font-size:9px;padding:7px 8px}.cal-day-count{display:grid;place-items:center;min-width:25px;height:25px;padding:0 5px;border-radius:99px;background:var(--panel);color:var(--muted);font:10px 'DM Mono'}.cal-events{display:grid;gap:5px}.cal-event{display:flex;align-items:flex-start;gap:8px;width:100%;padding:8px;border:1px solid transparent;border-radius:8px;background:transparent;color:var(--ink);text-align:left}.cal-event:hover{background:var(--panel);border-color:var(--line)}.cal-event-mark{width:18px;flex:none;text-align:center;color:var(--accent);font-size:13px}.cal-event.task .cal-event-mark{color:#78a7d7}.cal-event.goal .cal-event-mark{color:#c38dd3}.cal-event-body{display:grid;gap:3px;min-width:0;flex:1}.cal-event-meta{color:var(--muted);font:8px 'DM Mono';letter-spacing:.06em;text-transform:uppercase}.cal-event-body strong{font-size:11px;line-height:1.4;overflow-wrap:anywhere}.cal-event-detail{color:var(--muted);font-size:10px;line-height:1.4;overflow-wrap:anywhere}.cal-event.complete .cal-event-body strong{text-decoration:line-through;color:var(--muted)}.cal-event-arrow{color:var(--muted);font-size:11px}.cal-empty{padding:20px 4px 7px;text-align:center;color:var(--muted);font-size:11px;line-height:1.6}.cal-empty-actions{display:flex;justify-content:center;gap:6px;margin-top:10px}.cal-empty-actions .secondary,.cal-day-navigate .secondary{font-size:9px;padding:8px}.cal-single-day{max-width:580px;margin:10px auto}.cal-single-day .cal-day-card{padding:19px}.cal-single-day .cal-day-head h2{font-size:19px}.cal-single-day .cal-event{padding:11px}.cal-single-day .cal-event-body strong{font-size:13px}.cal-day-navigate{display:flex;justify-content:space-between;margin-top:10px}@media(max-width:700px){.cal-month-layout{grid-template-columns:1fr}.cal-toolbar{align-items:flex-start;flex-wrap:wrap}.cal-title{gap:5px}.cal-title h1{font-size:18px;min-width:112px}.cal-toolbar-actions{gap:6px}.cal-cell,.cal-blank{min-height:46px}.cal-cell{padding:5px 4px;gap:6px}.cal-number{font-size:11px}.cal-legend{gap:9px}.cal-day-card{padding:12px}.cal-event{padding:8px 5px}}`; + style.textContent = `.cal-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:16px 0}.cal-title,.cal-toolbar-actions,.cal-view-switch{display:flex;align-items:center}.cal-title{gap:8px}.cal-title h1{font-size:22px;min-width:130px;text-align:center;margin:0}.cal-nav{border:1px solid var(--line);background:var(--panel2);color:var(--ink);border-radius:8px;width:32px;height:32px;font-size:21px;line-height:1}.cal-toolbar-actions{gap:8px}.cal-view-switch{padding:3px;background:var(--panel2);border:1px solid var(--line);border-radius:9px}.cal-view-switch button{border:0;background:transparent;color:var(--muted);border-radius:6px;padding:7px 10px;font:10px 'DM Mono'}.cal-view-switch button.active{background:var(--panel);color:var(--ink);box-shadow:0 1px 4px #0002}.cal-today{padding:9px 11px;font-size:9px}.cal-month-layout{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(220px,.85fr);gap:15px;align-items:start}.cal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr))}.cal-weekdays{margin-bottom:5px}.cal-weekday{text-align:center;padding:5px 0;color:var(--muted);font:9px 'DM Mono';letter-spacing:.05em}.cal-weekday.weekend{color:var(--accent)}.cal-days{gap:3px}.cal-cell,.cal-blank{min-width:0;min-height:57px}.cal-cell{position:relative;display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:7px;border:1px solid transparent;border-radius:9px;background:transparent;color:var(--ink);text-align:left}.cal-cell:hover{background:var(--panel2)}.cal-cell.selected{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 9%,var(--panel))}.cal-cell.today .cal-number{color:var(--accent);font-weight:700}.cal-number{font-size:12px}.cal-dots{display:flex;gap:3px}.cal-dots i,.cal-legend i{display:block;width:5px;height:5px;border-radius:50%;background:var(--muted)}.cal-dots i.memory,.cal-legend i.memory{background:var(--accent)}.cal-dots i.task,.cal-legend i.task{background:#78a7d7}.cal-dots i.goal,.cal-legend i.goal{background:#c38dd3}.cal-dots i.holiday,.cal-legend i.holiday{background:#ffb55b}.cal-dots i.festival,.cal-legend i.festival{background:#f28a9d}.cal-dots i.workday,.cal-legend i.workday{background:#879398}.cal-cell.has-holiday .cal-number{color:#ffbf75}.cal-event.holiday,.cal-event.festival,.cal-event.workday{cursor:default}.cal-event.holiday .cal-event-mark{color:#ffb55b}.cal-event.festival .cal-event-mark{color:#f28a9d}.cal-event.workday .cal-event-mark{color:#879398}.cal-holiday-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:-4px 0 15px;color:var(--muted);font-size:10px}.cal-holiday-controls label{display:flex;align-items:center;gap:7px}.cal-holiday-controls select{max-width:200px;padding:6px 8px;border:1px solid var(--line);border-radius:7px;background:var(--panel2);color:var(--ink)}.cal-holiday-source a{color:var(--accent)}.cal-legend{display:flex;gap:12px;flex-wrap:wrap;padding:10px 2px 0;color:var(--muted);font-size:9px}.cal-legend span{display:flex;align-items:center;gap:5px}.cal-day-card{min-width:0;padding:14px;border:1px solid var(--line);border-radius:12px;background:color-mix(in srgb,var(--panel2) 45%,transparent)}.cal-day-head{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:9px}.cal-day-head .eyebrow{margin-bottom:5px}.cal-day-head h2{font-size:15px;line-height:1.35;margin:0}.cal-day-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap;justify-content:flex-end}.cal-add-memory{font-size:9px;padding:7px 8px}.cal-day-count{display:grid;place-items:center;min-width:25px;height:25px;padding:0 5px;border-radius:99px;background:var(--panel);color:var(--muted);font:10px 'DM Mono'}.cal-events{display:grid;gap:5px}.cal-event{display:flex;align-items:flex-start;gap:8px;width:100%;padding:8px;border:1px solid transparent;border-radius:8px;background:transparent;color:var(--ink);text-align:left}.cal-event:hover{background:var(--panel);border-color:var(--line)}.cal-event-mark{width:18px;flex:none;text-align:center;color:var(--accent);font-size:13px}.cal-event.task .cal-event-mark{color:#78a7d7}.cal-event.goal .cal-event-mark{color:#c38dd3}.cal-event-body{display:grid;gap:3px;min-width:0;flex:1}.cal-event-meta{color:var(--muted);font:8px 'DM Mono';letter-spacing:.06em;text-transform:uppercase}.cal-event-body strong{font-size:11px;line-height:1.4;overflow-wrap:anywhere}.cal-event-detail{color:var(--muted);font-size:10px;line-height:1.4;overflow-wrap:anywhere}.cal-event.complete .cal-event-body strong{text-decoration:line-through;color:var(--muted)}.cal-event-arrow{color:var(--muted);font-size:11px}.cal-empty{padding:20px 4px 7px;text-align:center;color:var(--muted);font-size:11px;line-height:1.6}.cal-empty-actions{display:flex;justify-content:center;gap:6px;margin-top:10px}.cal-empty-actions .secondary,.cal-day-navigate .secondary{font-size:9px;padding:8px}.cal-single-day{max-width:580px;margin:10px auto}.cal-single-day .cal-day-card{padding:19px}.cal-single-day .cal-day-head h2{font-size:19px}.cal-single-day .cal-event{padding:11px}.cal-single-day .cal-event-body strong{font-size:13px}.cal-day-navigate{display:flex;justify-content:space-between;margin-top:10px}@media(max-width:700px){.cal-month-layout{grid-template-columns:1fr}.cal-toolbar{align-items:flex-start;flex-wrap:wrap}.cal-title{gap:5px}.cal-title h1{font-size:18px;min-width:112px}.cal-toolbar-actions{gap:6px}.cal-cell,.cal-blank{min-height:46px}.cal-cell{padding:5px 4px;gap:6px}.cal-number{font-size:11px}.cal-legend{gap:9px}.cal-day-card{padding:12px}.cal-event{padding:8px 5px}}`; document.head.append(style); })(); diff --git a/public/i18n-renderers.js b/public/i18n-renderers.js index 55b6d2b..2819643 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 = t('system.browserDetected', { name: LifeI18n.names[LifeI18n.browserLanguage()] }); - container.innerHTML = `
${t('system.eyebrow')}

${t('system.title')}

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

${detected}

${LifeI18n.supported.map(code => ``).join('')}
${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.17.0'})}

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

${t('system.title')}

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

${detected}

${LifeI18n.supported.map(code => ``).join('')}
${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.18.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 cdacb57..9d6a732 100644 --- a/public/index.html +++ b/public/index.html @@ -14,7 +14,7 @@ function browserLanguage(){return window.LifeI18n?.browserLanguage()||'en'}funct 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(LifeI18n.locale(),{weekday:'long',month:'long',day:'numeric',year:'numeric'})}function shortDate(d){return new Date(d+'T12:00:00').toLocaleDateString(LifeI18n.locale(),lang().startsWith('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(LifeI18n.locale(),lang().startsWith('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.17.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(LifeI18n.locale(),lang().startsWith('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.18.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()],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(LifeI18n.locale(),{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.17.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.17.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.18.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.18.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/ar.json b/public/locales/ar.json index 7b663ca..c3aa078 100644 --- a/public/locales/ar.json +++ b/public/locales/ar.json @@ -367,7 +367,47 @@ "emptyDay": "لا توجد ذكريات أو محطات مسجلة لهذا اليوم.", "openToday": "اكتب ذكرى", "writeMemory": "إضافة ذكرى", - "openTasks": "فتح المهام" + "openTasks": "فتح المهام", + "holidayCountry": "البلد أو المنطقة", + "autoCountry": "تلقائي ({country})", + "publicHoliday": "عطلة رسمية", + "festival": "مناسبة", + "workingDay": "يوم عمل", + "observed": "يوم عطلة بديل", + "estimated": "التاريخ تقديري", + "workingWeekend": "عمل في عطلة نهاية الأسبوع", + "loadingHolidays": "جارٍ تحميل العطلات…", + "holidayUnavailable": "بيانات العطلات غير متاحة.", + "retryHolidays": "إعادة المحاولة", + "holidaySource": "بيانات العطلات الرسمية", + "outsideRange": "تتوفر بيانات العطلات من 2024 إلى 2030.", + "festivals": { + "valentine": "عيد الحب", + "earth": "يوم الأرض", + "halloween": "الهالوين", + "christmasEve": "ليلة عيد الميلاد", + "teachers": "يوم المعلم" + }, + "holidayNames": { + "New Year's Day": "رأس السنة الميلادية", + "Chinese New Year (Spring Festival)": "رأس السنة الصينية", + "Chinese New Year": "رأس السنة الصينية", + "Chinese New Year's Eve": "ليلة رأس السنة الصينية", + "Tomb-Sweeping Day": "عيد تشينغمينغ", + "Labor Day": "عيد العمال", + "Dragon Boat Festival": "مهرجان قوارب التنين", + "Mid-Autumn Festival": "عيد منتصف الخريف", + "National Day": "اليوم الوطني", + "Christmas Day": "عيد الميلاد", + "Christmas Eve": "ليلة عيد الميلاد", + "Good Friday": "الجمعة العظيمة", + "Easter Monday": "اثنين الفصح", + "Peace Memorial Day": "يوم إحياء ذكرى السلام", + "Children's Day": "يوم الطفل", + "Constitution Day": "يوم الدستور", + "Confucius' Birthday": "عيد ميلاد كونفوشيوس" + }, + "holidayCaveat": "قد تتغير المواعيد؛ راجع الإعلانات الرسمية المحلية." }, "tasks": { "eyebrow": "الأهداف والمهام", diff --git a/public/locales/en.json b/public/locales/en.json index 1b7ef33..83c955c 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -367,7 +367,47 @@ "emptyDay": "No memories or milestones recorded for this day.", "openToday": "Write a memory", "writeMemory": "Add memory", - "openTasks": "Open tasks" + "openTasks": "Open tasks", + "holidayCountry": "Country / region", + "autoCountry": "Auto ({country})", + "publicHoliday": "Public holiday", + "festival": "Festival", + "workingDay": "Working day", + "observed": "Substitute day off", + "estimated": "Date is estimated", + "workingWeekend": "Official working weekend", + "loadingHolidays": "Loading holidays…", + "holidayUnavailable": "Holiday data unavailable.", + "retryHolidays": "Retry", + "holidaySource": "Public holiday data", + "outsideRange": "Holiday data covers 2024–2030.", + "festivals": { + "valentine": "Valentine’s Day", + "earth": "Earth Day", + "halloween": "Halloween", + "christmasEve": "Christmas Eve", + "teachers": "Teachers’ Day" + }, + "holidayNames": { + "New Year's Day": "New Year's Day", + "Chinese New Year (Spring Festival)": "Chinese New Year (Spring Festival)", + "Chinese New Year": "Chinese New Year", + "Chinese New Year's Eve": "Chinese New Year's Eve", + "Tomb-Sweeping Day": "Tomb-Sweeping Day", + "Labor Day": "Labor Day", + "Dragon Boat Festival": "Dragon Boat Festival", + "Mid-Autumn Festival": "Mid-Autumn Festival", + "National Day": "National Day", + "Christmas Day": "Christmas Day", + "Christmas Eve": "Christmas Eve", + "Good Friday": "Good Friday", + "Easter Monday": "Easter Monday", + "Peace Memorial Day": "Peace Memorial Day", + "Children's Day": "Children's Day", + "Constitution Day": "Constitution Day", + "Confucius' Birthday": "Confucius' Birthday" + }, + "holidayCaveat": "Dates may change; check local official announcements." }, "tasks": { "eyebrow": "GOALS & TASKS", diff --git a/public/locales/es.json b/public/locales/es.json index 5eb6e98..176c55f 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -367,7 +367,47 @@ "emptyDay": "No hay recuerdos ni hitos ese día.", "openToday": "Escribir un recuerdo", "writeMemory": "Añadir recuerdo", - "openTasks": "Abrir tareas" + "openTasks": "Abrir tareas", + "holidayCountry": "País o región", + "autoCountry": "Auto ({country})", + "publicHoliday": "Día festivo", + "festival": "Celebración", + "workingDay": "Día laborable", + "observed": "Descanso compensatorio", + "estimated": "Fecha estimada", + "workingWeekend": "Fin de semana laborable", + "loadingHolidays": "Cargando festivos…", + "holidayUnavailable": "Datos de festivos no disponibles.", + "retryHolidays": "Reintentar", + "holidaySource": "Datos de festivos", + "outsideRange": "Datos disponibles de 2024 a 2030.", + "festivals": { + "valentine": "San Valentín", + "earth": "Día de la Tierra", + "halloween": "Halloween", + "christmasEve": "Nochebuena", + "teachers": "Día del Maestro" + }, + "holidayNames": { + "New Year's Day": "Año Nuevo", + "Chinese New Year (Spring Festival)": "Año Nuevo chino", + "Chinese New Year": "Año Nuevo chino", + "Chinese New Year's Eve": "Víspera del Año Nuevo chino", + "Tomb-Sweeping Day": "Festival Qingming", + "Labor Day": "Día del Trabajo", + "Dragon Boat Festival": "Festival del Bote del Dragón", + "Mid-Autumn Festival": "Festival de Medio Otoño", + "National Day": "Fiesta Nacional", + "Christmas Day": "Navidad", + "Christmas Eve": "Nochebuena", + "Good Friday": "Viernes Santo", + "Easter Monday": "Lunes de Pascua", + "Peace Memorial Day": "Día de Conmemoración de la Paz", + "Children's Day": "Día del Niño", + "Constitution Day": "Día de la Constitución", + "Confucius' Birthday": "Natalicio de Confucio" + }, + "holidayCaveat": "Las fechas pueden cambiar; consulte los anuncios oficiales locales." }, "tasks": { "eyebrow": "OBJETIVOS Y TAREAS", diff --git a/public/locales/fr.json b/public/locales/fr.json index 3b1c2dd..44ffdf7 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -367,7 +367,47 @@ "emptyDay": "Aucun souvenir ni étape pour ce jour.", "openToday": "Écrire un souvenir", "writeMemory": "Ajouter un souvenir", - "openTasks": "Ouvrir les tâches" + "openTasks": "Ouvrir les tâches", + "holidayCountry": "Pays ou région", + "autoCountry": "Auto ({country})", + "publicHoliday": "Jour férié", + "festival": "Fête", + "workingDay": "Jour ouvré", + "observed": "Jour de remplacement", + "estimated": "Date estimée", + "workingWeekend": "Week-end travaillé", + "loadingHolidays": "Chargement des jours fériés…", + "holidayUnavailable": "Données indisponibles.", + "retryHolidays": "Réessayer", + "holidaySource": "Données des jours fériés", + "outsideRange": "Données disponibles de 2024 à 2030.", + "festivals": { + "valentine": "Saint-Valentin", + "earth": "Jour de la Terre", + "halloween": "Halloween", + "christmasEve": "Réveillon de Noël", + "teachers": "Journée des enseignants" + }, + "holidayNames": { + "New Year's Day": "Jour de l’An", + "Chinese New Year (Spring Festival)": "Nouvel An chinois", + "Chinese New Year": "Nouvel An chinois", + "Chinese New Year's Eve": "Réveillon du Nouvel An chinois", + "Tomb-Sweeping Day": "Fête de Qingming", + "Labor Day": "Fête du Travail", + "Dragon Boat Festival": "Fête des bateaux-dragons", + "Mid-Autumn Festival": "Fête de la mi-automne", + "National Day": "Fête nationale", + "Christmas Day": "Noël", + "Christmas Eve": "Réveillon de Noël", + "Good Friday": "Vendredi saint", + "Easter Monday": "Lundi de Pâques", + "Peace Memorial Day": "Journée de commémoration de la paix", + "Children's Day": "Fête des enfants", + "Constitution Day": "Jour de la Constitution", + "Confucius' Birthday": "Anniversaire de Confucius" + }, + "holidayCaveat": "Les dates peuvent changer ; consultez les annonces officielles locales." }, "tasks": { "eyebrow": "OBJECTIFS ET TÂCHES", diff --git a/public/locales/ja.json b/public/locales/ja.json index 659d0a9..1b2bf6e 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -367,7 +367,47 @@ "emptyDay": "この日の記録はありません。", "openToday": "記憶を書く", "writeMemory": "記憶を追加", - "openTasks": "タスクを開く" + "openTasks": "タスクを開く", + "holidayCountry": "国・地域", + "autoCountry": "自動({country})", + "publicHoliday": "祝日", + "festival": "記念日", + "workingDay": "勤務日", + "observed": "振替休日", + "estimated": "日付は暫定", + "workingWeekend": "週末の振替出勤日", + "loadingHolidays": "祝日を読み込み中…", + "holidayUnavailable": "祝日データを取得できません。", + "retryHolidays": "再試行", + "holidaySource": "祝日データ", + "outsideRange": "祝日データは2024~2030年に対応しています。", + "festivals": { + "valentine": "バレンタインデー", + "earth": "アースデー", + "halloween": "ハロウィーン", + "christmasEve": "クリスマスイブ", + "teachers": "教師の日" + }, + "holidayNames": { + "New Year's Day": "元日", + "Chinese New Year (Spring Festival)": "春節", + "Chinese New Year": "春節", + "Chinese New Year's Eve": "大晦日(旧暦)", + "Tomb-Sweeping Day": "清明節", + "Labor Day": "労働者の日", + "Dragon Boat Festival": "端午節", + "Mid-Autumn Festival": "中秋節", + "National Day": "国慶節", + "Christmas Day": "クリスマス", + "Christmas Eve": "クリスマスイブ", + "Good Friday": "聖金曜日", + "Easter Monday": "イースターマンデー", + "Peace Memorial Day": "平和記念日", + "Children's Day": "こどもの日", + "Constitution Day": "憲法記念日", + "Confucius' Birthday": "孔子誕生日" + }, + "holidayCaveat": "日付は変更される場合があります。現地の公式発表をご確認ください。" }, "tasks": { "eyebrow": "目標とタスク", diff --git a/public/locales/ru.json b/public/locales/ru.json index 6852153..b867cee 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -367,7 +367,47 @@ "emptyDay": "За этот день нет записей или событий.", "openToday": "Записать воспоминание", "writeMemory": "Добавить воспоминание", - "openTasks": "Открыть задачи" + "openTasks": "Открыть задачи", + "holidayCountry": "Страна или регион", + "autoCountry": "Авто ({country})", + "publicHoliday": "Государственный праздник", + "festival": "Праздник", + "workingDay": "Рабочий день", + "observed": "Перенесённый выходной", + "estimated": "Дата предварительная", + "workingWeekend": "Рабочий выходной", + "loadingHolidays": "Загрузка праздников…", + "holidayUnavailable": "Данные о праздниках недоступны.", + "retryHolidays": "Повторить", + "holidaySource": "Данные о праздниках", + "outsideRange": "Данные доступны за 2024–2030 годы.", + "festivals": { + "valentine": "День святого Валентина", + "earth": "День Земли", + "halloween": "Хэллоуин", + "christmasEve": "Сочельник", + "teachers": "День учителя" + }, + "holidayNames": { + "New Year's Day": "Новый год", + "Chinese New Year (Spring Festival)": "Китайский Новый год", + "Chinese New Year": "Китайский Новый год", + "Chinese New Year's Eve": "Канун китайского Нового года", + "Tomb-Sweeping Day": "Праздник Цинмин", + "Labor Day": "День труда", + "Dragon Boat Festival": "Праздник драконьих лодок", + "Mid-Autumn Festival": "Праздник середины осени", + "National Day": "Национальный день", + "Christmas Day": "Рождество", + "Christmas Eve": "Сочельник", + "Good Friday": "Страстная пятница", + "Easter Monday": "Пасхальный понедельник", + "Peace Memorial Day": "День памяти мира", + "Children's Day": "День детей", + "Constitution Day": "День Конституции", + "Confucius' Birthday": "День рождения Конфуция" + }, + "holidayCaveat": "Даты могут измениться; сверяйтесь с официальными объявлениями." }, "tasks": { "eyebrow": "ЦЕЛИ И ЗАДАЧИ", diff --git a/public/locales/zh-CN.json b/public/locales/zh-CN.json index a7655b5..dbc46b2 100644 --- a/public/locales/zh-CN.json +++ b/public/locales/zh-CN.json @@ -367,7 +367,47 @@ "emptyDay": "这一天还没有记忆或目标节点。", "openToday": "写下一段记忆", "writeMemory": "新增记忆", - "openTasks": "打开待办" + "openTasks": "打开待办", + "holidayCountry": "国家或地区", + "autoCountry": "自动({country})", + "publicHoliday": "公共假期", + "festival": "节日", + "workingDay": "工作日", + "observed": "补休", + "estimated": "日期待确认", + "workingWeekend": "调休补班", + "loadingHolidays": "正在加载节假日…", + "holidayUnavailable": "节假日数据暂不可用。", + "retryHolidays": "重试", + "holidaySource": "公共假期数据", + "outsideRange": "节假日数据覆盖 2024—2030 年。", + "festivals": { + "valentine": "情人节", + "earth": "世界地球日", + "halloween": "万圣节", + "christmasEve": "平安夜", + "teachers": "教师节" + }, + "holidayNames": { + "New Year's Day": "元旦", + "Chinese New Year (Spring Festival)": "春节", + "Chinese New Year": "春节", + "Chinese New Year's Eve": "除夕", + "Tomb-Sweeping Day": "清明节", + "Labor Day": "劳动节", + "Dragon Boat Festival": "端午节", + "Mid-Autumn Festival": "中秋节", + "National Day": "国庆节", + "Christmas Day": "圣诞节", + "Christmas Eve": "平安夜", + "Good Friday": "耶稣受难日", + "Easter Monday": "复活节星期一", + "Peace Memorial Day": "和平纪念日", + "Children's Day": "儿童节", + "Constitution Day": "行宪纪念日", + "Confucius' Birthday": "孔子诞辰" + }, + "holidayCaveat": "日期可能调整,请以当地官方公告为准。" }, "tasks": { "eyebrow": "目标与待办", diff --git a/public/locales/zh-HK.json b/public/locales/zh-HK.json index 1f4a143..d2b36fc 100644 --- a/public/locales/zh-HK.json +++ b/public/locales/zh-HK.json @@ -367,7 +367,47 @@ "emptyDay": "這一天還沒有記憶或目標節點。", "openToday": "寫下一段記憶", "writeMemory": "新增記憶", - "openTasks": "打開待辦" + "openTasks": "打開待辦", + "holidayCountry": "國家或地區", + "autoCountry": "自動({country})", + "publicHoliday": "公眾假期", + "festival": "節日", + "workingDay": "工作日", + "observed": "補假", + "estimated": "日期待確認", + "workingWeekend": "週末補班", + "loadingHolidays": "正在載入節假日…", + "holidayUnavailable": "節假日資料暫不可用。", + "retryHolidays": "重試", + "holidaySource": "公眾假期資料", + "outsideRange": "節假日資料涵蓋 2024—2030 年。", + "festivals": { + "valentine": "情人節", + "earth": "世界地球日", + "halloween": "萬聖節", + "christmasEve": "平安夜", + "teachers": "教師節" + }, + "holidayNames": { + "New Year's Day": "元旦", + "Chinese New Year (Spring Festival)": "春節", + "Chinese New Year": "春節", + "Chinese New Year's Eve": "除夕", + "Tomb-Sweeping Day": "清明節", + "Labor Day": "勞動節", + "Dragon Boat Festival": "端午節", + "Mid-Autumn Festival": "中秋節", + "National Day": "國慶日", + "Christmas Day": "聖誕節", + "Christmas Eve": "平安夜", + "Good Friday": "耶穌受難日", + "Easter Monday": "復活節星期一", + "Peace Memorial Day": "和平紀念日", + "Children's Day": "兒童節", + "Constitution Day": "行憲紀念日", + "Confucius' Birthday": "孔子誕辰" + }, + "holidayCaveat": "日期或有調整,請以當地官方公告為準。" }, "tasks": { "eyebrow": "目標與待辦", diff --git a/public/locales/zh-MO.json b/public/locales/zh-MO.json index 4d2228f..6d44b2e 100644 --- a/public/locales/zh-MO.json +++ b/public/locales/zh-MO.json @@ -367,7 +367,47 @@ "emptyDay": "這一天還沒有記憶或目標節點。", "openToday": "寫下一段記憶", "writeMemory": "新增記憶", - "openTasks": "打開待辦" + "openTasks": "打開待辦", + "holidayCountry": "國家或地區", + "autoCountry": "自動({country})", + "publicHoliday": "公眾假期", + "festival": "節日", + "workingDay": "工作日", + "observed": "補假", + "estimated": "日期待確認", + "workingWeekend": "週末補班", + "loadingHolidays": "正在載入節假日…", + "holidayUnavailable": "節假日資料暫不可用。", + "retryHolidays": "重試", + "holidaySource": "公眾假期資料", + "outsideRange": "節假日資料涵蓋 2024—2030 年。", + "festivals": { + "valentine": "情人節", + "earth": "世界地球日", + "halloween": "萬聖節", + "christmasEve": "平安夜", + "teachers": "教師節" + }, + "holidayNames": { + "New Year's Day": "元旦", + "Chinese New Year (Spring Festival)": "春節", + "Chinese New Year": "春節", + "Chinese New Year's Eve": "除夕", + "Tomb-Sweeping Day": "清明節", + "Labor Day": "勞動節", + "Dragon Boat Festival": "端午節", + "Mid-Autumn Festival": "中秋節", + "National Day": "國慶日", + "Christmas Day": "聖誕節", + "Christmas Eve": "平安夜", + "Good Friday": "耶穌受難日", + "Easter Monday": "復活節星期一", + "Peace Memorial Day": "和平紀念日", + "Children's Day": "兒童節", + "Constitution Day": "行憲紀念日", + "Confucius' Birthday": "孔子誕辰" + }, + "holidayCaveat": "日期或有調整,請以當地官方公告為準。" }, "tasks": { "eyebrow": "目標與待辦", diff --git a/public/locales/zh-TW.json b/public/locales/zh-TW.json index 312e4a6..dc2a432 100644 --- a/public/locales/zh-TW.json +++ b/public/locales/zh-TW.json @@ -367,7 +367,47 @@ "emptyDay": "這一天還沒有記憶或目標節點。", "openToday": "寫下一段記憶", "writeMemory": "新增記憶", - "openTasks": "開啟待辦" + "openTasks": "開啟待辦", + "holidayCountry": "國家或地區", + "autoCountry": "自動({country})", + "publicHoliday": "國定假日", + "festival": "節日", + "workingDay": "工作日", + "observed": "補假", + "estimated": "日期待確認", + "workingWeekend": "週末補班", + "loadingHolidays": "正在載入節假日…", + "holidayUnavailable": "節假日資料暫時無法取得。", + "retryHolidays": "重試", + "holidaySource": "國定假日資料", + "outsideRange": "節假日資料涵蓋 2024—2030 年。", + "festivals": { + "valentine": "情人節", + "earth": "世界地球日", + "halloween": "萬聖節", + "christmasEve": "平安夜", + "teachers": "教師節" + }, + "holidayNames": { + "New Year's Day": "元旦", + "Chinese New Year (Spring Festival)": "春節", + "Chinese New Year": "春節", + "Chinese New Year's Eve": "除夕", + "Tomb-Sweeping Day": "清明節", + "Labor Day": "勞動節", + "Dragon Boat Festival": "端午節", + "Mid-Autumn Festival": "中秋節", + "National Day": "國慶日", + "Christmas Day": "聖誕節", + "Christmas Eve": "平安夜", + "Good Friday": "耶穌受難日", + "Easter Monday": "復活節星期一", + "Peace Memorial Day": "和平紀念日", + "Children's Day": "兒童節", + "Constitution Day": "行憲紀念日", + "Confucius' Birthday": "孔子誕辰" + }, + "holidayCaveat": "日期可能調整,請以當地官方公告為準。" }, "tasks": { "eyebrow": "目標與待辦", diff --git a/public/service-worker.js b/public/service-worker.js index a419156..2c40786 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const CACHE = 'life-os-shell-v1.17.0'; +const CACHE = 'life-os-shell-v1.18.0'; const SHELL = [ '/', '/index.html', '/manifest.webmanifest', '/life-os-icon.svg' ]; diff --git a/public/system-navigation.js b/public/system-navigation.js index 3c19a4d..4aa102b 100644 --- a/public/system-navigation.js +++ b/public/system-navigation.js @@ -26,7 +26,7 @@ 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.17.0' })}
${t('quickSettings')}
${['appearance', 'notifications', 'language', 'data'].map(id => `
${navigationButton(id, true)}

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

`).join('')}
`; + view.innerHTML = `
${t('device')}

${t('deviceHint')}

${t('version', { version: window.LifeOSVersion || '1.18.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'; diff --git a/public/terminal-app.js b/public/terminal-app.js index a22f2b2..a411fc9 100644 --- a/public/terminal-app.js +++ b/public/terminal-app.js @@ -150,7 +150,7 @@ return { message: new Intl.DateTimeFormat(LifeI18n.locale(), 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.17.0'}` }; } + if (kind === 'version') { if (verb) throw new Error('usage'); return { message: `Life OS v${window.LifeOSVersion || '1.18.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.17.0' }), kind: 'system' }); welcomed = true; } + if (!welcomed) { lines.unshift({ value: t('welcome', { version: window.LifeOSVersion || '1.18.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 95602d0..3db68b5 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -8,7 +8,7 @@ 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, 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.17.0"; +const release = "1.18.0"; const localeEnJs = `window.LifeOSLocales=window.LifeOSLocales||{};window.LifeOSLocales.en=${localeEn};`; const localeZhJs = `window.LifeOSLocales=window.LifeOSLocales||{};window.LifeOSLocales['zh-CN']=${localeZh};`; const extraLocaleCodes = ['zh-HK', 'zh-MO', 'zh-TW', 'ar', 'fr', 'ru', 'es', 'ja']; diff --git a/scripts/validate.mjs b/scripts/validate.mjs index cf6e596..d23c841 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -218,7 +218,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.17.0'); +assert.equal(command('version').message, 'Life OS v1.18.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'); @@ -475,4 +475,42 @@ assert.match(await enLogin.text(), /Sign in with ChatGPT/); const localeResponse = await worker.default.fetch(new Request("https://life-os.test/locales/zh-CN.js"), {}); assert.equal(localeResponse.status, 200); assert.match(await localeResponse.text(), /window\.LifeOSLocales\['zh-CN'\]/); +const invalidHolidays = await worker.default.fetch(new Request('https://life-os.test/api/holidays?country=../../x&year=2026'), {}); +assert.equal(invalidHolidays.status, 400); +const upstreamFetch = globalThis.fetch; +const holidayRequests = []; +globalThis.fetch = async url => { + holidayRequests.push(String(url)); + if (String(url).endsWith('/countries.json')) return Response.json({ countries: [{ code: 'CN', name: 'China' }, { code: 'FR', name: 'France' }] }); + return Response.json({ code: 'CN', year: 2026, holidays: [{ date: '2026-10-01', name: 'National Day' }, { date: '2026-10-02', name: 'Day off', observed: true }], working_weekend_days: ['2026-10-10'] }); +}; +try { + const response = await worker.default.fetch(new Request('https://life-os.test/api/holidays?country=CN&year=2026'), {}); + assert.equal(response.status, 200); + const data = await response.json(); + assert.equal(data.holidays[1].observed, true); + assert.deepEqual(data.workingDays, ['2026-10-10']); + assert.equal(holidayRequests[0], 'https://openworkdays.com/api/v1/cn/2026.json'); + const countryResponse = await worker.default.fetch(new Request('https://life-os.test/api/holiday-countries'), {}); + assert.equal((await countryResponse.json()).countries[1].code, 'FR'); +} finally { globalThis.fetch = upstreamFetch; } +const holidayStore = makeStorage(); +const holidayNode = { innerHTML: '' }; +const holidayContext = vm.createContext({ + document: { createElement: () => ({}), head: { append() {} } }, + navigator: { languages: ['en-GB'], language: 'en-GB' }, localStorage: holidayStore, + LifeI18n: { t: (key, values = {}) => String(key.split('.').reduce((item, segment) => item?.[segment], assets.localeEn) || key).replace(/\{(\w+)\}/g, (_, name) => values[name] ?? ''), locale: () => 'en-GB', language: () => 'en', dictionaries: { en: assets.localeEn } }, + mems: () => [], Date, Intl, + fetch: async url => ({ ok: true, json: async () => String(url).includes('holiday-countries') ? { countries: [{ code: 'GB', name: 'United Kingdom' }, { code: 'FR', name: 'France' }] } : { holidays: [{ date: '2026-10-01', name: 'National Day' }], workingDays: ['2026-10-10'] } }) +}); +holidayContext.window = holidayContext; +holidayContext.renderAll = () => holidayContext.calendarApp(holidayNode); +vm.runInContext(assets.calendarJs, holidayContext); +holidayContext.selectCalendarDate('2026-10-01'); +await new Promise(resolve => setTimeout(resolve, 0)); +assert.match(holidayNode.innerHTML, /Auto \(United Kingdom\)/, 'English resolves its browser region'); +assert.match(holidayNode.innerHTML, /National Day/, 'holiday appears on the selected day'); +holidayContext.setCalendarHolidayCountry('FR'); +assert.equal(holidayStore.getItem('lifeos_holiday_country_v1'), 'FR'); +assert.match(holidayNode.innerHTML, /