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