arcane-os 0.35.0 → 0.36.0

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.
@@ -330,6 +330,88 @@
330
330
  color:var(--danger-color);
331
331
  }
332
332
 
333
+ .file-preview,
334
+ .file-preview-collection{
335
+ display:grid;
336
+ gap:1em;
337
+ min-width:0;
338
+ overflow-wrap:anywhere;
339
+ }
340
+
341
+ .file-preview h2,
342
+ .file-preview h3,
343
+ .file-preview h4,
344
+ .file-preview p{
345
+ margin-top:0;
346
+ }
347
+
348
+ .file-preview pre{
349
+ margin:0;
350
+ overflow-wrap:anywhere;
351
+ white-space:pre-wrap;
352
+ user-select:text;
353
+ }
354
+
355
+ .file-preview img{
356
+ max-width:100%;
357
+ height:auto;
358
+ }
359
+
360
+ .file-preview audio,
361
+ .file-preview iframe{
362
+ width:100%;
363
+ }
364
+
365
+ .file-preview iframe{
366
+ min-height:60vh;
367
+ border:0;
368
+ }
369
+
370
+ .file-preview table{
371
+ display:block;
372
+ max-width:100%;
373
+ overflow:auto;
374
+ }
375
+
376
+ .file-preview-conversation{
377
+ display:grid;
378
+ gap:1em;
379
+ }
380
+
381
+ .file-preview-message{
382
+ display:grid;
383
+ gap:.5em;
384
+ padding:1em;
385
+ border:1px solid var(--border-color);
386
+ border-radius:.5em;
387
+ background:var(--background);
388
+ }
389
+
390
+ .file-preview-message header,
391
+ .file-preview-message footer{
392
+ display:flex;
393
+ flex-wrap:wrap;
394
+ gap:.75em;
395
+ }
396
+
397
+ .file-preview-message footer{
398
+ font-size:.85em;
399
+ }
400
+
401
+ .file-preview-download:focus-visible{
402
+ outline:2px solid var(--focus-color,var(--accent-color));
403
+ outline-offset:.2em;
404
+ }
405
+
406
+ </style>
407
+
408
+ <!-- Host preview defaults stay separate from the presentation styles copied into modals. -->
409
+ <style>
410
+ :host{
411
+ --modal-width:min(72rem,calc(100vw - 2rem));
412
+ --modal-max-height:calc(100vh - 2rem);
413
+ --modal-padding:1.25rem;
414
+ }
333
415
  </style>
334
416
 
335
417
  <div class="file-manager">
@@ -373,7 +455,8 @@
373
455
  ).filter(Boolean);
374
456
  const fileEntityPromise=import('../entities/File.js');
375
457
 
376
- let activeFileURL='';
458
+ let activePreview=null;
459
+ let markdownModulePromise=null;
377
460
  let uploadDirectory='';
378
461
  let initialized=false;
379
462
  let provider=null;
@@ -396,6 +479,7 @@
396
479
  host.loadAll=loadAll;
397
480
  host.setProvider=setProvider;
398
481
  host.setFilter=setFilter;
482
+ host.setDirectoryFilter=setDirectoryFilter;
399
483
  host.select=select;
400
484
  host.clearSelection=clearSelection;
401
485
  host.destroy=destroy;
@@ -434,24 +518,66 @@
434
518
  return [];
435
519
  }
436
520
 
