aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--build.ps13
-rw-r--r--front/contextmenu.ts51
-rw-r--r--front/dragging.ts123
-rw-r--r--front/explorer.ts624
-rw-r--r--front/main.ts1171
-rw-r--r--front/share_window.ts121
-rw-r--r--front/upload_form.ts66
-rw-r--r--front/util.ts105
-rw-r--r--front/window.ts104
-rw-r--r--mimeicons/image-gif.pngbin0 -> 409 bytes
-rw-r--r--php/move.php25
-rw-r--r--style.css6
-rw-r--r--tsconfig.json3
14 files changed, 1225 insertions, 1178 deletions
diff --git a/README.md b/README.md
index 1900d71..a9f63ee 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@ File upload service that just about works most of the time
- Make sure php/configuration.php is pointing to a sane database.
- Execute INIT_DATABASE.sql on that database
+- make sure the directory $storage_root exists and PHP has write access to it
- Build the JavaScript
## Building the JavaScript
diff --git a/build.ps1 b/build.ps1
index 4506966..9cdd2e6 100644
--- a/build.ps1
+++ b/build.ps1
@@ -1,5 +1,6 @@
-if (-Not Test-Path node_modules)
+if (-Not (Test-Path node_modules)) {
npm install
+}
# Clear the old build
Remove-Item -LiteralPath "temp" -Force -Recurse | out-null
diff --git a/front/contextmenu.ts b/front/contextmenu.ts
new file mode 100644
index 0000000..b5a6c3c
--- /dev/null
+++ b/front/contextmenu.ts
@@ -0,0 +1,51 @@
+import { mk } from './util';
+
+// Some elements have custom right click context menus
+// If there's a custom context menu active, this will be it
+var context_menu = null;
+
+// Create a right click context menu
+export 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);
+ }
+}
+
+// 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;
+ }
+}
+
+
+
+export function oncontextmenu_hook(_e) {
+ if (context_menu) {
+ context_menu.remove();
+ context_menu = null;
+ }
+} \ No newline at end of file
diff --git a/front/dragging.ts b/front/dragging.ts
new file mode 100644
index 0000000..f01e414
--- /dev/null
+++ b/front/dragging.ts
@@ -0,0 +1,123 @@
+import { FileView } from './explorer';
+
+// 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
+export var dragging = null;
+export 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
+export var dragging_candidate: HTMLElement = null;
+export 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
+export var dragging_fileview: FileView;
+export var dragging_placeholder = null;
+
+export function set_dragging_candidate(e, candidate: HTMLElement) {
+ dragging_candidate = candidate;
+ dragging_candidate_x = e.clientX;
+ dragging_candidate_y = e.clientY;
+}
+
+
+// Start dragging the 'obj' DOM element
+// e is a DOM event, this should only get called in response of a DOM event
+export 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);
+}
+
+export 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;
+}
+
+function set_iframe_enabled(en) {
+ const frames = document.getElementsByTagName('iframe');
+ for (var i = 0; i < frames.length; i++)
+ frames.item(i).hidden = !en;
+}
+
+document.body.onmouseup = (_e) => {
+ if (dragging_candidate)
+ dragging_candidate = null;
+ if (dragging)
+ end_drag(_e);
+}
+
+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);
+ }
+}
+// Dragging a fileview is a bit different from dragging a window
+// This does some setup work before calling the common begin_drag
+export 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);
+}
+
+export function oncontextmenu_hook(e) {
+ if (dragging) {
+ end_drag(e);
+ e.preventDefault();
+ }
+} \ No newline at end of file
diff --git a/front/explorer.ts b/front/explorer.ts
new file mode 100644
index 0000000..3a488ee
--- /dev/null
+++ b/front/explorer.ts
@@ -0,0 +1,624 @@
+import { BaseWindow } from './window'
+import { mk, path_combine, base64ArrayBuffer } from './util';
+import { show_upload_dialog, show_upload_dialog_replace_file } from './upload_form';
+import { dragging, dragging_fileview, begin_drag_fileview, end_drag } from './dragging'
+import { ShareWindow } from './share_window';
+import { context } from './contextmenu';
+
+declare var actions: any;
+
+export class ExplorerWindow extends BaseWindow {
+ foldercontents: any;
+ filecontents: any;
+ filecontentsroot: any;
+ filegrid: any;
+ save_btn_container: any;
+
+ opened_file: FileView;
+
+ constructor(pwd, x, y, w, h, has_close) {
+ super(pwd, x, y, w, h);
+ make_window(this, has_close);
+ }
+
+ openfile(is_directory) {
+ if (is_directory) {
+ openfile_dir(this);
+ } else {
+ openfile_nondir(this);
+ }
+ }
+}
+
+// A FileView is an entry inside the explorer window
+export class FileView {
+ filename: string;
+ wnd: ExplorerWindow;
+ visuals: HTMLElement;
+ mimetype: string;
+ is_directory: boolean;
+ write_permissions: boolean;
+
+ constructor(filename, wnd, mimetype, is_directory, write_permissions) {
+ this.filename = filename;
+ this.wnd = wnd;
+ this.visuals = null; // The DOM object with the icon and the filenam text
+ this.mimetype = mimetype;
+ this.is_directory = is_directory;
+ this.write_permissions = write_permissions;
+ }
+
+ full_path() {
+ return path_combine(this.wnd.get_path(), this.filename);
+ }
+}
+
+// make_window creates an explorer window - the kind that can list directories/open files
+function make_window(wnd, has_close: boolean): BaseWindow {
+ mk(wnd.h2, 'div', 'path');
+
+ if (has_close) {
+ var x_button = mk(wnd.h2, 'button', 'close_button');
+ x_button.innerText = "X";
+ x_button.onclick = () => { wnd.destroy(); };
+ }
+
+ // wnd.foldercontents is where the FileViews will be stored
+ // it also has a subheader (h3) with 'Upload' and 'New FOlder' buttons
+ {
+ wnd.foldercontents = mk(wnd.visuals, 'div', 'foldercontents');
+ var h3 = mk(wnd.foldercontents, 'h3');
+
+ var upload_btn = mk(h3, 'button');
+ upload_btn.innerText = "Upload";
+
+ upload_btn.onclick = () => {
+ show_upload_dialog();
+ }
+
+ mk(h3, 'div', 'separator');
+
+ var new_folder_btn = mk(h3, 'button');
+ new_folder_btn.innerText = "New Folder";
+ new_folder_btn.onclick = () => { new_folder(wnd); }
+
+ mk(h3, 'div', 'separator');
+
+ wnd.filegrid = mk(wnd.foldercontents, 'div', 'files');
+ }
+
+ // wnd.filecontentsroot is where the filedata will be stored for open files
+ // it also has a subheader (h3) with Share and Download buttons
+ {
+ wnd.filecontentsroot = mk(wnd.visuals, 'div', 'filecontentsroot');
+ var h3 = mk(wnd.filecontentsroot, 'h3');
+
+ let download_btn = mk(h3, 'button');
+ download_btn.innerText = "Download";
+ download_btn.onclick = () => { download_file(true); }
+ mk(h3, 'div', 'separator');
+
+ let share_btn = mk(h3, 'button');
+ share_btn.innerText = "Share";
+ share_btn.onclick = () => { alert("TODO NOT IMPLEMENTETD"); } //share(true, fileview.filename, wnd); }
+ mk(h3, 'div', 'separator');
+
+ wnd.save_btn_container = mk(h3, 'div');
+ wnd.save_btn_container.style.display = 'flex';
+
+ let save_btn = mk(wnd.save_btn_container, 'button');
+ save_btn.innerText = "Save";
+ save_btn.onclick = () => save_open_text_file(wnd);
+ mk(wnd.save_btn_container, 'div', 'separator');
+
+ wnd.filecontents = mk(wnd.filecontentsroot, 'div', 'filecontents');
+ }
+
+ return wnd;
+}
+
+// Create the visuals for a FileView
+function add_file_visuals(fileview: FileView, wnd: ExplorerWindow) {
+ // Are we in a subdirectory of the trash folder?
+ var is_in_trash = wnd.pwd.length > 0 && wnd.pwd[0] == "trash";
+
+ // Is the current filewview the trash folder itself?
+ var is_trash = wnd.pwd.length == 0 && fileview.filename == "trash";
+ var is_share = wnd.pwd.length == 0 && fileview.filename == "share";
+
+ var visuals = mk(wnd.filegrid, 'div');
+ fileview.visuals = visuals;
+
+ var img = document.createElement('img');
+ var filename = document.createElement('div');
+
+ if (fileview.is_directory) {
+ if (wnd.get_path() == "/" && fileview.filename == "trash")
+ img.src = "/mimeicons/user-trash.png";
+ else if (wnd.get_path() == "/" && fileview.filename == "share")
+ img.src = "/mimeicons/user-share.png";
+ else
+ img.src = "/mimeicons/directory.png";
+ } else {
+ img.src = `/mimeicons/${fileview.mimetype.replace("/", "-")}.png`;
+ }
+
+ fileview.visuals.onclick = () => {
+ wnd.pwd.push(fileview.filename);
+ if (!fileview.is_directory) {
+ wnd.opened_file = fileview;
+ }
+ wnd.openfile(fileview.is_directory);
+ }
+
+ visuals.oncontextmenu = (e) => {
+ if (!dragging) {
+
+ var context_list = [
+ // Open is always in the context list
+ ['Open', () => {
+ wnd.pwd.push(fileview.filename);
+ wnd.openfile(fileview.is_directory);
+ }],
+ ['Open in New Window', () => {
+ var new_pwd = wnd.pwd.slice();
+ new_pwd.push(fileview.filename);
+ var new_wnd = new ExplorerWindow(new_pwd, 100, 100, 800, 600, true);
+ new_wnd.opened_file = fileview;
+ new_wnd.openfile(fileview.is_directory);
+ new_wnd.focus();
+ }],
+ ];
+
+ if (is_in_trash) {
+ // If we're in the trash, we can restore files or delete them forever
+ context_list.push(['Restore', () => { restore_from_trash(wnd, fileview.filename); }]);
+ context_list.push(['Delete forever', () => { delete_file(wnd, fileview.filename); }]);
+ } else if (!is_trash && !is_share) {
+ // If we;'re not in trash we can rename/share/download/move files to trash
+ context_list.push(
+ ['Rename', () => { rename_file(fileview.filename, wnd); }],
+ );
+ if (!fileview.is_directory) {
+ for (let a of actions) {
+ if (fileview.filename.endsWith(a.extension)) {
+ context_list.push(
+ [a.text, () => {
+ read_file_contents(true, (x) => {
+ const ue = encodeURIComponent(x);
+ let url = a.url.replace("$content_urlencoded", ue)
+ .replace("$filename", fileview.filename);
+
+ if (a.open_in_iframe) {
+ const wnd = new BaseWindow([], 10, 10, 800, 600);
+
+ var title = mk(wnd.h2, 'span', 'wndtitle');
+ title.innerText = fileview.filename;
+
+ // Close button
+ var x_button = mk(wnd.h2, 'button', 'close_button');
+ x_button.innerText = "X";
+ x_button.onclick = () => { wnd.destroy(); };
+
+ const contents = mk(wnd.visuals, 'div', 'filecontentsroot');
+ const iframe = mk(contents, 'iframe') as HTMLIFrameElement;
+ iframe.style.flex = '1 0 0';
+ iframe.src = url;
+
+ wnd.focus();
+ } else {
+ window.location = url;
+ }
+ }, wnd.get_path(), fileview.filename);
+ }]
+ );
+ }
+ }
+
+ if (fileview.write_permissions) {
+ context_list.push(
+ ['Replace', () => { replace_file(false, fileview.filename, wnd); }],
+ );
+ }
+ context_list.push(
+ ['Share', () => { share(false, fileview.filename, wnd); }],
+ ['Download', () => { download_file(false, fileview.filename); }],
+ );
+ }
+ context_list.push(
+ ['Delete', () => { move_to_trash(wnd, fileview.filename); }]
+ );
+ }
+
+ context(e, context_list);
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ visuals.ondragstart = (e) => {
+ if (is_trash || is_in_trash || is_share) {
+ e.preventDefault();
+ return;
+ }
+ begin_drag_fileview(e, fileview);
+ e.preventDefault();
+ };
+
+ visuals.onmouseup = (e) => {
+ if (dragging) {
+ if (fileview.is_directory) {
+ if (wnd.get_path() == "/" && fileview.filename == "trash") {
+ // If we've dragged something onto the trashcan, it's trash
+ move_to_trash(wnd, dragging_fileview.filename);
+ }
+ else if (wnd.get_path() == "/" && fileview.filename == "share") {
+ // move to 'share' is invalid
+ } else {
+ // If we've dragged something onto a directory, move it into that directory
+ move_file(dragging_fileview.wnd, wnd, path_combine(wnd.get_path(), fileview.filename), dragging_fileview.filename);
+ }
+ } else {
+ // alert(`Dropped ${dst.filename} on ${src.filename}`);
+ }
+ end_drag(e);
+ }
+ e.preventDefault();
+ };
+
+ visuals.classList.add('file');
+ filename.classList.add('filename');
+
+ if (is_in_trash) {
+ var split = fileview.filename.split("/");
+ filename.innerText = split[split.length - 1];
+ } else if (is_trash) {
+ filename.innerText = "Trash";
+ } else if (is_share) {
+ var x = mk(filename, 'span');
+ x.style.fontSize = "0.8rem";
+ x.innerText = "Shared with me";
+ } else {
+ filename.innerText = fileview.filename;
+ }
+
+ visuals.appendChild(img);
+ visuals.appendChild(filename);
+}
+
+function openfile_nondir(wnd: ExplorerWindow) {
+
+ while (wnd.filecontents.children.length > 0)
+ wnd.filecontents.removeChild(wnd.filecontents.lastChild);
+
+ // Send a request to readfile.php, which will give us the contents
+ var data = new FormData();
+ data.append('folder', wnd.get_path(wnd.pwd.length - 1));
+ data.append('filename', wnd.pwd[wnd.pwd.length - 1]);
+
+ var xhr = new XMLHttpRequest();
+
+ update_path_visuals(wnd);
+
+ xhr.open('POST', '/php/readfile.php', true);
+
+ wnd.filecontents.innerText = "";
+ wnd.filecontentsroot.style.display = 'flex';
+ wnd.foldercontents.style.display = 'none';
+
+ let is_image = wnd.opened_file.mimetype.split("/")[0] == "image";
+ wnd.save_btn_container.style.display = (wnd.opened_file.write_permissions && !is_image) ? "flex" : "none";
+
+ if (is_image) {
+ xhr.responseType = 'arraybuffer';
+ xhr.onload = function () {
+ let b = `data:image/png;base64,${base64ArrayBuffer(xhr.response)}`;
+ wnd.filecontents.style.backgroundImage = `url('${b}')`;
+ wnd.filecontents.classList.add('imgview');
+ }
+ }
+ else {
+ wnd.filecontents.classList.remove('imgview');
+ wnd.filecontents.style.backgroundImage = "unset";
+
+ wnd.txt_editor = mk(wnd.filecontents, 'pre');
+
+ xhr.onload = function () {
+ wnd.txt_editor.innerText = xhr.responseText;
+ if (wnd.opened_file.write_permissions)
+ wnd.txt_editor.contentEditable = "true";
+ };
+ }
+
+ xhr.send(data);
+}
+
+// This loads the contents of the current directory
+function openfile_dir(wnd) {
+ update_path_visuals(wnd);
+
+ var data = new FormData();
+ data.append('path', wnd.get_path());
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('POST', '/php/readdir.php', true);
+ xhr.onload = function () {
+ for (const f of wnd.files)
+ f.visuals.remove();
+ wnd.files = [];
+
+ var json = JSON.parse(xhr.responseText);
+ if (!json)
+ return;
+
+ // Create the FileViews from the json response
+ for (const f of json) {
+ var view = new FileView(f.name,
+ wnd,
+ f.mimetype,
+ f.is_directory && f.is_directory != "0",
+ f.can_edit && f.can_edit != "0");
+ wnd.files.push(view);
+ }
+
+ // Sort the files nicely before adding their visuals
+ // Folders come first, then files, then the special trash directory
+ // Everything inside the categories is lexically sorted
+ wnd.files.sort((a, b) => {
+ if (wnd.pwd.length == 0 && a.filename == "share")
+ return -10;
+ if (wnd.pwd.length == 0 && b.filename == "share")
+ return 10;
+
+ if (wnd.pwd.length == 0 && a.filename == "trash")
+ return 10;
+ if (wnd.pwd.length == 0 && b.filename == "trash")
+ return -10;
+ if (a.is_directory && !b.is_directory)
+ return -1;
+ if (!a.is_directory && b.is_directory)
+ return 1;
+ return a.filename.localeCompare(b.filename);
+ });
+
+ for (const f of wnd.files) {
+ add_file_visuals(f, wnd);
+ }
+
+ };
+ xhr.send(data);
+
+ wnd.filecontentsroot.style.display = 'none';
+ wnd.foldercontents.style.display = 'flex';
+
+ wnd.foldercontents.onmouseup = () => {
+ if (dragging && dragging_fileview) {
+ move_file(dragging_fileview.wnd, wnd, wnd.get_path(), dragging_fileview.filename);
+ }
+ }
+}
+
+
+
+function move_to_trash(wnd, filename) {
+ move_file(wnd, wnd, "/trash", filename, path_combine(wnd.get_path(), filename));
+}
+
+function restore_from_trash(wnd, filename) {
+ var split = filename.split("/");
+ var new_filename = split.pop();
+ var new_directory = "/" + split.join("/");
+
+ move_file(wnd, wnd, new_directory, filename, new_filename);
+}
+
+
+// This deletes the file, *for real*
+// move_to_trash is what is actually called when the user clicks 'Delete'
+function delete_file(wnd, filename) {
+ var data = new FormData();
+ data.append('folder', wnd.get_path());
+ data.append('filename', filename);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('POST', '/php/delete.php', true);
+ xhr.onload = function () {
+ wnd.openfile(true);
+ };
+ xhr.send(data);
+}
+
+function rename_file(filename, wnd) {
+ var new_name = prompt(`Rename ${filename} to`, filename);
+ if (!new_name)
+ return;
+
+ var data = new FormData();
+ data.append('folder', wnd.get_path());
+ data.append('old_filename', filename);
+ data.append('new_filename', new_name);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('POST', '/php/rename.php', true);
+ xhr.onload = function () {
+ wnd.openfile(true);
+ };
+ xhr.send(data);
+}
+
+function move_file(srcwnd: ExplorerWindow, dstwnd: ExplorerWindow, new_folder: string, filename: string, new_filename?: string) {
+ if (!new_filename)
+ new_filename = filename;
+
+ var data = new FormData();
+ data.append('old_folder', srcwnd.get_path());
+ data.append('new_folder', new_folder);
+ data.append('filename', filename);
+ data.append('new_filename',new_filename);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('POST', '/php/move.php', true);
+ xhr.onload = () => {
+ srcwnd.openfile(true);
+ dstwnd.openfile(true);
+ };
+ xhr.send(data);
+}
+
+function new_folder(wnd) {
+ var dirname = prompt(`Directory name`, "New Folder");
+ if (!dirname)
+ return;
+
+ var data = new FormData();
+ data.append('parent_directory', wnd.get_path());
+ data.append('dirname', dirname);
+
+ var xhr = new XMLHttpRequest();
+ xhr.open('POST', '/php/mkdir.php', true);
+ xhr.onload = function () {
+ wnd.openfile(true);
+ };
+ xhr.send(data);
+}
+
+
+// Replace an existing file with a new one
+function replace_file(in_file, filename, wnd) {
+ if (in_file) {
+ var folder = wnd.get_path(wnd.pwd.length - 1);
+ filename = wnd.pwd[wnd.pwd.length - 1];
+ } else {
+ var folder = wnd.get_path();
+ }
+
+ show_upload_dialog_replace_file(folder, filename);
+}
+
+// This updates the path of the window's DOM (the "Root > Folder1 > Folder2 > foo.png")
+function update_path_visuals(wnd) {
+ var the_path = wnd.visuals.getElementsByClassName('path')[0];
+
+ // Remove the old path
+ while (the_path.children.length > 0)
+ the_path.removeChild(the_path.lastChild);
+
+ for (let i = -1; i < wnd.pwd.length; i++) {
+ var d;
+ // For each element after the first create a separator
+ if (i >= 0) {
+ d = wnd.pwd[i];
+ var separator_div = mk(the_path, 'div', 'separator');
+ separator_div.innerText = "ยป";
+ }
+ else
+ d = "Root";
+
+ var entry = mk(the_path, 'button', 'pathentry');
+ entry.innerText = d;
+
+ // When we click the entry, go to its folder
+ entry.onclick = (_e) => {
+ if (length < wnd.pwd.length) {
+ wnd.pwd.length = i + 1;
+ openfile_dir(wnd);
+ }
+ }
+
+ // We can drop files onto the path, which will omve them to teh folder
+ entry.onmouseup = (e) => {
+ if (dragging && dragging_fileview) {
+ var new_folder = wnd.get_path(i + 1);
+ move_file(dragging_fileview.wnd, wnd, new_folder, dragging_fileview.filename);
+ end_drag(e);
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+ }
+}
+
+function download_file(in_file, filename?: any, wnd?: any) {
+ if (in_file) {
+ var folder = wnd.get_path(wnd.pwd.length - 1);
+ filename = wnd.pwd[wnd.pwd.length - 1];
+ } else {
+ var folder = wnd.get_path();
+ }
+
+ // Read the file contents and then do DISGUSTING javascript things to download the ifle
+ // We create a invisible <a> that we click and then delete
+ // That <a> has its download attribute set so we download the contents instead of opening it in a new tab
+ // and of course its href is a virtual object URL that has its content set to a blob
+ read_file_contents(false, (x) => {
+ var blob = new Blob([new Uint8Array(x, 0, x.length)]);
+ var url = URL.createObjectURL(blob);
+ var a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(() => {
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ });
+ }, folder, filename);
+
+ return;
+}
+
+function save_open_text_file(wnd) {
+ const contents = wnd.txt_editor.innerText;
+ let xhr = new XMLHttpRequest();
+ xhr.open('POST', '/php/upload.php', true);
+
+ var data = new FormData();
+ data.append('parent_directory', wnd.get_path(wnd.pwd.length - 1));
+ data.append('filename', wnd.pwd[wnd.pwd.length - 1]);
+ data.append('content', contents);
+ data.append('overwrite', '1');
+
+ xhr.send(data);
+}
+
+// This asks the server for the contents of the specified file
+// The 'cb' callback is then called, which gives you the file as either text or binary
+// depending on whether or not text is true/false
+function read_file_contents(text, cb, folder, filename) {
+ var data = new FormData();
+ data.append('folder', folder);
+ data.append('filename', filename);
+
+ let xhr = new XMLHttpRequest();
+ xhr.open('POST', '/php/readfile.php', true);
+
+ if (text) {
+ xhr.onload = function () {
+ cb(xhr.responseText);
+ };
+ } else {
+ xhr.responseType = 'arraybuffer';
+ xhr.onload = function () {
+ cb(xhr.response);
+ };
+ }
+
+ xhr.send(data);
+}
+
+// This opens a file.
+// If the file has image/* mimetype, it will be displayed as an image
+// otherwise it will be displayed as plaintext
+// This is a tiny wrapper around the share_window.
+function share(in_file: boolean, filename: string, wnd: BaseWindow) {
+ if (in_file) {
+ var folder = wnd.get_path(wnd.pwd.length - 1);
+ filename = wnd.pwd[wnd.pwd.length - 1];
+ } else {
+ var folder = wnd.get_path();
+ }
+
+ var sharewnd = new ShareWindow(folder, filename, 400, 400, 400);
+ sharewnd.focus();
+}
diff --git a/front/main.ts b/front/main.ts
index 1761597..9075dfc 100644
--- a/front/main.ts
+++ b/front/main.ts
@@ -1,1176 +1,21 @@
-// 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;
-
-import { OurWindow } from './window';
-
-declare var actions: any;
-
-// 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
-
-
-// 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;
-
+import { ExplorerWindow } from './explorer';
+import { oncontextmenu_hook as contextmenu_oncontextmenu_hook } from './contextmenu';
+import { oncontextmenu_hook as dragging_oncontextmenu_hook } from './dragging';
function main() {
// Create a window that looks at the root directory
- var root_window = make_window([], false);
+ var root_window = new ExplorerWindow([], 100, 100, 800, 600, 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 <input type="file">'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 <img src="...">
-// 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();
- }
- }
- }
-}
-
+ root_window.focus();
+ root_window.openfile(true);
-// 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 <a> that we click and then delete
- // That <a> has its download attribute set so we download the contents instead of opening it in a new tab
- // and of course its href is a virtual object URL that has its content set to a blob
- read_file_contents(false, (x) => {
- var blob = new Blob([new Uint8Array(x, 0, x.length)]);
- var url = URL.createObjectURL(blob);
- var a = document.createElement('a');
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- setTimeout(() => {
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- });
- }, folder, filename);
-
- return;
-}
-
-
-// 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) {
- const frames = document.getElementsByTagName('iframe');
- for (var i = 0; i < frames.length; i++)
- frames.item(i).hidden = !en;
+ contextmenu_oncontextmenu_hook(e);
+ dragging_oncontextmenu_hook(e);
}
main();
diff --git a/front/share_window.ts b/front/share_window.ts
new file mode 100644
index 0000000..b62cb63
--- /dev/null
+++ b/front/share_window.ts
@@ -0,0 +1,121 @@
+import { BaseWindow } from './window';
+import { mk, mkhdiv, mkcheckbox } from './util';
+
+export class ShareWindow extends BaseWindow {
+ contents: HTMLElement;
+ constructor(folder, filename, x, y, w) {
+ super(null, x, y, w, 0);
+ make_share_window(this, folder, filename);
+ }
+}
+
+// This monstrocity creates the 'Share file' window
+function make_share_window(wnd, folder, filename) {
+ 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 = () => { wnd.destroy(); }
+
+ wnd.contents = mk(wnd.visuals, 'div', 'share_dialog_contents');
+ wnd.contents.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.contents, "Private link", (toggled) => {
+ add_user.style.display = toggled ? "block" : "none";
+ userlist.style.display = toggled ? "block" : "none";
+ data.private = toggled;
+ });
+
+ userlist = mk(wnd.contents, 'div');
+ userlist.style.display = "none";
+ add_user = mk(wnd.contents, '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.contents, "Give write permissions", (toggled) => {
+ data.write_permissions = toggled;
+ });
+
+ // If 'Password protected' is checked, show the password field
+ let password_container;
+ mkcheckbox(wnd.contents, "Password protected", (toggled) => {
+ data.has_password = toggled;
+ password_container.style.display = toggled ? "flex" : "none";
+ });
+
+ password_container = mkhdiv(wnd.contents);
+ 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.contents, '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);
+ wnd.destroy();
+ }
+} \ No newline at end of file
diff --git a/front/upload_form.ts b/front/upload_form.ts
new file mode 100644
index 0000000..a2f402e
--- /dev/null
+++ b/front/upload_form.ts
@@ -0,0 +1,66 @@
+import { focused_window } from './window'
+import { ExplorerWindow } from './explorer'
+
+
+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 = "";
+
+export function show_upload_dialog() {
+ override_file = false;
+ the_file.click();
+}
+
+export function show_upload_dialog_replace_file(folder, filename) {
+ override_file = true;
+ override_file_path = folder;
+ override_file_filename = filename;
+ the_file.click();
+}
+
+
+// This is called whenever the <input type="file">'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 = focused_window.get_path();
+ }
+
+ // 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) {
+ // TODO the focused window may have changed
+ // Reload the directory so the user can see the newly uploaded file
+ (focused_window as ExplorerWindow).openfile(true);
+ } else {
+ alert("Upload failed");
+ }
+ }, () => {
+ alert("Upload failed")
+ });
+ }
+ else {
+ alert("No files selected");
+ }
+}
+
+the_file.onchange = (e) => { on_file_added(e); }; \ No newline at end of file
diff --git a/front/util.ts b/front/util.ts
new file mode 100644
index 0000000..ded0f18
--- /dev/null
+++ b/front/util.ts
@@ -0,0 +1,105 @@
+
+// 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
+export 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;
+}
+
+export function path_combine(a, b) {
+ const last_char = a.slice(-1);
+ if (last_char == "/")
+ return a + b;
+ else
+ return a + "/" + b;
+}
+
+// Create a checkbocx with a label.
+// togglefn will be called when its value changes with an argument that's either true/false
+export function mkcheckbox(parent, label, togglefn): HTMLElement {
+ 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);
+ };
+
+ return hdiv;
+}
+
+// Crate a horizontal div
+export function mkhdiv(parent: HTMLElement): HTMLDivElement {
+ var hdiv = mk(parent, 'div') as HTMLDivElement;
+ hdiv.style.display = "flex";
+ hdiv.style.alignItems = "center";
+ hdiv.style.padding = "0.3rem";
+ hdiv.style.gap = "0.3rem";
+ return hdiv;
+}
+
+// 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 <img src="...">
+// 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
+export 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
+} \ No newline at end of file
diff --git a/front/window.ts b/front/window.ts
index 79d053b..eb81095 100644
--- a/front/window.ts
+++ b/front/window.ts
@@ -1,5 +1,21 @@
+import { mk } from './util';
-export class OurWindow {
+// An array with all the windows on the screen
+export var windows: BaseWindow[] = [];
+
+// The focused window
+export var focused_window: BaseWindow = null;
+
+
+import * as drag from './dragging'
+
+
+// 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;
+
+
+export class BaseWindow {
pwd: string[];
visuals: HTMLElement;
h2: HTMLElement; // The titlebar of the window
@@ -7,19 +23,91 @@ export class OurWindow {
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) {
+ constructor(pwd, x, y, w, h) {
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
+
+ make_window_base(this, pwd, x, y, w, h);
+ }
+
+ destroy() {
+ var index = windows.indexOf(this);
+
+ if (index >= 0)
+ windows.splice(index, 1);
+
+ this.visuals.parentNode.removeChild(this.visuals);
+ if (this == focused_window)
+ focused_window = null;
+ }
+
+ focus() {
+ if (focused_window == this)
+ return;
+
+ // Unfocus the old window
+ if (focused_window)
+ focused_window.visuals.classList.remove('focus');
+
+ focused_window = this;
+ // And focus the new one!
+ this.visuals.classList.add('focus');
+ this.visuals.style.zIndex = (depth++).toString();
}
+
+ get_path(max_length?: number) {
+ if (max_length == undefined) {
+ max_length = this.pwd.length;
+ }
+
+ var path = "/";
+ for (let i = 0; i < max_length; i++) {
+ path += this.pwd[i];
+ if (i != max_length - 1)
+ path += "/";
+ }
+ return path;
+ }
+}
+
+
+
+export function unfocus_window() {
+ if (focused_window) {
+ focused_window.visuals.classList.remove('focus');
+ focused_window = 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(wnd, pwd, x, y, w, h) {
+ 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) => {
+ wnd.focus();
+ }
+
+ wnd.h2.onmousedown = (e) => {
+ if (!drag.dragging)
+ drag.set_dragging_candidate(e, wnd.visuals);
+ };
+
+ return wnd;
} \ No newline at end of file
diff --git a/mimeicons/image-gif.png b/mimeicons/image-gif.png
new file mode 100644
index 0000000..1d5a936
--- /dev/null
+++ b/mimeicons/image-gif.png
Binary files differ
diff --git a/php/move.php b/php/move.php
index 5b8df30..ab70adf 100644
--- a/php/move.php
+++ b/php/move.php
@@ -13,9 +13,10 @@ if (!isset($_POST['old_folder']) || !isset($_POST['new_folder']) || !isset($_POS
exit(1);
}
-/*filename as we want it to be in the directory*/
+// what the name will be in the new directory
$new_filename = $_POST["filename"];
-/*filename as it is in the directory*/
+
+// what the name WAS in the old directory
$old_filename = $_POST["filename"];
if (isset($_POST['new_filename']))
@@ -31,6 +32,26 @@ $new_dir = get_directory($new_folder, $user);
$trash_dir = get_directory("/trash",$user);
$share_dir = get_directory("/share",$user);
+function path_combine($a, $b) {
+ $last_char = substr($a, -1);
+ if ($last_char == "/")
+ return $a . $b;
+ else
+ return $a . "/" . $b;
+}
+
+// We cannot move the folder '/foo' inside '/foo/bar'
+{
+ $old_path = path_combine($old_folder, $old_filename);
+
+ if (substr($new_folder, 0, strlen($old_path)) == $old_path) {
+ error_log("trying to move a parent directory into a subdirectory");
+ http_response_code(409);
+ exit(0);
+ }
+}
+
+
if (!$old_dir || !$new_dir || ($old_dir==$user->home_directory && ($old_filename=="share" || $old_filename=="trash"))) {
error_log("invalid src/dst dir");
http_response_code(409);
diff --git a/style.css b/style.css
index a7a1762..81573bf 100644
--- a/style.css
+++ b/style.css
@@ -2,6 +2,8 @@
html, body {
margin: 0;
height: 100%;
+
+ --window-border-radius: 0.3rem;
}
body {
@@ -280,7 +282,7 @@ input[type=submit]:hover {
margin: 0rem;
padding: 0;
box-shadow: 0 0.8rem 1.3rem rgba(0,0,0,0.2);
- border-radius: 0.5rem;
+ border-radius: var(--window-border-radius);
border: 1px solid #b9b9b9;
display: block;
@@ -354,8 +356,6 @@ pre {
.window > h2 {
font-size: 1.3rem;
- border-top-left-radius: 0.5rem;
- border-top-right-radius: 0.5rem;
cursor: grab;
user-select: none;
diff --git a/tsconfig.json b/tsconfig.json
index 1737107..3a80209 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"rootDir": "front",
- "outDir": "temp"
+ "outDir": "temp",
+ "target": "ES6"
}
} \ No newline at end of file