141 lines
13 KiB
JavaScript
141 lines
13 KiB
JavaScript
/* Standalone/embedded host, not an alternative editor. All snapshots are Go
|
|
* encoded .aeart files; no pixels are sent to an HTTP endpoint. */
|
|
'use strict';
|
|
(async function () {
|
|
const cfg=window.AegoStudioRelease,P=window.AegoStudioProtocol,H=window.AegoStudioHost;
|
|
const url=new URL(location.href),base=new URL(cfg.baseURL),relative=url.pathname.startsWith(base.pathname)?url.pathname.slice(base.pathname.length):url.pathname.replace(/^\//,'');
|
|
const embedded=relative.startsWith('embed/'),transferID=url.searchParams.get('transfer');
|
|
const locale=(url.searchParams.get('lang')||document.documentElement.lang)==='en'?'en':'ru';
|
|
const allLabels={ru:{full:'Открыть полный редактор',reset:'Сбросить пример',home:'Главная',fallback:'Скачать для переноса',sending:'Передача документа…',received:'Документ получен. Сохраните .aeart для постоянной копии.',sent:'Документ передан. Исходная вкладка и её Undo сохранены.',failed:'Передача не выполнена. Скачайте .aeart и откройте его в полной версии.',blocked:'Браузер заблокировал новую вкладку.',storage:'Локальное восстановление недоступно. Скачивайте .aeart перед закрытием.',limit:'GPU Preview использует отдельную сцену Aego; объекты открытого игрового проекта в браузере недоступны.',open:'Открыть выбранный файл',timeout:'Редактор не готов.'},en:{full:'Open Full Editor',reset:'Reset Example',home:'Home',fallback:'Download for transfer',sending:'Transferring document…',received:'Document received. Save .aeart for a permanent copy.',sent:'Document transferred. Source tab and its Undo are preserved.',failed:'Transfer failed. Download .aeart and open it in the full editor.',blocked:'The browser blocked the new tab.',storage:'Local recovery is unavailable. Download .aeart before closing.',limit:'GPU Preview uses an isolated Aego world; the browser has no current game-project object.',open:'Open a local file',timeout:'Editor is not ready.'}};
|
|
Object.assign(allLabels.ru,{incompatible:'Несовместимая версия редактора.',rejected:'Получатель отклонил снимок.',transferTimeout:'Время передачи истекло.',hashFailed:'Не удалось вычислить хеш.',noSource:'Не найдено подходящее исходное окно.',sourceTimeout:'Время ожидания исходного окна истекло.',snapshotRejected:'Снимок отклонён.',embedRejected:'Источник или сеанс встраивания отклонён. Откройте полный редактор.',busy:'Операция уже выполняется.',noExample:'Пример не выбран.'});
|
|
Object.assign(allLabels.en,{incompatible:'Incompatible target version.',rejected:'Receiver rejected the snapshot.',transferTimeout:'Transfer timed out.',hashFailed:'Hash calculation failed.',noSource:'No valid source window.',sourceTimeout:'Source timeout.',snapshotRejected:'Snapshot rejected.',embedRejected:'Embedding origin/session rejected. Open the full editor directly.',busy:'An operation is already in progress.',noExample:'No example is selected.'});
|
|
let labels=allLabels[locale];
|
|
const bar=document.createElement('div');bar.className='release-toolbar';
|
|
function button(text,fn,id){const b=document.createElement('button');b.type='button';b.textContent=text;if(id)b.id=id;b.onclick=fn;bar.append(b);return b}
|
|
const home=document.createElement('a');home.href=base.href;home.textContent=labels.home;bar.append(home);
|
|
let activeExample=null,pendingTransfer=null,fallbackSnapshot=null;
|
|
const full=button(labels.full,()=>startTransfer(),'studio-open-full');
|
|
const downloadButton=button(locale==='ru'?'Скачать .aeart':'Download .aeart',()=>{try{H.downloadSource()}catch(e){H.notice(e.message,true)}},'studio-download-source');
|
|
full.hidden=!embedded;
|
|
const reset=button(labels.reset,()=>{try{if(activeExample)H.resetExample(activeExample)}catch(e){H.notice(e.message,true)}},'studio-reset-example');reset.hidden=true;
|
|
const fallback=button(labels.fallback,()=>{try{H.downloadSource(fallbackSnapshot)}catch(e){H.notice(e.message,true)}},'studio-transfer-download');fallback.hidden=true;
|
|
const progress=document.createElement('span');progress.className='transfer-status';progress.setAttribute('role','status');bar.append(progress);
|
|
const badge=document.createElement('span');badge.className='release-version';badge.textContent=cfg.buildID+(cfg.snapshot?' · SNAPSHOT':'');bar.append(badge);
|
|
document.querySelector('.menubar').after(bar);document.body.classList.toggle('studio-embedded',embedded);
|
|
document.querySelector('.build-badge').textContent=cfg.buildID+(cfg.snapshot?' · snapshot':'');
|
|
function showStorageWarning(){
|
|
let warning=bar.querySelector('.storage-warning');
|
|
if(!warning){warning=document.createElement('div');warning.className='storage-warning';bar.append(warning)}
|
|
warning.textContent=labels.storage;
|
|
}
|
|
window.addEventListener('aego-art-recovery-error',showStorageWarning);
|
|
new MutationObserver(()=>{
|
|
const code=document.documentElement.lang==='en'?'en':'ru';labels=allLabels[code];
|
|
home.textContent=labels.home;full.textContent=labels.full;reset.textContent=labels.reset;fallback.textContent=labels.fallback;
|
|
downloadButton.textContent=code==='ru'?'Скачать .aeart':'Download .aeart';
|
|
if(bar.querySelector('.storage-warning'))showStorageWarning();
|
|
}).observe(document.documentElement,{attributes:true,attributeFilter:['lang']});
|
|
function failure(message,snapshot){fallbackSnapshot=snapshot;fallback.hidden=!snapshot;progress.textContent=message+' '+labels.failed;H.notice(progress.textContent,true)}
|
|
function startTransfer(){
|
|
if(pendingTransfer)return;
|
|
let snap;try{snap=H.capture()}catch(e){H.notice(e.message,true);return}
|
|
const session=P.token(),targetURL=new URL('editor/',base);targetURL.searchParams.set('transfer',session);targetURL.searchParams.set('lang',document.documentElement.lang);
|
|
const popup=window.open(targetURL.href,'_blank');
|
|
if(!popup){failure(labels.blocked,snap);return}
|
|
progress.textContent=labels.sending;full.disabled=true;
|
|
const pending={popup,session,snapshot:snap,sha:null,ready:false,sent:false};pendingTransfer=pending;
|
|
function cleanup(){clearTimeout(pending.timer);window.removeEventListener('message',pending.listener);pendingTransfer=null;full.disabled=false}
|
|
function transmit(){if(!pending.ready||!pending.sha||pending.sent)return;pending.sent=true;P.send(popup,location.origin,session,'transfer-document','document',{
|
|
operation:'import',format:'aeart',documentVersion:cfg.documentVersion,buildID:cfg.buildID,revision:snap.revision,selection:snap.selection,
|
|
bytes:snap.bytes.buffer,size:snap.bytes.length,sha256:pending.sha
|
|
})}
|
|
pending.listener=event=>{
|
|
if(!P.accepted(event,popup,location.origin,session))return;
|
|
if(event.data.type==='transfer-ready'){
|
|
if(event.data.payload.buildID!==cfg.buildID||event.data.payload.documentVersion!==cfg.documentVersion){cleanup();failure(labels.incompatible,snap);return}
|
|
pending.ready=true;transmit();
|
|
}else if(event.data.type==='transfer-ack' && event.data.payload.sha256===pending.sha){cleanup();fallback.hidden=true;progress.textContent=labels.sent}
|
|
else if(event.data.type==='error'){cleanup();failure(labels.rejected,snap)}
|
|
};
|
|
window.addEventListener('message',pending.listener);
|
|
pending.timer=setTimeout(()=>{cleanup();failure(labels.transferTimeout,snap)},30000);
|
|
P.sha256(snap.bytes).then(hash=>{if(pendingTransfer!==pending)return;pending.sha=hash;transmit()}).catch(()=>{cleanup();failure(labels.hashFailed,snap)});
|
|
}
|
|
try{await H.ready}catch(e){progress.textContent=labels.timeout;return}
|
|
H.language(locale);
|
|
if(!H.storageAvailable()){
|
|
showStorageWarning();
|
|
}
|
|
if(transferID){
|
|
// Only the same-origin opener that created this window can supply a snapshot.
|
|
if(!/^[a-f0-9]{32}$/.test(transferID)||!window.opener){failure(labels.noSource,null);return}
|
|
const opener=window.opener,origin=location.origin;let imported=false,inFlight=false;
|
|
const hello=()=>P.send(opener,origin,transferID,'transfer-ready','ready',{buildID:cfg.buildID,documentVersion:cfg.documentVersion});
|
|
const timer=setInterval(hello,250),timeout=setTimeout(()=>{clearInterval(timer);window.removeEventListener('message',receive);if(!imported)failure(labels.sourceTimeout,null)},30000);
|
|
async function receive(event){
|
|
if(!P.accepted(event,opener,origin,transferID)||event.data.type!=='transfer-document'||inFlight)return;
|
|
const data=event.data.payload;
|
|
try{
|
|
if(imported) return;
|
|
if(!P.snapshotShape(data,cfg.buildID,cfg.documentVersion))throw new Error('Invalid snapshot envelope');
|
|
inFlight=true;
|
|
if(await P.sha256(data.bytes)!==data.sha256)throw new Error('Snapshot SHA-256 mismatch');
|
|
H.importSnapshot(new Uint8Array(data.bytes),{unsaved:true,selection:data.selection});imported=true;
|
|
P.send(opener,origin,transferID,'transfer-ack',event.data.request,{sha256:data.sha256});
|
|
clearInterval(timer);clearTimeout(timeout);window.removeEventListener('message',receive);progress.textContent=labels.received;
|
|
url.searchParams.delete('transfer');history.replaceState(null,'',url.pathname+url.search);try{window.opener=null}catch{}
|
|
}catch(e){inFlight=false;P.send(opener,origin,transferID,'error',event.data.request,{code:'snapshot-rejected'});failure(labels.snapshotRejected,null)}
|
|
}
|
|
window.addEventListener('message',receive);hello();
|
|
} else if(relative.startsWith('examples/')) {
|
|
const id=relative.split('/')[1];try{activeExample=await H.loadExample(id);reset.hidden=false}catch(e){H.notice(e.message,true)}
|
|
} else if(url.searchParams.has('new')) {
|
|
H.newDocument();H.setNewSize(32,32);
|
|
if(url.searchParams.get('new')==='effect') {
|
|
try { H.setNewTemplate(url.searchParams.get('template') || 'explosion'); }
|
|
catch(e) { H.notice(e.message,true); }
|
|
}
|
|
// This query is a one-shot launch action, not persistent editor state.
|
|
// Otherwise a reload opens a modal over IndexedDB recovery for a dirty tab.
|
|
url.searchParams.delete('new');url.searchParams.delete('template');
|
|
history.replaceState(null,'',url.pathname+url.search+url.hash);
|
|
} else if(url.searchParams.has('open')) {
|
|
// Native pickers require user activation: present a real button, don't fake a click from an async load.
|
|
button(labels.open,()=>H.openFile(),'studio-open-local');
|
|
}
|
|
if(embedded){
|
|
const parentOrigin=url.searchParams.get('parentOrigin'),session=url.searchParams.get('session');
|
|
if(window.parent===window||!cfg.docsOrigins.includes(parentOrigin)||!/^[a-f0-9]{32}$/.test(session||'')){
|
|
H.notice(labels.embedRejected,true);return;
|
|
}
|
|
let ready=false,loading=false;const answered=new Map();
|
|
function reply(type,request,payload){P.send(window.parent,parentOrigin,session,type,request,payload)}
|
|
window.addEventListener('message',async event=>{
|
|
if(!P.accepted(event,window.parent,parentOrigin,session))return;
|
|
const {type,request,payload}=event.data;
|
|
if(type==='hello'){
|
|
if(payload.buildID!==cfg.buildID){reply('error',request,{code:'build-mismatch'});return}
|
|
ready=true;reply('ready',request,{...H.capabilities,buildID:cfg.buildID});return;
|
|
}
|
|
if(!ready)return;
|
|
if(answered.has(request)){reply('reply',request,answered.get(request));return}
|
|
try{
|
|
let result={};
|
|
if(type==='summary')result=H.summary();
|
|
else if(type==='load-example'){
|
|
if(loading)throw new Error(labels.busy);loading=true;
|
|
try{activeExample=await H.loadExample(payload.id);reset.hidden=false;result=H.summary()}finally{loading=false}
|
|
}else if(type==='reset-example'){if(!activeExample)throw new Error(labels.noExample);result=H.resetExample(activeExample)||{cancelled:true}}
|
|
else if(type==='download'){H.downloadSource();result={initiated:true,saved:false}}
|
|
else if(type==='open-full'){full.focus();result={requiresUserGesture:true};progress.textContent=labels.full}
|
|
else return;
|
|
if(answered.size>=64)answered.delete(answered.keys().next().value);answered.set(request,result);reply('reply',request,result);
|
|
}catch(e){reply('error',request,{code:'action-rejected'})}
|
|
});
|
|
let lastHeight=0;new ResizeObserver(()=>{
|
|
const height=Math.max(480,Math.min(1000,document.documentElement.clientHeight));if(!ready||height===lastHeight)return;lastHeight=height;reply('resize','viewport',{height});
|
|
}).observe(document.body);
|
|
}
|
|
window.artStudioSurfaceReady=true;
|
|
})();
|