From dd6cf660b033251c5ab08fa0c3e32a44022a7039 Mon Sep 17 00:00:00 2001 From: Alex Vitkov Date: Fri, 19 Mar 2021 21:58:50 +0200 Subject: Added evil microsoft typescript --- .gitignore | 1 + build/bundle.js | 974 ++++++++++++++++++++ css/sharefile_style.css | 25 - css/style.css | 513 ----------- documentation/web_project_documentataion.docx | Bin 24509 -> 0 bytes front/loggedin.ts | 1195 +++++++++++++++++++++++++ img/arrow.svg | 62 ++ img/bottom.svg | 62 ++ img/icons/.txt.svg | 65 ++ index.php | 4 +- loggedin.js | 1175 ------------------------ loggedin.php | 2 +- loginregister.php | 2 +- package-lock.json | 14 + package.json | 23 + php/file_type_recogniser.php | 2 +- share_frontend.php | 4 +- style.css | 513 +++++++++++ svg/arrow.svg | 62 -- svg/bottom.svg | 62 -- svg/icons/.txt.svg | 65 -- tsconfig.json | 6 + 22 files changed, 2922 insertions(+), 1909 deletions(-) create mode 100644 build/bundle.js delete mode 100644 css/sharefile_style.css delete mode 100644 css/style.css delete mode 100644 documentation/web_project_documentataion.docx create mode 100644 front/loggedin.ts create mode 100644 img/arrow.svg create mode 100644 img/bottom.svg create mode 100644 img/icons/.txt.svg delete mode 100644 loggedin.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 style.css delete mode 100644 svg/arrow.svg delete mode 100644 svg/bottom.svg delete mode 100644 svg/icons/.txt.svg create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 18e5cb4..b610a84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ screen files +node_modules php/custom_configuration.php diff --git a/build/bundle.js b/build/bundle.js new file mode 100644 index 0000000..c65d1de --- /dev/null +++ b/build/bundle.js @@ -0,0 +1,974 @@ +// This should be only set to false for debugging purposes +// If it's set to false, the upload requests will be synchronous +// and you will be able to see PHP's echo output in the browser +var FORM_ASYNC = true; +// A FileView is an entry inside the explorer window +var FileView = /** @class */ (function () { + function FileView(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; + } + FileView.prototype.full_path = function () { + return path_combine(get_path(this.wnd), this.filename); + }; + return FileView; +}()); +// An array of all fileviews currently open +var OurWindow = /** @class */ (function () { + function OurWindow(pwd) { + this.pwd = pwd; // pwd = [ "Folder1", "Folder2" ] means the current directory of that window is /Folder1/Folder2 + this.visuals = null; // The DOM object + this.h2 = null; // The titlebar of the window + this.fileview = null; + this.files = []; + this.txt_editor = null; // For editable text files, this is the DOM element the user can edit + } + return OurWindow; +}()); +// An array with all the windows on the screen +var windows = []; +// The focused window +var focused_window = null; +// Those all belong to the hidden file upload form +var upload_form = document.getElementById("upload_form"); +var filename_input = document.getElementById("filename"); +var override_input = document.getElementById("override_input"); +var upload_parent_directory = document.getElementById("upload_parent_directory"); +var the_file = document.getElementById("the_file"); +// If this is set to true, requests to uploads.php will be sent with the "override" flag +// which will override existing files with the same name +var override_file = false; +var override_file_filename = ""; +var override_file_path = ""; +var open_file = null; +// Some elements have custom right click context menus +// If there's a custom context menu active, this will be it +var context_menu = null; +// If we're currently dragging something (a window or a file), this is the DOM object we're dragging +// dragging_offset_x and dragging_offset_y are the difference between the objec's top left point and the cursor position +// this is then added to the cursor position when the mouse is moved +var dragging = null; +var dragging_offset_x = 0, dragging_offset_y = 0; +// If we have pressed down a window's title bar but haven't yet started dragging it, it's the drag candidate +// This is needed because we don't yet know whether we want to start dragging the window or click an element in the titlebar +// Once the mouse has moved sufficiently far away from dragging_candidate_x or y, we start dragging +var dragging_candidate = null; +var dragging_candidate_x, dragging_candidate_y; +// If we're dragging a fileview, this is set to the fileview class instance itself, because 'dragging' is just a DOM object +// The placeholder is a dummy DIV that we insert in the object's place on the grid to keep things nicely aligned +var dragging_fileview; +var dragging_placeholder = null; +// Windows have a z-index. When we click a window it is sent to the top this will be its new z-index +// We then increment the depth, and the next window we click will go on top of the current one +var depth = 20; +function main() { + // Create a window that looks at the root directory + var root_window = make_window([], false); + // Focus that window and load the directory + focus_window(root_window); + openfile(true, root_window); +} +function focus_window(wnd) { + // Unfocus the old window + if (focused_window) + focused_window.visuals.classList.remove('focus'); + focused_window = wnd; + // And focus the new one! + if (wnd) { + wnd.visuals.classList.add('focus'); + wnd.visuals.style.zIndex = (depth++).toString(); + } +} +// Delete the focused window +function delete_window(wnd) { + var index = windows.indexOf(wnd); + if (index >= 0) + windows.splice(index, 1); + wnd.visuals.parentNode.removeChild(wnd.visuals); + if (wnd == focused_window) + focused_window = null; +} +// Create a right click context menu +function context(e, entries) { + if (context_menu) + context_menu.remove(); + context_menu = mk(document.body, 'ul', 'context'); + context_menu.onmousedown = function (e) { + e.stopPropagation(); + }; + context_menu.onclick = function (_e) { + context_menu.remove(); + context_menu = null; + }; + context_menu.style.left = e.clientX + "px"; + context_menu.style.top = e.clientY + "px"; + for (var _i = 0, entries_1 = entries; _i < entries_1.length; _i++) { + var e_1 = entries_1[_i]; + var li = document.createElement('li'); + li.innerText = e_1[0]; + li.onclick = e_1[1]; + context_menu.appendChild(li); + } +} +// This is called whenever the 's value changes +function on_file_added(_e) { + if (the_file.files.length >= 1) { + if (override_file) { + filename_input.value = override_file_filename; + override_input.value = "1"; + upload_parent_directory.value = override_file_path; + console.log(filename_input.value, override_input.value, upload_parent_directory.value); + } + else { + filename_input.value = the_file.files[0].name; + override_input.value = "0"; + upload_parent_directory.value = get_path(focused_window); + } + if (!FORM_ASYNC) { + upload_form.submit(); + return; + } + // Send the form asynchronously through the fetch api + fetch(upload_form.action, { + method: upload_form.method, + body: new FormData(upload_form) + }).then(function (resp) { + if (resp.status == 200) { + // Reload the directory so the user can see the newly uploaded file + openfile(true, focused_window); + } + else { + alert("Upload failed"); + } + }, function () { + alert("Upload failed"); + }); + } + else { + alert("No files selected"); + } +} +// It's honestly really sad that we need this +// We have an image viewer, but we load the uploaded via the XMLHttpRequest API, which gives us an array buffer +// We need to base64 encode the image data so we can feed it into the +// and the standart base64 encode API is shit +// https://stackoverflow.com/questions/7370943/retrieving-binary-file-content-using-javascript-base64-encode-it-and-reverse-de +function base64ArrayBuffer(arrayBuffer) { + var base64 = ''; + var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + var bytes = new Uint8Array(arrayBuffer); + var byteLength = bytes.byteLength; + var byteRemainder = byteLength % 3; + var mainLength = byteLength - byteRemainder; + var a, b, c, d; + var chunk; + // Main loop deals with bytes in chunks of 3 + for (var i = 0; i < mainLength; i = i + 3) { + // Combine the three bytes into a single integer + chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; + // Use bitmasks to extract 6-bit segments from the triplet + a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18 + b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12 + c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 + d = chunk & 63; // 63 = 2^6 - 1 + // Convert the raw binary segments to the appropriate ASCII encoding + base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]; + } + // Deal with the remaining bytes and padding + if (byteRemainder == 1) { + chunk = bytes[mainLength]; + a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 + // Set the 4 least significant bits to zero + b = (chunk & 3) << 4; // 3 = 2^2 - 1 + base64 += encodings[a] + encodings[b] + '=='; + } + else if (byteRemainder == 2) { + chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]; + a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10 + b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 + // Set the 2 least significant bits to zero + c = (chunk & 15) << 2; // 15 = 2^4 - 1 + base64 += encodings[a] + encodings[b] + encodings[c] + '='; + } + return base64; +} +// This updates the path of the window's DOM (the "Root > Folder1 > Folder2 > foo.png") +function update_path_visuals(wnd) { + if (!wnd) { + alert("YOU ARE NOT SUPPOSED TO SEE THIS. PLEASE COPY THE STACKTRACE FROM THE CONSOLE AND SEND IT TO THE NEAREST DEVELOPER"); + wnd = focused_window; + } + var the_path = wnd.visuals.getElementsByClassName('path')[0]; + // Remove the old path + while (the_path.children.length > 0) + the_path.removeChild(the_path.lastChild); + var _loop_1 = function (i) { + // For each element after the first create a separator + if (i >= 0) { + d = wnd.pwd[i]; + separator_div = mk(the_path, 'div', 'separator'); + separator_div.innerText = "»"; + } + else + d = "Root"; + entry = mk(the_path, 'button', 'pathentry'); + entry.innerText = d; + // When we click the entry, go to its folder + entry.onclick = function (_e) { + if (length < wnd.pwd.length) { + wnd.pwd.length = i + 1; + openfile(true, wnd); + } + }; + // We can drop files onto the path, which will omve them to teh folder + entry.onmouseup = function (e) { + if (dragging && dragging_fileview) { + var new_folder = get_path(wnd, i + 1); + move_file(dragging_fileview.wnd, wnd, new_folder, dragging_fileview.filename); + end_drag(e); + e.preventDefault(); + e.stopPropagation(); + } + }; + }; + var d, separator_div, entry; + for (var i = -1; i < wnd.pwd.length; i++) { + _loop_1(i); + } +} +// 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); + var 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 +function openfile_nondir(wnd) { + 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', get_path(wnd, 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'; + var is_image = open_file.mimetype.split("/")[0] == "image"; + wnd.save_btn_container.style.display = (open_file.write_permissions && !is_image) ? "flex" : "none"; + if (is_image) { + xhr.responseType = 'arraybuffer'; + xhr.onload = function () { + var 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 (open_file.write_permissions) + wnd.txt_editor.contentEditable = "true"; + }; + } + xhr.send(data); +} +// This is a tiny wrapper around the share_window. +function share(in_file, filename, wnd) { + if (in_file) { + var folder = get_path(wnd, wnd.pwd.length - 1); + filename = wnd.pwd[wnd.pwd.length - 1]; + } + else { + var folder = get_path(wnd); + } + var wnd = make_share_window(folder, filename); + focus_window(wnd); +} +// Replace an existing file with a new one +function replace_file(in_file, filename, wnd) { + if (in_file) { + var folder = get_path(wnd, wnd.pwd.length - 1); + filename = wnd.pwd[wnd.pwd.length - 1]; + } + else { + var folder = get_path(wnd); + } + override_file = true; + override_file_path = folder; + override_file_filename = filename; + the_file.click(); +} +// This loads the contents of the current directory +function openfile_dir(wnd) { + update_path_visuals(wnd); + var data = new FormData(); + data.append('path', get_path(wnd)); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/php/readdir.php', true); + xhr.onload = function () { + for (var _i = 0, _a = wnd.files; _i < _a.length; _i++) { + var f = _a[_i]; + f.visuals.remove(); + } + wnd.files = []; + var json = JSON.parse(xhr.responseText); + if (!json) + return; + // Create the FileViews from the json response + for (var _b = 0, json_1 = json; _b < json_1.length; _b++) { + var f = json_1[_b]; + 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(function (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 (var _c = 0, _d = wnd.files; _c < _d.length; _c++) { + var f = _d[_c]; + add_file_visuals(f, wnd); + } + }; + xhr.send(data); + wnd.filecontentsroot.style.display = 'none'; + wnd.foldercontents.style.display = 'flex'; + wnd.foldercontents.onmouseup = function () { + if (dragging && dragging_fileview) { + move_file(dragging_fileview.wnd, wnd, get_path(wnd), dragging_fileview.filename); + } + }; +} +function openfile(is_directory, wnd) { + if (!wnd) { + alert("YOU ARE NOT SUPPOSED TO SEE THIS. PLEASE COPY THE STACKTRACE FROM THE CONSOLE AND SEND IT TO THE NEAREST DEVELOPER"); + console.trace(); + wnd = focused_window; + } + if (is_directory) { + openfile_dir(wnd); + } + else { + openfile_nondir(wnd); + } +} +function move_to_trash(wnd, filename) { + move_file(wnd, wnd, "/trash", filename, path_combine(get_path(wnd), 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', get_path(wnd)); + data.append('filename', filename); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/php/delete.php', true); + xhr.onload = function () { + openfile(true, wnd); + }; + 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', get_path(wnd)); + 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 () { + openfile(true, wnd); + }; + xhr.send(data); +} +function move_file(srcwnd, dstwnd, new_folder, filename, new_filename) { + if (!new_filename) + new_filename = filename; + var data = new FormData(); + data.append('old_folder', get_path(srcwnd)); + 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 = function () { + openfile(true, srcwnd); + openfile(true, dstwnd); + }; + 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', get_path(wnd)); + data.append('dirname', dirname); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/php/mkdir.php', true); + xhr.onload = function () { + openfile(true, wnd); + }; + xhr.send(data); +} +// Dragging a fileview is a bit different from dragging a window +// This does some setup work before calling the common begin_drag +function begin_drag_fileview(e, fileview) { + if (dragging) + end_drag(e); + // The dragging_placeholder is inserted into its place by the begin_drag function + dragging_placeholder = document.createElement('div'); + dragging_fileview = fileview; + dragging = fileview.visuals; + dragging.style.zIndex = 50000; + begin_drag(e, fileview.visuals); +} +// Start dragging the 'obj' DOM element +// e is a DOM event, this should only get called in response of a DOM event +function begin_drag(e, obj, dont_set_width) { + set_iframe_enabled(false); + dragging = obj; + dragging_candidate = null; + dragging.classList.add("dragged"); + var elemRect = dragging.getBoundingClientRect(); + dragging_offset_x = e.clientX - elemRect.left; + dragging_offset_y = -e.clientY + elemRect.top; + if (dragging_placeholder) + obj.parentNode.insertBefore(dragging_placeholder, obj); + dragging.style.left = (e.clientX - dragging_offset_x) + "px"; + dragging.style.top = (e.clientY + dragging_offset_y) + "px"; + if (!dont_set_width) { + dragging.style.width = elemRect.width + "px"; + dragging.style.height = elemRect.height + "px"; + } + dragging.style.position = "absolute"; + document.body.appendChild(dragging); +} +function end_drag(_e) { + set_iframe_enabled(true); + // If there's a dragging placeholder remove it and put the dragged node back into its place + if (dragging_placeholder) { + dragging_placeholder.parentNode.insertBefore(dragging, dragging_placeholder); + dragging_placeholder.remove(); + dragging_placeholder = null; + } + // If we were dragging a FileView, we need to reset some CSS + if (dragging_fileview) { + dragging.style.removeProperty("position"); + dragging.style.removeProperty("width"); + dragging.style.removeProperty("height"); + dragging.style.removeProperty("left"); + dragging.style.removeProperty("top"); + dragging_fileview = null; + } + dragging.classList.remove("dragged"); + dragging = null; +} +// This creates the parts of a window that are common between all window types +// This should only really be called by another function that will then fill up the window +function make_window_base(pwd, x, y, w, h) { + var wnd = new OurWindow(pwd); + windows.push(wnd); + wnd.visuals = mk(document.body, 'div', 'window'); + wnd.visuals.style.width = w + "px"; + wnd.visuals.style.height = h ? (h + "px") : "unset"; + wnd.visuals.style.position = "absolute"; + wnd.visuals.style.left = x + "px"; + wnd.visuals.style.top = y + "px"; + wnd.h2 = mk(wnd.visuals, 'h2'); + wnd.visuals.onmousedown = function (_e) { + focus_window(wnd); + }; + wnd.h2.onmousedown = function (e) { + if (!dragging) { + dragging_candidate = wnd.visuals; + dragging_candidate_x = e.clientX; + dragging_candidate_y = e.clientY; + } + }; + return wnd; +} +// This is a widely abused helper function that creates a DOM element, attaches it as the +// last child of 'parent' and possibly gives it a class +function mk(parent, type, _class) { + var el = document.createElement(type); + parent.appendChild(el); + if (_class) + el.classList.add(_class); + return el; +} +// Crate a horizontal div +function mkhdiv(parent) { + var hdiv = mk(parent, 'div'); + hdiv.style.display = "flex"; + hdiv.style.alignItems = "center"; + hdiv.style.padding = "0.3rem"; + hdiv.style.gap = "0.3rem"; + return hdiv; +} +// Create a checkbocx with a label. +// togglefn will be called when its value changes with an argument that's either true/false +function mkcheckbox(parent, label, togglefn) { + var hdiv = mkhdiv(parent); + var write_checkbox = mk(hdiv, 'input'); + write_checkbox.type = 'checkbox'; + var write_checkbox_label = mk(hdiv, 'label'); + write_checkbox_label.innerText = label; + write_checkbox_label.onclick = function (_e) { write_checkbox.click(); }; + write_checkbox_label.classList.add('noselect'); + write_checkbox.onchange = function (_e) { + togglefn(write_checkbox.checked); + }; +} +// This monstrocity creates the 'Share file' window +function make_share_window(folder, filename) { + var wnd = make_window_base(null, 400, 400, 400, 0); + wnd.h2.style.display = 'flex'; + // The title of the window. WE set its 'flex' to 1 1 0 so it fills up the titlebar + // and pushes the X button to the very right + var heading = mk(wnd.h2, 'span', 'wndtitle'); + heading.innerText = "Share " + filename; + // Close button + var x_button = mk(wnd.h2, 'button', 'close_button'); + x_button.innerText = "X"; + x_button.onclick = function () { return delete_window(wnd); }; + wnd.foldercontents = mk(wnd.visuals, 'div', 'share_dialog_contents'); + wnd.foldercontents.style.padding = "0.5rem"; + // This is the data that will be sent when we hit "Generate link" + var data = { + write_permissions: false, + private: false, + has_password: false, + password: "", + userlist: [] + }; + // If private link is clicked, show the "Add user" button and the user list + var userlist, add_user; + mkcheckbox(wnd.foldercontents, "Private link", function (toggled) { + add_user.style.display = toggled ? "block" : "none"; + userlist.style.display = toggled ? "block" : "none"; + data.private = toggled; + }); + userlist = mk(wnd.foldercontents, 'div'); + userlist.style.display = "none"; + add_user = mk(wnd.foldercontents, 'button'); + add_user.innerText = "Add user"; + add_user.style.display = "none"; + // When we hit 'Add user', add an input field for a new user + add_user.onclick = function (_e) { + var i = mk(userlist, 'input'); + i.value = 'John Doe'; + var index = data.userlist.length; + data.userlist.push(i.value); + i.onchange = function (_e) { + data.userlist[index] = i.value; + }; + }; + // Click the add_user once to add a default user, since a URL that nobody can use makes no sense + add_user.click(); + mkcheckbox(wnd.foldercontents, "Give write permissions", function (toggled) { + data.write_permissions = toggled; + }); + // If 'Password protected' is checked, show the password field + var password_container; + mkcheckbox(wnd.foldercontents, "Password protected", function (toggled) { + data.has_password = toggled; + password_container.style.display = toggled ? "flex" : "none"; + }); + password_container = mkhdiv(wnd.foldercontents); + password_container.style.display = 'none'; + var password_label = mk(password_container, 'label'); + password_label.innerText = "Password"; + var password_input = mk(password_container, 'input'); + password_input.type = 'password'; + password_input.autocomplete = 'off'; + password_input.style.flex = "1 0 0"; + password_input.onchange = function (_e) { + data.password = password_input.value; + }; + var generate_url_button = mk(wnd.foldercontents, 'button'); + generate_url_button.innerText = "Generate link"; + generate_url_button.onclick = function () { + // The backend expects the users to be either an empty string, if the URL is public + // or a comma separated list of usernaems + var users = ""; + if (data.private) { + users = data.userlist.join(','); + } + var form_data = new FormData(); + form_data.append('folder', folder); + form_data.append('filename', filename); + form_data.append('users', users); + // 0 = No permissions, 1 = Read only, 2 = Write , 1|2 = 3 = RW + // Only 1 and 3 make sense in the context of a URL + form_data.append('permissions', (data.write_permissions ? 3 : 1).toString()); + form_data.append('password', data.has_password ? data.password : ""); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/php/share.php', true); + xhr.onload = function () { + alert(xhr.response); + }; + xhr.send(form_data); + delete_window(wnd); + }; + return wnd; +} +function download_file(in_file, filename, wnd) { + if (!wnd) { + alert(802); + wnd = focused_window; + } + if (in_file) { + var folder = get_path(wnd, wnd.pwd.length - 1); + filename = wnd.pwd[wnd.pwd.length - 1]; + } + else { + var folder = get_path(wnd); + } + // 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, function (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(function () { + document.body.removeChild(a); + URL.revokeObjectURL(url); + }); + }, folder, filename); + return; +} +// make_window creates an explorer window - the kind that can list directories/open files +function make_window(pwd, has_close) { + var wnd = make_window_base(pwd, 100, 100, 800, 600); + mk(wnd.h2, 'div', 'path'); + if (has_close) { + var x_button = mk(wnd.h2, 'button', 'close_button'); + x_button.innerText = "X"; + x_button.onclick = function () { return delete_window(wnd); }; + } + // 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 = function () { + override_file = false; + the_file.click(); + }; + mk(h3, 'div', 'separator'); + var new_folder_btn = mk(h3, 'button'); + new_folder_btn.innerText = "New Folder"; + new_folder_btn.onclick = function () { 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'); + var download_btn = mk(h3, 'button'); + download_btn.innerText = "Download"; + download_btn.onclick = function () { download_file(true); }; + mk(h3, 'div', 'separator'); + var share_btn = mk(h3, 'button'); + share_btn.innerText = "Share"; + share_btn.onclick = function () { 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'; + var save_btn = mk(wnd.save_btn_container, 'button'); + save_btn.innerText = "Save"; + save_btn.onclick = function () { return save_open_text_file(wnd); }; + mk(wnd.save_btn_container, 'div', 'separator'); + wnd.filecontents = mk(wnd.filecontentsroot, 'div', 'filecontents'); + } + return wnd; +} +function save_open_text_file(wnd) { + var contents = wnd.txt_editor.innerText; + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/php/upload.php', true); + var data = new FormData(); + data.append('parent_directory', get_path(wnd, wnd.pwd.length - 1)); + data.append('filename', wnd.pwd[wnd.pwd.length - 1]); + data.append('content', contents); + data.append('overwrite', '1'); + xhr.send(data); +} +// Create the visuals for a FileView +function add_file_visuals(fileview, wnd) { + // 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 (get_path(wnd) == "/" && fileview.filename == "trash") + img.src = "/mimeicons/user-trash.png"; + else if (get_path(wnd) == "/" && 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 = function () { + wnd.pwd.push(fileview.filename); + if (!fileview.is_directory) { + open_file = fileview; + } + openfile(fileview.is_directory, wnd); + }; + visuals.oncontextmenu = function (e) { + if (!dragging) { + var context_list = [ + // Open is always in the context list + ['Open', function () { + wnd.pwd.push(fileview.filename); + openfile(fileview.is_directory, wnd); + }], + ['Open in New Window', function () { + var new_pwd = wnd.pwd.slice(); + new_pwd.push(fileview.filename); + var new_wnd = make_window(new_pwd, true); + open_file = fileview; + openfile(fileview.is_directory, new_wnd); + focus_window(new_wnd); + }], + ]; + if (is_in_trash) { + // If we're in the trash, we can restore files or delete them forever + context_list.push(['Restore', function () { restore_from_trash(wnd, fileview.filename); }]); + context_list.push(['Delete forever', function () { 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', function () { rename_file(fileview.filename, wnd); }]); + if (!fileview.is_directory) { + var _loop_2 = function (a) { + if (fileview.filename.endsWith(a.extension)) { + context_list.push([a.text, function () { + read_file_contents(true, function (x) { + var ue = encodeURIComponent(x); + var url = a.url.replace("$content_urlencoded", ue) + .replace("$filename", fileview.filename); + if (a.open_in_iframe) { + var wnd_1 = make_window_base([], 10, 10, 800, 600); + var title = mk(wnd_1.h2, 'span', 'wndtitle'); + title.innerText = fileview.filename; + // Close button + var x_button = mk(wnd_1.h2, 'button', 'close_button'); + x_button.innerText = "X"; + x_button.onclick = function () { return delete_window(wnd_1); }; + var contents = mk(wnd_1.visuals, 'div', 'filecontentsroot'); + var iframe = mk(contents, 'iframe'); + iframe.style.flex = '1 0 0'; + iframe.src = url; + focus_window(wnd_1); + } + else { + window.location = url; + } + }, get_path(wnd), fileview.filename); + }]); + } + }; + for (var _i = 0, actions_1 = actions; _i < actions_1.length; _i++) { + var a = actions_1[_i]; + _loop_2(a); + } + if (fileview.write_permissions) { + context_list.push(['Replace', function () { replace_file(false, fileview.filename, wnd); }]); + } + context_list.push(['Share', function () { share(false, fileview.filename, wnd); }], ['Download', function () { download_file(false, fileview.filename); }]); + } + context_list.push(['Delete', function () { move_to_trash(wnd, fileview.filename); }]); + } + context(e, context_list); + } + e.preventDefault(); + e.stopPropagation(); + }; + visuals.ondragstart = function (e) { + if (is_trash || is_in_trash || is_share) { + e.preventDefault(); + return; + } + begin_drag_fileview(e, fileview); + e.preventDefault(); + }; + visuals.onmouseup = function (e) { + if (dragging) { + if (fileview.is_directory) { + if (get_path(wnd) == "/" && fileview.filename == "trash") { + // If we've dragged something onto the trashcan, it's trash + move_to_trash(wnd, dragging_fileview.filename); + } + else if (get_path(wnd) == "/" && 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(get_path(wnd), 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); +} +// Reads the 'pwd' of the focused window +// If pwd is ['foo', 'bar', 'baz'], this returns '/foo/bar/baz' +function get_path(wnd, max_length) { + if (!wnd) { + alert("YOU ARE NOT SUPPOSED TO SEE THIS. PLEASE COPY THE STACKTRACE FROM THE CONSOLE AND SEND IT TO THE NEAREST DEVELOPER"); + console.trace(); + wnd = focused_window; + } + if (max_length == undefined) { + max_length = wnd.pwd.length; + } + var path = "/"; + for (var i = 0; i < max_length; i++) { + path += wnd.pwd[i]; + if (i != max_length - 1) + path += "/"; + } + return path; +} +function path_combine(a, b) { + var last_char = a.slice(-1); + if (last_char == "/") + return a + b; + else + return a + "/" + b; +} +// When we click anywhere, remove the context menu +// The context menu itself has a onmousedown that prevents propagation so we can click its elements +document.body.onmousedown = function (_e) { + if (context_menu) { + context_menu.remove(); + context_menu = null; + } +}; +document.body.onmousemove = function (e) { + if (dragging) { + dragging.style.left = (e.clientX - dragging_offset_x) + "px"; + dragging.style.top = (e.clientY + dragging_offset_y) + "px"; + } + else if (dragging_candidate) { + var d = Math.abs(e.clientX - dragging_candidate_x) + Math.abs(e.clientY - dragging_candidate_y); + if (d > 15) + begin_drag(e, dragging_candidate, true); + } +}; +document.body.onmouseup = function (_e) { + if (dragging_candidate) + dragging_candidate = null; + if (dragging) + end_drag(_e); +}; +document.body.oncontextmenu = function (e) { + if (dragging) { + end_drag(e); + e.preventDefault(); + } + if (context_menu) { + context_menu.remove(); + context_menu = null; + } +}; +the_file.onchange = function (e) { on_file_added(e); }; +function set_iframe_enabled(en) { + for (var _i = 0, _a = document.getElementsByTagName('iframe'); _i < _a.length; _i++) { + var iframe = _a[_i]; + iframe.hidden = !en; + } +} +main(); diff --git a/css/sharefile_style.css b/css/sharefile_style.css deleted file mode 100644 index f7118fc..0000000 --- a/css/sharefile_style.css +++ /dev/null @@ -1,25 +0,0 @@ -body { - display: flex; - align-items: center; - justify-content: center; -} - -.content { - flex: 0 0 0; - display: flex; - flex-direction: column; - gap: 1rem; - align-items: center; - justify-content: center; -} - -.h { - display: flex; - gap: 0.8rem; - align-items: center; - justify-content: center; -} - -input[type=submit] { - margin-top: 0.5rem; -} diff --git a/css/style.css b/css/style.css deleted file mode 100644 index a7a1762..0000000 --- a/css/style.css +++ /dev/null @@ -1,513 +0,0 @@ - -html, body { - margin: 0; - height: 100%; -} - -body { - background: #f0f0f0; - color: black; - font-family: Roboto, sans-serif; - overflow: hidden; -} - -.noselect { - user-select: none; - -webkit-user-select: none; - -ms-user-select: none; -} - -#page { - display: flex; - flex: 1 0 0; - align-items: stretch; - - user-select: none; - -webkit-user-select: none; - -ms-user-select: none; -} - -#page > * { - flex: 1 0 0; -} - -/* Disable the black outline that Chromium browsers put on focused buttons */ -input:focus, -button:focus -{ - outline:0; -} - -#header { - background: white; - margin: 0; - padding: 0; - height: 3rem; - font-size: 2em; - display: flex; - align-items: stretch; - user-select: none; - -webkit-user-select: none; - -ms-user-select: none; -} - -#topmenu { - font-size: 1.3rem; - list-style-type: none; - display: flex; - margin: 0; - padding: 0; -} - -#topmenu > li { - cursor: pointer; - padding: 10px; -} - -#topmenu > li:hover { - background: #eee; -} - -.logo { - font-family: monospace; - margin: 0; - align-self: center; - padding-left: 2rem; -} - -#hero { - flex: 1.5 0 0; - position: relative; - font-size: 3.5rem; -} - -.vcenter { - display: flex; - flex-direction: column; - justify-content: center; -} - -#hero .bg { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: -5; -} - -#hero > * > p { - color: #676d75; - margin: 0rem; - padding: 0.5rem; - white-space: nowrap; - text-align: center; -} - -#hero .big { - font-size: 1.2em; -} - -.blue { - color: #231179; - font-weight: bold; -} - -form { - background: #fbfbfb; - margin: 4.5rem; - padding-top: 0; - box-shadow: 0 0.8rem 1.3rem rgba(0,0,0,0.2); - border-radius: 0.5rem; - border: 1px solid #b9b9b9; -} - -.vert { - justify-content: center; - display: flex; - flex-direction: column; -} - -.vert2 { - display: flex; - flex-direction: column; -} - -.overlay { - display: grid; -} - -.overlay > * { - grid-row: 1; - grid-column: 1; -} - -form > .content { - margin: 2rem; - margin-top: 1rem; - margin-bottom: 1.7rem; - padding: 0; - display: flex; - flex-direction: column; - justify-content: center; -} - -form > h2 { - color: #4d4d4d; - margin: 0; - text-align: center; - padding: 1rem; - background: #f0f0f0; - border-top-left-radius: 0.5rem; - border-top-right-radius: 0.5rem; - align-content: center; -} - - -.window h3 button, -.window > h2 button -{ - border: none; - padding: 0.3rem 1rem; - background: inherit; - border-radius: 0; -} - -.window h3 button:not(.pathentry):hover { - background: white; -} - -.window h3 .separator { - flex: 0 0 1px; - align-self: stretch; - background: #bbb; -} - -.window > h2 > *:first-child { - border-top-left-radius: 0.5rem; -} -form p { - margin: 1rem 0px 0.3rem 0px; -} - -.hero_form_error { - animation: fadein 0.2s; - background-color: #ff4d4d; - color: #ffffff; - padding-left: 0.5rem; - border-bottom-left-radius: 0.5rem; - border-bottom-right-radius: 0.5rem; - margin-top: -0.2rem; - -} -@keyframes fadein { - from { opacity: 0; } - to { opacity: 1; } -} - -.content > input { - min-width: 300px; -} - -button, -input:not([type=file]) -{ - border: 1px solid #bbb; - padding: 0.5rem 0.8rem; - font-size: inherit; - font-family: inherit; - border-radius: 0.3rem; - background: #fcfcfc; -} - -input[type=button], button, input[type=submit] { - cursor: pointer; -} - -input[type=submit] { - margin-top: 2rem; - width: 100%; - display: block; - padding: 0.7rem; - font-size: 1.1em; - box-shadow: 0 0.2rem 0.6rem #eee; - background: #231179; - color: white; - outline: none; -} - - -input:hover, -button:hover -{ - background: white; -} - -input:focus { - border-color: black; -} - -input[type=submit]:hover { - background: #5b4d9d; -} - -.bgbottom { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - z-index: -100; -} - - -#arrows { - position: relative; -} - -#arrows > img { - position: absolute; - bottom: 0; - z-index: -200; -} - -#signupform { - display: none; -} - -.window { - overflow: hidden; - box-sizing: border-box; - margin: 0rem; - padding: 0; - box-shadow: 0 0.8rem 1.3rem rgba(0,0,0,0.2); - border-radius: 0.5rem; - border: 1px solid #b9b9b9; - - display: block; - position: absolute; - top: 0; - left: 0; - width: 800px; - height: 600px; - - transition: opacity 0.3s; - display: flex; - flex-direction: column; -} - -.window.focus { - border-color: black; -} - -.foldercontents, .filecontents, .filecontentsroot { - background: rgba(250, 250, 250, .9); - flex: 1 0 0; -} - -.filecontents, .filecontentsroot { - background: white; -} - -.filecontents { - font-family: monospace; -} - -.filecontents.imgview { - background-color: black; - background-repeat: no-repeat; - background-position: center; - background-size: contain; -} - -.filecontentsroot, .foldercontents { - display: flex; - flex-direction: column; -} - -.filecontents { - overflow-y: scroll; -} -[contenteditable] { - outline: 0px solid transparent; -} - -pre { - font-size: 1.3rem; - min-height: 100%; - margin: 0.3rem; - box-sizing: border-box; -} - -.window h3, -.window > h2 { - color: #4d4d4d; - background: #f0f0f0; - margin: 0; - - - display: flex; - align-items: stretch; - font-weight: normal; - padding: 0rem; - border-bottom: 1px solid #bbb; -} - -.window > h2 { - font-size: 1.3rem; - border-top-left-radius: 0.5rem; - border-top-right-radius: 0.5rem; - cursor: grab; - - user-select: none; - -webkit-user-select: none; - -ms-user-select: none; -} - -.window h3 { - font-size: 1.2rem; -} - -.window.dragged { - /* opacity: 0.9; */ -} - -.files { - padding: 0.3rem; - display: grid; - - grid-gap: 0rem; - grid-auto-rows: 8rem; - grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); -} - -.file { - box-sizing: border-box; - padding: 0.5rem; - cursor: pointer; - color: #333; - user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - - display: flex; - flex-direction: column; - align-items: center; - border: 1px solid rgba(0,0,0,0); - border-radius: 0.1rem; -} - -.file.dragged { - border: none; - pointer-events: none; -} - -.file:hover:not(.dragged) { - background: rgba(255,255,255, 0.5); - color: black; - border-color: #ddd; -} - -.file > img { - flex: 1 1 0; - min-width: 0; - min-height: 0; -} - -.file:hover > img { - filter: brightness(120%); -} - -.path { - display: flex; - align-items: stretch; - flex: 1 0 0; -} - -.path > .separator { - user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - padding: 0 0.1rem; - align-self: center; -} - -.pathentry:last-child { - cursor: inherit; -} - -.pathentry:not(:last-child):hover { - text-decoration: underline; -} - -.context { - background: white; - position: absolute; - z-index: 1000; - list-style-type: none; - margin: 0; - padding: 0; - top: 0; - left: 0; - - border: 1px solid #ccc; - box-shadow: 0 0.3rem 0.5rem rgba(0,0,0,0.2); -} - -.context > li { - padding: 0.4rem 1.5rem; - margin: 0; - - user-select: none; - -webkit-user-select: none; - -ms-user-select: none; -} - -.context > li:hover { - background: #2e91db; - color: white; -} - -.backdrop { - position: absolute; - width: 100%; - height: 100%; - - user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - background-image: url("/img/backdrop.jpg"); - background-size: cover; -} - -.share_dialog_contents { - display: flex; - flex-direction: column; - background: white; - background: rgba(255,255,255, 0.9); -} - -.window > h2 .close_button { - border-left: 1px solid #ccc; -} - -.close_button:hover { - background: white; -} - - -.wndtitle { - display: flex; - align-items: center; - flex: 1 1 0; - padding-left: 0.8rem; -} - -iframe { - margin: 0; - border: 0; -} - -.filename { - word-wrap: break-word; - word-break: break-word; -} diff --git a/documentation/web_project_documentataion.docx b/documentation/web_project_documentataion.docx deleted file mode 100644 index 1ca0ea4..0000000 Binary files a/documentation/web_project_documentataion.docx and /dev/null differ diff --git a/front/loggedin.ts b/front/loggedin.ts new file mode 100644 index 0000000..db237ac --- /dev/null +++ b/front/loggedin.ts @@ -0,0 +1,1195 @@ +// This should be only set to false for debugging purposes +// If it's set to false, the upload requests will be synchronous +// and you will be able to see PHP's echo output in the browser +var FORM_ASYNC = true; + +// A FileView is an entry inside the explorer window +class FileView { + filename: string; + wnd: OurWindow; + 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(get_path(this.wnd), this.filename); + } +} + +// An array of all fileviews currently open + +class OurWindow { + pwd: string[]; + visuals: HTMLElement; + h2: HTMLElement; // The titlebar of the window + fileview: any; // TODO what is this + files: any[]; + txt_editor: HTMLElement; // For editable text files, this is the DOM element the user can edit + + // TODO move these to a subclass + foldercontents: any; + filecontents: any; + filecontentsroot: any; + filegrid: any; + save_btn_container: any; + + constructor(pwd) { + this.pwd = pwd; // pwd = [ "Folder1", "Folder2" ] means the current directory of that window is /Folder1/Folder2 + this.visuals = null; // The DOM object + this.h2 = null; // The titlebar of the window + this.fileview = null; + this.files = []; + this.txt_editor = null; // For editable text files, this is the DOM element the user can edit + } +} + +// An array with all the windows on the screen +var windows: OurWindow[] = []; + +// The focused window +var focused_window: OurWindow = null; + +// Those all belong to the hidden file upload form +const upload_form: HTMLFormElement = document.getElementById("upload_form") as any; +const filename_input: HTMLInputElement = document.getElementById("filename") as any; +const override_input: HTMLInputElement = document.getElementById("override_input") as any; +const upload_parent_directory: HTMLInputElement = document.getElementById("upload_parent_directory") as any; +const the_file: HTMLInputElement = document.getElementById("the_file") as any; + +// If this is set to true, requests to uploads.php will be sent with the "override" flag +// which will override existing files with the same name +var override_file = false; +var override_file_filename = ""; +var override_file_path = ""; + +var open_file = null; + +// Some elements have custom right click context menus +// If there's a custom context menu active, this will be it +var context_menu = null; + + +// If we're currently dragging something (a window or a file), this is the DOM object we're dragging +// dragging_offset_x and dragging_offset_y are the difference between the objec's top left point and the cursor position +// this is then added to the cursor position when the mouse is moved +var dragging = null; +var dragging_offset_x = 0, dragging_offset_y = 0; + +// If we have pressed down a window's title bar but haven't yet started dragging it, it's the drag candidate +// This is needed because we don't yet know whether we want to start dragging the window or click an element in the titlebar +// Once the mouse has moved sufficiently far away from dragging_candidate_x or y, we start dragging +var dragging_candidate = null; +var dragging_candidate_x, dragging_candidate_y; + +// If we're dragging a fileview, this is set to the fileview class instance itself, because 'dragging' is just a DOM object +// The placeholder is a dummy DIV that we insert in the object's place on the grid to keep things nicely aligned +var dragging_fileview; +var dragging_placeholder = null; + +// Windows have a z-index. When we click a window it is sent to the top this will be its new z-index +// We then increment the depth, and the next window we click will go on top of the current one +var depth = 20; + + + +function main() { + // Create a window that looks at the root directory + var root_window = make_window([], false); + + // Focus that window and load the directory + focus_window(root_window); + openfile(true, root_window); +} + +function focus_window(wnd: OurWindow) { + // Unfocus the old window + if (focused_window) + focused_window.visuals.classList.remove('focus'); + focused_window = wnd; + // And focus the new one! + if (wnd) { + wnd.visuals.classList.add('focus'); + wnd.visuals.style.zIndex = (depth ++).toString(); + } +} + +// Delete the focused window +function delete_window(wnd: OurWindow) { + var index = windows.indexOf(wnd); + + if (index >= 0) + windows.splice(index, 1); + + wnd.visuals.parentNode.removeChild(wnd.visuals); + if (wnd == focused_window) + focused_window = null; +} + +// Create a right click context menu +function context(e, entries) { + if (context_menu) + context_menu.remove(); + + context_menu = mk(document.body, 'ul', 'context'); + + context_menu.onmousedown = (e) => { + e.stopPropagation(); + } + + context_menu.onclick = (_e) => { + context_menu.remove(); + context_menu = null; + } + + + context_menu.style.left = e.clientX + "px"; + context_menu.style.top = e.clientY + "px"; + + for (const e of entries) { + const li = document.createElement('li'); + li.innerText = e[0]; + li.onclick = e[1]; + context_menu.appendChild(li); + } +} + +// This is called whenever the 's value changes +function on_file_added(_e) { + if (the_file.files.length >= 1) { + + if (override_file) { + filename_input.value = override_file_filename; + override_input.value = "1"; + upload_parent_directory.value = override_file_path; + console.log(filename_input.value, override_input.value, upload_parent_directory.value); + } else { + filename_input.value = the_file.files[0].name; + override_input.value = "0"; + upload_parent_directory.value = get_path(focused_window); + } + + if (!FORM_ASYNC) { + upload_form.submit(); + return; + } + + // Send the form asynchronously through the fetch api + fetch(upload_form.action, { + method: upload_form.method, + body: new FormData(upload_form) + }).then((resp) => { + if (resp.status == 200) { + // Reload the directory so the user can see the newly uploaded file + openfile(true, focused_window); + } else { + alert("Upload failed"); + } + }, () => { + alert("Upload failed") + }); + } + else { + alert("No files selected"); + } +} + +// It's honestly really sad that we need this +// We have an image viewer, but we load the uploaded via the XMLHttpRequest API, which gives us an array buffer +// We need to base64 encode the image data so we can feed it into the +// and the standart base64 encode API is shit +// https://stackoverflow.com/questions/7370943/retrieving-binary-file-content-using-javascript-base64-encode-it-and-reverse-de +function base64ArrayBuffer(arrayBuffer) { + var base64 = '' + var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + + var bytes = new Uint8Array(arrayBuffer) + var byteLength = bytes.byteLength + var byteRemainder = byteLength % 3 + var mainLength = byteLength - byteRemainder + + var a, b, c, d + var chunk + + // Main loop deals with bytes in chunks of 3 + for (var i = 0; i < mainLength; i = i + 3) { + // Combine the three bytes into a single integer + chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2] + + // Use bitmasks to extract 6-bit segments from the triplet + a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18 + b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12 + c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6 + d = chunk & 63 // 63 = 2^6 - 1 + + // Convert the raw binary segments to the appropriate ASCII encoding + base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d] + } + + // Deal with the remaining bytes and padding + if (byteRemainder == 1) { + chunk = bytes[mainLength] + + a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2 + + // Set the 4 least significant bits to zero + b = (chunk & 3) << 4 // 3 = 2^2 - 1 + + base64 += encodings[a] + encodings[b] + '==' + } else if (byteRemainder == 2) { + chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1] + + a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10 + b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4 + + // Set the 2 least significant bits to zero + c = (chunk & 15) << 2 // 15 = 2^4 - 1 + + base64 += encodings[a] + encodings[b] + encodings[c] + '=' + } + + return base64 +} + + +// This updates the path of the window's DOM (the "Root > Folder1 > Folder2 > foo.png") +function update_path_visuals(wnd) { + if (!wnd) { + alert("YOU ARE NOT SUPPOSED TO SEE THIS. PLEASE COPY THE STACKTRACE FROM THE CONSOLE AND SEND IT TO THE NEAREST DEVELOPER"); + wnd = focused_window; + } + + 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(true, 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 = get_path(wnd, i + 1); + move_file(dragging_fileview.wnd, wnd, new_folder, dragging_fileview.filename); + end_drag(e); + + e.preventDefault(); + e.stopPropagation(); + } + } + } +} + + +// 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 +function openfile_nondir(wnd) { + + 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', get_path(wnd, 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 = open_file.mimetype.split("/")[0] == "image"; + wnd.save_btn_container.style.display = (open_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 (open_file.write_permissions) + wnd.txt_editor.contentEditable = "true"; + }; + } + + xhr.send(data); +} + +// This is a tiny wrapper around the share_window. +function share(in_file: boolean, filename: string, wnd: OurWindow) { + if (in_file) { + var folder = get_path(wnd, wnd.pwd.length - 1); + filename = wnd.pwd[wnd.pwd.length - 1]; + } else { + var folder = get_path(wnd); + } + + var wnd = make_share_window(folder, filename); + focus_window(wnd); +} + +// Replace an existing file with a new one +function replace_file(in_file, filename, wnd) { + if (in_file) { + var folder = get_path(wnd, wnd.pwd.length - 1); + filename = wnd.pwd[wnd.pwd.length - 1]; + } else { + var folder = get_path(wnd); + } + + override_file = true; + override_file_path = folder; + override_file_filename = filename; + the_file.click(); +} + +// This loads the contents of the current directory +function openfile_dir(wnd) { + update_path_visuals(wnd); + + var data = new FormData(); + data.append('path', get_path(wnd)); + + 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, get_path(wnd), dragging_fileview.filename); + } + } +} + + +function openfile(is_directory, wnd) { + if (!wnd) { + alert("YOU ARE NOT SUPPOSED TO SEE THIS. PLEASE COPY THE STACKTRACE FROM THE CONSOLE AND SEND IT TO THE NEAREST DEVELOPER"); + console.trace(); + wnd = focused_window; + } + + if (is_directory) { + openfile_dir(wnd); + } else { + openfile_nondir(wnd); + } +} + +function move_to_trash(wnd, filename) { + move_file(wnd, wnd, "/trash", filename, path_combine(get_path(wnd), 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', get_path(wnd)); + data.append('filename', filename); + + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/php/delete.php', true); + xhr.onload = function () { + openfile(true, wnd); + }; + 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', get_path(wnd)); + 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 () { + openfile(true, wnd); + }; + xhr.send(data); +} + +function move_file(srcwnd: OurWindow, dstwnd: OurWindow, new_folder: string, filename: string, new_filename?: string) { + if (!new_filename) + new_filename = filename; + + var data = new FormData(); + data.append('old_folder', get_path(srcwnd)); + 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 = () => { + openfile(true, srcwnd); + openfile(true, dstwnd); + }; + 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', get_path(wnd)); + data.append('dirname', dirname); + + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/php/mkdir.php', true); + xhr.onload = function () { + openfile(true, wnd); + }; + xhr.send(data); +} + + +// Dragging a fileview is a bit different from dragging a window +// This does some setup work before calling the common begin_drag +function begin_drag_fileview(e, fileview) { + if (dragging) + end_drag(e); + + // The dragging_placeholder is inserted into its place by the begin_drag function + dragging_placeholder = document.createElement('div'); + dragging_fileview = fileview; + + dragging = fileview.visuals; + dragging.style.zIndex = 50000; + + begin_drag(e, fileview.visuals); +} + +// Start dragging the 'obj' DOM element +// e is a DOM event, this should only get called in response of a DOM event +function begin_drag(e, obj: HTMLElement, dont_set_width?: boolean) { + set_iframe_enabled(false); + dragging = obj; + dragging_candidate = null; + dragging.classList.add("dragged"); + + var elemRect = dragging.getBoundingClientRect(); + dragging_offset_x = e.clientX - elemRect.left; + dragging_offset_y = -e.clientY + elemRect.top; + + if (dragging_placeholder) + obj.parentNode.insertBefore(dragging_placeholder, obj); + + dragging.style.left = (e.clientX - dragging_offset_x) + "px"; + dragging.style.top = (e.clientY + dragging_offset_y) + "px"; + + if (!dont_set_width) { + dragging.style.width = elemRect.width + "px"; + dragging.style.height = elemRect.height + "px"; + } + + dragging.style.position = "absolute"; + document.body.appendChild(dragging); +} + +function end_drag(_e) { + set_iframe_enabled(true); + + // If there's a dragging placeholder remove it and put the dragged node back into its place + if (dragging_placeholder) { + dragging_placeholder.parentNode.insertBefore(dragging, dragging_placeholder); + dragging_placeholder.remove(); + dragging_placeholder = null; + } + + // If we were dragging a FileView, we need to reset some CSS + if (dragging_fileview) { + dragging.style.removeProperty("position"); + dragging.style.removeProperty("width"); + dragging.style.removeProperty("height"); + dragging.style.removeProperty("left"); + dragging.style.removeProperty("top"); + dragging_fileview = null; + } + + dragging.classList.remove("dragged"); + dragging = null; +} + +// This creates the parts of a window that are common between all window types +// This should only really be called by another function that will then fill up the window +function make_window_base(pwd, x, y, w, h) { + var wnd = new OurWindow(pwd); + windows.push(wnd); + + wnd.visuals = mk(document.body, 'div', 'window'); + + wnd.visuals.style.width = w + "px"; + wnd.visuals.style.height = h ? (h + "px") : "unset"; + wnd.visuals.style.position = "absolute"; + wnd.visuals.style.left = x + "px"; + wnd.visuals.style.top = y + "px"; + + wnd.h2 = mk(wnd.visuals, 'h2'); + + wnd.visuals.onmousedown = (_e) => { + focus_window(wnd); + } + + wnd.h2.onmousedown = (e) => { + if (!dragging) { + dragging_candidate = wnd.visuals; + dragging_candidate_x = e.clientX; + dragging_candidate_y = e.clientY; + } + }; + + return wnd; +} + +// This is a widely abused helper function that creates a DOM element, attaches it as the +// last child of 'parent' and possibly gives it a class +function mk(parent: HTMLElement, type: keyof HTMLElementTagNameMap, _class?: string): HTMLElement { + var el = document.createElement(type); + parent.appendChild(el); + if (_class) + el.classList.add(_class); + return el; +} + +// Crate a horizontal div +function mkhdiv(parent) { + var hdiv = mk(parent, 'div'); + hdiv.style.display = "flex"; + hdiv.style.alignItems = "center"; + hdiv.style.padding = "0.3rem"; + hdiv.style.gap = "0.3rem"; + return hdiv; +} + +// Create a checkbocx with a label. +// togglefn will be called when its value changes with an argument that's either true/false +function mkcheckbox(parent, label, togglefn) { + var hdiv = mkhdiv(parent); + + var write_checkbox: HTMLInputElement = mk(hdiv, 'input') as any; + write_checkbox.type = 'checkbox'; + + var write_checkbox_label = mk(hdiv, 'label'); + write_checkbox_label.innerText = label; + write_checkbox_label.onclick = (_e) => { write_checkbox.click(); } + write_checkbox_label.classList.add('noselect'); + + write_checkbox.onchange = (_e) => { + togglefn(write_checkbox.checked); + }; +} + +// This monstrocity creates the 'Share file' window +function make_share_window(folder, filename): OurWindow { + var wnd = make_window_base(null, 400, 400, 400, 0); + + wnd.h2.style.display = 'flex'; + + // The title of the window. WE set its 'flex' to 1 1 0 so it fills up the titlebar + // and pushes the X button to the very right + var heading = mk(wnd.h2, 'span', 'wndtitle'); + heading.innerText = "Share " + filename; + + // Close button + var x_button = mk(wnd.h2, 'button', 'close_button'); + x_button.innerText = "X"; + x_button.onclick = () => delete_window(wnd); + + wnd.foldercontents = mk(wnd.visuals, 'div', 'share_dialog_contents'); + wnd.foldercontents.style.padding = "0.5rem"; + + // This is the data that will be sent when we hit "Generate link" + var data = { + write_permissions: false, + private: false, + has_password: false, + password: "", + userlist: [], + } + + // If private link is clicked, show the "Add user" button and the user list + var userlist, add_user; + mkcheckbox(wnd.foldercontents, "Private link", (toggled) => { + add_user.style.display = toggled ? "block" : "none"; + userlist.style.display = toggled ? "block" : "none"; + data.private = toggled; + }); + + userlist = mk(wnd.foldercontents, 'div'); + userlist.style.display = "none"; + add_user = mk(wnd.foldercontents, 'button'); + add_user.innerText = "Add user"; + add_user.style.display = "none"; + + // When we hit 'Add user', add an input field for a new user + add_user.onclick = (_e) => { + var i = mk(userlist, 'input') as HTMLInputElement; + i.value = 'John Doe'; + + let index = data.userlist.length; + data.userlist.push(i.value); + + i.onchange = (_e) => { + data.userlist[index] = i.value; + } + } + + // Click the add_user once to add a default user, since a URL that nobody can use makes no sense + add_user.click(); + + mkcheckbox(wnd.foldercontents, "Give write permissions", (toggled) => { + data.write_permissions = toggled; + }); + + // If 'Password protected' is checked, show the password field + let password_container; + mkcheckbox(wnd.foldercontents, "Password protected", (toggled) => { + data.has_password = toggled; + password_container.style.display = toggled ? "flex" : "none"; + }); + + password_container = mkhdiv(wnd.foldercontents); + password_container.style.display = 'none' + var password_label = mk(password_container, 'label'); + password_label.innerText = "Password"; + var password_input = mk(password_container, 'input') as HTMLInputElement; + password_input.type = 'password'; + password_input.autocomplete = 'off' + + password_input.style.flex = "1 0 0"; + password_input.onchange = (_e) => { + data.password = password_input.value; + }; + + var generate_url_button = mk(wnd.foldercontents, 'button'); + generate_url_button.innerText = "Generate link"; + + generate_url_button.onclick = () => { + // The backend expects the users to be either an empty string, if the URL is public + // or a comma separated list of usernaems + var users = ""; + if (data.private) { + users = data.userlist.join(','); + } + + var form_data = new FormData(); + form_data.append('folder', folder); + form_data.append('filename', filename); + form_data.append('users', users); + // 0 = No permissions, 1 = Read only, 2 = Write , 1|2 = 3 = RW + // Only 1 and 3 make sense in the context of a URL + form_data.append('permissions', (data.write_permissions ? 3 : 1).toString()); + form_data.append('password', data.has_password ? data.password : ""); + + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/php/share.php', true); + xhr.onload = function () { + alert(xhr.response); + } + + xhr.send(form_data); + delete_window(wnd); + } + + return wnd; +} + +function download_file(in_file, filename?: any, wnd?: any) { + if (!wnd) { + alert (802); + wnd = focused_window; + } + if (in_file) { + var folder = get_path(wnd, wnd.pwd.length - 1); + filename = wnd.pwd[wnd.pwd.length - 1]; + } else { + var folder = get_path(wnd); + } + + // 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; +} + + +// make_window creates an explorer window - the kind that can list directories/open files +function make_window(pwd: string[], has_close: boolean): OurWindow { + var wnd = make_window_base(pwd, 100, 100, 800, 600); + + mk(wnd.h2, 'div', 'path'); + + if (has_close) { + var x_button = mk(wnd.h2, 'button', 'close_button'); + x_button.innerText = "X"; + x_button.onclick = () => delete_window(wnd); + } + + // 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 = () => { + override_file = false; + the_file.click(); + } + + 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; +} + + +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', get_path (wnd, wnd.pwd.length - 1)); + data.append('filename', wnd.pwd[wnd.pwd.length - 1]); + data.append('content', contents); + data.append('overwrite', '1'); + + xhr.send(data); +} + + +// Create the visuals for a FileView +function add_file_visuals(fileview, wnd) { + // 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 (get_path(wnd) == "/" && fileview.filename == "trash") + img.src="/mimeicons/user-trash.png"; + else if (get_path(wnd) == "/" && 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) { + open_file = fileview; + } + openfile(fileview.is_directory, wnd); + } + + visuals.oncontextmenu = (e) => { + if (!dragging) { + + var context_list = [ + // Open is always in the context list + ['Open', () => { + wnd.pwd.push(fileview.filename); + openfile(fileview.is_directory, wnd); + }], + ['Open in New Window', () => { + var new_pwd = wnd.pwd.slice(); + new_pwd.push(fileview.filename); + var new_wnd = make_window(new_pwd, true); + open_file = fileview; + openfile(fileview.is_directory, new_wnd); + focus_window(new_wnd); + }], + ]; + + 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 = make_window_base([], 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 = () => delete_window(wnd); + + const contents = mk(wnd.visuals, 'div', 'filecontentsroot'); + const iframe = mk(contents, 'iframe') as HTMLIFrameElement; + iframe.style.flex = '1 0 0'; + iframe.src = url; + + focus_window(wnd); + } else { + window.location = url; + } + }, get_path(wnd), 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 (get_path(wnd) == "/" && fileview.filename == "trash") { + // If we've dragged something onto the trashcan, it's trash + move_to_trash(wnd, dragging_fileview.filename); + } + else if (get_path(wnd) == "/" && 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(get_path(wnd), 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); +} + + +// Reads the 'pwd' of the focused window +// If pwd is ['foo', 'bar', 'baz'], this returns '/foo/bar/baz' +function get_path(wnd: OurWindow, max_length?: number) { + if (!wnd) { + alert("YOU ARE NOT SUPPOSED TO SEE THIS. PLEASE COPY THE STACKTRACE FROM THE CONSOLE AND SEND IT TO THE NEAREST DEVELOPER"); + console.trace(); + wnd = focused_window; + } + if (max_length == undefined) { + max_length = wnd.pwd.length; + } + + var path = "/"; + for (let i = 0; i < max_length; i++) { + path += wnd.pwd[i]; + if (i != max_length - 1) + path += "/"; + } + return path; +} + +function path_combine(a, b) { + const last_char = a.slice(-1); + if (last_char == "/") + return a + b; + else + return a + "/" + b; +} + + +// When we click anywhere, remove the context menu +// The context menu itself has a onmousedown that prevents propagation so we can click its elements +document.body.onmousedown = (_e) => { + if (context_menu) { + context_menu.remove(); + context_menu = null; + } +} + +document.body.onmousemove = (e) => { + if (dragging) { + dragging.style.left = (e.clientX - dragging_offset_x) + "px"; + dragging.style.top = (e.clientY + dragging_offset_y) + "px"; + } + else if (dragging_candidate) { + var d = Math.abs(e.clientX - dragging_candidate_x) + Math.abs(e.clientY - dragging_candidate_y); + if (d > 15) + begin_drag(e, dragging_candidate, true); + } +} + +document.body.onmouseup = (_e) => { + if (dragging_candidate) + dragging_candidate = null; + if (dragging) + end_drag(_e); +} + +document.body.oncontextmenu = (e) => { + if (dragging) { + end_drag(e); + e.preventDefault(); + } + if (context_menu) { + context_menu.remove(); + context_menu = null; + } +} + +the_file.onchange = (e) => { on_file_added(e); }; + +function set_iframe_enabled(en) { + for (const iframe of document.getElementsByTagName('iframe')) + iframe.hidden = !en; +} + +main(); diff --git a/img/arrow.svg b/img/arrow.svg new file mode 100644 index 0000000..040182d --- /dev/null +++ b/img/arrow.svg @@ -0,0 +1,62 @@ + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/img/bottom.svg b/img/bottom.svg new file mode 100644 index 0000000..772081e --- /dev/null +++ b/img/bottom.svg @@ -0,0 +1,62 @@ + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/img/icons/.txt.svg b/img/icons/.txt.svg new file mode 100644 index 0000000..db35ac4 --- /dev/null +++ b/img/icons/.txt.svg @@ -0,0 +1,65 @@ + + + + + + + + image/svg+xml + + + + + + + txt + + diff --git a/index.php b/index.php index 3fc4afc..2daf053 100644 --- a/index.php +++ b/index.php @@ -9,7 +9,7 @@ - shady file upload + shady file upload @@ -54,7 +54,7 @@ - + diff --git a/loggedin.js b/loggedin.js deleted file mode 100644 index cceb284..0000000 --- a/loggedin.js +++ /dev/null @@ -1,1175 +0,0 @@ -// This should be only set to false for debugging purposes -// If it's set to false, the upload requests will be synchronous -// and you will be able to see PHP's echo output in the browser -var FORM_ASYNC = true; - -// A FileView is an entry inside the explorer window -class FileView { - 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(get_path(this.wnd), this.filename); - } -} - -// An array of all fileviews currently open - -class Window { - constructor(pwd) { - this.pwd = pwd; // pwd = [ "Folder1", "Folder2" ] means the current directory of that window is /Folder1/Folder2 - this.visuals = null; // The DOM object - this.h2 = null; // The titlebar of the window - this.fileview = null; - this.files = []; - this.txt_editor = null; // For editable text files, this is the DOM element the user can edit - } -} - -// An array with all the windows on the screen -var windows = []; - -// The focused window -var focus = null; - -// Those all belong to the hidden file upload form -const upload_form = document.getElementById("upload_form"); -const the_file = document.getElementById("the_file"); -const filename_input = document.getElementById("filename"); -const override_input = document.getElementById("override_input"); -const current_directory = document.getElementById("current_directory"); -const upload_parent_directory = document.getElementById("upload_parent_directory"); - -// If this is set to true, requests to uploads.php will be sent with the "override" flag -// which will override existing files with the same name -var override_file = false; -var override_file_filename = ""; -var override_file_path = ""; - -var open_file = null; - -// Some elements have custom right click context menus -// If there's a custom context menu active, this will be it -var context_menu = null; - - -// If we're currently dragging something (a window or a file), this is the DOM object we're dragging -// dragging_offset_x and dragging_offset_y are the difference between the objec's top left point and the cursor position -// this is then added to the cursor position when the mouse is moved -var dragging = null; -var dragging_offset_x = 0, dragging_offset_y = 0; - -// If we have pressed down a window's title bar but haven't yet started dragging it, it's the drag candidate -// This is needed because we don't yet know whether we want to start dragging the window or click an element in the titlebar -// Once the mouse has moved sufficiently far away from dragging_candidate_x or y, we start dragging -var dragging_candidate = null; -var dragging_candidate_x, dragging_candidate_y; - -// If we're dragging a fileview, this is set to the fileview class instance itself, because 'dragging' is just a DOM object -// The placeholder is a dummy DIV that we insert in the object's place on the grid to keep things nicely aligned -var dragging_fileview; -var dragging_placeholder = null; - -// Windows have a z-index. When we click a window it is sent to the top this will be its new z-index -// We then increment the depth, and the next window we click will go on top of the current one -var depth = 20; - - - -function main() { - // Create a window that looks at the root directory - var root_window = make_window([]); - - // Focus that window and load the directory - focus_window(root_window); - openfile(true, root_window); -} - -function focus_window(wnd) { - // Unfocus the old window - if (focus) - focus.visuals.classList.remove('focus'); - focus = wnd; - // And focus the new one! - if (wnd) { - wnd.visuals.classList.add('focus'); - wnd.visuals.style.zIndex = depth ++; - } -} - -// Delete the focused window -function delete_window(wnd) { - var index = windows.indexOf(wnd); - - if (index >= 0) - windows.splice(index, 1); - - wnd.visuals.parentNode.removeChild(wnd.visuals); - if (wnd == focus) - focus = null; -} - -// Create a right click context menu -function context(e, entries) { - if (context_menu) - context_menu.remove(); - - context_menu = mk(document.body, 'ul', 'context'); - - context_menu.onmousedown = (e) => { - e.stopPropagation(); - } - - context_menu.onclick = (_e) => { - context_menu.remove(); - context_menu = null; - } - - - context_menu.style.left = e.clientX + "px"; - context_menu.style.top = e.clientY + "px"; - - for (const e of entries) { - const li = document.createElement('li'); - li.innerText = e[0]; - li.onclick = e[1]; - context_menu.appendChild(li); - } -} - -// This is called whenever the 's value changes -function on_file_added(_e) { - if (the_file.files.length >= 1) { - - if (override_file) { - filename_input.value = override_file_filename; - override_input.value = "1"; - upload_parent_directory.value = override_file_path; - console.log(filename_input.value, override_input.value, upload_parent_directory.value); - } else { - filename_input.value = the_file.files[0].name; - override_input.value = "0"; - upload_parent_directory.value = get_path(focus); - } - - if (!FORM_ASYNC) { - upload_form.submit(); - return; - } - - // Send the form asynchronously through the fetch api - fetch(upload_form.action, { - method: upload_form.method, - body: new FormData(upload_form) - }).then((resp) => { - if (resp.status == 200) { - // Reload the directory so the user can see the newly uploaded file - openfile(true, focus); - } else { - alert("Upload failed"); - } - }, () => { - alert("Upload failed") - }); - } - else { - alert("No files selected"); - } -} - -// It's honestly really sad that we need this -// We have an image viewer, but we load the uploaded via the XMLHttpRequest API, which gives us an array buffer -// We need to base64 encode the image data so we can feed it into the -// and the standart base64 encode API is shit -// https://stackoverflow.com/questions/7370943/retrieving-binary-file-content-using-javascript-base64-encode-it-and-reverse-de -function base64ArrayBuffer(arrayBuffer) { - var base64 = '' - var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' - - var bytes = new Uint8Array(arrayBuffer) - var byteLength = bytes.byteLength - var byteRemainder = byteLength % 3 - var mainLength = byteLength - byteRemainder - - var a, b, c, d - var chunk - - // Main loop deals with bytes in chunks of 3 - for (var i = 0; i < mainLength; i = i + 3) { - // Combine the three bytes into a single integer - chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2] - - // Use bitmasks to extract 6-bit segments from the triplet - a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18 - b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12 - c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6 - d = chunk & 63 // 63 = 2^6 - 1 - - // Convert the raw binary segments to the appropriate ASCII encoding - base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d] - } - - // Deal with the remaining bytes and padding - if (byteRemainder == 1) { - chunk = bytes[mainLength] - - a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2 - - // Set the 4 least significant bits to zero - b = (chunk & 3) << 4 // 3 = 2^2 - 1 - - base64 += encodings[a] + encodings[b] + '==' - } else if (byteRemainder == 2) { - chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1] - - a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10 - b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4 - - // Set the 2 least significant bits to zero - c = (chunk & 15) << 2 // 15 = 2^4 - 1 - - base64 += encodings[a] + encodings[b] + encodings[c] + '=' - } - - return base64 -} - - -// This updates the path of the window's DOM (the "Root > Folder1 > Folder2 > foo.png") -function update_path_visuals(wnd) { - if (!wnd) { - alert("YOU ARE NOT SUPPOSED TO SEE THIS. PLEASE COPY THE STACKTRACE FROM THE CONSOLE AND SEND IT TO THE NEAREST DEVELOPER"); - wnd = focus; - } - - 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(true, 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 = get_path(wnd, i + 1); - move_file(dragging_fileview.wnd, wnd, new_folder, dragging_fileview.filename); - end_drag(); - - e.preventDefault(); - e.stopPropagation(); - } - } - } -} - - -// 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 -function openfile_nondir(wnd) { - - 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', get_path(wnd, 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 = open_file.mimetype.split("/")[0] == "image"; - wnd.save_btn_container.style.display = (open_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 (open_file.write_permissions) - wnd.txt_editor.contentEditable = "true"; - }; - } - - xhr.send(data); -} - -// This is a tiny wrapper around the share_window. -function share(in_file, filename, wnd) { - if (in_file) { - var folder = get_path(wnd, wnd.pwd.length - 1); - filename = wnd.pwd[wnd.pwd.length - 1]; - } else { - var folder = get_path(wnd); - } - - var wnd = make_share_window(folder, filename); - focus_window(wnd); -} - -// Replace an existing file with a new one -function replace_file(in_file, filename, wnd) { - if (in_file) { - var folder = get_path(wnd, wnd.pwd.length - 1); - filename = wnd.pwd[wnd.pwd.length - 1]; - } else { - var folder = get_path(wnd); - } - - override_file = true; - override_file_path = folder; - override_file_filename = filename; - the_file.click(); -} - -// This loads the contents of the current directory -function openfile_dir(wnd) { - update_path_visuals(wnd); - - var data = new FormData(); - data.append('path', get_path(wnd)); - - 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, get_path(wnd), dragging_fileview.filename); - } - } -} - - -function openfile(is_directory, wnd) { - if (!wnd) { - alert("YOU ARE NOT SUPPOSED TO SEE THIS. PLEASE COPY THE STACKTRACE FROM THE CONSOLE AND SEND IT TO THE NEAREST DEVELOPER"); - console.trace(); - wnd = focus; - } - - if (is_directory) { - openfile_dir(wnd); - } else { - openfile_nondir(wnd); - } -} - -function move_to_trash(wnd, filename) { - move_file(wnd, wnd, "/trash", filename, path_combine(get_path(wnd), filename)); -} - -function restore_from_trash(wnd, filename) { - var split = filename.split("/"); - var new_filename = split.pop(); - var new_directory = "/" + split.join("/"); - - move_file(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', get_path(wnd)); - data.append('filename', filename); - - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/php/delete.php', true); - xhr.onload = function () { - openfile(true, wnd); - }; - 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', get_path(wnd)); - 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 () { - openfile(true, wnd); - }; - xhr.send(data); -} - -function move_file(srcwnd, dstwnd, new_folder, filename, new_filename) { - if (!new_filename) - new_filename = filename; - - var data = new FormData(); - data.append('old_folder', get_path(srcwnd)); - 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 = () => { - openfile(true, srcwnd); - openfile(true, dstwnd); - }; - 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', get_path(wnd)); - data.append('dirname', dirname); - - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/php/mkdir.php', true); - xhr.onload = function () { - openfile(true, wnd); - }; - xhr.send(data); -} - - -// Dragging a fileview is a bit different from dragging a window -// This does some setup work before calling the common begin_drag -function begin_drag_fileview(e, fileview) { - if (dragging) - end_drag(); - - // The dragging_placeholder is inserted into its place by the begin_drag function - dragging_placeholder = document.createElement('div'); - dragging_fileview = fileview; - - dragging = fileview.visuals; - dragging.style.zIndex = 50000; - - begin_drag(e, fileview.visuals); -} - -// Start dragging the 'obj' DOM element -// e is a DOM event, this should only get called in response of a DOM event -function begin_drag(e, obj, dont_set_width) { - set_iframe_enabled(false); - dragging = obj; - dragging_candidate = null; - dragging.classList.add("dragged"); - - var elemRect = dragging.getBoundingClientRect(); - dragging_offset_x = e.clientX - elemRect.left; - dragging_offset_y = -e.clientY + elemRect.top; - - if (dragging_placeholder) - obj.parentNode.insertBefore(dragging_placeholder, obj); - - dragging.style.left = (e.clientX - dragging_offset_x) + "px"; - dragging.style.top = (e.clientY + dragging_offset_y) + "px"; - - if (!dont_set_width) { - dragging.style.width = elemRect.width + "px"; - dragging.style.height = elemRect.height + "px"; - } - - dragging.style.position = "absolute"; - document.body.appendChild(dragging); -} - -function end_drag(_e) { - set_iframe_enabled(true); - - // If there's a dragging placeholder remove it and put the dragged node back into its place - if (dragging_placeholder) { - dragging_placeholder.parentNode.insertBefore(dragging, dragging_placeholder); - dragging_placeholder.remove(); - dragging_placeholder = null; - } - - // If we were dragging a FileView, we need to reset some CSS - if (dragging_fileview) { - dragging.style.removeProperty("position"); - dragging.style.removeProperty("width"); - dragging.style.removeProperty("height"); - dragging.style.removeProperty("left"); - dragging.style.removeProperty("top"); - dragging_fileview = null; - } - - dragging.classList.remove("dragged"); - dragging = null; -} - -// This creates the parts of a window that are common between all window types -// This should only really be called by another function that will then fill up the window -function make_window_base(pwd, x, y, w, h) { - var wnd = new Window(pwd); - windows.push(wnd); - - wnd.visuals = mk(document.body, 'div', 'window'); - - wnd.visuals.style.width = w + "px"; - wnd.visuals.style.height = h ? (h + "px") : "unset"; - wnd.visuals.style.position = "absolute"; - wnd.visuals.style.left = x + "px"; - wnd.visuals.style.top = y + "px"; - - wnd.h2 = mk(wnd.visuals, 'h2'); - - wnd.visuals.onmousedown = (_e) => { - focus_window(wnd); - } - - wnd.h2.onmousedown = (e) => { - if (!dragging) { - dragging_candidate = wnd.visuals; - dragging_candidate_x = e.clientX; - dragging_candidate_y = e.clientY; - } - }; - - return wnd; -} - -// This is a widely abused helper function that creates a DOM element, attaches it as the -// last child of 'parent' and possibly gives it a class -function mk(parent, type, _class) { - var el = document.createElement(type); - parent.appendChild(el); - if (_class) - el.classList.add(_class); - return el; -} - -// Crate a horizontal div -function mkhdiv(parent) { - var hdiv = mk(parent, 'div'); - hdiv.style.display = "flex"; - hdiv.style.alignItems = "center"; - hdiv.style.padding = "0.3rem"; - hdiv.style.gap = "0.3rem"; - return hdiv; -} - -// Create a checkbocx with a label. -// togglefn will be called when its value changes with an argument that's either true/false -function mkcheckbox(parent, label, togglefn) { - var hdiv = mkhdiv(parent); - - var write_checkbox = mk(hdiv, 'input'); - write_checkbox.type = 'checkbox'; - - var write_checkbox_label = mk(hdiv, 'label'); - write_checkbox_label.innerText = label; - write_checkbox_label.onclick = (_e) => { write_checkbox.click(); } - write_checkbox_label.classList.add('noselect'); - - write_checkbox.onchange = (_e) => { - togglefn(write_checkbox.checked); - }; -} - -// This monstrocity creates the 'Share file' window -function make_share_window(folder, filename) { - var wnd = make_window_base(null, 400, 400, 400, 0); - - wnd.h2.style.display = 'flex'; - - // The title of the window. WE set its 'flex' to 1 1 0 so it fills up the titlebar - // and pushes the X button to the very right - var heading = mk(wnd.h2, 'span', 'wndtitle'); - heading.innerText = "Share " + filename; - - // Close button - var x_button = mk(wnd.h2, 'button', 'close_button'); - x_button.innerText = "X"; - x_button.onclick = () => delete_window(wnd); - - wnd.foldercontents = mk(wnd.visuals, 'div', 'share_dialog_contents'); - wnd.foldercontents.style.padding = "0.5rem"; - - // This is the data that will be sent when we hit "Generate link" - var data = { - write_permissions: false, - private: false, - has_password: false, - password: "", - userlist: [], - } - - // If private link is clicked, show the "Add user" button and the user list - var userlist, add_user; - mkcheckbox(wnd.foldercontents, "Private link", (toggled) => { - add_user.style.display = toggled ? "block" : "none"; - userlist.style.display = toggled ? "block" : "none"; - data.private = toggled; - }); - - userlist = mk(wnd.foldercontents, 'div'); - userlist.style.display = "none"; - add_user = mk(wnd.foldercontents, 'button'); - add_user.innerText = "Add user"; - add_user.style.display = "none"; - - // When we hit 'Add user', add an input field for a new user - add_user.onclick = (_e) => { - var i = mk(userlist, 'input'); - i.value = 'John Doe'; - - let index = data.userlist.length; - data.userlist.push(i.value); - - i.onchange = (_e) => { - data.userlist[index] = i.value; - } - } - - // Click the add_user once to add a default user, since a URL that nobody can use makes no sense - add_user.click(); - - mkcheckbox(wnd.foldercontents, "Give write permissions", (toggled) => { - data.write_permissions = toggled; - }); - - // If 'Password protected' is checked, show the password field - let password_container; - mkcheckbox(wnd.foldercontents, "Password protected", (toggled) => { - data.has_password = toggled; - password_container.style.display = toggled ? "flex" : "none"; - }); - - password_container = mkhdiv(wnd.foldercontents); - password_container.style.display = 'none' - var password_label = mk(password_container, 'label'); - password_label.innerText = "Password"; - var password_input = mk(password_container, 'input'); - password_input.type = 'password'; - password_input.autocomplete = 'off' - - password_input.style.flex = "1 0 0"; - password_input.onchange = (_e) => { - data.password = password_input.value; - }; - - var generate_url_button = mk(wnd.foldercontents, 'button'); - generate_url_button.innerText = "Generate link"; - - generate_url_button.onclick = () => { - // The backend expects the users to be either an empty string, if the URL is public - // or a comma separated list of usernaems - var users = ""; - if (data.private) { - users = data.userlist.join(','); - } - - var form_data = new FormData(); - form_data.append('folder', folder); - form_data.append('filename', filename); - form_data.append('users', users); - // 0 = No permissions, 1 = Read only, 2 = Write , 1|2 = 3 = RW - // Only 1 and 3 make sense in the context of a URL - form_data.append('permissions', data.write_permissions ? 3 : 1); - form_data.append('password', data.has_password ? data.password : ""); - - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/php/share.php', true); - xhr.onload = function () { - alert(xhr.response); - } - - xhr.send(form_data); - delete_window(wnd); - } - - return wnd; -} - -function download_file(in_file, filename, wnd) { - if (!wnd) { - alert (802); - wnd = focus; - } - if (in_file) { - var folder = get_path(wnd, wnd.pwd.length - 1); - filename = wnd.pwd[wnd.pwd.length - 1]; - } else { - var folder = get_path(wnd); - } - - // 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; -} - - -// make_window creates an explorer window - the kind that can list directories/open files -function make_window(pwd, has_close) { - var wnd = make_window_base(pwd, 100, 100, 800, 600); - - path = mk(wnd.h2, 'div', 'path'); - - if (has_close) { - var x_button = mk(wnd.h2, 'button', 'close_button'); - x_button.innerText = "X"; - x_button.onclick = () => delete_window(wnd); - } - - // 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 = () => { - override_file = false; - the_file.click(); - } - - 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 = () => { share(true, 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; -} - - -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', 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); -} - - -// Create the visuals for a FileView -function add_file_visuals(fileview, wnd) { - // 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 (get_path(wnd) == "/" && fileview.filename == "trash") - img.src="/mimeicons/user-trash.png"; - else if (get_path(wnd) == "/" && 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) { - open_file = fileview; - } - openfile(fileview.is_directory, wnd); - } - - visuals.oncontextmenu = (e) => { - if (!dragging) { - - var context_list = [ - // Open is always in the context list - ['Open', () => { - wnd.pwd.push(fileview.filename); - openfile(fileview.is_directory, wnd); - }], - ['Open in New Window', () => { - var new_pwd = wnd.pwd.slice(); - new_pwd.push(fileview.filename); - var new_wnd = make_window(new_pwd, true); - open_file = fileview; - openfile(fileview.is_directory, new_wnd); - focus_window(new_wnd); - }], - ]; - - 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 = make_window_base([], 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 = () => delete_window(wnd); - - const contents = mk(wnd.visuals, 'div', 'filecontentsroot'); - const iframe = mk(contents, 'iframe'); - iframe.style.flex = '1 0 0'; - iframe.src = url; - - focus_window(wnd); - } else { - window.location = url; - } - }, get_path(wnd), 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 (get_path(wnd) == "/" && fileview.filename == "trash") { - // If we've dragged something onto the trashcan, it's trash - move_to_trash(wnd, dragging_fileview.filename); - } - else if (get_path(wnd) == "/" && 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(get_path(wnd), fileview.filename), dragging_fileview.filename); - } - } else { - // alert(`Dropped ${dst.filename} on ${src.filename}`); - } - end_drag(); - } - 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); -} - - -// Reads the 'pwd' of the focused window -// If pwd is ['foo', 'bar', 'baz'], this returns '/foo/bar/baz' -function get_path(wnd, max_length) { - if (!wnd) { - alert("YOU ARE NOT SUPPOSED TO SEE THIS. PLEASE COPY THE STACKTRACE FROM THE CONSOLE AND SEND IT TO THE NEAREST DEVELOPER"); - console.trace(); - wnd = focus; - } - if (max_length == undefined) { - max_length = wnd.pwd.length; - } - - var path = "/"; - for (let i = 0; i < max_length; i++) { - path += wnd.pwd[i]; - if (i != max_length - 1) - path += "/"; - } - return path; -} - -function path_combine(a, b) { - const last_char = a.slice(-1); - if (last_char == "/") - return a + b; - else - return a + "/" + b; -} - - -// When we click anywhere, remove the context menu -// The context menu itself has a onmousedown that prevents propagation so we can click its elements -document.body.onmousedown = (_e) => { - if (context_menu) { - context_menu.remove(); - context_menu = null; - } -} - -document.body.onmousemove = (e) => { - if (dragging) { - dragging.style.left = (e.clientX - dragging_offset_x) + "px"; - dragging.style.top = (e.clientY + dragging_offset_y) + "px"; - } - else if (dragging_candidate) { - var d = Math.abs(e.clientX - dragging_candidate_x) + Math.abs(e.clientY - dragging_candidate_y); - if (d > 15) - begin_drag(e, dragging_candidate, true); - } -} - -document.body.onmouseup = (_e) => { - if (dragging_candidate) - dragging_candidate = null; - if (dragging) - end_drag(); -} - -document.body.oncontextmenu = (e) => { - if (dragging) { - end_drag(); - e.preventDefault(); - } - if (context_menu) { - context_menu.remove(); - context_menu = null; - } -} - -the_file.onchange = (e) => { on_file_added(e); }; - -function set_iframe_enabled(en) { - for (const iframe of document.getElementsByTagName('iframe')) - iframe.hidden = !en; -} - -main(); diff --git a/loggedin.php b/loggedin.php index e6e904a..0f3c085 100644 --- a/loggedin.php +++ b/loggedin.php @@ -9,4 +9,4 @@ - + diff --git a/loginregister.php b/loginregister.php index 8068b35..1a53211 100644 --- a/loginregister.php +++ b/loginregister.php @@ -1,6 +1,6 @@
- +
diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..38d959d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,14 @@ +{ + "name": "htdocs", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "typescript": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", + "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a93c1c8 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "htdocs", + "version": "1.0.0", + "description": "FILEUP", + "main": "actions.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/alexvitkov/india.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/alexvitkov/india/issues" + }, + "homepage": "https://github.com/alexvitkov/india#readme", + "devDependencies": { + "typescript": "^4.2.3" + } +} diff --git a/php/file_type_recogniser.php b/php/file_type_recogniser.php index d8e1d25..88122e6 100644 --- a/php/file_type_recogniser.php +++ b/php/file_type_recogniser.php @@ -71,7 +71,7 @@ function file_extension($path_to_file) { function get_icon($path_to_file) { - $file_ext="svg/icons/".file_extension($path_to_file).".svg"; + $file_ext="img/icons/".file_extension($path_to_file).".svg"; if(!file_exists($file_ext)) { return "svg/icons/.dat.svg"; diff --git a/share_frontend.php b/share_frontend.php index e196151..c8f4a9d 100644 --- a/share_frontend.php +++ b/share_frontend.php @@ -6,8 +6,8 @@ - shady file upload - shady file upload + shady file upload + diff --git a/style.css b/style.css new file mode 100644 index 0000000..a7a1762 --- /dev/null +++ b/style.css @@ -0,0 +1,513 @@ + +html, body { + margin: 0; + height: 100%; +} + +body { + background: #f0f0f0; + color: black; + font-family: Roboto, sans-serif; + overflow: hidden; +} + +.noselect { + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +#page { + display: flex; + flex: 1 0 0; + align-items: stretch; + + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +#page > * { + flex: 1 0 0; +} + +/* Disable the black outline that Chromium browsers put on focused buttons */ +input:focus, +button:focus +{ + outline:0; +} + +#header { + background: white; + margin: 0; + padding: 0; + height: 3rem; + font-size: 2em; + display: flex; + align-items: stretch; + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +#topmenu { + font-size: 1.3rem; + list-style-type: none; + display: flex; + margin: 0; + padding: 0; +} + +#topmenu > li { + cursor: pointer; + padding: 10px; +} + +#topmenu > li:hover { + background: #eee; +} + +.logo { + font-family: monospace; + margin: 0; + align-self: center; + padding-left: 2rem; +} + +#hero { + flex: 1.5 0 0; + position: relative; + font-size: 3.5rem; +} + +.vcenter { + display: flex; + flex-direction: column; + justify-content: center; +} + +#hero .bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -5; +} + +#hero > * > p { + color: #676d75; + margin: 0rem; + padding: 0.5rem; + white-space: nowrap; + text-align: center; +} + +#hero .big { + font-size: 1.2em; +} + +.blue { + color: #231179; + font-weight: bold; +} + +form { + background: #fbfbfb; + margin: 4.5rem; + padding-top: 0; + box-shadow: 0 0.8rem 1.3rem rgba(0,0,0,0.2); + border-radius: 0.5rem; + border: 1px solid #b9b9b9; +} + +.vert { + justify-content: center; + display: flex; + flex-direction: column; +} + +.vert2 { + display: flex; + flex-direction: column; +} + +.overlay { + display: grid; +} + +.overlay > * { + grid-row: 1; + grid-column: 1; +} + +form > .content { + margin: 2rem; + margin-top: 1rem; + margin-bottom: 1.7rem; + padding: 0; + display: flex; + flex-direction: column; + justify-content: center; +} + +form > h2 { + color: #4d4d4d; + margin: 0; + text-align: center; + padding: 1rem; + background: #f0f0f0; + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; + align-content: center; +} + + +.window h3 button, +.window > h2 button +{ + border: none; + padding: 0.3rem 1rem; + background: inherit; + border-radius: 0; +} + +.window h3 button:not(.pathentry):hover { + background: white; +} + +.window h3 .separator { + flex: 0 0 1px; + align-self: stretch; + background: #bbb; +} + +.window > h2 > *:first-child { + border-top-left-radius: 0.5rem; +} +form p { + margin: 1rem 0px 0.3rem 0px; +} + +.hero_form_error { + animation: fadein 0.2s; + background-color: #ff4d4d; + color: #ffffff; + padding-left: 0.5rem; + border-bottom-left-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; + margin-top: -0.2rem; + +} +@keyframes fadein { + from { opacity: 0; } + to { opacity: 1; } +} + +.content > input { + min-width: 300px; +} + +button, +input:not([type=file]) +{ + border: 1px solid #bbb; + padding: 0.5rem 0.8rem; + font-size: inherit; + font-family: inherit; + border-radius: 0.3rem; + background: #fcfcfc; +} + +input[type=button], button, input[type=submit] { + cursor: pointer; +} + +input[type=submit] { + margin-top: 2rem; + width: 100%; + display: block; + padding: 0.7rem; + font-size: 1.1em; + box-shadow: 0 0.2rem 0.6rem #eee; + background: #231179; + color: white; + outline: none; +} + + +input:hover, +button:hover +{ + background: white; +} + +input:focus { + border-color: black; +} + +input[type=submit]:hover { + background: #5b4d9d; +} + +.bgbottom { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + z-index: -100; +} + + +#arrows { + position: relative; +} + +#arrows > img { + position: absolute; + bottom: 0; + z-index: -200; +} + +#signupform { + display: none; +} + +.window { + overflow: hidden; + box-sizing: border-box; + margin: 0rem; + padding: 0; + box-shadow: 0 0.8rem 1.3rem rgba(0,0,0,0.2); + border-radius: 0.5rem; + border: 1px solid #b9b9b9; + + display: block; + position: absolute; + top: 0; + left: 0; + width: 800px; + height: 600px; + + transition: opacity 0.3s; + display: flex; + flex-direction: column; +} + +.window.focus { + border-color: black; +} + +.foldercontents, .filecontents, .filecontentsroot { + background: rgba(250, 250, 250, .9); + flex: 1 0 0; +} + +.filecontents, .filecontentsroot { + background: white; +} + +.filecontents { + font-family: monospace; +} + +.filecontents.imgview { + background-color: black; + background-repeat: no-repeat; + background-position: center; + background-size: contain; +} + +.filecontentsroot, .foldercontents { + display: flex; + flex-direction: column; +} + +.filecontents { + overflow-y: scroll; +} +[contenteditable] { + outline: 0px solid transparent; +} + +pre { + font-size: 1.3rem; + min-height: 100%; + margin: 0.3rem; + box-sizing: border-box; +} + +.window h3, +.window > h2 { + color: #4d4d4d; + background: #f0f0f0; + margin: 0; + + + display: flex; + align-items: stretch; + font-weight: normal; + padding: 0rem; + border-bottom: 1px solid #bbb; +} + +.window > h2 { + font-size: 1.3rem; + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; + cursor: grab; + + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +.window h3 { + font-size: 1.2rem; +} + +.window.dragged { + /* opacity: 0.9; */ +} + +.files { + padding: 0.3rem; + display: grid; + + grid-gap: 0rem; + grid-auto-rows: 8rem; + grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); +} + +.file { + box-sizing: border-box; + padding: 0.5rem; + cursor: pointer; + color: #333; + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + + display: flex; + flex-direction: column; + align-items: center; + border: 1px solid rgba(0,0,0,0); + border-radius: 0.1rem; +} + +.file.dragged { + border: none; + pointer-events: none; +} + +.file:hover:not(.dragged) { + background: rgba(255,255,255, 0.5); + color: black; + border-color: #ddd; +} + +.file > img { + flex: 1 1 0; + min-width: 0; + min-height: 0; +} + +.file:hover > img { + filter: brightness(120%); +} + +.path { + display: flex; + align-items: stretch; + flex: 1 0 0; +} + +.path > .separator { + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + padding: 0 0.1rem; + align-self: center; +} + +.pathentry:last-child { + cursor: inherit; +} + +.pathentry:not(:last-child):hover { + text-decoration: underline; +} + +.context { + background: white; + position: absolute; + z-index: 1000; + list-style-type: none; + margin: 0; + padding: 0; + top: 0; + left: 0; + + border: 1px solid #ccc; + box-shadow: 0 0.3rem 0.5rem rgba(0,0,0,0.2); +} + +.context > li { + padding: 0.4rem 1.5rem; + margin: 0; + + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +.context > li:hover { + background: #2e91db; + color: white; +} + +.backdrop { + position: absolute; + width: 100%; + height: 100%; + + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + background-image: url("/img/backdrop.jpg"); + background-size: cover; +} + +.share_dialog_contents { + display: flex; + flex-direction: column; + background: white; + background: rgba(255,255,255, 0.9); +} + +.window > h2 .close_button { + border-left: 1px solid #ccc; +} + +.close_button:hover { + background: white; +} + + +.wndtitle { + display: flex; + align-items: center; + flex: 1 1 0; + padding-left: 0.8rem; +} + +iframe { + margin: 0; + border: 0; +} + +.filename { + word-wrap: break-word; + word-break: break-word; +} diff --git a/svg/arrow.svg b/svg/arrow.svg deleted file mode 100644 index 040182d..0000000 --- a/svg/arrow.svg +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/svg/bottom.svg b/svg/bottom.svg deleted file mode 100644 index 772081e..0000000 --- a/svg/bottom.svg +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - image/svg+xml - - - - - - - - - - diff --git a/svg/icons/.txt.svg b/svg/icons/.txt.svg deleted file mode 100644 index db35ac4..0000000 --- a/svg/icons/.txt.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - image/svg+xml - - - - - - - txt - - diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5d197c8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "rootDir": "front", + "outFile": "build/bundle.js" + } +} \ No newline at end of file -- cgit v1.2.3