diff --git a/public/assets/components/breadcrumb.css b/public/assets/components/breadcrumb.css
index 28562bd6..98220782 100644
--- a/public/assets/components/breadcrumb.css
+++ b/public/assets/components/breadcrumb.css
@@ -3,19 +3,16 @@
z-index: 5;
}
.component_breadcrumb .breadcrumb {
- background: white;
- border-bottom: 2px solid rgba(100, 100, 100, 0.05);
- transition: 1s ease border-bottom;
margin: 0 0 0px 0;
z-index: 1000;
- padding: 3px 0;
+ padding: 3px 0 3px 10px;
}
.component_breadcrumb .breadcrumb .ul {
list-style-type: none;
- margin: 0 auto;
- width: 95%;
- max-width: 800px;
- padding: 0;
+ margin: 0;
+ width: 100%;
+ padding: 0 10px 0 10px;
+ box-sizing: border-box;
}
.component_breadcrumb .breadcrumb .ul > span {
display: block;
@@ -24,9 +21,9 @@
.component_breadcrumb .breadcrumb .ul div, .component_breadcrumb .breadcrumb .ul .li {
display: inline-block;
}
-.component_breadcrumb:hover .breadcrumb {
- border-bottom: 2px solid rgba(100, 100, 100, 0.1);
-}
+/* .component_breadcrumb:hover .breadcrumb { */
+/* border-bottom: 2px solid rgba(100, 100, 100, 0.1); */
+/* } */
.component_breadcrumb .component_logout {
float: right;
display: inline-block;
diff --git a/public/assets/components/breadcrumb.js b/public/assets/components/breadcrumb.js
index f1ffb0e2..0a365d5a 100644
--- a/public/assets/components/breadcrumb.js
+++ b/public/assets/components/breadcrumb.js
@@ -16,7 +16,6 @@ class ComponentBreadcrumb extends HTMLDivElement {
`;
const paths = (this.getAttribute("path") || "").replace(new RegExp("/$"), "").split("/");
- console.log(paths);
const htmlPathChunks = paths.map((chunk, idx) => {
const label = idx === 0 ? "Filestash" : chunk;
const link = paths.slice(0, idx + 1).join("/") + "/";
diff --git a/public/assets/components/decorator_shell_filemanager.css b/public/assets/components/decorator_shell_filemanager.css
new file mode 100644
index 00000000..a940d7e0
--- /dev/null
+++ b/public/assets/components/decorator_shell_filemanager.css
@@ -0,0 +1,63 @@
+.component_filemanager_shell {
+ display: flex;
+ height: 100%;
+}
+.component_filemanager_shell .component_sidebar {
+ width: 240px;
+ padding: 10px;
+}
+.component_filemanager_shell .component_sidebar > h3 {
+ padding-left: 10px;
+ margin-bottom: 5px;
+ color: var(--light);
+ font-size: 1rem;
+}
+.component_filemanager_shell .component_sidebar > div {
+ background: rgba(0,0,0,0.1);
+ padding-left: 10px;
+ width: 90%;
+ height: 50px;
+ margin: 0 auto;
+}
+.component_filemanager_shell [is="component-breadcrumb"]:hover ~ [data-bind="filemanager-children"],
+.component_filemanager_shell [data-bind="sidebar"]:hover ~ div > [data-bind="filemanager-children"] {
+ border-color: rgba(100, 100, 100, 0.1);
+}
+
+.component_filemanager_shell [data-bind="filemanager-children"] {
+ border-top: 2px solid rgba(0,0,0,0.02);
+ border-left: 2px solid rgba(0,0,0,0.02);
+ transition: 1000ms ease border-color;
+ border-top-left-radius: 15px;
+ display: flex;
+ height: 100%;
+ overflow: hidden;
+ background: white;
+ padding-top: 20px;
+}
+
+
+.component_filemanager_shell [data-bind="sidebar"].hidden ~ div > [data-bind="filemanager-children"] {
+ border-top-left-radius: 0;
+ border-left: none;
+}
+.component_filemanager_shell [data-bind="sidebar"].hidden ~ div > [is="component-breadcrumb"] > .component_breadcrumb,
+.component_filemanager_shell [data-bind="sidebar"].hidden ~ div > [data-bind="filemanager-children"] > div {
+ width: 95%;
+ margin: 0 auto;
+ max-width: 820px;
+}
+.component_filemanager_shell [data-bind="sidebar"].hidden ~ div [is="component-breadcrumb"] {
+ background: white;
+}
+.component_filemanager_shell [data-bind="sidebar"].hidden ~ div > [data-bind="filemanager-children"] {
+ background: var(--bg-color);
+}
+.component_filemanager_shell .component_filesystem {
+ padding: 15px;
+}
+
+.component_filemanager_shell [data-bind="filemanager-children"] > div,
+.component_filemanager_shell [is="component-breadcrumb"] {
+ max-width: 2100px;
+}
diff --git a/public/assets/components/decorator_shell_filemanager.js b/public/assets/components/decorator_shell_filemanager.js
new file mode 100644
index 00000000..e4dfd186
--- /dev/null
+++ b/public/assets/components/decorator_shell_filemanager.js
@@ -0,0 +1,56 @@
+import { createElement, createRender } from "../lib/skeleton/index.js";
+import { qs } from "../lib/dom.js";
+import { CSS } from "../helpers/loader.js";
+
+export default function(ctrl) {
+ const $page = createElement(`
+
-
-
`);
render($page);
-}
+})
diff --git a/public/assets/pages/filespage/ctrl_filesystem.css b/public/assets/pages/filespage/ctrl_filesystem.css
new file mode 100644
index 00000000..2352a6fd
--- /dev/null
+++ b/public/assets/pages/filespage/ctrl_filesystem.css
@@ -0,0 +1,4 @@
+.list .container_fs {
+ content-visibility: auto;
+ contain-intrinsic-size: 1px 2700px;
+}
diff --git a/public/assets/pages/filespage/ctrl_filesystem.js b/public/assets/pages/filespage/ctrl_filesystem.js
new file mode 100644
index 00000000..9e8a2ac9
--- /dev/null
+++ b/public/assets/pages/filespage/ctrl_filesystem.js
@@ -0,0 +1,169 @@
+import { createElement } from "../../lib/skeleton/index.js";
+import { animate, slideYIn } from "../../lib/animate.js";
+import rxjs, { effect } from "../../lib/rx.js";
+import { CSS } from "../../helpers/loader.js";
+import { qs } from "../../lib/dom.js";
+import { ApplicationError } from "../../lib/error.js";
+import { toggle as toggleLoader } from "../../components/loader.js";
+import ctrlError from "../ctrl_error.js";
+
+import { createThing, allocateMemory, css } from "./thing.js";
+import { handleError, getFiles } from "./ctrl_filesystem_state.js";
+import { ls } from "./model_files.js";
+
+export default async function(render) {
+ const $page = createElement(`
+
+ `);
+ render($page);
+
+ // feature: virtual scrolling
+ const path = location.pathname.replace(new RegExp("^/files"), "");
+ effect(rxjs.of(path).pipe(
+ toggleLoader($page, true),
+ rxjs.mergeMap(() => new Promise((done) => setTimeout(() => done({
+ files: new Array(400).fill(1),
+ }), 1000))),
+ toggleLoader($page, false),
+ rxjs.mergeMap(({ files }) => {
+ const BLOCK_SIZE = 8;
+ const COLUMN_PER_ROW = 2;
+ const FILE_HEIGHT = 160;
+ const size = Math.min(files.length, BLOCK_SIZE * COLUMN_PER_ROW);
+ allocateMemory(BLOCK_SIZE * COLUMN_PER_ROW);
+ const $fs = document.createDocumentFragment();
+ for (let i = 0; i < size; i++) {
+ $fs.appendChild(createThing({
+ name: `file ${i}`,
+ type: "file",
+ link: "/view/test.txt",
+ }));
+ }
+ const $list = qs($page, ".list");
+ const $listBefore = qs($page, ".ifscroll-before");
+ const $listAfter = qs($page, ".ifscroll-after");
+
+ const height = (Math.floor(files.length / COLUMN_PER_ROW) - BLOCK_SIZE) * FILE_HEIGHT;
+ const setHeight = (size) => {
+ $listBefore.style.height = `${size}px`;
+ $listAfter.style.height = `${height - size}px`;
+ };
+ setHeight(0);
+ animate($list, { time: 200, keyframes: slideYIn(5) });
+ $list.appendChild($fs);
+ if (files.length === size) return rxjs.EMPTY;
+ return rxjs.of({
+ files,
+ currentState: 0,
+ BLOCK_SIZE, COLUMN_PER_ROW, FILE_HEIGHT,
+ setHeight,
+ $list,
+ });
+ }),
+ rxjs.mergeMap(({
+ files,
+ BLOCK_SIZE, COLUMN_PER_ROW, FILE_HEIGHT,
+ currentState,
+ height, setHeight,
+ $list,
+ }) => rxjs.fromEvent(
+ $page.parentElement.parentElement.parentElement,
+ "scroll", { passive: true },
+ ).pipe(
+ rxjs.map((e) => Math.min(
+ Math.ceil(Math.max(0, e.target.scrollTop) / FILE_HEIGHT),
+ // cap state value when BLOCK_SIZE is larger than minimum value. This is to
+ // prevent issues when scrolling fast to the bottom (aka diff > 1)
+ Math.ceil(files.length / COLUMN_PER_ROW) - BLOCK_SIZE,
+ )),
+ 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 = diffSgn * BLOCK_SIZE; // fast scroll
+ let fileStart = nextState * COLUMN_PER_ROW;
+ if (diffSgn > 0) { // => scroll down
+ // eg: files[15] BLOCK_SIZE=1 COLUMN_PER_ROW=1
+ // -----------[currentState:0]--------------------------[nextState:5]---------
+ // -----------[fileStart=5+1=6]
+ fileStart += BLOCK_SIZE * COLUMN_PER_ROW;
+ // -----------[fileStart=6-min(5,1)=5]
+ fileStart -= Math.min(diff, BLOCK_SIZE) * COLUMN_PER_ROW;
+ }
+ let fileEnd = fileStart + diffSgn * diff * COLUMN_PER_ROW;
+
+ if (fileStart >= files.length) throw new ApplicationError(
+ "INTERNAL_ERROR",
+ `assert failed in virtual scroll range[${fileStart}:${fileEnd}] length[${files.length}]`,
+ ); 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
+ nextState = Math.floor(files.length / COLUMN_PER_ROW) - BLOCK_SIZE;
+ fileEnd = files.length;
+ do {
+ // add some padding to fileEnd to balance the list to the
+ // nearest COLUMN_PER_ROW
+ fileEnd += 1;
+ } while (fileEnd % COLUMN_PER_ROW !== 0);
+ }
+
+ // 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({
+ name: "dummy",
+ }))
+ else $fs.appendChild(createThing({
+ name: `file - ${i}`,
+ type: "file",
+ link: "/view/test.txt",
+ }));
+ n += 1;
+ }
+ // console.log(`n[${n}] state[${currentState} -> ${nextState}] files[${fileStart}:${fileEnd}]`)
+
+ // STEP3: update the DOM
+ if (diffSgn > 0) { // scroll down
+ // console.log("DOWN", n);
+ // // if (!isInViewport($list.firstChild)) // TODO
+ $list.appendChild($fs);
+ for (let i = 0; i < n; i++) $list.firstChild.remove();
+ } else { // scroll up
+ // console.log("UP", n)
+ $list.insertBefore($fs, $list.firstChild);
+ for (let i = 0; i < n; i++) $list.lastChild.remove();
+ }
+ setHeight(nextState * FILE_HEIGHT);
+ currentState = nextState;
+ }),
+ )),
+ rxjs.catchError(ctrlError()),
+ ));
+
+ // feature2: fs in "search" mode
+ // TODO
+}
+
+function isInViewport(element) {
+ const rect = element.getBoundingClientRect();
+ return rect.bottom > 0;
+}
diff --git a/public/assets/pages/filespage/state.js b/public/assets/pages/filespage/ctrl_filesystem_state.js
similarity index 95%
rename from public/assets/pages/filespage/state.js
rename to public/assets/pages/filespage/ctrl_filesystem_state.js
index 2cce69b5..3c455c34 100644
--- a/public/assets/pages/filespage/state.js
+++ b/public/assets/pages/filespage/ctrl_filesystem_state.js
@@ -35,3 +35,5 @@ export const onNewDirectory = () => {
export const onSearch = () => {
console.log("SEARCH");
};
+
+export const getFiles = (n) => {}
diff --git a/public/assets/pages/filespage/ctrl_submenu.css b/public/assets/pages/filespage/ctrl_submenu.css
new file mode 100644
index 00000000..7e2237c8
--- /dev/null
+++ b/public/assets/pages/filespage/ctrl_submenu.css
@@ -0,0 +1,133 @@
+.component_submenu.sticky {
+ position: sticky;
+ top: 0;
+ z-index: 3;
+ background: var(--bg-color);
+ opacity: 0.95;
+}
+
+.component_submenu {
+ padding-left: 0;
+ padding-right: 0;
+ padding-bottom: 0;
+ margin-bottom: -10px;
+}
+.component_submenu .menubar {
+ font-size: 15px;
+ line-height: 15px;
+ height: 15px;
+ margin-top: 5px;
+ color: var(--light);
+ margin: 5px 0 10px 0;
+ /* Create Buttons */
+}
+.component_submenu .menubar > span, .component_submenu .menubar form label {
+ display: inline-block;
+ margin-top: -5px;
+ border-radius: 2px;
+ cursor: pointer;
+ background-position: center;
+}
+.component_submenu .menubar > span:active, .component_submenu .menubar > span:hover, .component_submenu .menubar form label:active, .component_submenu .menubar form label:hover {
+ color: var(--color);
+}
+.component_submenu .menubar > span:hover, .component_submenu .menubar form label:hover {
+ transition: background 0.4s;
+ background: #ffffff00 radial-gradient(circle, transparent 1%, var(--bg-color) 1%) center/15000%;
+}
+.component_submenu .menubar > span:active, .component_submenu .menubar form label:active {
+ background-color: #00000033;
+ background-size: 100%;
+ transition: background 0s;
+}
+.component_submenu .menubar > span {
+ padding: 5px 8px;
+ letter-spacing: 0.06em;
+ display: inline-block;
+ text-transform: uppercase;
+ font-size: 12px;
+ color: var(--light);
+ vertical-align: text-bottom;
+}
+.component_submenu .menubar > div {
+ cursor: pointer;
+ margin-right: 15px;
+}
+.component_submenu .menubar .view {
+ float: right;
+ padding: 2px;
+ transition: 0.15s ease-out background;
+ margin-right: 0px;
+ margin-left: 0px;
+}
+.component_submenu .menubar .view .search, .component_submenu .menubar .view.list-grid, .component_submenu .menubar .view > .dropdown_button, .component_submenu .menubar .view form > label {
+ min-width: inherit;
+ border-radius: 2px;
+ padding: 5px;
+ margin: 2px 0 2px 0;
+}
+.component_submenu .menubar .view.list-grid, .component_submenu .menubar .view .dropdown_button, .component_submenu .menubar .view .search {
+ background-position: center;
+}
+.component_submenu .menubar .view.list-grid:hover, .component_submenu .menubar .view .dropdown_button:hover, .component_submenu .menubar .view .search:hover {
+ transition: background 0.4s;
+ background: #ffffff00 radial-gradient(circle, transparent 1%, var(--bg-color) 1%) center/15000%;
+}
+.component_submenu .menubar .view.list-grid:active, .component_submenu .menubar .view .dropdown_button:active, .component_submenu .menubar .view .search:active {
+ background-color: #00000033;
+ background-size: 100%;
+ transition: background 0s;
+}
+.component_submenu .menubar .view .dropdown_container .component_icon {
+ box-sizing: border-box;
+ border: 2px solid rgba(0, 0, 0, 0);
+}
+.component_submenu .menubar .view.list-grid, .component_submenu .menubar .view .search {
+ margin-top: -5px !important;
+}
+.component_submenu .menubar .view.component_dropdown {
+ margin-top: -10px;
+}
+.component_submenu .menubar .view.component_dropdown.active .dropdown_button {
+ box-shadow: none;
+}
+.component_submenu .menubar.search_focus form input {
+ transition: 0.2s width ease-in;
+ opacity: 1;
+}
+.component_submenu .menubar form {
+ float: right;
+ margin-top: -3px;
+}
+.component_submenu .menubar form input {
+ opacity: 0;
+ font-size: 1em;
+ margin-top: -3px;
+ border: none;
+ background: inherit;
+ border-bottom: 2px solid var(--light);
+ color: var(--color);
+}
+.component_submenu .menubar.search_focus > span {
+ display: none;
+}
+.component_submenu .menubar .component_icon {
+ height: 15px;
+ width: 15px;
+}
+
+.component_submenu .submenuwithSelection-appear {
+ display: inline-block;
+ opacity: 0;
+ transform: translateY(3px);
+ transition: opacity 0.2s ease, transform 0.4s ease;
+}
+
+.component_submenu .submenuwithSelection-appear.submenuwithSelection-appear-active {
+ opacity: 1;
+ transform: translateY(0px);
+}
+
+.dark-mode .component_submenu input#search {
+ color: rgba(255, 255, 255, 0.8);
+}
diff --git a/public/assets/pages/filespage/ctrl_submenu.js b/public/assets/pages/filespage/ctrl_submenu.js
new file mode 100644
index 00000000..5c248f53
--- /dev/null
+++ b/public/assets/pages/filespage/ctrl_submenu.js
@@ -0,0 +1,87 @@
+import { createElement, createRender } from "../../lib/skeleton/index.js";
+import { CSS } from "../../helpers/loader.js";
+
+export default async function(render) {
+
+ const $page = createElement(`
+
+ `)
+ render($page);
+}
+
+const css = `
+.component_submenu {
+ display: flex;
+ justify-content: space-between;
+ padding: 0 15px;
+}
+.component_submenu .action.left {
+ margin-left: 2px;
+ margin-right: 5px;
+}
+.component_submenu .action.right {
+ float: right;
+ margin-right: 10px;
+}
+.component_submenu .action.right button img {
+ width: 16px;
+ height: 16px;
+}
+.component_submenu .action.right button,
+.component_submenu .action.left button {
+ background: var(--bg-color);
+ border: 2px solid rgba(100, 100, 100, 0.05);
+ padding: 5px 10px;
+}
+`
+
+export async function OldOne(render) {
+ const $page = createElement(`
+
+
+`);
+
+ render($page);
+}
diff --git a/public/assets/pages/filespage/filesystem.js b/public/assets/pages/filespage/filesystem.js
deleted file mode 100644
index 50613add..00000000
--- a/public/assets/pages/filespage/filesystem.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { createElement } from "../../lib/skeleton/index.js";
-import rxjs, { effect } from "../../lib/rx.js";
-import { qs } from "../../lib/dom.js";
-
-import { toggle as toggleLoader } from "../../components/loader.js";
-
-import { createThing, css } from "./thing.js";
-import { handleError } from "./state.js";
-import { ls } from "./model_files.js";
-
-export default async function(render) {
- const $page = createElement(`
-