diff --git a/pyproject.toml b/pyproject.toml index e950ea0..f2eebb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,13 +35,8 @@ dependencies = [ "Pygments >= 2.12.0", "xstatic < 2.0.0", "XStatic-asciinema-player <= 2.6.1.1", - "xstatic-bootbox >= 5.4.0, <= 5.5.1.1", - "xstatic-bootstrap >=4.0.0.0, <= 4.5.3.1", "xstatic-font-awesome >= 6.0.0, <= 6.2.1.1", - "xstatic-jquery <= 3.5.1.1", - "xstatic-jquery-ui <= 1.13.0.1", - "xstatic-jquery-file-upload <= 10.31.0.1", - "xstatic-pygments <= 2.9.0.1", + "xstatic-pygments <= 2.9.0.1" ] [project.scripts] diff --git a/src/bepasty/bepasty_xstatic.py b/src/bepasty/bepasty_xstatic.py index bdda38a..05e6e16 100644 --- a/src/bepasty/bepasty_xstatic.py +++ b/src/bepasty/bepasty_xstatic.py @@ -3,12 +3,7 @@ # The names below must be package names. mod_names = [ 'asciinema_player', - 'bootbox', - 'bootstrap', 'font_awesome', - 'jquery', - 'jquery_ui', - 'jquery_file_upload', 'pygments', ] diff --git a/src/bepasty/static/app/css/style.css b/src/bepasty/static/app/css/style.css index ca07c31..e63c1c3 100644 --- a/src/bepasty/static/app/css/style.css +++ b/src/bepasty/static/app/css/style.css @@ -8,12 +8,6 @@ body { margin: 0; -webkit-font-smoothing: antialiased; font-size: 100%; - background-color: #fff; -} - -#footer { - height: 30px; - position: relative; } #formupload { @@ -22,13 +16,9 @@ body { font-family: monospace; } -#files p { +#files a { word-break: break-all; } -#files p .break-word { - word-break: break-word; - font-weight: normal !important; -} #filelist { height: auto; @@ -36,16 +26,6 @@ body { font-family: monospace; } -.fileupload-abort { - float: right; - margin-left: 10px; -} - -.dropzone { - width: 100%; - padding: 3em; -} - #wrapper { min-height: 100%; margin: 0 auto -30px; @@ -56,33 +36,11 @@ body { padding: 0 15px 60px; } -.jumbotron .fa { - font-size: 80px; -} - .jumbotron { font-size: 100%; text-align: center; } -.alert { - padding: 5px 10px; -} - -.alert-processing { - color: #464646; - background-color: #f0f0f0; - border-color: #eeeeee; -} - -.alert-processing hr { - border-top-color: #e6e6e6; -} - -.alert-processing .alert-link { - color: #959595; -} - /* If data is too wide, show scrollbars */ .data { width: 100%; @@ -127,12 +85,39 @@ table.highlighttable { border-left: 1px solid #ccc; } +@media (prefers-color-scheme: dark) { + .line-highlight { + background-color: hsl(var(--bulma-warning-h), var(--bulma-warning-s), 15%); + } + table.highlighttable { + border: 1px solid var(--bulma-dark); + } + .highlight pre { + border-left: 1px solid var(--bulma-dark); + } +} + /* Set minimal width and let the contents push the block width */ .linenos { width: 1px; } -/* Autocomplete for modify dialog */ -.ui-autocomplete { - z-index: 1065; +.mr-auto { + margin-right: auto; +} + +.is-text-6-5 { + font-size: 0.85rem; +} + +#header .navbar { + --bulma-navbar-background-color: var(--bulma-light); +} +.has-text-deemph { + color: hsl(var(--bulma-scheme-h), var(--bulma-scheme-s), var(--bulma-text-l)); +} +@media (prefers-color-scheme:dark) { + #header .navbar { + --bulma-navbar-background-color: var(--bulma-dark); + } } diff --git a/src/bepasty/static/app/js/fileuploader.js b/src/bepasty/static/app/js/fileuploader.js index 927e727..f317df7 100644 --- a/src/bepasty/static/app/js/fileuploader.js +++ b/src/bepasty/static/app/js/fileuploader.js @@ -1,139 +1,169 @@ -jqXHR = {}; -$(function () { - 'use strict'; - - // Generate human-readable file size - function humansize (size) { - var suffix = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"], - tier = 0; - - while (size >= 1024) { - size = size / 1024; - tier++; - } +function humansize (size) { + const suffix = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"]; + let tier = 0; + + while (size >= 1024) { + size = size / 1024; + tier++; + } + + return Math.round(size * 10) / 10 + " " + suffix[tier]; +} + +function process(fieldName, file, metadata, load, error, progress, abort, transfer, options) { + const state = { aborted: false, xhr: null }; + let uploadUrl = null; + let displayUrl = null; + + const controller = new AbortController(); + + const doUpload = (url, name, offset) => { + if (state.aborted) return; + + const end = Math.min(offset + MAX_BODY_SIZE, file.size); + const xhr = new XMLHttpRequest(); + state.xhr = xhr; + + xhr.open("POST", url, true); + xhr.setRequestHeader("Content-Range", `bytes ${offset}-${end - 1}/${file.size}`); + + xhr.upload.onprogress = (e) => progress(true, offset + e.loaded, file.size); + + xhr.onload = () => { + state.xhr = null; + if (xhr.status < 200 || xhr.status >= 300) { + error(`Chunk upload failed: ${xhr.status}`); + return; + } + const result = JSON.parse(xhr.responseText); + if (result.files?.[0]?.url) displayUrl = result.files[0].url; + + if (end < file.size) { + doUpload(url, name, end); + } else { + load(displayUrl ?? url); + const filesDiv = document.getElementById("files"); + const alert = document.createElement("div"); + alert.className = "notification is-success is-light"; + alert.dataset.name = name; + const a = document.createElement("a"); + a.href = displayUrl ?? url; + a.target = "_blank"; + a.textContent = `${file.name} (${humansize(file.size)})`; + alert.appendChild(a); + filesDiv.appendChild(alert); + + const fileList = document.getElementById("filelist"); + fileList.textContent += name + "\n"; + const fileListForm = document.getElementById("filelist-form"); + fileListForm.style.display = ""; + + const previewBody = document.getElementById("tab-copy-preview-body"); + const inlineBody = document.getElementById("tab-copy-inline-body"); + const urlEl = document.createElement("span"); + urlEl.dataset.name = name; + urlEl.append(window.location + name, document.createElement("br")); + previewBody.appendChild(urlEl); + const urlInlineEl = document.createElement("span"); + urlInlineEl.dataset.name = name; + urlInlineEl.append(urlEl.innerText + "/+inline", document.createElement("br")); + inlineBody.appendChild(urlInlineEl); + } + }; + + xhr.onerror = () => { state.xhr = null; error("Network error"); }; + xhr.onabort = () => { state.xhr = null; }; + + const formData = new FormData(); + formData.append("text", ""); + formData.append("contenttype", file.type || "application/octet-stream"); + formData.append("filename", file.name); + formData.append("maxlife-value", document.querySelector("input[name=maxlife-value]").value); + formData.append("maxlife-unit", document.querySelector("select[name=maxlife-unit]").value); + formData.append("file", file.slice(offset, end), file.name); + + xhr.send(formData); + }; + + fetch(UPLOAD_NEW_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + signal: controller.signal, + body: JSON.stringify({ + filename: file.name, + size: file.size, + type: file.type, + maxlife_unit: document.querySelector("select[name=maxlife-unit]").value, + maxlife_value: document.querySelector("input[name=maxlife-value]").value + }) + }) + .then(res => { + if (!res.ok) throw new Error("Failed to initialise upload"); + return res.json(); + }) + .then(({ name, url }) => { + uploadUrl = url; + doUpload(url, name, 0); + }) + .catch(err => { + if (err.name !== "AbortError") error(err.message); + }); - return Math.round(size * 10) / 10 + " " + suffix[tier]; + return { + abort: () => { + state.aborted = true; + controller.abort(); + if (state.xhr) state.xhr.abort(); + if (uploadUrl) fetch(`${uploadUrl}/abort`).catch(() => {}); + abort(); } + }; +} + +function revert(uniqueFileId, load, error) { + const name = uniqueFileId.split("#")[0].slice(1) + fetch(`/${name}/+delete`, { method: "POST" }) + .then(res => { + if (!res.ok) throw new Error(`Delete failed: ${res.status}`); + + document.querySelectorAll(`#right-panel [data-name=${name}]`).forEach(v=>v.remove()); + + const fileList = document.getElementById("filelist"); + fileList.textContent = fileList.textContent + .split("\n") + .filter(n => n !== name) + .join("\n"); + + if (!fileList.textContent.trim()) { + document.getElementById("filelist-form").style.display = "none"; + } + + load(); + }) + .catch(err => error(err.message)); +} + +const fp = FilePond.create(document.getElementById("filepond"), { + chunkUploads: true, + allowMinimumUploadDuration: false, + server: { + fetch: null, + revert, + restore: null, + load: null, + remove: null, + process + } +}); - $('#fileupload') - .fileupload({ - dataType: 'json', - autoUpload: true, - singleFileUploads: true, - maxChunkSize: MAX_BODY_SIZE, - maxFileSize: MAX_ALLOWED_FILE_SIZE - }) - - .on('fileuploadadd', function (e, data) { }) - - .on('fileuploadsubmit', function (e, data) { - var $this = $(this); - var file = data.files[0] - // Create new item - $.ajax({ - type: 'POST', - url: UPLOAD_NEW_URL, - data: JSON.stringify({ - filename: file.name, - size: file.size, - type: file.type, - maxlife_unit: $("select[name=maxlife-unit] option:selected").val(), - maxlife_value: $("input[name=maxlife-value]").val() - }), - contentType: 'application/json', - success: function (result) { - data.url = result.url; - - data.context = $('
') - .appendTo('#files'); - - var abortButton = $('').text('abort'); - abortButton.appendTo(data.context); - - abortButton.click(function (e) { - jqXHR[result.name].abort(); - abortButton.css('display', 'none') - }); - - var fileItem = $('').text(file.name); - fileItem.append(' (' - + humansize(file.size) - + ')'); - fileItem.appendTo(data.context); - - var _jqXHR = $this.fileupload('send', data); - _jqXHR.error(function (jqXHR, textStatus, errorThrown) { - // Delete file-upload garbage on the server - $.ajax({ - type: 'GET', - url: data.url+'/abort' - }); - }); - jqXHR[result.name] = _jqXHR; - } - }); - return false; - }) - - .on('fileuploaddone', function (e, data) { - $(data.context) - .attr('class', 'alert alert-success'); - $.each(data.result.files, function (index, file) { - $(data.context[0].childNodes[1]) - .wrapInner($('') - .prop('href', file.url)); - $('#filelist').append(file.name + "\n"); - delete jqXHR[file.name]; - $('#' + file.name).css('display', 'none'); - }); - $('#filelist-form').show(); - }) - - .on('fileuploadfail', function (e, data) { - $(data.context) - .attr('class', 'alert alert-danger') - .append('Upload failed!
'); - var name = data.url.split('/').pop(); - delete jqXHR[name]; - }) - - .on('fileuploadprogressall', function (e, data) { - var progress = parseInt(data.loaded / data.total * 100, 10); - $('#fileupload-progress').find('.progress-bar').css('width', progress + '%'); - }) - - .on('fileuploadstart', function (e, data) { - $('#fileupload-progress').css('visibility', 'visible'); - $('#fileupload-abort').css('visibility', 'visible') - }) - - .on('fileuploadstop', function (e, data) { - var progressBar = $('#fileupload-progress') - progressBar.css('visibility', 'hidden'); - progressBar.find('.progress-bar').css('width', 0 + '%'); - $('#fileupload-abort').css('visibility', 'hidden'); - }) - - .on('fileuploadprocessfail', function (e, data) { - $(data.context) - .attr('class', 'alert alert-danger'); - var index = data.index, - file = data.files[index]; - $(data.context.children()[index]) - .append('