`;
// Because script tags are stripped above, we add it in a tolerant way:
html += injection;
return html;
}
function safeHead() {
return ``;
}
function absolutizeURL(u, baseURL) {
try {
// data:, mailto:, javascript: remain untouched
if (/^(data:|mailto:|tel:|javascript:)/i.test(u)) return u;
const url = new URL(u, baseURL);
return url.toString();
} catch(e){ return u; }
}
function proxyURL(target) {
if (!state.proxyBase) return target; // fallback: direct (may fail due to CORS/CSP)
// If proxy expects path-append style: proxyBase + target
return state.proxyBase.replace(/\/+$/,'/') + target;
}
async function fetchThroughProxy(target) {
const prox = proxyURL(target);
const res = await fetch(prox, { mode: 'cors' });
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
const ct = res.headers.get('content-type') || '';
if (/text\/html|application\/xhtml\+xml/i.test(ct)) {
return await res.text();
} else if (/text\/plain/i.test(ct)) {
return await res.text();
} else {
// If non-HTML (e.g., image), show a minimal viewer
const blob = await res.blob();
const url = URL.createObjectURL(blob);
return `Resource
`;
}
}
back.addEventListener('click', ()=>{
const s = sessions.find(x=>x.id===activeId); if (!s) return;
if (s.index>0){ s.index--; navigateTo(s.history[s.index]); }
});
fwd.addEventListener('click', ()=>{
const s = sessions.find(x=>x.id===activeId); if (!s) return;
if (s.index < s.history.length-1){ s.index++; navigateTo(s.history[s.index]); }
});
reload.addEventListener('click', ()=>{
const s = sessions.find(x=>x.id===activeId); if (!s) return;
if (s.history[s.index]) navigateTo(s.history[s.index]);
});
go.addEventListener('click', ()=> navigateTo(url.value.trim()));
url.addEventListener('keydown', (e)=>{ if (e.key==='Enter') navigateTo(url.value.trim()); });
searchSel.addEventListener('change', ()=> state.searchEngine = searchSel.value);
window.addEventListener('message', (ev)=>{
if (ev.data && ev.data.type==='webos-open') addTab(ev.data.url);
});
// Start with one tab
addTab('https://example.org');
}, { width: 980, height: 640 });
}
/* ---------------------- Settings app ---------------------- */
function openSettings() {
createWindow('Settings', (content, win)=>{
const pane = document.createElement('div'); pane.className='pane';
const net = document.createElement('div'); net.className='group';
net.innerHTML = `Network
例: https://your-proxy/https://example.org のようにターゲットURLを末尾に連結する形式を想定しています。
`;
const personalize = document.createElement('div'); personalize.className='group';
personalize.innerHTML = `Personalization
`;
const search = document.createElement('div'); search.className='group';
search.innerHTML = `Search
`;
pane.append(net, personalize, search);
content.append(pane);
const proxyBase = pane.querySelector('#proxyBase');
const saveProxy = pane.querySelector('#saveProxy');
proxyBase.value = state.proxyBase;
saveProxy.addEventListener('click', ()=>{
state.proxyBase = proxyBase.value.trim();
localStorage.setItem('webos_proxy', state.proxyBase);
toast('プロキシ設定を保存しました');
});
const themeSel = pane.querySelector('#themeSel'); themeSel.value = document.body.getAttribute('data-theme') || 'dark';
pane.querySelector('#applyTheme').addEventListener('click', ()=>{
document.body.setAttribute('data-theme', themeSel.value);
localStorage.setItem('webos_theme', themeSel.value);
toast('テーマを適用しました');
});
const wallSel = pane.querySelector('#wallSel');
pane.querySelector('#applyWall').addEventListener('click', ()=>{
wallpaperEl.className = 'wallpaper ' + (wallSel.value==='default' ? '' : wallSel.value);
localStorage.setItem('webos_wall', wallSel.value);
toast('壁紙を適用しました');
});
const searchSel = pane.querySelector('#searchEngine'); searchSel.value = state.searchEngine;
pane.querySelector('#applySearch').addEventListener('click', ()=>{
state.searchEngine = searchSel.value;
localStorage.setItem('webos_search', state.searchEngine);
toast('検索エンジンを適用しました');
});
}, { width: 640, height: 420 });
}
/* ---------------------- Notes app ---------------------- */
function openNotes() {
createWindow('Notes', (content)=>{
const ta = document.createElement('textarea');
ta.className='note'; ta.placeholder='ここにメモ…';
ta.value = localStorage.getItem('webos_notes') || '';
ta.addEventListener('input', ()=> localStorage.setItem('webos_notes', ta.value));
content.append(ta);
}, { width: 600, height: 480 });
}
/* ---------------------- Files app (local storage viewer) ---------------------- */
function openFiles() {
createWindow('Files', (content)=>{
const pane = document.createElement('div'); pane.className='pane';
const grp = document.createElement('div'); grp.className='group';
grp.innerHTML = `Local storage
`;
const list = document.createElement('div'); list.style.maxHeight='60vh'; list.style.overflow='auto';
for (let i=0; i{ localStorage.removeItem(key); row.remove(); });
row.append(k, v, del);
list.append(row);
}
grp.append(list);
pane.append(grp);
content.append(pane);
}, { width: 720, height: 520 });
}
/* ---------------------- About app ---------------------- */
function openAbout() {
createWindow('About', (content)=>{
const pane = document.createElement('div'); pane.className='pane';
const grp = document.createElement('div'); grp.className='group';
grp.innerHTML = `webOS について
これはHTML/CSS/JavaScriptだけで再現したWindows 11風のWebデスクトップです。CORSプロキシ経由での表示に対応し、タブブラウズ・履歴・検索切替・テーマ・壁紙を備えています。
完全互換のブラウザではありません。サイトのCSPやX-Frame-Optionsなどにより、表示できないページや挙動差があります。
`;
pane.append(grp);
content.append(pane);
}, { width: 560, height: 360 });
}
/* ---------------------- Startup preferences ---------------------- */
(function bootstrap(){
const savedProxy = localStorage.getItem('webos_proxy'); if (savedProxy) state.proxyBase = savedProxy;
const savedTheme = localStorage.getItem('webos_theme'); if (savedTheme) document.body.setAttribute('data-theme', savedTheme);
const savedWall = localStorage.getItem('webos_wall'); if (savedWall) wallpaperEl.className = 'wallpaper ' + (savedWall==='default'?'':savedWall);
const savedSearch = localStorage.getItem('webos_search'); if (savedSearch) state.searchEngine = savedSearch;
})();
/* ---------------------- Start search quick launcher ---------------------- */
document.getElementById('startSearch').addEventListener('keydown', (e)=>{
if (e.key==='Enter') {
const q = e.target.value.trim(); if (!q) return;
openBrowser();
// Delay to allow window creation
setTimeout(()=>{
window.postMessage({ type:'webos-open', url: q }, '*');
}, 50);
startMenu.classList.remove('show');
}
});