mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-11-03 04:50:14 +08:00
212 lines
8.6 KiB
JavaScript
212 lines
8.6 KiB
JavaScript
import { createElement } from "../../lib/skeleton/index.js";
|
|
import { animate, slideYIn } from "../../lib/animate.js";
|
|
import rxjs, { effect } from "../../lib/rx.js";
|
|
import { loadCSS } from "../../helpers/loader.js";
|
|
import { qs } from "../../lib/dom.js";
|
|
import { ApplicationError } from "../../lib/error.js";
|
|
import { createLoader } from "../../components/loader.js";
|
|
import ctrlError from "../ctrl_error.js";
|
|
|
|
import { createThing } from "./thing.js";
|
|
import { getState$ } from "./ctrl_filesystem_state.js";
|
|
import { ls } from "./model_files.js";
|
|
|
|
export default async function(render) {
|
|
const $page = createElement(`
|
|
<div class="component_filesystem container">
|
|
<div class="ifscroll-before"></div>
|
|
<div data-target="list" class="list"></div>
|
|
<div class="ifscroll-after"></div>
|
|
<br>
|
|
</div>
|
|
`);
|
|
render($page);
|
|
|
|
// feature: virtual scrolling
|
|
const removeLoader = createLoader($page);
|
|
const path = location.pathname.replace(new RegExp("^/files"), "");
|
|
effect(rxjs.of(path).pipe(
|
|
ls(),
|
|
removeLoader,
|
|
rxjs.mergeMap(({ files, ...rest }) => getState$().pipe(rxjs.map((p) => {
|
|
// files = files.sort()
|
|
if (p.show_hidden === false) files = files.filter(({ name }) => name[0] !== ".");
|
|
return { ...rest, files, ...p };
|
|
}))),
|
|
rxjs.mergeMap(({ files, ...rest }) => {
|
|
if (files.length === 0) {
|
|
renderEmpty(render);
|
|
return rxjs.EMPTY;
|
|
}
|
|
return rxjs.of({...rest, files });
|
|
}),
|
|
rxjs.mergeMap(({ files, path, view }) => { // STEP1: setup the list of files
|
|
const FILE_HEIGHT = 160;
|
|
const BLOCK_SIZE = Math.ceil(document.body.clientHeight / FILE_HEIGHT) + 1;
|
|
// const BLOCK_SIZE = 6;
|
|
const COLUMN_PER_ROW = 4;
|
|
const VIRTUAL_SCROLL_MINIMUM_TRIGGER = 50;
|
|
let size = files.length;
|
|
if (size > VIRTUAL_SCROLL_MINIMUM_TRIGGER) {
|
|
size = Math.min(files.length, BLOCK_SIZE * COLUMN_PER_ROW);
|
|
}
|
|
const $list = qs($page, `[data-target="list"]`);
|
|
$list.closest(".scroll-y").scrollTop = 0;
|
|
const $fs = document.createDocumentFragment();
|
|
for (let i = 0; i < size; i++) {
|
|
const file = files[i];
|
|
$fs.appendChild(createThing({
|
|
name: file.name,
|
|
type: file.type,
|
|
...createLink(file, path),
|
|
view,
|
|
}));
|
|
}
|
|
animate($list, { time: 200, keyframes: slideYIn(5) });
|
|
$list.replaceChildren($fs);
|
|
|
|
//////////////////////////////////////
|
|
// CASE 1: virtual scroll isn't enabled
|
|
if (files.length <= VIRTUAL_SCROLL_MINIMUM_TRIGGER) {
|
|
return rxjs.EMPTY;
|
|
}
|
|
|
|
//////////////////////////////////////
|
|
// CASE 2: with virtual scroll
|
|
const $listBefore = qs($page, ".ifscroll-before");
|
|
const $listAfter = qs($page, ".ifscroll-after");
|
|
const height = (Math.ceil(files.length / COLUMN_PER_ROW) - BLOCK_SIZE) * FILE_HEIGHT;
|
|
if (height > 33554400) {
|
|
console.log(`maximum CSS height reached, requested height ${height} is too large`);
|
|
}
|
|
const setHeight = (size) => {
|
|
if (size < 0 || size > height) throw new ApplicationError(
|
|
"INTERNAL ERROR",
|
|
`assertion on size failed: size[${size}] height[${height}]`
|
|
);
|
|
$listBefore.style.height = `${size}px`;
|
|
$listAfter.style.height = `${height - size}px`;
|
|
};
|
|
setHeight(0);
|
|
// const top = ($node) => $node.getBoundingClientRect().top;
|
|
return rxjs.of({
|
|
files,
|
|
path,
|
|
currentState: 0,
|
|
$list,
|
|
setHeight,
|
|
FILE_HEIGHT,
|
|
BLOCK_SIZE,
|
|
COLUMN_PER_ROW,
|
|
MARGIN: 35, // TODO: top($list) - top($list.closest(".scroll-y"));
|
|
});
|
|
}),
|
|
rxjs.mergeMap(({
|
|
files, path,
|
|
BLOCK_SIZE, COLUMN_PER_ROW, FILE_HEIGHT,
|
|
MARGIN,
|
|
currentState,
|
|
height, setHeight,
|
|
$list,
|
|
}) => rxjs.fromEvent($page.closest(".scroll-y"), "scroll", { passive: true }).pipe(
|
|
rxjs.map((e) => {
|
|
// 0-------------0-----------1-----------2-----------3 ....
|
|
// [padding] $block1 $block2 $block3 ....
|
|
const nextState = Math.floor((e.target.scrollTop - MARGIN) / FILE_HEIGHT);
|
|
return Math.max(nextState, 0);
|
|
}),
|
|
rxjs.distinctUntilChanged(),
|
|
rxjs.debounce(() => new rxjs.Observable((observer) => {
|
|
const id = requestAnimationFrame(() => observer.next());
|
|
return () => cancelAnimationFrame(id);
|
|
})),
|
|
rxjs.tap((nextState) => {
|
|
// STEP1: calculate the virtual scroll paramameters
|
|
let diff = nextState - currentState;
|
|
const diffSgn = Math.sign(diff);
|
|
if (Math.abs(diff) > BLOCK_SIZE) { // diff is bound by BLOCK_SIZE
|
|
// we can't be moving more than what is on the screen
|
|
diff = diffSgn * BLOCK_SIZE;
|
|
}
|
|
let fileStart = nextState * COLUMN_PER_ROW;
|
|
if (diffSgn > 0) { // => scroll down
|
|
fileStart += BLOCK_SIZE * COLUMN_PER_ROW;
|
|
fileStart -= Math.min(diff, BLOCK_SIZE) * COLUMN_PER_ROW;
|
|
}
|
|
let fileEnd = fileStart + diffSgn * diff * COLUMN_PER_ROW;
|
|
if (fileStart >= files.length) { // occur when BLOCK_SIZE is larger than its absolute minimum
|
|
return;
|
|
}
|
|
else if (fileEnd > files.length) {
|
|
// occur when files.length isn't a multiple of COLUMN_PER_ROW and
|
|
// we've scrolled to the bottom of the list already
|
|
nextState = Math.ceil(files.length / COLUMN_PER_ROW) - BLOCK_SIZE;
|
|
fileEnd = files.length - 1;
|
|
for (let i=0; i<COLUMN_PER_ROW; i++) {
|
|
// add some padding to fileEnd to balance the list to the
|
|
// nearest COLUMN_PER_ROW
|
|
fileEnd += 1;
|
|
if (fileEnd % COLUMN_PER_ROW === 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// STEP2: create the new elements
|
|
const $fs = document.createDocumentFragment();
|
|
let n = 0;
|
|
for (let i = fileStart; i < fileEnd; i++) {
|
|
const file = files[i];
|
|
if (file === undefined) $fs.appendChild(createThing({
|
|
type: "hidden",
|
|
}));
|
|
else $fs.appendChild(createThing({
|
|
name: file.name,
|
|
type: file.type,
|
|
...createLink(file, path),
|
|
}));
|
|
n += 1;
|
|
}
|
|
|
|
// STEP3: update the DOM
|
|
if (diffSgn > 0) { // scroll down
|
|
$list.appendChild($fs);
|
|
for (let i = 0; i < n; i++) $list.firstChild.remove();
|
|
} else { // scroll up
|
|
$list.insertBefore($fs, $list.firstChild);
|
|
for (let i = 0; i < n; i++) $list.lastChild.remove();
|
|
}
|
|
setHeight(nextState * FILE_HEIGHT);
|
|
currentState = nextState;
|
|
}),
|
|
)),
|
|
rxjs.catchError(ctrlError()),
|
|
));
|
|
}
|
|
|
|
function renderEmpty(render) {
|
|
render(createElement(`
|
|
<div class="error">
|
|
<p class="empty_image no-select">
|
|
<img class="component_icon" draggable="false" src="/assets/icons/empty_folder.svg" alt="empty_folder">
|
|
</p>
|
|
<p class="label">There is nothing here</p>
|
|
</div>
|
|
`));
|
|
}
|
|
|
|
export function init() {
|
|
return Promise.all([
|
|
loadCSS(import.meta.url, "./ctrl_filesystem.css"),
|
|
loadCSS(import.meta.url, "./thing.css"),
|
|
]);
|
|
}
|
|
|
|
function createLink(file, filepath) {
|
|
let path = filepath + file.name;
|
|
let link = "";
|
|
if (file.type === "directory") path += "/";
|
|
link = file.type === "directory" ? "/files" + path : "/view" + path;
|
|
return { path, link };
|
|
}
|