fs.html 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216
  1. <html>
  2. <head>
  3. <title>File Manager</title>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
  6. <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  7. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css" />
  8. <script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
  9. <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js"></script>
  10. <link rel="stylesheet" href="fs.css">
  11. <script>
  12. </script>
  13. <style>
  14. body{
  15. background-color: white;
  16. }
  17. #uploadProgressBar{
  18. position: fixed;
  19. bottom: 0;
  20. left: 0;
  21. width: 100%;
  22. }
  23. .fileOprBtn.disabled{
  24. opacity: 0.5;
  25. user-select: none;
  26. pointer-events: none;
  27. }
  28. </style>
  29. </head>
  30. <body class="whiteTheme">
  31. <div id="navibar" class="navibar">
  32. <!-- File Opr Group-->
  33. <button class="fileOprBtn" title="Open" onclick="openViaButton(event);"><img class="opricon" src="img/opr/open.svg"><p class="oprtxt" locale="fileopr/Open">Open</p></button>
  34. <button id="copyButton" class="fileOprBtn" title="Copy" onclick="copy();"><img class="opricon" src="img/opr/copy.svg"><p class="oprtxt" locale="fileopr/Copy">Copy</p></button>
  35. <button id="pasteButton" class="fileOprBtn" title="Paste" onclick="paste();"><img class="opricon" src="img/opr/paste.svg"><p class="oprtxt" locale="fileopr/Paste">Paste</p></button>
  36. <div class="fileoprGroupDivider" style="display: inline-block; vertical-align: top;">
  37. <button class="fileoprSmallBtn" title="Refresh" onclick="refresh();"><i class="green refresh icon"></i> <span locale="fileopr/Refresh">Refresh</span></button><br>
  38. <button class="fileoprSmallBtn" title="Cut" onclick="cut();"><i class="blue cut icon"></i> <span locale="fileopr/Cut">Cut</span></button><br>
  39. <button class="fileoprSmallBtn" title="Rename" onclick="rename();"><i class="teal i cursor icon"></i> <span locale="fileopr/Rename">Rename</span></button>
  40. </div>
  41. <button class="fileOprBtn" title="Upload" onclick="upload(); "><img class="opricon" src="img/opr/upload.svg"><p class="oprtxt wideScreenOnly" locale="fileopr/Upload">Upload</p></button>
  42. <button class="fileOprBtn" title="Download" onclick="downloadFile(); "><img class="opricon" src="img/opr/download.svg"><p class="oprtxt wideScreenOnly" locale="fileopr/Download">Download</p></button>
  43. <div class="fileoprGroupDivider" style="display: inline-block; vertical-align: top;"></div>
  44. <div class="fileoprGroupDivider" style="display: inline-block; vertical-align: top;">
  45. <button class="fileoprSmallBtn" title="New File" onclick="newFile();"><i style="color: #c7c7c7 !important;" class="file outline icon"></i> <span locale="fileopr/New File">New File</span></button><br>
  46. <button class="fileoprSmallBtn" title="New Folder" onclick="newFolder();"><i style="color: #ffe79e !important;" class="yellow folder icon"></i> <span locale="fileopr/New Folder">New Folder</span></button><br>
  47. <button class="fileoprSmallBtn" title="Delete" onclick="deleteFile();"><i class="red times icon"></i> <span locale="fileopr/Delete">Delete</span></button><br>
  48. </div>
  49. <button class="fileOprBtn" title="Download" onclick="shareFile(); "><img class="opricon" src="img/opr/share.svg"><p class="oprtxt wideScreenOnly" locale="fileopr/Share">Share</p></button>
  50. <br>
  51. <!-- Directoy navigations -->
  52. <div class="addressBar">
  53. <button id="prevDir" class="navibarBtn" onclick="prevDir();" title="Back"><i class="left arrow icon"></i></button>
  54. <button id="ppbtn" class="navibarBtn" onclick="parentDir();" title="Parent Folder"><i class="up arrow icon"></i></button>
  55. <div id="pathDisplayField" class="ui breadcrumb addressText pathDisplay desktopOnly" >
  56. <div class="section selectable"><i class="folder icon"></i> user</div>
  57. <div class="divider">:/</div>
  58. <div class="section selectable">Desktop</div>
  59. </div>
  60. <button id="togglePropertiesViewBtn" style="margin-left: 0.4em;" class="ui icon tiny button videmode propbar" title="Show Properties" onclick="togglePropertiesView(this);"><i class="columns icon"></i></button>
  61. </div>
  62. <div class="msgbox" style="z-index:999; display:none; padding-bottom: 1em;">
  63. <i class="checkmark icon showicon"></i> <span style="word-break: break-all;">No Message</span>
  64. <div class="closeMsgButton" onclick="$(this).parent().stop().slideUp('fast');"><i class="caret down icon"></i></div>
  65. </div>
  66. </div>
  67. <div id="mainWindow">
  68. <div id="folderView" style="height: 100%;">
  69. <div id="folderList" class="fileviewList">
  70. </div>
  71. <div id="fileList" class="fileviewList">
  72. </div>
  73. <br>
  74. </div>
  75. <div id="propertiesView" class="small">
  76. <div class="preview" style="margin-top: 0.4em;" align="center">
  77. <img class="ui image" style="max-height: 300px;">
  78. <audio src="" controls style="display:none; width: 100%;"></audio>
  79. </div>
  80. <h3 class="ui header" style="margin-top: 0.4em;">
  81. <span class="filename" style="word-break: break-all;" locale="sidebar/default/nofileselected">No File Selected</span>
  82. <div id="fileVpath" class="sub header vpath" style="word-break: break-all;" locale="sidebar/default/instruction">Select a file to view file properties</div>
  83. </h3>
  84. <table class="ui very basic table">
  85. <tbody class="propertiesTable">
  86. </tbody>
  87. </table>
  88. <button id="loadPreviewButton" class="ui small fluid basic disabled button" onclick="loadPreview();">Load Preview</button>
  89. <button id="removeShareButton" class="ui small fluid basic disabled button" style="margin-top: 0.4em;" onclick="removeShare();"><i class="ui red remove icon"></i> Remove Share</button>
  90. </div>
  91. <div id="uploadProgressBar">
  92. <div class="ui small indicating progress" style="margin-bottom: 0px !important; border-radius: 0 !important;">
  93. <div class="bar" style="background-color: #92cfe7 !important; min-width: 0; border-radius: 0 !important;">
  94. <div class="progress"></div>
  95. </div>
  96. <div class="label">Uploading Files</div>
  97. </div>
  98. </div>
  99. </div>
  100. <script>
  101. let currentPath = "/";
  102. let listDirInitTime = 0;
  103. let propertiesView = true;
  104. //Uploads
  105. let uploadPendingFiles = [];
  106. let currentlyUploading = false;
  107. //File operations
  108. let cutMode = false;
  109. let cutPendingFilepath = [];
  110. let copyBuffering = false;
  111. let copySrcFilename = "";
  112. let copyPendingFile = undefined;
  113. //History
  114. let dirHistory = [];
  115. $(window).on("resize", function(){
  116. updateElementSize();
  117. })
  118. $(document).ready(function(){
  119. if (window.location.hash.length > 1){
  120. let previousPath = window.location.hash.substr(1);
  121. listDir(previousPath);
  122. }else{
  123. listDir("/");
  124. }
  125. //Add drop events to file view
  126. var folderView = document.getElementById('folderView');
  127. // Add event listeners for dragover and drop events
  128. folderView.addEventListener('dragover', handleDragOver, false);
  129. folderView.addEventListener('drop', handleFileDrop, false);
  130. });
  131. // Event handler for dragover event
  132. function handleDragOver(event) {
  133. event.preventDefault();
  134. event.stopPropagation();
  135. }
  136. function handleFileDrop(event) {
  137. event.preventDefault();
  138. event.stopPropagation();
  139. // Get the File object from the dataTransfer object
  140. var files = event.dataTransfer.files;
  141. //Push the files into queue
  142. for (var i = 0; i < files.length; i++) {
  143. if (files[i].name.indexOf(".") < 0){
  144. msgbox("Folder upload is not supported", false);
  145. if (files.length == 1){
  146. return;
  147. }
  148. continue;
  149. }
  150. let pathToUpload = currentPath;
  151. uploadPendingFiles.push({
  152. "file": files[i],
  153. "dir": pathToUpload
  154. });
  155. }
  156. //Upload the first file
  157. msgbox("File Upload Started")
  158. if (!currentlyUploading){
  159. uploadFileQueue();
  160. }
  161. }
  162. function uploadFileQueue(){
  163. if (uploadPendingFiles.length > 0){
  164. currentlyUploading = true;
  165. let nextFileToUpload = uploadPendingFiles.pop();
  166. handleFile(nextFileToUpload.file, nextFileToUpload.dir, function(){
  167. msgbox(nextFileToUpload.file.name + " uploaded");
  168. setTimeout(function(){
  169. uploadFileQueue();
  170. }, 300);
  171. });
  172. }else{
  173. msgbox("Upload Queue Completed");
  174. currentlyUploading = false;
  175. }
  176. }
  177. function deleteFile(){
  178. if ($(".fileObject.selected").length == 0){
  179. return;
  180. }
  181. if (confirm("Confirm removing " + $(".fileObject.selected").length + " files?")){
  182. let counter = $(".fileObject.selected").length;
  183. $(".fileObject.selected").each(function(){
  184. let thisFilepath = $(this).attr("filepath");
  185. $.ajax({
  186. url: "/api/fs/del?target=" + thisFilepath,
  187. method: "POST",
  188. success: function(data){
  189. if (data.error != undefined){
  190. msgbox(data.error, false);
  191. }else{
  192. counter--;
  193. if (counter == 0){
  194. //All removed
  195. msgbox("File(s) Removed");
  196. refresh();
  197. }
  198. }
  199. }
  200. });
  201. });
  202. }
  203. }
  204. //Check if a extension is code file, remember to trim the first dot
  205. function isCodeFiles(ext){
  206. if (ext == "html" || ext == "htm"|| ext == "css"|| ext == "js"|| ext == "json"){
  207. return true;
  208. }
  209. return false;
  210. }
  211. function openViaButton(evt){
  212. if ($(".fileObject.selected").length == 0){
  213. return;
  214. }
  215. let editableCodeFiles = [];
  216. $(".fileObject.selected").each(function(){
  217. let ftype = $(this).attr('type');
  218. let filepath = $(this).attr("filepath");
  219. let filename = $(this).attr("filename");
  220. if (ftype != "folder"){
  221. let ext = filepath.split(".").pop();
  222. if (isCodeFiles(ext)){
  223. editableCodeFiles.push({
  224. "filename": filename,
  225. "filepath": filepath
  226. });
  227. }else{
  228. openthis($(this), evt);
  229. }
  230. }
  231. });
  232. if (editableCodeFiles.length > 0){
  233. let hash = encodeURIComponent(JSON.stringify(editableCodeFiles))
  234. window.open("notepad/index.html#" + hash);
  235. }
  236. }
  237. function refresh(){
  238. listDir(currentPath);
  239. }
  240. function updatePathDisplay(){
  241. $("#pathDisplayField").empty();
  242. $("#pathDisplayField").append(`<div class="section selectable" onclick="jumpToDir('/');"><i class="microchip icon"></i> ESP8266</div><div class="divider">:/</div>`);
  243. let pathSegments = currentPath;
  244. if (pathSegments.startsWith("/")){
  245. pathSegments = pathSegments.substr(1);
  246. }
  247. if (pathSegments.endsWith("/")){
  248. pathSegments = pathSegments.substr(0, pathSegments.length - 1);
  249. }
  250. pathSegments = pathSegments.split("/");
  251. let htmlSegments = [];
  252. let accumulativeDir = "/";
  253. pathSegments.forEach(function(segment){
  254. accumulativeDir = accumulativeDir + segment + "/"
  255. htmlSegments.push(`<div class="section selectable" onclick="jumpToDir('${accumulativeDir}');">${segment}</div>`);
  256. });
  257. $("#pathDisplayField").append(htmlSegments.join(`<div class="divider">/</div>`));
  258. }
  259. function cut(){
  260. if ($(".fileObject.selected").length == 0){
  261. msgbox("No file selected", false);
  262. return;
  263. }
  264. cutMode = true;
  265. cutPendingFilepath = [];
  266. $(".fileObject.selected").each(function(){
  267. cutPendingFilepath.push({
  268. filename: $(this).attr("filename"),
  269. filepath: $(this).attr("filepath")
  270. });
  271. });
  272. msgbox("File Ready to Paste");
  273. }
  274. function downloadFile(){
  275. let selectedFiles = [];
  276. let filenames = [];
  277. let containsDir = false;
  278. $(".fileObject.selected").each(function(){
  279. if ($(this).attr("type") == "folder"){
  280. containsDir = true;
  281. }
  282. });
  283. if (containsDir){
  284. msgbox("Folder download is not supported", false);
  285. return;
  286. };
  287. if ($(".fileObject.selected").length > 0){
  288. $(".fileObject.selected").each(function(){
  289. selectedFiles.push($(this).attr("filepath"));
  290. filenames.push($(this).attr("filename"))
  291. });
  292. }
  293. if (selectedFiles.length == 1){
  294. //Only one file
  295. //window.open("/api/fs/download?file=" + selectedFiles[0]);
  296. var file_path = "/api/fs/download?file=" + selectedFiles[0];
  297. var a = document.createElement('A');
  298. a.href = file_path;
  299. a.download = file_path.substr(file_path.lastIndexOf('/') + 1);
  300. document.body.appendChild(a);
  301. a.click();
  302. document.body.removeChild(a);
  303. }else{
  304. let urls = [];
  305. selectedFiles.forEach(function(thisFilepath){
  306. urls.push("/api/fs/download?file=" + thisFilepath);
  307. });
  308. msgbox("Zipping might take a few minutes...");
  309. compressFileToZip(urls, filenames);
  310. }
  311. }
  312. function compressFileToZip(urls, filenames) {
  313. var zip = new JSZip();
  314. // Create a function to fetch each image and add it to the zip
  315. var addFileToZip = function (url, filename) {
  316. return new Promise(function (resolve, reject) {
  317. var xhr = new XMLHttpRequest();
  318. xhr.open('GET', url);
  319. xhr.responseType = 'blob';
  320. xhr.onload = function () {
  321. if (xhr.status === 200) {
  322. zip.file(filename, xhr.response);
  323. resolve();
  324. } else {
  325. reject(Error('Failed to fetch file: ' + url));
  326. }
  327. };
  328. xhr.onerror = function () {
  329. reject(Error('Error fetching file: ' + url));
  330. };
  331. xhr.send();
  332. });
  333. };
  334. // Iterate over each image URL and add it to the zip
  335. var promises = urls.map(function (url, index) {
  336. var filename = filenames[index];
  337. return addFileToZip(url, filename);
  338. });
  339. // When all promises are resolved, generate the zip file
  340. Promise.all(promises).then(function () {
  341. zip.generateAsync({ type: 'blob' }).then(function (content) {
  342. // Save the zip file or do something with it
  343. msgbox("Download Zip Created");
  344. saveAs(content, 'dl_' + Math.floor(Date.now() / 1000) +'.zip');
  345. });
  346. }).catch(function (error) {
  347. console.error(error);
  348. });
  349. }
  350. function saveAs(blob, filename) {
  351. if (navigator.msSaveBlob) {
  352. // For IE and Edge browsers
  353. navigator.msSaveBlob(blob, filename);
  354. } else {
  355. // For other browsers
  356. var link = document.createElement('a');
  357. link.href = URL.createObjectURL(blob);
  358. link.download = filename;
  359. link.style.display = 'none';
  360. document.body.appendChild(link);
  361. link.click();
  362. document.body.removeChild(link);
  363. }
  364. }
  365. //Jump to new directory via path input field
  366. function jumpToDir(newDir){
  367. if (newDir == currentPath){
  368. return;
  369. }
  370. listDir(newDir);
  371. }
  372. function prevDir(){
  373. if (dirHistory.length > 0){
  374. let pathToGo = dirHistory.pop();
  375. if (pathToGo == currentPath && dirHistory.length > 0){
  376. //pop again
  377. pathToGo = dirHistory.pop();
  378. listDir(pathToGo);
  379. return;
  380. }
  381. listDir(pathToGo, false);
  382. }
  383. }
  384. function listDir(path, recordHistory = true){
  385. if (path.length > 0 && path.substr(0, 1) != "/"){
  386. path = "/" + path;
  387. }
  388. if (!path.endsWith("/")){
  389. path = path + "/";
  390. }
  391. //Update the current path
  392. currentPath = path;
  393. if (recordHistory && currentPath != dirHistory[dirHistory.length - 1]){
  394. dirHistory.push(currentPath);
  395. }
  396. window.location.hash = currentPath;
  397. updatePathDisplay();
  398. listDirInitTime = Date.now();
  399. let validationTimestamp = listDirInitTime;
  400. $("#folderList").html(`<div class="fileObject item" style="pointer-events: none;">
  401. <span style="display:inline-block !important;word-break: break-all; width:100%;" class="normal object">
  402. <i class="loading spinner icon" style="margin-right:12px; color:#grey;"></i> <span class="filename">Loading</span>
  403. </span>
  404. </div>`);
  405. $("#fileList").html("");
  406. $.ajax({
  407. url: "/api/fs/list?dir=" + path,
  408. success: function(data){
  409. if (validationTimestamp != listDirInitTime){
  410. //Another refresh is in progress. Skip render.
  411. return;
  412. }
  413. $("#folderList").html("");
  414. if (data.error != undefined){
  415. $("#folderList").append(`<div class="ui segment">
  416. <div class="ui header themed">
  417. <i class="remove icon" style="display: inline-block;"></i> <span>Error Opening Folder</span>
  418. <div class="sub header" style="margin-top:12px;">Server return the following error message: <br><code>${data.error.toUpperCase()}</code><br>
  419. ${new Date().toLocaleString()}</div>
  420. </div>
  421. </div>`);
  422. msgbox(data.error, false);
  423. }else{
  424. data.forEach(function(filedata){
  425. let isDir = filedata.IsDir;
  426. let filename = filedata.Filename;
  427. let filesize = filedata.Filesize;
  428. let shareID = filedata.Share;
  429. if (isDir){
  430. $("#folderList").append(`<div class="fileObject item" draggable="true" filename="${filename}" filepath="${path + filename}" ondblclick="openthis(this,event);" type="folder">
  431. <span style="display:inline-block !important;word-break: break-all; width:100%;" class="normal object">
  432. <i class="folder icon" style="margin-right:12px; color:#eab54e;"></i> <span class="filename">${filename}</span>
  433. </span>
  434. </div>`);
  435. }else{
  436. let shareIcon = "";
  437. if (shareID != ""){
  438. shareIcon = ` <a href="/share?id=${shareID}" target="_blank"><i class="ui green share alternate icon"></i></a>`;
  439. }
  440. let extension = "." + filename.split(".").pop();
  441. let fileIcon = getFileIcon(extension);
  442. $("#fileList").append(`<div class="fileObject item" isShared="${(shareID!="")?"true":"false"}" draggable="true" filename="${filename}" filepath="${path + filename}" ondblclick="openthis(this,event);" type="file">
  443. <span style="display:inline-block !important;word-break: break-all; width:100%;" class="normal object">
  444. <i class="${fileIcon} icon" style="margin-right:12px; color:grey;"></i> <span class="filename">${filename} (${humanFileSize(filesize)}) ${shareIcon}</span>
  445. </span>
  446. </div>`);
  447. }
  448. });
  449. }
  450. $(".fileObject").off("click").on("click", function(e){
  451. if (!e.ctrlKey) {
  452. $(".fileObject.selected").removeClass("selected");
  453. getFileProperties( $(this).attr("filepath"));
  454. let fileType = $(this).attr('type');
  455. if (fileType == "folder"){
  456. $("#propertiesView").find(".preview").find("img").attr("src", "img/folder.svg");
  457. }else{
  458. $("#propertiesView").find(".preview").find("img").attr("src", "img/file.svg");
  459. }
  460. }
  461. $(this).addClass("selected");
  462. });
  463. sortFileList();
  464. }
  465. });
  466. }
  467. function openthis(target, event){
  468. let isDir = ($(target).attr("type") == "folder");
  469. if (isDir){
  470. let targetPath = $(target).attr("filepath");
  471. listDir(targetPath);
  472. }else{
  473. let ext = $(target).attr("filepath").split(".").pop();
  474. if (ext == "txt" || ext == "md"){
  475. //Open with markdown editor
  476. let hash = encodeURIComponent(JSON.stringify({
  477. "filename": $(target).attr("filename"),
  478. "filepath": $(target).attr("filepath")
  479. }))
  480. window.open("mde/index.html#" + hash);
  481. }else if (ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "gif" || ext == "webp"){
  482. //Open with photo viewer
  483. let hash = encodeURIComponent(JSON.stringify({
  484. "filename": $(target).attr("filename"),
  485. "filepath": $(target).attr("filepath")
  486. }))
  487. window.open("photo.html#" + hash);
  488. }else if (ext == "mp3" || ext == "aac" || ext == "ogg"){
  489. //Open with music player
  490. let hash = encodeURIComponent(JSON.stringify({
  491. "filename": $(target).attr("filename"),
  492. "filepath": $(target).attr("filepath")
  493. }))
  494. window.open("music.html#" + hash);
  495. }else if (ext == "mp4" || ext == "webm"){
  496. //Open with video player
  497. let hash = encodeURIComponent(JSON.stringify({
  498. "filename": $(target).attr("filename"),
  499. "filepath": $(target).attr("filepath")
  500. }))
  501. window.open("video.html#" + hash);
  502. }else if (isCodeFiles(ext)){
  503. //Open with notepad
  504. //**Notes the array wrapper in JSON object**
  505. let hash = encodeURIComponent(JSON.stringify([{
  506. "filename": $(target).attr("filename"),
  507. "filepath": $(target).attr("filepath")
  508. }]))
  509. window.open("notepad/index.html#" + hash);
  510. }else{
  511. window.open("/api/fs/download?file=" + $(target).attr("filepath") + "&preview=true");
  512. }
  513. }
  514. }
  515. function isFilenameValid(filename) {
  516. // Split the filename into the name and extension parts
  517. var name = filename.slice(0, filename.lastIndexOf('.'));
  518. var extension = filename.slice(filename.lastIndexOf('.') + 1);
  519. // Check if the name and extension lengths are within the limits
  520. if (name.length <= 8 && extension.length <= 3) {
  521. return true;
  522. } else {
  523. return false;
  524. }
  525. }
  526. function newFile(){
  527. var fileName = window.prompt("Name for new file: ", "file.txt");
  528. if (fileName.indexOf("/") >= 0){
  529. //Contains /. Reject
  530. msgbox("File name cannot contain path seperator", false);
  531. return;
  532. }
  533. if (fileName.indexOf(".") == -1){
  534. msgbox("Missing file extension")
  535. return
  536. }
  537. let filenameOnly = fileName.split(".");
  538. let ext = filenameOnly.pop();
  539. filenameOnly = filenameOnly.join(".");
  540. /*
  541. //Skip checking as modern FAT32 file systems can store as LONG FILENAME
  542. if (filenameOnly.length > 8){
  543. msgbox("File name too long (8 char max)", false);
  544. return;
  545. }
  546. if (ext.length > 3){
  547. msgbox("File extension too long (3 char max)", false);
  548. return
  549. }
  550. */
  551. //OK! Create the file
  552. const blob = new Blob(["\n"], { type: 'text/plain' });
  553. const file = new File([blob], fileName);
  554. handleFile(file, currentPath, function(){
  555. msgbox("New File Created");
  556. });
  557. }
  558. function newFolder(){
  559. var folderName = window.prompt("Name for new folder (8 char max): ", "Folder");
  560. if (folderName.indexOf("/") >= 0){
  561. //Contains /. Reject
  562. msgbox("Folder name cannot contain path seperator", false);
  563. return;
  564. }
  565. if (folderName.length > 8){
  566. msgbox("Folder name too long (8 char max)", false);
  567. return;
  568. }
  569. $.post("/api/fs/newFolder?path=" + currentPath + folderName, function(data){
  570. if (data.error != undefined){
  571. msgbox(data.error, false);
  572. }else{
  573. msgbox("Folder Created");
  574. refresh();
  575. }
  576. });
  577. }
  578. function rename(){
  579. if ($(".fileObject.selected").length > 1){
  580. //Too many objects
  581. }else if ($(".fileObject.selected").length == 1){
  582. var oldName = $(".fileObject.selected").attr("filename");
  583. var oldPath = $(".fileObject.selected").attr("filepath");
  584. var newName = window.prompt("Rename " + oldName + " to: ", oldName);
  585. if (newName.indexOf("/") >= 0){
  586. //Contains /. Reject
  587. msgbox("File name cannot contain path seperator", false);
  588. return;
  589. }
  590. /*
  591. //FAT32 allows filename longer than 8.3
  592. if (!isFilenameValid(newName)){
  593. msgbox("File name too long (8 char max)", false);
  594. return;
  595. }
  596. */
  597. if (newName && newName != oldName) {
  598. // User entered a new name, perform renaming logic here
  599. console.log(oldPath, currentPath + newName);
  600. $.ajax({
  601. url: "/api/fs/move?src=" + oldPath + "&dest=" + currentPath + newName,
  602. method: "POST",
  603. success: function(data){
  604. if (data.error != undefined){
  605. msgbox(data.error, false);
  606. }else{
  607. msgbox("File renamed");
  608. refresh();
  609. }
  610. }
  611. })
  612. }
  613. }
  614. }
  615. function getFileIcon(extension) {
  616. const textExtensions = [".md", ".txt"];
  617. const codeExtensions = [".js", ".json", ".css", ".html", ".htm"];
  618. const musicExtensions = [".mp3", ".aac", ".ogg", ".wav"];
  619. const videoExtensions = [".mp4", ".m4v", ".webm"];
  620. const photoExtensions = [".png", ".gif", ".jpg", ".ico", ".svg"];
  621. if (textExtensions.includes(extension)) {
  622. return "file alternate outline";
  623. } else if (codeExtensions.includes(extension)) {
  624. return "black file code outline";
  625. } else if (musicExtensions.includes(extension)) {
  626. return "blue music";
  627. } else if (videoExtensions.includes(extension)) {
  628. return "red video";
  629. } else if (photoExtensions.includes(extension)) {
  630. return "green image outline";
  631. } else {
  632. return "file outline";
  633. }
  634. }
  635. function isPreviewable(ext){
  636. let previeableFiles = [".png", ".gif", ".jpg", ".ico", ".svg"];
  637. return previeableFiles.includes(ext);
  638. }
  639. function getFileProperties(filepath){
  640. $.get("/api/fs/properties?file=" + filepath, function(data){
  641. if (data.error != undefined){
  642. msgbox(data.error, false);
  643. return;
  644. }
  645. $("#propertiesView").find(".filename").text(data.filename);
  646. $("#propertiesView").find(".vpath").text(data.filepath);
  647. let propTable = $("#propertiesView").find(".propertiesTable");
  648. let styleOverwrite = `min-width: 4em;`;
  649. $(propTable).html("");
  650. $(propTable).append(`<tr>
  651. <td style="${styleOverwrite}">
  652. File Size
  653. </td>
  654. <td>
  655. ${bytesToSize(data.filesize)}
  656. </td>
  657. </tr><tr>
  658. <td style="${styleOverwrite}">
  659. Disk Path
  660. </td>
  661. <td style="word-break: break-all;">
  662. /www${data.filepath}
  663. </td>
  664. </tr>
  665. <tr>
  666. <td style="${styleOverwrite}">
  667. Share ID
  668. </td>
  669. <td style="word-break: break-all;">
  670. ${(data.shareid=="" || data.shareid == undefined)?`File Not Shared`:`<a href="/share?id=${data.shareid}" target="_blank">${data.shareid}</a>`}
  671. </td>
  672. </tr>
  673. <tr>
  674. <td style="${styleOverwrite}">
  675. Folder
  676. </td>
  677. <td style="word-break: break-all;">
  678. ${data.isDir?`<i class="ui green check icon"></i>`:`<i class="ui red times icon"></i>`}
  679. </td>
  680. </tr>`);
  681. if (data.isDir){
  682. $(propTable).append(`<tr>
  683. <td style="${styleOverwrite}">
  684. Files #
  685. </td>
  686. <td>
  687. ${data.fileCounts}
  688. </td>
  689. </tr><tr>
  690. <td style="${styleOverwrite}">
  691. Folders #
  692. </td>
  693. <td style="word-break: break-all;">
  694. ${data.folderCounts}
  695. </td>
  696. </tr>`);
  697. //Folder is not previewable
  698. $("#propertiesView").find(".preview").find("img").attr("xsrc", "");
  699. $("#loadPreviewButton").addClass("disabled");
  700. }else{
  701. let ext = data.filepath.split(".").pop();
  702. if (isPreviewable("." + ext)){
  703. $("#propertiesView").find(".preview").find("img").attr("xsrc", "/api/fs/download?preview=true&file=" + data.filepath);
  704. $("#loadPreviewButton").removeClass("disabled");
  705. }else{
  706. $("#propertiesView").find(".preview").find("img").attr("xsrc", "");
  707. $("#loadPreviewButton").addClass("disabled");
  708. }
  709. if (data.shareid!=""){
  710. $("#removeShareButton").removeClass('disabled');
  711. }else{
  712. $("#removeShareButton").addClass('disabled');
  713. }
  714. }
  715. })
  716. }
  717. function loadPreview(){
  718. let readyToLoadSrc = $("#propertiesView").find(".preview").find("img").attr("xsrc");
  719. if (readyToLoadSrc == undefined || readyToLoadSrc == "" ){
  720. }else{
  721. let ext = readyToLoadSrc.split(".").pop();
  722. $("#propertiesView").find(".preview").find("img").show();
  723. $("#propertiesView").find(".preview").find("audio").hide();
  724. $("#propertiesView").find(".preview").find("img").attr("src", readyToLoadSrc);
  725. }
  726. }
  727. function bytesToSize(bytes) {
  728. var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
  729. if (bytes == 0) return '0 Byte';
  730. var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
  731. return Math.round(bytes / Math.pow(1024, i) * 100, 2) / 100 + ' ' + sizes[i];
  732. }
  733. function getParentDirectory(path) {
  734. // Remove any trailing slashes
  735. if (path.endsWith("/")){
  736. path = path.substr(0, path.length - 1);
  737. }
  738. // Find the last index of the slash character
  739. var lastIndex = path.lastIndexOf('/');
  740. // Extract the parent directory substring
  741. var parentDir = path.substring(0, lastIndex);
  742. return parentDir;
  743. }
  744. function parentDir(){
  745. if (currentPath.indexOf("/") >= 0 && currentPath != "/"){
  746. let parentPath = getParentDirectory(currentPath);
  747. listDir(parentPath);
  748. }else{
  749. //already top
  750. }
  751. }
  752. function humanFileSize(bytes, si=false, dp=1) {
  753. const thresh = si ? 1000 : 1024;
  754. if (Math.abs(bytes) < thresh) {
  755. return bytes + ' B';
  756. }
  757. const units = si
  758. ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
  759. : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
  760. let u = -1;
  761. const r = 10**dp;
  762. do {
  763. bytes /= thresh;
  764. ++u;
  765. } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
  766. return bytes.toFixed(dp) + ' ' + units[u];
  767. }
  768. function updateElementSize(){
  769. $("#mainWindow").css("height", window.innerHeight - $("#navibar").height() + "px");
  770. }
  771. updateElementSize();
  772. function upload() {
  773. // Create a file input element
  774. var fileInput = $('<input>').attr('type', 'file');
  775. // Trigger the file selector dialog
  776. fileInput.trigger('click');
  777. // Handle the selected file using a callback event handler
  778. fileInput.change(function(e) {
  779. var file = e.target.files[0];
  780. if (currentlyUploading){
  781. //Already tasks uploading in the background. Add it to queue
  782. uploadPendingFiles.push({
  783. "file": file,
  784. "dir": currentPath
  785. });
  786. msgbox("File Added to Upload Queue");
  787. return;
  788. }
  789. // Pass the file object to the callback event handler
  790. msgbox("File Upload Started")
  791. handleFile(file, currentPath, function(){
  792. msgbox("Upload Completed");
  793. });
  794. });
  795. }
  796. function handleFile(file, dir=currentPath, callback=undefined) {
  797. // Perform actions with the selected file
  798. $("#pasteButton").addClass("disabled");
  799. console.log('Selected file:', file);
  800. var formdata = new FormData();
  801. formdata.append("file1", file);
  802. var ajax = new XMLHttpRequest();
  803. ajax.upload.addEventListener("progress", progressHandler, false);
  804. ajax.addEventListener("load", function(event){
  805. let responseText = event.target.responseText;
  806. try{
  807. responseText = JSON.parse(responseText);
  808. if (responseText.error != undefined){
  809. alert(responseText.error);
  810. }
  811. }catch(ex){
  812. }
  813. completeHandler(event, dir==currentPath);
  814. $("#pasteButton").removeClass("disabled");
  815. if (callback != undefined){
  816. callback();
  817. }
  818. }, false); // doesnt appear to ever get called even upon success
  819. ajax.addEventListener("error", errorHandler, false);
  820. ajax.addEventListener("abort", abortHandler, false);
  821. ajax.open("POST", "/upload?dir=" + dir);
  822. ajax.send(formdata);
  823. }
  824. function progressHandler(event) {
  825. //_("loaded_n_total").innerHTML = "Uploaded " + event.loaded + " bytes of " + event.total; // event.total doesnt show accurate total file size
  826. var percent = (event.loaded / event.total) * 100;
  827. $("#uploadProgressBar").find(".bar").css("width", Math.round(percent) + "%");
  828. console.log("Uploaded " + event.loaded + " bytes => " + percent +"%");
  829. if (percent >= 100) {
  830. $("#uploadProgressBar").find(".bar").css("width", "100%");
  831. //_("status").innerHTML = "Please wait, writing file to filesystem";
  832. }
  833. }
  834. function completeHandler(event, requireRefresh=true) {
  835. $("#uploadProgressBar").find(".bar").css("width", "0%");
  836. if(requireRefresh){
  837. refresh();
  838. }
  839. }
  840. function errorHandler(event) {
  841. msgbox("Upload Failed", false);
  842. $("#pasteButton").removeClass("disabled");
  843. }
  844. function abortHandler(event) {
  845. msgbox("Upload Aborted", false);
  846. $("#pasteButton").removeClass("disabled");
  847. }
  848. function msgbox(message, succ=true){
  849. function capitalizeFirstLetter(string) {
  850. return string.charAt(0).toUpperCase() + string.slice(1);
  851. }
  852. message = capitalizeFirstLetter(message);
  853. if (succ){
  854. $(".msgbox").find(".showicon").attr("class", "green circle check icon showicon");
  855. }else{
  856. $(".msgbox").find(".showicon").attr("class", "red circle times icon showicon");
  857. }
  858. $(".msgbox").find("span").text(message);
  859. $(".msgbox").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
  860. }
  861. //Copy file
  862. function copy(){
  863. if ($(".fileObject.selected").length == 0){
  864. //No file selected
  865. msgbox("No file selected", false);
  866. return;
  867. }
  868. if ($(".fileObject.selected").length > 1){
  869. msgbox("WebStick can only support 1 file copy at a time", false);
  870. return;
  871. }
  872. if ($(".fileObject.selected").attr('type') == "folder"){
  873. msgbox("Folder copy is not supported", false);
  874. return;
  875. }
  876. let url = "/api/fs/download?file=" + $(".fileObject.selected").attr("filepath");
  877. let filename = $(".fileObject.selected").attr("filename");
  878. cutMode = false;
  879. copyBuffering = true;
  880. copySrcFilename = filename;
  881. console.log("Buffering " + filename + " from " + url);
  882. msgbox("Allocating memory for copy...")
  883. $("#pasteButton").addClass("disabled");
  884. $("#copyButton").addClass("disabled");
  885. // Fetch the content from the URL
  886. fetch(url).then(response => response.blob()).then(blob => {
  887. // Create a new File object from the Blob
  888. copyPendingFile = (blob);
  889. msgbox("File Ready to Paste");
  890. $("#pasteButton").removeClass("disabled");
  891. $("#copyButton").removeClass("disabled");
  892. copyBuffering = false;
  893. }).catch(error => {
  894. msgbox("Copy Allocation Failed", false);
  895. $("#pasteButton").removeClass("disabled");
  896. $("#copyButton").removeClass("disabled");
  897. copyBuffering = false;
  898. });
  899. }
  900. function fileExistsInThisFolder(filename){
  901. let exists = false;
  902. $(".fileObject").each(function(){
  903. if ($(this).attr("filename") == filename){
  904. exists = true;
  905. }
  906. });
  907. return exists;
  908. }
  909. function paste(){
  910. if (cutMode){
  911. let remainingFilesCounter = cutPendingFilepath.length;
  912. console.log("Moving " , cutPendingFilepath);
  913. cutPendingFilepath.forEach(fileToPaste => {
  914. let filename = fileToPaste.filename;
  915. let filepath = fileToPaste.filepath;
  916. $.ajax({
  917. url: "/api/fs/move?src=" + filepath + "&dest=" + currentPath + filename,
  918. method: "POST",
  919. success: function(data){
  920. if (data.error != undefined){
  921. msgbox(data.error)
  922. }else{
  923. remainingFilesCounter--;
  924. if (remainingFilesCounter == 0){
  925. msgbox("File Move Completed");
  926. refresh();
  927. }
  928. }
  929. }
  930. })
  931. });
  932. }else{
  933. //Copy and Paste
  934. if (copyBuffering){
  935. msgbox("Copy buffer allocation in progress", false);
  936. return;
  937. }
  938. let pasteFilename = copySrcFilename;
  939. let counter = 1;
  940. while(fileExistsInThisFolder(pasteFilename)){
  941. let nameOnly = copySrcFilename.split(".");
  942. let ext = nameOnly.pop();
  943. nameOnly = nameOnly.join(".");
  944. pasteFilename = nameOnly + "-" + counter + "." + ext;
  945. counter++;
  946. }
  947. //Change the name if there is name clash
  948. const file = new File([copyPendingFile], pasteFilename);
  949. //Upload the file
  950. msgbox("Writing file to SD card...");
  951. handleFile(file, currentPath, function(){
  952. msgbox("File Pasted");
  953. });
  954. }
  955. }
  956. function sortFileList(){
  957. sortFileObjects("folderList");
  958. sortFileObjects("fileList");
  959. }
  960. function sortFileObjects(listSelector) {
  961. const fileObjects = document.querySelectorAll(`#${listSelector} .fileObject`)
  962. // Convert the NodeList to an array for sorting
  963. const fileObjectsArray = Array.from(fileObjects);
  964. // Sort the elements based on their text content
  965. fileObjectsArray.sort((a, b) => {
  966. const textA = a.querySelector('.filename').textContent.toLowerCase();
  967. const textB = b.querySelector('.filename').textContent.toLowerCase();
  968. return textA.localeCompare(textB);
  969. });
  970. // Reorder the elements in the DOM
  971. const fileList = document.getElementById(listSelector);
  972. fileObjectsArray.forEach((fileObject) => {
  973. fileList.appendChild(fileObject);
  974. });
  975. }
  976. function togglePropertiesView(object){
  977. propertiesView = !propertiesView;
  978. if (propertiesView){
  979. $("#propertiesView").show();
  980. $(object).addClass('active');
  981. localStorage.setItem("file_explorer/viewProperties", "true");
  982. if ($(".fileObject.selected").length >= 1){
  983. //Load the file properties
  984. let targetFile = getFileObjectFromFID(lastClickedFileID);
  985. if (targetFile == null){
  986. targetFile = $(".fileObject.selected")[0];
  987. }
  988. let filepath = $(targetFile).attr("filepath");
  989. loadFileProperties(filepath);
  990. }
  991. }else{
  992. $("#propertiesView").hide();
  993. $(object).removeClass('active');
  994. localStorage.setItem("file_explorer/viewProperties", "false");
  995. }
  996. }
  997. // Bind the onDeleteKeyPress() function to the document's keydown event
  998. $(document).on("keydown", function(evt){
  999. if (event.ctrlKey) {
  1000. // Check for Ctrl key combinations
  1001. if (event.keyCode == 67) {
  1002. // Ctrl + C
  1003. evt.preventDefault();
  1004. copy();
  1005. } else if (event.keyCode == 86) {
  1006. // Ctrl + V
  1007. evt.preventDefault();
  1008. paste();
  1009. } else if (event.keyCode == 88) {
  1010. // Ctrl + X
  1011. evt.preventDefault();
  1012. cut();
  1013. }
  1014. } else {
  1015. if (event.keyCode == 46) {
  1016. //Delete
  1017. evt.preventDefault();
  1018. deleteFile();
  1019. }else if (event.keyCode == 13){
  1020. //Enter
  1021. evt.preventDefault();
  1022. $(".fileObject.selected").each(function(e){
  1023. openthis($(this), e);
  1024. });
  1025. }
  1026. }
  1027. });
  1028. //Share file API
  1029. function shareFile(){
  1030. if ($(".fileObject.selected").length > 1){
  1031. alert("File Share only support single file per share");
  1032. }else if ($(".fileObject.selected").length == 1){
  1033. var fileType = $(".fileObject.selected").attr("type");
  1034. if (fileType == "folder"){
  1035. alert("Folder sharing is not supported");
  1036. return;
  1037. }
  1038. //Create an share entry for this file.
  1039. var filename = $(".fileObject.selected").attr("filename");
  1040. var filepath = $(".fileObject.selected").attr("filepath");
  1041. $.post("/api/share/new?filename=" + filepath, function(data){
  1042. if (data.error != undefined){
  1043. alert(data.error);
  1044. }else{
  1045. refresh();
  1046. getFileProperties(filepath);
  1047. msgbox("File Shared");
  1048. }
  1049. });
  1050. }
  1051. }
  1052. function removeShare(){
  1053. let fileVpath = $("#fileVpath").text();
  1054. $.post("/api/share/del?filename=" + fileVpath, function(data){
  1055. if (data.error != undefined){
  1056. alert(data.error);
  1057. }else{
  1058. refresh();
  1059. getFileProperties(fileVpath);
  1060. msgbox("Share Removed");
  1061. }
  1062. });
  1063. }
  1064. </script>
  1065. </body>
  1066. </html>