chore (maintenance): tsconfig setup

This commit is contained in:
MickaelK 2024-08-14 08:33:02 +10:00
parent 28b645fb19
commit d6613c4452
67 changed files with 522 additions and 416 deletions

View file

@ -11,7 +11,7 @@ export default async function main() {
window.dispatchEvent(new window.Event("pagechange")); window.dispatchEvent(new window.Event("pagechange"));
} catch (err) { } catch (err) {
console.error(err); console.error(err);
const msg = window.navigator.onLine === false ? "OFFLINE" : (err.message || "CAN'T LOAD"); const msg = window.navigator.onLine === false ? "OFFLINE" : (err instanceof Error && err.message) || "CAN'T LOAD";
report("boot::" + msg, err, location.href); report("boot::" + msg, err, location.href);
$error(msg); $error(msg);
} }

View file

@ -12,7 +12,6 @@ export default async function main() {
setup_translation(), setup_translation(),
setup_xdg_open(), setup_xdg_open(),
setup_device(), setup_device(),
// setup_sw(), // TODO
setup_blue_death_screen(), setup_blue_death_screen(),
setup_history(), setup_history(),
setup_polyfill(), setup_polyfill(),
@ -26,7 +25,7 @@ export default async function main() {
window.dispatchEvent(new window.Event("pagechange")); window.dispatchEvent(new window.Event("pagechange"));
} catch (err) { } catch (err) {
console.error(err); console.error(err);
const msg = window.navigator.onLine === false ? "OFFLINE" : (err.message || "CAN'T LOAD"); const msg = window.navigator.onLine === false ? "OFFLINE" : (err instanceof Error && err.message) || "CAN'T LOAD";
report(msg, err, location.href); report(msg, err, location.href);
$error(msg); $error(msg);
} }
@ -69,23 +68,6 @@ async function setup_device() {
}); });
} }
async function setup_sw() { // eslint-disable-line no-unused-vars
if (!("serviceWorker" in window.navigator)) return;
if (window.navigator.userAgent.indexOf("Mozilla/") !== -1 &&
window.navigator.userAgent.indexOf("Firefox/") !== -1 &&
window.navigator.userAgent.indexOf("Gecko/") !== -1) {
// Firefox was acting weird with service worker so we disabled it
// see: https://github.com/mickael-kerjean/filestash/issues/255
return;
}
try {
await window.navigator.serviceWorker.register("/sw_cache.js");
} catch (err) {
report("ServiceWorker registration failed", err);
}
}
async function setup_blue_death_screen() { async function setup_blue_death_screen() {
window.onerror = function(msg, url, lineNo, colNo, error) { window.onerror = function(msg, url, lineNo, colNo, error) {
report(msg, error, url, lineNo, colNo); report(msg, error, url, lineNo, colNo);
@ -116,7 +98,7 @@ async function setup_history() {
} }
async function setup_title() { async function setup_title() {
document.title = window.CONFIG.name || "Filestash"; document.title = window.CONFIG["name"] || "Filestash";
} }
async function setup_polyfill() { async function setup_polyfill() {

View file

@ -13,10 +13,10 @@ const mv = (from, to) => withVirtualLayer(
mvVL(from, to), mvVL(from, to),
); );
class ComponentBreadcrumb extends window.HTMLElement { class ComponentBreadcrumb extends HTMLElement {
constructor() { constructor() {
super(); super();
if (new window.URL(location.href).searchParams.get("nav") === "false") { if (new URL(location.href).searchParams.get("nav") === "false") {
this.disabled = true; this.disabled = true;
return; return;
} }
@ -65,15 +65,15 @@ class ComponentBreadcrumb extends window.HTMLElement {
const tasks = []; const tasks = [];
for (let i=0; i<nToAnimate; i++) { for (let i=0; i<nToAnimate; i++) {
const n = previousChunks.length - i - 1; const n = previousChunks.length - i - 1;
const $chunk = assert.type(this.querySelector(`.component_path-element.n${n}`), window.HTMLElement); const $chunk = assert.type(this.querySelector(`.component_path-element.n${n}`), HTMLElement);
tasks.push(animate($chunk, { time: 100, keyframes: slideYOut(-10) })); tasks.push(animate($chunk, { time: 100, keyframes: slideYOut(-10) }));
} }
await Promise.all(tasks); await Promise.all(tasks);
} }
// STEP2: setup the actual content // STEP2: setup the actual content
assert.type(this.querySelector(`[data-bind="path"]`), window.HTMLElement).innerHTML = pathChunks.map((chunk, idx) => { assert.type(this.querySelector(`[data-bind="path"]`), HTMLElement).innerHTML = pathChunks.map((chunk, idx) => {
const label = idx === 0 ? (window.CONFIG.name || "Filestash") : chunk; const label = idx === 0 ? (window.CONFIG["name"] || "Filestash") : chunk;
const link = pathChunks.slice(0, idx + 1).join("/") + "/"; const link = pathChunks.slice(0, idx + 1).join("/") + "/";
const limitSize = (word, highlight = false) => { const limitSize = (word, highlight = false) => {
if (highlight === true && word.length > 30) { if (highlight === true && word.length > 30) {
@ -130,7 +130,7 @@ class ComponentBreadcrumb extends window.HTMLElement {
const nToAnimate = pathChunks.length - previousChunks.length; const nToAnimate = pathChunks.length - previousChunks.length;
for (let i=0; i<nToAnimate; i++) { for (let i=0; i<nToAnimate; i++) {
const n = pathChunks.length - i - 1; const n = pathChunks.length - i - 1;
const $chunk = assert.type(this.querySelector(`.component_path-element.n${n}`), window.HTMLElement); const $chunk = assert.type(this.querySelector(`.component_path-element.n${n}`), HTMLElement);
await animate($chunk, { time: 100, keyframes: slideYIn(-5) }); await animate($chunk, { time: 100, keyframes: slideYIn(-5) });
} }
} }
@ -140,8 +140,8 @@ class ComponentBreadcrumb extends window.HTMLElement {
let state = this.hasAttribute("indicator"); let state = this.hasAttribute("indicator");
if (state && this.getAttribute("indicator") !== "false") state = true; if (state && this.getAttribute("indicator") !== "false") state = true;
let $indicator = assert.type(this.querySelector(`[data-bind="path"]`), window.HTMLElement); let $indicator = assert.type(this.querySelector(`[data-bind="path"]`), HTMLElement);
$indicator = assert.type($indicator.lastChild.querySelector("span"), window.HTMLElement); $indicator = assert.type($indicator.lastChild.querySelector("span"), HTMLElement);
if (state) { if (state) {
$indicator.style.opacity = 1; $indicator.style.opacity = 1;
@ -162,8 +162,9 @@ class ComponentBreadcrumb extends window.HTMLElement {
} }
setupDragDropTarget() { setupDragDropTarget() {
this.querySelectorAll("a.label").forEach(($folder) => { this.querySelectorAll("a.label").forEach(($elmnt) => {
const $path = $folder.closest(".component_path-element"); const $folder = assert.type($elmnt, HTMLElement);
const $path = assert.truthy($folder.closest(".component_path-element"));
$folder.ondrop = async(e) => { $folder.ondrop = async(e) => {
$path.classList.remove("highlight"); $path.classList.remove("highlight");
const from = e.dataTransfer.getData("path"); const from = e.dataTransfer.getData("path");

View file

@ -45,7 +45,7 @@ export default function(ctrl) {
effect(rxjs.fromEvent(window, "keydown").pipe( effect(rxjs.fromEvent(window, "keydown").pipe(
rxjs.filter((e) => regexStartFiles.test(fromHref(location.pathname)) && rxjs.filter((e) => regexStartFiles.test(fromHref(location.pathname)) &&
e.keyCode === 8 && e.keyCode === 8 &&
assert.type(document.activeElement, window.HTMLElement).nodeName !== "INPUT"), // backspace in filemanager assert.type(document.activeElement, HTMLElement).nodeName !== "INPUT"), // backspace in filemanager
rxjs.tap(() => { rxjs.tap(() => {
const p = location.pathname.replace(new RegExp("/$"), "").split("/"); const p = location.pathname.replace(new RegExp("/$"), "").split("/");
p.pop(); p.pop();

View file

@ -3,7 +3,7 @@ import { animate, slideYIn } from "../lib/animate.js";
import assert from "../lib/assert.js"; import assert from "../lib/assert.js";
import { loadCSS } from "../helpers/loader.js"; import { loadCSS } from "../helpers/loader.js";
export default class ComponentDropdown extends HTMLDivElement { export default class ComponentDropdown extends HTMLElement {
constructor() { constructor() {
super(); super();
this.render(); this.render();
@ -78,9 +78,9 @@ export default class ComponentDropdown extends HTMLDivElement {
`)); `));
const setActive = () => this.classList.toggle("active"); const setActive = () => this.classList.toggle("active");
assert.type(this.querySelector(".dropdown_button"), window.HTMLElement).onclick = () => { assert.type(this.querySelector(".dropdown_button"), HTMLElement).onclick = () => {
setActive(); setActive();
animate(assert.type(this.querySelector(".dropdown_container"), window.HTMLElement), { animate(assert.type(this.querySelector(".dropdown_container"), HTMLElement), {
time: 100, time: 100,
keyframes: slideYIn(2), keyframes: slideYIn(2),
}); });
@ -88,4 +88,4 @@ export default class ComponentDropdown extends HTMLDivElement {
} }
} }
customElements.define("component-dropdown", ComponentDropdown, { extends: "div" }); customElements.define("component-dropdown", ComponentDropdown);

View file

@ -1,7 +1,7 @@
import { loadCSS } from "../helpers/loader.js"; import { loadCSS } from "../helpers/loader.js";
import assert from "../lib/assert.js"; import assert from "../lib/assert.js";
export default class ComponentFab extends window.HTMLButtonElement { export default class ComponentFab extends HTMLButtonElement {
constructor() { constructor() {
super(); super();
this.innerHTML = `<div class="content"></div>`; this.innerHTML = `<div class="content"></div>`;
@ -10,7 +10,7 @@ export default class ComponentFab extends window.HTMLButtonElement {
async render($icon) { async render($icon) {
await loadCSS(import.meta.url, "./fab.css"); await loadCSS(import.meta.url, "./fab.css");
assert.type(this.querySelector(".content"), window.HTMLElement).replaceChildren($icon); assert.type(this.querySelector(".content"), HTMLElement).replaceChildren($icon);
} }
} }

View file

@ -78,7 +78,7 @@ export function $renderInput(options = {}) {
class="component_input" class="component_input"
/> />
`); `);
if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); if (!($input instanceof HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input");
else if (value) $input.value = value; else if (value) $input.value = value;
attrs.map((setAttribute) => setAttribute($input)); attrs.map((setAttribute) => setAttribute($input));
@ -135,7 +135,7 @@ export function $renderInput(options = {}) {
class="component_input" class="component_input"
/> />
`); `);
if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); if (!($input instanceof HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input");
else if (value) $input.value = value; else if (value) $input.value = value;
attrs.map((setAttribute) => setAttribute($input)); attrs.map((setAttribute) => setAttribute($input));
return $input; return $input;
@ -151,14 +151,14 @@ export function $renderInput(options = {}) {
</div> </div>
`); `);
const $input = $div.querySelector("input"); const $input = $div.querySelector("input");
if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); if (!($input instanceof HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input");
else if (value) $input.value = value; else if (value) $input.value = value;
attrs.map((setAttribute) => setAttribute($input)); attrs.map((setAttribute) => setAttribute($input));
const $icon = $div.querySelector("component-icon"); const $icon = $div.querySelector("component-icon");
if ($icon instanceof window.HTMLElement) { if ($icon instanceof HTMLElement) {
$icon.onclick = function(e) { $icon.onclick = function(e) {
if (!(e.target instanceof window.HTMLElement)) return; if (!(e.target instanceof HTMLElement)) return;
const $input = e.target?.parentElement?.previousElementSibling; const $input = e.target?.parentElement?.previousElementSibling;
if (!$input) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); if (!$input) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input");
if ($input.getAttribute("type") === "password") $input.setAttribute("type", "text"); if ($input.getAttribute("type") === "password") $input.setAttribute("type", "text");
@ -174,7 +174,7 @@ export function $renderInput(options = {}) {
rows="8" rows="8"
></textarea> ></textarea>
`); `);
if (!($textarea instanceof window.HTMLTextAreaElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); if (!($textarea instanceof HTMLTextAreaElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input");
else if (value) $textarea.value = value; else if (value) $textarea.value = value;
attrs.map((setAttribute) => setAttribute($textarea)); attrs.map((setAttribute) => setAttribute($textarea));
return $textarea; return $textarea;
@ -187,7 +187,7 @@ export function $renderInput(options = {}) {
readonly readonly
/> />
`); `);
if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); if (!($input instanceof HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input");
else if (value) $input.value = value; else if (value) $input.value = value;
attrs.map((setAttribute) => setAttribute($input)); attrs.map((setAttribute) => setAttribute($input));
return $input; return $input;
@ -196,7 +196,7 @@ export function $renderInput(options = {}) {
const $input = createElement(` const $input = createElement(`
<input type="hidden" /> <input type="hidden" />
`); `);
if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); if (!($input instanceof HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input");
else if (value) $input.value = value; else if (value) $input.value = value;
$input.setAttribute("name", path.join(".")); $input.setAttribute("name", path.join("."));
return $input; return $input;
@ -219,7 +219,7 @@ export function $renderInput(options = {}) {
const $select = createElement(` const $select = createElement(`
<select class="component_select"></select> <select class="component_select"></select>
`); `);
if (!($select instanceof window.HTMLSelectElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); if (!($select instanceof HTMLSelectElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input");
else if (value) $select.value = value || props.default; else if (value) $select.value = value || props.default;
attrs.map((setAttribute) => setAttribute($select)); attrs.map((setAttribute) => setAttribute($select));
(options || []).forEach((name) => { (options || []).forEach((name) => {
@ -242,7 +242,7 @@ export function $renderInput(options = {}) {
class="component_input" class="component_input"
/> />
`); `);
if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); if (!($input instanceof HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input");
else if (value) $input.value = value; else if (value) $input.value = value;
attrs.map((setAttribute) => setAttribute($input)); attrs.map((setAttribute) => setAttribute($input));
return $input; return $input;
@ -254,7 +254,7 @@ export function $renderInput(options = {}) {
class="component_input" class="component_input"
/> />
`); `);
if (!($input instanceof window.HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input"); if (!($input instanceof HTMLInputElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: missing input");
else if (value) $input.value = value; else if (value) $input.value = value;
attrs.map((setAttribute) => setAttribute($input)); attrs.map((setAttribute) => setAttribute($input));
return $input; return $input;

View file

@ -1,4 +1,4 @@
class Icon extends window.HTMLElement { class Icon extends HTMLElement {
static get observedAttributes() { static get observedAttributes() {
return ["name"]; return ["name"];
} }

View file

@ -2,10 +2,10 @@ import { createElement, onDestroy } from "../lib/skeleton/index.js";
import rxjs from "../lib/rx.js"; import rxjs from "../lib/rx.js";
import { animate, opacityIn } from "../lib/animate.js"; import { animate, opacityIn } from "../lib/animate.js";
class Loader extends window.HTMLElement { class Loader extends HTMLElement {
constructor() { constructor() {
super(); super();
this.timeout = window.setTimeout(() => { this.timeout = setTimeout(() => {
this.innerHTML = this.render({ this.innerHTML = this.render({
inline: this.hasAttribute("inlined"), inline: this.hasAttribute("inlined"),
}); });
@ -13,7 +13,7 @@ class Loader extends window.HTMLElement {
} }
disconnectedCallback() { disconnectedCallback() {
window.clearTimeout(this.timeout); clearTimeout(this.timeout);
} }
render({ inline }) { render({ inline }) {
@ -61,7 +61,7 @@ export function createLoader($parent, opts = {}) {
<component-icon name="loading"></component-icon> <component-icon name="loading"></component-icon>
</div> </div>
`); `);
const id = window.setTimeout(() => { const id = setTimeout(() => {
append($icon); append($icon);
animate($icon, { time: 750, keyframes: opacityIn() }); animate($icon, { time: 750, keyframes: opacityIn() });
}, wait); }, wait);

View file

@ -6,7 +6,7 @@ import { qs, qsa } from "../lib/dom.js";
import { loadCSS } from "../helpers/loader.js"; import { loadCSS } from "../helpers/loader.js";
export function createModal(opts) { export function createModal(opts) {
const $dom = assert.type(qs(document.body, "component-modal"), window.HTMLElement); const $dom = assert.type(qs(document.body, "component-modal"), HTMLElement);
assert.type($dom, ModalComponent); assert.type($dom, ModalComponent);
return ($node, fn) => $dom.trigger($node, { onQuit: fn, ...opts }); return ($node, fn) => $dom.trigger($node, { onQuit: fn, ...opts });
@ -32,7 +32,7 @@ const $modal = createElement(`
</div> </div>
`); `);
class ModalComponent extends window.HTMLElement { class ModalComponent extends HTMLElement {
async connectedCallback() { async connectedCallback() {
await loadCSS(import.meta.url, "./modal.css"); await loadCSS(import.meta.url, "./modal.css");
} }
@ -123,7 +123,7 @@ class ModalComponent extends window.HTMLElement {
let size = targetHeight; let size = targetHeight;
if (size === null) { if (size === null) {
const $box = document.querySelector("#modal-box > div"); const $box = document.querySelector("#modal-box > div");
if ($box instanceof window.HTMLElement) size = $box.offsetHeight; if ($box instanceof HTMLElement) size = $box.offsetHeight;
} }
size = Math.round((document.body.offsetHeight - size) / 2); size = Math.round((document.body.offsetHeight - size) / 2);
if (size < 0) return 0; if (size < 0) return 0;

View file

@ -16,7 +16,7 @@ const createNotification = async(msg, type) => createElement(`
</span> </span>
`); `);
class NotificationComponent extends window.HTMLElement { class NotificationComponent extends HTMLElement {
buffer = []; buffer = [];
async connectedCallback() { async connectedCallback() {
@ -28,8 +28,8 @@ class NotificationComponent extends window.HTMLElement {
this.buffer.push({ message, type }); this.buffer.push({ message, type });
if (this.buffer.length !== 1) { if (this.buffer.length !== 1) {
const $close = this.querySelector(".close"); const $close = this.querySelector(".close");
if (!($close instanceof window.HTMLElement) || !$close.onclick) return; if (!($close instanceof HTMLElement) || !$close.onclick) return;
$close.onclick(new window.MouseEvent("mousedown")); $close.onclick(new MouseEvent("mousedown"));
return; return;
} }
await this.run(); await this.run();
@ -46,16 +46,16 @@ class NotificationComponent extends window.HTMLElement {
}); });
const ids = []; const ids = [];
await Promise.race([ await Promise.race([
new Promise((done) => ids.push(window.setTimeout(() => { new Promise((done) => ids.push(setTimeout(() => {
done(new window.MouseEvent("mousedown")); done(new MouseEvent("mousedown"));
}, this.buffer.length === 1 ? 8000 : 800))), }, this.buffer.length === 1 ? 8000 : 800))),
new Promise((done) => ids.push(window.setTimeout(() => { new Promise((done) => ids.push(setTimeout(() => {
const $close = $notification.querySelector(".close"); const $close = $notification.querySelector(".close");
if (!($close instanceof window.HTMLElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: notification close button missing"); if (!($close instanceof HTMLElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: notification close button missing");
$close.onclick = done; $close.onclick = done;
}, 1000))), }, 1000))),
]); ]);
ids.forEach((id) => window.clearTimeout(id)); ids.forEach((id) => clearTimeout(id));
await animate($notification, { await animate($notification, {
keyframes: slideYOut(10), keyframes: slideYOut(10),
time: 200, time: 200,

View file

@ -1 +1 @@
export function report(msg: Event|string, error?: Error, link?: string, lineNo?: number, columnno?: number); export function report(msg: Event|string, err?: any, link?: string, lineNo?: number, columnno?: number);

View file

@ -1,4 +1,4 @@
export function report(msg, error, link, lineNo, columnNo) { export function report(msg, err, link, lineNo, columnNo) {
if (window.navigator.onLine === false) return Promise.resolve(); if (window.navigator.onLine === false) return Promise.resolve();
let url = "./report?"; let url = "./report?";
url += "url=" + encodeURIComponent(location.href) + "&"; url += "url=" + encodeURIComponent(location.href) + "&";
@ -6,7 +6,7 @@ export function report(msg, error, link, lineNo, columnNo) {
url += "from=" + encodeURIComponent(link) + "&"; url += "from=" + encodeURIComponent(link) + "&";
url += "from.lineNo=" + lineNo + "&"; url += "from.lineNo=" + lineNo + "&";
url += "from.columnNo=" + columnNo; url += "from.columnNo=" + columnNo;
if (error) url += "error=" + encodeURIComponent(error.message) + "&"; if (err instanceof Error) url += "error=" + encodeURIComponent(err.message) + "&";
return fetch(url, { method: "post" }).catch(() => {}); return fetch(url, { method: "post" }).catch(() => {});
} }

View file

@ -23,7 +23,7 @@ export function animate($node: HTMLElement | null, opts: {
fill?: string; fill?: string;
onExit?: () => void; onExit?: () => void;
onEnter?: () => void; onEnter?: () => void;
}): Promise<void>; }): Promise<() => void>;
export function slideXIn(dist: number): AnimationFrames[]; export function slideXIn(dist: number): AnimationFrames[];

View file

@ -1,14 +1,32 @@
export default class assert { export default class assert {
/**
* @param {*} object
* @param {Function} type
* @param {string} [msg]
* @return {*}
* @throws {TypeError}
*/
static type(object, type, msg) { static type(object, type, msg) {
if (!(object instanceof type)) throw new TypeError(msg || `assertion failed - unexpected type for ${object.toString()}`); if (!(object instanceof type)) throw new TypeError(msg || `assertion failed - unexpected type for ${object.toString()}`);
return object; return object;
} }
/**
* @param {*} object
* @param {string} [msg]
* @return {*}
* @throws {TypeError}
*/
static truthy(object, msg) { static truthy(object, msg) {
if (!object) throw new TypeError(msg || `assertion failed - object is not truthy`); if (!object) throw new TypeError(msg || `assertion failed - object is not truthy`);
return object;
} }
static fail(object, msg) { /**
throw new TypeError(msg || `assertion failed - ${object}`); * @param {string} msg
* @throws {TypeError}
*/
static fail(msg) {
throw new TypeError(msg);
} }
} }

View file

@ -11,7 +11,7 @@ export function join(baseURL, segment) {
} }
export function forwardURLParams(url, allowed = []) { export function forwardURLParams(url, allowed = []) {
const _url = new URL(location.origin + "/" + url); const _url = new URL(window.location.origin + "/" + url);
for (const [key, value] of new URLSearchParams(location.search)) { for (const [key, value] of new URLSearchParams(location.search)) {
if (allowed.indexOf(key) < 0) continue; if (allowed.indexOf(key) < 0) continue;
_url.searchParams.set(key, value); _url.searchParams.set(key, value);

View file

@ -1,7 +1,7 @@
import type { Observer } from "rx-core"; import type { Observer, Observable as coreObservable } from "rx-core";
import { import {
Observable, fromEvent, startWith, fromEvent, startWith, Observable,
catchError, tap, first, of, catchError, tap, first, of,
map, mapTo, filter, mergeMap, EMPTY, empty, map, mapTo, filter, mergeMap, EMPTY, empty,
switchMapTo, switchMap, switchMapTo, switchMap,
@ -74,6 +74,6 @@ export function stateMutation($node: HTMLElement, attr: string);
export function preventDefault(): typeof tap; export function preventDefault(): typeof tap;
export function onClick($node: HTMLElement): typeof fromEvent; export function onClick($node: HTMLElement): ReturnType<typeof fromEvent>;
export function onLoad($node: HTMLElement): typeof fromEvent; export function onLoad($node: HTMLElement): void;

View file

@ -19,19 +19,19 @@ const getFn = (obj, arg0, ...args) => {
}; };
export function applyMutation($node, ...keys) { export function applyMutation($node, ...keys) {
assert.type($node, window.HTMLElement); assert.type($node, HTMLElement);
const execute = getFn($node, ...keys); const execute = getFn($node, ...keys);
return rxjs.tap((val) => Array.isArray(val) ? execute(...val) : execute(val)); return rxjs.tap((val) => Array.isArray(val) ? execute(...val) : execute(val));
} }
export function applyMutations($node, ...keys) { export function applyMutations($node, ...keys) {
assert.type($node, window.HTMLElement); assert.type($node, HTMLElement);
const execute = getFn($node, ...keys); 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) { export function stateMutation($node, attr) {
assert.type($node, window.HTMLElement); assert.type($node, HTMLElement);
return rxjs.tap((val) => $node[attr] = val); return rxjs.tap((val) => $node[attr] = val);
} }
@ -41,19 +41,19 @@ export function preventDefault() {
export function onClick($node) { export function onClick($node) {
const sideE = ($node) => { const sideE = ($node) => {
assert.type($node, window.HTMLElement); assert.type($node, HTMLElement);
return rxjs.fromEvent($node, "click").pipe( return rxjs.fromEvent($node, "click").pipe(
rxjs.map(() => $node) rxjs.map(() => $node)
); );
}; };
if ($node instanceof window.NodeList) return rxjs.merge( if ($node instanceof NodeList) return rxjs.merge(
...[...$node].map(($n) => sideE($n)), ...[...$node].map(($n) => sideE($n)),
); );
return sideE($node); return sideE($node);
} }
export function onLoad($node) { export function onLoad($node) {
assert.type($node, window.HTMLElement); assert.type($node, HTMLElement);
return new rxjs.Observable((observer) => { return new rxjs.Observable((observer) => {
$node.onload = () => { $node.onload = () => {
observer.next($node); observer.next($node);

View file

@ -59,7 +59,7 @@ async function load(route, opts) {
export function createElement(str) { export function createElement(str) {
const $n = window.document.createElement("div"); const $n = window.document.createElement("div");
$n.innerHTML = str; $n.innerHTML = str;
if (!($n.firstElementChild instanceof window.HTMLElement)) throw new Error("createElement - unexpected type"); if (!($n.firstElementChild instanceof HTMLElement)) throw new Error("createElement - unexpected type");
return $n.firstElementChild; return $n.firstElementChild;
} }
@ -73,10 +73,10 @@ export function createFragment(str) {
} }
export function createRender($parent) { export function createRender($parent) {
if (!($parent instanceof window.HTMLElement)) throw new Error(`assert failed: createRender on non HTMLElement`); if (!($parent instanceof HTMLElement)) throw new Error(`assert failed: createRender on non HTMLElement`);
return ($view) => { return ($view) => {
if ($view instanceof window.HTMLElement) $parent.replaceChildren($view); if ($view instanceof HTMLElement) $parent.replaceChildren($view);
else if ($view instanceof window.DocumentFragment) $parent.replaceChildren($view); else if ($view instanceof DocumentFragment) $parent.replaceChildren($view);
else throw new Error(`Unknown view type: ${typeof $view}`); else throw new Error(`Unknown view type: ${typeof $view}`);
return $parent; return $parent;
}; };

View file

@ -1,8 +1,8 @@
const triggerPageChange = () => window.dispatchEvent(new window.Event("pagechange")); const triggerPageChange = () => window.dispatchEvent(new window.Event("pagechange"));
const trimPrefix = (value, prefix) => value.startsWith(prefix) ? value.slice(prefix.length) : value; const trimPrefix = (value = "", prefix) => value.startsWith(prefix) ? value.slice(prefix.length) : value;
const _base = window.document.head.querySelector("base").getAttribute("href").replace(new RegExp("/$"), ""); const _base = window.document.head.querySelector("base")?.getAttribute("href")?.replace(new RegExp("/$"), "");
export const base = () => _base; export const base = () => _base || "";
export const fromHref = (href) => trimPrefix(href, base()); export const fromHref = (href) => trimPrefix(href, base());
export const toHref = (href) => base() + href; export const toHref = (href) => base() + href;

View file

@ -8,6 +8,7 @@
"A_FILE_NAMED_{{VALUE}}_WAS_CREATED": "Un fichier nommé \"{{VALUE}}\" a été créé", "A_FILE_NAMED_{{VALUE}}_WAS_CREATED": "Un fichier nommé \"{{VALUE}}\" a été créé",
"A_FOLDER_NAMED_{{VALUE}}_WAS_CREATED": "Un dossier nommé \"{{VALUE}}\" a été créé", "A_FOLDER_NAMED_{{VALUE}}_WAS_CREATED": "Un dossier nommé \"{{VALUE}}\" a été créé",
"BEAUTIFUL_URL": "id_du_lien", "BEAUTIFUL_URL": "id_du_lien",
"BOOKMARK": "favoris",
"CAMERA": "appareil", "CAMERA": "appareil",
"CANCEL": "annuler", "CANCEL": "annuler",
"CANNOT_ESTABLISH_A_CONNECTION": "Impossible d'établir une connexion", "CANNOT_ESTABLISH_A_CONNECTION": "Impossible d'établir une connexion",
@ -20,6 +21,7 @@
"CONNECT": "connexion", "CONNECT": "connexion",
"CONNECTION_LOST": "Connection perdue", "CONNECTION_LOST": "Connection perdue",
"COPIED_TO_CLIPBOARD": "Copié dans le presse-papier", "COPIED_TO_CLIPBOARD": "Copié dans le presse-papier",
"CREATE_A_TAG": "créer un tag",
"CREATE_A_NEW_LINK": "créer un lien partagé", "CREATE_A_NEW_LINK": "créer un lien partagé",
"CURRENT": "en cours", "CURRENT": "en cours",
"CURRENT_UPLOAD": "en cours", "CURRENT_UPLOAD": "en cours",

View file

@ -17,7 +17,7 @@ function mainReorderKey(argv) {
function mainAddTranslationKey(argv) { function mainAddTranslationKey(argv) {
const key = argv[3]; const key = argv[3];
const filepath = argv[2]; const filepath = argv[2];
if (!filepath) throw new Error("missing args") if (!filepath) throw new Error("missing args");
else if (!key) return; else if (!key) return;
const json = JSON.parse(fs.readFileSync(filepath)); const json = JSON.parse(fs.readFileSync(filepath));
@ -28,6 +28,6 @@ function mainAddTranslationKey(argv) {
// usage: find *.json -type f -exec node script.js {} \; // usage: find *.json -type f -exec node script.js {} \;
(function() { (function() {
mainAddTranslationKey(process.argv) mainAddTranslationKey(process.argv);
mainReorderKey(process.argv) mainReorderKey(process.argv);
})() })();

View file

@ -1,5 +1,5 @@
export async function init() { export async function init() {
if (!window.CONFIG.enable_chromecast) { if (!window.CONFIG["enable_chromecast"]) {
return Promise.resolve(); return Promise.resolve();
} else if (!("chrome" in window)) { } else if (!("chrome" in window)) {
return Promise.resolve(); return Promise.resolve();

View file

@ -1,6 +1,6 @@
import { ApplicationError } from "../../lib/error.js"; import { ApplicationError } from "../../lib/error.js";
class BoxItem extends window.HTMLDivElement { class BoxItem extends HTMLElement {
constructor() { constructor() {
super(); super();
this.attributeChangedCallback(); this.attributeChangedCallback();
@ -42,4 +42,4 @@ class BoxItem extends window.HTMLDivElement {
} }
} }
customElements.define("box-item", BoxItem, { extends: "div" }); customElements.define("box-item", BoxItem);

View file

@ -36,7 +36,7 @@ export default async function(render) {
const init$ = getMiddlewareAvailable().pipe( const init$ = getMiddlewareAvailable().pipe(
rxjs.first(), rxjs.first(),
rxjs.map((specs) => Object.keys(specs).map((label) => createElement(` rxjs.map((specs) => Object.keys(specs).map((label) => createElement(`
<div is="box-item" data-label="${label}"></div> <box-item data-label="${label}"></box-item>
`))), `))),
rxjs.tap(() => { rxjs.tap(() => {
qs($page, "h2").classList.remove("hidden"); qs($page, "h2").classList.remove("hidden");

View file

@ -101,7 +101,7 @@ export function getState() {
if (!authType) return config; if (!authType) return config;
const $formIDP = document.querySelector(`[data-bind="idp"]`); const $formIDP = document.querySelector(`[data-bind="idp"]`);
if (!($formIDP instanceof window.HTMLFormElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: idp isn't a form"); if (!($formIDP instanceof HTMLFormElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: idp isn't a form");
let formValues = [...new FormData($formIDP)]; let formValues = [...new FormData($formIDP)];
config.middleware.identity_provider = { config.middleware.identity_provider = {
type: authType, type: authType,
@ -121,7 +121,7 @@ export function getState() {
}; };
const $formAM = document.querySelector(`[data-bind="attribute-mapping"]`); const $formAM = document.querySelector(`[data-bind="attribute-mapping"]`);
if (!($formAM instanceof window.HTMLFormElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: attribute mapping isn't a form"); if (!($formAM instanceof HTMLFormElement)) throw new ApplicationError("INTERNAL_ERROR", "assumption failed: attribute mapping isn't a form");
formValues = [...new FormData($formAM)]; formValues = [...new FormData($formAM)];
config.middleware.attribute_mapping = { config.middleware.attribute_mapping = {
related_backend: (formValues.shift() || [])[1], related_backend: (formValues.shift() || [])[1],

View file

@ -193,7 +193,6 @@ function componentStep2(render) {
rxjs.filter((config) => config["log"]["telemetry"] !== true), rxjs.filter((config) => config["log"]["telemetry"] !== true),
rxjs.mergeMap(async(config) => { rxjs.mergeMap(async(config) => {
const enabled = await componentTelemetryPopup(createModal({ withButtonsRight: "OK" })); const enabled = await componentTelemetryPopup(createModal({ withButtonsRight: "OK" }));
console.log(enabled);
if (enabled === false) return null; if (enabled === false) return null;
config["log"]["telemetry"] = enabled; config["log"]["telemetry"] = enabled;
return config; return config;

View file

@ -4,6 +4,7 @@ import rxjs, { effect, applyMutation, applyMutations, preventDefault, onClick }
import ajax from "../../lib/ajax.js"; import ajax from "../../lib/ajax.js";
import { qs, qsa, safe } from "../../lib/dom.js"; import { qs, qsa, safe } from "../../lib/dom.js";
import { animate, slideYIn, transition, opacityIn } from "../../lib/animate.js"; import { animate, slideYIn, transition, opacityIn } from "../../lib/animate.js";
import assert from "../../lib/assert.js";
import { createForm } from "../../lib/form.js"; import { createForm } from "../../lib/form.js";
import { settings_get, settings_put } from "../../lib/settings.js"; import { settings_get, settings_put } from "../../lib/settings.js";
import t from "../../locales/index.js"; import t from "../../locales/index.js";
@ -128,7 +129,7 @@ export default async function(render) {
const toggleLoader = (hide) => { const toggleLoader = (hide) => {
if (hide) { if (hide) {
$page.classList.add("hidden"); $page.classList.add("hidden");
$page.parentElement.appendChild($loader); assert.truthy($page.parentElement).appendChild($loader);
} else { } else {
$loader.remove(); $loader.remove();
$page.classList.remove("hidden"); $page.classList.remove("hidden");
@ -216,7 +217,7 @@ export default async function(render) {
// feature7: empty connection handling // feature7: empty connection handling
effect(connections$.pipe( effect(connections$.pipe(
rxjs.filter((conns) => conns.length === 0), rxjs.filter((conns) => conns.length === 0),
rxjs.mergeMap((a) => Promise.reject(new Error("no backend selected"))), rxjs.mergeMap(() => Promise.reject(new Error("there is nothing here"))), // TODO: check translation?
rxjs.catchError(ctrlError()), rxjs.catchError(ctrlError()),
)); ));
} }

View file

@ -18,7 +18,7 @@ export default function(render = createRender(qs(document.body, "[role=\"main\"]
return function(err) { return function(err) {
const [msg, trace] = processError(err); const [msg, trace] = processError(err);
const link = forwardURLParams(calculateBacklink(fromHref(location.pathname)), ["share"]); const link = forwardURLParams(calculateBacklink(fromHref(window.location.pathname)), ["share"]);
const $page = createElement(` const $page = createElement(`
<div> <div>
<style>${css}</style> <style>${css}</style>
@ -32,7 +32,7 @@ export default function(render = createRender(qs(document.body, "[role=\"main\"]
<h2>${t(msg)}</h2> <h2>${t(msg)}</h2>
<p> <p>
<button class="light" data-bind="details">${t("More details")}</button> <button class="light" data-bind="details">${t("More details")}</button>
<button class="primary" data-bind="refresh">${t("Refresh")}</button> <button class="primary" data-bind="refresh">${t("Reload")}</button>
<pre class="hidden"><code>${strToHTML(trace)}</code></pre> <pre class="hidden"><code>${strToHTML(trace)}</code></pre>
</p> </p>
</div> </div>

View file

@ -25,8 +25,8 @@ export default function(render) {
else if (step === "email") return ctrlEmail(render, { shareID, setState }); else if (step === "email") return ctrlEmail(render, { shareID, setState });
else if (step === "code") return ctrlEmailCodeVerification(render, { shareID, setState }); else if (step === "code") return ctrlEmailCodeVerification(render, { shareID, setState });
else if (step === "done") { else if (step === "done") {
if (isDir(state.path)) navigate(toHref(`/files/?share=${shareID}`)); if (isDir(state["path"])) navigate(toHref(`/files/?share=${shareID}`));
else navigate(toHref(`/view/${basename(state.path)}?share=${shareID}&nav=false`)); else navigate(toHref(`/view/${basename(state["path"])}?share=${shareID}&nav=false`));
return rxjs.EMPTY; return rxjs.EMPTY;
} }
else assert.fail(`unknown step: "${step}"`); else assert.fail(`unknown step: "${step}"`);

View file

@ -2,6 +2,7 @@ import { createElement, createRender } from "../lib/skeleton/index.js";
import rxjs, { effect } from "../lib/rx.js"; import rxjs, { effect } from "../lib/rx.js";
import { ApplicationError } from "../lib/error.js"; import { ApplicationError } from "../lib/error.js";
import { basename } from "../lib/path.js"; import { basename } from "../lib/path.js";
import assert from "../lib/assert.js";
import { loadCSS } from "../helpers/loader.js"; import { loadCSS } from "../helpers/loader.js";
import WithShell, { init as initShell } from "../components/decorator_shell_filemanager.js"; import WithShell, { init as initShell } from "../components/decorator_shell_filemanager.js";
import { init as initMenubar } from "./viewerpage/component_menubar.js"; import { init as initMenubar } from "./viewerpage/component_menubar.js";
@ -46,7 +47,7 @@ export default WithShell(async function(render) {
render($page); render($page);
// feature: render viewer application // feature: render viewer application
effect(rxjs.of(window.CONFIG.mime || {}).pipe( effect(rxjs.of(window.CONFIG["mime"] || {}).pipe(
rxjs.map((mimes) => opener(basename(getCurrentPath()), mimes)), rxjs.map((mimes) => opener(basename(getCurrentPath()), mimes)),
rxjs.mergeMap(([opener, opts]) => rxjs.from(loadModule(opener)).pipe(rxjs.tap((module) => { rxjs.mergeMap(([opener, opts]) => rxjs.from(loadModule(opener)).pipe(rxjs.tap((module) => {
module.default(createRender($page), { ...opts, acl$: options() }); module.default(createRender($page), { ...opts, acl$: options() });
@ -58,8 +59,9 @@ export default WithShell(async function(render) {
effect(rxjs.of(new URL(location.toString()).searchParams.get("nav")).pipe( effect(rxjs.of(new URL(location.toString()).searchParams.get("nav")).pipe(
rxjs.filter((value) => value === "false"), rxjs.filter((value) => value === "false"),
rxjs.tap(() => { rxjs.tap(() => {
$page.parentElement.style.border = "none"; const $parent = assert.truthy($page.parentElement);
$page.parentElement.style.borderRadius = "0"; $parent.style.border = "none";
$parent.style.borderRadius = "0";
}), }),
)); ));
}); });
@ -68,7 +70,7 @@ export async function init() {
return Promise.all([ return Promise.all([
loadCSS(import.meta.url, "./ctrl_viewerpage.css"), loadCSS(import.meta.url, "./ctrl_viewerpage.css"),
initShell(), initMenubar(), initCache(), initShell(), initMenubar(), initCache(),
rxjs.of(window.CONFIG.mime || {}).pipe( rxjs.of(window.CONFIG["mime"] || {}).pipe(
rxjs.map((mimes) => opener(basename(getCurrentPath()), mimes)), rxjs.map((mimes) => opener(basename(getCurrentPath()), mimes)),
rxjs.mergeMap(([opener]) => loadModule(opener)), rxjs.mergeMap(([opener]) => loadModule(opener)),
rxjs.mergeMap((module) => typeof module.init === "function"? module.init() : rxjs.EMPTY), rxjs.mergeMap((module) => typeof module.init === "function"? module.init() : rxjs.EMPTY),

View file

@ -1,17 +1,38 @@
import assert from "../../lib/assert.js";
import { getSession } from "../../model/session.js"; import { getSession } from "../../model/session.js";
class ICache { class ICache {
async get() { throw new Error("NOT_IMPLEMENTED"); } /**
* @param {string} _path
* @return {Promise<any>}
*/
async get(_path) { throw new Error("NOT_IMPLEMENTED"); }
async store() { throw new Error("NOT_IMPLEMENTED"); } /**
* @param {string} _path
* @param {any} _data
* @return {Promise<void>}
*/
async store(_path, _data) { throw new Error("NOT_IMPLEMENTED"); }
/**
* @return {Promise<void>}
*/
async remove() { throw new Error("NOT_IMPLEMENTED"); } async remove() { throw new Error("NOT_IMPLEMENTED"); }
/**
* @param {string} path
* @param {function(any): any} fn
* @return {Promise<void>}
*/
async update(path, fn) { async update(path, fn) {
const data = await this.get(path); const data = await this.get(path);
return this.store(path, fn(data || {})); return this.store(path, fn(data || {}));
} }
/**
* @return {Promise<void>}
*/
async destroy() { throw new Error("NOT_IMPLEMENTED"); } async destroy() { throw new Error("NOT_IMPLEMENTED"); }
} }
@ -21,14 +42,23 @@ class InMemoryCache extends ICache {
this.data = {}; this.data = {};
} }
/**
* @override
*/
async get(path) { async get(path) {
return this.data[this._key(path)] || null; return this.data[this._key(path)] || null;
} }
/**
* @override
*/
async store(path, obj) { async store(path, obj) {
this.data[this._key(path)] = obj; this.data[this._key(path)] = obj;
} }
/**
* @override
*/
async remove(path, exact = true) { async remove(path, exact = true) {
if (!path) { if (!path) {
this.data = {}; this.data = {};
@ -46,6 +76,9 @@ class InMemoryCache extends ICache {
} }
} }
/**
* @override
*/
async destroy() { async destroy() {
this.data = {}; this.data = {};
} }
@ -58,7 +91,7 @@ class InMemoryCache extends ICache {
class IndexDBCache extends ICache { class IndexDBCache extends ICache {
DB_VERSION = 5; DB_VERSION = 5;
FILE_PATH = "file_path"; FILE_PATH = "file_path";
db = null; /** @type {Promise<IDBDatabase> | null} */ db = null;
constructor() { constructor() {
super(); super();
@ -68,25 +101,31 @@ class IndexDBCache extends ICache {
this.db = new Promise((done, err) => { this.db = new Promise((done, err) => {
request.onsuccess = (e) => { request.onsuccess = (e) => {
done(e.target.result); done(assert.truthy(e.target).result);
}; };
request.onerror = () => err(new Error("INDEXEDDB_NOT_SUPPORTED")); request.onerror = () => err(new Error("INDEXEDDB_NOT_SUPPORTED"));
}); });
} }
/**
* @override
*/
async get(path) { async get(path) {
const db = await this.db; const db = assert.truthy(await this.db);
const tx = db.transaction(this.FILE_PATH, "readonly"); const tx = db.transaction(this.FILE_PATH, "readonly");
const store = tx.objectStore(this.FILE_PATH); const store = tx.objectStore(this.FILE_PATH);
const query = store.get(this._key(path)); const query = store.get(this._key(path));
return await new Promise((done) => { return await new Promise((done) => {
query.onsuccess = (e) => done(query.result || null); query.onsuccess = () => done(query.result || null);
query.onerror = () => done(null); query.onerror = () => done(null);
}); });
} }
/**
* @override
*/
async store(path, value = {}) { async store(path, value = {}) {
const db = await this.db; const db = assert.truthy(await this.db);
const tx = db.transaction(this.FILE_PATH, "readwrite"); const tx = db.transaction(this.FILE_PATH, "readwrite");
const store = tx.objectStore(this.FILE_PATH); const store = tx.objectStore(this.FILE_PATH);
@ -103,8 +142,11 @@ class IndexDBCache extends ICache {
}); });
} }
/**
* @override
*/
async remove(path, exact = true) { async remove(path, exact = true) {
const db = await this.db; const db = assert.truthy(await this.db);
const tx = db.transaction(this.FILE_PATH, "readwrite"); const tx = db.transaction(this.FILE_PATH, "readwrite");
const store = tx.objectStore(this.FILE_PATH); const store = tx.objectStore(this.FILE_PATH);
const key = this._key(path); const key = this._key(path);
@ -179,7 +221,7 @@ export async function init() {
cache = new InMemoryCache(); cache = new InMemoryCache();
if (!("indexedDB" in window)) return; if (!("indexedDB" in window)) return;
cache = new IndexDBCache(); cache = assert.truthy(new IndexDBCache());
return cache.db.catch((err) => { return cache.db.catch((err) => {
if (err.message === "INDEXEDDB_NOT_SUPPORTED") { if (err.message === "INDEXEDDB_NOT_SUPPORTED") {
// Firefox in private mode act like if it supports indexedDB but // Firefox in private mode act like if it supports indexedDB but

View file

@ -77,7 +77,7 @@ export default async function(render) {
)), )),
rxjs.mergeMap(({ show_hidden, files, ...rest }) => { rxjs.mergeMap(({ show_hidden, files, ...rest }) => {
if (show_hidden === false) files = files.filter(({ name }) => name[0] !== "."); if (show_hidden === false) files = files.filter(({ name }) => name[0] !== ".");
files = sort(files, rest.sort, rest.order); files = sort(files, rest["sort"], rest["order"]);
return rxjs.of({ ...rest, files }); return rxjs.of({ ...rest, files });
}), }),
rxjs.map((data) => ({ ...data, count: count++ })), rxjs.map((data) => ({ ...data, count: count++ })),
@ -265,7 +265,7 @@ export default async function(render) {
rxjs.filter((e) => e.key === "a" && rxjs.filter((e) => e.key === "a" &&
(e.ctrlKey || e.metaKey) && (e.ctrlKey || e.metaKey) &&
(files$.value || []).length > 0 && (files$.value || []).length > 0 &&
assert.type(document.activeElement, window.HTMLElement).tagName !== "INPUT"), assert.type(document.activeElement, HTMLElement).tagName !== "INPUT"),
preventDefault(), preventDefault(),
rxjs.tap(() => { rxjs.tap(() => {
clearSelection(); clearSelection();
@ -289,10 +289,10 @@ export default async function(render) {
)); ));
effect(getSelection$().pipe(rxjs.tap(() => { effect(getSelection$().pipe(rxjs.tap(() => {
for (const $thing of $page.querySelectorAll(".component_thing")) { for (const $thing of $page.querySelectorAll(".component_thing")) {
const checked = isSelected(parseInt($thing.getAttribute("data-n"))); const checked = isSelected(parseInt(assert.truthy($thing.getAttribute("data-n"))));
$thing.classList.add(checked ? "selected" : "not-selected"); $thing.classList.add(checked ? "selected" : "not-selected");
$thing.classList.remove(checked ? "not-selected" : "selected"); $thing.classList.remove(checked ? "not-selected" : "selected");
qs($thing, `input[type="checkbox"]`).checked = checked; qs(assert.type($thing, HTMLElement), `input[type="checkbox"]`).checked = checked;
}; };
}))); })));

View file

@ -1,4 +1,4 @@
import { createElement } from "../../lib/skeleton/index.js"; import { createElement, nop } from "../../lib/skeleton/index.js";
import rxjs, { effect, onClick, preventDefault } from "../../lib/rx.js"; import rxjs, { effect, onClick, preventDefault } from "../../lib/rx.js";
import { qs } from "../../lib/dom.js"; import { qs } from "../../lib/dom.js";
import { animate } from "../../lib/animate.js"; import { animate } from "../../lib/animate.js";
@ -71,7 +71,7 @@ export default async function(render) {
$icon.setAttribute("alt", alt); $icon.setAttribute("alt", alt);
$input.value = ""; $input.value = "";
$input.nextSibling.setAttribute("name", alt); $input.nextSibling.setAttribute("name", alt);
let done = Promise.resolve(); let done = Promise.resolve(nop);
if ($node.classList.contains("hidden")) done = animate($node, { if ($node.classList.contains("hidden")) done = animate($node, {
keyframes: [{ height: `0px` }, { height: "50px" }], keyframes: [{ height: `0px` }, { height: "50px" }],
time: 100, time: 100,

View file

@ -2,6 +2,7 @@ import { createElement, createRender, createFragment, onDestroy } from "../../li
import rxjs, { effect, onClick, preventDefault } from "../../lib/rx.js"; import rxjs, { effect, onClick, preventDefault } from "../../lib/rx.js";
import { animate, slideXIn, slideYIn } from "../../lib/animate.js"; import { animate, slideXIn, slideYIn } from "../../lib/animate.js";
import { loadCSS } from "../../helpers/loader.js"; import { loadCSS } from "../../helpers/loader.js";
import assert from "../../lib/assert.js";
import { qs, qsa } from "../../lib/dom.js"; import { qs, qsa } from "../../lib/dom.js";
import { basename } from "../../lib/path.js"; import { basename } from "../../lib/path.js";
import t from "../../locales/index.js"; import t from "../../locales/index.js";
@ -51,7 +52,7 @@ export default async function(render) {
render($page); render($page);
onDestroy(() => clearSelection()); onDestroy(() => clearSelection());
const $scroll = $page.closest(".scroll-y"); const $scroll = assert.type($page.closest(".scroll-y"), HTMLElement);
componentLeft(createRender(qs($page, ".action.left")), { $scroll }); componentLeft(createRender(qs($page, ".action.left")), { $scroll });
componentRight(createRender(qs($page, ".action.right"))); componentRight(createRender(qs($page, ".action.right")));
@ -112,13 +113,13 @@ function componentLeft(render, { $scroll }) {
<button data-action="rename" title="${t("Rename")}"${toggleDependingOnPermission(currentPath(), "rename")}> <button data-action="rename" title="${t("Rename")}"${toggleDependingOnPermission(currentPath(), "rename")}>
${t("Rename")} ${t("Rename")}
</button> </button>
<button data-action="share" title="${t("Share")}" class="${(window.CONFIG.enable_share && !new URLSearchParams(location.search).has("share")) ? "" : "hidden"}"> <button data-action="share" title="${t("Share")}" class="${(window.CONFIG["enable_share"] && !new URLSearchParams(location.search).has("share")) ? "" : "hidden"}">
${t("Share")} ${t("Share")}
</button> </button>
<button data-action="embed" class="hidden" title="${t("Embed")}"> <button data-action="embed" title="${t("Embed")}">
${t("Embed")} ${t("Embed")}
</button> </button>
<button data-action="tag" class="hidden" title="${t("Tag")}"> <button data-action="tag" title="${t("Tag")}">
${t("Tag")} ${t("Tag")}
</button> </button>
`))), `))),
@ -396,6 +397,7 @@ export function init() {
loadCSS(import.meta.url, "../../css/designsystem_dropdown.css"), loadCSS(import.meta.url, "../../css/designsystem_dropdown.css"),
loadCSS(import.meta.url, "./ctrl_submenu.css"), loadCSS(import.meta.url, "./ctrl_submenu.css"),
loadCSS(import.meta.url, "./modal_share.css"), loadCSS(import.meta.url, "./modal_share.css"),
loadCSS(import.meta.url, "./modal_tag.css"),
]); ]);
} }
@ -407,10 +409,10 @@ function generateLinkAttributes(selections) {
const regDir = new RegExp("/$"); const regDir = new RegExp("/$");
const isDir = regDir.test(path); const isDir = regDir.test(path);
if (isDir) { if (isDir) {
filename = basename(path.replace(regDir, "")) + ".zip" filename = basename(path.replace(regDir, "")) + ".zip";
} else { } else {
filename = basename(path); filename = basename(path);
href = "api/files/cat?" href = "api/files/cat?";
} }
} }
href += selections.map(({ path }) => "path=" + encodeURIComponent(path)).join("&"); href += selections.map(({ path }) => "path=" + encodeURIComponent(path)).join("&");

View file

@ -27,8 +27,8 @@ export default async function(render) {
<div is="component_filezone"></div> <div is="component_filezone"></div>
<div is="component_upload_fab"></div> <div is="component_upload_fab"></div>
`); `);
componentFilezone(createRender(assert.type($page.children[0], window.HTMLElement)), { workers$ }); componentFilezone(createRender(assert.type($page.children[0], HTMLElement)), { workers$ });
componentUploadFAB(createRender(assert.type($page.children[1], window.HTMLElement)), { workers$ }); componentUploadFAB(createRender(assert.type($page.children[1], HTMLElement)), { workers$ });
render($page); render($page);
}), }),
)); ));
@ -62,7 +62,7 @@ function componentUploadFAB(render, { workers$ }) {
function componentFilezone(render, { workers$ }) { function componentFilezone(render, { workers$ }) {
const selector = `[data-bind="filemanager-children"]`; const selector = `[data-bind="filemanager-children"]`;
const $target = assert.type(document.body.querySelector(selector), window.HTMLElement); const $target = assert.type(qs(document.body, selector), HTMLElement);
$target.ondragenter = (e) => { $target.ondragenter = (e) => {
if (!isNativeFileUpload(e)) return; if (!isNativeFileUpload(e)) return;
@ -78,7 +78,7 @@ function componentFilezone(render, { workers$ }) {
} else if (e.dataTransfer.files instanceof window.FileList) { } else if (e.dataTransfer.files instanceof window.FileList) {
workers$.next(await processFiles(e.dataTransfer.files)); workers$.next(await processFiles(e.dataTransfer.files));
} else { } else {
assert.fail("NOT_IMPLEMENTED - unknown entry type in ctrl_upload.js", e.dataTransfer); assert.fail("NOT_IMPLEMENTED - unknown entry type in ctrl_upload.js");
} }
clearTimeout(loadID); clearTimeout(loadID);
render(createFragment("")); render(createFragment(""));
@ -134,15 +134,13 @@ function componentUploadQueue(render, { workers$ }) {
}; };
// feature1: close the queue // feature1: close the queue
onClick(qs($page, `img[alt="close"]`)).pipe( onClick(qs($page, `img[alt="close"]`)).pipe(rxjs.tap(async() => {
rxjs.tap(async(cancel) => {
const cleanup = await animate($page, { time: 200, keyframes: slideYOut(50) }); const cleanup = await animate($page, { time: 200, keyframes: slideYOut(50) });
$content.innerHTML = ""; $content.innerHTML = "";
$page.classList.add("hidden"); $page.classList.add("hidden");
updateTotal.reset(); updateTotal.reset();
cleanup(); cleanup();
}), })).subscribe();
).subscribe();
// feature2: setup the task queue in the dom // feature2: setup the task queue in the dom
workers$.subscribe(({ tasks }) => { workers$.subscribe(({ tasks }) => {
@ -150,7 +148,7 @@ function componentUploadQueue(render, { workers$ }) {
updateTotal.addToTotal(tasks.length); updateTotal.addToTotal(tasks.length);
const $fragment = document.createDocumentFragment(); const $fragment = document.createDocumentFragment();
for (let i = 0; i<tasks.length; i++) { for (let i = 0; i<tasks.length; i++) {
const $task = $file.cloneNode(true); const $task = assert.type($file.cloneNode(true), HTMLElement);
$fragment.appendChild($task); $fragment.appendChild($task);
$task.setAttribute("data-path", tasks[i]["path"]); $task.setAttribute("data-path", tasks[i]["path"]);
$task.firstElementChild.firstElementChild.textContent = tasks[i]["path"]; // qs($todo, ".file_path span.path") $task.firstElementChild.firstElementChild.textContent = tasks[i]["path"]; // qs($todo, ".file_path span.path")
@ -176,10 +174,10 @@ function componentUploadQueue(render, { workers$ }) {
let last = 0; let last = 0;
return (nworker, currentWorkerSpeed) => { return (nworker, currentWorkerSpeed) => {
workersSpeed[nworker] = currentWorkerSpeed; workersSpeed[nworker] = currentWorkerSpeed;
if (new Date() - last <= 500) return; if (new Date().getTime() - last <= 500) return;
last = new Date(); last = new Date().getTime();
const speed = workersSpeed.reduce((acc, el) => acc + el, 0); const speed = workersSpeed.reduce((acc, el) => acc + el, 0);
const $speed = assert.type($page.firstElementChild.nextElementSibling.firstElementChild, window.HTMLElement); const $speed = assert.type($page.firstElementChild?.nextElementSibling?.firstElementChild, HTMLElement);
$speed.textContent = formatSpeed(speed); $speed.textContent = formatSpeed(speed);
}; };
}(new Array(MAX_WORKERS).fill(0))); }(new Array(MAX_WORKERS).fill(0)));
@ -208,7 +206,7 @@ function componentUploadQueue(render, { workers$ }) {
$close.removeEventListener("click", cancel); $close.removeEventListener("click", cancel);
break; break;
case "error": case "error":
const $retry = assert.type($iconRetry.cloneNode(true), window.HTMLElement); const $retry = assert.type($iconRetry.cloneNode(true), HTMLElement);
updateDOMGlobalTitle($page, t("Error")); updateDOMGlobalTitle($page, t("Error"));
updateDOMGlobalSpeed(nworker, 0); updateDOMGlobalSpeed(nworker, 0);
updateDOMTaskProgress($task, t("Error")); updateDOMTaskProgress($task, t("Error"));
@ -276,7 +274,7 @@ function componentUploadQueue(render, { workers$ }) {
const nworker = reservations.indexOf(false); const nworker = reservations.indexOf(false);
if (nworker === -1) break; // the pool of workers is already to its max if (nworker === -1) break; // the pool of workers is already to its max
reservations[nworker] = true; reservations[nworker] = true;
noFailureAllowed(processWorkerQueue.bind(this, nworker)).then(() => reservations[nworker] = false); noFailureAllowed(processWorkerQueue.bind(null, nworker)).then(() => reservations[nworker] = false);
} }
}); });
} }
@ -295,17 +293,24 @@ function workerImplFile({ error, progress, speed }) {
this.prevProgress = []; this.prevProgress = [];
} }
/**
* @override
*/
cancel() { cancel() {
this.xhr.abort(); assert.type(this.xhr, XMLHttpRequest).abort();
} }
/**
* @override
*/
async run({ file, path, virtual }) { async run({ file, path, virtual }) {
const xhr = new XMLHttpRequest();
this.xhr = xhr;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.xhr = new XMLHttpRequest(); xhr.open("POST", "api/files/cat?path=" + encodeURIComponent(path));
this.xhr.open("POST", "api/files/cat?path=" + encodeURIComponent(path)); xhr.withCredentials = true;
this.xhr.withCredentials = true; xhr.setRequestHeader("X-Requested-With", "XmlHttpRequest");
this.xhr.setRequestHeader("X-Requested-With", "XmlHttpRequest"); xhr.upload.onprogress = (e) => {
this.xhr.upload.onprogress = (e) => {
if (!e.lengthComputable) return; if (!e.lengthComputable) return;
const percent = Math.floor(100 * e.loaded / e.total); const percent = Math.floor(100 * e.loaded / e.total);
progress(percent); progress(percent);
@ -329,26 +334,26 @@ function workerImplFile({ error, progress, speed }) {
this.prevProgress.shift(); this.prevProgress.shift();
} }
}; };
this.xhr.upload.onabort = () => { xhr.upload.onabort = () => {
reject(ABORT_ERROR); reject(ABORT_ERROR);
error(ABORT_ERROR); error(ABORT_ERROR);
virtual.afterError(); virtual.afterError();
}; };
this.xhr.onload = () => { xhr.onload = () => {
progress(100); progress(100);
if (this.xhr.status !== 200) { if (xhr.status !== 200) {
virtual.afterError(); virtual.afterError();
reject(new Error(this.xhr.statusText)); reject(new Error(xhr.statusText));
return; return;
} }
virtual.afterSuccess(); virtual.afterSuccess();
resolve(null); resolve(null);
}; };
this.xhr.onerror = function(e) { xhr.onerror = function(e) {
reject(new AjaxError("failed", e, "FAILED")); reject(new AjaxError("failed", e, "FAILED"));
virtual.afterError(); virtual.afterError();
}; };
file().then((f) => this.xhr.send(f)).catch((err) => this.xhr.onerror(err)); file().then((f) => xhr.send(f)).catch((err) => xhr.onerror && xhr.onerror(err));
}); });
} }
}(); }();
@ -361,17 +366,24 @@ function workerImplDirectory({ error, progress }) {
this.xhr = null; this.xhr = null;
} }
/**
* @override
*/
cancel() { cancel() {
if (this.xhr instanceof XMLHttpRequest) this.xhr.abort(); assert.type(this.xhr, XMLHttpRequest).abort();
} }
/**
* @override
*/
run({ virtual, path }) { run({ virtual, path }) {
const xhr = new XMLHttpRequest();
this.xhr = xhr;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.xhr = new XMLHttpRequest(); xhr.open("POST", "api/files/mkdir?path=" + encodeURIComponent(path));
this.xhr.open("POST", "api/files/mkdir?path=" + encodeURIComponent(path)); xhr.withCredentials = true;
this.xhr.withCredentials = true; xhr.setRequestHeader("X-Requested-With", "XmlHttpRequest");
this.xhr.setRequestHeader("X-Requested-With", "XmlHttpRequest"); xhr.onerror = function(e) {
this.xhr.onerror = function(e) {
reject(new AjaxError("failed", e, "FAILED")); reject(new AjaxError("failed", e, "FAILED"));
}; };
@ -384,24 +396,24 @@ function workerImplDirectory({ error, progress }) {
} }
progress(percent); progress(percent);
}, 100); }, 100);
this.xhr.upload.onabort = () => { xhr.upload.onabort = () => {
reject(ABORT_ERROR); reject(ABORT_ERROR);
error(ABORT_ERROR); error(ABORT_ERROR);
clearInterval(id); clearInterval(id);
virtual.afterError(); virtual.afterError();
}; };
this.xhr.onload = () => { xhr.onload = () => {
clearInterval(id); clearInterval(id);
progress(100); progress(100);
if (this.xhr.status !== 200) { if (xhr.status !== 200) {
virtual.afterError(); virtual.afterError();
err(new Error(this.xhr.statusText)); reject(new Error(xhr.statusText));
return; return;
} }
virtual.afterSuccess(); virtual.afterSuccess();
resolve(null); resolve(null);
}; };
this.xhr.send(null); xhr.send(null);
}); });
} }
}(); }();
@ -462,8 +474,9 @@ async function processFiles(filelist) {
}; };
break; break;
default: default:
assert.fail(type, `NOT_SUPPORTED type="${type}"`); assert.fail(`NOT_SUPPORTED type="${type}"`);
} }
task = assert.truthy(task);
task.virtual.before(); task.virtual.before();
tasks.push(task); tasks.push(task);
} }
@ -497,6 +510,7 @@ async function processItems(itemList) {
exec: workerImplFile, exec: workerImplFile,
virtual: save(path, entrySize), virtual: save(path, entrySize),
done: false, done: false,
ready: () => false,
}; };
size += entrySize; size += entrySize;
} else if (entry.isDirectory) { } else if (entry.isDirectory) {
@ -506,12 +520,13 @@ async function processItems(itemList) {
exec: workerImplDirectory, exec: workerImplDirectory,
virtual: mkdir(path), virtual: mkdir(path),
done: false, done: false,
ready: () => false,
}; };
queue = queue.concat(await new Promise((resolve) => { queue = queue.concat(await new Promise((resolve) => {
entry.createReader().readEntries(resolve); entry.createReader().readEntries(resolve);
})); }));
} else { } else {
assert.fail("NOT_IMPLEMENTED - unknown entry type in ctrl_upload.js", entry); assert.fail("NOT_IMPLEMENTED - unknown entry type in ctrl_upload.js");
} }
task.ready = () => { task.ready = () => {
const isInDirectory = (filepath, folder) => folder.indexOf(filepath) === 0; const isInDirectory = (filepath, folder) => folder.indexOf(filepath) === 0;

View file

@ -112,6 +112,4 @@ function _moveHiddenFilesDownward(fileA, fileB) {
return 0; return 0;
} }
export const isNativeFileUpload = (e) => { export const isNativeFileUpload = (e) => JSON.stringify(e.dataTransfer.types.slice(-1)) === "[\"Files\"]";
return JSON.stringify(e.dataTransfer.types.slice(-1)) === "[\"Files\"]";
}

View file

@ -36,7 +36,7 @@ function renderDesktop(render, removeLabel) {
ret.next(); ret.next();
ret.complete(); ret.complete();
return ret.toPromise(); return ret.toPromise();
}).bind(this, MODAL_RIGHT_BUTTON); }).bind(null, MODAL_RIGHT_BUTTON);
$input.focus(); $input.focus();

View file

@ -33,7 +33,7 @@ function renderDesktop(render, filename) {
} }
ret.next(value); ret.next(value);
ret.complete(); ret.complete();
}).bind(this, MODAL_RIGHT_BUTTON); }).bind(null, MODAL_RIGHT_BUTTON);
const ext = extname(filename); const ext = extname(filename);
$input.value = filename; $input.value = filename;

View file

@ -36,9 +36,10 @@ export default function(render, { path }) {
render($modal); render($modal);
const ret = new rxjs.Subject(); const ret = new rxjs.Subject();
const role$ = new rxjs.BehaviorSubject(null); const role$ = new rxjs.BehaviorSubject(null);
const state = { const state = {
form: {}, /** @type {object} */ form: {},
links: null, /** @type {any[] | null} */ links: null,
}; };
// feature: select // feature: select
@ -84,7 +85,12 @@ export default function(render, { path }) {
body, body,
url: `api/share/${id}`, url: `api/share/${id}`,
}).toPromise(); }).toPromise();
state.links.push({ ...body, path: body.path.substring(currentPath().length - 1) }); ;
// if (state.links === null) assert.fail("ttest");
assert.truthy(state.links).push({
...body,
path: body.path.substring(currentPath().length - 1),
});
role$.next(null); role$.next(null);
}, },
remove: async({ id }) => { remove: async({ id }) => {
@ -92,7 +98,7 @@ export default function(render, { path }) {
method: "DELETE", method: "DELETE",
url: `api/share/${id}`, url: `api/share/${id}`,
}).toPromise(); }).toPromise();
state.links = state.links.filter((link) => link.id !== id); state.links = (state.links || []).filter((link) => link && link.id !== id);
role$.next(null); role$.next(null);
}, },
all: async() => { all: async() => {
@ -240,7 +246,7 @@ async function ctrlCreateShare(render, { save, formState }) {
? t("Password") ? t("Password")
: label === "url_enable" : label === "url_enable"
? t("Custom Link url") ? t("Custom Link url")
: assert.fail(label, "unknown label"); : assert.fail("unknown label");
return createElement(` return createElement(`
<div class="component_supercheckbox"> <div class="component_supercheckbox">
<label> <label>
@ -273,7 +279,7 @@ async function ctrlCreateShare(render, { save, formState }) {
// sync editable custom link input with link id // sync editable custom link input with link id
effect(rxjs.fromEvent(qs($form, `[name="url"]`), "keyup").pipe(rxjs.tap((e) => { effect(rxjs.fromEvent(qs($form, `[name="url"]`), "keyup").pipe(rxjs.tap((e) => {
id = e.target.value.replaceAll(" ", "-").replace(new RegExp("[^A-Za-z\-]"), ""); id = e.target.value.replaceAll(" ", "-").replace(new RegExp("[^A-Za-z\-]"), "");
qs(assert.type($form.closest(".component_share"), window.HTMLElement), `input[name="create"]`).value = `${location.origin}${toHref("/s/" + id)}`; qs(assert.type($form.closest(".component_share"), HTMLElement), `input[name="create"]`).value = `${location.origin}${toHref("/s/" + id)}`;
}))); })));
// feature: create a shared link // feature: create a shared link
@ -281,7 +287,7 @@ async function ctrlCreateShare(render, { save, formState }) {
effect(onClick(qs($page, ".shared-link")).pipe( effect(onClick(qs($page, ".shared-link")).pipe(
rxjs.first(), rxjs.first(),
rxjs.switchMap(async() => { rxjs.switchMap(async() => {
const body = [...new FormData(assert.type(qs(document.body, ".component_share form"), window.HTMLFormElement))] const body = [...new FormData(assert.type(qs(document.body, ".component_share form"), HTMLFormElement))]
.reduce((acc, [key, value]) => { .reduce((acc, [key, value]) => {
if (value && key.slice(-7) !== "_enable") acc[key] = value; if (value && key.slice(-7) !== "_enable") acc[key] = value;
return acc; return acc;
@ -328,7 +334,7 @@ function shareObjToRole({ can_read, can_write, can_upload }) {
} else if (can_read === true && can_write === true && can_upload === true) { } else if (can_read === true && can_write === true && can_upload === true) {
return "editor"; return "editor";
} }
return null; return undefined;
} }
export function copyToClipboard(str) { export function copyToClipboard(str) {

View file

@ -0,0 +1,48 @@
.component_tag input {
font-size: 1.2em;
}
.component_tag input::placeholder {
font-weight: 100;
}
.component_tag .box {
display: flex;
background: var(--bg-color);
transition: background 0.1s ease;
padding: 5px 10px;
border-radius: 3px;
}
.component_tag .box.active {
background: var(--primary);
color: var(--color);
}
.component_tag .box > div {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.component_tag .box img {
width: 20px;
background: rgba(0, 0, 0, 0.05);
border-radius: 50%;
margin-left: 5px;
}
.component_tag .box img[alt="close"] {
width: 14px;
padding: 3px;
}
.component_tag .box .count {
opacity: 0.5;
font-size: 0.9rem;
}
.component_tag .box .count:before {
content: "[";
}
.component_tag .box .count:after {
content: "]";
}
.component_tag .scroll-y {
overflow-y: auto !important;
max-height: 200px;
}

View file

@ -1,9 +1,32 @@
import { createElement } from "../../lib/skeleton/index.js"; import { createElement } from "../../lib/skeleton/index.js";
import t from "../../locales/index.js";
export default function(render) { export default function(render) {
const $modal = createElement(` const $modal = createElement(`
<div> <div class="component_tag">
TAG MODAL <form>
<input class="component_input" type="text" placeholder="${t("Create a Tag")}" value="">
</form>
<div class="scroll-y">
<div class="box no-select">
<div>test <span class="count">2</span></div>
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPHBhdGggc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MC41MzMzMzMwOSIgZD0ibSAxNi44ODUsMTUuNDU1IC00LjU5LC00LjU4IC00LjU5LDQuNTggLTEuNDEsLTEuNDEgNiwtNiA2LDYgeiIgLz4KICA8cGF0aCBmaWxsPSJub25lIiBkPSJNMC0uMjVoMjR2MjRIMHoiIC8+Cjwvc3ZnPgo=" alt="arrow_top">
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPHBhdGggc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MC41MzMzMzMyMSIgZD0ibSA3LjcwNSw4LjA0NSA0LjU5LDQuNTggNC41OSwtNC41OCAxLjQxLDEuNDEgLTYsNiAtNiwtNiB6IiAvPgogIDxwYXRoIGZpbGw9Im5vbmUiIGQ9Ik0wLS4yNWgyNHYyNEgweiIgLz4KPC9zdmc+Cg==" alt="arrow_bottom">
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MS45NzYgNTEuOTc2Ij4KICA8cGF0aCBzdHlsZT0iZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eTowLjUzMzMzMjg1O3N0cm9rZS13aWR0aDoxLjQ1NjgxMTE5IiBkPSJtIDQxLjAwNTMxLDQwLjg0NDA2MiBjIC0xLjEzNzc2OCwxLjEzNzc2NSAtMi45ODIwODgsMS4xMzc3NjUgLTQuMTE5ODYxLDAgTCAyNi4wNjg2MjgsMzAuMDI3MjM0IDE0LjczNzU1MSw0MS4zNTgzMSBjIC0xLjEzNzc3MSwxLjEzNzc3MSAtMi45ODIwOTMsMS4xMzc3NzEgLTQuMTE5ODYxLDAgLTEuMTM3NzcyMiwtMS4xMzc3NjggLTEuMTM3NzcyMiwtMi45ODIwODggMCwtNC4xMTk4NjEgTCAyMS45NDg3NjYsMjUuOTA3MzcyIDExLjEzMTkzOCwxNS4wOTA1NTEgYyAtMS4xMzc3NjQ3LC0xLjEzNzc3MSAtMS4xMzc3NjQ3LC0yLjk4MzU1MyAwLC00LjExOTg2MSAxLjEzNzc3NCwtMS4xMzc3NzIxIDIuOTgyMDk4LC0xLjEzNzc3MjEgNC4xMTk4NjUsMCBMIDI2LjA2ODYyOCwyMS43ODc1MTIgMzYuMzY5NzM5LDExLjQ4NjM5OSBjIDEuMTM3NzY4LC0xLjEzNzc2OCAyLjk4MjA5MywtMS4xMzc3NjggNC4xMTk4NjIsMCAxLjEzNzc2NywxLjEzNzc2OSAxLjEzNzc2NywyLjk4MjA5NCAwLDQuMTE5ODYyIEwgMzAuMTg4NDg5LDI1LjkwNzM3MiA0MS4wMDUzMSwzNi43MjQxOTcgYyAxLjEzNzc3MSwxLjEzNzc2NyAxLjEzNzc3MSwyLjk4MjA5MSAwLDQuMTE5ODY1IHoiIC8+Cjwvc3ZnPgo=" alt="close">
</div>
<div class="box no-select">
<div>kjhjk <span class="count">1</span></div>
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPHBhdGggc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MC41MzMzMzMwOSIgZD0ibSAxNi44ODUsMTUuNDU1IC00LjU5LC00LjU4IC00LjU5LDQuNTggLTEuNDEsLTEuNDEgNiwtNiA2LDYgeiIgLz4KICA8cGF0aCBmaWxsPSJub25lIiBkPSJNMC0uMjVoMjR2MjRIMHoiIC8+Cjwvc3ZnPgo=" alt="arrow_top">
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPHBhdGggc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MC41MzMzMzMyMSIgZD0ibSA3LjcwNSw4LjA0NSA0LjU5LDQuNTggNC41OSwtNC41OCAxLjQxLDEuNDEgLTYsNiAtNiwtNiB6IiAvPgogIDxwYXRoIGZpbGw9Im5vbmUiIGQ9Ik0wLS4yNWgyNHYyNEgweiIgLz4KPC9zdmc+Cg==" alt="arrow_bottom">
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MS45NzYgNTEuOTc2Ij4KICA8cGF0aCBzdHlsZT0iZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eTowLjUzMzMzMjg1O3N0cm9rZS13aWR0aDoxLjQ1NjgxMTE5IiBkPSJtIDQxLjAwNTMxLDQwLjg0NDA2MiBjIC0xLjEzNzc2OCwxLjEzNzc2NSAtMi45ODIwODgsMS4xMzc3NjUgLTQuMTE5ODYxLDAgTCAyNi4wNjg2MjgsMzAuMDI3MjM0IDE0LjczNzU1MSw0MS4zNTgzMSBjIC0xLjEzNzc3MSwxLjEzNzc3MSAtMi45ODIwOTMsMS4xMzc3NzEgLTQuMTE5ODYxLDAgLTEuMTM3NzcyMiwtMS4xMzc3NjggLTEuMTM3NzcyMiwtMi45ODIwODggMCwtNC4xMTk4NjEgTCAyMS45NDg3NjYsMjUuOTA3MzcyIDExLjEzMTkzOCwxNS4wOTA1NTEgYyAtMS4xMzc3NjQ3LC0xLjEzNzc3MSAtMS4xMzc3NjQ3LC0yLjk4MzU1MyAwLC00LjExOTg2MSAxLjEzNzc3NCwtMS4xMzc3NzIxIDIuOTgyMDk4LC0xLjEzNzc3MjEgNC4xMTk4NjUsMCBMIDI2LjA2ODYyOCwyMS43ODc1MTIgMzYuMzY5NzM5LDExLjQ4NjM5OSBjIDEuMTM3NzY4LC0xLjEzNzc2OCAyLjk4MjA5MywtMS4xMzc3NjggNC4xMTk4NjIsMCAxLjEzNzc2NywxLjEzNzc2OSAxLjEzNzc2NywyLjk4MjA5NCAwLDQuMTE5ODYyIEwgMzAuMTg4NDg5LDI1LjkwNzM3MiA0MS4wMDUzMSwzNi43MjQxOTcgYyAxLjEzNzc3MSwxLjEzNzc2NyAxLjEzNzc3MSwyLjk4MjA5MSAwLDQuMTE5ODY1IHoiIC8+Cjwvc3ZnPgo=" alt="close">
</div>
<div class="box no-select active">
<div>jh <span class="count">2</span></div>
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPHBhdGggc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MC41MzMzMzMwOSIgZD0ibSAxNi44ODUsMTUuNDU1IC00LjU5LC00LjU4IC00LjU5LDQuNTggLTEuNDEsLTEuNDEgNiwtNiA2LDYgeiIgLz4KICA8cGF0aCBmaWxsPSJub25lIiBkPSJNMC0uMjVoMjR2MjRIMHoiIC8+Cjwvc3ZnPgo=" alt="arrow_top">
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPHBhdGggc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MC41MzMzMzMyMSIgZD0ibSA3LjcwNSw4LjA0NSA0LjU5LDQuNTggNC41OSwtNC41OCAxLjQxLDEuNDEgLTYsNiAtNiwtNiB6IiAvPgogIDxwYXRoIGZpbGw9Im5vbmUiIGQ9Ik0wLS4yNWgyNHYyNEgweiIgLz4KPC9zdmc+Cg==" alt="arrow_bottom">
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MS45NzYgNTEuOTc2Ij4KICA8cGF0aCBzdHlsZT0iZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eTowLjUzMzMzMjg1O3N0cm9rZS13aWR0aDoxLjQ1NjgxMTE5IiBkPSJtIDQxLjAwNTMxLDQwLjg0NDA2MiBjIC0xLjEzNzc2OCwxLjEzNzc2NSAtMi45ODIwODgsMS4xMzc3NjUgLTQuMTE5ODYxLDAgTCAyNi4wNjg2MjgsMzAuMDI3MjM0IDE0LjczNzU1MSw0MS4zNTgzMSBjIC0xLjEzNzc3MSwxLjEzNzc3MSAtMi45ODIwOTMsMS4xMzc3NzEgLTQuMTE5ODYxLDAgLTEuMTM3NzcyMiwtMS4xMzc3NjggLTEuMTM3NzcyMiwtMi45ODIwODggMCwtNC4xMTk4NjEgTCAyMS45NDg3NjYsMjUuOTA3MzcyIDExLjEzMTkzOCwxNS4wOTA1NTEgYyAtMS4xMzc3NjQ3LC0xLjEzNzc3MSAtMS4xMzc3NjQ3LC0yLjk4MzU1MyAwLC00LjExOTg2MSAxLjEzNzc3NCwtMS4xMzc3NzIxIDIuOTgyMDk4LC0xLjEzNzc3MjEgNC4xMTk4NjUsMCBMIDI2LjA2ODYyOCwyMS43ODc1MTIgMzYuMzY5NzM5LDExLjQ4NjM5OSBjIDEuMTM3NzY4LC0xLjEzNzc2OCAyLjk4MjA5MywtMS4xMzc3NjggNC4xMTk4NjIsMCAxLjEzNzc2NywxLjEzNzc2OSAxLjEzNzc2NywyLjk4MjA5NCAwLDQuMTE5ODYyIEwgMzAuMTg4NDg5LDI1LjkwNzM3MiA0MS4wMDUzMSwzNi43MjQxOTcgYyAxLjEzNzc3MSwxLjEzNzc2NyAxLjEzNzc3MSwyLjk4MjA5MSAwLDQuMTE5ODY1IHoiIC8+Cjwvc3ZnPgo=" alt="close">
</div>
</div>
</div> </div>
`); `);
render($modal, ({ id }) => { render($modal, ({ id }) => {

View file

@ -101,7 +101,7 @@ export const ls = (path) => {
rxjs.merge( rxjs.merge(
rxjs.of(null), rxjs.of(null),
rxjs.merge(rxjs.of(null), rxjs.fromEvent(window, "keydown").pipe( // "r" shorcut rxjs.merge(rxjs.of(null), rxjs.fromEvent(window, "keydown").pipe( // "r" shorcut
rxjs.filter((e) => e.keyCode === 82 && assert.type(document.activeElement, window.HTMLElement).tagName !== "INPUT"), rxjs.filter((e) => e.keyCode === 82 && assert.type(document.activeElement, HTMLElement).tagName !== "INPUT"),
)).pipe(rxjs.switchMap(() => lsFromHttp(path))), )).pipe(rxjs.switchMap(() => lsFromHttp(path))),
), ),
).pipe( ).pipe(

View file

@ -30,7 +30,7 @@ const mutationFiles$ = new rxjs.BehaviorSubject({
class IVirtualLayer { class IVirtualLayer {
before() { throw new Error("NOT_IMPLEMENTED"); } before() { throw new Error("NOT_IMPLEMENTED"); }
async afterSuccess() { throw new Error("NOT_IMPLEMENTED"); } async afterSuccess() { throw new Error("NOT_IMPLEMENTED"); }
async afterError() { throw new Error("NOT_IMPLEMENTED"); } async afterError() { return rxjs.EMPTY; }
} }
export function withVirtualLayer(ajax$, mutate) { export function withVirtualLayer(ajax$, mutate) {
@ -51,6 +51,9 @@ export function touch(path) {
}; };
return new class TouchVL extends IVirtualLayer { return new class TouchVL extends IVirtualLayer {
/**
* @override
*/
before() { before() {
stateAdd(virtualFiles$, basepath, { stateAdd(virtualFiles$, basepath, {
...file, ...file,
@ -58,6 +61,9 @@ export function touch(path) {
}); });
} }
/**
* @override
*/
async afterSuccess() { async afterSuccess() {
removeLoading(virtualFiles$, basepath, filename); removeLoading(virtualFiles$, basepath, filename);
onDestroy(() => statePop(virtualFiles$, basepath, filename)); onDestroy(() => statePop(virtualFiles$, basepath, filename));
@ -68,6 +74,9 @@ export function touch(path) {
hooks.mutation.emit({ op: "touch", path: basepath }); hooks.mutation.emit({ op: "touch", path: basepath });
} }
/**
* @override
*/
async afterError() { async afterError() {
statePop(virtualFiles$, basepath, filename); statePop(virtualFiles$, basepath, filename);
return rxjs.of(fscache().remove(basepath)).pipe( return rxjs.of(fscache().remove(basepath)).pipe(
@ -87,6 +96,9 @@ export function mkdir(path) {
}; };
return new class MkdirVL extends IVirtualLayer { return new class MkdirVL extends IVirtualLayer {
/**
* @override
*/
before() { before() {
stateAdd(virtualFiles$, basepath, { stateAdd(virtualFiles$, basepath, {
...file, ...file,
@ -95,6 +107,9 @@ export function mkdir(path) {
statePop(mutationFiles$, basepath, dirname); // case: rm followed by mkdir statePop(mutationFiles$, basepath, dirname); // case: rm followed by mkdir
} }
/**
* @override
*/
async afterSuccess() { async afterSuccess() {
removeLoading(virtualFiles$, basepath, dirname); removeLoading(virtualFiles$, basepath, dirname);
onDestroy(() => statePop(virtualFiles$, basepath, dirname)); onDestroy(() => statePop(virtualFiles$, basepath, dirname));
@ -105,6 +120,9 @@ export function mkdir(path) {
hooks.mutation.emit({ op: "mkdir", path: basepath }); hooks.mutation.emit({ op: "mkdir", path: basepath });
} }
/**
* @override
*/
async afterError() { async afterError() {
statePop(virtualFiles$, basepath, dirname); statePop(virtualFiles$, basepath, dirname);
return rxjs.of(fscache().remove(basepath)).pipe( return rxjs.of(fscache().remove(basepath)).pipe(
@ -124,6 +142,9 @@ export function save(path, size) {
}; };
return new class SaveVL extends IVirtualLayer { return new class SaveVL extends IVirtualLayer {
/**
* @override
*/
before() { before() {
stateAdd(virtualFiles$, basepath, { stateAdd(virtualFiles$, basepath, {
...file, ...file,
@ -132,6 +153,9 @@ export function save(path, size) {
statePop(mutationFiles$, basepath, filename); // eg: rm followed by save statePop(mutationFiles$, basepath, filename); // eg: rm followed by save
} }
/**
* @override
*/
async afterSuccess() { async afterSuccess() {
removeLoading(virtualFiles$, basepath, filename); removeLoading(virtualFiles$, basepath, filename);
onDestroy(() => statePop(virtualFiles$, basepath, filename)); onDestroy(() => statePop(virtualFiles$, basepath, filename));
@ -142,6 +166,9 @@ export function save(path, size) {
hooks.mutation.emit({ op: "save", path: basepath }); hooks.mutation.emit({ op: "save", path: basepath });
} }
/**
* @override
*/
async afterError() { async afterError() {
statePop(virtualFiles$, basepath, filename); statePop(virtualFiles$, basepath, filename);
return rxjs.EMPTY; return rxjs.EMPTY;
@ -160,6 +187,9 @@ export function rm(...paths) {
} }
return new class RmVL extends IVirtualLayer { return new class RmVL extends IVirtualLayer {
/**
* @override
*/
before() { before() {
for (let i=0; i<arr.length; i+=2) { for (let i=0; i<arr.length; i+=2) {
stateAdd(mutationFiles$, arr[i], { stateAdd(mutationFiles$, arr[i], {
@ -176,6 +206,9 @@ export function rm(...paths) {
} }
} }
/**
* @override
*/
async afterSuccess() { async afterSuccess() {
for (let i=0; i<arr.length; i+=2) { for (let i=0; i<arr.length; i+=2) {
stateAdd(mutationFiles$, arr[i], { stateAdd(mutationFiles$, arr[i], {
@ -208,6 +241,9 @@ export function rm(...paths) {
if (arr.length > 0) hooks.mutation.emit({ op: "rm", path: arr[0] }); if (arr.length > 0) hooks.mutation.emit({ op: "rm", path: arr[0] });
} }
/**
* @override
*/
async afterError() { async afterError() {
for (let i=0; i<arr.length; i+=2) { for (let i=0; i<arr.length; i+=2) {
stateAdd(mutationFiles$, arr[i], { stateAdd(mutationFiles$, arr[i], {
@ -232,6 +268,9 @@ export function mv(fromPath, toPath) {
let type = null; let type = null;
return new class MvVL extends IVirtualLayer { return new class MvVL extends IVirtualLayer {
/**
* @override
*/
before() { before() {
if (fromBasepath === toBasepath) this._beforeSamePath(); if (fromBasepath === toBasepath) this._beforeSamePath();
else this._beforeSamePath(); else this._beforeSamePath();
@ -270,6 +309,9 @@ export function mv(fromPath, toPath) {
}); });
} }
/**
* @override
*/
async afterSuccess() { async afterSuccess() {
fscache().remove(fromPath, false); fscache().remove(fromPath, false);
if (fromBasepath === toBasepath) await this._afterSuccessSamePath(); if (fromBasepath === toBasepath) await this._afterSuccessSamePath();
@ -325,6 +367,9 @@ export function mv(fromPath, toPath) {
hooks.mutation.emit({ op: "mv", path: toBasepath }); hooks.mutation.emit({ op: "mv", path: toBasepath });
} }
/**
* @override
*/
async afterError() { async afterError() {
statePop(mutationFiles$, fromBasepath, fromName); statePop(mutationFiles$, fromBasepath, fromName);
if (fromBasepath !== toBasepath) { if (fromBasepath !== toBasepath) {

View file

@ -4,9 +4,9 @@ import { settingsGet, settingsSave } from "../../lib/store.js";
let state$ = null; let state$ = null;
export function init() { export function init() {
state$ = new rxjs.BehaviorSubject(settingsGet({ state$ = new rxjs.BehaviorSubject(settingsGet({
view: window.CONFIG.default_view || "grid", view: window.CONFIG["default_view"] || "grid",
show_hidden: window.CONFIG.display_hidden || false, show_hidden: window.CONFIG["display_hidden"] || false,
sort: window.CONFIG.default_sort || "type", sort: window.CONFIG["default_sort"] || "type",
order: null, order: null,
search: "", search: "",
}, "filespage")); }, "filespage"));

View file

@ -24,7 +24,6 @@ const selection$ = new rxjs.BehaviorSubject([
onDestroy(clearSelection); onDestroy(clearSelection);
export function addSelection({ shift = false, n = 0, ...rest }) { export function addSelection({ shift = false, n = 0, ...rest }) {
// console.log(n, shift)
const selections = selection$.value; const selections = selection$.value;
const selection = { type: shift ? "range" : "anchor", n, ...rest }; const selection = { type: shift ? "range" : "anchor", n, ...rest };

View file

@ -0,0 +1,3 @@
export function init();
export function createThing(any): HTMLElement;

View file

@ -70,7 +70,7 @@ export function createThing({
}) { }) {
const [, ext] = formatFile(name); const [, ext] = formatFile(name);
const mime = TYPES.MIME[ext.toLowerCase()]; const mime = TYPES.MIME[ext.toLowerCase()];
const $thing = assert.type($tmpl.cloneNode(true), window.HTMLElement); const $thing = assert.type($tmpl.cloneNode(true), HTMLElement);
// you might wonder why don't we use querySelector to nicely get the dom nodes? Well, // you might wonder why don't we use querySelector to nicely get the dom nodes? Well,
// we're in the hot path, better performance here is critical to get 60FPS. // we're in the hot path, better performance here is critical to get 60FPS.
@ -107,9 +107,9 @@ export function createThing({
$img.src = "api/files/cat?path=" + encodeURIComponent(path) + "&thumbnail=true" + location.search.replace("?", "&"); $img.src = "api/files/cat?path=" + encodeURIComponent(path) + "&thumbnail=true" + location.search.replace("?", "&");
$img.loaded = false; $img.loaded = false;
const t = new Date(); const t = new Date().getTime();
$img.onload = async() => { $img.onload = async() => {
const duration = new Date() - t; const duration = new Date().getTime() - t;
$img.loaded = true; $img.loaded = true;
await Promise.all([ await Promise.all([
animate($img, { animate($img, {

View file

@ -0,0 +1 @@
export default function(any): void;

View file

@ -3,3 +3,5 @@ interface Window {
create: (options: any) => any; create: (options: any) => any;
}; };
} }
export default function(any): void;

View file

@ -3,3 +3,5 @@ interface Window {
Book: new (options: any) => any; Book: new (options: any) => any;
}; };
} }
export default function(any): void;

View file

@ -60,7 +60,7 @@ export default function(render) {
effect(setup$.pipe( effect(setup$.pipe(
rxjs.mergeMap(() => rxjs.merge( rxjs.mergeMap(() => rxjs.merge(
rxjs.fromEvent(document, "keydown"), rxjs.fromEvent(document, "keydown"),
rendition$.pipe(rxjs.mergeMap(() => rxjs.fromEvent(assert.type(qs(document.body, "iframe"), window.HTMLElement).contentDocument.body, "keydown"))), rendition$.pipe(rxjs.mergeMap(() => rxjs.fromEvent(assert.type(qs(document.body, "iframe"), HTMLElement).contentDocument.body, "keydown"))),
)), )),
rxjs.map((e) => { rxjs.map((e) => {
switch (e.code) { switch (e.code) {

View file

@ -7,3 +7,5 @@ interface Window {
}; };
}; };
} }
export default function(any): void;

View file

@ -97,7 +97,7 @@ export default async function(render, { acl$ }) {
"Ctrl-X Ctrl-C": () => window.history.back(), "Ctrl-X Ctrl-C": () => window.history.back(),
}); });
if (mode === "orgmode") { if (mode === "orgmode") {
const cleanup = CodeMirror.orgmode.init(editor); const cleanup = window.CodeMirror.orgmode.init(editor);
onDestroy(cleanup); onDestroy(cleanup);
} }

View file

@ -2,15 +2,14 @@ import "../../../lib/vendor/codemirror/addon/mode/simple.js";
import { import {
org_cycle, org_shifttab, org_metaleft, org_metaright, org_meta_return, org_metaup, org_cycle, org_shifttab, org_metaleft, org_metaright, org_meta_return, org_metaup,
org_metadown, org_insert_todo_heading, org_shiftleft, org_shiftright, fold, unfold, org_metadown, org_insert_todo_heading, org_shiftleft, org_shiftright, fold, unfold,
isFold, org_set_fold, org_shiftmetaleft, org_shiftmetaright, isFold, org_shiftmetaleft, org_shiftmetaright,
} from "./emacs-org.js"; } from "./emacs-org.js";
import { getCurrentPath } from "../common.js";
import { join } from "../../../lib/path.js"; import { join } from "../../../lib/path.js";
import { currentShare } from "../../filespage/cache.js"; import { currentShare } from "../../filespage/cache.js";
window.CodeMirror.__mode = "orgmode"; window.CodeMirror.__mode = "orgmode";
CodeMirror.defineSimpleMode("orgmode", { window.CodeMirror.defineSimpleMode("orgmode", {
start: [ start: [
{ regex: /(\*\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header level1 org-level-star", "header level1 org-todo", "header level1 org-done", "header level1 org-priority", "header level1", "header level1 void", "header level1 comment"] }, { regex: /(\*\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header level1 org-level-star", "header level1 org-todo", "header level1 org-done", "header level1 org-priority", "header level1", "header level1 void", "header level1 comment"] },
{ regex: /(\*{1,}\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header org-level-star", "header org-todo", "header org-done", "header org-priority", "header", "header void", "header comment"] }, { regex: /(\*{1,}\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header org-level-star", "header org-todo", "header org-done", "header org-priority", "header", "header void", "header comment"] },
@ -34,7 +33,7 @@ CodeMirror.defineSimpleMode("orgmode", {
], ],
}); });
CodeMirror.registerHelper("fold", "orgmode", function(cm, start) { window.CodeMirror.registerHelper("fold", "orgmode", function(cm, start) {
// init // init
const levelToMatch = headerLevel(start.line); const levelToMatch = headerLevel(start.line);
@ -54,21 +53,21 @@ CodeMirror.registerHelper("fold", "orgmode", function(cm, start) {
} }
return { return {
from: CodeMirror.Pos(start.line, cm.getLine(start.line).length), from: window.CodeMirror.Pos(start.line, cm.getLine(start.line).length),
to: CodeMirror.Pos(end, cm.getLine(end).length), to: window.CodeMirror.Pos(end, cm.getLine(end).length),
}; };
function headerLevel(lineNo) { function headerLevel(lineNo) {
const line = cm.getLine(lineNo); const line = cm.getLine(lineNo);
const match = /^\*+/.exec(line); const match = /^\*+/.exec(line);
if (match && match.length === 1 && /header/.test(cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0)))) { if (match && match.length === 1 && /header/.test(cm.getTokenTypeAt(window.CodeMirror.Pos(lineNo, 0)))) {
return match[0].length; return match[0].length;
} }
return null; return null;
} }
}); });
CodeMirror.registerGlobalHelper("fold", "drawer", function(mode) { window.CodeMirror.registerGlobalHelper("fold", "drawer", function(mode) {
return mode.name === "orgmode"; return mode.name === "orgmode";
}, function(cm, start) { }, function(cm, start) {
const drawer = isBeginningOfADrawer(start.line); const drawer = isBeginningOfADrawer(start.line);
@ -85,8 +84,8 @@ CodeMirror.registerGlobalHelper("fold", "drawer", function(mode) {
} }
return { return {
from: CodeMirror.Pos(start.line, cm.getLine(start.line).length), from: window.CodeMirror.Pos(start.line, cm.getLine(start.line).length),
to: CodeMirror.Pos(end, cm.getLine(end).length), to: window.CodeMirror.Pos(end, cm.getLine(end).length),
}; };
function isBeginningOfADrawer(lineNo) { function isBeginningOfADrawer(lineNo) {
@ -103,9 +102,9 @@ CodeMirror.registerGlobalHelper("fold", "drawer", function(mode) {
} }
}); });
CodeMirror.registerHelper("orgmode", "init", (editor) => { window.CodeMirror.registerHelper("orgmode", "init", (editor) => {
editor.setOption("extraKeys", { editor.setOption("extraKeys", {
"Tab": (cm) => org_cycle(cm), Tab: (cm) => org_cycle(cm),
"Shift-Tab": (cm) => org_shifttab(cm), "Shift-Tab": (cm) => org_shifttab(cm),
"Alt-Left": (cm) => org_metaleft(cm), "Alt-Left": (cm) => org_metaleft(cm),
"Alt-Right": (cm) => org_metaright(cm), "Alt-Right": (cm) => org_metaright(cm),
@ -126,15 +125,15 @@ CodeMirror.registerHelper("orgmode", "init", (editor) => {
// fold everything except headers by default // fold everything except headers by default
editor.operation(function() { editor.operation(function() {
for (let i = 0; i < editor.lineCount(); i++) { for (let i = 0; i < editor.lineCount(); i++) {
if (/header/.test(editor.getTokenTypeAt(CodeMirror.Pos(i, 0))) === false) { if (/header/.test(editor.getTokenTypeAt(window.CodeMirror.Pos(i, 0))) === false) {
fold(editor, CodeMirror.Pos(i, 0)); fold(editor, window.CodeMirror.Pos(i, 0));
} }
} }
}); });
return CodeMirror.orgmode.destroy.bind(this, editor); return window.CodeMirror.orgmode.destroy.bind(this, editor);
}); });
CodeMirror.registerHelper("orgmode", "destroy", (editor) => { window.CodeMirror.registerHelper("orgmode", "destroy", (editor) => {
editor.off("mousedown", toggleHandler); editor.off("mousedown", toggleHandler);
editor.off("touchstart", toggleHandler); editor.off("touchstart", toggleHandler);
editor.off("gutterClick", foldLine); editor.off("gutterClick", foldLine);
@ -272,7 +271,7 @@ function toggleHandler(cm, e) {
if (/^https?\:\/\//.test(src)) { if (/^https?\:\/\//.test(src)) {
$img.src = src; $img.src = src;
} else { } else {
let path = join(location, src).replace(/^\/view/, ""); const path = join(location, src).replace(/^\/view/, "");
$img.src = "/api/files/cat?path=" + path; $img.src = "/api/files/cat?path=" + path;
const share = currentShare(); const share = currentShare();
if (share) $img.src += "&share=" + share; if (share) $img.src += "&share=" + share;

View file

@ -0,0 +1,8 @@
interface Window {
EXIF: {
getAllTags: (any) => object;
getData: (HTMLElement, any) => void;
};
}
export default function(any): void;

View file

@ -30,17 +30,20 @@ export default function(render) {
render($page); render($page);
transition(qs($page, ".component_image_container")); transition(qs($page, ".component_image_container"));
const toggleInfo = () => qs($page, ".images_aside").classList.toggle("open");
const $imgContainer = qs($page, ".images_wrapper"); const $imgContainer = qs($page, ".images_wrapper");
const $photo = qs($page, "img.photo"); const $photo = qs($page, "img.photo");
const removeLoader = createLoader($imgContainer); const removeLoader = createLoader($imgContainer);
const load$ = new rxjs.BehaviorSubject(null); const load$ = new rxjs.BehaviorSubject(null);
const toggleInfo = () => {
qs($page, ".images_aside").classList.toggle("open");
componentMetadata(createRender(qs($page, ".images_aside")), { toggle: toggleInfo, load$ });
};
renderMenubar( renderMenubar(
qs($page, "component-menubar"), qs($page, "component-menubar"),
buttonDownload(getFilename(), getDownloadUrl()), buttonDownload(getFilename(), getDownloadUrl()),
buttonFullscreen(qs($page, ".component_image_container")), buttonFullscreen(qs($page, ".component_image_container")),
buttonInfo({ $img: $photo, toggle: toggleInfo }), buttonInfo({ toggle: toggleInfo }),
); );
effect(onLoad($photo).pipe( effect(onLoad($photo).pipe(
@ -59,7 +62,7 @@ export default function(render) {
], ],
})), })),
rxjs.catchError((err) => { rxjs.catchError((err) => {
if (err.target instanceof window.HTMLElement && err.type === "error") { if (err.target instanceof HTMLElement && err.type === "error") {
return rxjs.of($photo).pipe( return rxjs.of($photo).pipe(
removeLoader, removeLoader,
rxjs.tap(($img) => { rxjs.tap(($img) => {
@ -79,11 +82,10 @@ export default function(render) {
}), }),
)); ));
componentMetadata(createRender(qs($page, ".images_aside")), { toggle: toggleInfo, load$ });
componentPager(createRender(qs($page, ".component_pager"))); componentPager(createRender(qs($page, ".component_pager")));
} }
function buttonInfo({ $img, toggle }) { function buttonInfo({ toggle }) {
const $el = createElement(` const $el = createElement(`
<span> <span>
<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj4KICA8ZyB0cmFuc2Zvcm09Im1hdHJpeCgwLjg4MiwwLDAsMC44ODIsNS45LDUuOSkiPgogICAgPHBhdGggc3R5bGU9ImZpbGw6I2YyZjJmMjtmaWxsLW9wYWNpdHk6MSIgZD0ibSA2Mi4xNjIsMCBjIDYuNjk2LDAgMTAuMDQzLDQuNTY3IDEwLjA0Myw5Ljc4OSAwLDYuNTIyIC01LjgxNCwxMi41NTUgLTEzLjM5MSwxMi41NTUgLTYuMzQ0LDAgLTEwLjA0NSwtMy43NTIgLTkuODY5LC05Ljk0NyBDIDQ4Ljk0NSw3LjE3NiA1My4zNSwwIDYyLjE2MiwwIFogTSA0MS41NDMsMTAwIGMgLTUuMjg3LDAgLTkuMTY0LC0zLjI2MiAtNS40NjMsLTE3LjYxNSBsIDYuMDcsLTI1LjQ1NyBjIDEuMDU3LC00LjA3NyAxLjIzLC01LjcwNyAwLC01LjcwNyAtMS41ODgsMCAtOC40NTEsMi44MTYgLTEyLjUxLDUuNTkgTCAyNyw1Mi40MDYgQyAzOS44NjMsNDEuNDggNTQuNjYyLDM1LjA3MiA2MS4wMDQsMzUuMDcyIGMgNS4yODUsMCA2LjE2OCw2LjM2MSAzLjUyNSwxNi4xNDggTCA1Ny41OCw3Ny45OCBjIC0xLjIzNCw0LjcyOSAtMC43MDMsNi4zNTkgMC41MjcsNi4zNTkgMS41ODYsMCA2Ljc4NywtMS45NjMgMTEuODk2LC02LjA0MSBMIDczLDgyLjM3NyBDIDYwLjQ4OCw5NS4xIDQ2LjgzLDEwMCA0MS41NDMsMTAwIFoiIC8+CiAgPC9nPgo8L3N2Zz4K" alt="info"> <img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj4KICA8ZyB0cmFuc2Zvcm09Im1hdHJpeCgwLjg4MiwwLDAsMC44ODIsNS45LDUuOSkiPgogICAgPHBhdGggc3R5bGU9ImZpbGw6I2YyZjJmMjtmaWxsLW9wYWNpdHk6MSIgZD0ibSA2Mi4xNjIsMCBjIDYuNjk2LDAgMTAuMDQzLDQuNTY3IDEwLjA0Myw5Ljc4OSAwLDYuNTIyIC01LjgxNCwxMi41NTUgLTEzLjM5MSwxMi41NTUgLTYuMzQ0LDAgLTEwLjA0NSwtMy43NTIgLTkuODY5LC05Ljk0NyBDIDQ4Ljk0NSw3LjE3NiA1My4zNSwwIDYyLjE2MiwwIFogTSA0MS41NDMsMTAwIGMgLTUuMjg3LDAgLTkuMTY0LC0zLjI2MiAtNS40NjMsLTE3LjYxNSBsIDYuMDcsLTI1LjQ1NyBjIDEuMDU3LC00LjA3NyAxLjIzLC01LjcwNyAwLC01LjcwNyAtMS41ODgsMCAtOC40NTEsMi44MTYgLTEyLjUxLDUuNTkgTCAyNyw1Mi40MDYgQyAzOS44NjMsNDEuNDggNTQuNjYyLDM1LjA3MiA2MS4wMDQsMzUuMDcyIGMgNS4yODUsMCA2LjE2OCw2LjM2MSAzLjUyNSwxNi4xNDggTCA1Ny41OCw3Ny45OCBjIC0xLjIzNCw0LjcyOSAtMC43MDMsNi4zNTkgMC41MjcsNi4zNTkgMS41ODYsMCA2Ljc4NywtMS45NjMgMTEuODk2LC02LjA0MSBMIDczLDgyLjM3NyBDIDYwLjQ4OCw5NS4xIDQ2LjgzLDEwMCA0MS41NDMsMTAwIFoiIC8+CiAgPC9nPgo8L3N2Zz4K" alt="info">

View file

@ -1,7 +1,6 @@
import { createElement, createRender, onDestroy } from "../../lib/skeleton/index.js"; import { createElement, createRender } from "../../lib/skeleton/index.js";
import rxjs, { effect, onClick, onLoad } from "../../lib/rx.js"; import rxjs, { effect, onClick } from "../../lib/rx.js";
import { qs } from "../../lib/dom.js"; import { qs } from "../../lib/dom.js";
import assert from "../../lib/assert.js";
import t from "../../locales/index.js"; import t from "../../locales/index.js";
import { loadJS, loadCSS } from "../../helpers/loader.js"; import { loadJS, loadCSS } from "../../helpers/loader.js";
@ -30,7 +29,7 @@ function componentHeader(render, { toggle }) {
`); `);
render($header); render($header);
effect(onClick($header, `[alt="close"]`).pipe(rxjs.tap(toggle))); effect(onClick(qs($header, `[alt="close"]`)).pipe(rxjs.tap(toggle)));
} }
function componentBody(render, { load$ }) { function componentBody(render, { load$ }) {
@ -72,7 +71,7 @@ async function componentMap(render, { metadata }) {
if (!d || d.length !== 4) return null; if (!d || d.length !== 4) return null;
const [degrees, minutes, seconds, direction] = d; const [degrees, minutes, seconds, direction] = d;
const dd = degrees + minutes/60 + seconds/(60*60); const dd = degrees + minutes/60 + seconds/(60*60);
return direction == "S" || direction == "W" ? -dd : dd; return direction === "S" || direction === "W" ? -dd : dd;
}; };
const lat = DMSToDD(metadata.location[0]); const lat = DMSToDD(metadata.location[0]);
const lng = DMSToDD(metadata.location[1]); const lng = DMSToDD(metadata.location[1]);
@ -100,9 +99,11 @@ async function componentMap(render, { metadata }) {
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
const TILE_SERVER = "https://tile.openstreetmap.org/${z}/${x}/${y}.png"; const TILE_SERVER = "https://tile.openstreetmap.org/${z}/${x}/${y}.png";
const TILE_SIZE = parseInt($page.clientWidth / 3 * 100) / 100; const TILE_SIZE = Math.floor($page.clientWidth / 3 * 100) / 100;
if (TILE_SIZE === 0) return;
$page.style.height = "${TILE_SIZE*3}px;"; $page.style.height = "${TILE_SIZE*3}px;";
const mapper = function map_url(lat, lng, zoom) { const defaultTo = (val, def) => val === undefined ? val : def;
const mapper = (function map_url(lat, lng, zoom) {
// https://wiki.openstreetmap.org/wiki/Slippy_map_tilenamse // https://wiki.openstreetmap.org/wiki/Slippy_map_tilenamse
const n = Math.pow(2, zoom); const n = Math.pow(2, zoom);
const tile_numbers = [ const tile_numbers = [
@ -113,19 +114,21 @@ async function componentMap(render, { metadata }) {
return { return {
tile: function(tile_server, x = 0, y = 0) { tile: function(tile_server, x = 0, y = 0) {
return tile_server return tile_server
.replace("${x}", Math.floor(tile_numbers[0])+x) .replace("${x}", Math.floor(tile_numbers[0] || 0)+x)
.replace("${y}", Math.floor(tile_numbers[1])+y) .replace("${y}", Math.floor(tile_numbers[1] || 0)+y)
.replace("${z}", Math.floor(zoom)); .replace("${z}", Math.floor(zoom));
}, },
position: function() { position: function() {
const t0 = defaultTo(tile_numbers[0], 0);
const t1 = defaultTo(tile_numbers[1], 0);
return [ return [
tile_numbers[0] - Math.floor(tile_numbers[0]), t0 - Math.floor(t0),
tile_numbers[1] - Math.floor(tile_numbers[1]), t1 - Math.floor(t1),
]; ];
}, },
}; };
}(lat, lng, 11); }(lat, lng, 11));
const center = (position, i) => parseInt(TILE_SIZE * (1 + position[i]) * 1000)/1000; const center = (position, i) => Math.floor(TILE_SIZE * (1 + position[i]) * 1000)/1000;
const $tiles = createElement(` const $tiles = createElement(`
<div class="bigpicture"> <div class="bigpicture">
<div class="line"> <div class="line">
@ -146,9 +149,10 @@ async function componentMap(render, { metadata }) {
</div> </div>
`); `);
qs($page, `[data-bind="maptile"]`).appendChild($tiles); qs($page, `[data-bind="maptile"]`).appendChild($tiles);
const pos = mapper.position();
qs($page, ".marker").setAttribute("style", ` qs($page, ".marker").setAttribute("style", `
left: ${TILE_SIZE * (1 + mapper.position()[0]) - 15}px; left: ${TILE_SIZE * (1 + defaultTo(pos[0], 0)) - 15}px;
top: ${TILE_SIZE * (1 + mapper.position()[1]) - 30}px; top: ${TILE_SIZE * (1 + defaultTo(pos[1], 0)) - 30}px;
`); `);
$tiles.setAttribute("style", `transform-origin: ${center(mapper.position(), 0)}px ${center(mapper.position(), 1)}px;`); $tiles.setAttribute("style", `transform-origin: ${center(mapper.position(), 0)}px ${center(mapper.position(), 1)}px;`);
} }
@ -166,13 +170,13 @@ function componentMore(render, { metadata }) {
const formatValue = (str) => { const formatValue = (str) => {
if (!metadata.all || metadata.all[str] === undefined) return "-"; if (!metadata.all || metadata.all[str] === undefined) return "-";
if (typeof metadata.all[str] === "number") { if (typeof metadata.all[str] === "number") {
return parseInt(metadata.all[str]*100)/100; return Math.floor(metadata.all[str]*100)/100;
} else if (metadata.all[str].denominator !== undefined && } else if (metadata.all[str].denominator !== undefined &&
metadata.all[str].numerator !== undefined) { metadata.all[str].numerator !== undefined) {
if (metadata.all[str].denominator === 1) { if (metadata.all[str].denominator === 1) {
return metadata.all[str].numerator; return metadata.all[str].numerator;
} else if (metadata.all[str].numerator > metadata.all[str].denominator) { } else if (metadata.all[str].numerator > metadata.all[str].denominator) {
return parseInt( return Math.floor(
metadata.all[str].numerator * 10 / metadata.all[str].denominator, metadata.all[str].numerator * 10 / metadata.all[str].denominator,
) / 10; ) / 10;
} else { } else {
@ -195,7 +199,7 @@ function componentMore(render, { metadata }) {
if (a.toLowerCase().trim() < b.toLowerCase().trim()) return -1; if (a.toLowerCase().trim() < b.toLowerCase().trim()) return -1;
else if (a.toLowerCase().trim() > b.toLowerCase().trim()) return +1; else if (a.toLowerCase().trim() > b.toLowerCase().trim()) return +1;
return 0; return 0;
}).map((key) => { }).forEach((key) => {
switch (key) { switch (key) {
case "undefined": case "undefined":
case "thumbnail": case "thumbnail":
@ -218,18 +222,25 @@ export function init() {
]); ]);
} }
const extractExif = ($img) => new Promise((resolve) => EXIF.getData($img, function(data) { const extractExif = ($img) => new Promise((resolve) => window.EXIF.getData($img, function() {
const metadata = EXIF.getAllTags(this); const metadata = window.EXIF.getAllTags($img);
const to_date = (str) => { const to_date = (str = "") => {
if (!str) return null; const digits = str.split(/[ :]/).map((digit) => parseInt(digit));
return new Date(...str.split(/[ :]/)); return new Date(
digits[0] || 0,
digits[1] || 0,
digits[2] || 0,
digits[3] || 0,
digits[4] || 0,
digits[5] || 0,
);
}; };
resolve({ resolve({
date: to_date( date: to_date(
metadata["DateTime"] || metadata["DateTimeDigitized"] || metadata["DateTime"] || metadata["DateTimeDigitized"] ||
metadata["DateTimeOriginal"] || metadata["GPSDateStamp"], metadata["DateTimeOriginal"] || metadata["GPSDateStamp"],
), ),
location: metadata["GPSLatitude"] && metadata["GPSLongitude"] && [ location: (metadata["GPSLatitude"] && metadata["GPSLongitude"] && [
[ [
metadata["GPSLatitude"][0], metadata["GPSLatitude"][1], metadata["GPSLatitude"][0], metadata["GPSLatitude"][1],
metadata["GPSLatitude"][2], metadata["GPSLatitudeRef"], metadata["GPSLatitude"][2], metadata["GPSLatitudeRef"],
@ -238,34 +249,34 @@ const extractExif = ($img) => new Promise((resolve) => EXIF.getData($img, functi
metadata["GPSLongitude"][0], metadata["GPSLongitude"][1], metadata["GPSLongitude"][0], metadata["GPSLongitude"][1],
metadata["GPSLongitude"][2], metadata["GPSLongitudeRef"], metadata["GPSLongitude"][2], metadata["GPSLongitudeRef"],
], ],
] || null, ]) || null,
maker: metadata["Make"] || null, maker: metadata["Make"] || null,
model: metadata["Model"] || null, model: metadata["Model"] || null,
focal: metadata["FocalLength"] || null, focal: metadata["FocalLength"] || null,
aperture: metadata["FNumber"] || null, aperture: metadata["FNumber"] || null,
shutter: metadata["ExposureTime"] || null, shutter: metadata["ExposureTime"] || null,
iso: metadata["ISOSpeedRatings"] || null, iso: metadata["ISOSpeedRatings"] || null,
dimension: metadata["PixelXDimension"] && metadata["PixelYDimension"] && [ dimension: (metadata["PixelXDimension"] && metadata["PixelYDimension"] && [
metadata["PixelXDimension"], metadata["PixelXDimension"],
metadata["PixelYDimension"], metadata["PixelYDimension"],
] || null, ]) || null,
all: Object.keys(metadata).length === 0 ? null : metadata, all: Object.keys(metadata).length === 0 ? null : metadata,
}); });
})); }));
const formatTime = (t) => t && t.toLocaleTimeString( const formatTime = (t) => t?.toLocaleTimeString(
"en-us", "en-us",
{ weekday: "short", hour: "2-digit", minute: "2-digit" }, { weekday: "short", hour: "2-digit", minute: "2-digit" },
); );
const formatDate = (t) => t && t.toLocaleDateString( const formatDate = (t) => t?.toLocaleDateString(
navigator.language, navigator.language,
{ day: "numeric", year: "numeric", month: "short", day: "numeric" }, { year: "numeric", month: "short", day: "numeric" },
); );
const formatCameraSettings = (metadata) => { const formatCameraSettings = (metadata) => {
let str = format("model", metadata); const str = format("model", metadata);
const f = format("focal", metadata) const f = format("focal", metadata);
if (!f) return str; if (!f) return str;
return `${str} (${f})`; return `${str} (${f})`;
}; };
@ -286,11 +297,11 @@ const format = (key, metadata) => {
case "iso": case "iso":
return `ISO${metadata.iso}`; return `ISO${metadata.iso}`;
case "aperture": case "aperture":
return "ƒ"+parseInt(metadata.aperture*10)/10; return `ƒ${Math.floor(metadata.aperture*10)/10}`;
case "shutter": case "shutter":
if (metadata.shutter > 60) return metadata.shutter+"m"; if (metadata.shutter > 60) return metadata.shutter+"m";
else if (metadata.shutter > 1) return metadata.shutter+"s"; else if (metadata.shutter > 1) return metadata.shutter+"s";
return "1/"+parseInt(metadata.shutter.denominator / metadata.shutter.numerator)+"s"; return `1/${Math.floor(metadata.shutter.denominator / metadata.shutter.numerator)}s`;
case "dimension": case "dimension":
if (metadata.dimension.length !== 2 || !metadata.dimension[0] || !metadata.dimension[1]) return "-"; if (metadata.dimension.length !== 2 || !metadata.dimension[0] || !metadata.dimension[1]) return "-";
return metadata.dimension[0]+"x"+metadata.dimension[1]; return metadata.dimension[0]+"x"+metadata.dimension[1];

View file

@ -9,3 +9,5 @@ interface Window {
env?: string; env?: string;
chrome: object; chrome: object;
} }
export default function(any): void;

View file

@ -7,7 +7,7 @@ import assert from "../../lib/assert.js";
import "../../components/dropdown.js"; import "../../components/dropdown.js";
export default class ComponentMenubar extends window.HTMLElement { export default class ComponentMenubar extends HTMLElement {
constructor() { constructor() {
super(); super();
this.classList.add("component_menubar"); this.classList.add("component_menubar");
@ -20,13 +20,13 @@ export default class ComponentMenubar extends window.HTMLElement {
</div> </div>
`; `;
if (new URLSearchParams(location.search).get("nav") === "false") { if (new URLSearchParams(location.search).get("nav") === "false") {
const $container = assert.type(this.firstElementChild, window.HTMLElement); const $container = assert.type(this.firstElementChild, HTMLElement);
$container.classList.add("inherit-width"); $container.classList.add("inherit-width");
} }
} }
async connectedCallback() { async connectedCallback() {
const $title = assert.type(this.querySelector(".titlebar"), window.HTMLElement); const $title = assert.type(this.querySelector(".titlebar"), HTMLElement);
this.timeoutID = setTimeout(() => animate($title, { this.timeoutID = setTimeout(() => animate($title, {
time: 250, time: 250,
keyframes: slideYIn(2), keyframes: slideYIn(2),
@ -39,7 +39,7 @@ export default class ComponentMenubar extends window.HTMLElement {
} }
render(buttons) { render(buttons) {
const $item = assert.type(this.querySelector(".action-item"), window.HTMLElement); const $item = assert.type(this.querySelector(".action-item"), HTMLElement);
for (let i=buttons.length-1; i>=0; i--) { for (let i=buttons.length-1; i>=0; i--) {
$item.appendChild(buttons[i]); $item.appendChild(buttons[i]);
} }

View file

@ -1,129 +0,0 @@
const CACHE_NAME = "v0.3";
/*
* Control everything going through the wire, applying different
* strategy for caching, fetching resources
*/
self.addEventListener("fetch", function(event) {
const errResponse = (err) => (new Response(JSON.stringify({
code: "CANNOT_LOAD",
message: err.message
}), { status: 502 }));
if (is_a_ressource(event.request)) {
return event.respondWith(cacheFirstStrategy(event).catch(errResponse));
} else if (is_an_api_call(event.request)) {
return event;
} else if (is_an_index(event.request)) {
return event.respondWith(cacheFirstStrategy(event).catch(errResponse));
} else {
return event;
}
});
/*
* When a new service worker is coming in, we need to do a bit of
* cleanup to get rid of the rotten cache
*/
self.addEventListener("activate", function(event) {
vacuum(event);
});
self.addEventListener("error", function(err) {
console.error(err);
});
/*
* When a newly installed service worker is coming in, we want to use it
* straight away (make it active). By default it would be in a "waiting state"
*/
self.addEventListener("install", function() {
caches.open(CACHE_NAME).then(function(cache) {
return cache.addAll([
"/",
"/api/config"
]);
});
if (self.skipWaiting) {
self.skipWaiting();
}
});
function is_a_ressource(request) {
const p = _pathname(request);
if (["assets", "manifest.json", "favicon.ico"].indexOf(p[0]) !== -1) {
return true;
} else if (p[0] === "api" && (p[1] === "config")) {
return true;
}
return false;
}
function is_an_api_call(request) {
return _pathname(request)[0] === "api";
}
function is_an_index(request) {
return ["files", "view", "login", "logout", ""]
.indexOf(_pathname(request)[0]) >= 0;
}
// //////////////////////////////////////
// HELPERS
// //////////////////////////////////////
function vacuum(event) {
return event.waitUntil(
caches.keys().then(function(cachesName) {
return Promise.all(cachesName.map(function(cacheName) {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
return null;
}));
})
);
}
function _pathname(request) {
return request.url.replace(/^http[s]?:\/\/[^\/]*\//, "").split("/");
}
/*
* strategy is cache first:
* 1. use whatever is in the cache
* 2. perform the network call to update the cache
*/
function cacheFirstStrategy(event) {
return caches.open(CACHE_NAME).then(function(cache) {
return cache.match(event.request).then(function(response) {
if (!response) {
return fetchAndCache(event);
}
fetchAndCache(event).catch(nil);
return response;
});
});
function fetchAndCache(event) {
// A request is a stream and can only be consumed once. Since we are consuming this
// once by cache and once by the browser for fetch, we need to clone the response as
// seen on:
// https://developers.google.com/web/fundamentals/getting-started/primers/service-workers
return fetch(event.request)
.then(function(response) {
if (!response || response.status !== 200) {
return response;
}
// A response is a stream and can only because we want the browser to consume the
// response as well as the cache consuming the response, we need to clone it
const responseClone = response.clone();
caches.open(CACHE_NAME).then(function(cache) {
cache.put(event.request, responseClone);
});
return response;
});
}
function nil() {}
}

1
public/global.d.ts vendored
View file

@ -1,4 +1,5 @@
interface Window { interface Window {
chrome: object;
overrides: { overrides: {
[key: string]: any; [key: string]: any;
"xdg-open"?: (mime: string) => void; "xdg-open"?: (mime: string) => void;

View file

@ -9,10 +9,10 @@
<link rel="stylesheet" href="custom.css"> <link rel="stylesheet" href="custom.css">
{{ if eq .license "agpl" }} {{ if eq .license "agpl" }}
<script> <script>
class NyanCatLoader extends window.HTMLElement { class NyanCatLoader extends HTMLElement {
connectedCallback() { connectedCallback() {
this.innerHTML = this.render(); this.innerHTML = this.render();
this.timeout = window.setTimeout(function(){ this.timeout = setTimeout(function(){
const $rbw = document.querySelector("#rbw .w"); const $rbw = document.querySelector("#rbw .w");
$rbw.innerHTML = $rbw.innerHTML.repeat(10); $rbw.innerHTML = $rbw.innerHTML.repeat(10);
@ -22,7 +22,7 @@
} }
disconnectedCallback() { disconnectedCallback() {
window.clearTimeout(this.timeout); clearTimeout(this.timeout);
} }
render() { render() {

View file

@ -1,11 +1,21 @@
{ {
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"alwaysStrict": true,
"checkJs": true, "checkJs": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"noEmit": true, "noEmit": true,
"noErrorTruncation": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noImplicitThis": true, "noImplicitThis": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitAny": false,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
@ -13,9 +23,7 @@
"strictPropertyInitialization": true, "strictPropertyInitialization": true,
"strictBindCallApply": true, "strictBindCallApply": true,
"strictFunctionTypes": true, "strictFunctionTypes": true,
"strict": false, "strict": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"lib": [ "lib": [
"dom", "dom",
"dom.iterable", "dom.iterable",

View file

@ -6,13 +6,7 @@ export default defineConfig(({ comand, mode }) => {
test: { test: {
global: true, global: true,
environment: "jsdom", environment: "jsdom",
setupFiles: ["./test/setup.js"], setupFiles: ["./vite.setup.js"],
},
coverage: {
reporter: ["html"],
exclude: [
"./assets/lib/vendor/**"
],
} }
}; };
}); });

17
public/vite.setup.js Normal file
View file

@ -0,0 +1,17 @@
import {
describe, it, test, expect, vi,
afterEach, afterAll, beforeEach, beforeAll,
} from "vitest";
global.nextTick = () => new Promise((done) => setTimeout(done, 0));
global.requestAnimationFrame = (callback) => setTimeout(callback, 0);
global.describe = describe;
global.it = it;
global.test = test;
global.expect = expect;
global.vi = vi;
global.beforeEach = beforeEach;
global.beforeAll = beforeAll;
global.afterEach = afterEach;
global.afterAll = afterAll;