1 | let uid = null;
|
2 | let subscribed = false;
|
3 | let credits = 0;
|
4 | Promise.all([
|
5 | vrid.getUser(),
|
6 | vrid.getSubscribed(),
|
7 | ])
|
8 | .then(([
|
9 | newUser,
|
10 | newSubscribed,
|
11 | ]) => {
|
12 | const _getCredits = () => {
|
13 | if (newUser) {
|
14 | return vrid.getRaw('credits/' + newUser.uid)
|
15 | .then(credits => credits || 0);
|
16 | } else {
|
17 | return Promise.resolve(0);
|
18 | }
|
19 | };
|
20 |
|
21 | _getCredits()
|
22 | .then(newCredits => {
|
23 | if (newUser) {
|
24 | uid = newUser.uid;
|
25 | subscribed = newSubscribed;
|
26 | credits = newCredits;
|
27 |
|
28 | const accountTypeEl = document.getElementById('account-type');
|
29 | const accountTypeStoreEl = document.getElementById('account-type-store');
|
30 | if (subscribed) {
|
31 | accountTypeEl.innerText = 'Gold';
|
32 | accountTypeStoreEl.innerText = 'Gold';
|
33 |
|
34 | const goldInputEl = document.getElementById('gold-input');
|
35 | goldInputEl.checked = true;
|
36 | goldInputEl.dispatchEvent(new Event('change'));
|
37 | } else {
|
38 | accountTypeEl.innerText = 'Basic';
|
39 | accountTypeStoreEl.innerText = 'Basic';
|
40 | }
|
41 |
|
42 | _renderCredits(credits);
|
43 |
|
44 | document.body.classList.add('logged-in');
|
45 | }
|
46 |
|
47 | window.addEventListener('popstate', () => {
|
48 | _loadUrl(window.location.pathname);
|
49 | });
|
50 | _loadUrl(window.location.pathname);
|
51 |
|
52 | document.body.classList.add('loaded');
|
53 | });
|
54 | })
|
55 | .catch(err => {
|
56 | console.warn(err);
|
57 | });
|
58 |
|
59 | const CONTROLLER_DEFAULT_OFFSETS = [0.2, -0.1, -0.2];
|
60 |
|
61 | function $$(s) {
|
62 | const el = document.createElement('div');
|
63 | if (s) {
|
64 | el.innerHTML = s;
|
65 | return el.children[0];
|
66 | } else {
|
67 | return el;
|
68 | }
|
69 | }
|
70 | function _findParentNode(node, selector) {
|
71 | while (node) {
|
72 | const el = node.querySelector(selector);
|
73 | if (el) {
|
74 | return el;
|
75 | } else {
|
76 | node = node.parentNode;
|
77 | }
|
78 | }
|
79 | return null;
|
80 | }
|
81 | function _arrayify(array, numElements) {
|
82 | array = array || [];
|
83 |
|
84 | const result = Array(numElements);
|
85 | for (let i = 0; i < numElements; i++) {
|
86 | result[i] = array[i] || null;
|
87 | }
|
88 | return result;
|
89 | }
|
90 | function _capitalize(s) {
|
91 | return s[0].toUpperCase() + s.slice(1);
|
92 | }
|
93 | function _getAssetType(asset) {
|
94 | const match = asset.match(/^(ITEM|MOD|SKIN|FILE)\.(.+)$/);
|
95 | return {
|
96 | type: match[1].toLowerCase(),
|
97 | name: match[2].toLowerCase(),
|
98 | };
|
99 | }
|
100 | function _resJson(res) {
|
101 | if (res.status >= 200 && res.status < 300) {
|
102 | return res.json();
|
103 | } else {
|
104 | return Promise.reject({
|
105 | status: res.status,
|
106 | stack: 'API returned invalid status code: ' + res.status,
|
107 | });
|
108 | }
|
109 | }
|
110 | function _resBlob(res) {
|
111 | if (res.status >= 200 && res.status < 300) {
|
112 | return res.blog();
|
113 | } else {
|
114 | return Promise.reject({
|
115 | status: res.status,
|
116 | stack: 'API returned invalid status code: ' + res.status,
|
117 | });
|
118 | }
|
119 | }
|
120 | function _jsonParse(s) {
|
121 | try {
|
122 | return JSON.parse(s);
|
123 | } catch(err) {
|
124 | return undefined;
|
125 | }
|
126 | }
|
127 | const base64 = {
|
128 | encode: buffer => {
|
129 | var bytes = new Uint8Array(buffer);
|
130 | var base64 = ''
|
131 | var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
132 |
|
133 | var byteLength = bytes.byteLength
|
134 | var byteRemainder = byteLength % 3
|
135 | var mainLength = byteLength - byteRemainder
|
136 |
|
137 | var a, b, c, d
|
138 | var chunk
|
139 |
|
140 |
|
141 | for (var i = 0; i < mainLength; i = i + 3) {
|
142 |
|
143 | chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]
|
144 |
|
145 |
|
146 | a = (chunk & 16515072) >> 18
|
147 | b = (chunk & 258048) >> 12
|
148 | c = (chunk & 4032) >> 6
|
149 | d = chunk & 63
|
150 |
|
151 |
|
152 | base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
|
153 | }
|
154 |
|
155 |
|
156 | if (byteRemainder == 1) {
|
157 | chunk = bytes[mainLength]
|
158 |
|
159 | a = (chunk & 252) >> 2
|
160 |
|
161 |
|
162 | b = (chunk & 3) << 4
|
163 |
|
164 | base64 += encodings[a] + encodings[b] + '=='
|
165 | } else if (byteRemainder == 2) {
|
166 | chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]
|
167 |
|
168 | a = (chunk & 64512) >> 10
|
169 | b = (chunk & 1008) >> 4
|
170 |
|
171 |
|
172 | c = (chunk & 15) << 2
|
173 |
|
174 | base64 += encodings[a] + encodings[b] + encodings[c] + '='
|
175 | }
|
176 |
|
177 | return base64
|
178 | },
|
179 | };
|
180 | const _makeId = () => {
|
181 | const arrayBuffer = new ArrayBuffer(32);
|
182 | const array = new Uint8Array(arrayBuffer);
|
183 | crypto.getRandomValues(array);
|
184 | return hex.encode(arrayBuffer);
|
185 | };
|
186 |
|
187 | function _requestPkid(publicKey) {
|
188 | return crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(publicKey)))
|
189 | .then(digest => {
|
190 | let pkid = 0;
|
191 | for (let i = 0; i < digest.byteLength; i += 4) {
|
192 | pkid ^= new Uint32Array(digest, i, 1)[0];
|
193 | }
|
194 | return pkid;
|
195 | });
|
196 | }
|
197 | function _importPublicKey(publicKey) {
|
198 | return crypto.subtle.importKey('jwk', publicKey, {
|
199 | name: 'ECDSA',
|
200 | namedCurve: 'P-256',
|
201 | }, true, ['verify']);
|
202 | }
|
203 | function _importPrivateKey(privateKey) {
|
204 | return crypto.subtle.importKey('jwk', privateKey, {
|
205 | name: 'ECDSA',
|
206 | namedCurve: 'P-256',
|
207 | }, true, ['sign']);
|
208 | }
|
209 | function _importKey(key) {
|
210 | return Promise.all([
|
211 | _importPublicKey(key.publicKey),
|
212 | _importPrivateKey(key.privateKey),
|
213 | ])
|
214 | .then(([
|
215 | publicKey,
|
216 | privateKey,
|
217 | ]) => ({
|
218 | publicKey,
|
219 | privateKey,
|
220 | }));
|
221 | }
|
222 | function _requestVerifyAsset(assetSpec) {
|
223 | const {asset} = assetSpec;
|
224 |
|
225 | const {type, name} = _getAssetType(asset);
|
226 | if (type === 'item' || type === 'skin' || type === 'file') {
|
227 | const item = items[type === 'item' ? name : type];
|
228 | const {authority} = item;
|
229 |
|
230 | if (authority) {
|
231 | const {certificate} = assetSpec;
|
232 |
|
233 | if (certificate && Array.isArray(certificate) && certificate.length > 0) {
|
234 | const firstCertificate = certificate[0];
|
235 |
|
236 | if (firstCertificate && typeof firstCertificate.pkid === 'number' && typeof firstCertificate.publicKey === 'object' && typeof firstCertificate.timestamp === 'number' && typeof firstCertificate.signature === 'string') {
|
237 | const {publicKey, timestamp, signature} = firstCertificate;
|
238 |
|
239 | return Promise.all([
|
240 | _requestPkid(publicKey),
|
241 | _importPublicKey(publicKey),
|
242 | ])
|
243 | .then(([
|
244 | pkid,
|
245 | cryptoPublicKey,
|
246 | ]) => {
|
247 | if (pkid === authority) {
|
248 | const newAssetSpec = {
|
249 | asset: assetSpec.asset,
|
250 | quantity: assetSpec.quantity,
|
251 | owner: assetSpec.owner,
|
252 | timestamp: assetSpec.timestamp,
|
253 | };
|
254 | return crypto.subtle.verify({
|
255 | name: 'ECDSA',
|
256 | hash: {
|
257 | name: 'SHA-256',
|
258 | },
|
259 | }, cryptoPublicKey, hex.decode(signature), new TextEncoder().encode(JSON.stringify(newAssetSpec)))
|
260 | .catch(err => {
|
261 | console.warn(err);
|
262 | });
|
263 | } else {
|
264 | return Promise.resolve(false);
|
265 | }
|
266 | });
|
267 | } else {
|
268 | return Promise.resolve(false);
|
269 | }
|
270 | } else {
|
271 | return Promise.resolve(false);
|
272 | }
|
273 | } else {
|
274 | return Promise.resolve(true);
|
275 | }
|
276 | } else if (type === 'mod') {
|
277 | const mod = mods.find(modSpec => modSpec.name === name);
|
278 | if (mod) {
|
279 |
|
280 |
|
281 | return Promise.resolve(true);
|
282 | } else {
|
283 | console.warn('unknown asset: ', asset);
|
284 |
|
285 | return Promise.resolve(true);
|
286 | }
|
287 | } else {
|
288 | throw new Error('unknown asset type: ' + asset);
|
289 | }
|
290 | }
|
291 | function _requestFilterVerifiedAssets(assets) {
|
292 | const result = [];
|
293 | return Promise.all(assets.map(asset =>
|
294 | _requestVerifyAsset(asset).then(ok => {
|
295 | if (ok) {
|
296 | result.push(asset);
|
297 | }
|
298 | })
|
299 | ))
|
300 | .then(() => result);
|
301 | }
|
302 | const hex = {
|
303 | encode: buffer => {
|
304 | const bytes = new Uint8Array(buffer);
|
305 | let s = '';
|
306 | for (let i = 0; i < bytes.length; i++) {
|
307 | s += ('0' + (bytes[i]).toString(16)).slice(-2);
|
308 | }
|
309 | return s;
|
310 | },
|
311 | decode: hex => {
|
312 | const numBytes = hex.length / 2;
|
313 | const buffer = new ArrayBuffer(numBytes);
|
314 | const view = new Uint8Array(buffer);
|
315 | for (let i = 0; i < numBytes; i++) {
|
316 | const baseIndex = i * 2;
|
317 | view[i] = parseInt(hex.substring(baseIndex, baseIndex + 2), 16);
|
318 | }
|
319 | return buffer;
|
320 | },
|
321 | };
|
322 |
|
323 | const modalEl = document.getElementById('modal');
|
324 | modalEl.addEventListener('click', e => {
|
325 | if (e.target === modalEl) {
|
326 | modalEl.innerHTML = '';
|
327 |
|
328 | e.preventDefault();
|
329 | e.stopPropagation();
|
330 | }
|
331 | });
|
332 |
|
333 | let items = null;
|
334 | let mods = null;
|
335 | let sales = null;
|
336 | const _loadAllAssets = () => vrid.getRaw('items')
|
337 | .then(j => {
|
338 | items = j;
|
339 | });
|
340 | const _loadAllMods = () => fetch(`https://my-site.zeovr.io/mods`)
|
341 | .then(_resJson)
|
342 | .then(j => {
|
343 | mods = j;
|
344 | });
|
345 | const _loadAllSales = () => vrid.getRaw('sales')
|
346 | .then(sales => {
|
347 | sales = sales || {};
|
348 | return Promise.all(Object.keys(sales).map(k => {
|
349 | const sale = sales[k];
|
350 | return vrid.getRaw('users/' + sale.seller + '/name')
|
351 | .then(name => {
|
352 | sale.sellerName = name;
|
353 | return sale;
|
354 | });
|
355 | }));
|
356 | })
|
357 | .then(newSales => {
|
358 | sales = newSales;
|
359 | });
|
360 | const _loadAssets = (filterType, assetsEl, getHtml) => Promise.all([
|
361 | _loadAllAssets(),
|
362 | _loadAllMods(),
|
363 | vrid.get('assets'),
|
364 | ])
|
365 | .then(([
|
366 | allAssetsResult,
|
367 | allModsResult,
|
368 | assets,
|
369 | ]) => {
|
370 | assets = assets || [];
|
371 | assets = assets.map(assetSpec => {
|
372 | const {type, name} = _getAssetType(assetSpec.asset);
|
373 |
|
374 | if (filterType ? type === filterType : type !== 'skin') {
|
375 | const item = (() => {
|
376 | switch (type) {
|
377 | case 'item': return _normalizeItem(items[name])
|
378 | case 'skin':
|
379 | case 'file': return _normalizeItem(items[type]);
|
380 | case 'mod': return _normalizeMod(mods.find(modSpec => modSpec.name === name));
|
381 | default: return null;
|
382 | }
|
383 | })();
|
384 | item.id = assetSpec.id;
|
385 | item.asset = assetSpec.asset;
|
386 | item.json = assetSpec.json;
|
387 | item.certificate = assetSpec.certificate;
|
388 | return _onlyDefined(item);
|
389 | } else {
|
390 | return null;
|
391 | }
|
392 | }).filter(item => item !== null);
|
393 | assets = _quantizeAssets(assets);
|
394 |
|
395 | _renderAssets(assets, assetsEl, getHtml);
|
396 | });
|
397 | const _loadAvatars = () => Promise.all([
|
398 | vrid.getRaw('users'),
|
399 | vrid.getRaw('subscriptions'),
|
400 | ])
|
401 | .then(([
|
402 | users,
|
403 | subscriptions,
|
404 | ]) => users ? Object.keys(users).map(k => {
|
405 | const user = users[k];
|
406 | user.subscribed = Boolean(subscriptions[k]);
|
407 | return users[k];
|
408 | }) : []);
|
409 |
|
410 | let selectedTarget = null;
|
411 | let dragTarget = null;
|
412 | const _renderAssets = (assets, assetsEl, getHtml) => {
|
413 | while (assetsEl.firstChild) {
|
414 | assetsEl.firstChild.destroy && assetsEl.firstChild.destroy();
|
415 | assetsEl.removeChild(assetsEl.firstChild);
|
416 | }
|
417 |
|
418 | _requestFilterVerifiedAssets(assets)
|
419 | .then(assets => {
|
420 | for (let i = 0; i < assets.length; i++) {
|
421 | const assetSpec = assets[i];
|
422 | const {id, asset, icon, description, owner, file, json, certificate, sale} = assetSpec;
|
423 | const assetEl = $$(getHtml(assetSpec));
|
424 | const giftLinkEl = assetEl.querySelector('.gift-link');
|
425 | if (giftLinkEl) {
|
426 | giftLinkEl.addEventListener('click', e => {
|
427 | const assetSpec = assets.find(assetSpec => assetSpec.id === id);
|
428 | if (assetSpec) {
|
429 | modalEl.innerHTML = `\
|
430 | <form class=form style="background-color: #FFF; width: 400px; padding: 30px;">
|
431 | <div style="margin-bottom: 10px; font-size: 20px; font-weight: 400;">Gift <span style="font-weight: 600;">${assetSpec.asset}</span></div>
|
432 | <label for=quantity style="display: flex; align-items: center;">
|
433 | <div style="width: 60px; margin-right: 10px;">Quantity</div>
|
434 | <input type=number value=1 min=1 placeholder=1 name=quantity placeholder=0 class=quantity style="margin-bottom: 10px; padding: 10px; background-color: #EEE; border: 0; flex-grow: 1;">
|
435 | </label>
|
436 | <label for=vrid style="display: flex; align-items: center;">
|
437 | <div style="width: 60px; margin-right: 10px;">VRID</div>
|
438 | <input type=text placeholder="Enter avatar's VRID" name=vrid class=vrid style="margin-bottom: 10px; padding: 10px; background-color: #EEE; border: 0; flex-grow: 1;">
|
439 | </label>
|
440 | <label for=submit style="display: flex; align-items: center;">
|
441 | <div style="width: 60px; margin-right: 10px;"></div>
|
442 | <input type=submit value="Send gift" name=submit class=submit-button>
|
443 | </label>
|
444 | </form>
|
445 | `;
|
446 | const formEl = modalEl.querySelector('.form');
|
447 | const quantityEl = modalEl.querySelector('.quantity');
|
448 | const vridEl = modalEl.querySelector('.vrid');
|
449 | const submitButtonEl = modalEl.querySelector('.submit-button');
|
450 |
|
451 | quantityEl.max = assetSpec.assets.length;
|
452 |
|
453 | formEl.addEventListener('submit', e => {
|
454 | const quantity = quantityEl.value;
|
455 | const giftee = vridEl.value;
|
456 |
|
457 | if (quantity > 0 && quantity <= assetSpec.assets.length) {
|
458 | submitButtonEl.oldValue = submitButtonEl.value;
|
459 | submitButtonEl.value = 'Sending...';
|
460 | submitButtonEl.disabled = true;
|
461 |
|
462 | vrid.userExists(giftee)
|
463 | .then(exists => {
|
464 | if (exists) {
|
465 | const removedAssetSpecs = assetSpec.assets.slice(assetSpec.assets.length - quantity);
|
466 | const id = _makeId();
|
467 | const gift = {
|
468 | id,
|
469 | giftee,
|
470 | assets: removedAssetSpecs,
|
471 | }
|
472 |
|
473 | vrid.setRaw('gifts/' + id, gift)
|
474 | .then(() => vrid.onNullRaw(`gifts/${assetSpec.certificate[0].signature}`))
|
475 | .then(() => vrid.get('assets'))
|
476 | .then(globalAssets => {
|
477 | globalAssets = globalAssets.filter(assetSpec => !removedAssetSpecs.find(assetSpec2 => assetSpec2.id === assetSpec.id));
|
478 | return vrid.set('assets', globalAssets);
|
479 | })
|
480 | .then(() => {
|
481 | assetSpec.assets = assetSpec.assets.filter(assetSpec => !removedAssetSpecs.find(assetSpec2 => assetSpec2.id === assetSpec.id));
|
482 | if (assetSpec.assets.length === 0) {
|
483 | assets = assets.slice();
|
484 | assets.splice(assets.indexOf(assetSpec), 1);
|
485 | }
|
486 | _renderAssets(assets, assetsEl, getHtml);
|
487 |
|
488 | modalEl.innerHTML = '';
|
489 | })
|
490 | .catch(err => {
|
491 | console.warn(err);
|
492 | });
|
493 | } else {
|
494 | throw new Error('Giftee VRID does not exist: ' + giftee);
|
495 | }
|
496 | })
|
497 | .catch(err => {
|
498 | console.warn(err);
|
499 |
|
500 | submitButtonEl.value = submitButtonEl.oldValue;
|
501 | submitButtonEl.disabled = false;
|
502 | });
|
503 | } else {
|
504 | console.warn('invalid quantity');
|
505 | }
|
506 |
|
507 | e.preventDefault();
|
508 | });
|
509 | }
|
510 |
|
511 | e.preventDefault();
|
512 | e.stopPropagation();
|
513 | });
|
514 | }
|
515 | const sellLinkEl = assetEl.querySelector('.sell-link');
|
516 | if (sellLinkEl) {
|
517 | sellLinkEl.addEventListener('click', e => {
|
518 | const assetSpec = assets.find(assetSpec => assetSpec.id === id);
|
519 | if (assetSpec) {
|
520 | modalEl.innerHTML = `\
|
521 | <form class=form style="background-color: #FFF; width: 400px; padding: 30px;">
|
522 | <div style="margin-bottom: 10px; font-size: 20px; font-weight: 400;">Sell <span style="font-weight: 600;">${assetSpec.asset}</span></div>
|
523 | <label for=quantity style="display: flex; align-items: center;">
|
524 | <div style="width: 60px; margin-right: 10px;">Quantity</div>
|
525 | <input type=number value=1 min=1 placeholder=1 name=quantity class=quantity style="margin-bottom: 10px; padding: 10px; background-color: #EEE; border: 0; flex-grow: 1;">
|
526 | </label>
|
527 | <label for=value style="display: flex; align-items: center;">
|
528 | <div style="width: 60px; margin-right: 10px;">Value (each)</div>
|
529 | <input type=number value=1 name=value placeholder=0 class=value style="margin-bottom: 10px; padding: 10px; background-color: #EEE; border: 0; flex-grow: 1;">
|
530 | </label>
|
531 | <label for=submit style="display: flex; align-items: center;">
|
532 | <div style="width: 60px; margin-right: 10px;"></div>
|
533 | <input type=submit value=Sell name=submit class=submit-button>
|
534 | </label>
|
535 | </form>
|
536 | `;
|
537 | const formEl = modalEl.querySelector('.form');
|
538 | const quantityEl = modalEl.querySelector('.quantity');
|
539 | const valueEl = modalEl.querySelector('.value');
|
540 | const submitButtonEl = modalEl.querySelector('.submit-button');
|
541 |
|
542 | quantityEl.max = assetSpec.assets.length;
|
543 |
|
544 | formEl.addEventListener('submit', e => {
|
545 | const quantity = parseInt(quantityEl.value, 10);
|
546 | const value = parseInt(valueEl.value, 10);
|
547 |
|
548 | if (quantity > 0 && quantity <= assetSpec.assets.length && value > 0) {
|
549 | submitButtonEl.oldValue = submitButtonEl.value;
|
550 | submitButtonEl.value = 'Selling...';
|
551 | submitButtonEl.disabled = true;
|
552 |
|
553 | const removedAssetSpecs = assetSpec.assets.slice(assetSpec.assets.length - quantity);
|
554 | const id = _makeId();
|
555 | const sale = {
|
556 | id,
|
557 | seller: uid,
|
558 | asset: assetSpec.asset,
|
559 | value,
|
560 | assets: removedAssetSpecs,
|
561 | };
|
562 | vrid.setRaw('sales/' + id, sale)
|
563 | .then(() => vrid.get('assets'))
|
564 | .then(globalAssets => {
|
565 | globalAssets = globalAssets.filter(assetSpec => !removedAssetSpecs.find(assetSpec2 => assetSpec2.id === assetSpec.id));
|
566 | return vrid.set('assets', globalAssets);
|
567 | })
|
568 | .then(() => {
|
569 | assetSpec.assets = assetSpec.assets.filter(assetSpec => !removedAssetSpecs.find(assetSpec2 => assetSpec2.id === assetSpec.id));
|
570 | if (assetSpec.assets.length === 0) {
|
571 | assets = assets.slice();
|
572 | assets.splice(assets.indexOf(assetSpec), 1);
|
573 | }
|
574 | _renderAssets(assets, assetsEl, getHtml);
|
575 |
|
576 | modalEl.innerHTML = '';
|
577 | })
|
578 | .catch(err => {
|
579 | console.warn(err);
|
580 |
|
581 | submitButtonEl.value = submitButtonEl.oldValue;
|
582 | submitButtonEl.disabled = false;
|
583 | });
|
584 | } else {
|
585 | console.warn('invalid quantity');
|
586 | }
|
587 |
|
588 | e.preventDefault();
|
589 | });
|
590 | }
|
591 |
|
592 | e.preventDefault();
|
593 | e.stopPropagation();
|
594 | });
|
595 | }
|
596 | const exportLinkEl = assetEl.querySelector('.export-link');
|
597 | if (exportLinkEl) {
|
598 | exportLinkEl.addEventListener('click', e => {
|
599 | const assetSpec = assets.find(assetSpec => assetSpec.id === id);
|
600 | if (assetSpec) {
|
601 | modalEl.innerHTML = `\
|
602 | <form class=form style="background-color: #FFF; width: 400px; padding: 30px;">
|
603 | <div style="margin-bottom: 10px; font-size: 20px; font-weight: 400;">Export <span style="font-weight: 600;">${assetSpec.asset}</span>?</div>
|
604 | <div style="margin-bottom: 10px; color: #F44336; font-size: 13px; font-weight: 400;">Warning: Exporting an item removes it from your inventory.</div>
|
605 | <div style="margin-bottom: 10px; font-size: 13px; font-weight: 400;">Drag and drop the downloaded file into the inventory window to import.</div>
|
606 | <div style="display: flex; flex-direction: column;">
|
607 | <div style="margin: 0; margin-top: 20px; margin: 10px 0; font-size: 13px; font-weight: 700; text-transform: uppercase;">Quantity</div>
|
608 | <input type=number value=1 min=1 placeholder=1 class=quantity style="width: 100px; margin-bottom: 20px; background-color: #EEE; border: 0; border-radius: 5px; outline: none;">
|
609 | </div>
|
610 | <div style="display: flex;">
|
611 | <input type=submit value="Export item" name=submit style="margin-right: 15px;" class=submit-button>
|
612 | <input type=button value="Cancel" name=cancel class=cancel-button>
|
613 | </div>
|
614 | </form>
|
615 | `;
|
616 | const formEl = modalEl.querySelector('.form');
|
617 | const quantityEl = modalEl.querySelector('.quantity');
|
618 | const submitButtonEl = modalEl.querySelector('.submit-button');
|
619 |
|
620 | quantityEl.max = assetSpec.assets.length;
|
621 |
|
622 | formEl.addEventListener('submit', e => {
|
623 | const quantity = quantityEl.value;
|
624 | if (quantity > 0 && quantity <= assetSpec.assets.length) {
|
625 | submitButtonEl.oldValue = submitButtonEl.value;
|
626 | submitButtonEl.value = 'Exporting...';
|
627 | submitButtonEl.disabled = true;
|
628 |
|
629 | const removedAssetSpecs = assetSpec.assets.slice(assetSpec.assets.length - quantity);
|
630 | |
631 |
|
632 |
|
633 |
|
634 |
|
635 |
|
636 |
|
637 | const json = {
|
638 | _zeo_item: true,
|
639 | assets: removedAssetSpecs,
|
640 | };
|
641 | const s = JSON.stringify(json, null, 2);
|
642 | const blob = new Blob([s], {
|
643 | type: 'application/json',
|
644 | });
|
645 | const url = URL.createObjectURL(blob);
|
646 | const a = document.createElement('a');
|
647 | a.href = url;
|
648 | a.download = `${assetSpec.json ? assetSpec.json.name : asset}.json`;
|
649 | a.click();
|
650 | URL.revokeObjectURL(url);
|
651 |
|
652 |
|
653 | Promise.all(removedAssetSpecs.map(assetSpec =>
|
654 | vrid.removeRaw('burn/' + assetSpec.certificate[0].signature)
|
655 | ))
|
656 | .then(() => vrid.get('assets'))
|
657 | .then(globalAssets => {
|
658 | globalAssets = globalAssets.filter(assetSpec => !removedAssetSpecs.some(assetSpec2 => assetSpec2.id === assetSpec.id));
|
659 | return vrid.set('assets', globalAssets);
|
660 | })
|
661 | .then(() => {
|
662 | assetSpec.assets = assetSpec.assets.filter(assetSpec => !removedAssetSpecs.find(assetSpec2 => assetSpec2.id === assetSpec.id));
|
663 | if (assetSpec.assets.length === 0) {
|
664 | assets = assets.slice();
|
665 | assets.splice(assets.indexOf(assetSpec), 1);
|
666 | }
|
667 | _renderAssets(assets, assetsEl, getHtml);
|
668 |
|
669 | modalEl.innerHTML = '';
|
670 | })
|
671 | .catch(err => {
|
672 | console.warn(err);
|
673 |
|
674 | submitButtonEl.value = submitButtonEl.oldValue;
|
675 | submitButtonEl.disabled = false;
|
676 | });
|
677 | } else {
|
678 | console.warn('invalid quantity');
|
679 | }
|
680 |
|
681 | e.preventDefault();
|
682 | });
|
683 | const cancelButtonEl = modalEl.querySelector('.cancel-button');
|
684 | cancelButtonEl.addEventListener('click', e => {
|
685 | modalEl.innerHTML = '';
|
686 | });
|
687 | }
|
688 |
|
689 | e.preventDefault();
|
690 | e.stopPropagation();
|
691 | });
|
692 | }
|
693 | const removeLinkEl = assetEl.querySelector('.remove-link');
|
694 | if (removeLinkEl) {
|
695 | removeLinkEl.addEventListener('click', e => {
|
696 | vrid.get('assets')
|
697 | .then(globalAssets => {
|
698 | const globalAssetSpec = globalAssets.find(assetSpec => assetSpec.id === id);
|
699 | modalEl.innerHTML = `\
|
700 | <form class=form style="background-color: #FFF; width: 400px; padding: 30px;">
|
701 | <div style="margin-bottom: 10px; font-size: 20px; font-weight: 400;">Discard <span style="font-weight: 600;">${globalAssetSpec.asset}?</span></div>
|
702 | <div style="display: flex; flex-direction: column;">
|
703 | <div style="margin: 0; margin-top: 20px; margin: 10px 0; font-size: 13px; font-weight: 700; text-transform: uppercase;">Quantity</div>
|
704 | <input type=number value=1 min=1 placeholder=1 class=quantity style="width: 100px; margin-bottom: 20px; background-color: #EEE; border: 0; border-radius: 5px; outline: none;">
|
705 | </div>
|
706 | <input type=submit value=Discard item name=submit class=submit-button>
|
707 | </form>
|
708 | `;
|
709 |
|
710 | const formEl = modalEl.querySelector('.form');
|
711 | const quantityEl = modalEl.querySelector('.quantity');
|
712 | const submitButtonEl = modalEl.querySelector('.submit-button');
|
713 |
|
714 | quantityEl.max = assetSpec.assets.length;
|
715 |
|
716 | formEl.addEventListener('submit', e => {
|
717 | const quantity = quantityEl.value;
|
718 | if (quantity > 0 && quantity <= assetSpec.assets.length) {
|
719 | submitButtonEl.oldValue = submitButtonEl.value;
|
720 | submitButtonEl.value = 'Discarding...';
|
721 | submitButtonEl.disabled = true;
|
722 |
|
723 | const removedAssetSpecs = assetSpec.assets.slice(assetSpec.assets.length - quantity);
|
724 |
|
725 | globalAssets = globalAssets.filter(assetSpec => !removedAssetSpecs.some(assetSpec2 => assetSpec2.id === assetSpec.id));
|
726 |
|
727 | vrid.set('assets', globalAssets)
|
728 | .then(() => {
|
729 | assetSpec.assets = assetSpec.assets.filter(assetSpec => !removedAssetSpecs.find(assetSpec2 => assetSpec2.id === assetSpec.id));
|
730 | if (assetSpec.assets.length === 0) {
|
731 | assets = assets.slice();
|
732 | assets.splice(assets.indexOf(assetSpec), 1);
|
733 | }
|
734 | _renderAssets(assets, assetsEl, getHtml);
|
735 |
|
736 | modalEl.innerHTML = '';
|
737 | })
|
738 | .catch(err => {
|
739 | console.warn(err);
|
740 |
|
741 | submitButtonEl.value = submitButtonEl.oldValue;
|
742 | submitButtonEl.disabled = false;
|
743 | });
|
744 | } else {
|
745 | console.warn('invalid quantity');
|
746 | }
|
747 |
|
748 | e.preventDefault();
|
749 | });
|
750 | });
|
751 |
|
752 | e.preventDefault();
|
753 | e.stopPropagation();
|
754 | });
|
755 | }
|
756 | |
757 |
|
758 |
|
759 |
|
760 |
|
761 |
|
762 |
|
763 |
|
764 |
|
765 |
|
766 |
|
767 |
|
768 |
|
769 |
|
770 |
|
771 |
|
772 |
|
773 |
|
774 |
|
775 |
|
776 |
|
777 |
|
778 |
|
779 |
|
780 |
|
781 |
|
782 |
|
783 |
|
784 |
|
785 |
|
786 |
|
787 |
|
788 |
|
789 |
|
790 |
|
791 |
|
792 |
|
793 |
|
794 |
|
795 |
|
796 |
|
797 |
|
798 |
|
799 |
|
800 |
|
801 |
|
802 |
|
803 |
|
804 |
|
805 |
|
806 |
|
807 |
|
808 |
|
809 |
|
810 |
|
811 |
|
812 |
|
813 |
|
814 |
|
815 |
|
816 |
|
817 |
|
818 |
|
819 |
|
820 |
|
821 |
|
822 |
|
823 |
|
824 |
|
825 |
|
826 |
|
827 |
|
828 |
|
829 |
|
830 |
|
831 |
|
832 |
|
833 |
|
834 |
|
835 |
|
836 |
|
837 |
|
838 |
|
839 |
|
840 |
|
841 |
|
842 |
|
843 |
|
844 |
|
845 |
|
846 |
|
847 |
|
848 |
|
849 |
|
850 |
|
851 |
|
852 |
|
853 |
|
854 |
|
855 |
|
856 |
|
857 |
|
858 | if (!sale) {
|
859 | assetEl.tabIndex = -1;
|
860 | assetEl.draggable = true;
|
861 | assetEl.assetName = asset;
|
862 | assetEl.unselect = () => {
|
863 | assetEl.classList.remove('selected');
|
864 |
|
865 | const infoboxCanvasEl = _findParentNode(assetsEl, '.infobox-canvas');
|
866 | assetEl.removeCanvas(infoboxCanvasEl);
|
867 |
|
868 |
|
869 |
|
870 | };
|
871 | assetEl.addEventListener('click', e => {
|
872 | assetEl.focus();
|
873 |
|
874 | e.preventDefault();
|
875 | });
|
876 | assetEl.addEventListener('click', () => {
|
877 | if (selectedTarget) {
|
878 | selectedTarget.unselect();
|
879 | selectedTarget = null;
|
880 | }
|
881 |
|
882 | assetEl.classList.add('selected');
|
883 |
|
884 | const infoboxCanvasEl = _findParentNode(assetsEl, '.infobox-canvas');
|
885 | assetEl.addCanvas(infoboxCanvasEl);
|
886 |
|
887 |
|
888 |
|
889 |
|
890 | const infoboxAssetEl = _findParentNode(assetsEl, '.infobox-asset');
|
891 | infoboxAssetEl.innerText = asset;
|
892 | const infoboxDescriptionEl = _findParentNode(assetsEl, '.infobox-description');
|
893 | infoboxDescriptionEl.innerText = description;
|
894 |
|
895 | selectedTarget = assetEl;
|
896 | });
|
897 | assetEl.addEventListener('dragstart', e => {
|
898 | e.dataTransfer.setData('text/plain', asset);
|
899 | });
|
900 | assetEl.addEventListener('dragend', e => {
|
901 | if (dragTarget) {
|
902 | if (dragTarget.tagName === 'EQUIPMENT') {
|
903 | dragTarget.setAssetSpec(assetSpec);
|
904 | }
|
905 | }
|
906 |
|
907 | dragTarget = null;
|
908 | });
|
909 | assetEl.addEventListener('dragover', e => {
|
910 | e.preventDefault();
|
911 | });
|
912 | assetEl.addEventListener('dragenter', e => {
|
913 | requestAnimationFrame(() => {
|
914 | dragTarget = assetEl;
|
915 | });
|
916 | });
|
917 | assetEl.addEventListener('dragleave', e => {
|
918 | if (dragTarget === assetEl) {
|
919 | dragTarget = null;
|
920 | }
|
921 | });
|
922 | }
|
923 |
|
924 | let live = true;
|
925 | assetEl.destroy = () => {
|
926 | live = false;
|
927 | };
|
928 | const _requestIconImg = () => {
|
929 | if (icon) {
|
930 | return new Promise((accept, reject) => {
|
931 | const img = new Image();
|
932 | img.onload = () => {
|
933 | accept(img);
|
934 | };
|
935 | img.onerror = err => {
|
936 | reject(err);
|
937 | };
|
938 | img.crossOrigin = 'Anonymous';
|
939 | img.src = 'data:application/octet-stream;base64,' + icon;
|
940 | });
|
941 | } else {
|
942 | const {type} = _getAssetType(asset);
|
943 | return type === 'mod' ? _getModImage() : _getQuestionMarkImage();
|
944 | }
|
945 | };
|
946 | _requestIconImg()
|
947 | .then(img => {
|
948 | if (live) {
|
949 | const spriteRenderer = new SpriteRenderer(img, assetEl.querySelector('canvas'));
|
950 |
|
951 | assetEl.destroy = () => {
|
952 | spriteRenderer.destroy();
|
953 |
|
954 | if (selectedTarget === assetEl) {
|
955 | selectedTarget.unselect();
|
956 | selectedTarget = null;
|
957 | }
|
958 | };
|
959 | assetEl.addCanvas = canvas => {
|
960 | spriteRenderer.addCanvas(canvas);
|
961 | };
|
962 | assetEl.removeCanvas = canvas => {
|
963 | spriteRenderer.removeCanvas(canvas);
|
964 | };
|
965 | }
|
966 | })
|
967 | .catch(err => {
|
968 | if (live) {
|
969 | console.warn(err);
|
970 | }
|
971 | });
|
972 |
|
973 | assetsEl.appendChild(assetEl);
|
974 | }
|
975 |
|
976 | const emptyEl = $$(getHtml(null));
|
977 | assetsEl.appendChild(emptyEl);
|
978 | });
|
979 | };
|
980 |
|
981 | const _loadBuyables = (storeMode, storeLink, storeIndexType) => {
|
982 | let el;
|
983 | let getHtml;
|
984 | let buyables;
|
985 | if (storeMode === 1) {
|
986 | el = document.getElementById('store-buyables')
|
987 | getHtml = _getBuyableStoreHtml;
|
988 |
|
989 | if (storeLink === 1) {
|
990 | buyables = sales
|
991 |
|
992 | .map(itemSpec => {
|
993 | const {type, name} = _getAssetType(itemSpec.asset);
|
994 | if (type === 'item') {
|
995 | itemSpec.icon = items[name].icon;
|
996 | } else if (type === 'mod') {
|
997 | const mod = mods.find(modSpec => modSpec.name === name) || null;
|
998 | equipmentSpec.icon = mod ? mod.icon : null;
|
999 | } else if (type === 'skin' || type === 'file') {
|
1000 | itemSpec.icon = items[type].icon;
|
1001 | }
|
1002 | return itemSpec;
|
1003 | });
|
1004 | } else if (storeLink === 2) {
|
1005 | buyables = sales
|
1006 | .filter(itemSpec => _getAssetType(itemSpec.asset).type === 'item')
|
1007 |
|
1008 | .map(itemSpec => {
|
1009 | const {type, name} = _getAssetType(itemSpec.asset);
|
1010 | if (type === 'item') {
|
1011 | itemSpec.icon = items[name].icon;
|
1012 | } else if (type === 'mod') {
|
1013 | const mod = mods.find(modSpec => modSpec.name === name) || null;
|
1014 | equipmentSpec.icon = mod ? mod.icon : null;
|
1015 | } else if (type === 'skin' || type === 'file') {
|
1016 | itemSpec.icon = items[type].icon;
|
1017 | }
|
1018 | return itemSpec;
|
1019 | });
|
1020 | } else if (storeLink === 3) {
|
1021 | buyables = sales
|
1022 | .filter(itemSpec => _getAssetType(itemSpec.asset).type === 'mod')
|
1023 |
|
1024 | .map(itemSpec => {
|
1025 | const {type, name} = _getAssetType(itemSpec.asset);
|
1026 | if (type === 'item') {
|
1027 | itemSpec.icon = items[name].icon;
|
1028 | } else if (type === 'mod') {
|
1029 | const mod = mods.find(modSpec => modSpec.name === name) || null;
|
1030 | equipmentSpec.icon = mod ? mod.icon : null;
|
1031 | } else if (type === 'skin' || type === 'file') {
|
1032 | itemSpec.icon = items[type].icon;
|
1033 | }
|
1034 | return itemSpec;
|
1035 | });
|
1036 | } else if (storeLink === 4) {
|
1037 | buyables = sales
|
1038 | .filter(itemSpec => _getAssetType(itemSpec.asset).type === 'skin')
|
1039 |
|
1040 | .map(itemSpec => {
|
1041 | const {type, name} = _getAssetType(itemSpec.asset);
|
1042 | if (type === 'item') {
|
1043 | itemSpec.icon = items[name].icon;
|
1044 | } else if (type === 'mod') {
|
1045 | const mod = mods.find(modSpec => modSpec.name === name) || null;
|
1046 | equipmentSpec.icon = mod ? mod.icon : null;
|
1047 | } else if (type === 'skin' || type === 'file') {
|
1048 | itemSpec.icon = items[type].icon;
|
1049 | }
|
1050 | return itemSpec;
|
1051 | });
|
1052 | } else if (storeLink === 5) {
|
1053 | buyables = sales
|
1054 | .filter(itemSpec => _getAssetType(itemSpec.asset).type === 'file')
|
1055 |
|
1056 | .map(itemSpec => {
|
1057 | const {type, name} = _getAssetType(itemSpec.asset);
|
1058 | if (type === 'item') {
|
1059 | itemSpec.icon = items[name].icon;
|
1060 | } else if (type === 'mod') {
|
1061 | const mod = mods.find(modSpec => modSpec.name === name) || null;
|
1062 | equipmentSpec.icon = mod ? mod.icon : null;
|
1063 | } else if (type === 'skin' || type === 'file') {
|
1064 | itemSpec.icon = items[type].icon;
|
1065 | }
|
1066 | return itemSpec;
|
1067 | });
|
1068 | }
|
1069 | } else if (storeMode === 2) {
|
1070 | el = document.getElementById('index-buyables')
|
1071 | getHtml = _getBuyableIndexHtml;
|
1072 |
|
1073 | if (storeLink === 1) {
|
1074 | buyables = mods
|
1075 | .map(_normalizeMod)
|
1076 | .concat(
|
1077 | Object.keys(items)
|
1078 | .map(k => items[k])
|
1079 | .map(_normalizeItem)
|
1080 | .map(itemSpec => {
|
1081 | const {type} = _getAssetType(itemSpec.asset);
|
1082 | if (type === 'skin' || type === 'file') {
|
1083 | itemSpec.icon = items[type].icon;
|
1084 | }
|
1085 | return itemSpec;
|
1086 | })
|
1087 | );
|
1088 | } else if (storeLink === 2) {
|
1089 | buyables = Object.keys(items)
|
1090 | .map(k => items[k])
|
1091 | .filter(itemSpec => _getAssetType(itemSpec.asset).type === 'item')
|
1092 | .map(_normalizeItem)
|
1093 | .map(itemSpec => {
|
1094 | const {type} = _getAssetType(itemSpec.asset);
|
1095 | if (type === 'skin' || type === 'file') {
|
1096 | itemSpec.icon = items[type].icon;
|
1097 | }
|
1098 | return itemSpec;
|
1099 | });
|
1100 | } else if (storeLink === 3) {
|
1101 | buyables = mods
|
1102 | .map(_normalizeMod)
|
1103 | .map(itemSpec => {
|
1104 | const {type} = _getAssetType(itemSpec.asset);
|
1105 | if (type === 'skin' || type === 'file') {
|
1106 | itemSpec.icon = items[type].icon;
|
1107 | }
|
1108 | return itemSpec;
|
1109 | });
|
1110 | } else if (storeLink === 4) {
|
1111 | buyables = Object.keys(items)
|
1112 | .map(k => items[k])
|
1113 | .filter(itemSpec => _getAssetType(itemSpec.asset).type === 'skin')
|
1114 | .map(_normalizeItem)
|
1115 | .map(itemSpec => {
|
1116 | const {type} = _getAssetType(itemSpec.asset);
|
1117 | if (type === 'skin' || type === 'file') {
|
1118 | itemSpec.icon = items[type].icon;
|
1119 | }
|
1120 | return itemSpec;
|
1121 | });
|
1122 | } else if (storeLink === 5) {
|
1123 | buyables = Object.keys(items)
|
1124 | .map(k => items[k])
|
1125 | .filter(itemSpec => _getAssetType(itemSpec.asset).type === 'file')
|
1126 | .map(_normalizeItem)
|
1127 | .map(itemSpec => {
|
1128 | const {type} = _getAssetType(itemSpec.asset);
|
1129 | if (type === 'skin' || type === 'file') {
|
1130 | itemSpec.icon = items[type].icon;
|
1131 | }
|
1132 | return itemSpec;
|
1133 | });
|
1134 | }
|
1135 | }
|
1136 |
|
1137 | if (storeIndexType === 2) {
|
1138 | buyables = buyables
|
1139 | .filter(item => item.owner === uid);
|
1140 | }
|
1141 |
|
1142 | _renderBuyables(buyables, el, getHtml);
|
1143 | };
|
1144 | const _getBuyableStoreHtml = saleSpec => {
|
1145 | const {id, asset, seller, value, sellerName} = saleSpec;
|
1146 | const {type, name} = _getAssetType(asset);
|
1147 | return `\
|
1148 | <buyable class="${type}" style="display: flex; width: 200px; margin: 20px; background-color: #FFF; border-radius: 5px; flex-direction: column; overflow: hidden;">
|
1149 | <div style="position: relative; display: flex; height: 40px; padding: 10px; box-shadow: 0 1px 5px rgba(0,0,0,0.1); font-size: 11px; align-items: center; text-transform: uppercase;">
|
1150 | <div class=pill style="display: inline-flex; margin-right: 10px; padding: 2px 5px; color: #FFF; align-items: center;">${_capitalize(type)}</div>
|
1151 | <div style="display: flex; flex-grow: 1; align-items: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
1152 | Posted by ${sellerName}
|
1153 | </div>
|
1154 | </div>
|
1155 | <div style="position: relative; display: flex; width: 200px; height: 130px; border-radius: 5px; box-shadow: 0 1px 5px rgba(0,0,0,0.1); flex-direction: column; justify-content: center; align-items: center;">
|
1156 | <canvas width=80 height=80></canvas>
|
1157 | <div style="display: flex; height: 50px; font-size: 16px; justify-content: center; align-items: center;">${name}</div>
|
1158 | </div>
|
1159 | <div style="display: flex; height: 60px; padding: 10px 0; justify-content: space-evenly; align-items: center; text-transform: uppercase;">
|
1160 | <div style="display: flex; flex-direction: column;">
|
1161 | <div style="display: flex; color: #909090; font-size: 11px; justify-content: center; align-items: center;">Price</div>
|
1162 | <div style="display: flex; flex-grow: 1; font-size: 15px; justify-content: center; align-items: center;">${value} ☄</div>
|
1163 | </div>
|
1164 | <div style="display: flex; flex-direction: column;">
|
1165 | <div style="display: flex; color: #909090; font-size: 11px; justify-content: center; align-items: center;">Qty</div>
|
1166 | <div style="display: flex; flex-grow: 1; font-size: 15px; justify-content: center; align-items: center;">
|
1167 | <nav class=decrease-quantity style="cursor: pointer; user-select: none;">
|
1168 | <svg fill="#000000" height="16" viewBox="0 0 24 24" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"/><path d="M0-.5h24v24H0z" fill="none"/></svg>
|
1169 | </nav>
|
1170 | <div style="padding: 0 5px;"><span class=quantity>1</span>/${saleSpec.assets.length}</div>
|
1171 | <nav class=increase-quantity style="cursor: pointer; user-select: none;">
|
1172 | <svg fill="#000000" height="16" viewBox="0 0 24 24" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"/><path d="M0-.25h24v24H0z" fill="none"/></svg>
|
1173 | </nav>
|
1174 | </div>
|
1175 | </div>
|
1176 | <div style="display: flex; flex-direction: column;">
|
1177 | <div style="display: flex; color: #909090; font-size: 11px; justify-content: center; align-items: center;">Total</div>
|
1178 | <div style="display: flex; flex-grow: 1; font-size: 15px; justify-content: center; align-items: center;"><span class=total>${value}</span> ☄</div>
|
1179 | </div>
|
1180 | </div>
|
1181 | <div class=buy-button style="display: flex; height: 40px; padding: 10px; color: #FFF; font-size: 14px; justify-content: center; align-items: center; text-transform: uppercase;">Buy</div>
|
1182 | </buyable>
|
1183 | `;
|
1184 | };
|
1185 | const _getBuyableIndexHtml = assetSpec => {
|
1186 | const {id, asset, quantity, icon, description, owner, file, json, certificate, sale} = assetSpec;
|
1187 | const {type, name} = _getAssetType(asset);
|
1188 | return `\
|
1189 | <buyable class="${type}" style="display: flex; width: 100%; height: 50px; border-radius: 3px; align-items: center; white-space: nowrap; cursor: pointer;">
|
1190 | <canvas width=40 height=40 style="margin: 0 10px;"></canvas>
|
1191 | <div style="padding: 0 10px; flex: 1; overflow: hidden; text-overflow: ellipsis;">${name}</div>
|
1192 | <div style="padding: 0 10px; flex: 2; overflow: hidden; text-overflow: ellipsis;">${description}</div>
|
1193 | <div style="padding: 0 10px; flex: 1; overflow: hidden; text-overflow: ellipsis;">${owner}</div>
|
1194 | <!-- <div style="padding: 0 10px; flex: 1; overflow: hidden; text-overflow: ellipsis;">${formatRelative(Date.now() - 100000000, new Date())}</div> -->
|
1195 | <div style="padding: 0 10px; flex: 1; overflow: hidden; text-overflow: ellipsis;">$${10}</div>
|
1196 | ${owner === uid ? `\
|
1197 | <nav class=materialize-button style="padding: 0 10px; flex: 1; overflow: hidden; text-overflow: ellipsis; cursor: pointer;">
|
1198 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24" fill="#4CAF50"><path d="M16,13V11H21V13H16M14.83,7.76L17.66,4.93L19.07,6.34L16.24,9.17L14.83,7.76M11,16H13V21H11V16M11,3H13V8H11V3M4.93,17.66L7.76,14.83L9.17,16.24L6.34,19.07L4.93,17.66M4.93,6.34L6.34,4.93L9.17,7.76L7.76,9.17L4.93,6.34M8,13H3V11H8V13M19.07,17.66L17.66,19.07L14.83,16.24L16.24,14.83L19.07,17.66Z" /></svg>
|
1199 | </nav>
|
1200 | <nav class=unpublish-button style="padding: 0 10px; flex: 1; overflow: hidden; text-overflow: ellipsis; cursor: pointer;">
|
1201 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24" fill="#fe4242"><path d="M21.03,3L18,20.31C17.83,21.27 17,22 16,22H8C7,22 6.17,21.27 6,20.31L2.97,3H21.03M5.36,5L8,20H16L18.64,5H5.36M9,18V14H13V18H9M13,13.18L9.82,10L13,6.82L16.18,10L13,13.18Z" /></svg>
|
1202 | </nav>
|
1203 | ` : `\
|
1204 | <nav style="padding: 0 10px; flex: 1; overflow: hidden; text-overflow: ellipsis; cursor: pointer;"></nav>
|
1205 | <nav style="padding: 0 10px; flex: 1; overflow: hidden; text-overflow: ellipsis; cursor: pointer;"></nav>
|
1206 | `}
|
1207 | </buyable>
|
1208 | `;
|
1209 | };
|
1210 | const infoboxTypes = [
|
1211 | 'item',
|
1212 | 'mod',
|
1213 | 'skin',
|
1214 | 'file',
|
1215 | ];
|
1216 | const _renderBuyables = (assets, assetsEl, getHtml) => {
|
1217 | while (assetsEl.firstChild) {
|
1218 | assetsEl.firstChild.destroy && assetsEl.firstChild.destroy();
|
1219 | assetsEl.removeChild(assetsEl.firstChild);
|
1220 | }
|
1221 |
|
1222 | if (assets.length > 0) {
|
1223 | _requestFilterVerifiedAssets(assets)
|
1224 | .then(assets => {
|
1225 | for (let i = 0; i < assets.length; i++) {
|
1226 | const assetSpec = assets[i];
|
1227 | const {id, asset, icon, description, owner, file, json, certificate} = assetSpec;
|
1228 | const {type, name} = _getAssetType(asset);
|
1229 | const assetEl = $$(getHtml(assetSpec));
|
1230 |
|
1231 | assetEl.tabIndex = -1;
|
1232 | assetEl.assetName = asset;
|
1233 | assetEl.unselect = () => {
|
1234 | assetEl.classList.remove('selected');
|
1235 |
|
1236 | const infoboxCanvasEl = _findParentNode(assetsEl, '.infobox-canvas');
|
1237 | assetEl.removeCanvas(infoboxCanvasEl);
|
1238 | const infoboxTypeWrapEl = _findParentNode(assetsEl, '.infobox-type-wrap');
|
1239 | for (let i = 0; i < infoboxTypes.length; i++) {
|
1240 | infoboxTypeWrapEl.classList.remove(infoboxTypes[i]);
|
1241 | }
|
1242 | const infoboxAssetEl = _findParentNode(assetsEl, '.infobox-asset');
|
1243 | infoboxAssetEl.innerText = '';
|
1244 | const infoboxDescriptionEl = _findParentNode(assetsEl, '.infobox-description');
|
1245 | infoboxDescriptionEl.innerText = '';
|
1246 |
|
1247 |
|
1248 |
|
1249 | };
|
1250 | assetEl.addEventListener('click', e => {
|
1251 | assetEl.focus();
|
1252 |
|
1253 | e.preventDefault();
|
1254 | });
|
1255 | assetEl.addEventListener('click', () => {
|
1256 | if (selectedTarget) {
|
1257 | selectedTarget.unselect();
|
1258 | selectedTarget = null;
|
1259 | }
|
1260 |
|
1261 | assetEl.classList.add('selected');
|
1262 |
|
1263 | const infoboxTypeEl = _findParentNode(assetsEl, '.infobox-type');
|
1264 | infoboxTypeEl.innerText = type;
|
1265 | const infoboxTypeWrapEl = _findParentNode(assetsEl, '.infobox-type-wrap');
|
1266 | for (let i = 0; i < infoboxTypes.length; i++) {
|
1267 | infoboxTypeWrapEl.classList.remove(infoboxTypes[i]);
|
1268 | }
|
1269 | infoboxTypeWrapEl.classList.add(type);
|
1270 |
|
1271 | const infoboxCanvasEl = _findParentNode(assetsEl, '.infobox-canvas');
|
1272 | assetEl.addCanvas(infoboxCanvasEl);
|
1273 |
|
1274 |
|
1275 |
|
1276 |
|
1277 | const infoboxAssetEl = _findParentNode(assetsEl, '.infobox-asset');
|
1278 | infoboxAssetEl.innerText = asset;
|
1279 | const infoboxDescriptionEl = _findParentNode(assetsEl, '.infobox-description');
|
1280 | infoboxDescriptionEl.innerText = description;
|
1281 |
|
1282 | selectedTarget = assetEl;
|
1283 | });
|
1284 |
|
1285 | let live = true;
|
1286 | assetEl.destroy = () => {
|
1287 | live = false;
|
1288 | };
|
1289 | const _requestIconImg = () => {
|
1290 | if (icon) {
|
1291 | return new Promise((accept, reject) => {
|
1292 | const img = new Image();
|
1293 | img.onload = () => {
|
1294 | accept(img);
|
1295 | };
|
1296 | img.onerror = err => {
|
1297 | reject(err);
|
1298 | };
|
1299 | img.crossOrigin = 'Anonymous';
|
1300 | img.src = 'data:application/octet-stream;base64,' + icon;
|
1301 | });
|
1302 | } else {
|
1303 | const {type} = _getAssetType(asset);
|
1304 | return type === 'mod' ? _getModImage() : _getQuestionMarkImage();
|
1305 | }
|
1306 | };
|
1307 | _requestIconImg()
|
1308 | .then(img => {
|
1309 | if (live) {
|
1310 | const spriteRenderer = new SpriteRenderer(img, assetEl.querySelector('canvas'));
|
1311 |
|
1312 | assetEl.destroy = () => {
|
1313 | spriteRenderer.destroy();
|
1314 |
|
1315 | if (selectedTarget === assetEl) {
|
1316 | selectedTarget.unselect();
|
1317 | selectedTarget = null;
|
1318 | }
|
1319 | };
|
1320 | assetEl.addCanvas = canvas => {
|
1321 | spriteRenderer.addCanvas(canvas);
|
1322 | };
|
1323 | assetEl.removeCanvas = canvas => {
|
1324 | spriteRenderer.removeCanvas(canvas);
|
1325 | };
|
1326 | }
|
1327 | })
|
1328 | .catch(err => {
|
1329 | if (live) {
|
1330 | console.warn(err);
|
1331 | }
|
1332 | });
|
1333 |
|
1334 | const materializeButtonEl = assetEl.querySelector('.materialize-button');
|
1335 | if (materializeButtonEl) {
|
1336 | materializeButtonEl.addEventListener('click', e => {
|
1337 | modalEl.innerHTML = `\
|
1338 | <form class="form materialize-item-form" style="display: flex; background-color: #FFF; width: 400px; padding: 30px; flex-direction: column;" id=materialize-item-form>
|
1339 | <div style="margin-bottom: 10px; font-size: 20px; font-weight: 400;">Materialize <span style="font-weight: 600;">${asset}</span></div>
|
1340 | <div style="display: flex; flex-direction: column;">
|
1341 | <div style="margin: 0; margin-top: 20px; margin: 10px 0; font-size: 13px; font-weight: 700; text-transform: uppercase;">Quantity</div>
|
1342 | <input type=number value=1 placeholder=0 style="width: 100px; margin-bottom: 20px; background-color: #EEE; border: 0; border-radius: 5px; outline: none;" id=materialize-item-quantity>
|
1343 | <input type=submit value="Create" class=materialize-item-create-button style="margin-right: auto;">
|
1344 | <input type=button value="Creating..." class=materialize-item-creating-button style="margin-right: auto;">
|
1345 | </div>
|
1346 | </form>
|
1347 | `;
|
1348 |
|
1349 | const materializeItemFormEl = document.getElementById('materialize-item-form');
|
1350 | const materializeItemQuantityEl = document.getElementById('materialize-item-quantity');
|
1351 | materializeItemFormEl.addEventListener('submit', e => {
|
1352 | document.body.classList.add('materializing-item');
|
1353 |
|
1354 | const asset = selectedTarget.assetName;
|
1355 | const quantity = parseInt(materializeItemQuantityEl.value, 10);
|
1356 | const _requestNewAssets = () => {
|
1357 | const promises = Array(quantity);
|
1358 | for (let i = 0; i < quantity; i++) {
|
1359 | const assetSpec = {
|
1360 | id: _makeId(),
|
1361 | asset,
|
1362 | timestamp: Date.now(),
|
1363 | };
|
1364 | promises[i] = _signAsset(assetSpec);
|
1365 | }
|
1366 | return Promise.all(promises);
|
1367 | };
|
1368 |
|
1369 | Promise.all([
|
1370 | vrid.get('assets'),
|
1371 | _requestNewAssets(),
|
1372 | ])
|
1373 | .then(([
|
1374 | assets,
|
1375 | newAssets,
|
1376 | ]) => {
|
1377 | assets = assets || [];
|
1378 | assets = assets.concat(newAssets);
|
1379 |
|
1380 | return vrid.set('assets', assets)
|
1381 | .then(() => {
|
1382 | document.getElementById('inventory-link-2').click();
|
1383 |
|
1384 | _pushUrl('/');
|
1385 |
|
1386 | document.body.classList.remove('materializing-item');
|
1387 | modalEl.innerHTML = '';
|
1388 | });
|
1389 | })
|
1390 | .catch(err => {
|
1391 | console.warn(err);
|
1392 |
|
1393 | document.body.classList.remove('materializing-item');
|
1394 | modalEl.innerHTML = '';
|
1395 | });
|
1396 |
|
1397 | e.preventDefault();
|
1398 | });
|
1399 | });
|
1400 | }
|
1401 | const unpublishButtonEl = assetEl.querySelector('.unpublish-button');
|
1402 | if (unpublishButtonEl) {
|
1403 | unpublishButtonEl.addEventListener('click', e => {
|
1404 | modalEl.innerHTML = `\
|
1405 | <form class="form unpublish-item-form" style="display: flex; background-color: #FFF; width: 400px; padding: 30px; flex-direction: column;" id=unpublish-item-form>
|
1406 | <div style="margin-bottom: 10px; font-size: 20px; font-weight: 400;">Unpublish <span style="font-weight: 600;">${asset}</span>?</div>
|
1407 | <div style="display: flex; flex-direction: column;">
|
1408 | <input type=submit value="Unpublish" class=unpublish-item-publish-button style="margin-right: auto;">
|
1409 | <input type=button value="Unpublishing..." class=unpublish-item-publishing-button style="margin-right: auto;">
|
1410 | </div>
|
1411 | </form>
|
1412 | `;
|
1413 |
|
1414 | const unpublishItemFormEl = document.getElementById('unpublish-item-form');
|
1415 | unpublishItemFormEl.addEventListener('submit', e => {
|
1416 | document.body.classList.add('unpublishing-item');
|
1417 |
|
1418 | vrid.removeRaw('items/' + id)
|
1419 | .then(() => {
|
1420 | assets = assets.slice();
|
1421 | assets.splice(assets.indexOf(assetSpec), 1);
|
1422 | delete items[id];
|
1423 | _renderBuyables(assets, assetsEl, getHtml);
|
1424 |
|
1425 | document.body.classList.remove('unpublishing-item');
|
1426 | modalEl.innerHTML = '';
|
1427 | })
|
1428 | .catch(err => {
|
1429 | console.warn(err);
|
1430 |
|
1431 | document.body.classList.remove('unpublishing-item');
|
1432 | modalEl.innerHTML = '';
|
1433 | });
|
1434 |
|
1435 | e.preventDefault();
|
1436 | });
|
1437 | });
|
1438 | }
|
1439 | const quantityEl = assetEl.querySelector('.quantity');
|
1440 | const totalEl = assetEl.querySelector('.total');
|
1441 | const increaseQuantityEl = assetEl.querySelector('.increase-quantity');
|
1442 | if (increaseQuantityEl) {
|
1443 | increaseQuantityEl.addEventListener('click', () => {
|
1444 | let quantity = parseInt(quantityEl.innerText, 10);
|
1445 | quantity = Math.min(quantity + 1, assetSpec.assets.length);
|
1446 | quantityEl.innerText = String(quantity);
|
1447 | totalEl.innerText = String(quantity * assetSpec.value);
|
1448 | });
|
1449 | }
|
1450 | const decreaseQuantityEl = assetEl.querySelector('.decrease-quantity');
|
1451 | if (decreaseQuantityEl) {
|
1452 | decreaseQuantityEl.addEventListener('click', () => {
|
1453 | let quantity = parseInt(quantityEl.innerText, 10);
|
1454 | quantity = Math.max(quantity - 1, 1);
|
1455 | quantityEl.innerText = String(quantity);
|
1456 | totalEl.innerText = String(quantity * assetSpec.value);
|
1457 | });
|
1458 | }
|
1459 | const buyButtonEl = assetEl.querySelector('.buy-button');
|
1460 | if (buyButtonEl) {
|
1461 | buyButtonEl.addEventListener('click', e => {
|
1462 | const quantity = parseInt(quantityEl.innerText, 10);
|
1463 |
|
1464 | modalEl.innerHTML = `\
|
1465 | <form class="form buy-item-form" style="display: flex; background-color: #FFF; width: 400px; padding: 30px; flex-direction: column;" id=buy-item-form>
|
1466 | <div style="margin-bottom: 10px; font-size: 20px; font-weight: 400;">Buy <span style="font-weight: 600;">${quantity}</span> <span style="font-weight: 600;">${asset}</span> for <span style="font-weight: 600;">${quantity * assetSpec.value}☄</span>?</div>
|
1467 | <div style="display: flex; flex-direction: column;">
|
1468 | <input type=submit value="Buy" class=submit-button style="margin-right: auto;">
|
1469 | </div>
|
1470 | </form>
|
1471 | `;
|
1472 |
|
1473 | const buyItemFormEl = document.getElementById('buy-item-form');
|
1474 | const submitButtonEl = modalEl.querySelector('.submit-button');
|
1475 | buyItemFormEl.addEventListener('submit', e => {
|
1476 | submitButtonEl.oldValue = submitButtonEl.value;
|
1477 | submitButtonEl.value = 'Buying...';
|
1478 | submitButtonEl.disabled = true;
|
1479 |
|
1480 | const removedAssetSpecs = assetSpec.assets.slice(assetSpec.assets.length - quantity);
|
1481 | const buyId = _makeId();
|
1482 | const buy = {
|
1483 | id: buyId,
|
1484 | sale: id,
|
1485 | buyer: uid,
|
1486 | assets: removedAssetSpecs.map(assetSpec => assetSpec.id),
|
1487 | };
|
1488 | vrid.setRaw('buys/' + buyId, buy)
|
1489 | .then(() => vrid.onNullRaw('buys/' + buyId))
|
1490 | .then(() => vrid.get('assets'))
|
1491 | .then(globalAssets => {
|
1492 | globalAssets = globalAssets.concat(removedAssetSpecs);
|
1493 | return vrid.set('assets', globalAssets);
|
1494 | })
|
1495 | .then(() => vrid.getRaw('credits/' + uid))
|
1496 | .then(newCredits => {
|
1497 | assetSpec.assets = assetSpec.assets.filter(assetSpec => !removedAssetSpecs.find(assetSpec2 => assetSpec2.id === assetSpec.id));
|
1498 | if (assetSpec.assets.length === 0) {
|
1499 | assets = assets.slice();
|
1500 | assets.splice(assets.indexOf(assetSpec), 1);
|
1501 | }
|
1502 | _renderBuyables(assets, assetsEl, getHtml);
|
1503 |
|
1504 | credits = newCredits;
|
1505 | _renderCredits(credits);
|
1506 |
|
1507 | document.body.classList.remove('buying-item');
|
1508 | modalEl.innerHTML = '';
|
1509 | })
|
1510 | .catch(err => {
|
1511 | console.warn(err);
|
1512 |
|
1513 | submitButtonEl.value = submitButtonEl.oldValue;
|
1514 | submitButtonEl.disabled = false;
|
1515 | });
|
1516 |
|
1517 | e.preventDefault();
|
1518 | });
|
1519 | });
|
1520 | }
|
1521 |
|
1522 | assetsEl.appendChild(assetEl);
|
1523 | }
|
1524 | });
|
1525 | } else {
|
1526 | const emptyEl = $$(`\
|
1527 | <div style="display: flex; width: 100%; padding: 100px; font-size: 23px; font-weight: 400; justify-content: center; align-items: center;">Nothing here</div>
|
1528 | `);
|
1529 | assetsEl.appendChild(emptyEl);
|
1530 | }
|
1531 | };
|
1532 |
|
1533 | const avatarCanvasSize = 250;
|
1534 | const _renderAvatars = (avatars, avatarsEl) => {
|
1535 | while (avatarsEl.firstChild) {
|
1536 | avatarsEl.firstChild.destroy && avatarsEl.firstChild.destroy();
|
1537 | avatarsEl.removeChild(avatarsEl.firstChild);
|
1538 | }
|
1539 |
|
1540 | if (avatars.length > 0) {
|
1541 | for (let i = 0; i < avatars.length; i++) {
|
1542 | const avatar = avatars[i];
|
1543 | const {name, subscribed} = avatar;
|
1544 |
|
1545 | const avatarEl = $$(`\
|
1546 | <avatar style="margin-right: 10px; margin-bottom: 10px;">
|
1547 | <div style="display: flex; padding: 10px; background-color: #212121; color: #FFF; align-items: center;">
|
1548 | <div style="margin-right: auto;">${name}</div>
|
1549 | ${subscribed ? `<div style="padding: 2px 5px; background-color: #673AB7; border-radius: 100px; font-size: 10px; text-transform: uppercase;">Gold</div>` : ''}
|
1550 | </div>
|
1551 | <canvas width=${avatarCanvasSize} height=${avatarCanvasSize} class=avatar-canvas></canvas>
|
1552 | </avatar>
|
1553 | `);
|
1554 | const avatarCanvasEl = avatarEl.querySelector('.avatar-canvas');
|
1555 | avatarCanvasEl.width = avatarCanvasSize * window.devicePixelRatio;
|
1556 | avatarCanvasEl.height = avatarCanvasSize * window.devicePixelRatio;
|
1557 | avatarCanvasEl.style.width = `${avatarCanvasSize}px`;
|
1558 | avatarCanvasEl.style.height = `${avatarCanvasSize}px`;
|
1559 |
|
1560 | const equipment = _arrayify(avatar.equipment, 4);
|
1561 | const skinEquipment = equipment.find(assetSpec => {
|
1562 | if (assetSpec) {
|
1563 | const {type} = _getAssetType(assetSpec.asset);
|
1564 | return type === 'skin';
|
1565 | } else {
|
1566 | return false;
|
1567 | }
|
1568 | });
|
1569 | if (skinEquipment) {
|
1570 | const img = new Image();
|
1571 | img.onload = () => {
|
1572 | _drawSkin(img, avatarCanvasEl);
|
1573 | };
|
1574 | img.onerror = err => {
|
1575 | console.warn(err);
|
1576 | };
|
1577 | img.src = 'data:application/octet-stream;base64,' + skinEquipment.json.data;
|
1578 | } else {
|
1579 | _drawSkin(null, avatarCanvasEl);
|
1580 | }
|
1581 | avatarEl.destroy = () => {
|
1582 | avatarCanvasEl.destroy();
|
1583 | };
|
1584 |
|
1585 | avatarsEl.appendChild(avatarEl);
|
1586 | }
|
1587 | } else {
|
1588 | const emptyEl = $$(`\
|
1589 | <div style="display: flex; width: 100%; padding: 100px; font-size: 23px; font-weight: 400; justify-content: center; align-items: center;">Nothing here</div>
|
1590 | `);
|
1591 | avatarsEl.appendChild(emptyEl);
|
1592 | }
|
1593 | };
|
1594 |
|
1595 | const upVector = new THREE.Vector3(0, 1, 0);
|
1596 | const zeroQuaternion = new THREE.Quaternion();
|
1597 | const headBobRotation = new THREE.Quaternion().setFromUnitVectors(
|
1598 | new THREE.Vector3(0, 0, 1),
|
1599 | new THREE.Vector3(0, 0.05, 1).normalize()
|
1600 | );
|
1601 | const headRotation = new THREE.Quaternion().setFromUnitVectors(
|
1602 | new THREE.Vector3(0, 0, 1),
|
1603 | new THREE.Vector3(0.3, 0.1, 1).normalize()
|
1604 | );
|
1605 | const leftArmRotation = new THREE.Quaternion().setFromUnitVectors(
|
1606 | new THREE.Vector3(0, 0, 1),
|
1607 | new THREE.Vector3(1, 0.2, 1).normalize()
|
1608 | );
|
1609 | const rightArmRotation = new THREE.Quaternion().setFromUnitVectors(
|
1610 | new THREE.Vector3(0, 0, 1),
|
1611 | new THREE.Vector3(0.2, 1, -1).normalize()
|
1612 | );
|
1613 | let pauseAvatar = false;
|
1614 | [
|
1615 | 'skin-canvas',
|
1616 | 'skin-canvas-store',
|
1617 | ].forEach(selector => {
|
1618 | const skinCanvasEl = document.getElementById(selector);
|
1619 | skinCanvasEl.width = avatarCanvasSize * window.devicePixelRatio;
|
1620 | skinCanvasEl.height = avatarCanvasSize * window.devicePixelRatio;
|
1621 | skinCanvasEl.style.width = `${avatarCanvasSize}px`;
|
1622 | skinCanvasEl.style.height = `${avatarCanvasSize}px`;
|
1623 | skinCanvasEl.addEventListener('click', () => {
|
1624 | pauseAvatar = !pauseAvatar;
|
1625 | });
|
1626 | });
|
1627 | const _drawSkin = (() => {
|
1628 | const hmdMesh = (() => {
|
1629 | const hmdModelPath = '/models/hmd/hmd.json';
|
1630 | const controllerModelPath = '/models/controller/controller.json';
|
1631 |
|
1632 | const _requestJson = url => fetch(url, {
|
1633 | credentials: 'include',
|
1634 | })
|
1635 | .then(res => res.json());
|
1636 | const _requestJsonMesh = (modelJson, modelTexturePath) => new Promise((accept, reject) => {
|
1637 | const loader = new THREE.ObjectLoader();
|
1638 | loader.setTexturePath(modelTexturePath);
|
1639 | loader.parse(modelJson, accept);
|
1640 | });
|
1641 | const _requestHmdMesh = () => _requestJson(hmdModelPath)
|
1642 | .then(hmdModelJson => _requestJsonMesh(hmdModelJson, hmdModelPath.replace(/[^\/]+$/, '')))
|
1643 | .then(mesh => {
|
1644 | const object = new THREE.Object3D();
|
1645 |
|
1646 | mesh.scale.set(0.045, 0.045, 0.045);
|
1647 | mesh.rotation.order = camera.rotation.order;
|
1648 | mesh.rotation.y = Math.PI;
|
1649 |
|
1650 | object.add(mesh);
|
1651 |
|
1652 | return object;
|
1653 | });
|
1654 | const _requestControllerMesh = () => _requestJson(controllerModelPath)
|
1655 | .then(controllerModelJson => _requestJsonMesh(controllerModelJson, controllerModelPath.replace(/[^\/]+$/, '')));
|
1656 |
|
1657 | Promise.all([
|
1658 | _requestHmdMesh(),
|
1659 | _requestControllerMesh(),
|
1660 | ])
|
1661 | .then(([
|
1662 | hmdMesh,
|
1663 | controllerMesh,
|
1664 | ]) => {
|
1665 | hmdMesh.position.y = 1.6;
|
1666 | hmdMesh.updateMatrixWorld();
|
1667 | subobject.add(hmdMesh);
|
1668 |
|
1669 | const leftController = controllerMesh.clone(true);
|
1670 | leftController.position.copy(hmdMesh.position).add(new THREE.Vector3(-CONTROLLER_DEFAULT_OFFSETS[0], CONTROLLER_DEFAULT_OFFSETS[1], CONTROLLER_DEFAULT_OFFSETS[2]));
|
1671 | leftController.updateMatrixWorld();
|
1672 | subobject.add(leftController);
|
1673 |
|
1674 | const rightController = controllerMesh.clone(true);
|
1675 | rightController.position.copy(hmdMesh.position).add(new THREE.Vector3(CONTROLLER_DEFAULT_OFFSETS[0], CONTROLLER_DEFAULT_OFFSETS[1], CONTROLLER_DEFAULT_OFFSETS[2]));
|
1676 | rightController.updateMatrixWorld();
|
1677 | subobject.add(rightController);
|
1678 | });
|
1679 |
|
1680 | const object = new THREE.Object3D();
|
1681 | object.meshType = 'hmd';
|
1682 | const subobject = new THREE.Object3D();
|
1683 | object.add(subobject);
|
1684 | subobject.position.y = -3.7;
|
1685 | subobject.scale.set(3, 3, 3);
|
1686 | subobject.updateMatrixWorld();
|
1687 | return object;
|
1688 | })();
|
1689 |
|
1690 | const renderer = new THREE.WebGLRenderer();
|
1691 | renderer.setSize(200, 200);
|
1692 | renderer.setPixelRatio(window.devicePixelRatio);
|
1693 | renderer.setClearColor(0xFFFFFF, 1);
|
1694 | const scene = new THREE.Scene();
|
1695 | scene.matrixAutoUpdate = false;
|
1696 | const camera = new THREE.PerspectiveCamera(45, 200 / 200, 0.1, 100);
|
1697 | camera.position.set(0, 0, -2.8);
|
1698 | camera.lookAt(new THREE.Vector3(0, 0, 0));
|
1699 | camera.updateMatrixWorld();
|
1700 | camera.updateProjectionMatrix();
|
1701 | renderer.render(scene, camera);
|
1702 |
|
1703 | const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 3);
|
1704 | directionalLight.position.set(-1, 0.5, 0);
|
1705 | directionalLight.lookAt(new THREE.Vector3(0, 0, 0));
|
1706 | directionalLight.updateMatrixWorld();
|
1707 | scene.add(directionalLight);
|
1708 | const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.2);
|
1709 | scene.add(ambientLight);
|
1710 |
|
1711 | return (img, skinCanvasEl) => {
|
1712 | skinCanvasEl.destroy && skinCanvasEl.destroy();
|
1713 |
|
1714 | let mesh;
|
1715 | if (img) {
|
1716 | mesh = skin(img, {
|
1717 |
|
1718 | });
|
1719 | mesh.meshType = 'skin';
|
1720 | } else {
|
1721 | mesh = hmdMesh;
|
1722 | }
|
1723 |
|
1724 | mesh.position.set(0, -0.75, 0);
|
1725 | |
1726 |
|
1727 |
|
1728 |
|
1729 | mesh.updateMatrixWorld();
|
1730 |
|
1731 | const ROTATE_SPEED = 0.0004;
|
1732 | let animationFrame = null;
|
1733 | const _recurse = () => {
|
1734 | scene.add(mesh);
|
1735 |
|
1736 | if (!pauseAvatar) {
|
1737 | mesh.quaternion.setFromAxisAngle(
|
1738 | upVector,
|
1739 | -(Date.now() * ROTATE_SPEED * (Math.PI * 2) % (Math.PI * 2))
|
1740 | );
|
1741 | if (mesh.meshType === 'skin') {
|
1742 | mesh.material.uniforms.theta.value = 0.5 * Math.sin((Date.now() * ROTATE_SPEED * 1.25 * (Math.PI * 2)) % (Math.PI * 2));
|
1743 | mesh.material.uniforms.headRotation.value.fromArray(zeroQuaternion.clone()
|
1744 | .slerp(headBobRotation, Math.sin((Date.now() * ROTATE_SPEED * 3.5 * (Math.PI * 2)) % (Math.PI * 2))).toArray());
|
1745 | mesh.material.uniforms.leftArmRotation.value.fromArray(zeroQuaternion.toArray());
|
1746 | mesh.material.uniforms.rightArmRotation.value.fromArray(zeroQuaternion.toArray());
|
1747 | }
|
1748 | } else {
|
1749 | if (mesh.meshType === 'skin') {
|
1750 | mesh.material.uniforms.headRotation.value.fromArray(headRotation.toArray());
|
1751 | mesh.material.uniforms.leftArmRotation.value.fromArray(leftArmRotation.toArray());
|
1752 | mesh.material.uniforms.rightArmRotation.value.fromArray(rightArmRotation.toArray());
|
1753 | mesh.material.uniforms.theta.value = 0.4;
|
1754 | mesh.quaternion.setFromAxisAngle(
|
1755 | upVector,
|
1756 | -0.3
|
1757 | );
|
1758 | } else {
|
1759 | mesh.quaternion.set(0, 0, 0, 1);
|
1760 | }
|
1761 | }
|
1762 | mesh.updateMatrixWorld();
|
1763 | renderer.render(scene, camera);
|
1764 |
|
1765 | scene.remove(mesh);
|
1766 |
|
1767 | if (!skinCanvasEl.ctx) {
|
1768 | skinCanvasEl.ctx = skinCanvasEl.getContext('2d');
|
1769 |
|
1770 | }
|
1771 | skinCanvasEl.ctx.clearRect(0, 0, skinCanvasEl.width, skinCanvasEl.height);
|
1772 | skinCanvasEl.ctx.drawImage(renderer.domElement, 0, 0, skinCanvasEl.width, skinCanvasEl.height);
|
1773 |
|
1774 | animationFrame = requestAnimationFrame(_recurse);
|
1775 | };
|
1776 | _recurse();
|
1777 |
|
1778 | skinCanvasEl.destroy = () => {
|
1779 | scene.remove(mesh);
|
1780 |
|
1781 | cancelAnimationFrame(animationFrame);
|
1782 | };
|
1783 | };
|
1784 | })();
|
1785 |
|
1786 | const _loadEquipment = () => {
|
1787 | vrid.get('equipment')
|
1788 | .then(equipmentSpec => {
|
1789 | equipmentSpec = equipmentSpec || [];
|
1790 |
|
1791 | const equipment = _arrayify(equipmentSpec, 4)
|
1792 | .map(equipmentSpec => {
|
1793 | if (equipmentSpec) {
|
1794 | const {type, name} = _getAssetType(equipmentSpec.asset);
|
1795 | if (type === 'item') {
|
1796 | const item = items[name];
|
1797 | equipmentSpec.icon = item ? item.icon : null;
|
1798 | } else if (type === 'skin' || type === 'file') {
|
1799 | const item = items[type];
|
1800 | equipmentSpec.icon = item ? item.icon : null;
|
1801 | } else if (type === 'mod') {
|
1802 | const mod = mods.find(modSpec => modSpec.name === name) || null;
|
1803 | equipmentSpec.icon = mod ? mod.icon : null;
|
1804 | }
|
1805 | }
|
1806 | return equipmentSpec;
|
1807 | });
|
1808 | _renderEquipment(equipment);
|
1809 | _renderSkin(equipment, document.getElementById('skin-canvas'));
|
1810 | })
|
1811 | .catch(err => {
|
1812 | console.warn(err);
|
1813 | });
|
1814 | };
|
1815 | const _renderEquipment = equipment => {
|
1816 | const equipmentsEl = document.getElementById('equipments');
|
1817 | while (equipmentsEl.firstChild) {
|
1818 | equipmentsEl.firstChild.destroy && equipmentsEl.firstChild.destroy();
|
1819 | equipmentsEl.removeChild(equipmentsEl.firstChild);
|
1820 | }
|
1821 |
|
1822 | for (let i = 0; i < equipment.length; i++) {
|
1823 | let equipmentSpec = equipment[i];
|
1824 | const {asset, icon, file, json} = equipmentSpec || {};
|
1825 | const equipmentEl = $$(`\
|
1826 | <equipment style="display: flex; margin: 10px 30px;">
|
1827 | ${asset ? `\
|
1828 | <div style="position: relative; top: 27px; display: flex; margin-left: -27px; margin-right: -17px; margin-bottom: auto; padding: 1px; background-color: #FFF; border-radius: 5px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); justify-content: center; align-items: center; text-transform: uppercase; transform: rotateZ(-90deg);">
|
1829 | <div style="display: flex; width: 80px; padding: 5px 10px; background-color: #61c03c; border-radius: 5px; color: #FFF; font-size: 10px; font-weight: 700; justify-content: center; align-items: center;">
|
1830 | Equipped
|
1831 | </div>
|
1832 | </div>
|
1833 | <div style="display: flex; background-color: #FFF; border-radius: 5px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); flex-grow: 1; align-items: center;">
|
1834 | <canvas width=80 height=80></canvas>
|
1835 | <div style="display: flex; height: 36px; max-width: 100px; margin-right: auto; padding: 10px;">
|
1836 | <div style="margin-right: auto; text-overflow: ellipsis; overflow: hidden;">${!(file || json) ? (asset || ' ') : (file || json).name}</div>
|
1837 | </div>
|
1838 | <div class=unequip-button style="padding: 15px; cursor: pointer;">
|
1839 | <svg fill="#000000" height="20" viewBox="0 0 24 24" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
1840 | </div>
|
1841 | </div>
|
1842 | ` : `\
|
1843 | <div style="position: relative; top: 27px; display: flex; margin-left: -27px; margin-right: -17px; margin-bottom: auto; padding: 1px; border: 1px solid #DDD; border-radius: 5px; justify-content: center; align-items: center; text-transform: uppercase; transform: rotateZ(-90deg);">
|
1844 | <div style="display: flex; width: 80px; padding: 5px 10px; font-size: 10px; font-weight: 700; justify-content: center; align-items: center;">
|
1845 | Equip
|
1846 | </div>
|
1847 | </div>
|
1848 | <div style="display: flex; height: 80px; border: 1px solid #DDD; border-radius: 5px; flex-grow: 1; font-size: 24px; color: #CCC; justify-content: center; align-items: center;">+</div>
|
1849 | `}
|
1850 | </equipment>
|
1851 | `);
|
1852 | equipmentEl.getAssetSpec = () => equipmentSpec;
|
1853 | equipmentEl.setAssetSpec = assetSpec => {
|
1854 | equipmentSpec = assetSpec;
|
1855 | equipment[i] = assetSpec;
|
1856 |
|
1857 | vrid.set('equipment', equipment)
|
1858 | .catch(err => {
|
1859 | console.warn(err);
|
1860 | });
|
1861 |
|
1862 | _renderEquipment(equipment);
|
1863 | _renderSkin(equipment, document.getElementById('skin-canvas'));
|
1864 | _renderSkin(equipment, document.getElementById('skin-canvas-store'));
|
1865 | };
|
1866 | equipmentEl.setEquipment = otherEquipmentEl => {
|
1867 | equipmentEl.setAssetSpec(otherEquipmentEl.getAssetSpec());
|
1868 | otherEquipmentEl.setAssetSpec(null);
|
1869 |
|
1870 | _renderEquipment(equipment);
|
1871 | };
|
1872 | equipmentEl.setAttribute('draggable', true);
|
1873 | equipmentEl.addEventListener('dragstart', e => {
|
1874 | if (asset) {
|
1875 | e.dataTransfer.setData('text/plain', equipmentEl.assetSpec);
|
1876 | } else {
|
1877 | e.preventDefault();
|
1878 | }
|
1879 | });
|
1880 | equipmentEl.addEventListener('dragend', e => {
|
1881 | if (dragTarget !== equipmentEl) {
|
1882 | if (dragTarget && dragTarget.tagName === 'EQUIPMENT') {
|
1883 | dragTarget.setEquipment(equipmentEl);
|
1884 | } else {
|
1885 | equipmentEl.setAssetSpec(null);
|
1886 | }
|
1887 | }
|
1888 |
|
1889 | dragTarget = null;
|
1890 | });
|
1891 | equipmentEl.addEventListener('dragover', e => {
|
1892 | e.preventDefault();
|
1893 | });
|
1894 | equipmentEl.addEventListener('dragenter', e => {
|
1895 | requestAnimationFrame(() => {
|
1896 | dragTarget = equipmentEl;
|
1897 | });
|
1898 | });
|
1899 | equipmentEl.addEventListener('dragleave', e => {
|
1900 | if (dragTarget === equipmentEl) {
|
1901 | dragTarget = null;
|
1902 | }
|
1903 | });
|
1904 |
|
1905 | if (asset) {
|
1906 | const unequipButtonEl = equipmentEl.querySelector('.unequip-button');
|
1907 | unequipButtonEl.addEventListener('click', e => {
|
1908 | equipmentEl.setAssetSpec(null);
|
1909 | });
|
1910 |
|
1911 | let live = true;
|
1912 | equipmentEl.destroy = () => {
|
1913 | live = false;
|
1914 | };
|
1915 | const _requestIconImg = () => {
|
1916 | if (icon) {
|
1917 | return new Promise((accept, reject) => {
|
1918 | const img = new Image();
|
1919 | img.onload = () => {
|
1920 | accept(img);
|
1921 | };
|
1922 | img.onerror = err => {
|
1923 | reject(err);
|
1924 | };
|
1925 | img.crossOrigin = 'Anonymous';
|
1926 | img.src = 'data:application/octet-stream;base64,' + icon;
|
1927 | });
|
1928 | } else {
|
1929 | return _getQuestionMarkImage();
|
1930 | }
|
1931 | };
|
1932 | _requestIconImg()
|
1933 | .then(img => {
|
1934 | if (live) {
|
1935 | const spriteRenderer = new SpriteRenderer(img, equipmentEl.querySelector('canvas'));
|
1936 |
|
1937 | equipmentEl.destroy = () => {
|
1938 | spriteRenderer.destroy();
|
1939 | };
|
1940 | }
|
1941 | })
|
1942 | .catch(err => {
|
1943 | if (live) {
|
1944 | console.warn(err);
|
1945 | }
|
1946 | });
|
1947 | } else {
|
1948 | equipmentEl.destroy = () => {};
|
1949 | }
|
1950 |
|
1951 | equipmentsEl.appendChild(equipmentEl);
|
1952 | }
|
1953 | };
|
1954 |
|
1955 | const _loadStoreSkin = () => {
|
1956 | vrid.get('equipment')
|
1957 | .then(equipmentSpec => {
|
1958 | equipmentSpec = equipmentSpec || [];
|
1959 |
|
1960 | const equipment = _arrayify(equipmentSpec, 4)
|
1961 | .map(equipmentSpec => {
|
1962 | if (equipmentSpec) {
|
1963 | const {name} = _getAssetType(equipmentSpec.asset);
|
1964 | const item = items[name];
|
1965 | equipmentSpec.icon = item ? item.icon : null;
|
1966 | }
|
1967 | return equipmentSpec;
|
1968 | });
|
1969 | _renderSkin(equipment, document.getElementById('skin-canvas-store'));
|
1970 | })
|
1971 | .catch(err => {
|
1972 | console.warn(err);
|
1973 | });
|
1974 | };
|
1975 | const _renderSkin = (equipment, skinCanvasEl) => {
|
1976 | const skinEquipment = equipment.find(equipmentSpec => {
|
1977 | if (equipmentSpec) {
|
1978 | const {type} = _getAssetType(equipmentSpec.asset);
|
1979 | return type === 'skin';
|
1980 | } else {
|
1981 | return false;
|
1982 | }
|
1983 | });
|
1984 | if (skinEquipment) {
|
1985 | const img = new Image();
|
1986 | img.onload = () => {
|
1987 | _drawSkin(img, skinCanvasEl);
|
1988 | };
|
1989 | img.onerror = err => {
|
1990 | console.warn(err);
|
1991 | };
|
1992 | img.src = 'data:application/octet-stream;base64,' + skinEquipment.json.data;
|
1993 | } else {
|
1994 | _drawSkin(null, skinCanvasEl);
|
1995 | }
|
1996 | };
|
1997 |
|
1998 | const stripeConnectDisconnectButtonEl = document.getElementById('stripe-connect-disconnect-button');
|
1999 | stripeConnectDisconnectButtonEl.addEventListener('click', () => {
|
2000 | fetch('https://my-site.zeovr.io/stripe/disconnect', {
|
2001 | method: 'POST',
|
2002 | headers: (() => {
|
2003 | const headers = new Headers();
|
2004 | headers.append('Content-Type', 'application/json');
|
2005 | headers.append('Authorization', 'Basic ' + base64.encode(new TextEncoder().encode(`${uid}:`)));
|
2006 | return headers;
|
2007 | })(),
|
2008 | })
|
2009 | .then(_resBlob)
|
2010 | .then(() => {
|
2011 | stripeConnectUser = null;
|
2012 |
|
2013 | _renderAccount(username, stripeConnectUser);
|
2014 | })
|
2015 | .catch(err => {
|
2016 | console.warn(err);
|
2017 | });
|
2018 | });
|
2019 |
|
2020 | let username = null;
|
2021 | let stripeConnectUser = null;
|
2022 | const _loadAccount = () => {
|
2023 | Promise.all([
|
2024 | vrid.get('name'),
|
2025 | vrid.getRaw('stripe-connect-users/' + uid),
|
2026 | ])
|
2027 | .then(([
|
2028 | newUsername,
|
2029 | newStripeConnectUser,
|
2030 | ]) => {
|
2031 | username = newUsername;
|
2032 | stripeConnectUser = newStripeConnectUser;
|
2033 |
|
2034 | _renderAccount(username, stripeConnectUser);
|
2035 | });
|
2036 | };
|
2037 | const _renderAccount = (username, stripeConnectUser) => {
|
2038 | [
|
2039 | 'username',
|
2040 | 'username-store',
|
2041 | ].forEach(selector => {
|
2042 | const usernameEl = document.getElementById(selector);
|
2043 | usernameEl.innerText = username;
|
2044 | });
|
2045 | usernameInputEl.value = username;
|
2046 |
|
2047 | document.body.classList.toggle('stripe-connect-connected', Boolean(stripeConnectUser));
|
2048 | };
|
2049 |
|
2050 | const _signAsset = assetSpec => vrid.getRaw('keys/' + uid)
|
2051 | .then(key => Promise.all([
|
2052 | Promise.resolve(key),
|
2053 | _importKey(key),
|
2054 | ]))
|
2055 | .then(([
|
2056 | key,
|
2057 | cryptoKey,
|
2058 | ]) => crypto.subtle.sign({
|
2059 | name: 'ECDSA',
|
2060 | hash: {
|
2061 | name: 'SHA-256',
|
2062 | },
|
2063 | }, cryptoKey.privateKey, new TextEncoder().encode(JSON.stringify(assetSpec)))
|
2064 | .then(signature => {
|
2065 | assetSpec.certificate = [
|
2066 | {
|
2067 | publicKey: key.publicKey,
|
2068 | signature: hex.encode(signature),
|
2069 | },
|
2070 | ];
|
2071 | return assetSpec;
|
2072 | })
|
2073 | );
|
2074 | const _uploadFileItem = file => {
|
2075 | const fileId = _makeId();
|
2076 |
|
2077 | const _makeNotificationText = n => {
|
2078 | let s = 'Uploading ' + (n * 100).toFixed(1) + '% [';
|
2079 | let i;
|
2080 | const roundN = Math.round(n * 20);
|
2081 | for (i = 0; i < roundN; i++) {
|
2082 | s += '|';
|
2083 | }
|
2084 | for (; i < 20; i++) {
|
2085 | s += '.';
|
2086 | }
|
2087 | s += ']';
|
2088 | return s;
|
2089 | };
|
2090 | const notification = _addNotification(_makeNotificationText(0));
|
2091 |
|
2092 | return vrid.upload(fileId, file, progress => {
|
2093 | notification.set(_makeNotificationText(progress));
|
2094 | })
|
2095 | .then(() => {
|
2096 | const assetSpec = {
|
2097 | id: _makeId(),
|
2098 | asset: 'FILE.' + file.name,
|
2099 | quantity: 1,
|
2100 | file: {
|
2101 | name: file.name,
|
2102 | id: fileId,
|
2103 | },
|
2104 | timestamp: Date.now(),
|
2105 | };
|
2106 |
|
2107 | return Promise.all([
|
2108 | vrid.get('assets'),
|
2109 | _signAsset(assetSpec),
|
2110 | ])
|
2111 | .then(([
|
2112 | assets,
|
2113 | assetSpec,
|
2114 | ]) => {
|
2115 | assets = assets || [];
|
2116 | assets.push(assetSpec);
|
2117 |
|
2118 | return vrid.set('assets', assets)
|
2119 | .then(() => {
|
2120 | document.getElementById('inventory-link-4').click();
|
2121 | });
|
2122 | });
|
2123 | })
|
2124 | .then(() => {
|
2125 | _removeNotification(notification);
|
2126 | })
|
2127 | .catch(err => {
|
2128 | _removeNotification(notification);
|
2129 |
|
2130 | return Promise.reject(err);
|
2131 | });
|
2132 | };
|
2133 | const _uploadFile = file => {
|
2134 | if (/\.json/.test(file.name)) {
|
2135 | const reader = new FileReader();
|
2136 | reader.readAsText(file);
|
2137 | reader.onload = () => {
|
2138 | const s = reader.result;
|
2139 | const json = JSON.parse(s);
|
2140 | if (json._zeo_item === true) {
|
2141 | const {assets: newAssets} = json;
|
2142 |
|
2143 | vrid.get('assets')
|
2144 | .then(assets => {
|
2145 | assets = assets || [];
|
2146 | assets = assets.concat(newAssets);
|
2147 |
|
2148 | return vrid.set('assets', assets)
|
2149 | .then(() => {
|
2150 | document.getElementById('inventory-link-2').click();
|
2151 | });
|
2152 | })
|
2153 | .catch(err => {
|
2154 | console.warn(err);
|
2155 | });
|
2156 | } else {
|
2157 | _uploadFileItem(file)
|
2158 | .catch(err => {
|
2159 | console.warn(err);
|
2160 | });
|
2161 | }
|
2162 | };
|
2163 | reader.onerror = err => {
|
2164 | console.warn(err);
|
2165 | };
|
2166 | } else {
|
2167 | _uploadFileItem(file)
|
2168 | .catch(err => {
|
2169 | console.warn(err);
|
2170 | });
|
2171 | }
|
2172 | };
|
2173 | const _uploadSkin = file => {
|
2174 | const img = new Image();
|
2175 | const url = URL.createObjectURL(file);
|
2176 | img.onload = () => {
|
2177 | if (img.width === 64 && img.height === 64) {
|
2178 | const reader = new FileReader();
|
2179 | reader.readAsArrayBuffer(file);
|
2180 | reader.onload = () => {
|
2181 | const assetSpec = {
|
2182 | id: _makeId(),
|
2183 | asset: 'SKIN.' + file.name,
|
2184 | quantity: 1,
|
2185 | timestamp: Date.now(),
|
2186 | json: {
|
2187 | name: file.name,
|
2188 | data: base64.encode(reader.result),
|
2189 | },
|
2190 | };
|
2191 |
|
2192 | Promise.all([
|
2193 | vrid.get('assets'),
|
2194 | _signAsset(assetSpec),
|
2195 | ])
|
2196 | .then(([
|
2197 | assets,
|
2198 | assetSpec,
|
2199 | ]) => {
|
2200 | assets = assets || [];
|
2201 | assets.push(assetSpec);
|
2202 |
|
2203 | return vrid.set('assets', assets)
|
2204 | .then(() => {
|
2205 | _loadAssets('skin', document.getElementById('assets'), _getAssetsHtml);
|
2206 | });
|
2207 | })
|
2208 | .catch(err => {
|
2209 | console.warn(err);
|
2210 | });
|
2211 | };
|
2212 | reader.onerror = err => {
|
2213 | console.warn(err);
|
2214 | };
|
2215 | } else {
|
2216 | console.warn('invalid skin image size');
|
2217 | }
|
2218 |
|
2219 | URL.revokeObjectURL(url);
|
2220 | };
|
2221 | img.onerror = err => {
|
2222 | console.warn(err);
|
2223 |
|
2224 | URL.revokeObjectURL(url);
|
2225 | };
|
2226 | img.src = url;
|
2227 | };
|
2228 |
|
2229 | const inventoryLinks = [];
|
2230 | let inventoryType = null;
|
2231 | for (let i = 1; i <= 4; i++) {
|
2232 | const inventoryLinkEl = document.getElementById('inventory-link-' + i);
|
2233 | inventoryLinkEl.addEventListener('click', e => {
|
2234 | for (let i = 0; i < inventoryLinks.length; i++) {
|
2235 | const inventoryLinkEl = inventoryLinks[i];
|
2236 | inventoryLinkEl.classList.remove('selected');
|
2237 | }
|
2238 | inventoryLinkEl.classList.add('selected');
|
2239 |
|
2240 | const equippablesEl = document.getElementById('equippables');
|
2241 |
|
2242 | if (i === 1) {
|
2243 | inventoryType = null;
|
2244 | } else if (i === 2) {
|
2245 | inventoryType = 'item';
|
2246 | } else if (i === 3) {
|
2247 | inventoryType = 'mod';
|
2248 | } else if (i === 4) {
|
2249 | inventoryType = 'file';
|
2250 | }
|
2251 | _loadAssets(inventoryType, equippablesEl, _getEquippablesHtml);
|
2252 | });
|
2253 | inventoryLinks.push(inventoryLinkEl);
|
2254 | }
|
2255 |
|
2256 | const _initFile = () => {
|
2257 | document.addEventListener('dragover', e => {
|
2258 | e.preventDefault();
|
2259 | });
|
2260 | document.addEventListener('drop', e => {
|
2261 | e.preventDefault();
|
2262 |
|
2263 | if (uid) {
|
2264 | const file = e.dataTransfer.files[0];
|
2265 | if (file) {
|
2266 | _uploadFile(file);
|
2267 | }
|
2268 | }
|
2269 | });
|
2270 | };
|
2271 | _initFile();
|
2272 |
|
2273 | const headerButtonsEl = document.getElementById('header-buttons');
|
2274 | headerButtonsEl.querySelector(`.inventory-button`).addEventListener('click', e => {
|
2275 | _pushUrl(`/`);
|
2276 | e.preventDefault();
|
2277 | });
|
2278 | [
|
2279 | 'store',
|
2280 | 'avatars',
|
2281 | 'account',
|
2282 | 'sign-in',
|
2283 | ].forEach(tab => {
|
2284 | headerButtonsEl.querySelector(`.${tab}-button`).addEventListener('click', e => {
|
2285 | _pushUrl(`/${tab}.html`);
|
2286 | e.preventDefault();
|
2287 | });
|
2288 | });
|
2289 | headerButtonsEl.querySelector('.sign-out-button').addEventListener('click', e => {
|
2290 | vrid.signOut()
|
2291 | .then(() => {
|
2292 | document.location.href = '/sign-in.html';
|
2293 | })
|
2294 | .catch(err => {
|
2295 | console.warn(err);
|
2296 | });
|
2297 |
|
2298 | e.preventDefault();
|
2299 | });
|
2300 |
|
2301 | let uploadSkinEl = document.getElementById('upload-skin');
|
2302 | const _uploadSkinChange = e => {
|
2303 | if (e.target.files.length > 0) {
|
2304 | _uploadSkin(e.target.files[0]);
|
2305 | }
|
2306 |
|
2307 | const newUploadSkinEl = uploadSkinEl.cloneNode(true);
|
2308 | newUploadSkinEl.addEventListener('change', _uploadSkinChange);
|
2309 | uploadSkinEl.parentNode.insertBefore(newUploadSkinEl, uploadSkinEl);
|
2310 | uploadSkinEl.parentNode.removeChild(uploadSkinEl);
|
2311 | uploadSkinEl = newUploadSkinEl;
|
2312 | };
|
2313 | uploadSkinEl.addEventListener('change', _uploadSkinChange);
|
2314 |
|
2315 | let uploadFileEl = document.getElementById('upload-file');
|
2316 | const _uploadFileChange = e => {
|
2317 | if (e.target.files.length > 0) {
|
2318 | _uploadFile(e.target.files[0]);
|
2319 | }
|
2320 |
|
2321 | const newUploadFileEl = uploadFileEl.cloneNode(true);
|
2322 | newUploadFileEl.addEventListener('change', _uploadFileChange);
|
2323 | uploadFileEl.parentNode.insertBefore(newUploadFileEl, uploadFileEl);
|
2324 | uploadFileEl.parentNode.removeChild(uploadFileEl);
|
2325 | uploadFileEl = newUploadFileEl;
|
2326 | };
|
2327 | uploadFileEl.addEventListener('change', _uploadFileChange);
|
2328 |
|
2329 | const _registerSkin = file => {
|
2330 | const img = new Image();
|
2331 | const url = URL.createObjectURL(file);
|
2332 | img.onload = () => {
|
2333 | if (img.width === 64 && img.height === 64) {
|
2334 | const reader = new FileReader();
|
2335 | reader.readAsArrayBuffer(file);
|
2336 | reader.onload = () => {
|
2337 | const asset = 'SKIN.' + file.name;
|
2338 | const id = asset.replace(/\./g, '-') + '-' + _makeId();
|
2339 | const itemSpec = {
|
2340 | id,
|
2341 | asset,
|
2342 | json: {
|
2343 | name: file.name,
|
2344 | data: base64.encode(reader.result),
|
2345 | },
|
2346 | owner: uid,
|
2347 | };
|
2348 | return vrid.setRaw('items/' + id, itemSpec)
|
2349 | .then(() => {
|
2350 | items[id] = itemSpec;
|
2351 | _loadBuyables(storeMode, storeLink, storeIndexType);
|
2352 | });
|
2353 | };
|
2354 | reader.onerror = err => {
|
2355 | console.warn(err);
|
2356 | };
|
2357 | } else {
|
2358 | console.warn('invalid skin image size');
|
2359 | }
|
2360 |
|
2361 | URL.revokeObjectURL(url);
|
2362 | };
|
2363 | img.onerror = err => {
|
2364 | console.warn(err);
|
2365 |
|
2366 | URL.revokeObjectURL(url);
|
2367 | };
|
2368 | img.src = url;
|
2369 | };
|
2370 | let registerSkinEl = document.getElementById('register-skin');
|
2371 | const _registerSkinChange = e => {
|
2372 | if (e.target.files.length > 0) {
|
2373 | _registerSkin(e.target.files[0]);
|
2374 | }
|
2375 |
|
2376 | const newRegisterSkinEl = registerSkinEl.cloneNode(true);
|
2377 | newRegisterSkinEl.addEventListener('change', _registerSkinChange);
|
2378 | registerSkinEl.parentNode.insertBefore(newRegisterSkinEl, registerSkinEl);
|
2379 | registerSkinEl.parentNode.removeChild(registerSkinEl);
|
2380 | registerSkinEl = newRegisterSkinEl;
|
2381 | };
|
2382 | registerSkinEl.addEventListener('change', _registerSkinChange);
|
2383 |
|
2384 | const _registerFile = file => {
|
2385 | const fileId = _makeId();
|
2386 |
|
2387 | const _makeNotificationText = n => {
|
2388 | let s = 'Uploading ' + (n * 100).toFixed(1) + '% [';
|
2389 | let i;
|
2390 | const roundN = Math.round(n * 20);
|
2391 | for (i = 0; i < roundN; i++) {
|
2392 | s += '|';
|
2393 | }
|
2394 | for (; i < 20; i++) {
|
2395 | s += '.';
|
2396 | }
|
2397 | s += ']';
|
2398 | return s;
|
2399 | };
|
2400 | const notification = _addNotification(_makeNotificationText(0));
|
2401 |
|
2402 | return vrid.upload(fileId, file, progress => {
|
2403 | notification.set(_makeNotificationText(progress));
|
2404 | })
|
2405 | .then(() => {
|
2406 | const asset = 'FILE.' + file.name;
|
2407 | const id = asset.replace(/\./g, '-') + '-' + _makeId();
|
2408 | const itemSpec = {
|
2409 | id,
|
2410 | asset,
|
2411 | file: {
|
2412 | name: file.name,
|
2413 | id: fileId,
|
2414 | },
|
2415 | owner: uid,
|
2416 | };
|
2417 | return vrid.setRaw('items/' + id, itemSpec)
|
2418 | .then(() => {
|
2419 | items[id] = itemSpec;
|
2420 | _loadBuyables(storeMode, storeLink, storeIndexType);
|
2421 | });
|
2422 | })
|
2423 | .then(() => {
|
2424 | _removeNotification(notification);
|
2425 | })
|
2426 | .catch(err => {
|
2427 | _removeNotification(notification);
|
2428 |
|
2429 | return Promise.reject(err);
|
2430 | });
|
2431 | };
|
2432 | let registerFileEl = document.getElementById('register-file');
|
2433 | const _registerFileChange = e => {
|
2434 | if (e.target.files.length > 0) {
|
2435 | _registerFile(e.target.files[0]);
|
2436 | }
|
2437 |
|
2438 | const newRegisterFileEl = registerFileEl.cloneNode(true);
|
2439 | newRegisterFileEl.addEventListener('change', _registerFileChange);
|
2440 | registerFileEl.parentNode.insertBefore(newRegisterFileEl, registerFileEl);
|
2441 | registerFileEl.parentNode.removeChild(registerFileEl);
|
2442 | registerFileEl = newRegisterFileEl;
|
2443 | };
|
2444 | registerFileEl.addEventListener('change', _registerFileChange);
|
2445 |
|
2446 | const storeIndexTypes = [];
|
2447 | let storeIndexType = 1;
|
2448 | for (let i = 1; i <= 2; i++) {
|
2449 | const storeIndexTypeEl = document.getElementById('store-index-type-' + i);
|
2450 | storeIndexTypeEl.addEventListener('click', e => {
|
2451 | for (let i = 0; i < storeIndexTypes.length; i++) {
|
2452 | const storeIndexTypeEl = storeIndexTypes[i];
|
2453 | storeIndexTypeEl.classList.remove('selected');
|
2454 | }
|
2455 | storeIndexTypeEl.classList.add('selected');
|
2456 |
|
2457 | storeIndexType = i;
|
2458 |
|
2459 | _loadBuyables(storeMode, storeLink, storeIndexType);
|
2460 | });
|
2461 | storeIndexTypes.push(storeIndexTypeEl);
|
2462 | }
|
2463 |
|
2464 | const _requestImage = src => new Promise((accept, reject) => {
|
2465 | const img = new Image();
|
2466 | img.src = src;
|
2467 | img.onload = () => {
|
2468 | accept(img);
|
2469 | };
|
2470 | img.onerror = err => {
|
2471 | reject(err);
|
2472 | };
|
2473 | return img;
|
2474 | });
|
2475 | const _getQuestionMarkImage = () => _requestImage('/img/question.png');
|
2476 | const _getModImage = () => _requestImage('/img/mod.png');
|
2477 |
|
2478 | const storeStoreEl = document.getElementById('store-store');
|
2479 | const storeIndexEl = document.getElementById('store-index');
|
2480 | const storeModeLinks = [];
|
2481 | let storeMode = 1;
|
2482 | for (let i = 1; i <= 2; i++) {
|
2483 | const storeModeLinkEl = document.getElementById('store-mode-link-' + i);
|
2484 | storeModeLinkEl.addEventListener('click', e => {
|
2485 | for (let i = 0; i < storeModeLinks.length; i++) {
|
2486 | storeModeLinks[i].classList.remove('selected');
|
2487 | }
|
2488 | storeModeLinkEl.classList.add('selected');
|
2489 |
|
2490 | if (i === 1) {
|
2491 | storeStoreEl.style.display = 'flex';
|
2492 | storeIndexEl.style.display = 'none';
|
2493 | } else if (i === 2) {
|
2494 | storeStoreEl.style.display = 'none';
|
2495 | storeIndexEl.style.display = 'flex';
|
2496 | }
|
2497 | storeMode = i;
|
2498 |
|
2499 | _loadBuyables(storeMode, storeLink, storeIndexType);
|
2500 | });
|
2501 | storeModeLinks.push(storeModeLinkEl);
|
2502 | }
|
2503 |
|
2504 | const storeLinks = [];
|
2505 | let storeLink = 1;
|
2506 | for (let i = 1; i <= 5; i++) {
|
2507 | const storeLinkEl = document.getElementById('store-link-' + i);
|
2508 | storeLinkEl.addEventListener('click', e => {
|
2509 | for (let i = 0; i < storeLinks.length; i++) {
|
2510 | storeLinks[i].classList.remove('selected');
|
2511 | }
|
2512 | storeLinkEl.classList.add('selected');
|
2513 |
|
2514 | storeLink = i;
|
2515 |
|
2516 | _loadBuyables(storeMode, storeLink, storeIndexType);
|
2517 | });
|
2518 | storeLinks.push(storeLinkEl);
|
2519 | }
|
2520 |
|
2521 | const registerItemButtonEl = document.getElementById('register-item-button');
|
2522 | registerItemButtonEl.addEventListener('click', () => {
|
2523 | modalEl.innerHTML = `\
|
2524 | <form class="form register-item" style="background-color: #FFF; padding: 30px;" id="register-item-form">
|
2525 | <div style="position: relative; display: flex; margin-bottom: 15px; font-size: 20px;">
|
2526 | Register item
|
2527 | </div>
|
2528 | <div style="margin-bottom: 10px; font-size: 13px; font-weight: 700;">Item name</div>
|
2529 | <input type=text placeholder="Enter item name" required style="display: block; margin-bottom: 15px; background-color: #EEE; width: 400px; padding: 10px; border: 0; border-radius: 5px; font-size: 13px; font-weight: 400; outline: none;" id=register-item-name>
|
2530 | <div style="margin-bottom: 10px; font-size: 13px; font-weight: 700;">Item icon</div>
|
2531 | <div style="display: flex; margin-bottom: 15px;">
|
2532 | <canvas width=100 height=100 style="margin-right: 10px; padding: 5px; border: 1px solid #000; border-radius: 5px;" id=register-item-icon-preview></canvas>
|
2533 | <div class=button style="position: relative; margin-right: auto; margin-bottom: auto; overflow: hidden;">
|
2534 | <input type=file style="position: absolute; top: 0; bottom: 0; left: -100px; width: 100vw; opacity: 0; cursor: pointer;" id=upload-item-icon>
|
2535 | Choose file
|
2536 | </div>
|
2537 | </div>
|
2538 | <div style="margin-bottom: 10px; font-size: 13px; font-weight: 700;">Item description</div>
|
2539 | <textarea style="display: block; margin-bottom: 15px; background-color: #EEE; width: 400px; height: 100px; padding: 10px; border: 0; border-radius: 5px; font-family: inherit; font-size: 13px; font-weight: 400; outline: none;" placeholder="Enter item description" id=register-item-description></textarea>
|
2540 | <div style="display: flex;">
|
2541 | <input type=submit value="Create item" style="position: relative; margin-right: auto; overflow: hidden;">
|
2542 | </div>
|
2543 | </form>
|
2544 | `;
|
2545 |
|
2546 | const registerItemNameEl = document.getElementById('register-item-name');
|
2547 | const registerItemDescriptionEl = document.getElementById('register-item-description');
|
2548 | let registerItemIconPreviewSpriteRender = null;
|
2549 | let uploadItemIconFile = null;
|
2550 | const registerItemIconPreviewEl = document.getElementById('register-item-icon-preview');
|
2551 | _getQuestionMarkImage()
|
2552 | .then(img => {
|
2553 | registerItemIconPreviewSpriteRender = new SpriteRenderer(img, registerItemIconPreviewEl);
|
2554 | uploadItemIconFile = null;
|
2555 | })
|
2556 | .catch(err => {
|
2557 | console.warn(err);
|
2558 | });
|
2559 |
|
2560 | let uploadItemIconEl = document.getElementById('upload-item-icon');
|
2561 | const _uploadItemIconChange = e => {
|
2562 | if (registerItemIconPreviewSpriteRender) {
|
2563 | registerItemIconPreviewSpriteRender.destroy();
|
2564 | registerItemIconPreviewSpriteRender = null;
|
2565 | }
|
2566 |
|
2567 | if (e.target.files.length > 0) {
|
2568 | const file = e.target.files[0];
|
2569 |
|
2570 | const img = new Image();
|
2571 | const url = URL.createObjectURL(file);
|
2572 | img.onload = () => {
|
2573 | if (img.width === 16 && img.height === 16) {
|
2574 | registerItemIconPreviewSpriteRender = new SpriteRenderer(img, registerItemIconPreviewEl);
|
2575 | uploadItemIconFile = file;
|
2576 | } else {
|
2577 | console.warn('invalid item icon size');
|
2578 |
|
2579 | _getQuestionMarkImage()
|
2580 | .then(img => {
|
2581 | registerItemIconPreviewSpriteRender = new SpriteRenderer(img, registerItemIconPreviewEl);
|
2582 | uploadItemIconFile = null;
|
2583 | })
|
2584 | .catch(err => {
|
2585 | console.warn(err);
|
2586 | });
|
2587 | }
|
2588 |
|
2589 | URL.revokeObjectURL(url);
|
2590 | };
|
2591 | img.onerror = err => {
|
2592 | console.warn(err);
|
2593 |
|
2594 | URL.revokeObjectURL(url);
|
2595 | };
|
2596 | img.src = url;
|
2597 | } else {
|
2598 | _getQuestionMarkImage()
|
2599 | .then(img => {
|
2600 | registerItemIconPreviewSpriteRender = new SpriteRenderer(img, registerItemIconPreviewEl);
|
2601 | uploadItemIconFile = null;
|
2602 | })
|
2603 | .catch(err => {
|
2604 | console.warn(err);
|
2605 | });
|
2606 | }
|
2607 |
|
2608 | const newUploadItemIconEl = uploadItemIconEl.cloneNode(true);
|
2609 | newUploadItemIconEl.addEventListener('change', _uploadItemIconChange);
|
2610 | uploadItemIconEl.parentNode.insertBefore(newUploadItemIconEl, uploadItemIconEl);
|
2611 | uploadItemIconEl.parentNode.removeChild(uploadItemIconEl);
|
2612 | uploadItemIconEl = newUploadItemIconEl;
|
2613 | };
|
2614 | uploadItemIconEl.addEventListener('change', _uploadItemIconChange);
|
2615 | const registerItemFormEl = document.getElementById('register-item-form');
|
2616 | registerItemFormEl.addEventListener('submit', e => {
|
2617 | const _readIconFile = () => new Promise((accept, reject) => {
|
2618 | if (uploadItemIconFile) {
|
2619 | const reader = new FileReader();
|
2620 | reader.readAsArrayBuffer(uploadItemIconFile);
|
2621 | reader.onload = () => {
|
2622 | accept(base64.encode(reader.result));
|
2623 | };
|
2624 | reader.onerror = err => {
|
2625 | console.warn(err);
|
2626 | };
|
2627 | } else {
|
2628 | accept(null);
|
2629 | }
|
2630 | });
|
2631 |
|
2632 | _readIconFile()
|
2633 | .then(iconData => {
|
2634 | const localName = registerItemNameEl.value.toLowerCase();
|
2635 | registerItemNameEl.value = localName;
|
2636 | const localDescription = registerItemDescriptionEl.value;
|
2637 | vrid.existsRaw('items/' + localName)
|
2638 | .then(exists => {
|
2639 | if (!exists) {
|
2640 | const itemSpec = {
|
2641 | asset: 'ITEM.' + localName,
|
2642 | description: localDescription,
|
2643 | icon: iconData,
|
2644 | owner: uid,
|
2645 | };
|
2646 | return vrid.setRaw('items/' + localName, itemSpec)
|
2647 | .then(() => {
|
2648 | document.getElementById('inventory-link-2').click();
|
2649 | });
|
2650 | } else {
|
2651 | console.warn('item already registered', localName);
|
2652 | }
|
2653 | })
|
2654 | .catch(err => {
|
2655 | console.warn(err);
|
2656 |
|
2657 | modalEl.innerHTML = '';
|
2658 | });
|
2659 | });
|
2660 |
|
2661 | e.preventDefault();
|
2662 | });
|
2663 |
|
2664 | if (selectedTarget) {
|
2665 | selectedTarget.unselect();
|
2666 | selectedTarget = null;
|
2667 | }
|
2668 |
|
2669 | registerItemNameEl.value = '';
|
2670 | registerItemDescriptionEl.value = '';
|
2671 | if (registerItemIconPreviewSpriteRender) {
|
2672 | registerItemIconPreviewSpriteRender.destroy();
|
2673 | registerItemIconPreviewSpriteRender = null;
|
2674 |
|
2675 | _getQuestionMarkImage()
|
2676 | .then(img => {
|
2677 | registerItemIconPreviewSpriteRender = new SpriteRenderer(img, registerItemIconPreviewEl);
|
2678 | uploadItemIconFile = null;
|
2679 | })
|
2680 | .catch(err => {
|
2681 | console.warn(err);
|
2682 | });
|
2683 | }
|
2684 | });
|
2685 |
|
2686 | const usernameFormEl = document.getElementById('username-form');
|
2687 | const usernameInputEl = document.getElementById('username-input');
|
2688 | const saveUsernameButtonEl = document.getElementById('save-username-button');
|
2689 | usernameInputEl.addEventListener('keydown', e => {
|
2690 | requestAnimationFrame(() => {
|
2691 | saveUsernameButtonEl.style.display = username === usernameInputEl.value ? 'none' : null;
|
2692 | });
|
2693 | });
|
2694 | usernameFormEl.addEventListener('submit', e => {
|
2695 | const localUsername = usernameInputEl.value;
|
2696 |
|
2697 | vrid.set('name', localUsername)
|
2698 | .then(() => {
|
2699 | username = localUsername;
|
2700 |
|
2701 | _renderAccount(localUsername);
|
2702 |
|
2703 | saveUsernameButtonEl.style.display = localUsername === usernameInputEl.value ? 'none' : null;
|
2704 | })
|
2705 | .catch(err => {
|
2706 | console.warn(err);
|
2707 | });
|
2708 |
|
2709 | e.preventDefault();
|
2710 | });
|
2711 |
|
2712 | let subscribe = false;
|
2713 | const subscriptionSaveButtonEl = document.getElementById('subscription-save-button');
|
2714 | const basicInputEl = document.getElementById('basic-input');
|
2715 | basicInputEl.addEventListener('change', () => {
|
2716 | subscribe = false;
|
2717 |
|
2718 | document.body.classList.remove('paid-plan');
|
2719 | card.unmount();
|
2720 | subscriptionSaveButtonEl.style.display = (subscribe !== subscribed) ? null : 'none';
|
2721 | });
|
2722 | const goldInputEl = document.getElementById('gold-input');
|
2723 | goldInputEl.addEventListener('change', () => {
|
2724 | subscribe = true;
|
2725 |
|
2726 | document.body.classList.add('paid-plan');
|
2727 |
|
2728 | const mutated = subscribe !== subscribed;
|
2729 | if (mutated) {
|
2730 | card.mount('#card-element');
|
2731 | } else {
|
2732 | card.unmount();
|
2733 | }
|
2734 | subscriptionSaveButtonEl.style.display = mutated ? null : 'none';
|
2735 | });
|
2736 | const stripe = Stripe('pk_test_KjmGZ7ooNVFvzrrp9LbeWdRj');
|
2737 | const stripeElements = stripe.elements();
|
2738 | const card = stripeElements.create('card', {
|
2739 | style: {
|
2740 | base: {
|
2741 | fontSize: '16px',
|
2742 | lineHeight: '24px',
|
2743 | },
|
2744 | },
|
2745 | });
|
2746 | card.addEventListener('change', event => {
|
2747 | const displayError = document.getElementById('card-errors');
|
2748 | if (event.error) {
|
2749 | displayError.textContent = event.error.message;
|
2750 | } else {
|
2751 | displayError.textContent = '';
|
2752 | }
|
2753 | });
|
2754 |
|
2755 | const subscribeFormEl = document.getElementById('subscribe-form');
|
2756 | subscribeFormEl.addEventListener('submit', e => {
|
2757 | const localSubscribe = subscribe;
|
2758 |
|
2759 | document.body.classList.add('subscription-saving');
|
2760 | basicInputEl.disabled = true;
|
2761 | goldInputEl.disabled = true;
|
2762 |
|
2763 | const _subscribe = card => stripe.createToken(card)
|
2764 | .then(result => {
|
2765 | if (!result.error) {
|
2766 | return Promise.resolve(result.token.id);
|
2767 | } else {
|
2768 | return Promise.reject(result.error);
|
2769 | }
|
2770 | });
|
2771 | if (localSubscribe) {
|
2772 | _subscribe(card)
|
2773 | .then(token => fetch('https://my-site.zeovr.io/subscription', {
|
2774 | method: 'POST',
|
2775 | headers: (() => {
|
2776 | const headers = new Headers();
|
2777 | headers.append('Content-Type', 'application/json');
|
2778 | headers.append('Authorization', 'Basic ' + base64.encode(new TextEncoder().encode(`${uid}:`)));
|
2779 | return headers;
|
2780 | })(),
|
2781 | body: JSON.stringify({
|
2782 | token,
|
2783 | }),
|
2784 | }))
|
2785 | .then(_resJson)
|
2786 | .then(() => {
|
2787 | subscribed = localSubscribe;
|
2788 |
|
2789 | goldInputEl.dispatchEvent(new Event('change'));
|
2790 | card.clear();
|
2791 | subscriptionSaveButtonEl.style.display = 'none';
|
2792 | document.body.classList.remove('subscription-saving');
|
2793 | basicInputEl.disabled = false;
|
2794 | goldInputEl.disabled = false;
|
2795 | })
|
2796 | .catch(err => {
|
2797 | console.warn(err);
|
2798 |
|
2799 | document.body.classList.remove('subscription-saving');
|
2800 | basicInputEl.disabled = false;
|
2801 | goldInputEl.disabled = false;
|
2802 | });
|
2803 | } else {
|
2804 | fetch('https://my-site.zeovr.io/subscription', {
|
2805 | method: 'DELETE',
|
2806 | headers: (() => {
|
2807 | const headers = new Headers();
|
2808 | headers.append('Authorization', 'Basic ' + base64.encode(new TextEncoder().encode(`${uid}:`)));
|
2809 | return headers;
|
2810 | })(),
|
2811 | })
|
2812 | .then(_resJson)
|
2813 | .then(() => {
|
2814 | subscribed = localSubscribe;
|
2815 |
|
2816 | basicInputEl.dispatchEvent(new Event('change'));
|
2817 | card.clear();
|
2818 | subscriptionSaveButtonEl.style.display = 'none';
|
2819 | document.body.classList.remove('subscription-saving');
|
2820 | basicInputEl.disabled = false;
|
2821 | goldInputEl.disabled = false;
|
2822 | })
|
2823 | .catch(err => {
|
2824 | console.warn(err);
|
2825 |
|
2826 | document.body.classList.remove('subscription-saving');
|
2827 | basicInputEl.disabled = false;
|
2828 | goldInputEl.disabled = false;
|
2829 | });
|
2830 | }
|
2831 |
|
2832 | e.preventDefault();
|
2833 | });
|
2834 |
|
2835 | const downloadKeysButtonEl = document.getElementById('download-keys-button');
|
2836 | downloadKeysButtonEl.addEventListener('click', () => {
|
2837 | vrid.getRaw('keys/' + uid)
|
2838 | .then(keys => {
|
2839 | const s = JSON.stringify(keys, null, 2);
|
2840 | const blob = new Blob([s], {
|
2841 | type: 'application/json',
|
2842 | });
|
2843 | const url = URL.createObjectURL(blob);
|
2844 | const a = document.createElement('a');
|
2845 | a.href = url;
|
2846 | a.download = 'keys.json';
|
2847 | a.click();
|
2848 | URL.revokeObjectURL(url);
|
2849 | })
|
2850 | .catch(err => {
|
2851 | console.warn(err);
|
2852 | });
|
2853 | });
|
2854 |
|
2855 | const notifications = [];
|
2856 | class Notification {
|
2857 | constructor(text) {
|
2858 | this.text = text;
|
2859 | }
|
2860 | set(text) {
|
2861 | this.text = text;
|
2862 |
|
2863 | _renderNotifications();
|
2864 | }
|
2865 | }
|
2866 | const _addNotification = text => {
|
2867 | const notification = new Notification(text);
|
2868 | notifications.push(notification);
|
2869 |
|
2870 | _renderNotifications();
|
2871 |
|
2872 | return notification;
|
2873 | };
|
2874 | const _removeNotification = notification => {
|
2875 | notifications.splice(notifications.indexOf(notification), 1);
|
2876 |
|
2877 | _renderNotifications();
|
2878 | };
|
2879 | const _renderNotifications = () => {
|
2880 | document.getElementById('notifications').innerText = notifications.map(({text}) => text).join('\n');
|
2881 | };
|
2882 |
|
2883 | const _onlyDefined = o => {
|
2884 | const result = {};
|
2885 | for (const k in o) {
|
2886 | const v = o[k];
|
2887 | if (v !== undefined) {
|
2888 | result[k] = v;
|
2889 | }
|
2890 | }
|
2891 | return result;
|
2892 | };
|
2893 | const _normalizeItem = item => _onlyDefined({
|
2894 | id: item.id,
|
2895 | asset: item.asset || ('ITEM.' + item.name),
|
2896 | quantity: item.quantity,
|
2897 | icon: item.icon,
|
2898 | description: item.description,
|
2899 | owner: item.owner,
|
2900 | file: item.file,
|
2901 | json: item.json,
|
2902 | certificate: item.certificate,
|
2903 | sale: item.sale,
|
2904 | });
|
2905 | const _normalizeMod = mod => _onlyDefined({
|
2906 | asset: 'MOD.' + mod.name,
|
2907 | icon: mod.metadata && mod.metadata.icon,
|
2908 | description: mod.description,
|
2909 | owner: mod.metadata && mod.metadata.owner,
|
2910 | });
|
2911 | const _quantizeAssets = assets => {
|
2912 | const assetIndex = {};
|
2913 | for (let i = 0; i < assets.length; i++) {
|
2914 | const assetSpec = assets[i];
|
2915 | const {asset} = assetSpec;
|
2916 | let entry = assetIndex[asset];
|
2917 | if (!entry) {
|
2918 | entry = _normalizeItem(assetSpec);
|
2919 | entry.assets = [];
|
2920 | assetIndex[asset] = entry;
|
2921 | }
|
2922 | entry.assets.push(assetSpec);
|
2923 | }
|
2924 | return Object.keys(assetIndex).map(k => assetIndex[k]);
|
2925 | };
|
2926 | const _getAssetsHtml = assetSpec => {
|
2927 | if (assetSpec) {
|
2928 | const {id, asset, icon, description, owner, file, json, certificate, sale} = assetSpec;
|
2929 | return `\
|
2930 | <asset class="asset ${(file || json) ? 'file' : ''} ${(certificate && !sale) ? 'certificate' : ''} ${sale ? 'sale' : ''}">
|
2931 | <div class=asset-wrap>
|
2932 | ${sale ? `<div style="display: flex; font-size: 11px;">
|
2933 | ${uid ? `\
|
2934 | ${sale.seller !== uid ? `<a class=buy-button style="margin: 5px; margin-left: auto; margin-bottom: 0; padding: 2px 5px; border-radius: 3px; color: #FFF;">BUY ¤${sale.value}</a>` : ''}
|
2935 | ${sale.seller === uid ? `<a class=undo-sale-button style="margin: 5px; margin-left: auto; margin-bottom: 0; padding: 2px 5px; border-radius: 3px; color: #FFF;">UNDO SALE</a>` : ''}
|
2936 | ` : `\
|
2937 | ${sale.seller !== uid ? `<a class=log-in-to-buy-button style="margin: 5px; margin-left: auto; margin-bottom: 0; padding: 2px 5px; border-radius: 3px; color: #FFF;">LOGIN TO BUY (¤${sale.value})</a>` : ''}
|
2938 | `}
|
2939 | </div>` : ''}
|
2940 | <canvas width=80 height=80></canvas>
|
2941 | <div style="display: flex; padding: 0 10px; flex-grow: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
2942 | <div style="margin-right: auto; text-overflow: ellipsis; overflow: hidden;">${!(file || json) ? asset : (file || json).name}</div>
|
2943 | </div>
|
2944 | ${(id && !sale) ? `<div class=asset-header>
|
2945 | <a class="asset-header-link gift-link blue">
|
2946 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="16" height="16" viewBox="0 0 24 24"><path d="M22,12V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V12A1,1 0 0,1 1,11V8A2,2 0 0,1 3,6H6.17C6.06,5.69 6,5.35 6,5A3,3 0 0,1 9,2C10,2 10.88,2.5 11.43,3.24V3.23L12,4L12.57,3.23V3.24C13.12,2.5 14,2 15,2A3,3 0 0,1 18,5C18,5.35 17.94,5.69 17.83,6H21A2,2 0 0,1 23,8V11A1,1 0 0,1 22,12M4,20H11V12H4V20M20,20V12H13V20H20M9,4A1,1 0 0,0 8,5A1,1 0 0,0 9,6A1,1 0 0,0 10,5A1,1 0 0,0 9,4M15,4A1,1 0 0,0 14,5A1,1 0 0,0 15,6A1,1 0 0,0 16,5A1,1 0 0,0 15,4M3,8V10H11V8H3M13,8V10H21V8H13Z" /></svg>
|
2947 | </a>
|
2948 | <a class="asset-header-link sell-link blue">
|
2949 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="16" height="16" viewBox="0 0 24 24"><path d="M20,18H4V6H20M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4M11,17H13V16H14A1,1 0 0,0 15,15V12A1,1 0 0,0 14,11H11V10H15V8H13V7H11V8H10A1,1 0 0,0 9,9V12A1,1 0 0,0 10,13H13V14H9V16H11V17Z" /></svg>
|
2950 | </a>
|
2951 | <a class="asset-header-link export-link blue">
|
2952 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="16" height="16" viewBox="0 0 24 24"><path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" /></svg>
|
2953 | </a>
|
2954 | <a class="asset-header-link remove-link blue">
|
2955 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="16" height="16" viewBox="0 0 24 24"><path d="M21.03,3L18,20.31C17.83,21.27 17,22 16,22H8C7,22 6.17,21.27 6,20.31L2.97,3H21.03M5.36,5L8,20H16L18.64,5H5.36M9,18V14H13V18H9M13,13.18L9.82,10L13,6.82L16.18,10L13,13.18Z" /></svg>
|
2956 | </a>
|
2957 | </div>` : ''}
|
2958 | </div>
|
2959 | </asset>
|
2960 | `;
|
2961 | } else {
|
2962 | return '';
|
2963 | }
|
2964 | };
|
2965 | const _getEquippablesHtml = assetSpec => {
|
2966 | if (assetSpec) {
|
2967 | const {id, asset, icon, description, owner, file, json, certificate, sale} = assetSpec;
|
2968 | return `\
|
2969 | <equippable>
|
2970 | <div style="display: flex; height: 30px; padding: 0 10px; background-color: #333; color: #FFF; justify-content: space-evenly; align-items: center;">
|
2971 | <a class="asset-header-link gift-link blue">
|
2972 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="16" height="16" viewBox="0 0 24 24"><path d="M22,12V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V12A1,1 0 0,1 1,11V8A2,2 0 0,1 3,6H6.17C6.06,5.69 6,5.35 6,5A3,3 0 0,1 9,2C10,2 10.88,2.5 11.43,3.24V3.23L12,4L12.57,3.23V3.24C13.12,2.5 14,2 15,2A3,3 0 0,1 18,5C18,5.35 17.94,5.69 17.83,6H21A2,2 0 0,1 23,8V11A1,1 0 0,1 22,12M4,20H11V12H4V20M20,20V12H13V20H20M9,4A1,1 0 0,0 8,5A1,1 0 0,0 9,6A1,1 0 0,0 10,5A1,1 0 0,0 9,4M15,4A1,1 0 0,0 14,5A1,1 0 0,0 15,6A1,1 0 0,0 16,5A1,1 0 0,0 15,4M3,8V10H11V8H3M13,8V10H21V8H13Z" /></svg>
|
2973 | </a>
|
2974 | <a class="asset-header-link sell-link blue">
|
2975 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="16" height="16" viewBox="0 0 24 24"><path d="M20,18H4V6H20M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4M11,17H13V16H14A1,1 0 0,0 15,15V12A1,1 0 0,0 14,11H11V10H15V8H13V7H11V8H10A1,1 0 0,0 9,9V12A1,1 0 0,0 10,13H13V14H9V16H11V17Z" /></svg>
|
2976 | </a>
|
2977 | <a class="asset-header-link export-link blue">
|
2978 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="16" height="16" viewBox="0 0 24 24"><path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" /></svg>
|
2979 | </a>
|
2980 | <a class="asset-header-link remove-link blue">
|
2981 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="16" height="16" viewBox="0 0 24 24"><path d="M21.03,3L18,20.31C17.83,21.27 17,22 16,22H8C7,22 6.17,21.27 6,20.31L2.97,3H21.03M5.36,5L8,20H16L18.64,5H5.36M9,18V14H13V18H9M13,13.18L9.82,10L13,6.82L16.18,10L13,13.18Z" /></svg>
|
2982 | </a>
|
2983 | </div>
|
2984 | <div style="display: flex; width: 150px; height: 130px; background-color: #FFF; flex-direction: column; justify-content: center; align-items: center;">
|
2985 | <canvas width=80 height=80></canvas>
|
2986 | <div style="display: flex;">
|
2987 | <div>${assetSpec.asset}</div> <div style="font-weight: 700;">${assetSpec.assets.length > 1 ? `(${assetSpec.assets.length})` : ''}</div>
|
2988 | </div>
|
2989 | </div>
|
2990 | </equippable>
|
2991 | `;
|
2992 | } else {
|
2993 | return `<div style="display: flex; width: 150px; height: 160px; margin: 20px; border: 1px solid #E8E8E8; border-radius: 5px; color: #DDD; font-size: 50px; justify-content: center; align-items: center;">+</div>`;
|
2994 | }
|
2995 | };
|
2996 | const creditsEl = document.getElementById('credits');
|
2997 | const creditsStoreEl = document.getElementById('credits-store');
|
2998 | const _renderCredits = credits => {
|
2999 | creditsEl.innerText = credits;
|
3000 | creditsStoreEl.innerText = credits;
|
3001 | };
|
3002 |
|
3003 | const _loadUrl = url => {
|
3004 | headerButtonsEl.querySelectorAll('a').forEach(e => {
|
3005 | e.classList.remove('selected');
|
3006 | });
|
3007 | document.body.classList.remove(
|
3008 | 'managing-inventory',
|
3009 | 'browsing-mods',
|
3010 | 'browsing-items',
|
3011 | 'browsing-avatars',
|
3012 | 'managing-account'
|
3013 | );
|
3014 |
|
3015 | if (url === '/store.html') {
|
3016 | document.body.classList.add('browsing-items');
|
3017 | document.body.classList.remove('registering-item');
|
3018 | headerButtonsEl.querySelector('.store-button').classList.add('selected');
|
3019 |
|
3020 | Promise.all([
|
3021 | _loadAllAssets(),
|
3022 | _loadAllMods(),
|
3023 | _loadAllSales(),
|
3024 | ])
|
3025 | .then(([
|
3026 | allAssetsResult,
|
3027 | allModsResult,
|
3028 | allSalesResult,
|
3029 | ]) => {
|
3030 | _loadBuyables(storeMode, storeLink, storeIndexType);
|
3031 | });
|
3032 |
|
3033 | _loadStoreSkin(),
|
3034 | _loadAccount();
|
3035 | } else if (url === '/avatars.html') {
|
3036 | document.body.classList.add('browsing-avatars');
|
3037 | headerButtonsEl.querySelector('.avatars-button').classList.add('selected');
|
3038 |
|
3039 | Promise.all([
|
3040 | _loadAvatars(),
|
3041 | _loadAllAssets(),
|
3042 | ])
|
3043 | .then(([
|
3044 | avatars,
|
3045 | allAssetsResult,
|
3046 | ]) => {
|
3047 | _renderAvatars(avatars, document.getElementById('avatars'));
|
3048 | });
|
3049 | } else if (url === '/account.html') {
|
3050 | if (uid) {
|
3051 | document.body.classList.add('managing-account');
|
3052 | headerButtonsEl.querySelector('.account-button').classList.add('selected');
|
3053 |
|
3054 | const stripeConnectExpressLinkEl = document.getElementById('stripe-connect-express-link');
|
3055 | stripeConnectExpressLinkEl.href = `https://connect.stripe.com/express/oauth/authorize?client_id=ca_Bj6O5x5CFVCOELBhyjbiJxwUfW6l8ozd&state=${uid}`;
|
3056 | const stripeConnectLinkEl = document.getElementById('stripe-connect-link');
|
3057 | stripeConnectLinkEl.href = `https://connect.stripe.com/oauth/authorize?response_type=code&client_id=ca_Bj6O5x5CFVCOELBhyjbiJxwUfW6l8ozd&scope=read_write&state=${uid}`;
|
3058 |
|
3059 | _loadAccount();
|
3060 | } else {
|
3061 | document.location.href = '/sign-in.html';
|
3062 | }
|
3063 | } else {
|
3064 | if (uid) {
|
3065 | document.body.classList.add('managing-inventory');
|
3066 | headerButtonsEl.querySelector('.inventory-button').classList.add('selected');
|
3067 |
|
3068 | _loadAssets('skin', document.getElementById('assets'), _getAssetsHtml);
|
3069 | _loadAssets(null, document.getElementById('equippables'), _getEquippablesHtml);
|
3070 | _loadEquipment();
|
3071 | _loadAccount();
|
3072 | } else {
|
3073 | document.location.href = '/sign-in.html';
|
3074 | }
|
3075 | }
|
3076 | };
|
3077 | const _pushUrl = url => {
|
3078 | window.history.pushState({}, '', url);
|
3079 | _loadUrl(url);
|
3080 | };
|