mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-25 09:42:53 +01:00
feature (viewerpage): rewrite pdf app with new frontend
This commit is contained in:
parent
4e49d9054a
commit
addf2ea8b1
7 changed files with 179 additions and 98 deletions
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
38
public/assets/pages/viewerpage/mimetype.js
Normal file
38
public/assets/pages/viewerpage/mimetype.js
Normal 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";
|
||||
}
|
||||
Loading…
Reference in a new issue