vgapp 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +19 -16
  3. package/app/langs/en/buttons.json +17 -2
  4. package/app/langs/en/messages.json +36 -1
  5. package/app/langs/ru/buttons.json +17 -2
  6. package/app/langs/ru/messages.json +69 -34
  7. package/app/modules/module-fn.js +15 -9
  8. package/app/modules/vgfilepreview/index.js +3 -0
  9. package/app/modules/vgfilepreview/js/i18n.js +56 -0
  10. package/app/modules/vgfilepreview/js/renderers/image-modal.js +145 -0
  11. package/app/modules/vgfilepreview/js/renderers/image.js +92 -0
  12. package/app/modules/vgfilepreview/js/renderers/index.js +19 -0
  13. package/app/modules/vgfilepreview/js/renderers/office-modal.js +168 -0
  14. package/app/modules/vgfilepreview/js/renderers/office.js +79 -0
  15. package/app/modules/vgfilepreview/js/renderers/pdf-modal.js +260 -0
  16. package/app/modules/vgfilepreview/js/renderers/pdf.js +76 -0
  17. package/app/modules/vgfilepreview/js/renderers/playlist.js +71 -0
  18. package/app/modules/vgfilepreview/js/renderers/text-modal.js +343 -0
  19. package/app/modules/vgfilepreview/js/renderers/text.js +83 -0
  20. package/app/modules/vgfilepreview/js/renderers/video-modal.js +272 -0
  21. package/app/modules/vgfilepreview/js/renderers/video.js +80 -0
  22. package/app/modules/vgfilepreview/js/renderers/zip-modal.js +522 -0
  23. package/app/modules/vgfilepreview/js/renderers/zip.js +89 -0
  24. package/app/modules/vgfilepreview/js/vgfilepreview.js +532 -0
  25. package/app/modules/vgfilepreview/readme.md +68 -0
  26. package/app/modules/vgfilepreview/scss/_variables.scss +113 -0
  27. package/app/modules/vgfilepreview/scss/vgfilepreview.scss +460 -0
  28. package/app/modules/vgfiles/js/base.js +463 -175
  29. package/app/modules/vgfiles/js/droppable.js +260 -260
  30. package/app/modules/vgfiles/js/render.js +153 -153
  31. package/app/modules/vgfiles/js/vgfiles.js +41 -29
  32. package/app/modules/vgfiles/readme.md +116 -217
  33. package/app/modules/vgfiles/scss/_variables.scss +18 -10
  34. package/app/modules/vgfiles/scss/vgfiles.scss +153 -59
  35. package/app/modules/vgformsender/js/vgformsender.js +13 -13
  36. package/app/modules/vgmodal/js/vgmodal.js +12 -0
  37. package/app/modules/vgnav/js/vgnav.js +135 -135
  38. package/app/modules/vgnav/readme.md +67 -67
  39. package/app/modules/vgnestable/README.md +307 -307
  40. package/app/modules/vgnestable/scss/_variables.scss +60 -60
  41. package/app/modules/vgnestable/scss/vgnestable.scss +163 -163
  42. package/app/modules/vgselect/js/vgselect.js +39 -39
  43. package/app/modules/vgselect/scss/vgselect.scss +22 -22
  44. package/app/modules/vgspy/readme.md +28 -28
  45. package/app/utils/js/components/audio-metadata.js +240 -0
  46. package/app/utils/js/components/file-icon.js +109 -0
  47. package/app/utils/js/components/file-preview.js +304 -0
  48. package/app/utils/js/components/sanitize.js +150 -150
  49. package/build/vgapp.css +1 -1
  50. package/build/vgapp.css.map +1 -1
  51. package/index.js +1 -0
  52. package/index.scss +9 -6
  53. package/package.json +1 -1
@@ -1,10 +1,12 @@
1
1
  import BaseModule from "../../base-module";
2
2
  import {mergeDeepObject} from "../../../utils/js/functions";
3
3
  import Html from "../../../utils/js/components/templater";