437
- function isVisibleEntry(entry={}){
521
+ function filterContext(directory='',fileName=''){
522
+ return {
523
+ directory,
524
+ fileName,
525
+ path:joinProviderPath(directory,fileName),
526
+ layout
527
+ };
528
+ }
529
+
530
+ function isVisibleDirectory(directory='',entry=null){
531
+ const candidate=entry||{name:directory,path:directory,kind:'directory'};
532
+ if(configuredHiddenPrefixes().some(function hasHiddenPrefix(prefix){
533
+ return String(candidate.name||'').startsWith(prefix);
534
+ })){
535
+ return false;
536
+ }
537
+ return !is.function(host.directoryFilter)
538
+ ||host.directoryFilter(candidate,filterContext(directory))!==false;
539
+ }
540
+
541
+ function isVisibleEntry(entry={},context=null){
438
542
  const normalized=is.string(entry)
439
543
  ?{name:entry,path:entry,kind:'file'}
440
544
  :entry;
545
+ const directory=normalized.directory
546
+ ||(normalized.kind==='directory'?normalized.path:'');
547
+ const details=context||filterContext(directory,normalized.fileName||normalized.name);
548
+
549
+ if(normalized.kind==='directory'
550
+ &&!isVisibleDirectory(normalized.path||normalized.name,normalized)){
551
+ return false;
552
+ }
441
553
  const activeFilter=filter||provider?.filter;
442
554
 
443
555
  if(is.function(activeFilter)){
444
- return activeFilter(normalized)!==false;
556
+ return activeFilter(normalized,details)!==false;
445
557
  }
446
558
 
447
559
  const name=String(normalized.name||'');
448
560
  return !configuredHiddenPrefixes().some(prefix=>name.startsWith(prefix));
449
561
  }
450
562
 
451
- function isVisibleFileName(fileName=''){
452
- return isVisibleEntry({name:fileName,path:fileName,kind:'file'});
563
+ function visibleFileNames(fileNames=[],directory=''){
564
+ return fileNames.filter(function isVisibleFileName(fileName){
565
+ return isVisibleEntry(
566
+ {name:fileName,path:fileName,kind:'file'},
567
+ filterContext(directory,fileName)
568
+ );
569
+ });
453
570
  }
454
571
 
572
+ fileModal.addEventListener(
573
+ 'modal-closed',
574
+ function closeFilePreview(){
575
+ fileOpenGeneration+=1;
576
+ releasePreview(activePreview,'file-preview-closed');
577
+ },
578
+ {signal:lifecycleController.signal}
579
+ );
580
+
455
581
  fileUpload.addEventListener(
456
582
  'change',
457
583
  ()=>uploadFiles().catch(
@@ -572,6 +698,21 @@
572
698
  return destroyed||generation!==filterGeneration?null:filter;
573
699
  }
574
700
 
701
+ async function setDirectoryFilter(nextFilter=null){
702
+ if(destroyed){
703
+ return null;
704
+ }
705
+ if(nextFilter!==null&&!is.function(nextFilter)){
706
+ throw new TypeError('File manager directory filters must be functions or null.');
707
+ }
708
+ const generation=++filterGeneration;
709
+ host.directoryFilter=nextFilter;
710
+ if(initialized){
711
+ await loadAll();
712
+ }
713
+ return destroyed||generation!==filterGeneration?null:host.directoryFilter;
714
+ }
715
+
575
716
  function normalizeProviderPath(path=''){
576
717
  return String(path||'')
577
718
  .replaceAll('\\','/')
@@ -632,7 +773,7 @@
632
773
  for(const candidate of entries){
633
774
  const entry=normalizeProviderEntry(candidate,parentPath);
634
775
 
635
- if(entry&&isVisibleEntry(entry)&&!unique.has(entry.path)){
776
+ if(entry&&isVisibleEntry(entry,filterContext(parentPath,entry.name))&&!unique.has(entry.path)){
636
777
  unique.set(entry.path,entry);
637
778
  }
638
779
  }
@@ -665,9 +806,10 @@
665
806
  }));
666
807
  }
667
808
 
668
- const files=(await dbopfs.getAllKeys(normalizedPath))
669
- .filter(isVisibleFileName)
670
- .sort();
809
+ if(!isVisibleDirectory(normalizedPath)){
810
+ return [];
811
+ }
812
+ const files=visibleFileNames(await dbopfs.getAllKeys(normalizedPath),normalizedPath).sort();
671
813
  const metadata=await getFilesMetadata(normalizedPath,files);
672
814
 
673
815
  return files.map((fileName,index)=>({
@@ -1301,7 +1443,7 @@
1301
1443
  }
1302
1444
 
1303
1445
  async function openDirectory(directory=''){
1304
- if(destroyed){
1446
+ if(destroyed||!isVisibleDirectory(directory)){
1305
1447
  return false;
1306
1448
  }
1307
1449
  const generation=++directoryOpenGeneration;
@@ -1315,16 +1457,14 @@
1315
1457
  },
1316
1458
  {signal:lifecycleController.signal}
1317
1459
  );
1318
- if(destroyed||generation!==directoryOpenGeneration){
1460
+ if(destroyed||generation!==directoryOpenGeneration||!isVisibleDirectory(directory)){
1319
1461
  return false;
1320
1462
  }
1321
1463
  const keys=await dbopfs.getAllKeys(directory);
1322
1464
  if(destroyed||generation!==directoryOpenGeneration){
1323
1465
  return false;
1324
1466
  }
1325
- const files=keys
1326
- .filter(isVisibleFileName)
1327
- .sort();
1467
+ const files=visibleFileNames(keys,directory).sort();
1328
1468
  const metadata=await getFilesMetadata(directory,files);
1329
1469
  if(destroyed||generation!==directoryOpenGeneration){
1330
1470
  return false;
@@ -1382,20 +1522,39 @@
1382
1522
  if(destroyed){
1383
1523
  return false;
1384
1524
  }
1525
+ releasePreview(activePreview,'file-preview-replaced');
1385
1526
  const generation=++fileOpenGeneration;
1386
- const modal=await waitForComponent(
1387
- fileModal,
1388
- {
1389
- methods:['populate','open'],
1390
- property:'ready',
1391
- event:'modal-ready',
1392
- signal:lifecycleController.signal
1393
- },
1394
- {signal:lifecycleController.signal}
1395
- );
1396
- if(destroyed||generation!==fileOpenGeneration){
1527
+ const preview={
1528
+ controller:new AbortController(),
1529
+ generation,
1530
+ urls:new Set(),
1531
+ media:new Set()
1532
+ };
1533
+ activePreview=preview;
1534
+ let modal;
1535
+ try{
1536
+ modal=await waitForComponent(
1537
+ fileModal,
1538
+ {
1539
+ methods:['populate','open'],
1540
+ property:'ready',
1541
+ event:'modal-ready',
1542
+ signal:preview.controller.signal
1543
+ },
1544
+ {signal:preview.controller.signal}
1545
+ );
1546
+ }catch(error){
1547
+ const current=isCurrentPreview(preview);
1548
+ releasePreview(preview,'file-preview-unavailable');
1549
+ if(current){
1550
+ throw error;
1551
+ }
1397
1552
  return false;
1398
1553
  }
1554
+ if(!isCurrentPreview(preview)){
1555
+ return false;
1556
+ }
1557
+ configureFileModal(modal);
1399
1558
  const content=document.createElement('section');
1400
1559
  const styles=componentStyles.cloneNode(true);
1401
1560
  const heading=document.createElement('h1');
@@ -1413,18 +1572,17 @@
1413
1572
  deleteIcon.className='action-icon';
1414
1573
  deleteIcon.src=new URL('../img/trash.svg',componentHref).href;
1415
1574
  deleteButton.prepend(deleteIcon);
1416
- deleteButton.addEventListener(
1417
- 'click',
1418
- ()=>confirmDelete(directory,fileName,true).catch(
1575
+ deleteButton.onclick=function deletePreviewFile(){
1576
+ return confirmDelete(directory,fileName,true).catch(
1419
1577
  error=>{
1420
1578
  if(!destroyed){
1421
1579
  arcaneLogging.error('Unable to delete file:',error);
1422
1580
  }
1423
1581
  }
1424
- ),
1425
- {signal:lifecycleController.signal}
1426
- );
1582
+ );
1583
+ };
1427
1584
  status.innerText='Opening file...';
1585
+ status.setAttribute('role','status');
1428
1586
  actions.append(deleteButton);
1429
1587
  content.append(
1430
1588
  styles,
@@ -1435,107 +1593,363 @@
1435
1593
  );
1436
1594
 
1437
1595
  await modal.populate(content,false);
1438
- if(destroyed||generation!==fileOpenGeneration){
1596
+ if(!isCurrentPreview(preview)){
1439
1597
  return false;
1440
1598
  }
1441
1599
  modal.open();
1442
1600
 
1443
1601
  try{
1444
1602
  const {default:FileEntity}=await fileEntityPromise;
1445
- if(destroyed||generation!==fileOpenGeneration){
1603
+ if(!isCurrentPreview(preview)){
1446
1604
  return false;
1447
1605
  }
1448
1606
  const file=new FileEntity(fileName,directory);
1449
1607
  const fileRef=await file.open();
1450
- if(destroyed||generation!==fileOpenGeneration){
1608
+ if(!isCurrentPreview(preview)){
1451
1609
  return false;
1452
1610
  }
1453
1611
 
1454
- status.remove();
1455
-
1456
- const rendered=await renderFile(fileRef,{directory,fileName});
1457
- if(destroyed||generation!==fileOpenGeneration){
1612
+ const rendered=await renderFile(fileRef,{directory,fileName},preview);
1613
+ if(!isCurrentPreview(preview)){
1614
+ releasePreview(preview,'file-preview-superseded');
1458
1615
  return false;
1459
1616
  }
1617
+ status.remove();
1460
1618
  content.append(rendered);
1461
1619
  return true;
1462
1620
  }catch(error){
1463
- if(!destroyed&&generation===fileOpenGeneration){
1621
+ if(isCurrentPreview(preview)){
1464
1622
  status.innerText='Unable to open this file.';
1623
+ status.setAttribute('role','alert');
1465
1624
  arcaneLogging.error('Unable to open file:',error);
1466
1625
  }
1626
+ releasePreview(preview,'file-preview-failed');
1467
1627
  return false;
1468
1628
  }
1469
1629
  }
1470
1630
 
1471
- async function renderFile(fileRef,context={}){
1472
- if(activeFileURL){
1473
- URL.revokeObjectURL(activeFileURL);
1474
- activeFileURL='';
1631
+ function configureFileModal(modal){
1632
+ const appearance=getComputedStyle(host);
1633
+ for(const property of ['--modal-width','--modal-max-height','--modal-padding']){
1634
+ if(!modal.style.getPropertyValue(property)){
1635
+ modal.style.setProperty(property,appearance.getPropertyValue(property).trim());
1636
+ }
1475
1637
  }
1638
+ }
1476
1639
 
1477
- if(['png','jpg','jpeg','webp','svg'].includes(fileRef.ext)){
1478
- const image=document.createElement('img');
1640
+ function isCurrentPreview(preview){
1641
+ return !destroyed
1642
+ &&activePreview===preview
1643
+ &&preview.generation===fileOpenGeneration
1644
+ &&!preview.controller.signal.aborted;
1645
+ }
1479
1646
 
1480
- activeFileURL=URL.createObjectURL(
1481
- new Blob([fileRef],{type:fileRef.mime})
1482
- );
1483
- image.alt=fileRef.name;
1484
- image.src=activeFileURL;
1485
- image.style.maxWidth='100%';
1486
- return image;
1647
+ function releasePreview(preview,reason){
1648
+ if(!preview){
1649
+ return;
1650
+ }
1651
+ preview.controller.abort(reason);
1652
+ for(const element of preview.media){
1653
+ if(element.localName==='audio'){
1654
+ element.pause();
1655
+ }
1656
+ if(element.localName==='iframe'){
1657
+ element.removeAttribute('srcdoc');
1658
+ }
1659
+ element.removeAttribute(element.localName==='a'?'href':'src');
1660
+ if(element.localName==='audio'){
1661
+ element.load();
1662
+ }
1663
+ }
1664
+ for(const url of preview.urls){
1665
+ URL.revokeObjectURL(url);
1487
1666
  }
1667
+ preview.media.clear();
1668
+ preview.urls.clear();
1669
+ if(activePreview===preview){
1670
+ activePreview=null;
1671
+ }
1672
+ }
1488
1673
 
1489
- if(fileRef.ext==='pdf'){
1490
- const frame=document.createElement('iframe');
1674
+ function previewObjectURL(content,preview){
1675
+ if(!(content instanceof Blob)){
1676
+ throw new TypeError('File preview media and downloads require a Blob or File.');
1677
+ }
1678
+ preview.controller.signal.throwIfAborted();
1679
+ const url=URL.createObjectURL(content);
1680
+ preview.urls.add(url);
1681
+ return url;
1682
+ }
1491
1683
 
1492
- activeFileURL=URL.createObjectURL(
1493
- new Blob([fileRef],{type:fileRef.mime})
1494
- );
1495
- frame.src=activeFileURL;
1496
- frame.style.height='60vh';
1497
- frame.style.width='100%';
1498
- frame.title=fileRef.name;
1499
- return frame;
1684
+ async function renderDescriptor(descriptor,preview,depth=0){
1685
+ preview.controller.signal.throwIfAborted();
1686
+ if(!descriptor||!is.object(descriptor)||is.array(descriptor)){
1687
+ throw new TypeError('File preview descriptors must be objects.');
1688
+ }
1689
+ const rendered=document.createElement('section');
1690
+ rendered.className='file-preview';
1691
+ if(descriptor.title!==undefined){
1692
+ const heading=document.createElement(`h${Math.min(depth+2,6)}`);
1693
+ heading.textContent=descriptor.title;
1694
+ rendered.append(heading);
1500
1695
  }
1501
1696
 
1502
- if(!layout&&['html','md'].includes(fileRef.ext)){
1503
- const rendered=document.createElement('div');
1697
+ switch(descriptor.kind){
1698
+ case 'text':
1699
+ case 'markdown':
1700
+ case 'html':{
1701
+ if(!is.string(descriptor.content)){
1702
+ throw new TypeError(`File preview ${descriptor.kind} content must be a string.`);
1703
+ }
1704
+ if(descriptor.kind==='text'){
1705
+ rendered.append(createPreformatted(descriptor.content));
1706
+ break;
1707
+ }
1708
+ const body=document.createElement(descriptor.kind==='html'?'iframe':'div');
1709
+ if(descriptor.kind==='markdown'){
1710
+ markdownModulePromise=markdownModulePromise||import('../modules/MD.js');
1711
+ const {default:MD}=await markdownModulePromise;
1712
+ preview.controller.signal.throwIfAborted();
1713
+ body.innerHTML=new MD(descriptor.content).safeRendered;
1714
+ }else{
1715
+ body.title=descriptor.title||'HTML preview';
1716
+ body.srcdoc=descriptor.content;
1717
+ preview.media.add(body);
1718
+ }
1719
+ rendered.append(body);
1720
+ break;
1721
+ }
1722
+ case 'json':
1723
+ rendered.append(createPreformatted(JSON.stringify(descriptor.content,null,4)));
1724
+ break;
1725
+ case 'conversation':{
1726
+ if(!is.array(descriptor.messages)){
1727
+ throw new TypeError('File preview conversation messages must be an array.');
1728
+ }
1729
+ const messages=document.createElement('div');
1730
+ messages.className='file-preview-conversation';
1731
+ const entries=await Promise.all(
1732
+ descriptor.messages.map(async function renderConversationMessage(message){
1733
+ if(!message||!is.string(message.role)||!is.string(message.content)){
1734
+ throw new TypeError('File preview messages require role and content strings.');
1735
+ }
1736
+ const article=document.createElement('article');
1737
+ const header=document.createElement('header');
1738
+ const speaker=document.createElement('strong');
1739
+ const footer=document.createElement('footer');
1740
+ const time=document.createElement('time');
1741
+ article.className='file-preview-message';
1742
+ article.dataset.role=message.role;
1743
+ speaker.textContent=message.name===undefined
1744
+ ?message.role
1745
+ :`${message.name} · ${message.role}`;
1746
+ header.append(speaker);
1747
+ if(message.timestamp===undefined||message.timestamp===null){
1748
+ time.textContent='Time unavailable';
1749
+ }else{
1750
+ const timestamp=new Date(message.timestamp);
1751
+ if(is.finite(timestamp.getTime())){
1752
+ time.dateTime=timestamp.toISOString();
1753
+ time.textContent=timestamp.toLocaleString();
1754
+ }else{
1755
+ time.textContent=String(message.timestamp);
1756
+ }
1757
+ }
1758
+ footer.append(time);
1759
+ if(message.status!==undefined){
1760
+ const state=document.createElement('span');
1761
+ state.textContent=message.status;
1762
+ footer.append(state);
1763
+ }
1764
+ const body=await renderDescriptor(
1765
+ {kind:'markdown',content:message.content},
1766
+ preview,
1767
+ depth+1
1768
+ );
1769
+ article.append(header,body,footer);
1770
+ return article;
1771
+ })
1772
+ );
1773
+ preview.controller.signal.throwIfAborted();
1774
+ for(const entry of entries){
1775
+ messages.append(entry);
1776
+ }
1777
+ rendered.append(messages);
1778
+ break;
1779
+ }
1780
+ case 'collection':{
1781
+ if(!is.array(descriptor.items)){
1782
+ throw new TypeError('File preview collection items must be an array.');
1783
+ }
1784
+ const items=await Promise.all(
1785
+ descriptor.items.map(function renderCollectionItem(item){
1786
+ return renderDescriptor(item,preview,depth+1);
1787
+ })
1788
+ );
1789
+ preview.controller.signal.throwIfAborted();
1790
+ const collection=document.createElement('div');
1791
+ collection.className='file-preview-collection';
1792
+ for(const item of items){
1793
+ collection.append(item);
1794
+ }
1795
+ rendered.append(collection);
1796
+ break;
1797
+ }
1798
+ case 'image':
1799
+ case 'audio':
1800
+ case 'pdf':
1801
+ case 'download':{
1802
+ if(descriptor.kind==='download'&&!is.string(descriptor.fileName)){
1803
+ throw new TypeError('File preview downloads require a fileName string.');
1804
+ }
1805
+ if(is.function(descriptor.content)){
1806
+ const item=document.createElement('div');
1807
+ const loadButton=document.createElement('button');
1808
+ const state=document.createElement('p');
1809
+ loadButton.type='button';
1810
+ loadButton.textContent=`Load ${descriptor.title||descriptor.fileName||descriptor.kind}`;
1811
+ state.setAttribute('role','status');
1812
+ loadButton.addEventListener(
1813
+ 'click',
1814
+ async function loadPreviewMedia(){
1815
+ loadButton.disabled=true;
1816
+ state.setAttribute('role','status');
1817
+ state.textContent='Loading...';
1818
+ try{
1819
+ const content=await descriptor.content({signal:preview.controller.signal});
1820
+ if(!isCurrentPreview(preview)){
1821
+ return;
1822
+ }
1823
+ item.replaceChildren(createPreviewMedia(descriptor,content,preview));
1824
+ }catch(error){
1825
+ if(isCurrentPreview(preview)){
1826
+ state.setAttribute('role','alert');
1827
+ state.textContent='Unable to load this item.';
1828
+ loadButton.disabled=false;
1829
+ arcaneLogging.error('Unable to load file preview item:',error);
1830
+ }
1831
+ }
1832
+ },
1833
+ {signal:preview.controller.signal}
1834
+ );
1835
+ item.append(loadButton,state);
1836
+ rendered.append(item);
1837
+ }else{
1838
+ rendered.append(createPreviewMedia(descriptor,descriptor.content,preview));
1839
+ }
1840
+ break;
1841
+ }
1842
+ default:
1843
+ throw new TypeError(`Unsupported file preview kind: ${String(descriptor.kind)}.`);
1844
+ }
1845
+ return rendered;
1846
+ }
1847
+
1848
+ function createPreviewMedia(descriptor,content,preview){
1849
+ const tag={image:'img',audio:'audio',pdf:'iframe',download:'a'}[descriptor.kind];
1850
+ const media=document.createElement(tag);
1851
+ const url=previewObjectURL(content,preview);
1852
+ preview.media.add(media);
1853
+ if(descriptor.kind==='download'){
1854
+ media.className='file-preview-download';
1855
+ media.href=url;
1856
+ media.download=descriptor.fileName;
1857
+ media.textContent=descriptor.title||descriptor.fileName;
1858
+ }else{
1859
+ media.src=url;
1860
+ if(descriptor.kind==='image'){
1861
+ media.alt=descriptor.alt??descriptor.title??content.name??'';
1862
+ }else if(descriptor.kind==='audio'){
1863
+ media.controls=true;
1864
+ media.preload='metadata';
1865
+ media.setAttribute('aria-label',descriptor.title||content.name||'Audio preview');
1866
+ }else{
1867
+ media.title=descriptor.title||content.name||'PDF preview';
1868
+ }
1869
+ }
1870
+ return media;
1871
+ }
1872
+
1873
+ async function renderFile(fileRef,context={},preview){
1874
+ const details={
1875
+ directory:String(context.directory||''),
1876
+ extension:String(fileRef.ext||''),
1877
+ fileName:String(context.fileName||fileRef.name||''),
1878
+ path:joinProviderPath(context.directory,context.fileName||fileRef.name),
1879
+ mimeType:String(fileRef.mime||fileRef.type||''),
1880
+ signal:preview.controller.signal
1881
+ };
1882
+ const mimeType=details.mimeType.split(';')[0].trim().toLowerCase();
1883
+ if(is.function(host.previewDescriptor)){
1884
+ const descriptor=await host.previewDescriptor(fileRef,details);
1885
+ preview.controller.signal.throwIfAborted();
1886
+ if(descriptor!==null&&descriptor!==undefined){
1887
+ return renderDescriptor(descriptor,preview);
1888
+ }
1889
+ }
1504
1890
 
1505
- rendered.innerHTML=fileRef.ext==='md'
1506
- ?fileRef.parsed
1507
- :await fileRef.text();
1508
- return rendered;
1891
+ if(mimeType.startsWith('image/')||['png','jpg','jpeg','webp','svg'].includes(fileRef.ext)){
1892
+ return renderDescriptor(
1893
+ {kind:'image',content:new Blob([fileRef],{type:fileRef.mime}),alt:fileRef.name},
1894
+ preview
1895
+ );
1896
+ }
1897
+ if(mimeType.startsWith('audio/')){
1898
+ return renderDescriptor(
1899
+ {kind:'audio',content:new Blob([fileRef],{type:fileRef.mime})},
1900
+ preview
1901
+ );
1902
+ }
1903
+ if(fileRef.ext==='pdf'||mimeType==='application/pdf'){
1904
+ return renderDescriptor(
1905
+ {kind:'pdf',content:new Blob([fileRef],{type:fileRef.mime}),title:fileRef.name},
1906
+ preview
1907
+ );
1908
+ }
1909
+ if(fileRef.ext==='html'||mimeType==='text/html'){
1910
+ const html=await fileRef.text();
1911
+ preview.controller.signal.throwIfAborted();
1912
+ return renderDescriptor({kind:'html',content:html,title:fileRef.name},preview);
1913
+ }
1914
+ if(fileRef.ext==='md'||mimeType==='text/markdown'){
1915
+ const markdown=await fileRef.text();
1916
+ preview.controller.signal.throwIfAborted();
1917
+ return renderDescriptor({kind:'markdown',content:markdown},preview);
1509
1918
  }
1510
1919
 
1511
1920
  const rendered=document.createElement('div');
1512
- let preview=fileRef.parsed;
1513
-
1514
- if(
1515
- ['json','jsonl','ndjson'].includes(fileRef.ext)
1516
- &&is.function(host.previewTransform)
1517
- ){
1518
- preview=await host.previewTransform(
1519
- preview,
1921
+ rendered.className='file-preview';
1922
+ let value=fileRef.parsed;
1923
+ const transformed=['json','jsonl','ndjson'].includes(fileRef.ext)
1924
+ &&is.function(host.previewTransform);
1925
+
1926
+ if(transformed){
1927
+ value=await host.previewTransform(
1928
+ value,
1520
1929
  {
1521
1930
  directory:String(context.directory||''),
1522
1931
  extension:String(fileRef.ext||''),
1523
1932
  fileName:String(context.fileName||fileRef.name||'')
1524
1933
  }
1525
1934
  );
1935
+ preview.controller.signal.throwIfAborted();
1526
1936
  }
1527
1937
 
1528
1938
  switch(fileRef.ext){
1529
1939
  case 'json':
1530
1940
  rendered.append(
1531
1941
  createPreformatted(
1532
- JSON.stringify(preview,null,4)
1942
+ JSON.stringify(value,null,4)
1533
1943
  )
1534
1944
  );
1535
1945
  break;
1536
1946
  case 'jsonl':
1537
1947
  case 'ndjson':{
1538
- const records=is.array(preview)?preview:[];
1948
+ if(!transformed){
1949
+ rendered.append(createPreformatted(await fileRef.text()));
1950
+ break;
1951
+ }
1952
+ const records=is.array(value)?value:[];
1539
1953
 
1540
1954
  for(let i=0;i<records.length;i++){
1541
1955
  rendered.append(
@@ -1551,11 +1965,23 @@
1551
1965
  break;
1552
1966
  }
1553
1967
  default:
1554
- rendered.append(
1555
- createPreformatted(await fileRef.text())
1556
- );
1968
+ if(mimeType.startsWith('text/')
1969
+ ||['application/json','application/javascript','application/xml'].includes(mimeType)
1970
+ ||mimeType.endsWith('+json')
1971
+ ||mimeType.endsWith('+xml')
1972
+ ||['txt','log','css','js','mjs'].includes(fileRef.ext)){
1973
+ rendered.append(createPreformatted(await fileRef.text()));
1974
+ }else{
1975
+ rendered.append(
1976
+ await renderDescriptor(
1977
+ {kind:'download',content:fileRef,fileName:details.fileName},
1978
+ preview
1979
+ )
1980
+ );
1981
+ }
1557
1982
  }
1558
1983
 
1984
+ preview.controller.signal.throwIfAborted();
1559
1985
  return rendered;
1560
1986
  }
1561
1987
 
@@ -1879,10 +2305,7 @@
1879
2305
  fileOpenGeneration+=1;
1880
2306
  deleteGeneration+=1;
1881
2307
  selectionGeneration+=1;
1882
- if(activeFileURL){
1883
- URL.revokeObjectURL(activeFileURL);
1884
- activeFileURL='';
1885
- }
2308
+ releasePreview(activePreview,'file-manager-destroyed');
1886
2309
  treeItems.clear();
1887
2310
  provider=null;
1888
2311
  filter=null;
@@ -1917,13 +2340,14 @@
1917
2340
 
1918
2341
  for(let i=0;i<tableNames.length;i++){
1919
2342
  const tableName=tableNames[i];
2343
+ if(!isVisibleDirectory(tableName)){
2344
+ continue;
2345
+ }
1920
2346
  const keys=await dbopfs.getAllKeys(tableName);
1921
2347
  if(destroyed||sequence!==gridLoadSequence){
1922
2348
  return [];
1923
2349
  }
1924
- const files=keys
1925
- .filter(isVisibleFileName)
1926
- .sort();
2350
+ const files=visibleFileNames(keys,tableName).sort();
1927
2351
  const metadata=layout==='files'
1928
2352
  ?await getFilesMetadata(tableName,files)
1929
2353
  :[];