feature (viewerpage): rewrite pdf app with new frontend

This commit is contained in:
MickaelK 2023-12-12 23:23:04 +11:00
parent 4e49d9054a
commit addf2ea8b1
7 changed files with 179 additions and 98 deletions

View file

@ -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(`<component-icon name="loading"></component-icon>`);
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("<component-loader></component-loader>");
export function toggle($node, show = false) {
if (show === true) return tap(() => $node.appendChild(createElement("<component-loader></component-loader>")));
else return tap(() => $node.querySelector("component-loader")?.remove());
if (show === true) return rxjs.tap(() => $node.appendChild(createElement("<component-loader></component-loader>")));
else return rxjs.tap(() => $node.querySelector("component-loader")?.remove());
}

View file

@ -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);
});
}

View file

@ -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(`<div class="component_page_viewerpage"></div>`);
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(),
]);
}

View file

@ -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;

View file

@ -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(`
<div class="component_pdfviewer" style="background: #525659">
<div class="component_pdfviewer">
<component-menubar></component-menubar>
<embed
style="width:100%;height:100%;opacity:0"
src="${getDownloadUrl()}#toolbar=0"
type="application/pdf"
/>
<div data-bind="pdf">
<embed
class="hidden"
src="${getDownloadUrl()}#toolbar=0"
type="application/pdf"
/>
</div>
</div>
`);
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(`
<div class="component_pdfviewer" style="background: #525659;text-align:center;">
<div class="component_pdfviewer">
<component-menubar></component-menubar>
<div data-bind="pdf"></div>
</div>
`);
render($page);
const createBr = () => $container.appendChild(document.createElement("br"));
const $container = qs($page, `[data-bind="pdf"]`);
const timeoutID = window.setTimeout(() => {
const $icon = createElement(`<component-icon name="loading"></component-icon>`);
$container.appendChild($icon);
}, 300);
const pdf = await pdfjsLib.getDocument(getDownloadUrl()).promise;
clearTimeout(timeoutID);
$container.innerHTML = "";
createBr();
for (let i=0; i<pdf.numPages; i++) {
const page = await pdf.getPage(i + 1);
const viewport = page.getViewport({
scale: Math.min(
Math.max(document.body.clientWidth - 200, 0),
800,
) / page.getViewport({ scale: 1 }).width,
});
const $canvas = document.createElement("canvas");
$canvas.height = viewport.height;
$canvas.width = viewport.width;
$container.appendChild($canvas);
await page.render({
canvasContext: $canvas.getContext("2d"),
viewport: viewport,
});
if (i % 5 === 0) await new Promise((done) => requestAnimationFrame(done));
}
for (let i=0; i<4; i++) createBr();
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<pdf.numPages; i++) {
const page = await pdf.getPage(i + 1);
const viewport = page.getViewport({
scale: Math.min(
Math.max(document.body.clientWidth - 200, 0),
800,
) / page.getViewport({ scale: 1 }).width,
});
const $canvas = document.createElement("canvas");
$canvas.height = viewport.height;
$canvas.width = viewport.width;
$container.appendChild($canvas);
await page.render({
canvasContext: $canvas.getContext("2d"),
viewport: viewport,
});
if (i % 2 === 0) await new Promise((done) => 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");
}));
}
return Promise.all(deps);
}

View file

@ -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

View file

@ -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";
}