4
- import {lang_messages} from "../../../utils/js/components/lang";
5
- import {Classes, Manipulator} from "../../../utils/js/dom/manipulator";
6
- import Selectors from "../../../utils/js/dom/selectors";
7
- import {getSVG} from "../../module-fn";
4
+ import {lang_messages} from "../../../utils/js/components/lang";
5
+ import {Classes, Manipulator} from "../../../utils/js/dom/manipulator";
6
+ import Selectors from "../../../utils/js/dom/selectors";
7
+ import {getSVG} from "../../module-fn";
8
+ import VGFilePreview from "../../vgfilepreview";
9
+ import {extractAudioMetadata} from "../../../utils/js/components/audio-metadata";
8
10
 
9
11
  class VGFilesBase extends BaseModule {
10
12
  constructor(element, params = {}, defaults = {}) {
@@ -19,10 +21,11 @@ class VGFilesBase extends BaseModule {
19
21
  }
20
22
  this._isInitialized = true;
21
23
 
22
- this._tpl = Html('dom');
23
- this._files = [];
24
- this._errors = new Set();
25
- this._objectUrls = [];
24
+ this._tpl = Html('dom');
25
+ this._files = [];
26
+ this._errors = new Set();
27
+ this._fileObjectUrls = new Map();
28
+ this._audioMetaPromises = new Map();
26
29
 
27
30
  this._nodes = {
28
31
  stat: Selectors.find(`.${this._getClass('stat')}`, this._element),
@@ -183,81 +186,98 @@ class VGFilesBase extends BaseModule {
183
186
  }
184
187
  }
185
188
 
186
- append(values, replace = true) {
187
- const incoming = this._applyRenameToIncomingFiles(Array.from(values));
188
- let filesToProcess;
189
-
190
- if (replace) {
191
- filesToProcess = incoming;
192
- } else {
193
- const fileMap = new Map(this._files.map(f => [this._getFileKey(f), f]));
194
- incoming.forEach(file => {
195
- fileMap.set(this._getFileKey(file), file);
196
- });
197
- filesToProcess = Array.from(fileMap.values());
198
- }
199
-
200
- this._files = this._filterFiles(filesToProcess);
201
- if (this._params.prepend) this._files.reverse();
202
-
203
- this._renderErrors();
204
-
205
- return this._files;
206
- }
207
-
208
- _getFileKey(file) {
209
- return `${file.name}-${file.size}-${file.type}`;
210
- }
211
-
212
- _getFileCustomData(file) {
213
- const customData = file?.customData;
214
- if (!customData || typeof customData !== 'object' || Array.isArray(customData)) return {};
215
- return customData;
216
- }
217
-
218
- _toDataAttributeKey(key) {
219
- if (!key) return '';
220
-
221
- return String(key)
222
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
223
- .replace(/[_\s]+/g, '-')
224
- .replace(/[^a-zA-Z0-9-]/g, '')
225
- .replace(/-+/g, '-')
226
- .replace(/^-|-$/g, '')
227
- .toLowerCase();
228
- }
229
-
230
- _toDataAttributeValue(value) {
231
- if (value === undefined || value === null || value === '') return null;
232
- if (typeof value === 'object') {
233
- try {
234
- return JSON.stringify(value);
235
- } catch (e) {
236
- return String(value);
237
- }
238
- }
239
- return value;
240
- }
241
-
242
- _buildFileDataAttributes(file, baseAttrs = {}) {
243
- const attrs = { ...baseAttrs };
244
- const customData = this._getFileCustomData(file);
245
-
246
- Object.entries(customData).forEach(([key, value]) => {
247
- const attrKey = this._toDataAttributeKey(key);
248
- if (!attrKey) return;
249
-
250
- const attrName = `data-${attrKey}`;
251
- if (Object.prototype.hasOwnProperty.call(attrs, attrName)) return;
252
-
253
- const attrValue = this._toDataAttributeValue(value);
254
- if (attrValue === null) return;
255
-
256
- attrs[attrName] = attrValue;
257
- });
258
-
259
- return attrs;
260
- }
189
+ append(values, replace = true) {
190
+ const incoming = this._applyRenameToIncomingFiles(Array.from(values));
191
+ let filesToProcess;
192
+
193
+ if (replace) {
194
+ filesToProcess = incoming;
195
+ } else {
196
+ filesToProcess = this._mergeFilesByOrder(incoming, this._files, Boolean(this._params.prepend));
197
+ }
198
+
199
+ this._files = this._filterFiles(filesToProcess);
200
+
201
+ this._renderErrors();
202
+ this._enrichAudioMetadata(this._files);
203
+
204
+ return this._files;
205
+ }
206
+
207
+ _mergeFilesByOrder(incoming = [], existing = [], prepend = false) {
208
+ const ordered = prepend
209
+ ? [...incoming, ...existing]
210
+ : [...existing, ...incoming];
211
+
212
+ const seen = new Set();
213
+ const result = [];
214
+
215
+ ordered.forEach((file) => {
216
+ const key = this._getFileKey(file);
217
+ if (seen.has(key)) {
218
+ return;
219
+ }
220
+
221
+ seen.add(key);
222
+ result.push(file);
223
+ });
224
+
225
+ return result;
226
+ }
227
+
228
+ _getFileKey(file) {
229
+ return `${file.name}-${file.size}-${file.type}`;
230
+ }
231
+
232
+ _getFileCustomData(file) {
233
+ const customData = file?.customData;
234
+ if (!customData || typeof customData !== 'object' || Array.isArray(customData)) return {};
235
+ return customData;
236
+ }
237
+
238
+ _toDataAttributeKey(key) {
239
+ if (!key) return '';
240
+
241
+ return String(key)
242
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
243
+ .replace(/[_\s]+/g, '-')
244
+ .replace(/[^a-zA-Z0-9-]/g, '')
245
+ .replace(/-+/g, '-')
246
+ .replace(/^-|-$/g, '')
247
+ .toLowerCase();
248
+ }
249
+
250
+ _toDataAttributeValue(value) {
251
+ if (value === undefined || value === null || value === '') return null;
252
+ if (typeof value === 'object') {
253
+ try {
254
+ return JSON.stringify(value);
255
+ } catch (e) {
256
+ return String(value);
257
+ }
258
+ }
259
+ return value;
260
+ }
261
+
262
+ _buildFileDataAttributes(file, baseAttrs = {}) {
263
+ const attrs = { ...baseAttrs };
264
+ const customData = this._getFileCustomData(file);
265
+
266
+ Object.entries(customData).forEach(([key, value]) => {
267
+ const attrKey = this._toDataAttributeKey(key);
268
+ if (!attrKey) return;
269
+
270
+ const attrName = `data-${attrKey}`;
271
+ if (Object.prototype.hasOwnProperty.call(attrs, attrName)) return;
272
+
273
+ const attrValue = this._toDataAttributeValue(value);
274
+ if (attrValue === null) return;
275
+
276
+ attrs[attrName] = attrValue;
277
+ });
278
+
279
+ return attrs;
280
+ }
261
281
 
262
282
  _filterFiles(files) {
263
283
  this._errors.clear();
@@ -434,15 +454,15 @@ class VGFilesBase extends BaseModule {
434
454
  if (part) parts.push(part);
435
455
  });
436
456
 
437
- const $li = this._tpl.li(
438
- this._buildFileDataAttributes(file, {
439
- 'data-name': file.name,
440
- 'data-size': file.size ?? 0,
441
- 'data-id': file.id || '',
442
- class: 'file ' + classes.join(' ')
443
- }),
444
- parts
445
- );
457
+ const $li = this._tpl.li(
458
+ this._buildFileDataAttributes(file, {
459
+ 'data-name': file.name,
460
+ 'data-size': file.size ?? 0,
461
+ 'data-id': file.id || '',
462
+ class: 'file ' + classes.join(' ')
463
+ }),
464
+ parts
465
+ );
446
466
 
447
467
  fragment.appendChild($li);
448
468
  });
