UNPKG

116 kBJavaScriptView Raw
1let uid = null;
2let subscribed = false;
3let credits = 0;
4Promise.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
59const CONTROLLER_DEFAULT_OFFSETS = [0.2, -0.1, -0.2];
60
61function $$(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}
70function _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}
81function _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}
90function _capitalize(s) {
91 return s[0].toUpperCase() + s.slice(1);
92}
93function _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}
100function _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}
110function _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}
120function _jsonParse(s) {
121 try {
122 return JSON.parse(s);
123 } catch(err) {
124 return undefined;
125 }
126}
127const 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 // Main loop deals with bytes in chunks of 3
141 for (var i = 0; i < mainLength; i = i + 3) {
142 // Combine the three bytes into a single integer
143 chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]
144
145 // Use bitmasks to extract 6-bit segments from the triplet
146 a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18
147 b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12
148 c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6
149 d = chunk & 63 // 63 = 2^6 - 1
150
151 // Convert the raw binary segments to the appropriate ASCII encoding
152 base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
153 }
154
155 // Deal with the remaining bytes and padding
156 if (byteRemainder == 1) {
157 chunk = bytes[mainLength]
158
159 a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2
160
161 // Set the 4 least significant bits to zero
162 b = (chunk & 3) << 4 // 3 = 2^2 - 1
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 // 64512 = (2^6 - 1) << 10
169 b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4
170
171 // Set the 2 least significant bits to zero
172 c = (chunk & 15) << 2 // 15 = 2^4 - 1
173
174 base64 += encodings[a] + encodings[b] + encodings[c] + '='
175 }
176
177 return base64
178 },
179};
180const _makeId = () => {
181 const arrayBuffer = new ArrayBuffer(32);
182 const array = new Uint8Array(arrayBuffer);
183 crypto.getRandomValues(array);
184 return hex.encode(arrayBuffer);
185};
186
187function _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}
197function _importPublicKey(publicKey) {
198 return crypto.subtle.importKey('jwk', publicKey, {
199 name: 'ECDSA',
200 namedCurve: 'P-256',
201 }, true, ['verify']);
202}
203function _importPrivateKey(privateKey) {
204 return crypto.subtle.importKey('jwk', privateKey, {
205 name: 'ECDSA',
206 namedCurve: 'P-256',
207 }, true, ['sign']);
208}
209function _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}
222function _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 // XXX actually verify mod ownership
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}
291function _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}
302const 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
323const modalEl = document.getElementById('modal');
324modalEl.addEventListener('click', e => {
325 if (e.target === modalEl) {
326 modalEl.innerHTML = '';
327
328 e.preventDefault();
329 e.stopPropagation();
330 }
331});
332
333let items = null;
334let mods = null;
335let sales = null;
336const _loadAllAssets = () => vrid.getRaw('items')
337 .then(j => {
338 items = j;
339 });
340const _loadAllMods = () => fetch(`https://my-site.zeovr.io/mods`)
341 .then(_resJson)
342 .then(j => {
343 mods = j;
344 });
345const _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 });
360const _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 });
397const _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
410let selectedTarget = null;
411let dragTarget = null;
412const _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 /* const {type} = _getAssetType(assetSpec.asset);
631 if (type === 'FILE') {
632 const a = document.createElement('a');
633 a.href = `https://my-site.zeovr.io/files/${assetSpec.file.id}/${assetSpec.file.name}`;
634 a.download = assetSpec.file.name;
635 a.click();
636 } else { */
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 /* const buyButtonEl = assetEl.querySelector('.buy-button');
757 if (buyButtonEl) {
758 buyButtonEl.addEventListener('click', e => {
759 modalEl.innerHTML = `\
760 <form class=form style="background-color: #FFF; width: 400px; padding: 30px;">
761 <div style="margin-bottom: 10px; font-size: 20px; font-weight: 400;">Buy <span style="font-weight: 600;">${quantity}</span> <span style="font-weight: 600;">${assetSpec.asset}</span> for <span style="font-weight: 600;">¤${sale.value}?</span></div>
762 <input type=submit value="Confirm purchase" name=submit class=submit-button>
763 </form>
764 `;
765 const formEl = modalEl.querySelector('.form');
766 const submitButtonEl = modalEl.querySelector('.submit-button');
767 formEl.addEventListener('submit', e => {
768 submitButtonEl.oldValue = submitButtonEl.value;
769 submitButtonEl.value = 'Purchasing...';
770 submitButtonEl.disabled = true;
771
772 vrid.setRaw(`buys/${assetSpec.certificate[0].signature}`, true)
773 .then(() => vrid.onNullRaw(`buys/${assetSpec.certificate[0].signature}`))
774 .then(() => vrid.get('assets'))
775 .then(inventoryAssets => {
776 const newAssetSpec = JSON.parse(JSON.stringify(assetSpec));
777 delete newAssetSpec.sale;
778 inventoryAssets = inventoryAssets.concat([newAssetSpec]);
779
780 return vrid.set('assets', inventoryAssets)
781 .then(() => vrid.getRaw('credits/' + newUser.uid))
782 .then(newCredits => {
783 assets = assets.slice();
784 assets.splice(assets.indexOf(assetSpec), 1);
785 _renderAssets(assets, assetsEl, getHtml);
786
787 credits = newCredits;
788 _renderCredits(credits);
789
790 modalEl.innerHTML = '';
791 });
792 })
793 .catch(err => {
794 console.warn(err);
795
796 submitButtonEl.value = submitButtonEl.oldValue;
797 submitButtonEl.disabled = false;
798 });
799
800 e.preventDefault();
801 });
802 });
803 }
804 const undoSaleButtonEl = assetEl.querySelector('.undo-sale-button');
805 if (undoSaleButtonEl) {
806 undoSaleButtonEl.addEventListener('click', e => {
807 modalEl.innerHTML = `\
808 <form class=form style="background-color: #FFF; width: 400px; padding: 30px;">
809 <div style="margin-bottom: 10px; font-size: 20px; font-weight: 400;">Undo sale of <span style="font-weight: 600;">${quantity}</span> <span style="font-weight: 600;">${assetSpec.asset}</span> for <span style="font-weight: 600;">¤${sale.value}?</span></div>
810 <input type=submit value="Confirm undo" name=submit class=submit-button>
811 </form>
812 `;
813 const formEl = modalEl.querySelector('.form');
814 const submitButtonEl = modalEl.querySelector('.submit-button');
815 formEl.addEventListener('submit', e => {
816 submitButtonEl.oldValue = submitButtonEl.value;
817 submitButtonEl.value = 'Undoing...';
818 submitButtonEl.disabled = true;
819
820 vrid.setRaw(`unsales/${assetSpec.certificate[0].signature}`, true)
821 .then(() => vrid.onNullRaw(`unsales/${assetSpec.certificate[0].signature}`))
822 .then(() => vrid.get('assets'))
823 .then(inventoryAssets => {
824 const newAssetSpec = JSON.parse(JSON.stringify(assetSpec));
825 delete newAssetSpec.sale;
826 inventoryAssets = inventoryAssets.concat([newAssetSpec]);
827
828 return vrid.set('assets', inventoryAssets)
829 .then(() => {
830 assets = assets.slice();
831 assets.splice(assets.indexOf(assetSpec), 1);
832
833 _renderAssets(assets, assetsEl, getHtml);
834
835 modalEl.innerHTML = '';
836 });
837 })
838 .catch(err => {
839 console.warn(err);
840
841 submitButtonEl.value = submitButtonEl.oldValue;
842 submitButtonEl.disabled = false;
843 });
844
845 e.preventDefault();
846 });
847 });
848 }
849 const logInToBuyButtonEl = assetEl.querySelector('.log-in-to-buy-button');
850 if (logInToBuyButtonEl) {
851 logInToBuyButtonEl.addEventListener('click', e => {
852 _pushUrl('/');
853
854 e.preventDefault();
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 // const infoboxEl = _findParentNode(assetsEl, '.infobox');
869 // infoboxEl.style.display = 'none';
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 // const infoboxEl = _findParentNode(assetsEl, '.infobox');
888 // infoboxEl.style.display = 'flex';
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
981const _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 // .map(_normalizeItem)
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 // .map(_normalizeItem)
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 // .map(_normalizeItem)
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 // .map(_normalizeItem)
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 // .map(_normalizeItem)
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};
1144const _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}&nbsp;☄</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>&nbsp;☄</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};
1185const _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};
1210const infoboxTypes = [
1211 'item',
1212 'mod',
1213 'skin',
1214 'file',
1215];
1216const _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 // const infoboxEl = _findParentNode(assetsEl, '.infobox');
1248 // infoboxEl.style.display = 'none';
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 // const infoboxEl = _findParentNode(assetsEl, '.infobox');
1275 // infoboxEl.style.display = 'flex';
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
1533const avatarCanvasSize = 250;
1534const _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
1595const upVector = new THREE.Vector3(0, 1, 0);
1596const zeroQuaternion = new THREE.Quaternion();
1597const headBobRotation = new THREE.Quaternion().setFromUnitVectors(
1598 new THREE.Vector3(0, 0, 1),
1599 new THREE.Vector3(0, 0.05, 1).normalize()
1600);
1601const headRotation = new THREE.Quaternion().setFromUnitVectors(
1602 new THREE.Vector3(0, 0, 1),
1603 new THREE.Vector3(0.3, 0.1, 1).normalize()
1604);
1605const leftArmRotation = new THREE.Quaternion().setFromUnitVectors(
1606 new THREE.Vector3(0, 0, 1),
1607 new THREE.Vector3(1, 0.2, 1).normalize()
1608);
1609const rightArmRotation = new THREE.Quaternion().setFromUnitVectors(
1610 new THREE.Vector3(0, 0, 1),
1611 new THREE.Vector3(0.2, 1, -1).normalize()
1612);
1613let 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});
1627const _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 // limbs: true,
1718 });
1719 mesh.meshType = 'skin';
1720 } else {
1721 mesh = hmdMesh;
1722 }
1723
1724 mesh.position.set(0, -0.75, 0);
1725 /* mesh.quaternion.setFromAxisAngle(
1726 upVector,
1727 Math.PI
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 // skinCanvasEl.ctx.imageSmoothingEnabled = false;
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
1786const _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};
1815const _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
1955const _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};
1975const _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
1998const stripeConnectDisconnectButtonEl = document.getElementById('stripe-connect-disconnect-button');
1999stripeConnectDisconnectButtonEl.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
2020let username = null;
2021let stripeConnectUser = null;
2022const _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};
2037const _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
2050const _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 );
2074const _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};
2133const _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};
2173const _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
2229const inventoryLinks = [];
2230let inventoryType = null;
2231for (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
2256const _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
2273const headerButtonsEl = document.getElementById('header-buttons');
2274headerButtonsEl.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});
2289headerButtonsEl.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
2301let uploadSkinEl = document.getElementById('upload-skin');
2302const _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};
2313uploadSkinEl.addEventListener('change', _uploadSkinChange);
2314
2315let uploadFileEl = document.getElementById('upload-file');
2316const _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};
2327uploadFileEl.addEventListener('change', _uploadFileChange);
2328
2329const _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};
2370let registerSkinEl = document.getElementById('register-skin');
2371const _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};
2382registerSkinEl.addEventListener('change', _registerSkinChange);
2383
2384const _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};
2432let registerFileEl = document.getElementById('register-file');
2433const _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};
2444registerFileEl.addEventListener('change', _registerFileChange);
2445
2446const storeIndexTypes = [];
2447let storeIndexType = 1;
2448for (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
2464const _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});
2475const _getQuestionMarkImage = () => _requestImage('/img/question.png');
2476const _getModImage = () => _requestImage('/img/mod.png');
2477
2478const storeStoreEl = document.getElementById('store-store');
2479const storeIndexEl = document.getElementById('store-index');
2480const storeModeLinks = [];
2481let storeMode = 1;
2482for (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
2504const storeLinks = [];
2505let storeLink = 1;
2506for (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
2521const registerItemButtonEl = document.getElementById('register-item-button');
2522registerItemButtonEl.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
2686const usernameFormEl = document.getElementById('username-form');
2687const usernameInputEl = document.getElementById('username-input');
2688const saveUsernameButtonEl = document.getElementById('save-username-button');
2689usernameInputEl.addEventListener('keydown', e => {
2690 requestAnimationFrame(() => {
2691 saveUsernameButtonEl.style.display = username === usernameInputEl.value ? 'none' : null;
2692 });
2693});
2694usernameFormEl.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
2712let subscribe = false;
2713const subscriptionSaveButtonEl = document.getElementById('subscription-save-button');
2714const basicInputEl = document.getElementById('basic-input');
2715basicInputEl.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});
2722const goldInputEl = document.getElementById('gold-input');
2723goldInputEl.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});
2736const stripe = Stripe('pk_test_KjmGZ7ooNVFvzrrp9LbeWdRj');
2737const stripeElements = stripe.elements();
2738const card = stripeElements.create('card', {
2739 style: {
2740 base: {
2741 fontSize: '16px',
2742 lineHeight: '24px',
2743 },
2744 },
2745});
2746card.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
2755const subscribeFormEl = document.getElementById('subscribe-form');
2756subscribeFormEl.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
2835const downloadKeysButtonEl = document.getElementById('download-keys-button');
2836downloadKeysButtonEl.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
2855const notifications = [];
2856class Notification {
2857 constructor(text) {
2858 this.text = text;
2859 }
2860 set(text) {
2861 this.text = text;
2862
2863 _renderNotifications();
2864 }
2865}
2866const _addNotification = text => {
2867 const notification = new Notification(text);
2868 notifications.push(notification);
2869
2870 _renderNotifications();
2871
2872 return notification;
2873};
2874const _removeNotification = notification => {
2875 notifications.splice(notifications.indexOf(notification), 1);
2876
2877 _renderNotifications();
2878};
2879const _renderNotifications = () => {
2880 document.getElementById('notifications').innerText = notifications.map(({text}) => text).join('\n');
2881};
2882
2883const _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};
2893const _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});
2905const _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});
2911const _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};
2926const _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};
2965const _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>&nbsp;<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};
2996const creditsEl = document.getElementById('credits');
2997const creditsStoreEl = document.getElementById('credits-store');
2998const _renderCredits = credits => {
2999 creditsEl.innerText = credits;
3000 creditsStoreEl.innerText = credits;
3001};
3002
3003const _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};
3077const _pushUrl = url => {
3078 window.history.pushState({}, '', url);
3079 _loadUrl(url);
3080};