diff options
Diffstat (limited to 'front/explorer.ts')
-rw-r--r-- | front/explorer.ts | 624 |
1 files changed, 624 insertions, 0 deletions
diff --git a/front/explorer.ts b/front/explorer.ts new file mode 100644 index 0000000..3a488ee --- /dev/null +++ b/front/explorer.ts @@ -0,0 +1,624 @@ +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, dragging_fileview, begin_drag_fileview, end_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); + } +} + +// 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.visuals, '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.visuals, '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.visuals, '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_fileview(e, 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_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_fileview.wnd, wnd, path_combine(wnd.get_path(), fileview.filename), dragging_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_fileview) { + move_file(dragging_fileview.wnd, wnd, wnd.get_path(), dragging_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_fileview) { + var new_folder = wnd.get_path(i + 1); + move_file(dragging_fileview.wnd, wnd, new_folder, dragging_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 <a> that we click and then delete + // That <a> 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(); +} |