mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-15 21:04:46 +01:00
chore (rewrite): frontend rewrite
This commit is contained in:
parent
708ba9ea21
commit
b142392307
22 changed files with 314 additions and 137 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import rxjs, { ajax } from "../lib/rx.js";
|
||||
// import { setup_cache } from "../helpers/cache.js";
|
||||
import { init as setup_translation } from "../locales/index.js";
|
||||
import { init as setup_loader, loadJS } from "../helpers/loader.js";
|
||||
import { report } from "../helpers/log.js";
|
||||
|
||||
|
|
@ -50,6 +49,45 @@ function $error(msg) {
|
|||
document.body.appendChild($code);
|
||||
}
|
||||
|
||||
/// /////////////////////////////////////////
|
||||
// boot steps helpers
|
||||
function setup_translation() {
|
||||
let selectedLanguage = "en";
|
||||
switch (window.navigator.language) {
|
||||
case "zh-TW":
|
||||
selectedLanguage = "zh_tw";
|
||||
break;
|
||||
default:
|
||||
const userLanguage = window.navigator.language.split("-")[0] || "en";
|
||||
const idx = [
|
||||
"az", "be", "bg", "ca", "cs", "da", "de", "el", "es", "et",
|
||||
"eu", "fi", "fr", "gl", "hr", "hu", "id", "is", "it", "ja",
|
||||
"ka", "ko", "lt", "lv", "mn", "nb", "nl", "pl", "pt", "ro",
|
||||
"ru", "sk", "sl", "sr", "sv", "th", "tr", "uk", "vi", "zh"
|
||||
].indexOf(window.navigator.language.split("-")[0] || "");
|
||||
if (idx !== -1) {
|
||||
selectedLanguage = userLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedLanguage === "en") {
|
||||
return;
|
||||
}
|
||||
return ajax({
|
||||
url: "/assets/locales/" + selectedLanguage + ".json",
|
||||
responseType: "json"
|
||||
}).pipe(
|
||||
rxjs.tap(({ responseHeaders, response }) => {
|
||||
const contentType = responseHeaders["content-type"].trim();
|
||||
if (contentType !== "application/json") {
|
||||
report("boot::translation", new Error(`wrong content type '${contentType}'`), "ctrl_boot_frontoffice.js");
|
||||
return;
|
||||
}
|
||||
window.LNG = response;
|
||||
})
|
||||
).toPromise();
|
||||
}
|
||||
|
||||
async function setup_xdg_open() {
|
||||
window.overrides = {};
|
||||
return loadJS(import.meta.url, "/overrides/xdg-open.js");
|
||||
|
|
@ -62,7 +100,7 @@ async function setup_device() {
|
|||
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
document.body.classList.add("dark-mode");
|
||||
}
|
||||
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function(e) {
|
||||
window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function(e) {
|
||||
e.matches ? document.body.classList.add("dark-mode") : document.body.classList.remove("dark-mode");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { animate, slideYOut, slideYIn, opacityOut } from "../lib/animate.js";
|
|||
import { loadCSS } from "../helpers/loader.js";
|
||||
|
||||
import { mv } from "../pages/filespage/model_files.js";
|
||||
import { extractPath } from "../pages/filespage/helper.js";
|
||||
import { extractPath, isDir } from "../pages/filespage/helper.js";
|
||||
|
||||
class ComponentBreadcrumb extends window.HTMLDivElement {
|
||||
constructor() {
|
||||
|
|
@ -171,7 +171,8 @@ class ComponentBreadcrumb extends window.HTMLDivElement {
|
|||
let to = $path.getAttribute("data-path");
|
||||
|
||||
const [fromBasepath, fromName] = extractPath(from);
|
||||
to += fromName + "/";
|
||||
to += fromName;
|
||||
if (isDir(from)) to += "/";
|
||||
await mv(from, to).toPromise();
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import ajax from "../../lib/ajax.js";
|
|||
|
||||
const release$ = ajax({
|
||||
url: "/about",
|
||||
responseType: "text"
|
||||
responseType: "text",
|
||||
}).pipe(rxjs.shareReplay(1));
|
||||
|
||||
export function get() {
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ const css = `
|
|||
}
|
||||
`;
|
||||
|
||||
function calculateBacklink(pathname) {
|
||||
function calculateBacklink(pathname = "") {
|
||||
let url = "/";
|
||||
const listPath = pathname.replace(new RegExp("/$"), "").split("/");
|
||||
switch (listPath[1]) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import rxjs, { effect } from "../lib/rx.js";
|
|||
import { qs } from "../lib/dom.js";
|
||||
import { loadCSS } from "../helpers/loader.js";
|
||||
import WithShell, { init as initShell } from "../components/decorator_shell_filemanager.js";
|
||||
import { get as getConfig } from "./filespage/model_config.js";
|
||||
|
||||
import componentFilesystem, { init as initFilesystem } from "./filespage/ctrl_filesystem.js";
|
||||
import componentSubmenu, { init as initSubmenu } from "./filespage/ctrl_submenu.js";
|
||||
|
|
@ -38,7 +39,7 @@ export default WithShell(function(render) {
|
|||
export function init() {
|
||||
return Promise.all([
|
||||
loadCSS(import.meta.url, "./ctrl_filespage.css"),
|
||||
initShell(), initFilesystem(),
|
||||
initShell(), initFilesystem(), getConfig().toPromise(),
|
||||
initSubmenu(), initNewItem(), initUpload(),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { onDestroy } from "../../lib/skeleton/index.js";
|
||||
import rxjs from "../../lib/rx.js";
|
||||
import fscache from "./cache.js";
|
||||
import { extractPath } from "./helper.js";
|
||||
import { extractPath, isDir } from "./helper.js";
|
||||
|
||||
/*
|
||||
* The transcient cache is used to rerender the list of files in a particular location. That's used
|
||||
|
|
@ -215,7 +215,6 @@ export function mv(ajax$, fromPath, toPath) {
|
|||
});
|
||||
}
|
||||
const onSuccess = async () => {
|
||||
console.log(fromPath, toPath)
|
||||
fscache().remove(fromPath, false);
|
||||
if (fromBasepath === toBasepath) {
|
||||
stateAdd(mutationFiles$, fromBasepath, {
|
||||
|
|
@ -244,7 +243,21 @@ export function mv(ajax$, fromPath, toPath) {
|
|||
return file;
|
||||
},
|
||||
});
|
||||
removeLoading(virtualFiles$, toBasepath, toName);
|
||||
onDestroy(() => statePop(mutationFiles$, fromBasepath, fromName));
|
||||
statePop(virtualFiles$, toBasepath, toName);
|
||||
await fscache().update(fromBasepath, ({ files, ...rest }) => ({
|
||||
files: files.filter((file) => file.name === fromName ? false : true),
|
||||
...rest,
|
||||
}))
|
||||
await fscache().update(toBasepath, ({ files, ...rest }) => ({
|
||||
files: files.concat([{
|
||||
name: fromName,
|
||||
time: new Date().getTime(),
|
||||
type,
|
||||
}]),
|
||||
...rest,
|
||||
}));
|
||||
if (isDir(fromPath)) await fscache.remove(fromPath);
|
||||
}
|
||||
};
|
||||
const onFailure = () => {
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ export default async function(render) {
|
|||
shift: false, files: [],
|
||||
});
|
||||
if (elm1) addSelection({
|
||||
n: (files$.value || []).length,
|
||||
n: (files$.value || []).length - 1,
|
||||
path: path + elm1.name + (elm1.type === "directory" ? "/" : ""),
|
||||
shift: true, files: (files$.value || []),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export default async function(render) {
|
|||
|
||||
function componentLeft(render, { $scroll }) {
|
||||
effect(getSelection$().pipe(
|
||||
rxjs.filter((selections) => selections.length === 0),
|
||||
rxjs.filter(() => lengthSelection() === 0),
|
||||
rxjs.mergeMap(() => rxjs.merge(rxjs.fromEvent(window, "resize"), rxjs.of(null))),
|
||||
rxjs.mergeMap(() => getPermission()),
|
||||
rxjs.map(() => render(createFragment(`
|
||||
|
|
@ -87,15 +87,15 @@ function componentLeft(render, { $scroll }) {
|
|||
onDestroy(() => setAction(null));
|
||||
|
||||
effect(getSelection$().pipe(
|
||||
rxjs.filter((selections) => selections.length === 1),
|
||||
rxjs.filter(() => lengthSelection() === 1),
|
||||
rxjs.map(() => render(createFragment(`
|
||||
<a ${generateLinkAttributes(expandSelection())}><button data-action="download" title="${t("Download")}">
|
||||
<a target="_blank" ${generateLinkAttributes(expandSelection())}><button data-action="download" title="${t("Download")}">
|
||||
${t("Download")}
|
||||
</button></a>
|
||||
<button data-action="delete"${toggleDependingOnPermission(currentPath(), "delete")} title="${t("Remove")}">
|
||||
${t("Remove")}
|
||||
</button>
|
||||
<button data-action="share" title="${t("Share")}">
|
||||
<button data-action="share" title="${t("Share")}" class="hidden">
|
||||
${t("Share")}
|
||||
</button>
|
||||
<button data-action="embed" class="hidden" title="${t("Embed")}">
|
||||
|
|
@ -152,9 +152,9 @@ function componentLeft(render, { $scroll }) {
|
|||
));
|
||||
|
||||
effect(getSelection$().pipe(
|
||||
rxjs.filter((selections) => selections.length > 1),
|
||||
rxjs.filter((selections) => lengthSelection() > 1),
|
||||
rxjs.map(() => render(createFragment(`
|
||||
<a ${generateLinkAttributes(expandSelection())}><button data-action="download">
|
||||
<a target="_blank" ${generateLinkAttributes(expandSelection())}><button data-action="download">
|
||||
${t("Download")}
|
||||
</button></a>
|
||||
<button data-action="delete"${toggleDependingOnPermission(currentPath(), "delete")}>
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@ import t from "../../locales/index.js";
|
|||
|
||||
export default function(render) {
|
||||
const $page = createFragment(`
|
||||
<div is="component_filezone"></div>
|
||||
<div is="component_upload_queue"></div>
|
||||
|
||||
<div is="component_filezone"></div>
|
||||
<div is="component_upload_fab"></div>
|
||||
`);
|
||||
|
||||
componentFilezone(createRender($page.children[0]));
|
||||
componentUploadQueue(createRender($page.children[1]));
|
||||
componentFilezone(createRender($page.children[0]));
|
||||
componentUploadFAB(createRender($page.children[2]));
|
||||
|
||||
render($page);
|
||||
|
|
@ -25,7 +26,7 @@ export function init() {
|
|||
|
||||
function componentUploadQueue(render) {
|
||||
const $page = createElement(`
|
||||
<div class="component_upload hidden">
|
||||
<div class="component_upload">
|
||||
<h2>${t("Current Upload")} <div class="count_block">
|
||||
<span class="completed">24</span>
|
||||
<span class="grandTotal">24</span>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import { extname } from "../../lib/path.js";
|
||||
|
||||
const regexCurrentPath = new RegExp("^/files")
|
||||
export function currentPath() {
|
||||
return decodeURIComponent(location.pathname.replace(new RegExp("^/files"), ""));
|
||||
return decodeURIComponent(location.pathname.replace(regexCurrentPath, ""));
|
||||
}
|
||||
|
||||
const regexDir = new RegExp("/$");
|
||||
export function isDir(path) {
|
||||
return regexDir.test(path);
|
||||
}
|
||||
|
||||
export function extractPath(path) {
|
||||
path = path.replace(new RegExp("/$"), "");
|
||||
path = path.replace(regexDir, "");
|
||||
const p = path.split("/");
|
||||
const filename = p.pop();
|
||||
return [p.join("/") + "/", filename];
|
||||
|
|
|
|||
8
public/assets/pages/filespage/model_config.js
Normal file
8
public/assets/pages/filespage/model_config.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import rxjs from "../../lib/rx.js";
|
||||
import { get as getConfig } from "../../model/config.js";
|
||||
|
||||
const config$ = getConfig().pipe(rxjs.shareReplay(1));
|
||||
|
||||
export function get() {
|
||||
return config$;
|
||||
}
|
||||
|
|
@ -22,40 +22,38 @@ import {
|
|||
*/
|
||||
|
||||
export function touch(path) {
|
||||
const ajax$ = rxjs.of(null).pipe(
|
||||
rxjs.delay(1000),
|
||||
// rxjs.tap(() => {
|
||||
// throw new Error("NOOOO");
|
||||
// }),
|
||||
rxjs.delay(1000),
|
||||
);
|
||||
const ajax$ = ajax({
|
||||
url: `/api/files/touch?path=${encodeURIComponent(path)}`,
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
});
|
||||
return cacheTouch(ajax$, path);
|
||||
}
|
||||
|
||||
export function mkdir(path) {
|
||||
const ajax$ = rxjs.of(null).pipe(
|
||||
rxjs.delay(1000),
|
||||
// rxjs.tap(() => {
|
||||
// throw new Error("NOOOO");
|
||||
// }),
|
||||
rxjs.delay(1000),
|
||||
);
|
||||
const ajax$ = ajax({
|
||||
url: `/api/files/mkdir?path=${encodeURIComponent(path)}`,
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
});
|
||||
return cacheMkdir(ajax$, path);
|
||||
}
|
||||
|
||||
export function rm(...paths) {
|
||||
const ajax$ = rxjs.of(null).pipe(
|
||||
rxjs.delay(1000),
|
||||
// rxjs.tap(() => {
|
||||
// throw new Error("NOOOO");
|
||||
// }),
|
||||
rxjs.delay(1000),
|
||||
);
|
||||
const ajax$ = rxjs.forkJoin(paths.map((path) => ajax({
|
||||
url: `/api/files/rm?path=${encodeURIComponent(path)}`,
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
})));
|
||||
return cacheRm(ajax$, ...paths);
|
||||
}
|
||||
|
||||
export function mv(from, to) {
|
||||
const ajax$ = rxjs.of(null).pipe(rxjs.delay(1000));
|
||||
const ajax$ = ajax({
|
||||
url: `/api/files/mv?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`,
|
||||
method: "POST",
|
||||
responseType: "json",
|
||||
});
|
||||
return cacheMv(ajax$, from, to);
|
||||
}
|
||||
|
||||
|
|
@ -66,8 +64,9 @@ export function save(path) { // TODO
|
|||
export function ls(path) {
|
||||
const lsFromCache = (path) => rxjs.from(fscache().get(path));
|
||||
const lsFromHttp = (path) => ajax({
|
||||
url: `/api/files/ls?path=${path}`,
|
||||
responseType: "json"
|
||||
url: `/api/files/ls?path=${encodeURIComponent(path)}`,
|
||||
method: "GET",
|
||||
responseType: "json",
|
||||
}).pipe(
|
||||
rxjs.map(({ responseJSON }) => ({
|
||||
files: responseJSON.results,
|
||||
|
|
@ -114,7 +113,7 @@ export function ls(path) {
|
|||
export function search(term) {
|
||||
const path = location.pathname.replace(new RegExp("^/files/"), "/");
|
||||
return ajax({
|
||||
url: `/api/files/search?path=${path}&q=${term}`,
|
||||
url: `/api/files/search?path=${encodeURIComponent(path)}&q=${encodeURIComponent(term)}`,
|
||||
responseType: "json"
|
||||
}).pipe(rxjs.map(({ responseJSON }) => ({
|
||||
files: responseJSON.results,
|
||||
|
|
|
|||
|
|
@ -1,32 +1,51 @@
|
|||
import rxjs from "../../lib/rx.js";
|
||||
import { onDestroy } from "../../lib/skeleton/index.js";
|
||||
import { ApplicationError } from "../../lib/error.js";
|
||||
import { currentPath } from "./helper.js";
|
||||
import { currentPath, extractPath } from "./helper.js";
|
||||
|
||||
const selection$ = new rxjs.BehaviorSubject([]);
|
||||
/*
|
||||
* CAUTION: Use a lot of caution if you change this file as it's easy for a bug to slip in and be responsible
|
||||
* for someone using selection to delete something and lose some data. We really don't want this to happen!
|
||||
*
|
||||
* BACKGROUND: There's many way we could have work on the selection. In fact I've made a couple prior attempts
|
||||
* with very simple implementations but none of them felt "right" from a user point of view. The approach
|
||||
* taken here mimicks the behavior of osx finder. To understand what is going on, I strongly advise you
|
||||
* open up osx finder and play with it first.
|
||||
*
|
||||
* DATA STRUCTURE: The supporting data structure is a list containing 2 kind of elements:
|
||||
* - "anchors" which is created when clicking on something with the cmd key
|
||||
* - "range" which is when you click somewhere with the expectation it will expand your range
|
||||
* using the shift key
|
||||
*/
|
||||
const selection$ = new rxjs.BehaviorSubject([
|
||||
// { "type": "anchor", ... } => user click somewhere and the shift key is NOT pressed
|
||||
// { "type": "range", ... } => ----------------------------------------- pressed
|
||||
]);
|
||||
|
||||
onDestroy(clearSelection);
|
||||
|
||||
export function addSelection({ shift = false, n = 0, ...rest }) {
|
||||
const newSelection = selection$.value;
|
||||
let alreadyKnown = false;
|
||||
for (let i=0; i<newSelection.length; i++) {
|
||||
if (newSelection[i].n !== n) {
|
||||
continue;
|
||||
}
|
||||
alreadyKnown = true;
|
||||
if (newSelection[i].shift === shift) {
|
||||
continue;
|
||||
}
|
||||
newSelection[i].shift = shift;
|
||||
selection$.next(newSelection);
|
||||
}
|
||||
// console.log(n, shift)
|
||||
const selections = selection$.value;
|
||||
const selection = { type: shift ? "range" : "anchor", n, ...rest };
|
||||
|
||||
if (alreadyKnown === false) selection$.next(
|
||||
selection$.value
|
||||
.concat({ shift, n, ...rest })
|
||||
.sort((prev, curr) => prev.n - curr.n)
|
||||
);
|
||||
// case1: select a single file/folder
|
||||
if (selection.type === "anchor") {
|
||||
selection$.next(selections.concat([selection]));
|
||||
return
|
||||
}
|
||||
// case2: range selection
|
||||
const last = selection$.value[selections.length - 1] || { n: 0, type: "anchor" };
|
||||
if (last.type === "anchor") {
|
||||
selection$.next(selections.concat([selection]));
|
||||
return;
|
||||
}
|
||||
// clear out previous range selector. That's the behavior on apple finder when we do:
|
||||
// [A,1] [R,3] = [A,1] [R,3] => expands to [1,2,3]
|
||||
// [A,1] [R,3] [R,8] = [A,1] [R,8] => expands to [1,2,3,4,5,6,7,8]
|
||||
// [A,1] [R,3] [R,8] [R,6] = [A,1] [R,6] => expands to [1,2,3,4,5,6]
|
||||
selections[selections.length - 1] = selection;
|
||||
selection$.next(selections);
|
||||
}
|
||||
|
||||
export function clearSelection() {
|
||||
|
|
@ -39,51 +58,72 @@ export function getSelection$() {
|
|||
|
||||
export function isSelected(n) {
|
||||
let isChecked = false;
|
||||
for (let i=0;i<selection$.value.length;i++) {
|
||||
if (selection$.value[i]["n"] === n) isChecked = !isChecked;
|
||||
else if (selection$.value[i]["shift"]
|
||||
&& isBetween(n, selection$.value[i-1]?.n, selection$.value[i]?.n))
|
||||
isChecked = !isChecked
|
||||
const selections = selection$.value;
|
||||
for (let i=0;i<selections.length;i++) {
|
||||
if (selections[i].type === "anchor" && selections[i].n === n) {
|
||||
isChecked = !isChecked;
|
||||
}
|
||||
else if (selections[i].type === "range" && isBetween(
|
||||
n, selections[i-1]?.n || 0, selections[i]?.n,
|
||||
)) {
|
||||
isChecked = true; // WARNING: change with caution
|
||||
}
|
||||
}
|
||||
return isChecked;
|
||||
}
|
||||
|
||||
export function lengthSelection() {
|
||||
const selections = selection$.value;
|
||||
let l = 0;
|
||||
for (let i=0; i<selections.length; i++) {
|
||||
l += selections[i].shift && selections[i-1] ?
|
||||
selections[i]["n"] - selections[i-1]["n"] :
|
||||
1;
|
||||
}
|
||||
return l;
|
||||
return _selectionHelper((set) => set.size);
|
||||
}
|
||||
|
||||
export function expandSelection() {
|
||||
const selections = [];
|
||||
for (let i=0; i<selection$.value.length; i++) {
|
||||
const curr = selection$.value[i];
|
||||
if (curr.shift === false) {
|
||||
selections.push({ path: curr.path });
|
||||
continue;
|
||||
return _selectionHelper((set) => {
|
||||
const arr = new Array(set.size);
|
||||
let i = 0;
|
||||
for (const path of set) {
|
||||
arr[i] = { path };
|
||||
i += 1;
|
||||
}
|
||||
let prev = selection$.value[i-1];
|
||||
if (!prev) prev = { n: 0 };
|
||||
for (let i=1; i<= curr.n - prev.n; i++) {
|
||||
if (i > 100000) throw new ApplicationError(
|
||||
"Internal error",
|
||||
`pages::state_selection.js curr=${curr.n} prev=${prev.n}`,
|
||||
);
|
||||
const path = currentPath();
|
||||
const f = curr.files[prev.n + i];
|
||||
selections.push({
|
||||
path: path + f.name + (f.type === "directory" ? "/" : ""),
|
||||
});
|
||||
}
|
||||
}
|
||||
return selections;
|
||||
// console.log(JSON.stringify(arr, null, 2));
|
||||
return arr;
|
||||
});
|
||||
}
|
||||
|
||||
function isBetween(n, lowerBound, higherBound) {
|
||||
return n < higherBound && n > lowerBound;
|
||||
// This is the core function used to not only calculate the selection length but also expand the
|
||||
// selection. We're quite slow with an algo running in 3N selection size with room for improvement
|
||||
// but be very cautious, a bug in here could cause terrible consequence
|
||||
function _selectionHelper(fn) {
|
||||
const set = new Set();
|
||||
const selections = selection$.value;
|
||||
for (let i=0; i<selections.length; i++) {
|
||||
const curr = selections[i];
|
||||
if (selections[i].type === "anchor") {
|
||||
if (isSelected(selections[i].n)) {
|
||||
set.add(curr.path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const [basepath] = extractPath(curr.path);
|
||||
if (!selections[i-1]) {
|
||||
for (let j=0; j<=selections[i].n; j++) {
|
||||
if (isSelected(j) === false) continue;
|
||||
const file = selections[i].files[j];
|
||||
set.add(basepath + file.name + (file.type === "directory" ? "/" : ""));
|
||||
}
|
||||
} else {
|
||||
const min = Math.min(selections[i].n, selections[i-1]?.n || 0);
|
||||
const max = Math.max(selections[i].n, selections[i-1]?.n || 0);
|
||||
for (let j=min; j<=max; j++) {
|
||||
if (isSelected(j) === false) continue;
|
||||
const file = selections[i].files[j];
|
||||
set.add(basepath + file.name + (file.type === "directory" ? "/" : ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
return fn(set);
|
||||
}
|
||||
|
||||
|
||||
function isBetween(n, lowerBound, higherBound) {
|
||||
return n <= Math.max(higherBound, lowerBound) && n >= Math.min(lowerBound, higherBound);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,16 @@
|
|||
margin: 0;
|
||||
display: block;
|
||||
}
|
||||
.list > .component_thing.view-grid > img.component_icon.thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: 50% 50%;
|
||||
background: var(--dark);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: block;
|
||||
}
|
||||
.list > .component_thing.view-grid > img.component_icon {
|
||||
padding: 30px;
|
||||
box-sizing: border-box;
|
||||
|
|
@ -131,14 +141,6 @@
|
|||
margin: 0 auto;
|
||||
z-index: 0;
|
||||
}
|
||||
.list > .component_thing.view-grid > img.thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: 50% 50%;
|
||||
background: var(--dark);
|
||||
z-index: 0;
|
||||
}
|
||||
.list > .component_thing.view-grid .info_extension {
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
|
|
@ -168,7 +170,7 @@
|
|||
}
|
||||
.list > .component_thing.view-grid .component_filename {
|
||||
letter-spacing: -0.5px;
|
||||
z-index: 2;
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 2px;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -14,7 +14,7 @@ import { STLLoader } from "../../lib/vendor/three/STLLoader.js";
|
|||
import { FBXLoader } from "../../lib/vendor/three/FBXLoader.js";
|
||||
import { Rhino3dmLoader } from "../../lib/vendor/three/3DMLoader.js";
|
||||
|
||||
import ctrlError from "../ctrl_error.js";
|
||||
import componentDownloader, { init as initDownloader } from "./application_downloader.js";
|
||||
|
||||
import "../../components/menubar.js";
|
||||
|
||||
|
|
@ -29,6 +29,13 @@ export default function(render, { mime }) {
|
|||
|
||||
const removeLoader = createLoader(qs($page, ".threeviewer_container"));
|
||||
effect(rxjs.of(getLoader(mime)).pipe(
|
||||
rxjs.mergeMap(([loader, createMesh]) => {
|
||||
if (!loader) {
|
||||
componentDownloader(render);
|
||||
return rxjs.EMPTY;
|
||||
}
|
||||
return rxjs.of([loader, createMesh]);
|
||||
}),
|
||||
rxjs.mergeMap(([loader, createMesh]) => new rxjs.Observable((observer) => loader.load(
|
||||
getDownloadUrl(),
|
||||
(object) => observer.next(createMesh(object)),
|
||||
|
|
@ -81,7 +88,6 @@ export default function(render, { mime }) {
|
|||
renderer.render(scene, camera);
|
||||
}));
|
||||
}),
|
||||
rxjs.catchError(ctrlError()),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -105,10 +111,13 @@ function getLoader(mime) {
|
|||
case "application/fbx":
|
||||
return [new FBXLoader(), identity];
|
||||
default:
|
||||
throw new Error(`Invalid loader for "${mime}"`);
|
||||
return [null, null];
|
||||
}
|
||||
}
|
||||
|
||||
export function init() {
|
||||
return loadCSS(import.meta.url, "./application_3d.css");
|
||||
return Promise.all([
|
||||
loadCSS(import.meta.url, "./application_3d.css"),
|
||||
initDownloader(),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import rxjs, { effect } from "../../lib/rx.js";
|
|||
import { animate, slideXIn, opacityOut } from "../../lib/animate.js";
|
||||
import { qs } from "../../lib/dom.js";
|
||||
import { createLoader } from "../../components/loader.js";
|
||||
import modal from "../../components/modal.js";
|
||||
import { createModal } from "../../components/modal.js";
|
||||
import { loadCSS, loadJS } from "../../helpers/loader.js";
|
||||
import ajax from "../../lib/ajax.js";
|
||||
import { extname } from "../../lib/path.js";
|
||||
|
|
@ -169,7 +169,7 @@ export default async function(render) {
|
|||
// },
|
||||
// );
|
||||
return new Promise((done) => {
|
||||
modal.open(createElement(`
|
||||
createModal(createElement(`
|
||||
<div style="text-align:center;padding-bottom:5px;">
|
||||
Do you want to save the changes ?
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default function(render) {
|
|||
<div class="formviewer_container hidden">
|
||||
<form class="sticky box"></form>
|
||||
</div>
|
||||
<button is="component-fab"></button>
|
||||
<button is="component-fab" data-options="download"></button>
|
||||
</div>
|
||||
`);
|
||||
render($page);
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ import rxjs, { effect } from "../../lib/rx.js";
|
|||
import { loadCSS } from "../../helpers/loader.js";
|
||||
import assert from "../../lib/assert.js";
|
||||
import ctrlError from "../ctrl_error.js";
|
||||
import notification from "../../components/notification.js";
|
||||
|
||||
import { getDownloadUrl } from "./common.js";
|
||||
import { getCurrentPath } from "./common.js";
|
||||
|
||||
export default function(render, opts = {}) {
|
||||
export default function(render, { endpoint = "" }) {
|
||||
const $page = createElement(`
|
||||
<div class="component_appframe">
|
||||
<iframe src="${getDownloadUrl()}"></iframe>
|
||||
<iframe src="${endpoint}?path=${encodeURIComponent(getCurrentPath())}"></iframe>
|
||||
</div>
|
||||
`);
|
||||
render($page);
|
||||
|
|
@ -19,10 +20,10 @@ export default function(render, opts = {}) {
|
|||
rxjs.tap((event) => { // TODO: notification
|
||||
switch (event.data.type) {
|
||||
case "notify::info":
|
||||
// notify.send(t(event.data.message), "info");
|
||||
notify.info(t(event.data.message));
|
||||
break;
|
||||
case "notify::error":
|
||||
// notify.send(t(event.data.message), "error");
|
||||
notify.error(t(event.data.message));
|
||||
break;
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default function(render) {
|
|||
<img class="photo idle hidden" draggable="true" src="${getDownloadUrl()}">
|
||||
</div>
|
||||
<div class="images_aside scroll-y"></div>
|
||||
<div class="component_pager"></div>
|
||||
<div class="component_pager hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@
|
|||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"noEmit": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
|
|
|
|||
18
public/vite.config.js
Normal file
18
public/vite.config.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig(({ comand, mode }) => {
|
||||
return {
|
||||
plugins: [],
|
||||
test: {
|
||||
global: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./test/setup.js"],
|
||||
},
|
||||
coverage: {
|
||||
reporter: ["text", "html"],
|
||||
exclude: [
|
||||
"./public/assets/lib/vendor/**"
|
||||
],
|
||||
}
|
||||
};
|
||||
});
|
||||
Loading…
Reference in a new issue