fs.html 55 KB

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