@@ -466,8 +486,8 @@ class VGFilesBase extends BaseModule {
466
486
  Classes.add(this._nodes.drop, 'active');
467
487
  }
468
488
 
469
- _renderInfoList(files) {
470
- if (!this._nodes.info) return;
489
+ _renderInfoList(files) {
490
+ if (!this._nodes.info) return;
471
491
 
472
492
  let $list = Selectors.find(`.${this._getClass('info-list')}`, this._element);
473
493
  if (!$list) {
@@ -475,44 +495,149 @@ class VGFilesBase extends BaseModule {
475
495
  this._nodes.info.appendChild($list);
476
496
  }
477
497
 
478
- if (!this._params.info) Classes.add($list, 'list-row');
479
-
480
- $list.innerHTML = '';
481
-
482
- const $itemsTemplate = this._parseTemplate().children;
483
- const $itemsTemplateClasses = this._parseTemplate().liClasses.filter(cls => cls !== 'file');
484
- const fragment = document.createDocumentFragment();
485
-
486
- files.forEach((file, i) => {
487
- let classes = $itemsTemplateClasses;
488
-
489
- if (this._params.image) classes.push('with-image');
490
- if (this._params.info) classes.push('with-info');
491
- if (this._params.detach) classes.push('with-remove')
492
- if (this._params.sortable.enabled) classes.push('with-sortable');
493
-
494
- let parts = [];
495
- $itemsTemplate.forEach(tmpl => {
496
- const part = this._renderTemplatePart(tmpl.element, file, i);
497
- if (part) parts.push(part);
498
- });
499
-
500
- const $li = this._tpl.li(
501
- this._buildFileDataAttributes(file, {
502
- 'data-name': file.name,
503
- 'data-size': file.size ?? 0,
504
- 'data-type': file.type || '',
505
- 'data-id': file.id || '',
506
- class: 'file ' + classes.join(' ') + ' '
507
- }),
508
- parts
509
- );
510
- fragment.appendChild($li);
511
- });
512
- $list.appendChild(fragment);
513
-
514
- Classes.add(this._nodes.info, 'show')
515
- }
498
+ if (!this._params.info) Classes.add($list, 'list-row');
499
+
500
+ const $itemsTemplate = this._parseTemplate().children;
501
+ const $itemsTemplateClasses = this._parseTemplate().liClasses.filter(cls => cls !== 'file');
502
+ const currentChildren = Array.from($list.children || []);
503
+ const existingByKey = new Map();
504
+ currentChildren.forEach((child) => {
505
+ const key = this._getInfoNodeKey(child);
506
+ if (key) {
507
+ if (!child.hasAttribute('data-file-key')) {
508
+ child.setAttribute('data-file-key', key);
509
+ }
510
+ existingByKey.set(key, child);
511
+ }
512
+ });
513
+
514
+ const nextNodes = [];
515
+
516
+ files.forEach((file, i) => {
517
+ const fileKey = this._getFileKey(file);
518
+ const signature = this._getInfoItemSignature(file, i);
519
+ const existingNode = existingByKey.get(fileKey);
520
+
521
+ if (existingNode && existingNode.getAttribute('data-vg-info-signature') === signature) {
522
+ const iterationNode = existingNode.querySelector('.iteration');
523
+ if (iterationNode) {
524
+ iterationNode.textContent = `${i + 1}.`;
525
+ }
526
+ nextNodes.push(existingNode);
527
+ existingByKey.delete(fileKey);
528
+ return;
529
+ }
530
+
531
+ if (existingNode) {
532
+ existingNode.remove();
533
+ }
534
+
535
+ const nextNode = this._createInfoListItem(file, i, $itemsTemplate, $itemsTemplateClasses, fileKey, signature);
536
+ nextNodes.push(nextNode);
537
+ existingByKey.delete(fileKey);
538
+ });
539
+
540
+ if (existingByKey.size) {
541
+ const removedNodes = Array.from(existingByKey.values());
542
+ VGFilePreview.stopActiveInlineAudioIfDetached(removedNodes);
543
+ removedNodes.forEach((node) => node.remove());
544
+ }
545
+
546
+ nextNodes.forEach((node) => {
547
+ $list.appendChild(node);
548
+ });
549
+
550
+ this._initFilePreviewInInfo($list);
551
+
552
+ Classes.add(this._nodes.info, 'show')
553
+ }
554
+
555
+ _getInfoItemSignature(file, index) {
556
+ const displayName = this._resolveDisplayName(file);
557
+ const previewPath = this._resolveFilePreviewPath(file);
558
+ const id = file?.id || '';
559
+ const name = file?.name || '';
560
+ const size = file?.size ?? 0;
561
+ const type = file?.type || '';
562
+
563
+ return `${id}|${name}|${size}|${type}|${displayName}|${previewPath}`;
564
+ }
565
+
566
+ _getInfoNodeKey(node) {
567
+ if (!node || typeof node.getAttribute !== 'function') {
568
+ return '';
569
+ }
570
+
571
+ const fromAttr = String(node.getAttribute('data-file-key') || '').trim();
572
+ if (fromAttr) {
573
+ return fromAttr;
574
+ }
575
+
576
+ const name = String(node.getAttribute('data-name') || '').trim();
577
+ const size = String(node.getAttribute('data-size') || '').trim();
578
+ const type = String(node.getAttribute('data-type') || '').trim();
579
+ if (name && size && type) {
580
+ return `${name}-${size}-${type}`;
581
+ }
582
+
583
+ const raw = String(node.getAttribute('data-file') || '').trim();
584
+ if (!raw) {
585
+ return '';
586
+ }
587
+
588
+ try {
589
+ const parsed = JSON.parse(raw);
590
+ const pName = String(parsed?.name || '').trim();
591
+ const pSize = String(parsed?.size ?? '').trim();
592
+ const pType = String(parsed?.type || '').trim();
593
+ if (pName && pSize && pType) {
594
+ return `${pName}-${pSize}-${pType}`;
595
+ }
596
+ } catch {
597
+ return '';
598
+ }
599
+
600
+ return '';
601
+ }
602
+
603
+ _createInfoListItem(file, i, itemsTemplate, itemTemplateClasses, fileKey, signature) {
604
+ let classes = [...itemTemplateClasses];
605
+
606
+ if (this._params.image) classes.push('with-image');
607
+ if (this._params.info) classes.push('with-info');
608
+ if (this._params.detach) classes.push('with-remove')
609
+ if (this._params.sortable.enabled) classes.push('with-sortable');
610
+ const previewPath = this._resolveFilePreviewPath(file);
611
+ const displayName = this._resolveDisplayName(file);
612
+
613
+ let parts = [];
614
+ itemsTemplate.forEach(tmpl => {
615
+ const part = this._renderTemplatePart(tmpl.element, file, i);
616
+ if (part) parts.push(part);
617
+ });
618
+
619
+ const liAttrs = {
620
+ 'data-name': file.name,
621
+ 'data-size': file.size ?? 0,
622
+ 'data-type': file.type || '',
623
+ 'data-id': file.id || '',
624
+ 'data-file-key': fileKey,
625
+ 'data-vg-info-signature': signature,
626
+ class: 'file ' + classes.join(' ') + ' '
627
+ };
628
+
629
+ if (previewPath) {
630
+ liAttrs['data-vg-filepreview'] = previewPath;
631
+ liAttrs['data-fields'] = 'name,size,download';
632
+ liAttrs['data-original-name'] = file.name || '';
633
+ liAttrs['data-vg-filepreview-display-name'] = displayName || file.name || '';
634
+ }
635
+
636
+ return this._tpl.li(
637
+ this._buildFileDataAttributes(file, liAttrs),
638
+ parts
639
+ );
640
+ }
516
641
 
517
642
  _renderTemplatePart(element, file, index = null, options = {}) {
518
643
  if (!element) return null;
@@ -566,45 +691,205 @@ class VGFilesBase extends BaseModule {
566
691
  }
567
692
  }
568
693
 
569
- _renderUIInfo(file, i) {
570
- if (this._params.info) {
571
- return this._tpl.div({ class: 'file-info' }, [
572
- this._tpl.span({ class: 'iteration' }, `${i + 1}.`),
573
- this._tpl.span({ class: 'name' }, file.name),
574
- this._tpl.span({ class: 'size' }, `[${this._getSizes(file.size)}]`)
575
- ]);
576
- }
577
- }
578
-
579
- _renderUIImage(file) {
580
- const $container = this._tpl.div({ class: 'file-image' });
581
-
582
- const src = file?.src || file?.image;
583
- if (src) {
584
- $container.appendChild(this._tpl.img(src, file.name || '', { class: 'file-preview' }));
585
- return $container;
586
- }
587
-
588
- if (file?.type && file.type.startsWith('image/')) {
589
- const objectUrl = URL.createObjectURL(file);
590
- this._objectUrls.push(objectUrl);
591
- $container.appendChild(this._tpl.img(objectUrl, file.name, { class: 'file-preview' }));
592
- return $container;
593
- }
594
-
595
- const icon = this._getIconByFileType(file);
596
- $container.appendChild(this._tpl.i({}, icon, { isHTML: true }));
597
- return $container;
598
- }
599
-
600
- _getIconByFileType(file) {
601
- if (file.type === 'application/pdf') return getSVG('file-pdf');
602
- if (file.type.includes('word') || file.name.endsWith('.doc') || file.name.endsWith('.docx')) return getSVG('file-word');
603
- if (file.type.includes('excel') || file.name.endsWith('.xls') || file.name.endsWith('.xlsx')) return getSVG('file-exel');
604
- if (file.type === 'application/zip' || file.name.endsWith('.zip')) return getSVG('file-zip');
605
- if (file.name.endsWith('.txt')) return getSVG('file-text');
606
- return getSVG('file-generic');
607
- }
694
+ _renderUIInfo(file, i) {
695
+ if (this._params.info) {
696
+ const displayName = this._resolveDisplayName(file);
697
+ return this._tpl.div({ class: 'file-info' }, [
698
+ this._tpl.span({ class: 'iteration' }, `${i + 1}.`),
699
+ this._tpl.span({ class: 'name' }, displayName),
700
+ this._tpl.span({ class: 'size' }, `[${this._getSizes(file.size)}]`),
701
+ this._tpl.span({ class: 'download' }, '')
702
+ ]);
703
+ }
704
+ }
705
+
706
+ _renderUIImage(file) {
707
+ const $container = this._tpl.div({ class: 'file-image' });
708
+
709
+ const src = file?.src || file?.image;
710
+ if (src) {
711
+ $container.appendChild(this._tpl.img(src, file.name || '', { class: 'file-preview' }));
712
+ return $container;
713
+ }
714
+
715
+ const customData = this._getFileCustomData(file);
716
+ const audioCover = String(customData.audioCover || '').trim();
717
+ if (audioCover) {
718
+ $container.appendChild(this._tpl.img(audioCover, this._resolveDisplayName(file), { class: 'file-preview' }));
719
+ return $container;
720
+ }
721
+
722
+ if (file?.type && file.type.startsWith('image/')) {
723
+ const objectUrl = this._getFileObjectUrl(file);
724
+ if (!objectUrl) {
725
+ return $container;
726
+ }
727
+ $container.appendChild(this._tpl.img(objectUrl, file.name, { class: 'file-preview' }));
728
+ return $container;
729
+ }
730
+
731
+ const icon = this._getIconByFileType(file);
732
+ $container.appendChild(this._tpl.i({}, icon, { isHTML: true }));
733
+ return $container;
734
+ }
735
+
736
+ _resolveDisplayName(file) {
737
+ const customData = this._getFileCustomData(file);
738
+ const metaTitle = String(customData.audioTitle || '').trim();
739
+ if (metaTitle) {
740
+ return metaTitle;
741
+ }
742
+
743
+ return String(file?.name || '').trim();
744
+ }
745
+
746
+ _getIconByFileType(file) {
747
+ return getSVG(file);
748
+ }
749
+
750
+ _resolveFilePreviewPath(file) {
751
+ const src = String(file?.src || file?.image || '').trim();
752
+ if (src) {
753
+ return src;
754
+ }
755
+
756
+ return this._getFileObjectUrl(file);
757
+ }
758
+
759
+ _getFileObjectUrl(file) {
760
+ if (!file || typeof File === 'undefined' || !(file instanceof File)) {
761
+ return '';
762
+ }
763
+
764
+ const key = this._getFileKey(file);
765
+ if (this._fileObjectUrls.has(key)) {
766
+ return this._fileObjectUrls.get(key);
767
+ }
768
+
769
+ const objectUrl = URL.createObjectURL(file);
770
+ this._fileObjectUrls.set(key, objectUrl);
771
+ return objectUrl;
772
+ }
773
+
774
+ _isAudioFile(file) {
775
+ if (!file) {
776
+ return false;
777
+ }
778
+
779
+ const fileType = String(file.type || '').toLowerCase();
780
+ if (fileType.startsWith('audio/')) {
781
+ return true;
782
+ }
783
+
784
+ const name = String(file.name || '').toLowerCase();
785
+ return /\.(mp3|m4a|aac|wav|ogg|flac|opus|wma)$/.test(name);
786
+ }
787
+
788
+ _setFileCustomDataValue(file, key, value) {
789
+ if (!file || !key) {
790
+ return;
791
+ }
792
+
793
+ if (!file.customData || typeof file.customData !== 'object' || Array.isArray(file.customData)) {
794
+ Object.defineProperty(file, 'customData', {
795
+ value: {},
796
+ writable: true,
797
+ enumerable: true
798
+ });
799
+ }
800
+
801
+ file.customData[key] = value;
802
+ }
803
+
804
+ _enrichAudioMetadata(files = []) {
805
+ if (!Array.isArray(files) || !files.length) {
806
+ return;
807
+ }
808
+
809
+ const pending = [];
810
+
811
+ files.forEach((file) => {
812
+ if (!this._isAudioFile(file)) {
813
+ return;
814
+ }
815
+
816
+ const fileKey = this._getFileKey(file);
817
+ if (this._audioMetaPromises.has(fileKey)) {
818
+ return;
819
+ }
820
+
821
+ const customData = this._getFileCustomData(file);
822
+ if (customData.audioTitle || customData.audioCover) {
823
+ return;
824
+ }
825
+
826
+ const task = extractAudioMetadata(file)
827
+ .then((meta) => {
828
+ if (!meta) {
829
+ return false;
830
+ }
831
+
832
+ let changed = false;
833
+
834
+ if (meta.title) {
835
+ this._setFileCustomDataValue(file, 'audioTitle', meta.title);
836
+ changed = true;
837
+ }
838
+
839
+ if (meta.pictureBlob) {
840
+ const coverKey = `cover:${fileKey}`;
841
+ let coverUrl = this._fileObjectUrls.get(coverKey);
842
+ if (!coverUrl) {
843
+ coverUrl = URL.createObjectURL(meta.pictureBlob);
844
+ this._fileObjectUrls.set(coverKey, coverUrl);
845
+ }
846
+ this._setFileCustomDataValue(file, 'audioCover', coverUrl);
847
+ changed = true;
848
+ }
849
+
850
+ return changed;
851
+ })
852
+ .catch(() => false)
853
+ .then((result) => {
854
+ this._audioMetaPromises.delete(fileKey);
855
+ return result;
856
+ });
857
+
858
+ this._audioMetaPromises.set(fileKey, task);
859
+ pending.push(task);
860
+ });
861
+
862
+ if (!pending.length) {
863
+ return;
864
+ }
865
+
866
+ Promise.allSettled(pending).then((results) => {
867
+ const hasChanges = results.some((item) => item.status === 'fulfilled' && item.value === true);
868
+ if (!hasChanges || !this._files.length) {
869
+ return;
870
+ }
871
+
872
+ this._renderUI(this._files);
873
+ });
874
+ }
875
+
876
+ _initFilePreviewInInfo(root) {
877
+ if (!this._nodes.info || !root) {
878
+ return;
879
+ }
880
+
881
+ const lang = this._params?.lang || document.documentElement.lang || 'ru';
882
+ const nodes = Selectors.findAll('[data-vg-filepreview]', root) || [];
883
+
884
+ nodes.forEach((node) => {
885
+ VGFilePreview.getOrCreateInstance(node, {
886
+ lang,
887
+ ui: {
888
+ nameOnly: true
889
+ }
890
+ });
891
+ });
892
+ }
608
893
 
609
894
  _updateStat() {
610
895
  if (!this._nodes.stat) return;
@@ -684,10 +969,13 @@ class VGFilesBase extends BaseModule {
684
969
  }
685
970
  }
686
971
 
687
- _revokeUrls() {
688
- this._objectUrls.forEach(url => URL.revokeObjectURL(url));
689
- this._objectUrls = [];
690
- }
972
+ _revokeUrls() {
973
+ this._fileObjectUrls.forEach((url) => {
974
+ URL.revokeObjectURL(url);
975
+ });
976
+ this._fileObjectUrls.clear();
977
+ this._audioMetaPromises.clear();
978
+ }
691
979
 
692
980
  _resetFileInput() {
693
981
  Selectors.findAll('[data-vg-toggle="files"]', this._element).forEach(input => input.value = '');