@huyooo/file-explorer-core 0.3.0 → 0.4.2

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.
package/dist/index.js CHANGED
@@ -25,22 +25,22 @@ function encodeFileUrl(filePath) {
25
25
  }
26
26
  function decodeFileUrl(url) {
27
27
  if (!url) return "";
28
- let path12 = url;
29
- if (path12.startsWith(APP_PROTOCOL_PREFIX)) {
30
- path12 = path12.slice(APP_PROTOCOL_PREFIX.length);
28
+ let path18 = url;
29
+ if (path18.startsWith(APP_PROTOCOL_PREFIX)) {
30
+ path18 = path18.slice(APP_PROTOCOL_PREFIX.length);
31
31
  }
32
- const hashIndex = path12.indexOf("#");
32
+ const hashIndex = path18.indexOf("#");
33
33
  if (hashIndex !== -1) {
34
- path12 = path12.substring(0, hashIndex);
34
+ path18 = path18.substring(0, hashIndex);
35
35
  }
36
- const queryIndex = path12.indexOf("?");
36
+ const queryIndex = path18.indexOf("?");
37
37
  if (queryIndex !== -1) {
38
- path12 = path12.substring(0, queryIndex);
38
+ path18 = path18.substring(0, queryIndex);
39
39
  }
40
40
  try {
41
- return decodeURIComponent(path12);
41
+ return decodeURIComponent(path18);
42
42
  } catch {
43
- return path12;
43
+ return path18;
44
44
  }
45
45
  }
46
46
  function isAppProtocolUrl(url) {
@@ -388,16 +388,16 @@ async function createFile(filePath, content = "") {
388
388
  await fs2.writeFile(filePath, content, "utf-8");
389
389
  return { success: true, data: { finalPath: filePath } };
390
390
  }
391
- const dirname = path3.dirname(filePath);
391
+ const dirname3 = path3.dirname(filePath);
392
392
  const ext = path3.extname(filePath);
393
- const basename = path3.basename(filePath, ext);
393
+ const basename2 = path3.basename(filePath, ext);
394
394
  let counter = 2;
395
- let finalPath = path3.join(dirname, `${basename} ${counter}${ext}`);
395
+ let finalPath = path3.join(dirname3, `${basename2} ${counter}${ext}`);
396
396
  while (true) {
397
397
  try {
398
398
  await fs2.access(finalPath);
399
399
  counter++;
400
- finalPath = path3.join(dirname, `${basename} ${counter}${ext}`);
400
+ finalPath = path3.join(dirname3, `${basename2} ${counter}${ext}`);
401
401
  } catch {
402
402
  break;
403
403
  }
@@ -573,25 +573,487 @@ async function isDirectory(filePath) {
573
573
  }
574
574
  }
575
575
 
576
+ // src/operations/shell.ts
577
+ import { exec } from "child_process";
578
+ import { promisify } from "util";
579
+ import * as path6 from "path";
580
+ var execAsync = promisify(exec);
581
+ function getPlatform() {
582
+ switch (process.platform) {
583
+ case "darwin":
584
+ return "mac";
585
+ case "win32":
586
+ return "windows";
587
+ default:
588
+ return "linux";
589
+ }
590
+ }
591
+ async function showFileInfo(filePath) {
592
+ const platform2 = getPlatform();
593
+ try {
594
+ switch (platform2) {
595
+ case "mac": {
596
+ const script = `tell application "Finder"
597
+ activate
598
+ set theFile to POSIX file "${filePath}" as alias
599
+ open information window of theFile
600
+ end tell`;
601
+ await execAsync(`osascript -e '${script}'`);
602
+ break;
603
+ }
604
+ case "windows": {
605
+ const escapedPath = filePath.replace(/'/g, "''");
606
+ const psScript = `
607
+ $shell = New-Object -ComObject Shell.Application
608
+ $folder = $shell.Namespace((Split-Path '${escapedPath}'))
609
+ $item = $folder.ParseName((Split-Path '${escapedPath}' -Leaf))
610
+ $item.InvokeVerb('properties')
611
+ `;
612
+ await execAsync(`powershell -Command "${psScript.replace(/\n/g, " ")}"`);
613
+ break;
614
+ }
615
+ case "linux": {
616
+ const commands = [
617
+ `nautilus --select "${filePath}" && nautilus -q`,
618
+ // GNOME
619
+ `dolphin --select "${filePath}"`,
620
+ // KDE
621
+ `thunar --quit && thunar "${path6.dirname(filePath)}"`,
622
+ // XFCE
623
+ `xdg-open "${path6.dirname(filePath)}"`
624
+ // 通用
625
+ ];
626
+ let lastError = null;
627
+ for (const cmd of commands) {
628
+ try {
629
+ await execAsync(cmd);
630
+ return { success: true };
631
+ } catch (e) {
632
+ lastError = e;
633
+ }
634
+ }
635
+ throw lastError || new Error("No file manager found");
636
+ }
637
+ }
638
+ return { success: true };
639
+ } catch (error) {
640
+ return {
641
+ success: false,
642
+ error: error instanceof Error ? error.message : String(error)
643
+ };
644
+ }
645
+ }
646
+ async function revealInFileManager(filePath) {
647
+ const platform2 = getPlatform();
648
+ try {
649
+ switch (platform2) {
650
+ case "mac": {
651
+ await execAsync(`open -R "${filePath}"`);
652
+ break;
653
+ }
654
+ case "windows": {
655
+ await execAsync(`explorer.exe /select,"${filePath}"`);
656
+ break;
657
+ }
658
+ case "linux": {
659
+ await execAsync(`xdg-open "${path6.dirname(filePath)}"`);
660
+ break;
661
+ }
662
+ }
663
+ return { success: true };
664
+ } catch (error) {
665
+ return {
666
+ success: false,
667
+ error: error instanceof Error ? error.message : String(error)
668
+ };
669
+ }
670
+ }
671
+ async function openInTerminal(dirPath) {
672
+ const platform2 = getPlatform();
673
+ try {
674
+ switch (platform2) {
675
+ case "mac": {
676
+ try {
677
+ const itermScript = `tell application "iTerm"
678
+ activate
679
+ try
680
+ set newWindow to (create window with default profile)
681
+ tell current session of newWindow
682
+ write text "cd '${dirPath}'"
683
+ end tell
684
+ on error
685
+ tell current window
686
+ create tab with default profile
687
+ tell current session
688
+ write text "cd '${dirPath}'"
689
+ end tell
690
+ end tell
691
+ end try
692
+ end tell`;
693
+ await execAsync(`osascript -e '${itermScript}'`);
694
+ } catch {
695
+ const terminalScript = `tell application "Terminal"
696
+ activate
697
+ do script "cd '${dirPath}'"
698
+ end tell`;
699
+ await execAsync(`osascript -e '${terminalScript}'`);
700
+ }
701
+ break;
702
+ }
703
+ case "windows": {
704
+ try {
705
+ await execAsync(`wt -d "${dirPath}"`);
706
+ } catch {
707
+ await execAsync(`start cmd /K "cd /d ${dirPath}"`);
708
+ }
709
+ break;
710
+ }
711
+ case "linux": {
712
+ const terminals = [
713
+ `gnome-terminal --working-directory="${dirPath}"`,
714
+ `konsole --workdir "${dirPath}"`,
715
+ `xfce4-terminal --working-directory="${dirPath}"`,
716
+ `xterm -e "cd '${dirPath}' && $SHELL"`
717
+ ];
718
+ let lastError = null;
719
+ for (const cmd of terminals) {
720
+ try {
721
+ await execAsync(cmd);
722
+ return { success: true };
723
+ } catch (e) {
724
+ lastError = e;
725
+ }
726
+ }
727
+ throw lastError || new Error("No terminal found");
728
+ }
729
+ }
730
+ return { success: true };
731
+ } catch (error) {
732
+ return {
733
+ success: false,
734
+ error: error instanceof Error ? error.message : String(error)
735
+ };
736
+ }
737
+ }
738
+ async function openInEditor(targetPath) {
739
+ const platform2 = getPlatform();
740
+ try {
741
+ const editors = platform2 === "mac" ? [
742
+ // macOS: 优先 Cursor,回退到 VSCode
743
+ `open -a "Cursor" "${targetPath}"`,
744
+ `open -a "Visual Studio Code" "${targetPath}"`,
745
+ `code "${targetPath}"`
746
+ ] : platform2 === "windows" ? [
747
+ // Windows
748
+ `cursor "${targetPath}"`,
749
+ `code "${targetPath}"`
750
+ ] : [
751
+ // Linux
752
+ `cursor "${targetPath}"`,
753
+ `code "${targetPath}"`
754
+ ];
755
+ let lastError = null;
756
+ for (const cmd of editors) {
757
+ try {
758
+ await execAsync(cmd);
759
+ return { success: true };
760
+ } catch (e) {
761
+ lastError = e;
762
+ }
763
+ }
764
+ throw lastError || new Error("No editor found");
765
+ } catch (error) {
766
+ return {
767
+ success: false,
768
+ error: error instanceof Error ? error.message : String(error)
769
+ };
770
+ }
771
+ }
772
+
773
+ // src/operations/compress.ts
774
+ import * as compressing from "compressing";
775
+ import * as path7 from "path";
776
+ import * as fs7 from "fs";
777
+ import { promises as fsPromises } from "fs";
778
+ import { pipeline } from "stream/promises";
779
+ function getExtension(format) {
780
+ switch (format) {
781
+ case "zip":
782
+ return ".zip";
783
+ case "tar":
784
+ return ".tar";
785
+ case "tgz":
786
+ return ".tar.gz";
787
+ case "tarbz2":
788
+ return ".tar.bz2";
789
+ default:
790
+ return ".zip";
791
+ }
792
+ }
793
+ function detectArchiveFormat(filePath) {
794
+ const lowerPath = filePath.toLowerCase();
795
+ if (lowerPath.endsWith(".zip")) return "zip";
796
+ if (lowerPath.endsWith(".tar.gz") || lowerPath.endsWith(".tgz")) return "tgz";
797
+ if (lowerPath.endsWith(".tar.bz2") || lowerPath.endsWith(".tbz2")) return "tarbz2";
798
+ if (lowerPath.endsWith(".tar")) return "tar";
799
+ return null;
800
+ }
801
+ function isArchiveFile(filePath) {
802
+ return detectArchiveFormat(filePath) !== null;
803
+ }
804
+ async function getAllFiles(dirPath) {
805
+ const files = [];
806
+ const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
807
+ for (const entry of entries) {
808
+ const fullPath = path7.join(dirPath, entry.name);
809
+ if (entry.isDirectory()) {
810
+ files.push(...await getAllFiles(fullPath));
811
+ } else {
812
+ files.push(fullPath);
813
+ }
814
+ }
815
+ return files;
816
+ }
817
+ async function countFiles(sources) {
818
+ let count = 0;
819
+ for (const source of sources) {
820
+ const stats = await fsPromises.stat(source);
821
+ if (stats.isDirectory()) {
822
+ const files = await getAllFiles(source);
823
+ count += files.length;
824
+ } else {
825
+ count += 1;
826
+ }
827
+ }
828
+ return count;
829
+ }
830
+ async function compressFiles(sources, options, onProgress) {
831
+ try {
832
+ const { format, level = "normal", outputName, outputDir, deleteSource } = options;
833
+ const ext = getExtension(format);
834
+ const finalName = outputName.endsWith(ext) ? outputName : outputName + ext;
835
+ const outputPath = path7.join(outputDir, finalName);
836
+ await fsPromises.mkdir(outputDir, { recursive: true });
837
+ const totalCount = await countFiles(sources);
838
+ let processedCount = 0;
839
+ switch (format) {
840
+ case "zip": {
841
+ const stream = new compressing.zip.Stream();
842
+ for (const source of sources) {
843
+ const stats = await fsPromises.stat(source);
844
+ const baseName = path7.basename(source);
845
+ if (stats.isDirectory()) {
846
+ const files = await getAllFiles(source);
847
+ for (const file of files) {
848
+ const relativePath = path7.relative(path7.dirname(source), file);
849
+ stream.addEntry(file, { relativePath });
850
+ processedCount++;
851
+ onProgress?.({
852
+ currentFile: path7.basename(file),
853
+ processedCount,
854
+ totalCount,
855
+ percent: Math.round(processedCount / totalCount * 100)
856
+ });
857
+ }
858
+ } else {
859
+ stream.addEntry(source, { relativePath: baseName });
860
+ processedCount++;
861
+ onProgress?.({
862
+ currentFile: baseName,
863
+ processedCount,
864
+ totalCount,
865
+ percent: Math.round(processedCount / totalCount * 100)
866
+ });
867
+ }
868
+ }
869
+ const destStream = fs7.createWriteStream(outputPath);
870
+ await pipeline(stream, destStream);
871
+ break;
872
+ }
873
+ case "tar": {
874
+ const stream = new compressing.tar.Stream();
875
+ for (const source of sources) {
876
+ const stats = await fsPromises.stat(source);
877
+ const baseName = path7.basename(source);
878
+ if (stats.isDirectory()) {
879
+ const files = await getAllFiles(source);
880
+ for (const file of files) {
881
+ const relativePath = path7.relative(path7.dirname(source), file);
882
+ stream.addEntry(file, { relativePath });
883
+ processedCount++;
884
+ onProgress?.({
885
+ currentFile: path7.basename(file),
886
+ processedCount,
887
+ totalCount,
888
+ percent: Math.round(processedCount / totalCount * 100)
889
+ });
890
+ }
891
+ } else {
892
+ stream.addEntry(source, { relativePath: baseName });
893
+ processedCount++;
894
+ onProgress?.({
895
+ currentFile: baseName,
896
+ processedCount,
897
+ totalCount,
898
+ percent: Math.round(processedCount / totalCount * 100)
899
+ });
900
+ }
901
+ }
902
+ const destStream = fs7.createWriteStream(outputPath);
903
+ await pipeline(stream, destStream);
904
+ break;
905
+ }
906
+ case "tgz": {
907
+ const stream = new compressing.tgz.Stream();
908
+ for (const source of sources) {
909
+ const stats = await fsPromises.stat(source);
910
+ const baseName = path7.basename(source);
911
+ if (stats.isDirectory()) {
912
+ const files = await getAllFiles(source);
913
+ for (const file of files) {
914
+ const relativePath = path7.relative(path7.dirname(source), file);
915
+ stream.addEntry(file, { relativePath });
916
+ processedCount++;
917
+ onProgress?.({
918
+ currentFile: path7.basename(file),
919
+ processedCount,
920
+ totalCount,
921
+ percent: Math.round(processedCount / totalCount * 100)
922
+ });
923
+ }
924
+ } else {
925
+ stream.addEntry(source, { relativePath: baseName });
926
+ processedCount++;
927
+ onProgress?.({
928
+ currentFile: baseName,
929
+ processedCount,
930
+ totalCount,
931
+ percent: Math.round(processedCount / totalCount * 100)
932
+ });
933
+ }
934
+ }
935
+ const destStream = fs7.createWriteStream(outputPath);
936
+ await pipeline(stream, destStream);
937
+ break;
938
+ }
939
+ case "tarbz2": {
940
+ console.warn("tar.bz2 format not fully supported, using tgz instead");
941
+ const tgzStream = new compressing.tgz.Stream();
942
+ for (const source of sources) {
943
+ const stats = await fsPromises.stat(source);
944
+ const baseName = path7.basename(source);
945
+ if (stats.isDirectory()) {
946
+ const files = await getAllFiles(source);
947
+ for (const file of files) {
948
+ const relativePath = path7.relative(path7.dirname(source), file);
949
+ tgzStream.addEntry(file, { relativePath });
950
+ processedCount++;
951
+ onProgress?.({
952
+ currentFile: path7.basename(file),
953
+ processedCount,
954
+ totalCount,
955
+ percent: Math.round(processedCount / totalCount * 100)
956
+ });
957
+ }
958
+ } else {
959
+ tgzStream.addEntry(source, { relativePath: baseName });
960
+ processedCount++;
961
+ onProgress?.({
962
+ currentFile: baseName,
963
+ processedCount,
964
+ totalCount,
965
+ percent: Math.round(processedCount / totalCount * 100)
966
+ });
967
+ }
968
+ }
969
+ const destStream = fs7.createWriteStream(outputPath.replace(".tar.bz2", ".tar.gz"));
970
+ await pipeline(tgzStream, destStream);
971
+ break;
972
+ }
973
+ }
974
+ if (deleteSource) {
975
+ for (const source of sources) {
976
+ const stats = await fsPromises.stat(source);
977
+ if (stats.isDirectory()) {
978
+ await fsPromises.rm(source, { recursive: true });
979
+ } else {
980
+ await fsPromises.unlink(source);
981
+ }
982
+ }
983
+ }
984
+ return { success: true, outputPath };
985
+ } catch (error) {
986
+ return {
987
+ success: false,
988
+ error: error instanceof Error ? error.message : String(error)
989
+ };
990
+ }
991
+ }
992
+ async function extractArchive(archivePath, options, onProgress) {
993
+ try {
994
+ const { targetDir, deleteArchive } = options;
995
+ const format = detectArchiveFormat(archivePath);
996
+ if (!format) {
997
+ return { success: false, error: "\u4E0D\u652F\u6301\u7684\u538B\u7F29\u683C\u5F0F" };
998
+ }
999
+ await fsPromises.mkdir(targetDir, { recursive: true });
1000
+ onProgress?.({
1001
+ currentFile: path7.basename(archivePath),
1002
+ processedCount: 0,
1003
+ totalCount: 1,
1004
+ percent: 0
1005
+ });
1006
+ switch (format) {
1007
+ case "zip":
1008
+ await compressing.zip.uncompress(archivePath, targetDir);
1009
+ break;
1010
+ case "tar":
1011
+ await compressing.tar.uncompress(archivePath, targetDir);
1012
+ break;
1013
+ case "tgz":
1014
+ await compressing.tgz.uncompress(archivePath, targetDir);
1015
+ break;
1016
+ case "tarbz2":
1017
+ console.warn("tar.bz2 format not fully supported");
1018
+ return { success: false, error: "tar.bz2 \u683C\u5F0F\u6682\u4E0D\u652F\u6301" };
1019
+ }
1020
+ onProgress?.({
1021
+ currentFile: path7.basename(archivePath),
1022
+ processedCount: 1,
1023
+ totalCount: 1,
1024
+ percent: 100
1025
+ });
1026
+ if (deleteArchive) {
1027
+ await fsPromises.unlink(archivePath);
1028
+ }
1029
+ return { success: true, outputPath: targetDir };
1030
+ } catch (error) {
1031
+ return {
1032
+ success: false,
1033
+ error: error instanceof Error ? error.message : String(error)
1034
+ };
1035
+ }
1036
+ }
1037
+
576
1038
  // src/system-paths.ts
577
- import path6 from "path";
1039
+ import path8 from "path";
578
1040
  import os from "os";
579
1041
  var platform = process.platform;
580
1042
  function getSystemPath(pathId) {
581
1043
  const homeDir = os.homedir();
582
1044
  switch (pathId) {
583
1045
  case "desktop":
584
- return path6.join(homeDir, "Desktop");
1046
+ return path8.join(homeDir, "Desktop");
585
1047
  case "documents":
586
- return path6.join(homeDir, "Documents");
1048
+ return path8.join(homeDir, "Documents");
587
1049
  case "downloads":
588
- return path6.join(homeDir, "Downloads");
1050
+ return path8.join(homeDir, "Downloads");
589
1051
  case "pictures":
590
- return path6.join(homeDir, "Pictures");
1052
+ return path8.join(homeDir, "Pictures");
591
1053
  case "music":
592
- return path6.join(homeDir, "Music");
1054
+ return path8.join(homeDir, "Music");
593
1055
  case "videos":
594
- return platform === "darwin" ? path6.join(homeDir, "Movies") : path6.join(homeDir, "Videos");
1056
+ return platform === "darwin" ? path8.join(homeDir, "Movies") : path8.join(homeDir, "Videos");
595
1057
  case "applications":
596
1058
  return platform === "darwin" ? "/Applications" : platform === "win32" ? process.env.ProgramFiles || "C:\\Program Files" : "/usr/share/applications";
597
1059
  case "home":
@@ -623,14 +1085,14 @@ function getAllSystemPaths() {
623
1085
  function getHomeDirectory() {
624
1086
  return os.homedir();
625
1087
  }
626
- function getPlatform() {
1088
+ function getPlatform2() {
627
1089
  return process.platform;
628
1090
  }
629
1091
 
630
1092
  // src/search.ts
631
1093
  import { fdir } from "fdir";
632
- import path7 from "path";
633
- import { promises as fs7 } from "fs";
1094
+ import path9 from "path";
1095
+ import { promises as fs8 } from "fs";
634
1096
  function patternToRegex(pattern) {
635
1097
  return new RegExp(pattern.replace(/\*/g, ".*"), "i");
636
1098
  }
@@ -643,7 +1105,7 @@ async function searchFiles(searchPath, pattern, maxDepth) {
643
1105
  const files = await api.withPromise();
644
1106
  if (pattern) {
645
1107
  const regex = patternToRegex(pattern);
646
- return files.filter((file) => regex.test(path7.basename(file)));
1108
+ return files.filter((file) => regex.test(path9.basename(file)));
647
1109
  }
648
1110
  return files;
649
1111
  }
@@ -654,12 +1116,12 @@ async function searchFilesStream(searchPath, pattern, onResults, maxResults = 10
654
1116
  async function searchDir(dirPath) {
655
1117
  if (stopped) return;
656
1118
  try {
657
- const entries = await fs7.readdir(dirPath, { withFileTypes: true });
1119
+ const entries = await fs8.readdir(dirPath, { withFileTypes: true });
658
1120
  const matched = [];
659
1121
  const subdirs = [];
660
1122
  for (const entry of entries) {
661
1123
  if (stopped) return;
662
- const fullPath = path7.join(dirPath, entry.name);
1124
+ const fullPath = path9.join(dirPath, entry.name);
663
1125
  if (regex.test(entry.name)) {
664
1126
  matched.push(fullPath);
665
1127
  results.push(fullPath);
@@ -696,44 +1158,29 @@ function searchFilesSync(searchPath, pattern, maxDepth) {
696
1158
  const files = api.sync();
697
1159
  if (pattern) {
698
1160
  const regex = new RegExp(pattern.replace(/\*/g, ".*"), "i");
699
- return files.filter((file) => regex.test(path7.basename(file)));
1161
+ return files.filter((file) => regex.test(path9.basename(file)));
700
1162
  }
701
1163
  return files;
702
1164
  }
703
1165
 
704
1166
  // src/hash.ts
705
- import { createHash } from "crypto";
1167
+ import { xxhash64 } from "hash-wasm";
706
1168
  import { stat } from "fs/promises";
707
- async function getFileHash(filePath) {
708
- try {
709
- const stats = await stat(filePath);
710
- const hashInput = `${filePath}:${stats.size}:${stats.mtime.getTime()}`;
711
- return createHash("md5").update(hashInput).digest("hex");
712
- } catch (error) {
713
- console.error(`Error computing hash for ${filePath}:`, error);
714
- return createHash("md5").update(filePath).digest("hex");
715
- }
716
- }
717
- async function getFileHashes(filePaths) {
718
- const hashMap = /* @__PURE__ */ new Map();
719
- await Promise.allSettled(
720
- filePaths.map(async (filePath) => {
721
- const hash = await getFileHash(filePath);
722
- hashMap.set(filePath, hash);
723
- })
724
- );
725
- return hashMap;
1169
+ async function getFileHash(filePath, stats) {
1170
+ const fileStats = stats || await stat(filePath);
1171
+ const hashInput = `${filePath}:${fileStats.size}:${fileStats.mtime.getTime()}`;
1172
+ return await xxhash64(hashInput);
726
1173
  }
727
1174
 
728
1175
  // src/clipboard.ts
729
- import { promises as fs8 } from "fs";
730
- import path8 from "path";
1176
+ import { promises as fs9 } from "fs";
1177
+ import path10 from "path";
731
1178
  async function copyFilesToClipboard(filePaths, clipboard) {
732
1179
  try {
733
1180
  const cleanPaths = [];
734
1181
  for (const p of filePaths) {
735
1182
  try {
736
- await fs8.access(p);
1183
+ await fs9.access(p);
737
1184
  cleanPaths.push(p);
738
1185
  } catch {
739
1186
  }
@@ -768,24 +1215,24 @@ async function pasteFiles(targetDir, sourcePaths) {
768
1215
  }
769
1216
  const pastedPaths = [];
770
1217
  for (const sourcePath of sourcePaths) {
771
- const fileName = path8.basename(sourcePath);
772
- let destPath = path8.join(targetDir, fileName);
1218
+ const fileName = path10.basename(sourcePath);
1219
+ let destPath = path10.join(targetDir, fileName);
773
1220
  let counter = 1;
774
1221
  while (true) {
775
1222
  try {
776
- await fs8.access(destPath);
777
- const ext = path8.extname(fileName);
778
- const baseName = path8.basename(fileName, ext);
779
- destPath = path8.join(targetDir, `${baseName} ${++counter}${ext}`);
1223
+ await fs9.access(destPath);
1224
+ const ext = path10.extname(fileName);
1225
+ const baseName = path10.basename(fileName, ext);
1226
+ destPath = path10.join(targetDir, `${baseName} ${++counter}${ext}`);
780
1227
  } catch {
781
1228
  break;
782
1229
  }
783
1230
  }
784
- const stats = await fs8.stat(sourcePath);
1231
+ const stats = await fs9.stat(sourcePath);
785
1232
  if (stats.isDirectory()) {
786
1233
  await copyDirectory2(sourcePath, destPath);
787
1234
  } else {
788
- await fs8.copyFile(sourcePath, destPath);
1235
+ await fs9.copyFile(sourcePath, destPath);
789
1236
  }
790
1237
  pastedPaths.push(destPath);
791
1238
  }
@@ -795,28 +1242,28 @@ async function pasteFiles(targetDir, sourcePaths) {
795
1242
  }
796
1243
  }
797
1244
  async function copyDirectory2(source, dest) {
798
- await fs8.mkdir(dest, { recursive: true });
799
- const entries = await fs8.readdir(source, { withFileTypes: true });
1245
+ await fs9.mkdir(dest, { recursive: true });
1246
+ const entries = await fs9.readdir(source, { withFileTypes: true });
800
1247
  for (const entry of entries) {
801
- const sourcePath = path8.join(source, entry.name);
802
- const destPath = path8.join(dest, entry.name);
1248
+ const sourcePath = path10.join(source, entry.name);
1249
+ const destPath = path10.join(dest, entry.name);
803
1250
  if (entry.isDirectory()) {
804
1251
  await copyDirectory2(sourcePath, destPath);
805
1252
  } else {
806
- await fs8.copyFile(sourcePath, destPath);
1253
+ await fs9.copyFile(sourcePath, destPath);
807
1254
  }
808
1255
  }
809
1256
  }
810
1257
 
811
1258
  // src/application-icon.ts
812
- import { promises as fs9 } from "fs";
813
- import path9 from "path";
1259
+ import { promises as fs10 } from "fs";
1260
+ import path11 from "path";
814
1261
  import os2 from "os";
815
- import { exec } from "child_process";
816
- import { promisify } from "util";
817
- var execAsync = promisify(exec);
1262
+ import { exec as exec2 } from "child_process";
1263
+ import { promisify as promisify2 } from "util";
1264
+ var execAsync2 = promisify2(exec2);
818
1265
  async function findAppIconPath(appPath) {
819
- const resourcesPath = path9.join(appPath, "Contents", "Resources");
1266
+ const resourcesPath = path11.join(appPath, "Contents", "Resources");
820
1267
  try {
821
1268
  const commonIconNames = [
822
1269
  "AppIcon.icns",
@@ -824,18 +1271,18 @@ async function findAppIconPath(appPath) {
824
1271
  "application.icns",
825
1272
  "icon.icns"
826
1273
  ];
827
- const infoPlistPath = path9.join(appPath, "Contents", "Info.plist");
1274
+ const infoPlistPath = path11.join(appPath, "Contents", "Info.plist");
828
1275
  try {
829
- const infoPlistContent = await fs9.readFile(infoPlistPath, "utf-8");
1276
+ const infoPlistContent = await fs10.readFile(infoPlistPath, "utf-8");
830
1277
  const iconFileMatch = infoPlistContent.match(/<key>CFBundleIconFile<\/key>\s*<string>([^<]+)<\/string>/);
831
1278
  if (iconFileMatch && iconFileMatch[1]) {
832
1279
  let iconFileName = iconFileMatch[1].trim();
833
1280
  if (!iconFileName.endsWith(".icns")) {
834
1281
  iconFileName += ".icns";
835
1282
  }
836
- const iconPath = path9.join(resourcesPath, iconFileName);
1283
+ const iconPath = path11.join(resourcesPath, iconFileName);
837
1284
  try {
838
- await fs9.access(iconPath);
1285
+ await fs10.access(iconPath);
839
1286
  return iconPath;
840
1287
  } catch {
841
1288
  }
@@ -843,19 +1290,19 @@ async function findAppIconPath(appPath) {
843
1290
  } catch {
844
1291
  }
845
1292
  for (const iconName of commonIconNames) {
846
- const iconPath = path9.join(resourcesPath, iconName);
1293
+ const iconPath = path11.join(resourcesPath, iconName);
847
1294
  try {
848
- await fs9.access(iconPath);
1295
+ await fs10.access(iconPath);
849
1296
  return iconPath;
850
1297
  } catch {
851
1298
  continue;
852
1299
  }
853
1300
  }
854
1301
  try {
855
- const entries = await fs9.readdir(resourcesPath);
1302
+ const entries = await fs10.readdir(resourcesPath);
856
1303
  const icnsFile = entries.find((entry) => entry.toLowerCase().endsWith(".icns"));
857
1304
  if (icnsFile) {
858
- return path9.join(resourcesPath, icnsFile);
1305
+ return path11.join(resourcesPath, icnsFile);
859
1306
  }
860
1307
  } catch {
861
1308
  }
@@ -869,7 +1316,7 @@ async function getApplicationIcon(appPath) {
869
1316
  return null;
870
1317
  }
871
1318
  try {
872
- const stats = await fs9.stat(appPath);
1319
+ const stats = await fs10.stat(appPath);
873
1320
  if (!stats.isDirectory() || !appPath.endsWith(".app")) {
874
1321
  return null;
875
1322
  }
@@ -881,13 +1328,13 @@ async function getApplicationIcon(appPath) {
881
1328
  return null;
882
1329
  }
883
1330
  try {
884
- const tempPngPath = path9.join(os2.tmpdir(), `app-icon-${Date.now()}.png`);
885
- await execAsync(
1331
+ const tempPngPath = path11.join(os2.tmpdir(), `app-icon-${Date.now()}.png`);
1332
+ await execAsync2(
886
1333
  `sips -s format png "${iconPath}" --out "${tempPngPath}" --resampleHeightWidthMax 128`
887
1334
  );
888
- const pngBuffer = await fs9.readFile(tempPngPath);
1335
+ const pngBuffer = await fs10.readFile(tempPngPath);
889
1336
  try {
890
- await fs9.unlink(tempPngPath);
1337
+ await fs10.unlink(tempPngPath);
891
1338
  } catch {
892
1339
  }
893
1340
  const base64 = pngBuffer.toString("base64");
@@ -897,11 +1344,150 @@ async function getApplicationIcon(appPath) {
897
1344
  }
898
1345
  }
899
1346
 
1347
+ // src/watch.ts
1348
+ import * as fs11 from "fs";
1349
+ import * as path12 from "path";
1350
+ var debounceTimers = /* @__PURE__ */ new Map();
1351
+ var DEBOUNCE_DELAY = 100;
1352
+ function watchDirectory(dirPath, callback) {
1353
+ let watcher = null;
1354
+ try {
1355
+ watcher = fs11.watch(dirPath, { persistent: true }, (eventType, filename) => {
1356
+ if (!filename) return;
1357
+ const fullPath = path12.join(dirPath, filename);
1358
+ const key = `${dirPath}:${filename}`;
1359
+ const existingTimer = debounceTimers.get(key);
1360
+ if (existingTimer) {
1361
+ clearTimeout(existingTimer);
1362
+ }
1363
+ const timer = setTimeout(() => {
1364
+ debounceTimers.delete(key);
1365
+ fs11.access(fullPath, fs11.constants.F_OK, (err) => {
1366
+ let type;
1367
+ if (err) {
1368
+ type = "remove";
1369
+ } else if (eventType === "rename") {
1370
+ type = "add";
1371
+ } else {
1372
+ type = "change";
1373
+ }
1374
+ callback({
1375
+ type,
1376
+ path: fullPath,
1377
+ filename
1378
+ });
1379
+ });
1380
+ }, DEBOUNCE_DELAY);
1381
+ debounceTimers.set(key, timer);
1382
+ });
1383
+ watcher.on("error", (error) => {
1384
+ console.error("Watch error:", error);
1385
+ });
1386
+ } catch (error) {
1387
+ console.error("Failed to watch directory:", error);
1388
+ }
1389
+ return {
1390
+ close: () => {
1391
+ if (watcher) {
1392
+ watcher.close();
1393
+ watcher = null;
1394
+ }
1395
+ for (const [key, timer] of debounceTimers.entries()) {
1396
+ if (key.startsWith(`${dirPath}:`)) {
1397
+ clearTimeout(timer);
1398
+ debounceTimers.delete(key);
1399
+ }
1400
+ }
1401
+ },
1402
+ path: dirPath
1403
+ };
1404
+ }
1405
+ var WatchManager = class {
1406
+ watchers = /* @__PURE__ */ new Map();
1407
+ callbacks = /* @__PURE__ */ new Map();
1408
+ /**
1409
+ * 开始监听目录
1410
+ */
1411
+ watch(dirPath, callback) {
1412
+ const normalizedPath = path12.normalize(dirPath);
1413
+ let callbackSet = this.callbacks.get(normalizedPath);
1414
+ if (!callbackSet) {
1415
+ callbackSet = /* @__PURE__ */ new Set();
1416
+ this.callbacks.set(normalizedPath, callbackSet);
1417
+ }
1418
+ callbackSet.add(callback);
1419
+ let watcherInfo = this.watchers.get(normalizedPath);
1420
+ if (watcherInfo) {
1421
+ watcherInfo.refCount++;
1422
+ } else {
1423
+ const watcher = watchDirectory(normalizedPath, (event) => {
1424
+ const callbacks = this.callbacks.get(normalizedPath);
1425
+ if (callbacks) {
1426
+ for (const cb of callbacks) {
1427
+ try {
1428
+ cb(event);
1429
+ } catch (error) {
1430
+ console.error("Watch callback error:", error);
1431
+ }
1432
+ }
1433
+ }
1434
+ });
1435
+ watcherInfo = { watcher, refCount: 1 };
1436
+ this.watchers.set(normalizedPath, watcherInfo);
1437
+ }
1438
+ return () => {
1439
+ this.unwatch(normalizedPath, callback);
1440
+ };
1441
+ }
1442
+ /**
1443
+ * 停止监听
1444
+ */
1445
+ unwatch(dirPath, callback) {
1446
+ const normalizedPath = path12.normalize(dirPath);
1447
+ const callbackSet = this.callbacks.get(normalizedPath);
1448
+ if (callbackSet) {
1449
+ callbackSet.delete(callback);
1450
+ if (callbackSet.size === 0) {
1451
+ this.callbacks.delete(normalizedPath);
1452
+ }
1453
+ }
1454
+ const watcherInfo = this.watchers.get(normalizedPath);
1455
+ if (watcherInfo) {
1456
+ watcherInfo.refCount--;
1457
+ if (watcherInfo.refCount <= 0) {
1458
+ watcherInfo.watcher.close();
1459
+ this.watchers.delete(normalizedPath);
1460
+ }
1461
+ }
1462
+ }
1463
+ /**
1464
+ * 关闭所有监听器
1465
+ */
1466
+ closeAll() {
1467
+ for (const [, watcherInfo] of this.watchers) {
1468
+ watcherInfo.watcher.close();
1469
+ }
1470
+ this.watchers.clear();
1471
+ this.callbacks.clear();
1472
+ }
1473
+ };
1474
+ var globalWatchManager = null;
1475
+ function getWatchManager() {
1476
+ if (!globalWatchManager) {
1477
+ globalWatchManager = new WatchManager();
1478
+ }
1479
+ return globalWatchManager;
1480
+ }
1481
+
900
1482
  // src/thumbnail/service.ts
901
- import { promises as fs10 } from "fs";
902
- import path10 from "path";
903
- var IMAGE_EXTENSIONS2 = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"];
904
- var VIDEO_EXTENSIONS2 = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".wmv"];
1483
+ import { promises as fs12 } from "fs";
1484
+ import path13 from "path";
1485
+ function isImageFile(filePath, fileType) {
1486
+ return fileType === "image" /* IMAGE */;
1487
+ }
1488
+ function isVideoFile(filePath, fileType) {
1489
+ return fileType === "video" /* VIDEO */;
1490
+ }
905
1491
  var ThumbnailService = class {
906
1492
  database;
907
1493
  imageProcessor;
@@ -920,7 +1506,7 @@ var ThumbnailService = class {
920
1506
  */
921
1507
  async getCachedThumbnailUrl(filePath) {
922
1508
  try {
923
- const stats = await fs10.stat(filePath);
1509
+ const stats = await fs12.stat(filePath);
924
1510
  const fileType = getFileType(filePath, stats);
925
1511
  if (fileType === "application" /* APPLICATION */ && this.getApplicationIcon) {
926
1512
  return await this.getApplicationIcon(filePath);
@@ -933,7 +1519,7 @@ var ThumbnailService = class {
933
1519
  if (cachedPath) {
934
1520
  return this.urlEncoder(cachedPath);
935
1521
  }
936
- getFileHash(filePath).then((fileHash) => {
1522
+ getFileHash(filePath, stats).then((fileHash) => {
937
1523
  this.generateThumbnail(filePath, fileHash, mtime).catch(() => {
938
1524
  });
939
1525
  }).catch(() => {
@@ -948,7 +1534,7 @@ var ThumbnailService = class {
948
1534
  */
949
1535
  async getThumbnailUrl(filePath) {
950
1536
  try {
951
- const stats = await fs10.stat(filePath);
1537
+ const stats = await fs12.stat(filePath);
952
1538
  const fileType = getFileType(filePath, stats);
953
1539
  if (fileType === "application" /* APPLICATION */ && this.getApplicationIcon) {
954
1540
  return await this.getApplicationIcon(filePath);
@@ -961,7 +1547,7 @@ var ThumbnailService = class {
961
1547
  if (cachedPath) {
962
1548
  return this.urlEncoder(cachedPath);
963
1549
  }
964
- const fileHash = await getFileHash(filePath);
1550
+ const fileHash = await getFileHash(filePath, stats);
965
1551
  const thumbnailPath = await this.generateThumbnail(filePath, fileHash, mtime);
966
1552
  if (thumbnailPath) {
967
1553
  return this.urlEncoder(thumbnailPath);
@@ -981,13 +1567,14 @@ var ThumbnailService = class {
981
1567
  return cachedPath;
982
1568
  }
983
1569
  try {
984
- const ext = path10.extname(filePath).toLowerCase();
1570
+ const stats = await fs12.stat(filePath);
1571
+ const fileType = getFileType(filePath, stats);
985
1572
  const hashPrefix = fileHash.substring(0, 16);
986
1573
  const thumbnailFileName = `${hashPrefix}.jpg`;
987
- const thumbnailPath = path10.join(this.database.getCacheDir(), thumbnailFileName);
988
- if (IMAGE_EXTENSIONS2.includes(ext) && this.imageProcessor) {
1574
+ const thumbnailPath = path13.join(this.database.getCacheDir(), thumbnailFileName);
1575
+ if (isImageFile(filePath, fileType) && this.imageProcessor) {
989
1576
  await this.imageProcessor.resize(filePath, thumbnailPath, 256);
990
- } else if (VIDEO_EXTENSIONS2.includes(ext) && this.videoProcessor) {
1577
+ } else if (isVideoFile(filePath, fileType) && this.videoProcessor) {
991
1578
  await this.videoProcessor.screenshot(filePath, thumbnailPath, "00:00:01", "256x256");
992
1579
  } else {
993
1580
  return null;
@@ -1000,12 +1587,20 @@ var ThumbnailService = class {
1000
1587
  }
1001
1588
  }
1002
1589
  /**
1003
- * 批量生成缩略图
1590
+ * 批量生成缩略图(带并发限制,避免资源耗尽)
1004
1591
  */
1005
- async generateThumbnailsBatch(files) {
1006
- await Promise.allSettled(
1007
- files.map((file) => this.generateThumbnail(file.path, file.hash, file.mtime))
1008
- );
1592
+ async generateThumbnailsBatch(files, concurrency = 3) {
1593
+ const execute = async (file) => {
1594
+ try {
1595
+ await this.generateThumbnail(file.path, file.hash, file.mtime);
1596
+ } catch (error) {
1597
+ console.debug(`Failed to generate thumbnail for ${file.path}:`, error);
1598
+ }
1599
+ };
1600
+ for (let i = 0; i < files.length; i += concurrency) {
1601
+ const batch = files.slice(i, i + concurrency);
1602
+ await Promise.allSettled(batch.map(execute));
1603
+ }
1009
1604
  }
1010
1605
  /**
1011
1606
  * 删除缩略图
@@ -1031,13 +1626,18 @@ function getThumbnailService() {
1031
1626
 
1032
1627
  // src/thumbnail/database.ts
1033
1628
  import Database from "better-sqlite3";
1034
- import path11 from "path";
1629
+ import path14 from "path";
1035
1630
  import { existsSync, mkdirSync } from "fs";
1036
1631
  var SqliteThumbnailDatabase = class {
1037
1632
  db = null;
1038
1633
  cacheDir;
1039
- constructor(userDataPath) {
1040
- this.cacheDir = path11.join(userDataPath, "cache");
1634
+ dbPath;
1635
+ constructor(userDataPath, options = {}) {
1636
+ const defaultDirName = options.dirName || "thumbnails";
1637
+ const defaultDbFileName = options.dbFileName || "thumbnails.db";
1638
+ const inferredDirFromDbPath = options.dbPath ? path14.dirname(options.dbPath) : null;
1639
+ this.cacheDir = options.thumbnailDir ? options.thumbnailDir : inferredDirFromDbPath || path14.join(userDataPath, defaultDirName);
1640
+ this.dbPath = options.dbPath ? options.dbPath : path14.join(this.cacheDir, defaultDbFileName);
1041
1641
  if (!existsSync(this.cacheDir)) {
1042
1642
  mkdirSync(this.cacheDir, { recursive: true });
1043
1643
  }
@@ -1047,8 +1647,7 @@ var SqliteThumbnailDatabase = class {
1047
1647
  */
1048
1648
  init() {
1049
1649
  if (this.db) return;
1050
- const dbPath = path11.join(this.cacheDir, "thumbnails.db");
1051
- this.db = new Database(dbPath, {
1650
+ this.db = new Database(this.dbPath, {
1052
1651
  fileMustExist: false
1053
1652
  });
1054
1653
  this.db.pragma("journal_mode = WAL");
@@ -1120,15 +1719,19 @@ var SqliteThumbnailDatabase = class {
1120
1719
  }
1121
1720
  close() {
1122
1721
  if (this.db) {
1722
+ try {
1723
+ this.db.pragma("wal_checkpoint(TRUNCATE)");
1724
+ } catch (error) {
1725
+ }
1123
1726
  this.db.close();
1124
1727
  this.db = null;
1125
1728
  }
1126
1729
  }
1127
1730
  };
1128
1731
  var thumbnailDb = null;
1129
- function createSqliteThumbnailDatabase(userDataPath) {
1732
+ function createSqliteThumbnailDatabase(options) {
1130
1733
  if (!thumbnailDb) {
1131
- thumbnailDb = new SqliteThumbnailDatabase(userDataPath);
1734
+ thumbnailDb = new SqliteThumbnailDatabase(options.userDataPath, options);
1132
1735
  thumbnailDb.init();
1133
1736
  }
1134
1737
  return thumbnailDb;
@@ -1136,58 +1739,698 @@ function createSqliteThumbnailDatabase(userDataPath) {
1136
1739
  function getSqliteThumbnailDatabase() {
1137
1740
  return thumbnailDb;
1138
1741
  }
1742
+ function closeThumbnailDatabase() {
1743
+ if (thumbnailDb) {
1744
+ thumbnailDb.close();
1745
+ thumbnailDb = null;
1746
+ }
1747
+ }
1139
1748
 
1140
1749
  // src/thumbnail/processors.ts
1141
- import { execFile } from "child_process";
1142
- import { promisify as promisify2 } from "util";
1143
- var execFileAsync = promisify2(execFile);
1750
+ import { spawn } from "child_process";
1144
1751
  function createSharpImageProcessor(sharp) {
1145
1752
  return {
1146
1753
  async resize(filePath, outputPath, size) {
1147
- await sharp(filePath).resize(size, size, {
1148
- fit: "inside",
1754
+ await sharp(filePath).resize({
1755
+ width: size,
1149
1756
  withoutEnlargement: true
1150
- }).jpeg({ quality: 85 }).toFile(outputPath);
1757
+ }).jpeg({
1758
+ quality: 80,
1759
+ optimiseCoding: true
1760
+ // 优化编码,提升压缩率
1761
+ }).toFile(outputPath);
1151
1762
  }
1152
1763
  };
1153
1764
  }
1154
1765
  function createFfmpegVideoProcessor(ffmpegPath) {
1155
1766
  return {
1156
1767
  async screenshot(filePath, outputPath, timestamp, size) {
1157
- const scaleSize = size.replace("x", ":");
1158
- await execFileAsync(ffmpegPath, [
1159
- "-i",
1160
- filePath,
1161
- "-ss",
1162
- timestamp,
1163
- "-vframes",
1164
- "1",
1165
- "-vf",
1166
- `scale=${scaleSize}:force_original_aspect_ratio=decrease`,
1167
- "-y",
1168
- outputPath
1169
- ]);
1768
+ const width = size.split("x")[0];
1769
+ return new Promise((resolve, reject) => {
1770
+ const ffmpeg = spawn(ffmpegPath, [
1771
+ "-y",
1772
+ "-ss",
1773
+ timestamp,
1774
+ "-i",
1775
+ filePath,
1776
+ "-vframes",
1777
+ "1",
1778
+ "-vf",
1779
+ `thumbnail,scale=${width}:-1:force_original_aspect_ratio=decrease`,
1780
+ // 使用 mjpeg 编码器,性能更好(借鉴 pixflow)
1781
+ "-c:v",
1782
+ "mjpeg",
1783
+ "-q:v",
1784
+ "6",
1785
+ // 质量参数,6 表示中等质量(范围 2-31,数值越小质量越高)
1786
+ outputPath
1787
+ ]);
1788
+ const timeout = setTimeout(() => {
1789
+ ffmpeg.kill();
1790
+ reject(new Error("Video thumbnail generation timeout"));
1791
+ }, 3e4);
1792
+ ffmpeg.stderr.on("data", (data) => {
1793
+ const output = data.toString();
1794
+ if (output.includes("Unsupported pixel format")) {
1795
+ return;
1796
+ }
1797
+ });
1798
+ ffmpeg.on("close", (code) => {
1799
+ clearTimeout(timeout);
1800
+ if (code === 0) {
1801
+ resolve();
1802
+ } else {
1803
+ reject(new Error(`ffmpeg exited with code ${code}`));
1804
+ }
1805
+ });
1806
+ ffmpeg.on("error", (error) => {
1807
+ clearTimeout(timeout);
1808
+ reject(error);
1809
+ });
1810
+ });
1170
1811
  }
1171
1812
  };
1172
1813
  }
1814
+
1815
+ // src/media/format-detector.ts
1816
+ import { execFile } from "child_process";
1817
+ import { promisify as promisify3 } from "util";
1818
+ import path15 from "path";
1819
+ var execFileAsync = promisify3(execFile);
1820
+ var BROWSER_VIDEO_CONTAINERS = /* @__PURE__ */ new Set(["mp4", "webm", "ogg", "ogv"]);
1821
+ var BROWSER_VIDEO_CODECS = /* @__PURE__ */ new Set(["h264", "avc1", "vp8", "vp9", "theora", "av1"]);
1822
+ var BROWSER_AUDIO_CODECS = /* @__PURE__ */ new Set(["aac", "mp3", "opus", "vorbis", "flac"]);
1823
+ var BROWSER_AUDIO_CONTAINERS = /* @__PURE__ */ new Set(["mp3", "wav", "ogg", "oga", "webm", "m4a", "aac", "flac"]);
1824
+ var BROWSER_AUDIO_ONLY_CODECS = /* @__PURE__ */ new Set(["mp3", "aac", "opus", "vorbis", "flac", "pcm_s16le", "pcm_s24le"]);
1825
+ var REMUXABLE_VIDEO_CODECS = /* @__PURE__ */ new Set(["h264", "avc1", "hevc", "h265"]);
1826
+ var REMUXABLE_AUDIO_CODECS = /* @__PURE__ */ new Set(["aac", "alac"]);
1827
+ var VIDEO_EXTENSIONS2 = /* @__PURE__ */ new Set([
1828
+ "mp4",
1829
+ "mkv",
1830
+ "avi",
1831
+ "mov",
1832
+ "wmv",
1833
+ "flv",
1834
+ "webm",
1835
+ "ogv",
1836
+ "ogg",
1837
+ "m4v",
1838
+ "mpeg",
1839
+ "mpg",
1840
+ "3gp",
1841
+ "ts",
1842
+ "mts",
1843
+ "m2ts",
1844
+ "vob",
1845
+ "rmvb",
1846
+ "rm"
1847
+ ]);
1848
+ var AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([
1849
+ "mp3",
1850
+ "wav",
1851
+ "flac",
1852
+ "aac",
1853
+ "m4a",
1854
+ "ogg",
1855
+ "oga",
1856
+ "wma",
1857
+ "ape",
1858
+ "alac",
1859
+ "aiff",
1860
+ "aif",
1861
+ "opus",
1862
+ "mid",
1863
+ "midi",
1864
+ "wv",
1865
+ "mka"
1866
+ ]);
1867
+ function getMediaTypeByExtension(filePath) {
1868
+ const ext = path15.extname(filePath).toLowerCase().slice(1);
1869
+ if (VIDEO_EXTENSIONS2.has(ext)) return "video";
1870
+ if (AUDIO_EXTENSIONS.has(ext)) return "audio";
1871
+ return null;
1872
+ }
1873
+ async function getMediaFormat(filePath, ffprobePath) {
1874
+ try {
1875
+ const { stdout } = await execFileAsync(ffprobePath, [
1876
+ "-v",
1877
+ "quiet",
1878
+ "-print_format",
1879
+ "json",
1880
+ "-show_format",
1881
+ "-show_streams",
1882
+ filePath
1883
+ ]);
1884
+ const data = JSON.parse(stdout);
1885
+ const format = data.format || {};
1886
+ const streams = data.streams || [];
1887
+ const videoStream = streams.find((s) => s.codec_type === "video");
1888
+ const audioStream = streams.find((s) => s.codec_type === "audio");
1889
+ const type = videoStream ? "video" : "audio";
1890
+ const formatName = format.format_name || "";
1891
+ const container = formatName.split(",")[0].toLowerCase();
1892
+ return {
1893
+ type,
1894
+ container,
1895
+ videoCodec: videoStream?.codec_name?.toLowerCase(),
1896
+ audioCodec: audioStream?.codec_name?.toLowerCase(),
1897
+ duration: parseFloat(format.duration) || void 0,
1898
+ width: videoStream?.width,
1899
+ height: videoStream?.height,
1900
+ bitrate: parseInt(format.bit_rate) || void 0
1901
+ };
1902
+ } catch {
1903
+ const type = getMediaTypeByExtension(filePath);
1904
+ if (!type) return null;
1905
+ const ext = path15.extname(filePath).toLowerCase().slice(1);
1906
+ return {
1907
+ type,
1908
+ container: ext
1909
+ };
1910
+ }
1911
+ }
1912
+ function canPlayVideoDirectly(format) {
1913
+ if (!BROWSER_VIDEO_CONTAINERS.has(format.container)) {
1914
+ return false;
1915
+ }
1916
+ if (format.videoCodec && !BROWSER_VIDEO_CODECS.has(format.videoCodec)) {
1917
+ return false;
1918
+ }
1919
+ if (format.audioCodec && !BROWSER_AUDIO_CODECS.has(format.audioCodec)) {
1920
+ return false;
1921
+ }
1922
+ return true;
1923
+ }
1924
+ function canPlayAudioDirectly(format) {
1925
+ if (!BROWSER_AUDIO_CONTAINERS.has(format.container)) {
1926
+ return false;
1927
+ }
1928
+ if (format.audioCodec && !BROWSER_AUDIO_ONLY_CODECS.has(format.audioCodec)) {
1929
+ return false;
1930
+ }
1931
+ return true;
1932
+ }
1933
+ function canRemuxVideo(format) {
1934
+ if (!format.videoCodec || !REMUXABLE_VIDEO_CODECS.has(format.videoCodec)) {
1935
+ return false;
1936
+ }
1937
+ if (format.audioCodec) {
1938
+ const audioOk = BROWSER_AUDIO_CODECS.has(format.audioCodec) || REMUXABLE_AUDIO_CODECS.has(format.audioCodec);
1939
+ if (!audioOk) {
1940
+ return false;
1941
+ }
1942
+ }
1943
+ return true;
1944
+ }
1945
+ function canRemuxAudio(format) {
1946
+ return format.audioCodec ? REMUXABLE_AUDIO_CODECS.has(format.audioCodec) : false;
1947
+ }
1948
+ function estimateTranscodeTime(duration, method) {
1949
+ if (!duration || method === "direct") return void 0;
1950
+ if (method === "remux") {
1951
+ return Math.ceil(duration / 50);
1952
+ }
1953
+ return Math.ceil(duration / 3);
1954
+ }
1955
+ async function detectTranscodeNeeds(filePath, ffprobePath) {
1956
+ const formatInfo = await getMediaFormat(filePath, ffprobePath);
1957
+ if (!formatInfo) {
1958
+ const type2 = getMediaTypeByExtension(filePath) || "video";
1959
+ return {
1960
+ type: type2,
1961
+ needsTranscode: false,
1962
+ method: "direct"
1963
+ };
1964
+ }
1965
+ const { type } = formatInfo;
1966
+ if (type === "video") {
1967
+ if (canPlayVideoDirectly(formatInfo)) {
1968
+ return {
1969
+ type,
1970
+ needsTranscode: false,
1971
+ method: "direct",
1972
+ formatInfo
1973
+ };
1974
+ }
1975
+ if (canRemuxVideo(formatInfo)) {
1976
+ return {
1977
+ type,
1978
+ needsTranscode: true,
1979
+ method: "remux",
1980
+ formatInfo,
1981
+ targetFormat: "mp4",
1982
+ estimatedTime: estimateTranscodeTime(formatInfo.duration, "remux")
1983
+ };
1984
+ }
1985
+ return {
1986
+ type,
1987
+ needsTranscode: true,
1988
+ method: "transcode",
1989
+ formatInfo,
1990
+ targetFormat: "mp4",
1991
+ estimatedTime: estimateTranscodeTime(formatInfo.duration, "transcode")
1992
+ };
1993
+ }
1994
+ if (canPlayAudioDirectly(formatInfo)) {
1995
+ return {
1996
+ type,
1997
+ needsTranscode: false,
1998
+ method: "direct",
1999
+ formatInfo
2000
+ };
2001
+ }
2002
+ if (canRemuxAudio(formatInfo)) {
2003
+ return {
2004
+ type,
2005
+ needsTranscode: true,
2006
+ method: "remux",
2007
+ formatInfo,
2008
+ targetFormat: "m4a",
2009
+ estimatedTime: estimateTranscodeTime(formatInfo.duration, "remux")
2010
+ };
2011
+ }
2012
+ return {
2013
+ type,
2014
+ needsTranscode: true,
2015
+ method: "transcode",
2016
+ formatInfo,
2017
+ targetFormat: "mp3",
2018
+ estimatedTime: estimateTranscodeTime(formatInfo.duration, "transcode")
2019
+ };
2020
+ }
2021
+
2022
+ // src/media/transcoder.ts
2023
+ import { spawn as spawn2 } from "child_process";
2024
+ import fs13 from "fs/promises";
2025
+ import path16 from "path";
2026
+ import os3 from "os";
2027
+ async function getTempOutputPath(sourceFile, targetFormat, tempDir) {
2028
+ const dir = tempDir || path16.join(os3.tmpdir(), "file-explorer-media");
2029
+ await fs13.mkdir(dir, { recursive: true });
2030
+ const baseName = path16.basename(sourceFile, path16.extname(sourceFile));
2031
+ const timestamp = Date.now();
2032
+ const outputName = `${baseName}_${timestamp}.${targetFormat}`;
2033
+ return path16.join(dir, outputName);
2034
+ }
2035
+ function parseProgress(stderr, duration) {
2036
+ const timeMatch = stderr.match(/time=(\d+):(\d+):(\d+)\.(\d+)/);
2037
+ if (!timeMatch) return null;
2038
+ const hours = parseInt(timeMatch[1]);
2039
+ const minutes = parseInt(timeMatch[2]);
2040
+ const seconds = parseInt(timeMatch[3]);
2041
+ const ms = parseInt(timeMatch[4]);
2042
+ const currentTime = hours * 3600 + minutes * 60 + seconds + ms / 100;
2043
+ const speedMatch = stderr.match(/speed=\s*([\d.]+)x/);
2044
+ const speed = speedMatch ? `${speedMatch[1]}x` : void 0;
2045
+ let percent = 0;
2046
+ if (duration && duration > 0) {
2047
+ percent = Math.min(100, Math.round(currentTime / duration * 100));
2048
+ }
2049
+ return {
2050
+ percent,
2051
+ time: currentTime,
2052
+ duration,
2053
+ speed
2054
+ };
2055
+ }
2056
+ async function remuxVideo(ffmpegPath, inputPath, outputPath, duration, onProgress) {
2057
+ return new Promise((resolve, reject) => {
2058
+ const args = [
2059
+ "-i",
2060
+ inputPath,
2061
+ "-c",
2062
+ "copy",
2063
+ // 复制流,不重新编码
2064
+ "-movflags",
2065
+ "+faststart",
2066
+ // 优化 MP4 播放
2067
+ "-y",
2068
+ // 覆盖输出文件
2069
+ outputPath
2070
+ ];
2071
+ const ffmpeg = spawn2(ffmpegPath, args);
2072
+ let stderrBuffer = "";
2073
+ ffmpeg.stderr.on("data", (data) => {
2074
+ stderrBuffer += data.toString();
2075
+ if (onProgress) {
2076
+ const progress = parseProgress(stderrBuffer, duration);
2077
+ if (progress) {
2078
+ onProgress(progress);
2079
+ }
2080
+ }
2081
+ });
2082
+ ffmpeg.on("close", (code) => {
2083
+ if (code === 0) {
2084
+ resolve();
2085
+ } else {
2086
+ reject(new Error(`ffmpeg exited with code ${code}`));
2087
+ }
2088
+ });
2089
+ ffmpeg.on("error", reject);
2090
+ });
2091
+ }
2092
+ async function transcodeVideo(ffmpegPath, inputPath, outputPath, duration, onProgress) {
2093
+ return new Promise((resolve, reject) => {
2094
+ const args = [
2095
+ "-i",
2096
+ inputPath,
2097
+ "-c:v",
2098
+ "libx264",
2099
+ // H.264 编码
2100
+ "-preset",
2101
+ "fast",
2102
+ // 编码速度预设
2103
+ "-crf",
2104
+ "23",
2105
+ // 质量(18-28,越小越好)
2106
+ "-c:a",
2107
+ "aac",
2108
+ // AAC 音频
2109
+ "-b:a",
2110
+ "192k",
2111
+ // 音频比特率
2112
+ "-movflags",
2113
+ "+faststart",
2114
+ "-y",
2115
+ outputPath
2116
+ ];
2117
+ const ffmpeg = spawn2(ffmpegPath, args);
2118
+ let stderrBuffer = "";
2119
+ ffmpeg.stderr.on("data", (data) => {
2120
+ stderrBuffer += data.toString();
2121
+ if (onProgress) {
2122
+ const progress = parseProgress(stderrBuffer, duration);
2123
+ if (progress) {
2124
+ onProgress(progress);
2125
+ }
2126
+ }
2127
+ });
2128
+ ffmpeg.on("close", (code) => {
2129
+ if (code === 0) {
2130
+ resolve();
2131
+ } else {
2132
+ reject(new Error(`ffmpeg exited with code ${code}`));
2133
+ }
2134
+ });
2135
+ ffmpeg.on("error", reject);
2136
+ });
2137
+ }
2138
+ async function remuxAudio(ffmpegPath, inputPath, outputPath, duration, onProgress) {
2139
+ return new Promise((resolve, reject) => {
2140
+ const args = [
2141
+ "-i",
2142
+ inputPath,
2143
+ "-c",
2144
+ "copy",
2145
+ "-y",
2146
+ outputPath
2147
+ ];
2148
+ const ffmpeg = spawn2(ffmpegPath, args);
2149
+ let stderrBuffer = "";
2150
+ ffmpeg.stderr.on("data", (data) => {
2151
+ stderrBuffer += data.toString();
2152
+ if (onProgress) {
2153
+ const progress = parseProgress(stderrBuffer, duration);
2154
+ if (progress) {
2155
+ onProgress(progress);
2156
+ }
2157
+ }
2158
+ });
2159
+ ffmpeg.on("close", (code) => {
2160
+ if (code === 0) {
2161
+ resolve();
2162
+ } else {
2163
+ reject(new Error(`ffmpeg exited with code ${code}`));
2164
+ }
2165
+ });
2166
+ ffmpeg.on("error", reject);
2167
+ });
2168
+ }
2169
+ async function transcodeAudio(ffmpegPath, inputPath, outputPath, duration, onProgress) {
2170
+ return new Promise((resolve, reject) => {
2171
+ const ext = path16.extname(outputPath).toLowerCase();
2172
+ const isM4a = ext === ".m4a";
2173
+ const args = [
2174
+ "-i",
2175
+ inputPath,
2176
+ "-c:a",
2177
+ isM4a ? "aac" : "libmp3lame",
2178
+ "-b:a",
2179
+ "192k",
2180
+ "-y",
2181
+ outputPath
2182
+ ];
2183
+ const ffmpeg = spawn2(ffmpegPath, args);
2184
+ let stderrBuffer = "";
2185
+ ffmpeg.stderr.on("data", (data) => {
2186
+ stderrBuffer += data.toString();
2187
+ if (onProgress) {
2188
+ const progress = parseProgress(stderrBuffer, duration);
2189
+ if (progress) {
2190
+ onProgress(progress);
2191
+ }
2192
+ }
2193
+ });
2194
+ ffmpeg.on("close", (code) => {
2195
+ if (code === 0) {
2196
+ resolve();
2197
+ } else {
2198
+ reject(new Error(`ffmpeg exited with code ${code}`));
2199
+ }
2200
+ });
2201
+ ffmpeg.on("error", reject);
2202
+ });
2203
+ }
2204
+ async function transcodeMedia(ffmpegPath, inputPath, transcodeInfo, tempDir, onProgress) {
2205
+ try {
2206
+ if (!transcodeInfo.needsTranscode) {
2207
+ return {
2208
+ success: true,
2209
+ outputPath: inputPath
2210
+ };
2211
+ }
2212
+ const targetFormat = transcodeInfo.targetFormat || (transcodeInfo.type === "video" ? "mp4" : "mp3");
2213
+ const outputPath = await getTempOutputPath(inputPath, targetFormat, tempDir);
2214
+ const duration = transcodeInfo.formatInfo?.duration;
2215
+ if (transcodeInfo.type === "video") {
2216
+ if (transcodeInfo.method === "remux") {
2217
+ await remuxVideo(ffmpegPath, inputPath, outputPath, duration, onProgress);
2218
+ } else {
2219
+ await transcodeVideo(ffmpegPath, inputPath, outputPath, duration, onProgress);
2220
+ }
2221
+ } else {
2222
+ if (transcodeInfo.method === "remux") {
2223
+ await remuxAudio(ffmpegPath, inputPath, outputPath, duration, onProgress);
2224
+ } else {
2225
+ await transcodeAudio(ffmpegPath, inputPath, outputPath, duration, onProgress);
2226
+ }
2227
+ }
2228
+ if (onProgress) {
2229
+ onProgress({ percent: 100, duration });
2230
+ }
2231
+ return {
2232
+ success: true,
2233
+ outputPath
2234
+ };
2235
+ } catch (error) {
2236
+ return {
2237
+ success: false,
2238
+ error: error instanceof Error ? error.message : "Unknown error"
2239
+ };
2240
+ }
2241
+ }
2242
+ async function cleanupTranscodedFile(filePath) {
2243
+ try {
2244
+ if (filePath.includes("file-explorer-media")) {
2245
+ await fs13.unlink(filePath);
2246
+ }
2247
+ } catch {
2248
+ }
2249
+ }
2250
+ async function cleanupAllTranscodedFiles(tempDir) {
2251
+ const dir = tempDir || path16.join(os3.tmpdir(), "file-explorer-media");
2252
+ try {
2253
+ const files = await fs13.readdir(dir);
2254
+ await Promise.all(
2255
+ files.map((file) => fs13.unlink(path16.join(dir, file)).catch(() => {
2256
+ }))
2257
+ );
2258
+ } catch {
2259
+ }
2260
+ }
2261
+
2262
+ // src/media/service.ts
2263
+ import path17 from "path";
2264
+ import { execFile as execFile2 } from "child_process";
2265
+ import { promisify as promisify4 } from "util";
2266
+ var execFileAsync2 = promisify4(execFile2);
2267
+ var mediaServiceInstance = null;
2268
+ var MediaService = class {
2269
+ ffmpegPath;
2270
+ ffprobePath;
2271
+ tempDir;
2272
+ urlEncoder;
2273
+ // 缓存转码信息,避免重复检测
2274
+ transcodeInfoCache = /* @__PURE__ */ new Map();
2275
+ // 缓存已转码文件路径
2276
+ transcodedFiles = /* @__PURE__ */ new Map();
2277
+ constructor(options) {
2278
+ this.ffmpegPath = options.ffmpegPath;
2279
+ this.ffprobePath = options.ffprobePath || path17.join(path17.dirname(options.ffmpegPath), "ffprobe");
2280
+ this.tempDir = options.tempDir;
2281
+ this.urlEncoder = options.urlEncoder;
2282
+ }
2283
+ /**
2284
+ * 检测文件是否需要转码
2285
+ */
2286
+ async needsTranscode(filePath) {
2287
+ const cached = this.transcodeInfoCache.get(filePath);
2288
+ if (cached) {
2289
+ return cached;
2290
+ }
2291
+ const info = await detectTranscodeNeeds(filePath, this.ffprobePath);
2292
+ this.transcodeInfoCache.set(filePath, info);
2293
+ return info;
2294
+ }
2295
+ /**
2296
+ * 执行转码并返回可播放的 URL
2297
+ */
2298
+ async transcode(filePath, onProgress) {
2299
+ const existingOutput = this.transcodedFiles.get(filePath);
2300
+ if (existingOutput) {
2301
+ return {
2302
+ success: true,
2303
+ outputPath: existingOutput,
2304
+ url: this.urlEncoder ? this.urlEncoder(existingOutput) : `file://${existingOutput}`
2305
+ };
2306
+ }
2307
+ const transcodeInfo = await this.needsTranscode(filePath);
2308
+ if (!transcodeInfo.needsTranscode) {
2309
+ const url = this.urlEncoder ? this.urlEncoder(filePath) : `file://${filePath}`;
2310
+ return {
2311
+ success: true,
2312
+ outputPath: filePath,
2313
+ url
2314
+ };
2315
+ }
2316
+ const result = await transcodeMedia(
2317
+ this.ffmpegPath,
2318
+ filePath,
2319
+ transcodeInfo,
2320
+ this.tempDir,
2321
+ onProgress
2322
+ );
2323
+ if (result.success && result.outputPath) {
2324
+ this.transcodedFiles.set(filePath, result.outputPath);
2325
+ result.url = this.urlEncoder ? this.urlEncoder(result.outputPath) : `file://${result.outputPath}`;
2326
+ }
2327
+ return result;
2328
+ }
2329
+ /**
2330
+ * 获取媒体元数据
2331
+ */
2332
+ async getMetadata(filePath) {
2333
+ try {
2334
+ const { stdout } = await execFileAsync2(this.ffprobePath, [
2335
+ "-v",
2336
+ "quiet",
2337
+ "-print_format",
2338
+ "json",
2339
+ "-show_format",
2340
+ "-show_streams",
2341
+ filePath
2342
+ ]);
2343
+ const data = JSON.parse(stdout);
2344
+ const format = data.format || {};
2345
+ const tags = format.tags || {};
2346
+ const formatInfo = await getMediaFormat(filePath, this.ffprobePath);
2347
+ if (!formatInfo) return null;
2348
+ return {
2349
+ filePath,
2350
+ type: formatInfo.type,
2351
+ duration: parseFloat(format.duration) || 0,
2352
+ format: formatInfo,
2353
+ title: tags.title || tags.TITLE,
2354
+ artist: tags.artist || tags.ARTIST,
2355
+ album: tags.album || tags.ALBUM,
2356
+ year: tags.date || tags.DATE || tags.year || tags.YEAR
2357
+ };
2358
+ } catch {
2359
+ return null;
2360
+ }
2361
+ }
2362
+ /**
2363
+ * 获取可播放的 URL
2364
+ * 如果文件需要转码,则执行转码;否则直接返回文件 URL
2365
+ */
2366
+ async getPlayableUrl(filePath, onProgress) {
2367
+ const result = await this.transcode(filePath, onProgress);
2368
+ return result.success ? result.url || null : null;
2369
+ }
2370
+ /**
2371
+ * 清理指定文件的转码缓存
2372
+ */
2373
+ async cleanupFile(filePath) {
2374
+ const transcodedPath = this.transcodedFiles.get(filePath);
2375
+ if (transcodedPath) {
2376
+ await cleanupTranscodedFile(transcodedPath);
2377
+ this.transcodedFiles.delete(filePath);
2378
+ }
2379
+ this.transcodeInfoCache.delete(filePath);
2380
+ }
2381
+ /**
2382
+ * 清理所有转码缓存
2383
+ */
2384
+ async cleanup() {
2385
+ await cleanupAllTranscodedFiles(this.tempDir);
2386
+ this.transcodedFiles.clear();
2387
+ this.transcodeInfoCache.clear();
2388
+ }
2389
+ /**
2390
+ * 清除缓存(不删除文件)
2391
+ */
2392
+ clearCache() {
2393
+ this.transcodeInfoCache.clear();
2394
+ }
2395
+ };
2396
+ function initMediaService(options) {
2397
+ mediaServiceInstance = new MediaService(options);
2398
+ return mediaServiceInstance;
2399
+ }
2400
+ function getMediaService() {
2401
+ return mediaServiceInstance;
2402
+ }
2403
+ function createMediaService(options) {
2404
+ return new MediaService(options);
2405
+ }
1173
2406
  export {
1174
2407
  APP_PROTOCOL_HOST,
1175
2408
  APP_PROTOCOL_PREFIX,
1176
2409
  APP_PROTOCOL_SCHEME,
1177
2410
  FileType,
2411
+ MediaService,
1178
2412
  SqliteThumbnailDatabase,
1179
2413
  ThumbnailService,
2414
+ WatchManager,
2415
+ cleanupAllTranscodedFiles,
2416
+ cleanupTranscodedFile,
2417
+ closeThumbnailDatabase,
2418
+ compressFiles,
1180
2419
  copyFiles,
1181
2420
  copyFilesToClipboard,
1182
2421
  createFfmpegVideoProcessor,
1183
2422
  createFile,
1184
2423
  createFolder,
2424
+ createMediaService,
1185
2425
  createSharpImageProcessor,
1186
2426
  createSqliteThumbnailDatabase,
1187
2427
  decodeFileUrl,
1188
2428
  deleteFiles,
2429
+ detectArchiveFormat,
2430
+ detectTranscodeNeeds,
1189
2431
  encodeFileUrl,
1190
2432
  exists,
2433
+ extractArchive,
1191
2434
  formatDate,
1192
2435
  formatDateTime,
1193
2436
  formatFileSize,
@@ -1195,28 +2438,39 @@ export {
1195
2438
  getApplicationIcon,
1196
2439
  getClipboardFiles,
1197
2440
  getFileHash,
1198
- getFileHashes,
1199
2441
  getFileInfo,
1200
2442
  getFileType,
1201
2443
  getHomeDirectory,
1202
- getPlatform,
2444
+ getMediaFormat,
2445
+ getMediaService,
2446
+ getMediaTypeByExtension,
2447
+ getPlatform2 as getPlatform,
1203
2448
  getSqliteThumbnailDatabase,
1204
2449
  getSystemPath,
1205
2450
  getThumbnailService,
2451
+ getWatchManager,
2452
+ initMediaService,
1206
2453
  initThumbnailService,
1207
2454
  isAppProtocolUrl,
2455
+ isArchiveFile,
1208
2456
  isDirectory,
1209
2457
  isMediaFile,
1210
2458
  isPreviewable,
1211
2459
  moveFiles,
2460
+ openInEditor,
2461
+ openInTerminal,
1212
2462
  pasteFiles,
1213
2463
  readDirectory,
1214
2464
  readFileContent,
1215
2465
  readImageAsBase64,
1216
2466
  renameFile,
2467
+ revealInFileManager,
1217
2468
  searchFiles,
1218
2469
  searchFilesStream,
1219
2470
  searchFilesSync,
2471
+ showFileInfo,
2472
+ transcodeMedia,
2473
+ watchDirectory,
1220
2474
  writeFileContent
1221
2475
  };
1222
2476
  //# sourceMappingURL=index.js.map