diff --git a/public/assets/boot/ctrl_boot_frontoffice.js b/public/assets/boot/ctrl_boot_frontoffice.js index 50ea666f..e3c4780a 100644 --- a/public/assets/boot/ctrl_boot_frontoffice.js +++ b/public/assets/boot/ctrl_boot_frontoffice.js @@ -1,4 +1,5 @@ import rxjs, { ajax } from "../lib/rx.js"; +import { toHref } from "../lib/skeleton/router.js"; // import { setup_cache } from "../helpers/cache.js"; import { init as setup_loader, loadJS } from "../helpers/loader.js"; import { init as setup_translation } from "../locales/index.js"; @@ -56,7 +57,7 @@ function $error(msg) { /// ///////////////////////////////////////// async function setup_xdg_open() { window.overrides = {}; - return loadJS(import.meta.url, "/overrides/xdg-open.js"); + return loadJS(import.meta.url, toHref("/overrides/xdg-open.js")); } async function setup_device() { diff --git a/public/assets/components/breadcrumb.js b/public/assets/components/breadcrumb.js index c68760d0..874565ae 100644 --- a/public/assets/components/breadcrumb.js +++ b/public/assets/components/breadcrumb.js @@ -1,3 +1,4 @@ +import { toHref } from "../lib/skeleton/router.js"; import { animate, slideYOut, slideYIn, opacityOut } from "../lib/animate.js"; import { loadCSS } from "../helpers/loader.js"; @@ -111,7 +112,7 @@ class ComponentBreadcrumb extends window.HTMLDivElement { return `
- + ${tmpl}
@@ -188,7 +189,7 @@ class ComponentBreadcrumb extends window.HTMLDivElement { __htmlLogout() { if (window.self !== window.top) return ""; // no logout button from an iframe return ` - + power `; diff --git a/public/assets/components/decorator_shell_filemanager.js b/public/assets/components/decorator_shell_filemanager.js index 430bd6ec..ce75c937 100644 --- a/public/assets/components/decorator_shell_filemanager.js +++ b/public/assets/components/decorator_shell_filemanager.js @@ -1,5 +1,5 @@ import { createElement, createRender } from "../lib/skeleton/index.js"; -import { navigate } from "../lib/skeleton/router.js"; +import { navigate, fromHref } from "../lib/skeleton/router.js"; import rxjs, { effect, preventDefault } from "../lib/rx.js"; import { onDestroy } from "../lib/skeleton/lifecycle.js"; import { animate, slideYOut } from "../lib/animate.js"; @@ -25,7 +25,7 @@ export default function(ctrl) { // feature1: setup the breadcrumb path const $breadcrumb = qs($page, `[is="component-breadcrumb"]`); - $breadcrumb.setAttribute("path", urlToPath(location.pathname + location.hash)); + $breadcrumb.setAttribute("path", urlToPath(fromHref(location.pathname + location.hash))); // feature2: setup the childrens const $main = qs($page, `[data-bind="filemanager-children"]`); diff --git a/public/assets/components/loader.js b/public/assets/components/loader.js index f4fe0a27..73071dac 100644 --- a/public/assets/components/loader.js +++ b/public/assets/components/loader.js @@ -59,13 +59,16 @@ export function createLoader($parent, opts = {}) {
`); + let $cache = null; const id = window.setTimeout(() => { + $cache = $parent.cloneNode(true); $parent.replaceChildren($icon); animate($icon, { time: 750, keyframes: opacityIn() }); }, wait); return () => { clearTimeout(id); $icon.remove(); + if ($cache) $parent.replaceChildren(...$cache.children); }; })); return rxjs.tap(() => cancel()); diff --git a/public/assets/helpers/loader.js b/public/assets/helpers/loader.js index 5c9d555b..99922ae8 100644 --- a/public/assets/helpers/loader.js +++ b/public/assets/helpers/loader.js @@ -1,15 +1,15 @@ import { get as getRelease } from "../pages/adminpage/model_release.js"; +import { toHref } from "../lib/skeleton/router.js"; let version = null; export async function loadJS(baseURL, path, opts = {}) { const $script = document.createElement("script"); - const link = new URL(path, baseURL) + "?version=" + version; + const link = new URL(path, baseURL) + (version ? "?version=" + version : ""); $script.setAttribute("src", link.toString()); for (const key in opts) { $script.setAttribute(key, opts[key]); } - if (typeof type === "string") ; if (document.head.querySelector(`[src="${link.toString()}"]`)) return Promise.resolve(); document.head.appendChild($script); return new Promise((done) => { diff --git a/public/assets/helpers/log.js b/public/assets/helpers/log.js index 263f63e5..c5cd809b 100644 --- a/public/assets/helpers/log.js +++ b/public/assets/helpers/log.js @@ -1,6 +1,6 @@ export function report(msg, error, link, lineNo, columnNo) { if (window.navigator.onLine === false) return Promise.resolve(); - let url = "/report?"; + let url = "./report?"; url += "url=" + encodeURIComponent(location.href) + "&"; url += "msg=" + encodeURIComponent(msg) + "&"; url += "from=" + encodeURIComponent(link) + "&"; diff --git a/public/assets/lib/animate.js b/public/assets/lib/animate.js index 1d281098..0ed1b33d 100644 --- a/public/assets/lib/animate.js +++ b/public/assets/lib/animate.js @@ -22,7 +22,9 @@ export function animate($node, opts = {}) { duration: time, fill, easing, - }).onfinish = done; + }).onfinish = () => done(() => { + $node.animate(keyframes.reverse(), { duration: 0, fill }); + }); }); } diff --git a/public/assets/lib/skeleton/router.js b/public/assets/lib/skeleton/router.js index 7175ecf4..4ff19f99 100644 --- a/public/assets/lib/skeleton/router.js +++ b/public/assets/lib/skeleton/router.js @@ -1,4 +1,10 @@ const triggerPageChange = () => window.dispatchEvent(new window.Event("pagechange")); +const trimPrefix = (value, prefix) => value.startsWith(prefix) ? value.slice(prefix.length) : value; + +const _base = window.document.head.querySelector("base").getAttribute("href").replace(new RegExp("/$"), ""); +export const base = () => _base; +export const fromHref = (href) => trimPrefix(href, base()); +export const toHref = (href) => base() + href; export async function init($root) { window.addEventListener("DOMContentLoaded", triggerPageChange); @@ -22,13 +28,8 @@ export async function navigate(href) { triggerPageChange(); } -const trimPrefix = (value, prefix) => value.startsWith(prefix) ? value.slice(prefix.length) : value; - export function currentRoute(r, notFoundRoute) { - const currentRoute = trimPrefix( - window.location.pathname, - window.document.head.querySelector("base")?.getAttribute("href") || "" - ); + const currentRoute = fromHref(window.location.pathname); for (const routeKey in r) { if (new RegExp("^" + routeKey + "$").test(currentRoute)) { return r[routeKey]; diff --git a/public/assets/model/backend.js b/public/assets/model/backend.js index d3028b2d..e753ad09 100644 --- a/public/assets/model/backend.js +++ b/public/assets/model/backend.js @@ -2,7 +2,7 @@ import rxjs from "../lib/rx.js"; import ajax from "../lib/ajax.js"; const backend$ = ajax({ - url: "/api/backend", + url: "api/backend", method: "GET", responseType: "json" }).pipe( diff --git a/public/assets/model/config.js b/public/assets/model/config.js index 5f2a5d4e..fcfcd046 100644 --- a/public/assets/model/config.js +++ b/public/assets/model/config.js @@ -2,7 +2,7 @@ import rxjs from "../lib/rx.js"; import ajax from "../lib/ajax.js"; const config$ = ajax({ - url: "/api/config", + url: "api/config", method: "GET", responseType: "json", }).pipe( diff --git a/public/assets/model/session.js b/public/assets/model/session.js index 760ef61b..96eb1151 100644 --- a/public/assets/model/session.js +++ b/public/assets/model/session.js @@ -4,7 +4,7 @@ import ajax from "../lib/ajax.js"; export function createSession(authenticationRequest) { return ajax({ method: "POST", - url: "/api/session", + url: "./api/session", body: authenticationRequest, responseType: "json", }); @@ -12,7 +12,7 @@ export function createSession(authenticationRequest) { export function getSession() { return ajax({ - url: "/api/session", + url: "./api/session", method: "GET", responseType: "json" }).pipe( @@ -22,7 +22,7 @@ export function getSession() { export function deleteSession() { return ajax({ - url: "/api/session", + url: "./api/session", method: "DELETE" }); } diff --git a/public/assets/pages/adminpage/decorator_sidemenu.js b/public/assets/pages/adminpage/decorator_sidemenu.js index 9061cd41..7818fb3c 100644 --- a/public/assets/pages/adminpage/decorator_sidemenu.js +++ b/public/assets/pages/adminpage/decorator_sidemenu.js @@ -1,4 +1,5 @@ import { createElement, createRender } from "../../lib/skeleton/index.js"; +import { toHref } from "../../lib/skeleton/router.js"; import rxjs, { effect, stateMutation } from "../../lib/rx.js"; import { qs } from "../../lib/dom.js"; @@ -16,7 +17,7 @@ export default function(ctrl) {
- + @@ -25,22 +26,22 @@ export default function(ctrl) {

Admin console

  • - + Backend
  • - + Settings
  • - + Logs
  • - +  
  • diff --git a/public/assets/pages/adminpage/model_admin_session.js b/public/assets/pages/adminpage/model_admin_session.js index 3a68b34f..a48a92e3 100644 --- a/public/assets/pages/adminpage/model_admin_session.js +++ b/public/assets/pages/adminpage/model_admin_session.js @@ -10,7 +10,7 @@ const adminSession$ = rxjs.merge( rxjs.fromEvent(document, "visibilitychange").pipe(rxjs.filter(() => !document.hidden)), ).pipe( rxjs.startWith(null), - rxjs.mergeMap(() => ajax({ url: "/admin/api/session", responseType: "json" })), + rxjs.mergeMap(() => ajax({ url: "admin/api/session", responseType: "json" })), rxjs.map(({ responseJSON }) => responseJSON.result), ) ).pipe( @@ -24,7 +24,7 @@ export function isAdmin$() { export function authenticate$(body) { return ajax({ - url: "/admin/api/session", + url: "admin/api/session", method: "POST", body, responseType: "json" diff --git a/public/assets/pages/adminpage/model_audit.js b/public/assets/pages/adminpage/model_audit.js index b94a83d7..4f732c6f 100644 --- a/public/assets/pages/adminpage/model_audit.js +++ b/public/assets/pages/adminpage/model_audit.js @@ -5,7 +5,7 @@ const isLoading$ = new rxjs.BehaviorSubject(false); export function get(searchParams = new URLSearchParams()) { return ajax({ - url: "/admin/api/audit?" + searchParams.toString(), + url: "admin/api/audit?" + searchParams.toString(), responseType: "json" }).pipe( rxjs.map(({ responseJSON }) => responseJSON.result) diff --git a/public/assets/pages/adminpage/model_auth_middleware.js b/public/assets/pages/adminpage/model_auth_middleware.js index 38270ea9..f0fed50f 100644 --- a/public/assets/pages/adminpage/model_auth_middleware.js +++ b/public/assets/pages/adminpage/model_auth_middleware.js @@ -2,7 +2,7 @@ import rxjs from "../../lib/rx.js"; import ajax from "../../lib/ajax.js"; const model$ = ajax({ - url: "/admin/api/middlewares/authentication", + url: "admin/api/middlewares/authentication", method: "GET", responseType: "json" }).pipe( diff --git a/public/assets/pages/adminpage/model_config.js b/public/assets/pages/adminpage/model_config.js index f81b524b..c7b1f043 100644 --- a/public/assets/pages/adminpage/model_config.js +++ b/public/assets/pages/adminpage/model_config.js @@ -6,7 +6,7 @@ const isSaving$ = new rxjs.BehaviorSubject(false); const config$ = isSaving$.pipe( rxjs.filter((loading) => !loading), rxjs.switchMapTo(ajax({ - url: "/admin/api/config", + url: "admin/api/config", method: "GET", responseType: "json" })), @@ -31,7 +31,7 @@ export function save() { rxjs.tap(() => isSaving$.next(true)), rxjs.debounceTime(800), rxjs.mergeMap((formData) => ajax({ - url: "/admin/api/config", + url: "admin/api/config", method: "POST", responseType: "json", body: formData, diff --git a/public/assets/pages/adminpage/model_log.js b/public/assets/pages/adminpage/model_log.js index c2a6089e..ac5542a0 100644 --- a/public/assets/pages/adminpage/model_log.js +++ b/public/assets/pages/adminpage/model_log.js @@ -9,7 +9,7 @@ const log$ = ajax({ ); export function url(logSize = 0) { - return "/admin/api/logs" + (logSize ? `?maxSize=${logSize}` : ""); + return "admin/api/logs" + (logSize ? `?maxSize=${logSize}` : ""); } export function get() { diff --git a/public/assets/pages/adminpage/model_release.js b/public/assets/pages/adminpage/model_release.js index 8bfeb091..df117413 100644 --- a/public/assets/pages/adminpage/model_release.js +++ b/public/assets/pages/adminpage/model_release.js @@ -2,7 +2,7 @@ import rxjs from "../../lib/rx.js"; import ajax from "../../lib/ajax.js"; const release$ = ajax({ - url: "/about", + url: "about", responseType: "text", }).pipe(rxjs.shareReplay(1)); @@ -15,6 +15,6 @@ export function get() { html: a.querySelector("table")?.outerHTML, version: responseHeaders["x-powered-by"].trim().replace(/^Filestash\/([v\.0-9]*).*$/, "$1") }; - }) + }), ); } diff --git a/public/assets/pages/connectpage/ctrl_form.js b/public/assets/pages/connectpage/ctrl_form.js index a8e90417..463ca957 100644 --- a/public/assets/pages/connectpage/ctrl_form.js +++ b/public/assets/pages/connectpage/ctrl_form.js @@ -1,4 +1,5 @@ import { createElement, navigate } from "../../lib/skeleton/index.js"; +import { toHref } from "../../lib/skeleton/router.js"; import rxjs, { effect, applyMutation, applyMutations, preventDefault, onClick } from "../../lib/rx.js"; import ajax from "../../lib/ajax.js"; import { qs, qsa, safe } from "../../lib/dom.js"; @@ -167,7 +168,7 @@ export default async function(render) { ).pipe( rxjs.mergeMap((formData) => { // CASE 1: authentication middleware flow if (!("middleware" in formData)) return rxjs.of(formData); - let url = "/api/session/auth/?action=redirect"; + let url = "api/session/auth/?action=redirect"; url += "&label=" + formData["label"]; const p = getURLParams(); if (Object.keys(p).length > 0) { @@ -197,11 +198,11 @@ export default async function(render) { return rxjs.of(null).pipe( rxjs.tap(() => toggleLoader(true)), rxjs.mergeMap(() => createSession(formData)), - rxjs.tap(({ responseJSON }) => { - let redirectURL = "/files/"; + rxjs.tap(({ responseJSON }) => { // TODO + let redirectURL = toHref("/files/"); const GET = getURLParams(); if (GET["next"]) redirectURL = GET["next"]; - else if (responseJSON.result) redirectURL = "/files" + responseJSON.result; + else if (responseJSON.result) redirectURL = toHref("/files" + responseJSON.result); navigate(redirectURL); }), rxjs.catchError((err) => { diff --git a/public/assets/pages/connectpage/model_backend.js b/public/assets/pages/connectpage/model_backend.js index cb97ad04..bc3ecab0 100644 --- a/public/assets/pages/connectpage/model_backend.js +++ b/public/assets/pages/connectpage/model_backend.js @@ -2,7 +2,7 @@ import rxjs from "../../lib/rx.js"; import ajax from "../../lib/ajax.js"; export default ajax({ - url: "/api/backend", + url: "api/backend", responseType: "json" }).pipe( rxjs.map(({ responseJSON }) => responseJSON.result), diff --git a/public/assets/pages/connectpage/model_config.js b/public/assets/pages/connectpage/model_config.js index cdf7c668..6df5b313 100644 --- a/public/assets/pages/connectpage/model_config.js +++ b/public/assets/pages/connectpage/model_config.js @@ -2,7 +2,7 @@ import rxjs from "../../lib/rx.js"; import ajax from "../../lib/ajax.js"; export default ajax({ - url: "/api/config", + url: "api/config", responseType: "json" }).pipe( rxjs.map(({ responseJSON }) => responseJSON.result), diff --git a/public/assets/pages/ctrl_adminpage.js b/public/assets/pages/ctrl_adminpage.js index 60167a70..52919486 100644 --- a/public/assets/pages/ctrl_adminpage.js +++ b/public/assets/pages/ctrl_adminpage.js @@ -1,6 +1,7 @@ import { navigate } from "../lib/skeleton/index.js"; +import { toHref } from "../lib/skeleton/router.js"; import AdminOnly from "./adminpage/decorator_admin_only.js"; export default AdminOnly(function() { - navigate("/admin/backend"); + navigate(toHref("/admin/backend")); }); diff --git a/public/assets/pages/ctrl_error.js b/public/assets/pages/ctrl_error.js index dcc4f334..c197a7b4 100644 --- a/public/assets/pages/ctrl_error.js +++ b/public/assets/pages/ctrl_error.js @@ -1,4 +1,5 @@ import { createElement, createRender } from "../lib/skeleton/index.js"; +import { toHref, fromHref } from "../lib/skeleton/router.js"; import rxjs, { effect, applyMutation } from "../lib/rx.js"; import { qs } from "../lib/dom.js"; import t from "../locales/index.js"; @@ -14,7 +15,7 @@ export default function(render = createRender(qs(document.body, "[role=\"main\"] const $page = createElement(`
    - + ${t("home")} @@ -138,5 +139,5 @@ function calculateBacklink(pathname = "") { url = listPath.join("/") + "/"; break; } - return url === "/files/" ? "/" : url; + return toHref(url === "/files/" ? "/" : url); } diff --git a/public/assets/pages/ctrl_filespage.js b/public/assets/pages/ctrl_filespage.js index d6856a24..86b297be 100644 --- a/public/assets/pages/ctrl_filespage.js +++ b/public/assets/pages/ctrl_filespage.js @@ -1,18 +1,23 @@ import { createElement, createRender } from "../lib/skeleton/index.js"; +import { navigate } from "../lib/skeleton/router.js"; import rxjs, { effect } from "../lib/rx.js"; import { qs } from "../lib/dom.js"; import { loadCSS } from "../helpers/loader.js"; import WithShell, { init as initShell } from "../components/decorator_shell_filemanager.js"; -import { get as getConfig } from "./filespage/model_config.js"; import componentFilesystem, { init as initFilesystem } from "./filespage/ctrl_filesystem.js"; import componentSubmenu, { init as initSubmenu } from "./filespage/ctrl_submenu.js"; import componentNewItem, { init as initNewItem } from "./filespage/ctrl_newitem.js"; import componentUpload, { init as initUpload } from "./filespage/ctrl_upload.js"; +import { get as getConfig } from "./filespage/model_config.js"; import "../components/breadcrumb.js"; export default WithShell(function(render) { + if (new RegExp("/$").test(location.pathname) === false) { + navigate(location.pathname + "/"); + return; + } const $page = createElement(`
    @@ -38,7 +43,7 @@ export default WithShell(function(render) { export function init() { return Promise.all([ - loadCSS(import.meta.url, "./ctrl_filespage.css"), + loadCSS(import.meta.url, "ctrl_filespage.css"), initShell(), initFilesystem(), getConfig().toPromise(), initSubmenu(), initNewItem(), initUpload(), ]); diff --git a/public/assets/pages/ctrl_homepage.js b/public/assets/pages/ctrl_homepage.js index bf57efa5..23d74d8a 100644 --- a/public/assets/pages/ctrl_homepage.js +++ b/public/assets/pages/ctrl_homepage.js @@ -1,4 +1,5 @@ import { createElement, navigate } from "../lib/skeleton/index.js"; +import { toHref } from "../lib/skeleton/router.js"; import rxjs, { effect } from "../lib/rx.js"; import { ApplicationError, AjaxError } from "../lib/error.js"; import ctrlError from "./ctrl_error.js"; @@ -30,8 +31,8 @@ export default function(render) { return rxjs.throwError(err); }), rxjs.tap(({ is_authenticated, home = "/" }) => { - if (is_authenticated !== true) return navigate("/login"); - return navigate(`/files${home}`); + if (is_authenticated !== true) return navigate(toHref("/login")); + return navigate(toHref(`/files${home}`)); }), rxjs.catchError(ctrlError(render)), )); diff --git a/public/assets/pages/ctrl_logout.js b/public/assets/pages/ctrl_logout.js index d160e116..2a699881 100644 --- a/public/assets/pages/ctrl_logout.js +++ b/public/assets/pages/ctrl_logout.js @@ -1,4 +1,5 @@ import { navigate } from "../lib/skeleton/index.js"; +import { toHref } from "../lib/skeleton/router.js"; import rxjs, { effect } from "../lib/rx.js"; import { deleteSession } from "../model/session.js"; @@ -9,7 +10,7 @@ export default function(render) { render($loader); effect(deleteSession().pipe( - rxjs.tap(() => navigate("/")), + rxjs.tap(() => navigate(toHref("/"))), rxjs.catchError(ctrlError(render)), )); } diff --git a/public/assets/pages/filespage/ctrl_filesystem.js b/public/assets/pages/filespage/ctrl_filesystem.js index 63f8871b..a36aa87c 100644 --- a/public/assets/pages/filespage/ctrl_filesystem.js +++ b/public/assets/pages/filespage/ctrl_filesystem.js @@ -316,8 +316,7 @@ export function init() { function createLink(file, currentPath) { let path = file.path; - if (!path) path = currentPath + file.name; - if (file.type === "directory") path += "/"; + if (!path) path = currentPath + file.name + (file.type === "directory" ? "/" : ""); const link = file.type === "directory" ? "/files" + path : "/view" + path; return { path: path, diff --git a/public/assets/pages/filespage/ctrl_upload.css b/public/assets/pages/filespage/ctrl_upload.css index eb97aaf7..292aa4c7 100644 --- a/public/assets/pages/filespage/ctrl_upload.css +++ b/public/assets/pages/filespage/ctrl_upload.css @@ -79,7 +79,7 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - padding: 3px 0; + padding: 3px 3px 3px 0; } .component_upload .stats_content .file_row .file_path .speed { font-size: 0.7rem; diff --git a/public/assets/pages/filespage/ctrl_upload.js b/public/assets/pages/filespage/ctrl_upload.js index 36790816..b168853f 100644 --- a/public/assets/pages/filespage/ctrl_upload.js +++ b/public/assets/pages/filespage/ctrl_upload.js @@ -115,28 +115,39 @@ function componentUploadQueue(render, { workers$ }) { // feature1: close the queue onClick(qs($page, `img[alt="close"]`)).pipe( - // rxjs.mergeMap(() => animate($page, { time: 200, keyframes: slideYOut(50) })), - rxjs.tap(() => $page.classList.add("hidden")), + rxjs.tap(async (cancel) => { + const cleanup = await animate($page, { time: 200, keyframes: slideYOut(50) }); + console.log(workers$.value); + qs($page, ".stats_content").innerHTML = ""; + $page.classList.add("hidden"); + cleanup(); + }), ).subscribe(); // feature2: setup the task queue in the dom workers$.subscribe(({ tasks }) => { if (tasks.length === 0) return; - $page.classList.remove("hidden"); const $fragment = document.createDocumentFragment(); for (let i = 0; i`) + const ICON = { + STOP: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgdmlld0JveD0iMCAwIDM4NCA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggc3R5bGU9ImZpbGw6ICM2MjY0Njk7IiBkPSJNMCAxMjhDMCA5Mi43IDI4LjcgNjQgNjQgNjRIMzIwYzM1LjMgMCA2NCAyOC43IDY0IDY0VjM4NGMwIDM1LjMtMjguNyA2NC02NCA2NEg2NGMtMzUuMyAwLTY0LTI4LjctNjQtNjRWMTI4eiIgLz4KPC9zdmc+Cg==", + RETRY: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0Ij48cGF0aCBkPSJNMTcuNjUgNi4zNUMxNi4yIDQuOSAxNC4yMSA0IDEyIDRjLTQuNDIgMC03Ljk5IDMuNTgtNy45OSA4czMuNTcgOCA3Ljk5IDhjMy43MyAwIDYuODQtMi41NSA3LjczLTZoLTIuMDhjLS44MiAyLjMzLTMuMDQgNC01LjY1IDQtMy4zMSAwLTYtMi42OS02LTZzMi42OS02IDYtNmMxLjY2IDAgMy4xNC42OSA0LjIyIDEuNzhMMTMgMTFoN1Y0bC0yLjM1IDIuMzV6Ii8+PHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==", + }; + const $iconStop = createElement(`stop`); + const $iconRetry = createElement(`retry`); const $close = qs($page, `img[alt="close"]`); const updateDOMTaskProgress = ($task, text) => $task.firstElementChild.nextElementSibling.textContent = text; const updateDOMTaskSpeed = ($task, text) => $task.firstElementChild.firstElementChild.nextElementSibling.textContent = formatSpeed(text); @@ -159,9 +170,8 @@ function componentUploadQueue(render, { workers$ }) { break; case "doing": updateDOMTaskProgress($task, formatPercent(0)); - const $stop = $icon.cloneNode(true); - $task.firstElementChild.nextElementSibling.nextElementSibling.appendChild($stop); - $stop.onclick = () => { + $task.firstElementChild.nextElementSibling.nextElementSibling.appendChild($iconStop); + $iconStop.onclick = () => { cancel(); $task.firstElementChild.nextElementSibling.nextElementSibling.classList.add("hidden"); }; @@ -177,14 +187,18 @@ function componentUploadQueue(render, { workers$ }) { $close.removeEventListener("click", cancel); break; case "error": - updateDOMGlobalTitle($page, t("Error")); // TODO: only apply if err is not abort type + const $retry = $iconRetry.cloneNode(true); + updateDOMGlobalTitle($page, t("Error")); updateDOMGlobalSpeed(nworker, 0); updateDOMTaskProgress($task, t("Error")); updateDOMTaskSpeed($task, 0); $task.removeAttribute("data-path"); $task.classList.remove("todo_color"); - $task.classList.add("error_color"); + $task.firstElementChild.nextElementSibling.nextElementSibling.firstElementChild.remove(); + $task.firstElementChild.nextElementSibling.nextElementSibling.appendChild($retry); + $retry.onclick = () => { console.log("CLICK RETRY"); } $close.removeEventListener("click", cancel); + $task.classList.add("error_color"); break; default: assert.fail(`UNEXPECTED_STATUS status="${status}" path="${$task.getAttribute("path")}"`); @@ -195,8 +209,12 @@ function componentUploadQueue(render, { workers$ }) { const reservations = new Array(MAX_WORKERS).fill(false); const processWorkerQueue = async (nworker) => { while(tasks.length > 0) { - updateDOMGlobalTitle($page, t("Running")+"...") - const task = tasks.shift(); + updateDOMGlobalTitle($page, t("Running")+"..."); + const task = nextTask(tasks); + if (!task) { + await new Promise((done) => setTimeout(done, 1000)); + continue; + } const $task = qs($page, `[data-path="${task.path}"]`); const exec = task.exec({ error: (err) => updateDOMWithStatus($task, { status: "error", nworker }), @@ -207,14 +225,28 @@ function componentUploadQueue(render, { workers$ }) { }, }); updateDOMWithStatus($task, { exec, status: "doing", nworker }); - await exec.run(task); - updateDOMWithStatus($task, { exec, status: "done", nworker }); + try { + await exec.run(task); + updateDOMWithStatus($task, { exec, status: "done", nworker }); + } catch(err) { + updateDOMWithStatus($task, { exec, status: "error", nworker }); + } + task.done = true; if (tasks.length === 0 // no remaining tasks && reservations.filter((t) => t === true).length === 1 // only for the last remaining job ) updateDOMGlobalTitle($page, t("Done")); } }; + const nextTask = (tasks) => { + for (let i=0;i fn().catch(() => noFailureAllowed(fn)); workers$.subscribe(async ({ tasks: newTasks }) => { tasks = tasks.concat(newTasks); // add new tasks to the pool @@ -282,6 +314,11 @@ function workerImplFile({ error, progress, speed }) { }; this.xhr.onload = () => { progress(100); + if (this.xhr.status !== 200) { + virtual.afterError(); + err(new Error(this.xhr.statusText)); + return; + } virtual.afterSuccess(); done(); }; @@ -294,7 +331,6 @@ function workerImplFile({ error, progress, speed }) { (err) => this.xhr.onerror(err), ); }); - } } } @@ -338,8 +374,13 @@ function workerImplDirectory({ error, progress }) { this.xhr.onload = () => { clearInterval(id); progress(100); + if (this.xhr.status !== 200) { + virtual.afterError(); + err(new Error(this.xhr.statusText)); + return; + } virtual.afterSuccess(); - setTimeout(() => done(), 500); + done(); }; this.xhr.send(null); }); @@ -393,13 +434,14 @@ async function processItems(itemList) { const path = basepath + entry.fullPath.substring(1); let task = null; if (entry === null) continue; - if (entry.isFile) { + else if (entry.isFile) { const entrySize = await new Promise((done) => entry.getMetadata(({ size }) => done(size))); task = { type: "file", entry, path, exec: workerImplFile, virtual: save(path, entrySize), + done: false, }; size += entrySize; } else if (entry.isDirectory) { @@ -408,6 +450,7 @@ async function processItems(itemList) { path: path + "/", exec: workerImplDirectory, virtual: mkdir(path), + done: false, }; size += 5000; // that's to calculate the remaining time for an upload, aka made up size is ok queue = queue.concat(await new Promise((done) => { @@ -416,10 +459,23 @@ async function processItems(itemList) { } else { assert.fail("NOT_IMPLEMENTED - unknown entry type in ctrl_upload.js", entry); } + task.ready = () => { + const isInDirectory = (filepath, folder) => folder.indexOf(filepath) === 0; + for (let i=0;i { ); }; -export const search = (term) => { - const path = location.pathname.replace(new RegExp("^/files/"), "/"); - return ajax({ - url: `api/files/search?path=${encodeURIComponent(path)}&q=${encodeURIComponent(term)}`, - responseType: "json" - }).pipe(rxjs.map(({ responseJSON }) => ({ - files: responseJSON.results, - }))); -}; +export const search = (term) => ajax({ + url: `api/files/search?path=${encodeURIComponent(currentPath())}&q=${encodeURIComponent(term)}`, + responseType: "json" +}).pipe(rxjs.map(({ responseJSON }) => ({ + files: responseJSON.results, +}))); diff --git a/public/assets/pages/filespage/model_virtual_layer.js b/public/assets/pages/filespage/model_virtual_layer.js index 693b80fa..ba5fe880 100644 --- a/public/assets/pages/filespage/model_virtual_layer.js +++ b/public/assets/pages/filespage/model_virtual_layer.js @@ -26,6 +26,12 @@ const mutationFiles$ = new rxjs.BehaviorSubject({ // "/home/": [{ name: "test", fn: (file) => file, ...] }); + +window.debug = () => { + console.log("VIRTUAL", JSON.stringify(virtualFiles$.value, null, 4)); + console.log("MUTATION", JSON.stringify(mutationFiles$.value)); +}; + class IVirtualLayer { constructor() {} before() { throw new Error("NOT_IMPLEMENTED"); } @@ -95,6 +101,7 @@ export function mkdir(path) { ...file, loading: true, }); + statePop(mutationFiles$, basepath, dirname); // case: rm followed by mkdir } async afterSuccess() { @@ -132,6 +139,7 @@ export function save(path, size) { ...file, loading: true, }); + statePop(mutationFiles$, basepath, filename); // eg: rm followed by save } async afterSuccess() { @@ -144,7 +152,7 @@ export function save(path, size) { } async afterError() { - statePop(virtualFiles$, basepath, dirname); + statePop(virtualFiles$, basepath, filename); return rxjs.EMPTY; } } @@ -164,31 +172,38 @@ export function rm(...paths) { constructor() { super(); } before() { - stateAdd(mutationFiles$, basepath, { - name: basepath, - fn: (file) => { - for (let i=0;i { if (file.name === arr[i+1]) { file.loading = true; file.last = true; } - } - return file; - }, - }); + return file; + }, + }); + statePop(virtualFiles$, arr[i], arr[i+1]); // eg: touch followed by rm + } } async afterSuccess() { - stateAdd(mutationFiles$, basepath, { - name: basepath, - fn: (file) => { - for (let i=0;i { + for (let i=0; i { + for (let i=0;i statePop(mutationFiles$, basepath, basepath)); await Promise.all(paths.map((path) => fscache().remove(path, false))); await fscache().update(basepath, ({ files = [], ...rest }) => ({ files: files.filter(({ name }) => { @@ -204,18 +219,18 @@ export function rm(...paths) { } async afterError() { - stateAdd(mutationFiles$, basepath, { - name: basepath, - fn: (file) => { - for (let i=0;i { if (file.name === arr[i+1]) { delete file.loading; delete file.last; } - } - return file; - }, - }); + return file; + }, + }); + } return rxjs.EMPTY; } } @@ -327,16 +342,7 @@ export function mv(fromPath, toPath) { export function ls(path) { return rxjs.pipe( - // case1: virtual files = additional files we want to see displayed in the UI - rxjs.switchMap(({ files, ...res }) => virtualFiles$.pipe(rxjs.mergeMap((virtualFiles) => { - const shouldContinue = !!(virtualFiles[path] && virtualFiles[path].length > 0); - if (!shouldContinue) return rxjs.of({ ...res, files }); - return rxjs.of({ - ...res, - files: files.concat(virtualFiles[path]), - }); - }))), - // case2: file mutation = update a file state, typically to add a loading state to an + // case1: file mutation = update a file state, typically to add a loading state to an // file or remove it entirely rxjs.switchMap(({ files, ...res }) => mutationFiles$.pipe(rxjs.mergeMap((fns) => { const shouldContinue = !!(fns[path] && fns[path].length > 0); @@ -352,6 +358,15 @@ export function ls(path) { } return rxjs.of({ ...res, files }); }))), + // case2: virtual files = additional files we want to see displayed in the UI + rxjs.switchMap(({ files, ...res }) => virtualFiles$.pipe(rxjs.mergeMap((virtualFiles) => { + const shouldContinue = !!(virtualFiles[path] && virtualFiles[path].length > 0); + if (!shouldContinue) return rxjs.of({ ...res, files }); + return rxjs.of({ + ...res, + files: files.concat(virtualFiles[path]), + }); + }))), ); } diff --git a/public/assets/pages/filespage/thing.css b/public/assets/pages/filespage/thing.css index f56dfbc9..950b30f7 100644 --- a/public/assets/pages/filespage/thing.css +++ b/public/assets/pages/filespage/thing.css @@ -170,7 +170,7 @@ } .list > .component_thing.view-grid .component_filename { letter-spacing: -0.5px; - z-index: -1; + z-index: 0; position: absolute; bottom: 2px; left: 2px; diff --git a/public/assets/pages/filespage/thing.js b/public/assets/pages/filespage/thing.js index 0466351c..1d23e7e4 100644 --- a/public/assets/pages/filespage/thing.js +++ b/public/assets/pages/filespage/thing.js @@ -1,4 +1,5 @@ import { createElement, createFragment } from "../../lib/skeleton/index.js"; +import { toHref } from "../../lib/skeleton/router.js"; import { qs } from "../../lib/dom.js"; import { animate, opacityIn } from "../../lib/animate.js"; import assert from "../../lib/assert.js"; @@ -82,7 +83,7 @@ export function createThing({ const $label = $thing.children[3].firstElementChild.firstElementChild; // = qs($thing, ".component_filename .file-details > span"); const $time = $thing.children[4]; // = qs($thing, ".component_datetime"); - $link.setAttribute("href", link); + $link.setAttribute("href", toHref(link)); $thing.setAttribute("data-droptarget", type === "directory"); $thing.setAttribute("data-n", n); $thing.setAttribute("data-path", path); @@ -103,10 +104,11 @@ export function createThing({ $img.classList.add("thumbnail"); const $placeholder = $img.cloneNode(true); $img.parentElement.appendChild($placeholder); - $img.setAttribute("src", "/api/files/cat?path=" + encodeURIComponent(path) + "&thumbnail=true"); + $img.setAttribute("src", "api/files/cat?path=" + encodeURIComponent(path) + "&thumbnail=true"); $img.style.opacity = 0; $img.style.position = "absolute"; $img.style.top = 0; + $img.style.zIndex = 1; $placeholder.setAttribute("src", IMAGE.THUMBNAIL_PLACEHOLDER); const t = new Date(); $img.onload = async () => { diff --git a/public/assets/pages/viewerpage/application_editor.js b/public/assets/pages/viewerpage/application_editor.js index f51417fc..8bc4526e 100644 --- a/public/assets/pages/viewerpage/application_editor.js +++ b/public/assets/pages/viewerpage/application_editor.js @@ -3,17 +3,18 @@ import rxjs, { effect } from "../../lib/rx.js"; import { animate, slideXIn, opacityOut } from "../../lib/animate.js"; import { qs } from "../../lib/dom.js"; import { createLoader } from "../../components/loader.js"; -import { createModal } from "../../components/modal.js"; +import { createModal, MODAL_RIGHT_BUTTON } from "../../components/modal.js"; import { loadCSS, loadJS } from "../../helpers/loader.js"; import ajax from "../../lib/ajax.js"; import { extname } from "../../lib/path.js"; import { get as getConfig } from "../../model/config.js"; +import t from "../../locales/index.js"; import ctrlError from "../ctrl_error.js"; import ctrlDownloader, { init as initDownloader } from "./application_downloader.js"; -import { getFile$, saveFile$, transition, getFilename, getCurrentPath } from "./common.js"; +import { transition, getFilename, getCurrentPath } from "./common.js"; import { $ICON } from "./common_fab.js"; -import { fileOptions } from "./model_files.js"; +import { options, cat, save } from "./model_files.js"; import "../../components/menubar.js"; import "../../components/fab.js"; @@ -31,17 +32,19 @@ export default async function(render) { `); render($page); - const $editor = qs($page, ".component_editor"); - const $menubar = qs($page, "component-menubar"); - const $fab = qs($page, `[is="component-fab"]`); + const $dom = { + editor: () => qs($page, ".component_editor"), + menubar: () => qs($page, "component-menubar"), + fab: () => qs($page, `[is="component-fab"]`), + }; const getConfig$ = getConfig().pipe(rxjs.shareReplay(1)); const content$ = new rxjs.ReplaySubject(1); // feature1: setup the dom const removeLoader = createLoader($page); const setup$ = rxjs.race( - getFile$(), - ajax("/about").pipe(rxjs.delay(TIME_BEFORE_ABORT_EDIT), rxjs.map(() => null)), + cat(), + ajax("about").pipe(rxjs.delay(TIME_BEFORE_ABORT_EDIT), rxjs.map(() => null)), ).pipe( rxjs.mergeMap((content) => { if (content === null || has_binary(content)) { @@ -61,12 +64,13 @@ export default async function(render) { rxjs.mergeMap((arr) => rxjs.from(loadMode(extname(getFilename()))).pipe( rxjs.map((mode) => arr.concat([mode])), )), - rxjs.mergeMap((arr) => fileOptions(getCurrentPath()).pipe( + rxjs.mergeMap((arr) => options(getCurrentPath()).pipe( rxjs.map((acl) => arr.concat([acl])), )), )), removeLoader, rxjs.map(([content, config, mode, acl]) => { + const $editor = $dom.editor(); content$.next(content); $editor.classList.remove("hidden"); const editor = window.CodeMirror($editor, { @@ -82,17 +86,18 @@ export default async function(render) { matchTags: { bothTags: true }, autoCloseTags: true, }); - transition($editor); + // transition($editor); editor.getWrapperElement().setAttribute("mode", mode); if (!("ontouchstart" in window)) editor.focus(); if (config["editor"] === "emacs") editor.addKeyMap({ "Ctrl-X Ctrl-C": (cm) => window.history.back(), }); onDestroy(() => editor.clearHistory()); - $menubar.classList.remove("hidden"); + $dom.menubar().classList.remove("hidden"); editor.execCommand("save"); return editor; }), + // rxjs.tap(() => { debugger; }), rxjs.tap((editor) => requestAnimationFrame(() => editor.refresh())), rxjs.catchError(ctrlError()), rxjs.share(), @@ -111,6 +116,7 @@ export default async function(render) { rxjs.switchMap((editor) => new rxjs.Observable((observer) => editor.on("change", (cm) => observer.next(cm)))), rxjs.mergeMap((editor) => content$.pipe(rxjs.map((oldContent) => [editor, editor.getValue(), oldContent]))), rxjs.tap(async([editor, newContent = "", oldContent = ""]) => { + const $fab = $dom.fab(); if ($fab.disabled) return; const $breadcrumb = qs(document.body, `[is="component-breadcrumb"]`); if (newContent === oldContent) { @@ -135,11 +141,12 @@ export default async function(render) { window.CodeMirror.commands.save = (cm) => observer.next(cm); })), rxjs.mergeMap((cm) => { + const $fab = $dom.fab(); $fab.classList.remove("hidden"); $fab.render($ICON.LOADING); $fab.disabled = true; return rxjs.of(cm.getValue()).pipe( - saveFile$(), + save(), rxjs.tap((content) => { $fab.removeAttribute("disabled"); content$.next(content); @@ -151,30 +158,26 @@ export default async function(render) { // feature5: save on exit effect(setup$.pipe( - rxjs.tap((cm) => window.history.block = async(href) => { + rxjs.tap((cm) => window.history.block = async (href) => { const block = qs(document.body, `[is="component-breadcrumb"]`).hasAttribute("indicator"); if (block === false) return false; - - // confirm.now( - //
    - // { t("Do you want to save the changes ?") } - //
    , - // () =>{ - // return this.save() - // .then(() => this.props.history.push(nextLocation)); - // }, - // () => { - // this.props.needSavingUpdate(false) - // .then(() => this.props.history.push(nextLocation)); - // }, - // ); - return new Promise((done) => { - createModal(createElement(` -
    - Do you want to save the changes ? -
    - `, { onQuit: () => { done(false); } })); + const userAction = await new Promise((done) => { + createModal({ + withButtonsRight: t("Yes"), + withButtonsLeft: t("No"), + })( + createElement(` +
    + Do you want to save the changes ? +
    + `), + (val) => done(val), + ); }); + if (userAction === MODAL_RIGHT_BUTTON) { + console.log("TODO: SAVE THE DATA"); + } + return false; }), )); } diff --git a/public/assets/pages/viewerpage/common.js b/public/assets/pages/viewerpage/common.js index b2fb3ac6..c205c4c4 100644 --- a/public/assets/pages/viewerpage/common.js +++ b/public/assets/pages/viewerpage/common.js @@ -1,35 +1,24 @@ -import { transition as transitionLib, slideYIn } from "../../lib/animate.js"; -import { basename } from "../../lib/path.js"; import rxjs from "../../lib/rx.js"; import ajax from "../../lib/ajax.js"; +import { fromHref } from "../../lib/skeleton/router.js"; +import { transition as transitionLib, slideYIn } from "../../lib/animate.js"; +import { basename } from "../../lib/path.js"; export function transition($node) { return transitionLib($node, { timeEnter: 150, enter: slideYIn(2) }); } -export function getFile$() { - return ajax(getDownloadUrl()).pipe( - rxjs.map(({ response }) => response), - ); -} - -export function saveFile$() { - return rxjs.pipe( - rxjs.delay(2000), - rxjs.tap((content) => console.log("SAVED")), - ); -} - export function getFilename() { return basename(getCurrentPath()) || "untitled.dat"; } export function getDownloadUrl() { - return "/api/files/cat?path=" + getCurrentPath().replace(/%23/g, "#") + location.hash; + return "api/files/cat?path=" + encodeURIComponent(getCurrentPath()); } export function getCurrentPath() { - return decodeURIComponent(location.pathname.replace("/view", "") + (location.hash || "")); + const fullpath = fromHref(location.pathname + location.hash); + return decodeURIComponent(fullpath.replace(new RegExp("^/view"), "")); } // function prepare(path) { diff --git a/public/assets/pages/viewerpage/model_files.js b/public/assets/pages/viewerpage/model_files.js index 409672d6..eafd2032 100644 --- a/public/assets/pages/viewerpage/model_files.js +++ b/public/assets/pages/viewerpage/model_files.js @@ -1,9 +1,19 @@ import rxjs from "../../lib/rx.js"; import ajax from "../../lib/ajax.js"; +import { getDownloadUrl } from "./common.js"; -export function fileOptions(path) { - return ajax({ - url: `/api/files/cat?path=${path}`, - method: "OPTIONS", - }).pipe(rxjs.map((res) => res.responseHeaders.allow.replace(/\r/, "").split(", "))); -} +export const options = (path) => ajax({ + url: `api/files/cat?path=${path}`, + method: "OPTIONS", +}).pipe(rxjs.map((res) => res.responseHeaders.allow.replace(/\r/, "").split(", "))); + +export const cat = () => ajax(getDownloadUrl()).pipe( + rxjs.map(({ response }) => response), +); + +export const save = () => { + return rxjs.pipe( + rxjs.delay(2000), + rxjs.tap((content) => console.log("SAVED")), + ); +}; diff --git a/public/index.backoffice.html b/public/index.backoffice.html index 47025580..50dc63b6 100644 --- a/public/index.backoffice.html +++ b/public/index.backoffice.html @@ -1,30 +1,32 @@ + - + + Admin Console - +
    - + - +