diff --git a/public/assets/components/loader.js b/public/assets/components/loader.js index 1f142520..97267964 100644 --- a/public/assets/components/loader.js +++ b/public/assets/components/loader.js @@ -1,5 +1,6 @@ import { createElement } from "../lib/skeleton/index.js"; -import rxjs from "../lib/rx.js"; +import rxjs, { effect } from "../lib/rx.js"; +import { animate, opacityIn } from "../lib/animate.js"; class Loader extends window.HTMLElement { constructor() { @@ -42,25 +43,27 @@ class Loader extends window.HTMLElement { } } - -// function tap(sideEffectFn) { -// return (source) => { -// // TODO: use source.lift -// console.log("SOURCE", source) -// source.lift({ -// call: (subscriber, source) => { -// console.log(source, subscriber); -// return subscriber; -// }, -// }) -// } -// } -const tap = rxjs.tap; - customElements.define("component-loader", Loader); +export function createLoader($parent, opts = {}) { + const { wait = 500 } = opts + const cancel = effect(new rxjs.Observable((observer) => { + const $icon = createElement(``); + const id = window.setTimeout(() => { + $parent.appendChild($icon); + animate($icon, { time: 1000, keyframes: opacityIn() }); + }, wait); + return () => { + clearTimeout(id); + $icon.remove(); + }; + })); + return rxjs.tap(() => cancel()); +} + +// > after this should be deprecated export default createElement(""); export function toggle($node, show = false) { - if (show === true) return tap(() => $node.appendChild(createElement(""))); - else return tap(() => $node.querySelector("component-loader")?.remove()); + if (show === true) return rxjs.tap(() => $node.appendChild(createElement(""))); + else return rxjs.tap(() => $node.querySelector("component-loader")?.remove()); } diff --git a/public/assets/lib/rx.js b/public/assets/lib/rx.js index bef94e6e..5cf23e5b 100644 --- a/public/assets/lib/rx.js +++ b/public/assets/lib/rx.js @@ -8,8 +8,9 @@ export default rxjs; export const ajax = ajaxModule.ajax; export function effect(obs) { - const tmp = obs.subscribe(() => {}, (err) => { throw err; }); - onDestroy(() => tmp.unsubscribe()); + const sub = obs.subscribe(() => {}, (err) => { throw err; }); + onDestroy(() => sub.unsubscribe()); + return sub.unsubscribe.bind(sub); } const getFn = (obj, arg0, ...args) => { @@ -27,9 +28,7 @@ export function applyMutation($node, ...keys) { export function applyMutations($node, ...keys) { if (!$node) throw new Error("undefined node"); const execute = getFn($node, ...keys); - return rxjs.tap((vals) => vals.forEach((val) => { - execute(val); - })); + return rxjs.tap((vals) => vals.forEach((val) => execute(val))); } export function stateMutation($node, attr) { @@ -42,8 +41,17 @@ export function preventDefault() { } export function onClick($node) { - if (!$node) return rxjs.EMPTY; - return rxjs.fromEvent($node, "click").pipe( - rxjs.map(() => $node), - ); + if (!$node) throw new Error("undefined node"); + return rxjs.fromEvent($node, "click").pipe(rxjs.map(() => $node)); +} + +export function onLoad($node) { + if (!$node) throw new Error("undefined node"); + return new rxjs.Observable((observer) => { + $node.onload = () => { + observer.next($node); + observer.complete(); + }; + $node.onerror = (err) => observer.error(err); + }); } diff --git a/public/assets/pages/ctrl_viewerpage.js b/public/assets/pages/ctrl_viewerpage.js index 64c4d24f..737ae59e 100644 --- a/public/assets/pages/ctrl_viewerpage.js +++ b/public/assets/pages/ctrl_viewerpage.js @@ -1,12 +1,20 @@ import { createElement, createRender } from "../lib/skeleton/index.js"; -import WithShell from "../components/decorator_shell_filemanager.js"; +import rxjs, { effect } from "../lib/rx.js"; +import { ApplicationError } from "../lib/error.js"; +import { basename } from "../lib/path.js"; import { loadCSS } from "../helpers/loader.js"; +import WithShell from "../components/decorator_shell_filemanager.js"; +import { get as getConfig } from "../model/config.js"; + +import { opener } from "./viewerpage/mimetype.js"; +import { getCurrentPath } from "./viewerpage/common.js"; import "../components/breadcrumb.js"; -function opener() { - return "_"; -}; +const mime$ = getConfig().pipe( + rxjs.map((config) => config.mime), + rxjs.shareReplay(), +); function loadModule(appName) { switch(appName) { @@ -14,23 +22,32 @@ function loadModule(appName) { return import("./viewerpage/application_codemirror.js"); case "pdf": return import("./viewerpage/application_pdf.js"); + case "download": + return import("./viewerpage/application_downloader.js"); + default: + throw new ApplicationError("Internal Error", `Unknown opener app "${appName}" at "${getCurrentPath()}"`); } - return import("./viewerpage/application_downloader.js"); }; export default WithShell(async function(render) { const $page = createElement(`
`); render($page); - const module = await loadModule(opener()); - module.default(createRender($page)); + effect(mime$.pipe( + rxjs.map((mimes) => opener(basename(getCurrentPath()), mimes)), + rxjs.mergeMap(([opener]) => loadModule(opener)), + rxjs.map((module) => module.default(createRender($page))), + )); }) export async function init() { - const module = await loadModule(opener()); return Promise.all([ loadCSS(import.meta.url, "./ctrl_viewerpage.css"), loadCSS(import.meta.url, "../components/decorator_shell_filemanager.css"), - typeof module.init === "function" ? module.init() : Promise.resolve(), + mime$.pipe( + rxjs.map((mimes) => opener(basename(getCurrentPath()), mimes)), + rxjs.mergeMap(([opener]) => loadModule(opener)), + rxjs.mergeMap((module) => typeof module.init === "function"? module.init() : rxjs.EMPTY), + ).toPromise(), ]); } diff --git a/public/assets/pages/viewerpage/application_pdf.css b/public/assets/pages/viewerpage/application_pdf.css index 0e98c025..254a1106 100644 --- a/public/assets/pages/viewerpage/application_pdf.css +++ b/public/assets/pages/viewerpage/application_pdf.css @@ -1,11 +1,17 @@ .component_page_viewerpage .component_pdfviewer { display: flex; flex-direction: column; + background: #525659; + text-align:center; } .component_page_viewerpage .component_pdfviewer [data-bind="pdf"] { overflow-y: scroll; flex: 1 1 auto; } +.component_page_viewerpage .component_pdfviewer [data-bind="pdf"] embed { + width:100%; + height:100%; +} .component_page_viewerpage .component_pdfviewer [data-bind="pdf"] component-icon[name="loading"] { padding-top: 75px; display: block; diff --git a/public/assets/pages/viewerpage/application_pdf.js b/public/assets/pages/viewerpage/application_pdf.js index 2fd1b887..dd78d411 100644 --- a/public/assets/pages/viewerpage/application_pdf.js +++ b/public/assets/pages/viewerpage/application_pdf.js @@ -1,89 +1,98 @@ import { createElement } from "../../lib/skeleton/index.js"; +import rxjs, { effect, onLoad } from "../../lib/rx.js"; import { animate, opacityIn } from "../../lib/animate.js"; import { qs } from "../../lib/dom.js"; +import { createLoader } from "../../components/loader.js"; import { loadCSS, loadJS } from "../../helpers/loader.js"; import { join } from "../../lib/path.js"; +import ctrlError from "../ctrl_error.js"; import { getFilename, getDownloadUrl } from "./common.js"; import "../../components/menubar.js"; import "../../components/icon.js"; -const hasNativePDF = "application/pdf" in navigator.mimeTypes; +// const hasNativePDF = "application/pdf" in navigator.mimeTypes; +const hasNativePDF = true export default async function(render) { - hasNativePDF ? pdfNative(render) : pdfJs(render); + const ctrl = hasNativePDF ? ctrlPDFNative : ctrlPDFJs; + ctrl(render); } -export function init() { - if (hasNativePDF) return Promise.resolve(); - - return Promise.all([ - loadJS(import.meta.url, "../../lib/vendor/pdfjs/pdf.js", { type: "module" }), - loadJS(import.meta.url, "../../lib/vendor/pdfjs/pdf.worker.js", { type: "module" }), - loadCSS(import.meta.url, "./application_pdf.css"), - ]).then(() => { - pdfjsLib.GlobalWorkerOptions.workerSrc = join(import.meta.url, "../../lib/vendor/pdfjs/pdf.worker.js"); - }); -} - -function pdfNative(render) { +function ctrlPDFNative(render) { const $page = createElement(` -
+
- +
+ +
`); render($page); - const $embed = $page.querySelector("embed"); - $embed.onload = () => { - $embed.style.opacity = 1; - animate($embed, { time: 300, keyframes: opacityIn() }); - }; + const removeLoader = createLoader(qs($page, `[data-bind="pdf"]`)); + effect(onLoad(qs($page, "embed")).pipe( + removeLoader, + rxjs.tap(($embed) => $embed.classList.remove("hidden")), + rxjs.tap(($embed) => animate($embed, { time: 200, keyframes: opacityIn() })), + rxjs.catchError(ctrlError(render)), + )); } -async function pdfJs(render) { +async function ctrlPDFJs(render) { const $page = createElement(` -
+
`); render($page); - - const createBr = () => $container.appendChild(document.createElement("br")); const $container = qs($page, `[data-bind="pdf"]`); - const timeoutID = window.setTimeout(() => { - const $icon = createElement(``); - $container.appendChild($icon); - }, 300); - const pdf = await pdfjsLib.getDocument(getDownloadUrl()).promise; - clearTimeout(timeoutID); - $container.innerHTML = ""; - createBr(); - for (let i=0; i requestAnimationFrame(done)); + const createBr = () => $container.appendChild(document.createElement("br")); + const removeLoader = createLoader($container); + effect(rxjs.from(pdfjsLib.getDocument(getDownloadUrl()).promise).pipe( + removeLoader, + rxjs.mergeMap(async (pdf) => { + createBr(); + for (let i=0; i requestAnimationFrame(done)); + } + createBr(); + }), + rxjs.catchError(ctrlError(render)), + )); +} + +export function init() { + const deps = [ + loadCSS(import.meta.url, "./application_pdf.css"), + ]; + if (!hasNativePDF) { + deps.push(loadJS(import.meta.url, "../../lib/vendor/pdfjs/pdf.js", { type: "module" })); + deps.push(loadJS(import.meta.url, "../../lib/vendor/pdfjs/pdf.worker.js", { type: "module" }).then(() => { + pdfjsLib.GlobalWorkerOptions.workerSrc = join(import.meta.url, "../../lib/vendor/pdfjs/pdf.worker.js"); + })); } - for (let i=0; i<4; i++) createBr(); + return Promise.all(deps); } diff --git a/public/assets/pages/viewerpage/common.js b/public/assets/pages/viewerpage/common.js index 906d4c94..fcd8b64a 100644 --- a/public/assets/pages/viewerpage/common.js +++ b/public/assets/pages/viewerpage/common.js @@ -8,13 +8,13 @@ export function getDownloadUrl() { return "/api/files/cat?path=" + getCurrentPath().replace(/%23/g, "#"); } -function getCurrentPath() { - return location.pathname.replace("/view", "") + (location.hash || ""); +export function getCurrentPath() { + return decodeURIComponent(location.pathname.replace("/view", "") + (location.hash || "")); } -function prepare(path) { - return encodeURIComponent(decodeURIComponent(path.replace(/%/g, "%25"))); -} +// function prepare(path) { +// return encodeURIComponent(decodeURIComponent(path.replace(/%/g, "%25"))); +// } function appendShareToUrl() { // TODO diff --git a/public/assets/pages/viewerpage/mimetype.js b/public/assets/pages/viewerpage/mimetype.js new file mode 100644 index 00000000..9fe270c7 --- /dev/null +++ b/public/assets/pages/viewerpage/mimetype.js @@ -0,0 +1,38 @@ +export function opener(file = "", mimes) { + const mime = getMimeType(file, mimes); + const type = mime.split("/")[0]; + + if (window.overrides && typeof window.overrides["xdg-open"] === "function") { + const openerFromPlugin = window.overrides["xdg-open"](mime); + if (openerFromPlugin !== null) { + return openerFromPlugin; + } + } + + if (type === "text") { + return ["editor", null]; + } else if (mime === "application/pdf") { + return ["pdf", null]; + } else if (type === "image") { + return ["image", null]; + } else if (["application/javascript", "application/xml", "application/json", + "application/x-perl"].indexOf(mime) !== -1) { + return ["editor", null]; + } else if (["audio/wave", "audio/mp3", "audio/flac", "audio/ogg"].indexOf(mime) !== -1) { + return ["audio", null]; + } else if (mime === "application/x-form") { + return ["form", null]; + } else if (type === "video" || mime === "application/ogg") { + return ["video", null]; + } else if(["application/epub+zip"].indexOf(mime) !== -1) { + return ["ebook", null]; + } else if (type === "application") { + return ["download", null]; + } + + return ["editor", null]; +} + +function getMimeType(file, mimes = {}) { + return mimes[file.split(".")[1]] || "text/plain"; +}