7254f7b4d1
Build / Build (macos-latest) (push) Has been cancelled
Build / Build (ubuntu-latest) (push) Has been cancelled
Build / Build (windows-latest) (push) Has been cancelled
Build / Analyze (javascript) (push) Has been cancelled
Build / Analyze (python) (push) Has been cancelled
917 lines
34 KiB
JavaScript
917 lines
34 KiB
JavaScript
|
|
import * as base from './base.js';
|
|
|
|
const browser = {};
|
|
|
|
browser.Host = class {
|
|
|
|
constructor() {
|
|
this._window = window;
|
|
this._navigator = window.navigator;
|
|
this._document = window.document;
|
|
this._telemetry = new base.Telemetry(this._window);
|
|
this._window.eval = () => {
|
|
throw new Error('window.eval() not supported.');
|
|
};
|
|
this._meta = {};
|
|
for (const element of Array.from(this._document.getElementsByTagName('meta'))) {
|
|
if (element.name !== undefined && element.name !== '' && element.content !== undefined) {
|
|
this._meta[element.name] = this._meta[element.name] || [];
|
|
this._meta[element.name].push(element.content);
|
|
}
|
|
}
|
|
this._environment = {
|
|
name: this._document.title,
|
|
type: this._meta.type ? this._meta.type[0] : 'Browser',
|
|
version: this._meta.version ? this._meta.version[0] : null,
|
|
date: Array.isArray(this._meta.date) && this._meta.date.length > 0 && this._meta.date[0] ? new Date(`${this._meta.date[0].split(' ').join('T')}Z`) : new Date(),
|
|
packaged: this._meta.version && this._meta.version[0] !== '0.0.0',
|
|
platform: /(Mac|iPhone|iPod|iPad)/i.test(this._navigator.platform) ? 'darwin' : undefined,
|
|
agent: this._navigator.userAgent.toLowerCase().indexOf('safari') !== -1 && this._navigator.userAgent.toLowerCase().indexOf('chrome') === -1 ? 'safari' : '',
|
|
repository: this._element('logo-github').getAttribute('href'),
|
|
menu: true
|
|
};
|
|
if (this.version && !/^\d+\.\d+\.\d+$/.test(this.version)) {
|
|
throw new Error('Invalid version.');
|
|
}
|
|
}
|
|
|
|
get window() {
|
|
return this._window;
|
|
}
|
|
|
|
get document() {
|
|
return this._document;
|
|
}
|
|
|
|
get version() {
|
|
return this._environment.version;
|
|
}
|
|
|
|
get type() {
|
|
return this._environment.type;
|
|
}
|
|
|
|
async view(view) {
|
|
const window = this.window;
|
|
const document = this.document;
|
|
this._view = view;
|
|
const age = async () => {
|
|
const days = (new Date() - new Date(this._environment.date)) / (24 * 60 * 60 * 1000);
|
|
if (days > 180) {
|
|
const link = this._element('logo-github').href;
|
|
document.body.classList.remove('spinner');
|
|
for (;;) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await this.message('Please update to the newest version.', null, 'Update');
|
|
this.openURL(link);
|
|
}
|
|
}
|
|
return Promise.resolve();
|
|
};
|
|
const consent = async () => {
|
|
if (this._getCookie('consent') || this._getCookie('_ga')) {
|
|
return;
|
|
}
|
|
let consent = true;
|
|
try {
|
|
const text = await this._fetch('https://ipinfo.io/json', { 'Content-Type': 'application/json' }, 'utf-8', null, 2000);
|
|
const json = JSON.parse(text);
|
|
const countries = ['AT', 'BE', 'BG', 'HR', 'CZ', 'CY', 'DK', 'EE', 'FI', 'FR', 'DE', 'EL', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'NO', 'PL', 'PT', 'SK', 'ES', 'SE', 'GB', 'UK', 'GR', 'EU', 'RO'];
|
|
if (json && json.country && countries.indexOf(json.country) === -1) {
|
|
consent = false;
|
|
}
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
if (consent) {
|
|
document.body.classList.remove('spinner');
|
|
await this.message('This app uses cookies to report errors and anonymous usage information.', null, 'Accept');
|
|
}
|
|
this._setCookie('consent', Date.now().toString(), 30);
|
|
};
|
|
const telemetry = async () => {
|
|
if (this._environment.packaged) {
|
|
window.addEventListener('error', (event) => {
|
|
if (event instanceof window.ErrorEvent && event.error && event.error instanceof Error) {
|
|
this.exception(event.error, true);
|
|
} else {
|
|
const message = event && event.message ? event.message : JSON.stringify(event);
|
|
const error = new Error(message);
|
|
this.exception(error, true);
|
|
}
|
|
});
|
|
const measurement_id = '848W2NVWVH';
|
|
const user = this._getCookie('_ga').replace(/^(GA1\.\d\.)*/, '');
|
|
const session = this._getCookie(`_ga${measurement_id}`);
|
|
await this._telemetry.start(`G-${measurement_id}`, user, session);
|
|
this._telemetry.set('page_location', document.location && document.location.href ? document.location.href : null);
|
|
this._telemetry.set('page_title', document.title ? document.title : null);
|
|
this._telemetry.set('page_referrer', document.referrer ? document.referrer : null);
|
|
this._telemetry.send('page_view', {
|
|
app_name: this.type,
|
|
app_version: this.version,
|
|
});
|
|
this._telemetry.send('scroll', {
|
|
percent_scrolled: 90,
|
|
app_name: this.type,
|
|
app_version: this.version
|
|
});
|
|
this._setCookie('_ga', `GA1.2.${this._telemetry.get('client_id')}`, 1200);
|
|
this._setCookie(`_ga${measurement_id}`, `GS1.1.${this._telemetry.session}`, 1200);
|
|
}
|
|
};
|
|
const capabilities = async () => {
|
|
const filter = (list) => {
|
|
return list.filter((capability) => {
|
|
const path = capability.split('.').reverse();
|
|
let obj = window[path.pop()];
|
|
while (obj && path.length > 0) {
|
|
obj = obj[path.pop()];
|
|
}
|
|
return obj;
|
|
});
|
|
};
|
|
const capabilities = filter(['fetch', 'DataView.prototype.getBigInt64', 'Worker', 'Array.prototype.flat']);
|
|
this.event('browser_open', {
|
|
browser_capabilities: capabilities.map((capability) => capability.split('.').pop()).join(',')
|
|
});
|
|
return Promise.resolve();
|
|
};
|
|
await age();
|
|
await consent();
|
|
await telemetry();
|
|
await capabilities();
|
|
}
|
|
|
|
async start() {
|
|
if (this._meta.file) {
|
|
const [url] = this._meta.file;
|
|
if (this._view.accept(url)) {
|
|
const identifier = Array.isArray(this._meta.identifier) && this._meta.identifier.length === 1 ? this._meta.identifier[0] : null;
|
|
const name = this._meta.name || null;
|
|
const status = await this._openModel(this._url(url), identifier || null, name);
|
|
if (status === '') {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
const window = this.window;
|
|
const document = this.document;
|
|
const search = window.location.search;
|
|
const params = new Map(search ? new window.URLSearchParams(window.location.search) : []);
|
|
const hash = window.location.hash ? window.location.hash.replace(/^#/, '') : '';
|
|
const url = hash ? hash : params.get('url');
|
|
if (url) {
|
|
const identifier = params.get('identifier') || null;
|
|
const location = url
|
|
.replace(/^https:\/\/github\.com\/([\w-]*\/[\w-]*)\/blob\/([\w/\-_.]*)(\?raw=true)?$/, 'https://raw.githubusercontent.com/$1/$2')
|
|
.replace(/^https:\/\/github\.com\/([\w-]*\/[\w-]*)\/raw\/([\w/\-_.]*)$/, 'https://raw.githubusercontent.com/$1/$2')
|
|
.replace(/^https:\/\/huggingface.co\/(.*)\/blob\/(.*)$/, 'https://huggingface.co/$1/resolve/$2');
|
|
if (this._view.accept(identifier || location) && location.indexOf('*') === -1) {
|
|
const status = await this._openModel(location, identifier);
|
|
if (status === '') {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
const gist = params.get('gist');
|
|
if (gist) {
|
|
this._openGist(gist);
|
|
return;
|
|
}
|
|
const openFileButton = this._element('open-file-button');
|
|
const openFileDialog = this._element('open-file-dialog');
|
|
if (openFileButton && openFileDialog) {
|
|
openFileButton.addEventListener('click', () => {
|
|
this.execute('open');
|
|
});
|
|
const mobileSafari = this.environment('platform') === 'darwin' && window.navigator.maxTouchPoints && window.navigator.maxTouchPoints > 1;
|
|
if (!mobileSafari) {
|
|
const extensions = new base.Metadata().extensions.map((extension) => `.${extension}`);
|
|
openFileDialog.setAttribute('accept', extensions.join(', '));
|
|
}
|
|
openFileDialog.addEventListener('change', (e) => {
|
|
if (e.target && e.target.files && e.target.files.length > 0) {
|
|
const files = Array.from(e.target.files);
|
|
const file = files.find((file) => this._view.accept(file.name, file.size));
|
|
if (file) {
|
|
this._open(file, files);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
document.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
});
|
|
document.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
});
|
|
document.body.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
const files = Array.from(e.dataTransfer.files);
|
|
const file = files.find((file) => this._view.accept(file.name, file.size));
|
|
if (file) {
|
|
this._open(file, files);
|
|
}
|
|
}
|
|
});
|
|
this._view.show('welcome');
|
|
}
|
|
|
|
environment(name) {
|
|
return this._environment[name];
|
|
}
|
|
|
|
async require(id) {
|
|
return import(`${id}.js`);
|
|
}
|
|
|
|
worker(id) {
|
|
const window = this.window;
|
|
return new window.Worker(`${id}.js`, { type: 'module' });
|
|
}
|
|
|
|
async save(name, extension, defaultPath) {
|
|
return `${defaultPath}.${extension}`;
|
|
}
|
|
|
|
async export(file, blob) {
|
|
const window = this.window;
|
|
const document = this.document;
|
|
const element = document.createElement('a');
|
|
element.download = file;
|
|
const url = window.URL.createObjectURL(blob);
|
|
element.href = url;
|
|
document.body.appendChild(element);
|
|
element.click();
|
|
document.body.removeChild(element);
|
|
window.URL.revokeObjectURL(url);
|
|
}
|
|
|
|
async execute(name /*, value */) {
|
|
switch (name) {
|
|
case 'open': {
|
|
const openFileDialog = this._element('open-file-dialog');
|
|
if (openFileDialog) {
|
|
openFileDialog.value = '';
|
|
openFileDialog.click();
|
|
}
|
|
break;
|
|
}
|
|
case 'report-issue': {
|
|
this.openURL(`${this.environment('repository')}/issues/new`);
|
|
break;
|
|
}
|
|
case 'about': {
|
|
this._view.about();
|
|
break;
|
|
}
|
|
default: {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
async fetch(file, encoding, base) {
|
|
const url = base ? `${base}/${file}` : this._url(file);
|
|
return this._fetch(url, null, encoding);
|
|
}
|
|
|
|
async asset(file) {
|
|
this._assets = this._assets || new Map();
|
|
if (!this._assets.has(file)) {
|
|
const url = this._url(file);
|
|
const separator = (/\?/).test(url) ? '&' : '?';
|
|
const tag = this.version && this.version !== '0.0.0' ? `version=${this.version}` : `cb=${new Date().getTime()}`;
|
|
const request = this._request(`${url}${separator}${tag}`, null, 'utf-8');
|
|
request.catch(() => this._assets.delete(file));
|
|
this._assets.set(file, request);
|
|
}
|
|
return this._assets.get(file);
|
|
}
|
|
|
|
openURL(url) {
|
|
const window = this.window;
|
|
window.location = url;
|
|
}
|
|
|
|
exception(error, fatal) {
|
|
if (this._telemetry && error) {
|
|
const name = error.name ? `${error.name}: ` : '';
|
|
const message = error.message ? error.message : JSON.stringify(error);
|
|
let context = '';
|
|
let stack = '';
|
|
if (error.stack) {
|
|
const format = (file, line, column) => {
|
|
return `${file.split('\\').join('/').split('/').pop()}:${line}:${column}`;
|
|
};
|
|
const match = error.stack.match(/\n {4}at (.*) \((.*):(\d*):(\d*)\)/);
|
|
if (match) {
|
|
stack = `${match[1]} (${format(match[2], match[3], match[4])})`;
|
|
} else {
|
|
const match = error.stack.match(/\n {4}at (.*):(\d*):(\d*)/);
|
|
if (match) {
|
|
stack = `(${format(match[1], match[2], match[3])})`;
|
|
} else {
|
|
const match = error.stack.match(/\n {4}at (.*)\((.*)\)/);
|
|
if (match) {
|
|
stack = `(${format(match[1], match[2], match[3])})`;
|
|
} else {
|
|
const match = error.stack.match(/\s*@\s*(.*):(.*):(.*)/);
|
|
if (match) {
|
|
stack = `(${format(match[1], match[2], match[3])})`;
|
|
} else {
|
|
const match = error.stack.match(/.*\n\s*(.*)\s*/);
|
|
if (match) {
|
|
[, stack] = match;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (error.context) {
|
|
context = typeof error.context === 'string' ? error.context : JSON.stringify(error.context);
|
|
}
|
|
this._telemetry.send('exception', {
|
|
app_name: this.type,
|
|
app_version: this.version,
|
|
error_name: name,
|
|
error_message: message,
|
|
error_context: context,
|
|
error_stack: stack,
|
|
error_fatal: fatal ? true : false
|
|
});
|
|
}
|
|
}
|
|
|
|
event(name, params) {
|
|
if (name && params) {
|
|
params.app_name = this.type;
|
|
params.app_version = this.version;
|
|
this._telemetry.send(name, params);
|
|
}
|
|
}
|
|
|
|
async _fetch(url, headers, encoding, callback, timeout) {
|
|
if (!url.startsWith('data:')) {
|
|
const separator = (/\?/).test(url) ? '&' : '?';
|
|
url = `${url}${separator}cb=${new Date().getTime()}`;
|
|
}
|
|
return this._request(url, headers, encoding, callback, timeout);
|
|
}
|
|
|
|
async _request(url, headers, encoding, callback, timeout) {
|
|
const window = this.window;
|
|
return new Promise((resolve, reject) => {
|
|
const request = new window.XMLHttpRequest();
|
|
if (!encoding) {
|
|
request.responseType = 'arraybuffer';
|
|
}
|
|
if (timeout) {
|
|
request.timeout = timeout;
|
|
}
|
|
const progress = (value) => {
|
|
if (callback) {
|
|
callback(value);
|
|
}
|
|
};
|
|
request.onload = () => {
|
|
progress(0);
|
|
if (request.status === 200) {
|
|
let value = null;
|
|
if (request.responseType === 'arraybuffer') {
|
|
const buffer = new Uint8Array(request.response);
|
|
value = new base.BinaryStream(buffer);
|
|
} else {
|
|
value = request.responseText;
|
|
}
|
|
resolve(value);
|
|
} else {
|
|
const error = new Error(`The web request failed with status code '${request.status}'.`);
|
|
error.context = url;
|
|
reject(error);
|
|
}
|
|
};
|
|
request.onerror = () => {
|
|
progress(0);
|
|
const error = new Error(`The web request failed.`);
|
|
error.context = url;
|
|
reject(error);
|
|
};
|
|
request.ontimeout = () => {
|
|
progress(0);
|
|
request.abort();
|
|
const error = new Error('The web request timed out.', 'timeout', url);
|
|
error.context = url;
|
|
reject(error);
|
|
};
|
|
request.onprogress = (e) => {
|
|
if (e && e.lengthComputable) {
|
|
progress(e.loaded / e.total * 100);
|
|
}
|
|
};
|
|
request.open('GET', url, true);
|
|
if (headers) {
|
|
for (const [name, value] of Object.entries(headers)) {
|
|
request.setRequestHeader(name, value);
|
|
}
|
|
}
|
|
request.send();
|
|
});
|
|
}
|
|
|
|
_url(file) {
|
|
if (file.startsWith('./')) {
|
|
file = file.substring(2);
|
|
} else if (file.startsWith('/')) {
|
|
file = file.substring(1);
|
|
}
|
|
const window = this.window;
|
|
const location = window.location;
|
|
const pathname = location.pathname.endsWith('/') ? location.pathname : `${location.pathname.split('/').slice(0, -1).join('/')}/`;
|
|
return `${location.protocol}//${location.host}${pathname}${file}`;
|
|
}
|
|
|
|
async _openModel(url, identifier, name) {
|
|
this._view.show('welcome spinner');
|
|
let context = null;
|
|
try {
|
|
const progress = (value) => {
|
|
this._view.progress(value);
|
|
};
|
|
let stream = await this._fetch(url, null, null, progress);
|
|
if (url.startsWith('https://raw.githubusercontent.com/') && stream.length < 150) {
|
|
const buffer = stream.peek();
|
|
const content = Array.from(buffer).map((c) => String.fromCodePoint(c)).join('');
|
|
if (content.split('\n')[0] === 'version https://git-lfs.github.com/spec/v1') {
|
|
url = url.replace('https://raw.githubusercontent.com/', 'https://media.githubusercontent.com/media/');
|
|
stream = await this._fetch(url, null, null, progress);
|
|
}
|
|
}
|
|
context = new browser.Context(this, url, identifier, name, stream);
|
|
this._telemetry.set('session_engaged', 1);
|
|
} catch (error) {
|
|
await this._view.error(error, 'Model load request failed.');
|
|
this._view.show('welcome');
|
|
return null;
|
|
}
|
|
return await this._openContext(context);
|
|
}
|
|
|
|
async _open(file, files) {
|
|
this._view.show('welcome spinner');
|
|
const context = new browser.BrowserFileContext(this, file, files);
|
|
try {
|
|
await context.open();
|
|
await this._openContext(context);
|
|
} catch (error) {
|
|
await this._view.error(error);
|
|
}
|
|
}
|
|
|
|
async _openGist(gist) {
|
|
this._view.show('welcome spinner');
|
|
const url = `https://api.github.com/gists/${gist}`;
|
|
try {
|
|
const text = await this._fetch(url, { 'Content-Type': 'application/json' }, 'utf-8');
|
|
const json = JSON.parse(text);
|
|
let message = json.message;
|
|
let file = null;
|
|
if (!message) {
|
|
file = Object.values(json.files).find((file) => this._view.accept(file.filename));
|
|
if (!file) {
|
|
message = 'Gist does not contain a model file.';
|
|
}
|
|
}
|
|
if (message) {
|
|
const error = new Error(message);
|
|
error.name = 'Error while loading Gist.';
|
|
throw error;
|
|
}
|
|
const identifier = file.filename;
|
|
const encoder = new TextEncoder();
|
|
const buffer = encoder.encode(file.content);
|
|
const stream = new base.BinaryStream(buffer);
|
|
const context = new browser.Context(this, '', identifier, null, stream);
|
|
await this._openContext(context);
|
|
} catch (error) {
|
|
await this._view.error(error, 'Error while loading Gist.');
|
|
this._view.show('welcome');
|
|
}
|
|
}
|
|
|
|
async _openContext(context) {
|
|
const document = this.document;
|
|
this._telemetry.set('session_engaged', 1);
|
|
try {
|
|
const attachment = await this._view.attach(context);
|
|
if (attachment) {
|
|
this._view.show(null);
|
|
return 'context-open-attachment';
|
|
}
|
|
const model = await this._view.open(context);
|
|
if (model) {
|
|
this._view.show(null);
|
|
document.title = context.name || context.identifier;
|
|
return '';
|
|
}
|
|
document.title = '';
|
|
return 'context-open-failed';
|
|
} catch (error) {
|
|
await this._view.error(error, error.name);
|
|
return 'context-open-error';
|
|
}
|
|
}
|
|
|
|
_setCookie(name, value, days) {
|
|
const window = this.window;
|
|
const document = this.document;
|
|
document.cookie = `${name}=; Max-Age=0`;
|
|
const location = window.location;
|
|
const domain = location && location.hostname && location.hostname.indexOf('.') !== -1 ? `;domain=.${location.hostname.split('.').slice(-2).join('.')}` : '';
|
|
const date = new Date();
|
|
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
document.cookie = `${name}=${value}${domain};path=/;expires=${date.toUTCString()}`;
|
|
}
|
|
|
|
_getCookie(name) {
|
|
const document = this.document;
|
|
for (const cookie of document.cookie.split(';')) {
|
|
const entry = cookie.split('=');
|
|
if (entry[0].trim() === name) {
|
|
return entry[1].trim();
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
get(name) {
|
|
const window = this.window;
|
|
try {
|
|
if (typeof window.localStorage !== 'undefined') {
|
|
const content = window.localStorage.getItem(name);
|
|
return JSON.parse(content);
|
|
}
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
set(name, value) {
|
|
const window = this.window;
|
|
try {
|
|
if (typeof window.localStorage !== 'undefined') {
|
|
window.localStorage.setItem(name, JSON.stringify(value));
|
|
}
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
}
|
|
|
|
delete(name) {
|
|
const window = this.window;
|
|
try {
|
|
if (typeof window.localStorage !== 'undefined') {
|
|
window.localStorage.removeItem(name);
|
|
}
|
|
} catch {
|
|
// continue regardless of error
|
|
}
|
|
}
|
|
|
|
_element(id) {
|
|
const document = this.document;
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
update() {
|
|
}
|
|
|
|
async message(message, alert, action) {
|
|
return new Promise((resolve) => {
|
|
const document = this.document;
|
|
const type = document.body.getAttribute('class');
|
|
this._element('message-text').innerText = message || '';
|
|
const button = this._element('message-button');
|
|
if (action) {
|
|
button.style.removeProperty('display');
|
|
button.innerText = action;
|
|
button.onclick = () => {
|
|
button.onclick = null;
|
|
document.body.setAttribute('class', type);
|
|
resolve(0);
|
|
};
|
|
} else {
|
|
button.style.display = 'none';
|
|
button.onclick = null;
|
|
}
|
|
if (alert) {
|
|
document.body.setAttribute('class', 'alert');
|
|
} else {
|
|
document.body.classList.add('notification');
|
|
document.body.classList.remove('default');
|
|
}
|
|
if (action) {
|
|
button.focus();
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
browser.BrowserFileContext = class {
|
|
|
|
constructor(host, file, blobs) {
|
|
this._host = host;
|
|
this._file = file;
|
|
this._blobs = {};
|
|
for (const blob of blobs) {
|
|
this._blobs[blob.name] = blob;
|
|
}
|
|
}
|
|
|
|
get identifier() {
|
|
return this._file.name;
|
|
}
|
|
|
|
get stream() {
|
|
return this._stream;
|
|
}
|
|
|
|
async asset(file) {
|
|
return this._host.asset(file);
|
|
}
|
|
|
|
async fetch(file, encoding, basename) {
|
|
if (basename !== undefined) {
|
|
return this._host.fetch(file, encoding, basename);
|
|
}
|
|
const blob = this._blobs[file];
|
|
if (!blob) {
|
|
throw new Error(`File not found '${file}'.`);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const window = this._host.window;
|
|
const reader = new window.FileReader();
|
|
const size = 0x10000000;
|
|
let position = 0;
|
|
const chunks = [];
|
|
reader.onload = (e) => {
|
|
if (encoding) {
|
|
resolve(e.target.result);
|
|
} else {
|
|
const buffer = new Uint8Array(e.target.result);
|
|
if (position === 0 && buffer.length === blob.size) {
|
|
const stream = new base.BinaryStream(buffer);
|
|
resolve(stream);
|
|
} else {
|
|
chunks.push(buffer);
|
|
position += buffer.length;
|
|
if (position < blob.size) {
|
|
const slice = blob.slice(position, Math.min(position + size, blob.size));
|
|
reader.readAsArrayBuffer(slice);
|
|
} else {
|
|
const stream = new browser.FileStream(chunks, size, 0, position);
|
|
resolve(stream);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
reader.onerror = (event) => {
|
|
event = event || this._host.window.event;
|
|
let message = '';
|
|
const error = event.target.error;
|
|
switch (error.code) {
|
|
case error.NOT_FOUND_ERR:
|
|
message = `File not found '${file}'.`;
|
|
break;
|
|
case error.NOT_READABLE_ERR:
|
|
message = `File not readable '${file}'.`;
|
|
break;
|
|
case error.SECURITY_ERR:
|
|
message = `File access denied '${file}'.`;
|
|
break;
|
|
default:
|
|
message = error.message ? error.message : `File read '${error.code}' error '${file}'.`;
|
|
break;
|
|
}
|
|
reject(new Error(message));
|
|
};
|
|
if (encoding === 'utf-8') {
|
|
reader.readAsText(blob, encoding);
|
|
} else {
|
|
const slice = blob.slice(position, Math.min(position + size, blob.size));
|
|
reader.readAsArrayBuffer(slice);
|
|
}
|
|
});
|
|
}
|
|
|
|
async require(id) {
|
|
return this._host.require(id);
|
|
}
|
|
|
|
error(error, fatal) {
|
|
this._host.exception(error, fatal);
|
|
}
|
|
|
|
async open() {
|
|
this._stream = await this.fetch(this._file.name, null);
|
|
}
|
|
};
|
|
|
|
browser.FileStream = class {
|
|
|
|
constructor(chunks, size, start, length) {
|
|
this._chunks = chunks;
|
|
this._size = size;
|
|
this._start = start;
|
|
this._length = length;
|
|
this._position = 0;
|
|
}
|
|
|
|
get position() {
|
|
return this._position;
|
|
}
|
|
|
|
get length() {
|
|
return this._length;
|
|
}
|
|
|
|
stream(length) {
|
|
const file = new browser.FileStream(this._chunks, this._size, this._start + this._position, length);
|
|
this.skip(length);
|
|
return file;
|
|
}
|
|
|
|
seek(position) {
|
|
this._position = position >= 0 ? position : this._length + position;
|
|
}
|
|
|
|
skip(offset) {
|
|
this._position += offset;
|
|
if (this._position > this._length) {
|
|
throw new Error(`Expected ${this._position - this._length} more bytes. The file might be corrupted. Unexpected end of file.`);
|
|
}
|
|
}
|
|
|
|
peek(length) {
|
|
length = length === undefined ? this._length - this._position : length;
|
|
if (length < 0x10000000) {
|
|
const position = this._fill(length);
|
|
this._position -= length;
|
|
return this._buffer.subarray(position, position + length);
|
|
}
|
|
const position = this._start + this._position;
|
|
if (position % this._size === 0) {
|
|
const index = Math.floor(position / this._size);
|
|
const chunk = this._chunks[index];
|
|
if (chunk && chunk.length === length) {
|
|
return chunk;
|
|
}
|
|
}
|
|
const buffer = new Uint8Array(length);
|
|
this._read(buffer, position);
|
|
return buffer;
|
|
}
|
|
|
|
read(length) {
|
|
length = length === undefined ? this._length - this._position : length;
|
|
if (length < 0x10000000) {
|
|
const position = this._fill(length);
|
|
return this._buffer.slice(position, position + length);
|
|
}
|
|
const position = this._start + this._position;
|
|
this.skip(length);
|
|
if (position % this._size === 0) {
|
|
const index = Math.floor(position / this._size);
|
|
const chunk = this._chunks[index];
|
|
if (chunk && chunk.length === length) {
|
|
return chunk;
|
|
}
|
|
}
|
|
const buffer = new Uint8Array(length);
|
|
this._read(buffer, position);
|
|
return buffer;
|
|
}
|
|
|
|
_fill(length) {
|
|
if (this._position + length > this._length) {
|
|
throw new Error(`Expected ${this._position + length - this._length} more bytes. The file might be corrupted. Unexpected end of file.`);
|
|
}
|
|
if (!this._buffer || this._position < this._offset || this._position + length > this._offset + this._buffer.length) {
|
|
this._offset = this._start + this._position;
|
|
const length = Math.min(0x10000000, this._start + this._length - this._offset);
|
|
if (!this._buffer || length !== this._buffer.length) {
|
|
this._buffer = new Uint8Array(length);
|
|
}
|
|
this._read(this._buffer, this._offset);
|
|
}
|
|
const position = this._start + this._position - this._offset;
|
|
this._position += length;
|
|
return position;
|
|
}
|
|
|
|
_read(buffer, offset) {
|
|
let index = Math.floor(offset / this._size);
|
|
offset -= index * this._size;
|
|
const chunk = this._chunks[index++];
|
|
let destination = Math.min(chunk.length - offset, buffer.length);
|
|
buffer.set(chunk.subarray(offset, offset + destination), 0);
|
|
while (destination < buffer.length) {
|
|
const chunk = this._chunks[index++];
|
|
const size = Math.min(this._size, buffer.length - destination);
|
|
buffer.set(chunk.subarray(0, size), destination);
|
|
destination += size;
|
|
}
|
|
}
|
|
};
|
|
|
|
browser.Context = class {
|
|
|
|
constructor(host, url, identifier, name, stream) {
|
|
this._host = host;
|
|
this._name = name;
|
|
this._stream = stream;
|
|
const parts = url.split('?')[0].split('/');
|
|
this._identifier = parts.pop();
|
|
this._base = parts.join('/');
|
|
if (identifier) {
|
|
this._identifier = identifier;
|
|
}
|
|
}
|
|
|
|
get identifier() {
|
|
return this._identifier;
|
|
}
|
|
|
|
get name() {
|
|
return this._name;
|
|
}
|
|
|
|
get stream() {
|
|
return this._stream;
|
|
}
|
|
|
|
async asset(file) {
|
|
return this._host.asset(file);
|
|
}
|
|
|
|
async fetch(file, encoding, base) {
|
|
base = base === undefined ? this._base : base;
|
|
return this._host.fetch(file, encoding, base);
|
|
}
|
|
|
|
async require(id) {
|
|
return this._host.require(id);
|
|
}
|
|
|
|
error(error, fatal) {
|
|
this._host.exception(error, fatal);
|
|
}
|
|
};
|
|
|
|
if (!('scrollBehavior' in window.document.documentElement.style)) {
|
|
const __scrollTo__ = window.Element.prototype.scrollTo;
|
|
window.Element.prototype.scrollTo = function(...args) {
|
|
const [options] = args;
|
|
if (options !== undefined) {
|
|
if (options === null || typeof options !== 'object' || options.behavior === undefined || options.behavior === 'auto' || options.behavior === 'instant') {
|
|
if (__scrollTo__) {
|
|
__scrollTo__.apply(this, args);
|
|
}
|
|
} else {
|
|
const now = () => window.performance && window.performance.now ? window.performance.now() : Date.now();
|
|
const ease = (k) => 0.5 * (1 - Math.cos(Math.PI * k));
|
|
const step = (context) => {
|
|
const value = ease(Math.min((now() - context.startTime) / 468, 1));
|
|
const x = context.startX + (context.x - context.startX) * value;
|
|
const y = context.startY + (context.y - context.startY) * value;
|
|
context.element.scrollLeft = x;
|
|
context.element.scrollTop = y;
|
|
if (x !== context.x || y !== context.y) {
|
|
window.requestAnimationFrame(step.bind(window, context));
|
|
}
|
|
};
|
|
const context = {
|
|
element: this,
|
|
x: typeof options.left === 'undefined' ? this.scrollLeft : ~~options.left,
|
|
y: typeof options.top === 'undefined' ? this.scrollTop : ~~options.top,
|
|
startX: this.scrollLeft,
|
|
startY: this.scrollTop,
|
|
startTime: now()
|
|
};
|
|
step(context);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
if (typeof window !== 'undefined' && window.exports) {
|
|
window.exports.browser = browser;
|
|
}
|
|
|
|
export const Host = browser.Host;
|