feat(browser): add Google iframe browser app
This commit is contained in:
1 parent
27292095a3
commit
93d0487617
13 files changed
+141
-23
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.8.0';
|
||||
const APP_VERSION = '1.9.0';
|
||||
window.LifeOSVersion = APP_VERSION;
|
||||
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system'];
|
||||
const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system', 'browser'];
|
||||
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-8]$/.test(e.key)) { e.preventDefault(); openApp(appIds[Number(e.key) - 1]); }
|
||||
if (e.altKey && /^[1-9]$/.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(); }
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,65 @@
|
||||
/* Small iframe browser. Cross-origin pages decide whether they allow embedding. */
|
||||
(() => {
|
||||
const HOME = 'https://www.google.com/webhp?igu=1';
|
||||
const pages = [HOME];
|
||||
let position = 0;
|
||||
const t = key => LifeI18n.t(`browser.${key}`);
|
||||
const container = () => document.querySelector('#content-browser');
|
||||
const current = () => pages[position];
|
||||
|
||||
function address(value) {
|
||||
const input = String(value || '').trim();
|
||||
if (!input) return HOME;
|
||||
if (/^[a-z][a-z\d+.-]*:/i.test(input) && !/^https?:\/\//i.test(input)) return null;
|
||||
let url;
|
||||
try {
|
||||
if (/^https?:\/\//i.test(input)) url = new URL(input);
|
||||
else if (/^[^\s/]+\.[^\s]+/.test(input)) url = new URL(`https://${input}`);
|
||||
else return `https://www.google.com/search?igu=1&q=${encodeURIComponent(input)}`;
|
||||
} catch { return null; }
|
||||
if (!['http:', 'https:'].includes(url.protocol) || !url.hostname) return null;
|
||||
if (url.protocol === 'http:') url.protocol = 'https:';
|
||||
if (['google.com', 'www.google.com'].includes(url.hostname.toLowerCase())) {
|
||||
if (url.pathname === '/') url.pathname = '/webhp';
|
||||
if (['/webhp', '/search'].includes(url.pathname)) url.searchParams.set('igu', '1');
|
||||
}
|
||||
return url.href;
|
||||
}
|
||||
|
||||
function updateChrome(node, navigate = false) {
|
||||
const frame = node.querySelector('iframe'), input = node.querySelector('.browser-address');
|
||||
if (!frame) return;
|
||||
if (navigate || !frame.getAttribute('src')) frame.src = current();
|
||||
if (input && document.activeElement !== input) input.value = current();
|
||||
node.querySelector('[data-action="back"]').disabled = position === 0;
|
||||
node.querySelector('[data-action="forward"]').disabled = position === pages.length - 1;
|
||||
node.querySelector('.browser-external').href = current();
|
||||
node.querySelector('.browser-hint-link').href = current();
|
||||
}
|
||||
|
||||
function go(value) {
|
||||
const url = address(value);
|
||||
if (!url) { toast(t('invalid')); return; }
|
||||
if (url !== current()) { pages.splice(position + 1); pages.push(url); position = pages.length - 1; }
|
||||
const node = container(); if (node) updateChrome(node, true);
|
||||
}
|
||||
|
||||
window.browserApp = node => {
|
||||
if (node.querySelector('.browser-shell') && node.dataset.browserLocale === lang()) { updateChrome(node); return; }
|
||||
node.dataset.browserLocale = lang();
|
||||
node.innerHTML = `<div class="browser-shell"><form class="browser-toolbar"><button type="button" data-action="back" aria-label="${t('back')}" title="${t('back')}">←</button><button type="button" data-action="forward" aria-label="${t('forward')}" title="${t('forward')}">→</button><button type="button" data-action="reload" aria-label="${t('reload')}" title="${t('reload')}">↻</button><label class="sr-only" for="browser-address">${t('address')}</label><input id="browser-address" class="browser-address" type="text" inputmode="url" autocomplete="off" spellcheck="false" aria-label="${t('address')}" placeholder="${t('placeholder')}"><button type="submit" class="browser-go">${t('go')}</button><a class="browser-external" target="_blank" rel="noopener noreferrer" aria-label="${t('external')}" title="${t('external')}">↗</a></form><div class="browser-frame-wrap"><iframe title="${t('frame')}" referrerpolicy="no-referrer" sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox"></iframe></div><p class="browser-hint">${t('embedHint')} <a class="browser-hint-link" target="_blank" rel="noopener noreferrer">${t('external')}</a></p></div>`;
|
||||
const toolbar = node.querySelector('.browser-toolbar');
|
||||
toolbar.addEventListener('submit', event => { event.preventDefault(); go(node.querySelector('.browser-address').value); });
|
||||
toolbar.addEventListener('click', event => {
|
||||
const action = event.target.closest('[data-action]')?.dataset.action;
|
||||
if (action === 'back' && position > 0) { position--; updateChrome(node, true); }
|
||||
if (action === 'forward' && position < pages.length - 1) { position++; updateChrome(node, true); }
|
||||
if (action === 'reload') updateChrome(node, true);
|
||||
});
|
||||
updateChrome(node, true);
|
||||
};
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `#content-browser{padding:0;overflow:hidden}.browser-shell{height:100%;min-height:0;display:flex;flex-direction:column}.browser-toolbar{display:flex;align-items:center;gap:6px;padding:10px;background:var(--panel);border-bottom:1px solid var(--line)}.browser-toolbar button,.browser-external{flex:none;display:grid;place-items:center;min-width:34px;height:34px;border:1px solid var(--line);border-radius:8px;background:var(--panel2);color:var(--ink);text-decoration:none;font-size:18px}.browser-toolbar button:disabled{opacity:.38;cursor:default}.browser-toolbar .browser-go{font-size:12px;padding:0 11px;background:var(--accent);color:var(--bg);border-color:var(--accent)}.browser-address{flex:1;min-width:0;height:36px;border:1px solid var(--line);border-radius:8px;background:var(--panel2);color:var(--ink);padding:0 11px;font:13px Manrope,system-ui;outline:none}.browser-address:focus{border-color:var(--accent)}.browser-frame-wrap{flex:1;min-height:0;background:#fff}.browser-frame-wrap iframe{display:block;width:100%;height:100%;border:0}.browser-hint{margin:0;padding:7px 12px;border-top:1px solid var(--line);color:var(--muted);font-size:12px;line-height:1.4}.browser-hint a{color:var(--accent)}@media(max-width:650px){.browser-toolbar{gap:4px;padding:8px}.browser-toolbar button,.browser-external{min-width:30px;height:32px}.browser-address{font-size:12px}.browser-hint{padding:7px 9px;font-size:11px}}`;
|
||||
document.head.append(style);
|
||||
})();
|
||||
@@ -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.8.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.9.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 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