feat(terminal): add Life OS command interface
This commit is contained in:
1 parent
93d0487617
commit
8a1dee3c49
15 files changed
+321
-34
No files matched your search
+3
-3
@@ -3,9 +3,9 @@
|
||||
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.9.0';
|
||||
const APP_VERSION = '1.10.0';
|
||||
window.LifeOSVersion = APP_VERSION;
|
||||
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system', 'browser'];
|
||||
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system', 'browser', 'terminal'];
|
||||
const t = (key, values) => LifeI18n.t(key, values);
|
||||
|
||||
function readDraft() {
|
||||
@@ -171,7 +171,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-9]$/.test(e.key)) { e.preventDefault(); openApp(appIds[Number(e.key) - 1]); }
|
||||
if (e.altKey && /^[0-9]$/.test(e.key)) { e.preventDefault(); openApp(appIds[e.key === '0' ? 9 : 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(); }
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,50 @@
|
||||
/* Shared record writes for forms and the Life OS terminal. */
|
||||
(() => {
|
||||
const board = () => {
|
||||
const current = sys().tasksBoard;
|
||||
return current && Array.isArray(current.goals) && Array.isArray(current.tasks) ? current : { goals: [], tasks: [] };
|
||||
};
|
||||
const validDate = value => !value || (/^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(`${value}T12:00:00`)) && new Date(`${value}T12:00:00`).getFullYear() === Number(value.slice(0, 4)) && new Date(`${value}T12:00:00`).getMonth() + 1 === Number(value.slice(5, 7)) && new Date(`${value}T12:00:00`).getDate() === Number(value.slice(8)));
|
||||
const required = (value, max) => {
|
||||
const result = String(value || '').trim();
|
||||
if (!result || result.length > max) throw new Error('invalidTitle');
|
||||
return result;
|
||||
};
|
||||
const persistBoard = data => { const state = sys(); state.tasksBoard = data; save('lifeos_system', state); renderAll(); };
|
||||
window.LifeOSActions = {
|
||||
board,
|
||||
createGoal({ title, description = '', targetDate = '', showOnDesktop = false }) {
|
||||
title = required(title, 100);
|
||||
if (!validDate(targetDate)) throw new Error('invalidDate');
|
||||
const goal = { id: `goal_${crypto.randomUUID()}`, title, description: String(description).trim().slice(0, 240), targetDate, showOnDesktop: Boolean(targetDate && showOnDesktop), done: false, createdAt: new Date().toISOString() };
|
||||
const data = board(); data.goals.unshift(goal); persistBoard(data); return goal;
|
||||
},
|
||||
createTask({ title, dueDate = '', goalId = '', priority = 'normal' }) {
|
||||
title = required(title, 140);
|
||||
if (!validDate(dueDate)) throw new Error('invalidDate');
|
||||
if (!['normal', 'high', 'low'].includes(priority)) throw new Error('invalidPriority');
|
||||
const data = board();
|
||||
if (goalId && !data.goals.some(goal => goal.id === goalId)) throw new Error('notFound');
|
||||
const task = { id: `task_${crypto.randomUUID()}`, title, dueDate, goalId, priority, done: false, createdAt: new Date().toISOString() };
|
||||
data.tasks.push(task); persistBoard(data); return task;
|
||||
},
|
||||
setTaskDone(id, done) {
|
||||
const data = board(), task = data.tasks.find(item => item.id === id);
|
||||
if (!task) throw new Error('notFound');
|
||||
task.done = done; task.completedAt = done ? new Date().toISOString() : null;
|
||||
persistBoard(data); return task;
|
||||
},
|
||||
createMemory({ date = day(), title, text = '', mood = 'cloudy', energy = 3, tags = [], location = '', taskId = '', replace = false }) {
|
||||
title = String(title || '').trim();
|
||||
if (title.length > 120) throw new Error('invalidTitle');
|
||||
if (!validDate(date) || !date) throw new Error('invalidDate');
|
||||
if (!['clear', 'light', 'cloudy', 'rain', 'storm'].includes(mood) || !Number.isInteger(Number(energy)) || Number(energy) < 1 || Number(energy) > 5) throw new Error('invalidValue');
|
||||
if (taskId && !board().tasks.some(task => task.id === taskId)) throw new Error('notFound');
|
||||
const all = mems(), old = all.find(item => item.date === date);
|
||||
if (old && !replace) throw new Error('memoryExists');
|
||||
const film = filmFor(mood, Number(energy)), now = new Date().toISOString();
|
||||
const memory = { ...old, id: old?.id || `memory_${crypto.randomUUID()}`, date, title, text: String(text).trim(), mood, energy: Number(energy), tags: [...new Set(tags.map(tag => String(tag).trim()).filter(Boolean))].slice(0, 12), location: String(location).trim().slice(0, 100), taskId, filmTitle: film.filmTitle, filmSubtitle: film.filmSubtitle, filmLine: film.filmLine, createdAt: old?.createdAt || now, updatedAt: now };
|
||||
save('lifeos_memories', [...all.filter(item => item.date !== date), memory].sort((a, b) => b.date.localeCompare(a.date))); renderAll(); return memory;
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -29,10 +29,10 @@
|
||||
container.innerHTML = `<div class="eyebrow">${weekday(entryDate).toUpperCase()}</div><h1>${uiDate(entryDate)}</h1><p class="sub">${t('today.prompt')} ${entryDate !== day() ? `<button class="text-link" onclick="todayTargetDate=null;todayDraft=null;todayTitleDraft=null;renderAll()">${t('today.backToToday')}</button>` : ''}</p><div class="today-grid"><div><textarea id="entry" class="entry" placeholder="${t('today.placeholder')}" oninput="todayDraft=this.value">${esc(text)}</textarea><label class="memory-title-field">${t('memory.title')}<input id="memory-title" maxlength="120" value="${escAttr(todayTitleDraft === null ? (memory?.title || '') : todayTitleDraft)}" oninput="todayTitleDraft=this.value" placeholder="${t('memory.titlePlaceholder')}"></label>${memoryMetaFields(memory)}<div class="actions"><button class="primary" onclick="archiveToday()">${t(entryDate === day() ? (memory ? 'today.update' : 'today.archive') : (memory ? 'today.updateDate' : 'today.archiveDate'))}</button><span id="saveStatus" class="status"></span></div></div><aside><div class="eyebrow">${t('today.weather')}</div><div class="choices">${['clear','light','cloudy','rain','storm'].map(item => `<button class="chip ${item === selectedMood ? 'selected' : ''}" onclick="setMood('${item}')">${weather(item)}</button>`).join('')}</div><div class="eyebrow" style="margin-top:22px">${t('today.energy')}</div><div class="energy">${[1,2,3,4,5].map(item => `<button class="${item === selectedEnergy ? 'selected' : ''}" onclick="setEnergy(${item})">${item}</button>`).join('')}</div><div class="film"><div class="eyebrow">${t('today.film')}</div><div class="film-title">${film.filmTitle}</div><div class="sub">${film.filmSubtitle}</div><p class="film-copy">“${film.filmLine}”</p></div></aside></div>`;
|
||||
};
|
||||
window.archiveToday = () => {
|
||||
const entryDate = todayTargetDate || day(), text = document.querySelector('#entry')?.value.trim() || '', old = mems().find(item => item.date === entryDate), film = filmFor(selectedMood, selectedEnergy), now = new Date().toISOString();
|
||||
const memory = { ...old, id: old?.id || `memory_${crypto.randomUUID()}`, date: entryDate, text, title: document.querySelector('#memory-title')?.value.trim() || '', mood: selectedMood, energy: selectedEnergy, tags: tagList(document.querySelector('#memory-tags')?.value), location: document.querySelector('#memory-place')?.value.trim() || '', taskId: document.querySelector('#memory-task')?.value || '', filmTitle: film.filmTitle, filmSubtitle: film.filmSubtitle, filmLine: film.filmLine, createdAt: old?.createdAt || now, updatedAt: now };
|
||||
const status = document.querySelector('#saveStatus'); status.textContent = t('today.archiving');
|
||||
setTimeout(() => { save('lifeos_memories', [...mems().filter(item => item.date !== entryDate), memory].sort((a, b) => b.date.localeCompare(a.date))); todayDraft = null; todayTitleDraft = null; status.textContent = t('today.archived'); renderAll(); }, 360);
|
||||
const entryDate = todayTargetDate || day(), status = document.querySelector('#saveStatus');
|
||||
const draft = { date: entryDate, text: document.querySelector('#entry')?.value.trim() || '', title: document.querySelector('#memory-title')?.value.trim() || '', mood: selectedMood, energy: selectedEnergy, tags: tagList(document.querySelector('#memory-tags')?.value), location: document.querySelector('#memory-place')?.value.trim() || '', taskId: document.querySelector('#memory-task')?.value || '', replace: true };
|
||||
status.textContent = t('today.archiving');
|
||||
setTimeout(() => { todayDraft = null; todayTitleDraft = null; window.LifeOSActions.createMemory(draft); }, 360);
|
||||
};
|
||||
window.timeline = container => {
|
||||
const all = mems(), tags = [...new Set(all.flatMap(item => item.tags || []))].sort((a,b) => a.localeCompare(b));
|
||||
@@ -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 = `<div class="eyebrow">${t('system.eyebrow')}</div><h1>${t('system.title')}</h1><div class="settings"><section><div class="eyebrow">${t('system.appearance')}</div><div class="choices">${['dark','light','auto'].map(item => `<button class="chip ${state.theme === item ? 'selected' : ''}" onclick="theme('${item}')">${t(`system.theme.${item}`)}</button>`).join('')}</div></section><section><div class="eyebrow">${t('system.language')}</div><p class="sub">${detected}</p><div class="choices"><button class="chip ${preference === 'auto' ? 'selected' : ''}" onclick="window.setLanguage('auto')">${t('system.auto')}</button><button class="chip ${preference === 'en' ? 'selected' : ''}" onclick="window.setLanguage('en')">${t('system.languageEnglish')}</button><button class="chip ${preference === 'zh' ? 'selected' : ''}" onclick="window.setLanguage('zh')">${t('system.languageChinese')}</button></div></section><section><div class="eyebrow">${t('system.data')}</div><div class="actions"><button class="primary" onclick="exportData()">${t('system.export')}</button><button class="secondary" onclick="document.querySelector('#importer').click()">${t('system.import')}</button><button class="danger" onclick="resetConfirm()">${t('system.reset')}</button><input id="importer" class="hidden" type="file" accept=".json" onchange="readImport(this)"></div></section><section><div class="eyebrow">${t('system.storage')}</div><p>${t('system.local')}<br>${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))})}</p></section><section><div class="eyebrow">${t('system.dataStorage')}</div><p>${t('system.stored')}<br>${t('system.backups')}</p><p>${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `<br><b style="color:var(--accent)">${t('system.recommended')}</b>` : ''}</p></section><section><div class="eyebrow">${t('system.version')}</div><p>${t('system.release',{version:window.LifeOSVersion||'1.9.0'})}</p></section></div>`;
|
||||
container.innerHTML = `<div class="eyebrow">${t('system.eyebrow')}</div><h1>${t('system.title')}</h1><div class="settings"><section><div class="eyebrow">${t('system.appearance')}</div><div class="choices">${['dark','light','auto'].map(item => `<button class="chip ${state.theme === item ? 'selected' : ''}" onclick="theme('${item}')">${t(`system.theme.${item}`)}</button>`).join('')}</div></section><section><div class="eyebrow">${t('system.language')}</div><p class="sub">${detected}</p><div class="choices"><button class="chip ${preference === 'auto' ? 'selected' : ''}" onclick="window.setLanguage('auto')">${t('system.auto')}</button><button class="chip ${preference === 'en' ? 'selected' : ''}" onclick="window.setLanguage('en')">${t('system.languageEnglish')}</button><button class="chip ${preference === 'zh' ? 'selected' : ''}" onclick="window.setLanguage('zh')">${t('system.languageChinese')}</button></div></section><section><div class="eyebrow">${t('system.data')}</div><div class="actions"><button class="primary" onclick="exportData()">${t('system.export')}</button><button class="secondary" onclick="document.querySelector('#importer').click()">${t('system.import')}</button><button class="danger" onclick="resetConfirm()">${t('system.reset')}</button><input id="importer" class="hidden" type="file" accept=".json" onchange="readImport(this)"></div></section><section><div class="eyebrow">${t('system.storage')}</div><p>${t('system.local')}<br>${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))})}</p></section><section><div class="eyebrow">${t('system.dataStorage')}</div><p>${t('system.stored')}<br>${t('system.backups')}</p><p>${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `<br><b style="color:var(--accent)">${t('system.recommended')}</b>` : ''}</p></section><section><div class="eyebrow">${t('system.version')}</div><p>${t('system.release',{version:window.LifeOSVersion||'1.10.0'})}</p></section></div>`;
|
||||
};
|
||||
window.importPreview = () => { const data = importData, board = data.system?.tasksBoard || {}; rootModal().innerHTML = `<div class="modal"><div class="modalbox"><div class="eyebrow">${t('dialog.backup')}</div><h2>${t('dialog.ready')}</h2><p class="sub">${t('dialog.exported',{date:short(data.exportedAt.slice(0,10))})}<br>${t('dialog.memories',{count:data.memories.length})}<br>${t('dialog.museumItems',{count:data.museum.length})}<br>${t('dialog.goals',{count:board.goals?.length || 0})}<br>${t('dialog.tasks',{count:board.tasks?.length || 0})}</p><div class="actions"><button class="primary" onclick="applyImport('merge')">${t('dialog.merge')}</button><button class="danger" onclick="replaceConfirm()">${t('dialog.replaceAll')}</button><button class="secondary" onclick="modal.innerHTML=''">${t('common.cancel')}</button></div></div></div>`; };
|
||||
window.replaceConfirm = () => { rootModal().innerHTML = `<div class="modal"><div class="modalbox"><div class="eyebrow">${t('dialog.final')}</div><h2>${t('dialog.replaceQuestion')}</h2><p class="sub">${t('dialog.replaceWarning')}</p><div class="actions"><button class="danger" onclick="applyImport('replace')">${t('dialog.replace')}</button><button class="secondary" onclick="importPreview()">${t('common.cancel')}</button></div></div></div>`; };
|
||||
|
||||
Loaded 3 of 15 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user