Compare commits
No files matched your search
@@ -10,11 +10,13 @@
|
||||
- **Today**:每日文字记录、情绪天气、1–5 能量等级,以及随机生成的“今日电影”。
|
||||
- **Timeline**:按时间轴浏览、编辑和删除记忆。
|
||||
- **Museum**:将重要记忆长期珍藏为馆藏。
|
||||
- **Tasks / Goals**:创建目标和待办,关联任务、设置截止日期与优先级,查看今天/即将开始/已完成事项并追踪目标进度。
|
||||
- **Insights / Profile**:查看情绪、能量、连续记录与个人系统状态。
|
||||
- **System**:深浅主题、自动识别浏览器语言(也可固定中文/English)、JSON 导入导出与本地存储说明。
|
||||
- **Bing 每日一图**:桌面壁纸每日自动更新;由 Life OS 同源代理读取,避免浏览器跨域限制。
|
||||
- **Windows 式操作**:桌面端单击选择、双击打开;右键桌面或 App 图标可使用快捷菜单。
|
||||
- **快捷键与草稿恢复**:`Ctrl / ⌘ + K` 打开启动器,`Ctrl / ⌘ + F` 搜索记忆,`Ctrl / ⌘ + S` 归档 Today;当天未归档文字会自动恢复。
|
||||
- **Tasks 快捷键**:`Alt + 6` 打开目标与待办,`Alt + 7` 打开 System。
|
||||
- **移动端**:App 以全屏方式打开,保留任务栏恢复入口。
|
||||
- **个人云端**:使用 ChatGPT 登录后,数据按账号隔离并同步到 SQLite 兼容的 D1 数据库。
|
||||
|
||||
@@ -32,7 +34,7 @@
|
||||
- `lifeos_settings`
|
||||
- `lifeos_system`
|
||||
|
||||
备份格式包含 `schemaVersion: 1`,为后续数据迁移预留兼容空间。
|
||||
备份格式包含 `schemaVersion: 1`,为后续数据迁移预留兼容空间。目标和待办保存在 `lifeos_system.tasksBoard`,随现有云端 system 字段同步,并包含在新版 JSON 备份中。
|
||||
|
||||
## 运行
|
||||
|
||||
@@ -50,7 +52,8 @@ npm run validate
|
||||
## 项目结构
|
||||
|
||||
```text
|
||||
public/index.html # 界面、样式与本地数据逻辑
|
||||
public/index.html # 桌面框架与窗口交互
|
||||
public/tasks.js # 目标与待办 App
|
||||
public/addons.js # 窗口与辅助交互
|
||||
worker/index.js # ChatGPT 登录门槛、云端数据 API 与 Bing 壁纸代理
|
||||
drizzle/ # SQLite / D1 数据库迁移
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "life-os",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+23
-31
@@ -1,10 +1,9 @@
|
||||
/* Life OS reliability and productivity add-ons. */
|
||||
(() => {
|
||||
const DRAFT_KEY = 'lifeos_today_draft_v1';
|
||||
const APP_VERSION = '1.1.0';
|
||||
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'system'];
|
||||
const zh = () => (typeof lang === 'function' ? lang() : 'en') === 'zh';
|
||||
const copy = (en, cn) => (zh() ? cn : en);
|
||||
const APP_VERSION = '1.2.0';
|
||||
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'system'];
|
||||
const t = (key, values) => LifeI18n.t(key, values);
|
||||
|
||||
function readDraft() {
|
||||
try { return JSON.parse(localStorage.getItem(DRAFT_KEY)); } catch { return null; }
|
||||
@@ -18,7 +17,7 @@
|
||||
if (!draft || draft.date !== today || !draft.text || entry.value) return;
|
||||
entry.value = draft.text;
|
||||
entry.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
if (typeof toast === 'function') toast(copy('DRAFT RESTORED', '草稿已恢复'));
|
||||
if (typeof toast === 'function') toast('addon.draftRestored');
|
||||
}
|
||||
function wireToday() {
|
||||
const entry = document.querySelector('#entry');
|
||||
@@ -27,21 +26,7 @@
|
||||
restoreDraft(entry);
|
||||
entry.addEventListener('input', () => saveDraft(entry.value));
|
||||
}
|
||||
function wireLanguageControl() {
|
||||
const settingsPanel = document.querySelector('#content-system .settings');
|
||||
if (!settingsPanel || settingsPanel.querySelector('#lifeos-browser-language')) return;
|
||||
const state = settings();
|
||||
const actual = typeof browserLanguage === 'function' ? browserLanguage() : 'en';
|
||||
const section = document.createElement('section');
|
||||
section.id = 'lifeos-browser-language';
|
||||
section.innerHTML = `<div class="eyebrow">${copy('BROWSER LANGUAGE', '浏览器语言')}</div><p class="sub">${copy(`Detected: ${actual === 'zh' ? 'Chinese' : 'English'}`, `当前识别:${actual === 'zh' ? '中文' : 'English'}`)}</p><div class="choices"><button class="chip ${state.languageMode !== 'manual' ? 'selected' : ''}" data-language="auto">${copy('AUTO', '跟随浏览器')}</button><button class="chip ${state.languageMode === 'manual' && state.language === 'en' ? 'selected' : ''}" data-language="en">ENGLISH</button><button class="chip ${state.languageMode === 'manual' && state.language === 'zh' ? 'selected' : ''}" data-language="zh">中文</button></div>`;
|
||||
section.querySelectorAll('button').forEach(button => button.addEventListener('click', () => setLanguage(button.dataset.language)));
|
||||
[...settingsPanel.children].forEach(node => {
|
||||
const label = node.querySelector?.('.eyebrow')?.textContent.trim();
|
||||
if (label === 'LANGUAGE' || label === '语言') node.remove();
|
||||
});
|
||||
settingsPanel.prepend(section);
|
||||
}
|
||||
function wireLanguageControl() { /* The System renderer owns this control. */ }
|
||||
function clearDraftAfterSave() {
|
||||
setTimeout(() => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
@@ -57,7 +42,7 @@
|
||||
closeSearch();
|
||||
const box = document.createElement('div');
|
||||
box.id = 'lifeos-search';
|
||||
box.innerHTML = `<div class="lifeos-search-backdrop"></div><section class="lifeos-search-panel"><div class="lifeos-search-head"><span>${copy('SEARCH MEMORY ARCHIVE', '搜索记忆档案')}</span><button aria-label="Close">×</button></div><input autofocus placeholder="${copy('Search titles, notes, moods…', '搜索标题、记录或情绪…')}"><div class="lifeos-search-results"></div><p class="lifeos-search-hint">${copy('ESC to close · click a result to open it', 'ESC 关闭 · 点击结果即可打开')}</p></section>`;
|
||||
box.innerHTML = `<div class="lifeos-search-backdrop"></div><section class="lifeos-search-panel"><div class="lifeos-search-head"><span>${t('addon.searchTitle')}</span><button aria-label="${t('common.close')}">×</button></div><input autofocus placeholder="${t('addon.searchPlaceholder')}"><div class="lifeos-search-results"></div><p class="lifeos-search-hint">${t('addon.searchHint')}</p></section>`;
|
||||
document.body.append(box);
|
||||
const input = box.querySelector('input');
|
||||
const results = box.querySelector('.lifeos-search-results');
|
||||
@@ -66,7 +51,7 @@
|
||||
try { memories = JSON.parse(localStorage.getItem('lifeos_memories') || '[]'); } catch {}
|
||||
const query = input.value.trim().toLowerCase();
|
||||
const found = memories.filter(m => !query || [m.filmTitle, m.text, m.mood, m.date].join(' ').toLowerCase().includes(query)).slice(0, 8);
|
||||
results.innerHTML = found.length ? found.map(m => `<button data-id="${m.id}"><small>${m.date} · ${(m.mood || '').toUpperCase()}</small><strong>${m.filmTitle || copy('Untitled memory', '未命名记忆')}</strong><span>${(m.text || copy('No written note.', '未写下记录。')).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c]))}</span></button>`).join('') : `<div class="lifeos-search-empty">${copy('No memories found.', '没有找到记忆。')}</div>`;
|
||||
results.innerHTML = found.length ? found.map(m => `<button data-id="${m.id}"><small>${m.date} · ${t(`weather.${m.mood || 'cloudy'}`).toUpperCase()}</small><strong>${m.filmTitle || t('addon.untitled')}</strong><span>${(m.text || t('timeline.noNote')).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c]))}</span></button>`).join('') : `<div class="lifeos-search-empty">${t('addon.noResults')}</div>`;
|
||||
results.querySelectorAll('button').forEach(button => button.addEventListener('click', () => { closeSearch(); openApp('timeline'); setTimeout(() => detail(button.dataset.id), 20); }));
|
||||
};
|
||||
input.addEventListener('input', render);
|
||||
@@ -90,11 +75,11 @@
|
||||
});
|
||||
}
|
||||
function exportData() {
|
||||
const data = { app: 'Life OS', version: APP_VERSION, schemaVersion: 1, exportedAt: new Date().toISOString(), profile: profile(), memories: mems(), museum: museum(), settings: settings() };
|
||||
const data = { app: 'Life OS', version: APP_VERSION, schemaVersion: 1, exportedAt: new Date().toISOString(), profile: profile(), memories: mems(), museum: museum(), settings: settings(), system: sys() };
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
|
||||
link.download = `life-os-backup-${day()}.json`; link.click();
|
||||
const state = settings(); state.lastBackup = new Date().toISOString(); save('lifeos_settings', state); renderAll(); toast(copy('BACKUP CREATED', '备份已创建'));
|
||||
const state = settings(); state.lastBackup = new Date().toISOString(); save('lifeos_settings', state); renderAll(); toast('feedback.backup');
|
||||
}
|
||||
const CLOUD_KEYS = { profile: 'lifeos_profile', memories: 'lifeos_memories', museum: 'lifeos_museum', settings: 'lifeos_settings', system: 'lifeos_system' };
|
||||
let cloudReady = false, applyingCloud = false, syncTimer = null, syncErrorShown = false;
|
||||
@@ -104,8 +89,15 @@
|
||||
try { return [field, JSON.parse(localStorage.getItem(key) || JSON.stringify(cloudFallback[field]))]; } catch { return [field, cloudFallback[field]]; }
|
||||
}));
|
||||
}
|
||||
function hasLocalLife(state) { return state.memories.length || state.museum.length || Object.keys(state.profile).length || Object.keys(state.settings).length > 2; }
|
||||
function hasLocalLife(state) { return state.memories.length || state.museum.length || Object.keys(state.profile).length || Object.keys(state.settings).length > 2 || state.system?.tasksBoard?.goals?.length || state.system?.tasksBoard?.tasks?.length; }
|
||||
function applyCloudState(state) {
|
||||
let localSystem = {};
|
||||
try { localSystem = JSON.parse(localStorage.getItem('lifeos_system') || '{}'); } catch {}
|
||||
const localBoard = localSystem.tasksBoard, cloudBoard = state.system?.tasksBoard;
|
||||
if ((localBoard?.goals?.length || localBoard?.tasks?.length) && state.system) {
|
||||
const mergeById = (cloudItems = [], localItems = []) => [...cloudItems, ...localItems.filter(item => !cloudItems.some(existing => existing.id === item.id))];
|
||||
state = { ...state, system: { ...state.system, tasksBoard: { goals: mergeById(cloudBoard?.goals, localBoard.goals), tasks: mergeById(cloudBoard?.tasks, localBoard.tasks) } } };
|
||||
}
|
||||
applyingCloud = true;
|
||||
Object.entries(CLOUD_KEYS).forEach(([field, key]) => localStorage.setItem(key, JSON.stringify(state[field] ?? cloudFallback[field])));
|
||||
applyingCloud = false;
|
||||
@@ -120,15 +112,15 @@
|
||||
if (!cloudReady || applyingCloud) return;
|
||||
clearTimeout(syncTimer);
|
||||
syncTimer = setTimeout(() => uploadCloudState().catch(() => {
|
||||
if (!syncErrorShown) { syncErrorShown = true; toast(copy('CLOUD SYNC PAUSED — CHANGES STAY ON THIS DEVICE', '云端同步暂停——更改仍保存在此设备')); }
|
||||
if (!syncErrorShown) { syncErrorShown = true; toast('addon.syncPaused'); }
|
||||
}), 450);
|
||||
}
|
||||
function migrationPrompt(state) {
|
||||
const dialog = document.createElement('div');
|
||||
dialog.id = 'lifeos-cloud-migration';
|
||||
dialog.innerHTML = `<div class="lifeos-cloud-card"><div class="eyebrow">${copy('CLOUD SYNC READY', '云端同步已就绪')}</div><h2>${copy('Move this browser’s Life OS to the cloud?', '将此浏览器的 Life OS 迁移到云端?')}</h2><p>${copy('Your existing local records will become the first synced copy for this ChatGPT account.', '现有本地记录将成为这个 ChatGPT 账号的第一份云端同步数据。')}</p><div class="actions"><button class="primary" data-cloud="move">${copy('MIGRATE & SYNC', '迁移并同步')}</button><button class="secondary" data-cloud="empty">${copy('START FRESH', '从空白开始')}</button></div></div>`;
|
||||
dialog.innerHTML = `<div class="lifeos-cloud-card"><div class="eyebrow">${t('addon.syncReady')}</div><h2>${t('addon.migrateQuestion')}</h2><p>${t('addon.migrateBody')}</p><div class="actions"><button class="primary" data-cloud="move">${t('addon.migrate')}</button><button class="secondary" data-cloud="empty">${t('addon.fresh')}</button></div></div>`;
|
||||
document.body.append(dialog);
|
||||
dialog.querySelector('[data-cloud="move"]').addEventListener('click', async () => { dialog.remove(); cloudReady = true; await uploadCloudState(); toast(copy('LOCAL LIFE OS MIGRATED', '本地 Life OS 已迁移')); });
|
||||
dialog.querySelector('[data-cloud="move"]').addEventListener('click', async () => { dialog.remove(); cloudReady = true; await uploadCloudState(); toast('addon.migrated'); });
|
||||
dialog.querySelector('[data-cloud="empty"]').addEventListener('click', async () => { dialog.remove(); applyCloudState(cloudFallback); cloudReady = true; await uploadCloudState(); });
|
||||
}
|
||||
async function startCloudSync() {
|
||||
@@ -139,14 +131,14 @@
|
||||
const local = cloudSnapshot();
|
||||
if (state) {
|
||||
if (hasLocalLife(local)) localStorage.setItem('lifeos_precloud_backup_v1', JSON.stringify({ exportedAt: new Date().toISOString(), state: local }));
|
||||
applyCloudState(state); cloudReady = true; toast(copy('CLOUD LIFE OS RESTORED', '云端 Life OS 已恢复'));
|
||||
applyCloudState(state); cloudReady = true; toast('addon.cloudRestored');
|
||||
} else if (hasLocalLife(local)) {
|
||||
migrationPrompt(local);
|
||||
} else {
|
||||
cloudReady = true; await uploadCloudState();
|
||||
}
|
||||
} catch {
|
||||
if (!syncErrorShown) { syncErrorShown = true; toast(copy('CLOUD STORAGE UNAVAILABLE — LOCAL MODE ACTIVE', '云端存储暂不可用——已使用本地模式')); }
|
||||
if (!syncErrorShown) { syncErrorShown = true; toast('addon.cloudUnavailable'); }
|
||||
}
|
||||
}
|
||||
const originalSetItem = Storage.prototype.setItem;
|
||||
@@ -171,7 +163,7 @@
|
||||
if (mod && e.key.toLowerCase() === 'k') { e.preventDefault(); toggleLauncher(); }
|
||||
if (mod && e.key.toLowerCase() === 'f') { e.preventDefault(); openSearch(); }
|
||||
if (mod && e.key.toLowerCase() === 's' && document.querySelector('#entry')) { e.preventDefault(); archiveToday(); }
|
||||
if (e.altKey && /^[1-6]$/.test(e.key)) { e.preventDefault(); openApp(appIds[Number(e.key) - 1]); }
|
||||
if (e.altKey && /^[1-7]$/.test(e.key)) { e.preventDefault(); openApp(appIds[Number(e.key) - 1]); }
|
||||
if (e.key === 'Escape') { closeSearch(); document.querySelector('#launcher')?.classList.add('hidden'); document.querySelector('#calendar')?.classList.add('hidden'); document.querySelector('#life-context')?.remove(); }
|
||||
});
|
||||
})();
|
||||
Loaded 3 of 13 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user