frontend: head

Massimo Melina committed Dec 13, 2021 at 00:29 UTC fa1b57130f8d5159283724616b0040dc7f9c5f74
6 files changed +189 -91
NOTES.md
+14 -10
@@ -1,19 +1,23 @@
1 # File browser
2 -* Consider packing toolkit
3 - * if it's a "no", download CDN stuff
2 +- Consider packing toolkit
3 + - if it's a "no", download CDN stuff
4
5 ## What GUI lib?
6
7 ### Material
8 -* css+js
9 - * https://materializecss.com/
10 -* preact wrapper for [MCW](https://github.com/material-components/material-components-web)
11 - * https://github.com/developit/preact-material-components
8 +- css+js
9 + - https://materializecss.com/
10 +- preact wrapper for [MCW](https://github.com/material-components/material-components-web)
11 + - https://github.com/developit/preact-material-components
12
13 ### Others
14 -* https://preactjs.com/about/libraries-addons/
15 - * [fluid](https://unpkg.com/preact-fluid@0.9.1/lib/index.js)
16 - * no module?
14 +- https://preactjs.com/about/libraries-addons/
15 + - [fluid](https://unpkg.com/preact-fluid@0.9.1/lib/index.js)
16 + - no module?
17
18 # Study
19 -https://www.tutorialspoint.com/koajs/koajs_quick_guide.htm
\ No newline at end of file
19 +https://www.tutorialspoint.com/koajs/koajs_quick_guide.htm
20 +
21 +# To do
22 +- vfs
23 +- sticky breadcrumbs
frontend/src/App.tsx
+4 -79
@@ -1,8 +1,7 @@
1 -import React from 'react'
2 -import { BrowserRouter, Link, Route, Routes, useLocation } from "react-router-dom"
3 -import { createElement as h, Fragment } from 'react'
1 +import { BrowserRouter, Route, Routes } from "react-router-dom"
2 +import { createElement as h } from 'react'
3 import './App.css'
5 -import { useApi } from './api'
4 +import { BrowseFiles } from "./BrowseFiles";
5
6 function App() {
7 return h(BrowserRouter, {},
@@ -12,78 +11,4 @@ function App() {
11 )
12 }
13
15 -export default App;
16 -
17 -function BrowseFiles() {
18 - return h(Fragment, {},
19 - h(Head),
20 - h(FilesList))
21 -}
22 -
23 -function Head() {
24 - return null
25 -}
26 -
27 -interface DirEntry { n:string, s?:number, m?:string, c?:string }
28 -
29 -function FilesList() {
30 - const path = useLocation().pathname
31 - let res = useApi('file_list', { path })?.list
32 - if (!res)
33 - return h(Loading)
34 - if (path > '/')
35 - res = [{ n: '../' }, ...res]
36 - return h('ul', { className:'dir' }, res.map((entry:DirEntry) => h(File, { key: entry.n, ...entry })))
37 -}
38 -
39 -function File({ n, m, c, s }: DirEntry) {
40 - const isDir = n.endsWith('/')
41 - const t = m||c ||null
42 - return h('li', {},
43 - isDir ? h(Link, { to: n }, hIcon('folder'), n)
44 - : h('a', { href: n }, hIcon('description'), n),
45 - h('div', { className:'entry-props' },
46 - s !== undefined && h(Fragment, {},
47 - h('span', { className:'entry-size' }, formatBytes(s)),
48 - hIcon('download'),
49 - ),
50 - t && h('span', { className:'entry-ts' }, new Date(t).toLocaleString()),
51 - ),
52 - h('div', { style:{ clear:'both' } })
53 - )
54 -}
55 -
56 -function hIcon(name: string) {
57 - return h(Icon, { name })
58 -}
59 -
60 -function Icon({ name }: { name:string }) {
61 - return h('span',{
62 - className: 'material-icons-outlined icon',
63 - }, name)
64 -}
65 -
66 -function Loading() {
67 - return 'loading' as React.ReactNode as React.ReactElement
68 -}
69 -
70 -function formatBytes(n: number, post: string = 'B') {
71 - if (isNaN(Number(n)))
72 - return ''
73 - let x = ['', 'K', 'M', 'G', 'T']
74 - let prevMul = 1
75 - let mul = 1024
76 - let i = 0
77 - while (i < x.length && n > mul) {
78 - prevMul = mul
79 - mul *= 1024
80 - ++i
81 - }
82 - n /= prevMul
83 - return round(n, 1) + ' ' + (x[i]||'') + post
84 -} // formatBytes
85 -
86 -function round(v: number, decimals: number = 0) {
87 - decimals = Math.pow(10, decimals)
88 - return Math.round(v * decimals) / decimals
89 -} // round
14 +export default App;
\ No newline at end of file
frontend/src/BrowseFiles.ts new
+44
@@ -0,0 +1,44 @@
1 +import { Link, useLocation } from 'react-router-dom'
2 +import { useApi } from './api'
3 +import { createElement as h, Fragment } from 'react'
4 +import { formatBytes, hIcon, Loading } from './misc'
5 +import { Head } from './Head'
6 +
7 +export function BrowseFiles() {
8 + const path = useLocation().pathname
9 + let res = useApi('file_list', { path })
10 + if (!res)
11 + return h(Loading)
12 + const { list } = res
13 + return h(Fragment, {},
14 + h(Head, { list }),
15 + h(FilesList, { list }))
16 +}
17 +
18 +interface DirEntry { n:string, s?:number, m?:string, c?:string }
19 +export type DirList = DirEntry[]
20 +
21 +function FilesList({ list }:{ list:DirList }) {
22 + return h('ul', { className:'dir' },
23 + list.map((entry:DirEntry) =>
24 + h(File, { key: entry.n, ...entry }) ))
25 +}
26 +
27 +function File({ n, m, c, s }: DirEntry) {
28 + const base = useLocation().pathname
29 + const isDir = n.endsWith('/')
30 + const t = m||c ||null
31 + return h('li', {},
32 + isDir ? h(Link, { to: base+n }, hIcon('folder'), n)
33 + : h('a', { href: n }, hIcon('file'), n),
34 + h('div', { className:'entry-props' },
35 + s !== undefined && h(Fragment, {},
36 + h('span', { className:'entry-size' }, formatBytes(s)),
37 + hIcon('download'),
38 + ),
39 + t && h('span', { className:'entry-ts' }, new Date(t).toLocaleString()),
40 + ),
41 + h('div', { style:{ clear:'both' } })
42 + )
43 +}
44 +
frontend/src/Head.ts new
+54
@@ -0,0 +1,54 @@
1 +import { createElement as h, Fragment, useMemo } from 'react'
2 +import { Link, useLocation } from 'react-router-dom'
3 +import { DirList } from './BrowseFiles'
4 +import { formatBytes, hIcon, prefix } from './misc'
5 +
6 +export function Head({ list }:{ list:DirList }) {
7 + return h(Fragment, {},
8 + h(MenuPanel),
9 + h(Breadcrumbs),
10 + h(FolderStats, { list })
11 + )
12 +}
13 +
14 +function MenuPanel() {
15 + return null
16 +}
17 +
18 +function FolderStats({ list }:{ list:DirList }) {
19 + const stats = useMemo(() =>{
20 + let files = 0, folders = 0, size = 0
21 + for (const x of list) {
22 + if (x.n.endsWith('/'))
23 + ++folders
24 + else
25 + ++files
26 + size += x.s||0
27 + }
28 + return { files, folders, size }
29 + }, [list])
30 + return h('div', { id:'folder-stats' },
31 + [
32 + prefix('', stats.files,' file(s)'),
33 + prefix('', stats.folders, ' folder(s)'),
34 + formatBytes(stats.size),
35 + ].filter(Boolean).join(', ')
36 + )
37 +}
38 +
39 +function Breadcrumbs() {
40 + const path = useLocation().pathname.slice(1,-1)
41 + let prev = ''
42 + const breadcrumbs = path ? path.split('/').map(x => [prev = prev + x + '/', x]) : []
43 + return h('div', { id: 'folder-path' },
44 + h(Breadcrumb),
45 + breadcrumbs.map(([path,label]) => h(Breadcrumb, { key: path, path, label }))
46 + )
47 +}
48 +
49 +function Breadcrumb({ path, label }:{ path?: string, label?: string }) {
50 + return h(Link, { to:path||'/' },
51 + h('button', {},
52 + label || hIcon('home')) )
53 +}
54 +
frontend/src/index.scss
+29 -2
@@ -20,12 +20,39 @@ code {
20 a {
21 text-decoration: none;
22 color: #357;
23 - border: 1px solid transparent;
24 - padding: 0 0.1em;
23 +}
24 +button {
25 + background-color: #bcd;
26 + color: #444;
27 + padding: 0.5em 1em;
28 + border: transparent;
29 + text-decoration: none;
30 + border-radius: 0.3em;
31 + vertical-align: middle;
32 + cursor: pointer;
33 +}
34 +
35 +#folder-path {
36 + margin: 0.2em;
37 + & a {
38 + float: left;
39 + height: 2em;
40 + }
41 + & button {
42 + padding: 0.4em 0.6em;
43 + border-radius: 0.7em;
44 + }
45 +}
46 +#folder-stats {
47 + font-size: 90%;
48 + padding: 0.1em 0.3em;
49 + margin: 0.2em 0.5em;
50 + float: right;
51 }
52
53 ul.dir {
54 padding:0;
55 + clear: both;
56 & li {
57 display: block;
58 list-style-type: none;
frontend/src/misc.ts new
+44
@@ -0,0 +1,44 @@
1 +import React, { createElement as h } from 'react'
2 +
3 +export function hIcon(name: string) {
4 + return h(Icon, { name })
5 +}
6 +
7 +const SYS_ICONS: Record<string,string> = {
8 + file: 'description',
9 +}
10 +export function Icon({ name }: { name:string }) {
11 + name = SYS_ICONS[name] || name
12 + return h('span',{
13 + className: 'material-icons-outlined icon',
14 + }, name)
15 +}
16 +
17 +export function Loading() {
18 + return 'loading' as React.ReactNode as React.ReactElement
19 +}
20 +
21 +export function formatBytes(n: number, post: string = 'B') {
22 + if (isNaN(Number(n)))
23 + return ''
24 + let x = ['', 'K', 'M', 'G', 'T']
25 + let prevMul = 1
26 + let mul = 1024
27 + let i = 0
28 + while (i < x.length && n > mul) {
29 + prevMul = mul
30 + mul *= 1024
31 + ++i
32 + }
33 + n /= prevMul
34 + return round(n, 1) + ' ' + (x[i]||'') + post
35 +} // formatBytes
36 +
37 +export function round(v: number, decimals: number = 0) {
38 + decimals = Math.pow(10, decimals)
39 + return Math.round(v * decimals) / decimals
40 +} // round
41 +
42 +export function prefix(pre:string, v:string|number, post:string='') {
43 + return v ? pre+v+post : ''
44 +}
\ No newline at end of file