diff --git a/CHANGELOG.md b/CHANGELOG.md index 6988dd0..cc4fc9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 本项目按 `v主版本.次版本.修订版本` 标记发布。下列内容以对应 Git tag 和已提交功能为准。 +## v1.8.0 — 2026-09-28 + +- 目标倒计时成为可拖动、可缩放的小部件,位置和尺寸保存在本设备。 +- 拖动图标及小部件时显示吸附网格;小部件操作柄支持方向键调整。 +- 增加时钟、今日概览与快捷操作小部件,可在系统设置中分别开关和重置布局。 +- 小部件布局纳入 JSON 备份与数据重置。 + ## v1.7.0 — 2026-09-28 - 桌面图标可拖动排列并保存格位;可选择显示哪些 App 快捷方式。 diff --git a/README.md b/README.md index 6c36114..36fe1fe 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 以桌面操作系统为交互方式的个人记录网站。用 App 记录日常、管理目标与待办,并沿时间线回看已经留下的内容。 -在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.7.0** · [更新日志](CHANGELOG.md) +在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.8.0** · [更新日志](CHANGELOG.md) ## 功能 @@ -17,7 +17,7 @@ | 档案 | 查看记录概况并维护个人资料。 | | 系统设置 | 选择语言、主题、壁纸、桌面快捷方式和开机 App,管理备份。 | -桌面端支持单击选择、双击打开 App、多窗口、任务栏恢复、图标拖动排列和窗口布局记忆;移动端以横排图标和全屏 App 为主。可选必应每日图片、内置背景或自定义 HTTPS 图片链接。中文与英文可自动跟随浏览器语言,也可手动选择。 +桌面端支持单击选择、双击打开 App、多窗口、任务栏恢复、图标拖动排列和窗口布局记忆。拖动图标或小部件时会显示吸附网格;目标倒计时、时钟、今日概览与快捷操作小部件可移动、调整大小和分别开关。移动端以横排图标和全屏 App 为主,小部件在桌面下方排列。可选必应每日图片、内置背景或自定义 HTTPS 图片链接。中文与英文可自动跟随浏览器语言,也可手动选择。 快捷键:`Ctrl / ⌘ + K` 打开启动器,`Ctrl / ⌘ + F` 搜索记忆,`Ctrl / ⌘ + S` 保存今日记忆;`Alt + 1–8` 打开对应 App。站点包含 PWA 清单和 Service Worker,离线时可打开已缓存的应用外壳;云同步与必应图片仍需要联网。 @@ -25,7 +25,7 @@ 站点使用 ChatGPT 登录标识隔离不同用户的数据。登录后,资料通过同源 API 保存到 Sites 的 D1 数据库;浏览器 `localStorage` 充当缓存、草稿及首次迁移来源。首次云端恢复前,本地资料会保留一份迁移前副本。目标与任务存于 `lifeos_system.tasksBoard`,记忆及其可选信息存于 `lifeos_memories`。 -**系统设置 → 导出数据** 会生成 JSON 备份;导入支持合并或覆盖。备份格式使用 `schemaVersion: 2`,仍可导入 v1 备份。壁纸来源、快捷方式、开机 App 等设置包含在同步与备份中;窗口位置和尺寸保存在本设备的 `lifeos_window_layout_v1`,也包含在 JSON 导出中,覆盖导入时可恢复。未保存的今日草稿仅留在本机。 +**系统设置 → 导出数据** 会生成 JSON 备份;导入支持合并或覆盖。备份格式使用 `schemaVersion: 2`,仍可导入 v1 备份。壁纸来源、快捷方式、开机 App 和小部件开关包含在同步与备份中;窗口布局与小部件格位分别保存在本设备的 `lifeos_window_layout_v1`、`lifeos_widget_layout_v1`,也包含在 JSON 导出中,覆盖导入时可恢复。未保存的今日草稿仅留在本机。 主要数据键:`lifeos_memories`、`lifeos_museum`、`lifeos_profile`、`lifeos_settings`、`lifeos_system`。D1 的初始迁移位于 `drizzle/0000_lifeos_cloud_state.sql`:它将各类数据作为 JSON 字段保存在每个用户的一行记录中;应用的备份 schema 版本与数据库迁移编号是两回事。建议定期导出备份。 @@ -48,6 +48,7 @@ public/tasks.js 目标与待办 public/calendar.js 日历 public/i18n-renderers.js 记忆、洞察、设置等双语界面 public/desktop-personalization.js 图标、窗口与壁纸偏好 +public/desktop-widgets.js 吸附网格与桌面小部件 public/addons.js 导入导出、云同步与辅助操作 public/locales/ 中文、英文文案 public/service-worker.js PWA 缓存 diff --git a/package.json b/package.json index 75d4374..20d94ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "life-os", - "version": "1.7.0", + "version": "1.8.0", "private": true, "type": "module", "scripts": { diff --git a/public/addons.js b/public/addons.js index a1c8a22..efa4b06 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.7.0'; + const APP_VERSION = '1.8.0'; window.LifeOSVersion = APP_VERSION; const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system']; const t = (key, values) => LifeI18n.t(key, values); diff --git a/public/desktop-widgets.js b/public/desktop-widgets.js index 4e2060e..1998d3a 100644 --- a/public/desktop-widgets.js +++ b/public/desktop-widgets.js @@ -145,6 +145,6 @@ style.textContent = `#desktop-widget-layer{position:absolute;inset:0;z-index:1;pointer-events:none}#desktop-snap-grid{position:absolute;z-index:20;left:24px;top:72px;width:calc(100% - 48px);height:calc(100% - 175px);pointer-events:none;opacity:0;transition:opacity .12s;background-image:linear-gradient(to right,color-mix(in srgb,var(--accent) 25%,transparent) 1px,transparent 1px),linear-gradient(to bottom,color-mix(in srgb,var(--accent) 25%,transparent) 1px,transparent 1px);background-size:110px 98px}#desktop-snap-grid.visible{opacity:1}.snap-target{position:absolute;border:2px solid var(--accent);border-radius:13px;background:color-mix(in srgb,var(--accent) 14%,transparent);box-shadow:0 0 0 2px var(--bg)}.desktop-widget{position:absolute;display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden;background:color-mix(in srgb,var(--panel) 88%,transparent);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);backdrop-filter:blur(12px);pointer-events:auto}.desktop-widget.is-adjusting{border-color:var(--accent)}.widget-drag{display:flex;align-items:center;justify-content:space-between;gap:8px;flex:none;padding:10px 13px;border-bottom:1px solid var(--line);font:500 11px 'DM Mono';letter-spacing:.1em;color:var(--muted);cursor:grab;touch-action:none}.widget-drag:active{cursor:grabbing}.widget-drag:focus-visible,.widget-resize:focus-visible{outline:2px solid var(--accent);outline-offset:-3px}.widget-body{flex:1;min-height:0;overflow:auto;padding:10px}.widget-resize{position:absolute;right:0;bottom:0;width:22px;height:22px;cursor:nwse-resize;touch-action:none;background:linear-gradient(135deg,transparent 65%,var(--accent) 65%,var(--accent) 72%)}.desktop-widget .desktop-goal-countdowns{position:static!important;width:100%!important;height:auto!important;max-height:none!important;margin:0!important;display:grid;grid-template-columns:1fr;gap:8px;overflow:visible}.desktop-widget .desktop-goal-countdowns.hidden{display:none!important}.desktop-widget .desktop-goal-heading{display:none}.desktop-widget .desktop-goal-card{box-shadow:none;padding:10px 12px}.widget-empty-message{border:0;background:transparent;color:var(--muted);padding:10px;text-align:left;font-size:13px}@media(max-width:650px){#desktop-snap-grid{display:none}#desktop-widget-layer{position:relative;inset:auto;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:10px 14px 90px}.desktop-widget{position:relative;left:auto!important;top:auto!important;width:auto!important;height:auto!important;min-height:140px}.widget-drag{cursor:default}.widget-resize{display:none}.widget-body{max-height:35vh}}`; document.head.append(style); const contentStyle = document.createElement('style'); - contentStyle.textContent = `.widget-clock-time{font:600 clamp(26px,3vw,42px) Manrope,system-ui;letter-spacing:-.06em;line-height:1.2}.widget-clock-date{font-size:13px;color:var(--muted);line-height:1.5}.widget-today-row{display:flex;align-items:baseline;gap:10px;margin:6px 0}.widget-today-row strong{font-size:25px;color:var(--accent)}.widget-today-row span{font-size:13px;color:var(--muted)}.widget-link{border:0;background:none;color:var(--accent);padding:5px 0;text-decoration:underline;font-size:12px}.widget-actions{display:grid;gap:8px}.widget-actions button{border:1px solid var(--line);border-radius:8px;background:var(--panel2);color:var(--ink);padding:9px;text-align:left;font-size:13px}.widget-actions button:hover,.widget-actions button:focus-visible{border-color:var(--accent)}.widget-preferences .desktop-app-choices{display:flex;gap:8px;flex-wrap:wrap}.widget-preferences .chip{font-size:12px;min-height:36px}`; + contentStyle.textContent = `.widget-clock-time{font:600 clamp(26px,3vw,42px) Manrope,system-ui;letter-spacing:-.06em;line-height:1.2}.widget-clock-date{font-size:13px;color:var(--muted);line-height:1.5}.widget-today-row{display:flex;align-items:baseline;gap:10px;margin:6px 0}.widget-today-row strong{font-size:25px;color:var(--accent)}.widget-today-row span{font-size:13px;color:var(--muted)}.widget-link{border:0;background:none;color:var(--accent);padding:5px 0;text-decoration:underline;font-size:12px}.widget-actions{display:grid;gap:8px}.widget-actions button{border:1px solid var(--line);border-radius:8px;background:var(--panel2);color:var(--ink);padding:9px;text-align:left;font-size:13px}.widget-actions button:hover,.widget-actions button:focus-visible{border-color:var(--accent)}.widget-preferences .desktop-app-choices{display:flex;gap:8px;flex-wrap:wrap}.widget-preferences .chip{font-size:12px;min-height:36px}@media(max-width:650px){.taskbar{position:fixed}}`; document.head.append(contentStyle); })(); diff --git a/public/i18n-renderers.js b/public/i18n-renderers.js index 200a4d6..4da1616 100644 --- a/public/i18n-renderers.js +++ b/public/i18n-renderers.js @@ -110,7 +110,7 @@ window.saveProfile = () => { save('lifeos_profile', { name: pname.value, birthday: pbday.value, startDate: pstart.value || day(), motto: pmotto.value }); renderAll(); toast(t('profile.saved')); }; window.systemApp = container => { const state = settings(), preference = LifeI18n.preference(), bytes = new Blob([JSON.stringify({ mems: mems(), museum: museum(), profile: profile(), system: sys() })]).size, oldBackup = state.lastBackup && Date.now() - new Date(state.lastBackup) > 12096e5, detected = browserLanguage() === 'zh' ? t('system.detectedZh') : t('system.detectedEn'); - container.innerHTML = `
${detected}
${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.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.release',{version:window.LifeOSVersion||'1.7.0'})}
${detected}
${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.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.release',{version:window.LifeOSVersion||'1.8.0'})}
${t('dialog.exported',{date:short(data.exportedAt.slice(0,10))})}
${t('dialog.memories',{count:data.memories.length})}
${t('dialog.museumItems',{count:data.museum.length})}
${t('dialog.goals',{count:board.goals?.length || 0})}
${t('dialog.tasks',{count:board.tasks?.length || 0})}
${t('dialog.replaceWarning')}
${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]?``:''}`:`${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]?``:''}`:`How was your day?
${i18n('How was your day?')}
Local Storage
${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB
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':''}
Life OS
Version ${window.LifeOSVersion||'1.7.0'} · Schema v2
Exported: ${fmtDate(d.exportedAt.slice(0,10))}
Memories: ${d.memories.length}
Museum Items: ${d.museum.length}
This will replace all current Life OS data.
Export a backup first if you want to keep it.
Local Storage
${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB
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':''}
Life OS
Version ${window.LifeOSVersion||'1.8.0'} · Schema v2
Exported: ${fmtDate(d.exportedAt.slice(0,10))}
Memories: ${d.memories.length}
Museum Items: ${d.museum.length}
This will replace all current Life OS data.
Export a backup first if you want to keep it.