import { createElement, createRender, onDestroy } from "../lib/skeleton/index.js"; import rxjs, { effect, onClick } from "../lib/rx.js"; import ajax from "../lib/ajax.js"; import assert from "../lib/assert.js"; import { fromHref, toHref } from "../lib/skeleton/router.js"; import { qs, qsa, safe } from "../lib/dom.js"; import { forwardURLParams } from "../lib/path.js"; import { settingsGet, settingsSave } from "../lib/store.js"; import { get as getConfig } from "../model/config.js"; import { loadCSS } from "../helpers/loader.js"; import t from "../locales/index.js"; import cache from "../pages/filespage/cache.js"; import { hooks, mv as mv$ } from "../pages/filespage/model_files.js"; import { extractPath, isDir, isNativeFileUpload } from "../pages/filespage/helper.js"; import { mv as mvVL, withVirtualLayer } from "../pages/filespage/model_virtual_layer.js"; import { getCurrentPath } from "../pages/viewerpage/common.js"; import { generateSkeleton } from "./skeleton.js"; const state = { scrollTop: 0, $cache: null }; const mv = (from, to) => withVirtualLayer( mv$(from, to), mvVL(from, to), ); export default async function ctrlSidebar(render, { nRestart = 0 }) { if (!shouldDisplay()) return; const $sidebar = render(createElement(`

close

`)); withResize($sidebar); // feature: visibility of the sidebar const forceRefresh = () => window.dispatchEvent(new Event("resize")); const isVisible = () => settingsGet({ visible: true }, "sidebar").visible; effect(rxjs.merge(rxjs.fromEvent(window, "keydown")).pipe( rxjs.filter((e) => e.key === "b" && e.ctrlKey === true), rxjs.tap(() => { settingsSave({ visible: $sidebar.classList.contains("hidden") }, "sidebar"); isVisible() ? $sidebar.classList.remove("hidden") : $sidebar.classList.add("hidden"); forceRefresh(); }), )); effect(rxjs.merge( rxjs.fromEvent(window, "resize"), rxjs.of(null), ).pipe(rxjs.tap(() => { const $breadcrumbButton = qs(document.body, "[alt=\"sidebar-open\"]"); if (document.body.clientWidth < 1100) $sidebar.classList.add("hidden"); else if (isVisible()) { $sidebar.classList.remove("hidden"); $breadcrumbButton.classList.add("hidden"); } else { $sidebar.classList.add("hidden"); $breadcrumbButton.classList.remove("hidden"); } }), rxjs.catchError((err) => { if (err instanceof DOMException) return rxjs.EMPTY; throw err; }))); effect(onClick(qs($sidebar, `img[alt="close"]`)).pipe(rxjs.tap(() => { settingsSave({ visible: false }, "sidebar"); $sidebar.classList.add("hidden"); forceRefresh(); }))); // fature: navigation pane ctrlNavigationPane(render, { $sidebar, nRestart }); // feature: tag viewer effect(rxjs.merge( rxjs.of(null), rxjs.fromEvent(window, "filestash::tag"), ).pipe(rxjs.tap(() => { ctrlTagPane(createRender(qs($sidebar, `[data-bind="your-tags"]`))); }))); } const withResize = (function() { let memory = null; return ($sidebar) => { const $resize = createElement(`
`); effect(rxjs.fromEvent($resize, "mousedown").pipe( rxjs.mergeMap((e0) => rxjs.fromEvent(document, "mousemove").pipe( rxjs.takeUntil(rxjs.fromEvent(document, "mouseup")), rxjs.startWith(e0), rxjs.pairwise(), rxjs.map(([prevX, currX]) => currX.clientX - prevX.clientX), rxjs.scan((width, delta) => width + delta, $sidebar.clientWidth), )), rxjs.startWith(memory), rxjs.filter((w) => !!w), rxjs.map((w) => Math.min(Math.max(w, 250), 350)), rxjs.tap((w) => { $sidebar.style.width = `${w}px`; memory = w; }), )); $sidebar.appendChild($resize); }; }()); async function ctrlNavigationPane(render, { $sidebar, nRestart }) { // feature: setup the DOM const $files = qs($sidebar, `[data-bind="your-files"]`); if (state.$cache) { $files.replaceChildren(state.$cache); $sidebar.firstElementChild.scrollTop = state.scrollTop; } onDestroy(() => { $sidebar.classList.remove("search"); state.$cache = $files.firstElementChild?.cloneNode(true); state.scrollTop = $sidebar.firstElementChild.scrollTop; }); const chunk = new PathChunk(); const arr = chunk.toArray(); const dirpath = chunk.toString(); const $tree = document.createDocumentFragment(); for (let i = 0; i { const $list = await _createListOfFiles(path); try { const $ul = qs($sidebar, `[data-path="${path}"] ul`); $ul.replaceWith($list); } catch (err) { $files.replaceChildren($list); } })); cleaners.push(hooks.mutation.listen(async({ op, path }) => { if (["mv", "mkdir", "rm"].indexOf(op) === -1) return; const $list = await _createListOfFiles(path); try { const $ul = qs($sidebar, `[data-path="${path}"] ul`); $ul.replaceWith($list); } catch (err) {} })); onDestroy(() => cleaners.map((fn) => fn())); // feature: highlight current selection try { const $active = qs($sidebar, `[data-path="${chunk.toString()}"] a`); $active.setAttribute("aria-selected", "true"); if (checkVisible($active) === false) { $active.offsetTop < window.innerHeight ? $sidebar.firstChild.scrollTo({ top: 0, behavior: "smooth" }) : $active.scrollIntoView({ behavior: "smooth", block: "nearest" }); } } catch (err) {} // feature: quick search effect(rxjs.fromEvent(qs($sidebar, "h3 input"), "keydown").pipe( rxjs.debounceTime(200), rxjs.tap((e) => { const inputValue = e.target.value.toLowerCase(); qsa($sidebar, "[data-bind=\"your-files\"] li a").forEach(($li) => { if (inputValue === "") { $li.classList.remove("hidden"); $sidebar.classList.remove("search"); return; } $sidebar.classList.add("search"); qs($li, "div").textContent.toLowerCase().indexOf(inputValue) === -1 ? $li.classList.add("hidden") : $li.classList.remove("hidden"); }); }), )); } async function _createListOfFiles(path, currentName, dirpath) { const r = await cache().get(path); const whats = r === null ? (currentName ? [currentName] : []) : r.files .filter(({ type, name }) => type === "directory" && name[0] !== ".") .map(({ name }) => name) .sort(); const MAX_DISPLAY = 100; const $lis = document.createDocumentFragment(); const $fragment = document.createDocumentFragment(); const $ul = document.createElement("ul"); for (let i=0; i directory
${safe(whats[i])}
`); const $link = qs($li, "a"); if ($li.getAttribute("data-path") === dirpath && location.pathname.startsWith(toHref("/files/"))) { $link.removeAttribute("href", ""); $link.removeAttribute("data-link"); } else { $link.ondrop = async(e) => { $link.classList.remove("highlight"); const from = e.dataTransfer.getData("path"); let to = $link.parentElement.getAttribute("data-path"); const [, fromName] = extractPath(from); to += fromName; if (isDir(from)) to += "/"; if (from === to) return; await mv(from, to).toPromise(); }; $link.ondragover = (e) => { if (isNativeFileUpload(e)) return; e.preventDefault(); $link.classList.add("highlight"); }; $link.ondragleave = () => { $link.classList.remove("highlight"); }; } if (i <= MAX_DISPLAY) $lis.appendChild($li); else $fragment.appendChild($li); if (i === MAX_DISPLAY) { const $more = createElement(`
  • ...
  • `); $lis.appendChild($more); $more.onclick = () => { $ul.appendChild($fragment); $more.remove(); }; } } $ul.appendChild($lis); return $ul; } async function ctrlTagPane(render) { if (!getConfig("enable_tags", false)) return; render(createElement(`
    ${generateSkeleton(2)}
    `)); const $page = createElement(`

    tag ${t("Tags")}

    `); const tags = await ajax({ url: forwardURLParams(`api/metadata/search`, ["share"]), method: "POST", responseType: "json", body: JSON.stringify({ "tags": [], "path": getCurrentPath("(/view/|/files/)"), }), }).pipe( rxjs.map(({ responseJSON }) => responseJSON.results .filter(({type}) => type === "folder") .map(({ name }) => name) .sort() ), rxjs.catchError(() => rxjs.of([])), ).toPromise(); if (tags.length === 0) { render(createElement("
    ")); return; } render($page); const $fragment = document.createDocumentFragment(); tags.forEach((name) => { const $tag = createElement(`
    ${name}
    `); const url = new URL(location.href); if (url.searchParams.getAll("tag").indexOf(name) === -1) { $tag.setAttribute("href", forwardURLParams(getCurrentPath() + "?tag=" + name, ["share", "tag"])); } else { url.searchParams.delete("tag", name); $tag.setAttribute("href", url.toString()); $tag.setAttribute("aria-selected", "true"); } $fragment.appendChild($tag); }); qs($page, `[data-bind="taglist"]`).appendChild($fragment); } export function init() { return loadCSS(import.meta.url, "./sidebar.css"); } function checkVisible($el) { const rect = $el.getBoundingClientRect(); return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth); } function shouldDisplay() { if (new URL(location.toString()).searchParams.get("nav") === "false") return false; else if (window.self !== window.top) return false; else if (document.body.clientWidth < 850) return false; return true; } class PathChunk { constructor() { this.pathname = [""].concat(fromHref( location.pathname.replace(new RegExp("[^/]*$"), "") ).split("/").slice(2)); } toArray() { return this.pathname; } toString(i) { if (i >= 0) return decodeURIComponent(this.pathname.slice(0, i+1).join("/") + "/"); return decodeURIComponent(this.pathname.join("/")); } }