import { BaseWindow } from './window' import { mk, path_combine, base64ArrayBuffer } from './util'; import { show_upload_dialog, show_upload_dialog_replace_file } from './upload_form'; import { dragging, Draggable, end_drag, begin_drag } from './dragging' import { ShareWindow } from './share_window'; import { context } from './contextmenu'; declare var actions: any; export class ExplorerWindow extends BaseWindow { foldercontents: any; filecontents: any; filecontentsroot: any; filegrid: any; save_btn_container: any; opened_file: FileView; constructor(pwd, x, y, w, h, has_close) { super(pwd, x, y, w, h); make_window(this, has_close); } openfile(is_directory) { if (is_directory) { openfile_dir(this); } else { openfile_nondir(this); } } } // A FileView is an entry inside the explorer window export class FileView { filename: string; wnd: ExplorerWindow; visuals: HTMLElement; mimetype: string; is_directory: boolean; write_permissions: boolean; constructor(filename, wnd, mimetype, is_directory, write_permissions) { this.filename = filename; this.wnd = wnd; this.visuals = null; // The DOM object with the icon and the filenam text this.mimetype = mimetype; this.is_directory = is_directory; this.write_permissions = write_permissions; } full_path() { return path_combine(this.wnd.get_path(), this.filename); } } class FileViewDraggable extends Draggable { fileview: FileView; placeholder: HTMLDivElement; constructor(fileview: FileView) { super(fileview.visuals); this.fileview = fileview; this.onBeforeDragStart = () => { this.placeholder = document.createElement('div'); fileview.visuals.parentNode.insertBefore(this.placeholder, fileview.visuals); // fileview.style.zIndex = 50000; }; this.onAfterDragEnd = () => { // If there's a dragging placeholder remove it and put the dragged node back into its place this.placeholder.parentNode.insertBefore(this.el, this.placeholder); this.placeholder.remove(); fileview.visuals.style.removeProperty("position"); fileview.visuals.style.removeProperty("width"); fileview.visuals.style.removeProperty("height"); fileview.visuals.style.removeProperty("left"); fileview.visuals.style.removeProperty("top"); } } } // make_window creates an explorer window - the kind that can list directories/open files function make_window(wnd, has_close: boolean): BaseWindow { mk(wnd.h2, 'div', 'path'); if (has_close) { var x_button = mk(wnd.h2, 'button', 'close_button'); x_button.innerText = "X"; x_button.onclick = () => { wnd.destroy(); }; } // wnd.foldercontents is where the FileViews will be stored // it also has a subheader (h3) with 'Upload' and 'New FOlder' buttons { wnd.foldercontents = mk(wnd.wc, 'div', 'foldercontents'); var h3 = mk(wnd.foldercontents, 'h3'); var upload_btn = mk(h3, 'button'); upload_btn.innerText = "Upload"; upload_btn.onclick = () => { show_upload_dialog(); } mk(h3, 'div', 'separator'); var new_folder_btn = mk(h3, 'button'); new_folder_btn.innerText = "New Folder"; new_folder_btn.onclick = () => { new_folder(wnd); } mk(h3, 'div', 'separator'); wnd.filegrid = mk(wnd.foldercontents, 'div', 'files'); } // wnd.filecontentsroot is where the filedata will be stored for open files // it also has a subheader (h3) with Share and Download buttons { wnd.filecontentsroot = mk(wnd.wc, 'div', 'filecontentsroot'); var h3 = mk(wnd.filecontentsroot, 'h3'); let download_btn = mk(h3, 'button'); download_btn.innerText = "Download"; download_btn.onclick = () => { download_file(true); } mk(h3, 'div', 'separator'); let share_btn = mk(h3, 'button'); share_btn.innerText = "Share"; share_btn.onclick = () => { alert("TODO NOT IMPLEMENTETD"); } //share(true, fileview.filename, wnd); } mk(h3, 'div', 'separator'); wnd.save_btn_container = mk(h3, 'div'); wnd.save_btn_container.style.display = 'flex'; let save_btn = mk(wnd.save_btn_container, 'button'); save_btn.innerText = "Save"; save_btn.onclick = () => save_open_text_file(wnd); mk(wnd.save_btn_container, 'div', 'separator'); wnd.filecontents = mk(wnd.filecontentsroot, 'div', 'filecontents'); } return wnd; } // Create the visuals for a FileView function add_file_visuals(fileview: FileView, wnd: ExplorerWindow) { // Are we in a subdirectory of the trash folder? var is_in_trash = wnd.pwd.length > 0 && wnd.pwd[0] == "trash"; // Is the current filewview the trash folder itself? var is_trash = wnd.pwd.length == 0 && fileview.filename == "trash"; var is_share = wnd.pwd.length == 0 && fileview.filename == "share"; var visuals = mk(wnd.filegrid, 'div'); fileview.visuals = visuals; var img = document.createElement('img'); var filename = document.createElement('div'); if (fileview.is_directory) { if (wnd.get_path() == "/" && fileview.filename == "trash") img.src = "/mimeicons/user-trash.png"; else if (wnd.get_path() == "/" && fileview.filename == "share") img.src = "/mimeicons/user-share.png"; else img.src = "/mimeicons/directory.png"; } else { img.src = `/mimeicons/${fileview.mimetype.replace("/", "-")}.png`; } fileview.visuals.onclick = () => { wnd.pwd.push(fileview.filename); if (!fileview.is_directory) { wnd.opened_file = fileview; } wnd.openfile(fileview.is_directory); } visuals.oncontextmenu = (e) => { if (!dragging) { var context_list = [ // Open is always in the context list ['Open', () => { wnd.pwd.push(fileview.filename); wnd.openfile(fileview.is_directory); }], ['Open in New Window', () => { var new_pwd = wnd.pwd.slice(); new_pwd.push(fileview.filename); var new_wnd = new ExplorerWindow(new_pwd, 100, 100, 800, 600, true); new_wnd.opened_file = fileview; new_wnd.openfile(fileview.is_directory); new_wnd.focus(); }], ]; if (is_in_trash) { // If we're in the trash, we can restore files or delete them forever context_list.push(['Restore', () => { restore_from_trash(wnd, fileview.filename); }]); context_list.push(['Delete forever', () => { delete_file(wnd, fileview.filename); }]); } else if (!is_trash && !is_share) { // If we;'re not in trash we can rename/share/download/move files to trash context_list.push( ['Rename', () => { rename_file(fileview.filename, wnd); }], ); if (!fileview.is_directory) { for (let a of actions) { if (fileview.filename.endsWith(a.extension)) { context_list.push( [a.text, () => { read_file_contents(true, (x) => { const ue = encodeURIComponent(x); let url = a.url.replace("$content_urlencoded", ue) .replace("$filename", fileview.filename); if (a.open_in_iframe) { const wnd = new BaseWindow([], 10, 10, 800, 600); var title = mk(wnd.h2, 'span', 'wndtitle'); title.innerText = fileview.filename; // Close button var x_button = mk(wnd.h2, 'button', 'close_button'); x_button.innerText = "X"; x_button.onclick = () => { wnd.destroy(); }; const contents = mk(wnd.wc, 'div', 'filecontentsroot'); const iframe = mk(contents, 'iframe') as HTMLIFrameElement; iframe.style.flex = '1 0 0'; iframe.src = url; wnd.focus(); } else { window.location = url; } }, wnd.get_path(), fileview.filename); }] ); } } if (fileview.write_permissions) { context_list.push( ['Replace', () => { replace_file(false, fileview.filename, wnd); }], ); } context_list.push( ['Share', () => { share(false, fileview.filename, wnd); }], ['Download', () => { download_file(false, fileview.filename); }], ); } context_list.push( ['Delete', () => { move_to_trash(wnd, fileview.filename); }] ); } context(e, context_list); } e.preventDefault(); e.stopPropagation(); } visuals.ondragstart = (e) => { if (is_trash || is_in_trash || is_share) { e.preventDefault(); return; } begin_drag(e, new FileViewDraggable(fileview)); e.preventDefault(); }; visuals.onmouseup = (e) => { if (dragging) { if (fileview.is_directory) { if (wnd.get_path() == "/" && fileview.filename == "trash") { // If we've dragged something onto the trashcan, it's trash move_to_trash(wnd, (dragging as FileViewDraggable).fileview.filename); } else if (wnd.get_path() == "/" && fileview.filename == "share") { // move to 'share' is invalid } else { // If we've dragged something onto a directory, move it into that directory move_file((dragging as FileViewDraggable).fileview.wnd, wnd, path_combine(wnd.get_path(), fileview.filename), (dragging as FileViewDraggable).fileview.filename); } } else { // alert(`Dropped ${dst.filename} on ${src.filename}`); } end_drag(e); } e.preventDefault(); }; visuals.classList.add('file'); filename.classList.add('filename'); if (is_in_trash) { var split = fileview.filename.split("/"); filename.innerText = split[split.length - 1]; } else if (is_trash) { filename.innerText = "Trash"; } else if (is_share) { var x = mk(filename, 'span'); x.style.fontSize = "0.8rem"; x.innerText = "Shared with me"; } else { filename.innerText = fileview.filename; } visuals.appendChild(img); visuals.appendChild(filename); } function openfile_nondir(wnd: ExplorerWindow) { while (wnd.filecontents.children.length > 0) wnd.filecontents.removeChild(wnd.filecontents.lastChild); // Send a request to readfile.php, which will give us the contents var data = new FormData(); data.append('folder', wnd.get_path(wnd.pwd.length - 1)); data.append('filename', wnd.pwd[wnd.pwd.length - 1]); var xhr = new XMLHttpRequest(); update_path_visuals(wnd); xhr.open('POST', '/php/readfile.php', true); wnd.filecontents.innerText = ""; wnd.filecontentsroot.style.display = 'flex'; wnd.foldercontents.style.display = 'none'; let is_image = wnd.opened_file.mimetype.split("/")[0] == "image"; wnd.save_btn_container.style.display = (wnd.opened_file.write_permissions && !is_image) ? "flex" : "none"; if (is_image) { xhr.responseType = 'arraybuffer'; xhr.onload = function () { let b = `data:image/png;base64,${base64ArrayBuffer(xhr.response)}`; wnd.filecontents.style.backgroundImage = `url('${b}')`; wnd.filecontents.classList.add('imgview'); } } else { wnd.filecontents.classList.remove('imgview'); wnd.filecontents.style.backgroundImage = "unset"; wnd.txt_editor = mk(wnd.filecontents, 'pre'); xhr.onload = function () { wnd.txt_editor.innerText = xhr.responseText; if (wnd.opened_file.write_permissions) wnd.txt_editor.contentEditable = "true"; }; } xhr.send(data); } // This loads the contents of the current directory function openfile_dir(wnd) { update_path_visuals(wnd); var data = new FormData(); data.append('path', wnd.get_path()); var xhr = new XMLHttpRequest(); xhr.open('POST', '/php/readdir.php', true); xhr.onload = function () { for (const f of wnd.files) f.visuals.remove(); wnd.files = []; var json = JSON.parse(xhr.responseText); if (!json) return; // Create the FileViews from the json response for (const f of json) { var view = new FileView(f.name, wnd, f.mimetype, f.is_directory && f.is_directory != "0", f.can_edit && f.can_edit != "0"); wnd.files.push(view); } // Sort the files nicely before adding their visuals // Folders come first, then files, then the special trash directory // Everything inside the categories is lexically sorted wnd.files.sort((a, b) => { if (wnd.pwd.length == 0 && a.filename == "share") return -10; if (wnd.pwd.length == 0 && b.filename == "share") return 10; if (wnd.pwd.length == 0 && a.filename == "trash") return 10; if (wnd.pwd.length == 0 && b.filename == "trash") return -10; if (a.is_directory && !b.is_directory) return -1; if (!a.is_directory && b.is_directory) return 1; return a.filename.localeCompare(b.filename); }); for (const f of wnd.files) { add_file_visuals(f, wnd); } }; xhr.send(data); wnd.filecontentsroot.style.display = 'none'; wnd.foldercontents.style.display = 'flex'; wnd.foldercontents.onmouseup = () => { if (dragging && dragging instanceof FileViewDraggable) { move_file((dragging as FileViewDraggable).fileview.wnd, wnd, wnd.get_path(), (dragging as FileViewDraggable).fileview.filename); } } } function move_to_trash(wnd, filename) { move_file(wnd, wnd, "/trash", filename, path_combine(wnd.get_path(), filename)); } function restore_from_trash(wnd, filename) { var split = filename.split("/"); var new_filename = split.pop(); var new_directory = "/" + split.join("/"); move_file(wnd, wnd, new_directory, filename, new_filename); } // This deletes the file, *for real* // move_to_trash is what is actually called when the user clicks 'Delete' function delete_file(wnd, filename) { var data = new FormData(); data.append('folder', wnd.get_path()); data.append('filename', filename); var xhr = new XMLHttpRequest(); xhr.open('POST', '/php/delete.php', true); xhr.onload = function () { wnd.openfile(true); }; xhr.send(data); } function rename_file(filename, wnd) { var new_name = prompt(`Rename ${filename} to`, filename); if (!new_name) return; var data = new FormData(); data.append('folder', wnd.get_path()); data.append('old_filename', filename); data.append('new_filename', new_name); var xhr = new XMLHttpRequest(); xhr.open('POST', '/php/rename.php', true); xhr.onload = function () { wnd.openfile(true); }; xhr.send(data); } function move_file(srcwnd: ExplorerWindow, dstwnd: ExplorerWindow, new_folder: string, filename: string, new_filename?: string) { if (!new_filename) new_filename = filename; var data = new FormData(); data.append('old_folder', srcwnd.get_path()); data.append('new_folder', new_folder); data.append('filename', filename); data.append('new_filename',new_filename); var xhr = new XMLHttpRequest(); xhr.open('POST', '/php/move.php', true); xhr.onload = () => { srcwnd.openfile(true); dstwnd.openfile(true); }; xhr.send(data); } function new_folder(wnd) { var dirname = prompt(`Directory name`, "New Folder"); if (!dirname) return; var data = new FormData(); data.append('parent_directory', wnd.get_path()); data.append('dirname', dirname); var xhr = new XMLHttpRequest(); xhr.open('POST', '/php/mkdir.php', true); xhr.onload = function () { wnd.openfile(true); }; xhr.send(data); } // Replace an existing file with a new one function replace_file(in_file, filename, wnd) { if (in_file) { var folder = wnd.get_path(wnd.pwd.length - 1); filename = wnd.pwd[wnd.pwd.length - 1]; } else { var folder = wnd.get_path(); } show_upload_dialog_replace_file(folder, filename); } // This updates the path of the window's DOM (the "Root > Folder1 > Folder2 > foo.png") function update_path_visuals(wnd) { var the_path = wnd.visuals.getElementsByClassName('path')[0]; // Remove the old path while (the_path.children.length > 0) the_path.removeChild(the_path.lastChild); for (let i = -1; i < wnd.pwd.length; i++) { var d; // For each element after the first create a separator if (i >= 0) { d = wnd.pwd[i]; var separator_div = mk(the_path, 'div', 'separator'); separator_div.innerText = "ยป"; } else d = "Root"; var entry = mk(the_path, 'button', 'pathentry'); entry.innerText = d; // When we click the entry, go to its folder entry.onclick = (_e) => { if (length < wnd.pwd.length) { wnd.pwd.length = i + 1; openfile_dir(wnd); } } // We can drop files onto the path, which will omve them to teh folder entry.onmouseup = (e) => { if (dragging && dragging instanceof FileViewDraggable) { var new_folder = wnd.get_path(i + 1); move_file((dragging as FileViewDraggable).fileview.wnd, wnd, new_folder, (dragging as FileViewDraggable).fileview.filename); end_drag(e); e.preventDefault(); e.stopPropagation(); } } } } function download_file(in_file, filename?: any, wnd?: any) { if (in_file) { var folder = wnd.get_path(wnd.pwd.length - 1); filename = wnd.pwd[wnd.pwd.length - 1]; } else { var folder = wnd.get_path(); } // Read the file contents and then do DISGUSTING javascript things to download the ifle // We create a invisible that we click and then delete // That has its download attribute set so we download the contents instead of opening it in a new tab // and of course its href is a virtual object URL that has its content set to a blob read_file_contents(false, (x) => { var blob = new Blob([new Uint8Array(x, 0, x.length)]); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }); }, folder, filename); return; } function save_open_text_file(wnd) { const contents = wnd.txt_editor.innerText; let xhr = new XMLHttpRequest(); xhr.open('POST', '/php/upload.php', true); var data = new FormData(); data.append('parent_directory', wnd.get_path(wnd.pwd.length - 1)); data.append('filename', wnd.pwd[wnd.pwd.length - 1]); data.append('content', contents); data.append('overwrite', '1'); xhr.send(data); } // This asks the server for the contents of the specified file // The 'cb' callback is then called, which gives you the file as either text or binary // depending on whether or not text is true/false function read_file_contents(text, cb, folder, filename) { var data = new FormData(); data.append('folder', folder); data.append('filename', filename); let xhr = new XMLHttpRequest(); xhr.open('POST', '/php/readfile.php', true); if (text) { xhr.onload = function () { cb(xhr.responseText); }; } else { xhr.responseType = 'arraybuffer'; xhr.onload = function () { cb(xhr.response); }; } xhr.send(data); } // This opens a file. // If the file has image/* mimetype, it will be displayed as an image // otherwise it will be displayed as plaintext // This is a tiny wrapper around the share_window. function share(in_file: boolean, filename: string, wnd: BaseWindow) { if (in_file) { var folder = wnd.get_path(wnd.pwd.length - 1); filename = wnd.pwd[wnd.pwd.length - 1]; } else { var folder = wnd.get_path(); } var sharewnd = new ShareWindow(folder, filename, 400, 400, 400); sharewnd.focus(); }