fs.html 52 KB

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