better code: split files
Massimo Melina committed
Jan 3, 2022 at 10:10 UTC
d3efc39d6f241e5eab92abb1aa4907e427498a49
4 files changed
+183
-163
README.md
+1
-1
@@ -76,7 +76,7 @@ As soon as the file is read HFS will encrypt passwords in a non-reversible way.
76
# Building instructions
77
78
0. Install [Node.js](https://nodejs.org/) 16+
79
-1. Launch `npm -g i typescript`
79
+1. Install Typescript: launch `npm -g i typescript`
80
3. Launch `npm run build-all` in the root
81
82
You'll find the output in `dist` folder.
frontend/src/Breadcrumbs.ts
new
+37
@@ -0,0 +1,37 @@
1
+import { Link, useLocation } from 'react-router-dom'
2
+import { createElement as h, Fragment, useContext } from 'react'
3
+import { ListContext } from './BrowseFiles'
4
+import { confirmDialog } from './dialog'
5
+import { hIcon } from './misc'
6
+
7
+export function Breadcrumbs() {
8
+ const currentPath = useLocation().pathname.slice(1,-1)
9
+ let prev = ''
10
+ const breadcrumbs = currentPath ? currentPath.split('/').map(x => [prev = prev + x + '/', decodeURIComponent(x)]) : []
11
+ return h(Fragment, {},
12
+ h(Breadcrumb),
13
+ breadcrumbs.map(([path,label]) =>
14
+ h(Breadcrumb, {
15
+ key: path,
16
+ path,
17
+ label,
18
+ current: path === currentPath+'/',
19
+ }) )
20
+ )
21
+}
22
+
23
+function Breadcrumb({ path, label, current }:{ current: boolean, path?: string, label?: string }) {
24
+ const PAD = '\u00A0' // make small elements easier to tap. Don't use min-width 'cause it requires display-inline that breaks word-wrapping
25
+ if (label && label.length < 3)
26
+ label = PAD+label+PAD
27
+ const { reload } = useContext(ListContext)
28
+ return h(Link, {
29
+ className: 'breadcrumb',
30
+ to: path || '/',
31
+ async onClick() {
32
+ if (current && await confirmDialog('Reload?'))
33
+ reload?.()
34
+ }
35
+ }, label || hIcon('home') )
36
+}
37
+
frontend/src/Head.ts
+4
-162
@@ -1,13 +1,10 @@
1
-import { createElement as h, Fragment, useContext, useEffect, useMemo, useState } from 'react'
2
-import { Link, useLocation } from 'react-router-dom'
1
+import { createElement as h, useContext, useMemo} from 'react'
2
import { ListContext } from './BrowseFiles'
4
-import { login, logout } from './login'
3
import { formatBytes, hIcon, prefix } from './misc'
4
import { Spinner } from './components'
7
-import { state, useSnapState } from './state'
8
-import { useDebounce } from 'use-debounce'
9
-import { alertDialog, closeDialog, confirmDialog, newDialog, promptDialog } from './dialog'
10
-import { apiCall } from './api'
5
+import { useSnapState } from './state'
6
+import { MenuPanel } from './menu'
7
+import { Breadcrumbs } from './Breadcrumbs'
8
9
export function Head() {
10
return h('header', {},
@@ -18,129 +15,6 @@ export function Head() {
15
)
16
}
17
21
-function MenuPanel() {
22
- const { remoteSearch, stopSearch, stoppedSearch, listFilter } = useSnapState()
23
- const [showFilter, setShowFilter] = useState(listFilter > '')
24
- const [filter, setFilter] = useState(listFilter)
25
- ;[state.listFilter] = useDebounce(showFilter ? filter : '', 300)
26
-
27
- const [started1secAgo, setStarted1secAgo] = useState(false)
28
- useEffect(()=>{
29
- if (!stopSearch) return
30
- setStarted1secAgo(false)
31
- setTimeout(()=> setStarted1secAgo(true), 1000)
32
- }, [stopSearch])
33
-
34
- const searchButtonProps = stopSearch && started1secAgo ? {
35
- icon: 'stop',
36
- label: 'Stop list',
37
- className: 'ani-working',
38
- onClick() {
39
- stopSearch()
40
- state.stoppedSearch = true
41
- }
42
- } : state.remoteSearch ? {
43
- icon: 'search_off',
44
- label: 'Clear search',
45
- onClick() {
46
- state.remoteSearch = ''
47
- }
48
- } : {
49
- icon: 'search',
50
- label: 'Search',
51
- async onClick() {
52
- state.remoteSearch = await promptDialog('Search for...') ||''
53
- }
54
- }
55
-
56
- return h('div', { id:'menu-panel' },
57
- h('div', { id:'menu-bar' },
58
- h(LoginButton),
59
- h(MenuButton, {
60
- icon: 'filter',
61
- label: 'Filter',
62
- toggled: showFilter,
63
- onClick() {
64
- setShowFilter(!showFilter)
65
- }
66
- }),
67
- h(MenuButton, searchButtonProps),
68
- h(MenuButton, {
69
- icon: 'archive',
70
- label: 'Archive',
71
- onClick() {
72
- window.location.href = '?get=zip'
73
- }
74
- })
75
- ),
76
- remoteSearch && h('div', { id: 'searched' }, (stopSearch ? 'Searching' : 'Searched') + ': ' + remoteSearch + prefix(' (',stoppedSearch && 'interrupted',')')),
77
- showFilter && h('input',{
78
- id: 'filter',
79
- placeholder: 'Filter',
80
- value: filter,
81
- autoFocus: true,
82
- onChange(ev) {
83
- setFilter(ev.target.value)
84
- }
85
- }),
86
- )
87
-}
88
-
89
-function MenuButton({ icon, label, toggled, onClick, className='' }:{ icon:string, label:string, toggled?:boolean, className?:string, onClick?:()=>void }) {
90
- return h('button', { title:label, onClick, className:className+' '+(toggled ? 'toggled' : '') },
91
- hIcon(icon),
92
- h('label',{}, label))
93
-}
94
-
95
-function LoginButton() {
96
- const snap = useSnapState()
97
- return MenuButton(snap.username ? {
98
- icon: 'user',
99
- label: snap.username,
100
- onClick(){
101
- newDialog({ content: UserPanel })
102
- },
103
- } : {
104
- icon: 'login',
105
- label: 'Login',
106
- async onClick(){
107
- const user = await promptDialog('Username')
108
- if (!user) return
109
- const password = await promptDialog('Password', { type:'password' })
110
- if (!password) return
111
- await login(user, password)
112
- }
113
- })
114
-}
115
-
116
-function UserPanel() {
117
- const snap = useSnapState()
118
- return h('div',{ id:'user-panel' },
119
- h('div',{}, 'User: '+snap.username),
120
- h(MenuButton,{
121
- icon: 'key',
122
- label: 'Change password',
123
- async onClick(){
124
- const pwd = await promptDialog('Enter new password', { type:'password' })
125
- if (!pwd) return
126
- const check = await promptDialog('RE-enter new password', { type:'password' })
127
- if (!check) return
128
- if (check !== pwd)
129
- return alertDialog('The second password you entered did not match the first. Procedure aborted.', 'warning')
130
- await apiCall('change_pwd', { newPassword: pwd })
131
- return alertDialog('Password changed')
132
- }
133
- }),
134
- h(MenuButton,{
135
- icon: 'logout',
136
- label: 'Logout',
137
- onClick(){
138
- logout().then(closeDialog)
139
- }
140
- })
141
- )
142
-}
143
-
18
function FolderStats() {
19
const { list, loading } = useContext(ListContext)
20
const stats = useMemo(() =>{
@@ -166,35 +40,3 @@ function FolderStats() {
40
].filter(Boolean).join(', ')
41
)
42
}
169
-
170
-function Breadcrumbs() {
171
- const currentPath = useLocation().pathname.slice(1,-1)
172
- let prev = ''
173
- const breadcrumbs = currentPath ? currentPath.split('/').map(x => [prev = prev + x + '/', decodeURIComponent(x)]) : []
174
- return h(Fragment, {},
175
- h(Breadcrumb),
176
- breadcrumbs.map(([path,label]) =>
177
- h(Breadcrumb, {
178
- key: path,
179
- path,
180
- label,
181
- current: path === currentPath+'/',
182
- }) )
183
- )
184
-}
185
-
186
-function Breadcrumb({ path, label, current }:{ current: boolean, path?: string, label?: string }) {
187
- const PAD = '\u00A0' // make small elements easier to tap. Don't use min-width 'cause it requires display-inline that breaks word-wrapping
188
- if (label && label.length < 3)
189
- label = PAD+label+PAD
190
- const { reload } = useContext(ListContext)
191
- return h(Link, {
192
- className: 'breadcrumb',
193
- to: path || '/',
194
- async onClick() {
195
- if (current && await confirmDialog('Reload?'))
196
- reload?.()
197
- }
198
- }, label || hIcon('home') )
199
-}
200
-
frontend/src/menu.ts
new
+141
@@ -0,0 +1,141 @@
1
+import { state, useSnapState } from './state'
2
+import { createElement as h, useEffect, useState } from 'react'
3
+import { useDebounce } from 'use-debounce'
4
+import { alertDialog, closeDialog, newDialog, promptDialog } from './dialog'
5
+import { hIcon, prefix } from './misc'
6
+import { login, logout } from './login'
7
+import { apiCall } from './api'
8
+
9
+export function MenuPanel() {
10
+ const { remoteSearch, stopSearch, stoppedSearch, listFilter } = useSnapState()
11
+ const [showFilter, setShowFilter] = useState(listFilter > '')
12
+ const [filter, setFilter] = useState(listFilter)
13
+ ;[state.listFilter] = useDebounce(showFilter ? filter : '', 300)
14
+
15
+ const [started1secAgo, setStarted1secAgo] = useState(false)
16
+ useEffect(() => {
17
+ if (!stopSearch) return
18
+ setStarted1secAgo(false)
19
+ setTimeout(() => setStarted1secAgo(true), 1000)
20
+ }, [stopSearch])
21
+ return h('div', { id: 'menu-panel' },
22
+ h('div', { id: 'menu-bar' },
23
+ h(LoginButton),
24
+ h(MenuButton, {
25
+ icon: 'filter',
26
+ label: 'Filter',
27
+ toggled: showFilter,
28
+ onClick() {
29
+ setShowFilter(!showFilter)
30
+ }
31
+ }),
32
+ h(MenuButton, getSearchProps()),
33
+ h(MenuButton, {
34
+ icon: 'archive',
35
+ label: 'Archive',
36
+ onClick() {
37
+ window.location.href = '?get=zip'
38
+ }
39
+ })
40
+ ),
41
+ remoteSearch && h('div', { id: 'searched' }, (stopSearch ? 'Searching' : 'Searched') + ': ' + remoteSearch + prefix(' (', stoppedSearch && 'interrupted', ')')),
42
+ showFilter && h('input', {
43
+ id: 'filter',
44
+ placeholder: 'Filter',
45
+ value: filter,
46
+ autoFocus: true,
47
+ onChange(ev) {
48
+ setFilter(ev.target.value)
49
+ }
50
+ }),
51
+ )
52
+
53
+
54
+ function getSearchProps() {
55
+ return stopSearch && started1secAgo ? {
56
+ icon: 'stop',
57
+ label: 'Stop list',
58
+ className: 'ani-working',
59
+ onClick() {
60
+ stopSearch()
61
+ state.stoppedSearch = true
62
+ }
63
+ } : state.remoteSearch ? {
64
+ icon: 'search_off',
65
+ label: 'Clear search',
66
+ onClick() {
67
+ state.remoteSearch = ''
68
+ }
69
+ } : {
70
+ icon: 'search',
71
+ label: 'Search',
72
+ async onClick() {
73
+ state.remoteSearch = await promptDialog('Search for...') || ''
74
+ }
75
+ }
76
+ }
77
+}
78
+
79
+interface MenuButtonProps {
80
+ icon: string,
81
+ label: string,
82
+ toggled?: boolean,
83
+ className?: string,
84
+ onClick?: () => void
85
+}
86
+
87
+function MenuButton({ icon, label, toggled, onClick, className = '' }: MenuButtonProps) {
88
+ return h('button', { title: label, onClick, className: className + ' ' + (toggled ? 'toggled' : '') },
89
+ hIcon(icon),
90
+ h('label', {}, label))
91
+}
92
+
93
+function LoginButton() {
94
+ const snap = useSnapState()
95
+ return MenuButton(snap.username ? {
96
+ icon: 'user',
97
+ label: snap.username,
98
+ onClick() {
99
+ newDialog({ content: UserPanel })
100
+ },
101
+ } : {
102
+ icon: 'login',
103
+ label: 'Login',
104
+ async onClick() {
105
+ const user = await promptDialog('Username')
106
+ if (!user) return
107
+ const password = await promptDialog('Password', { type: 'password' })
108
+ if (!password) return
109
+ await login(user, password)
110
+ }
111
+ })
112
+}
113
+
114
+function UserPanel() {
115
+ const snap = useSnapState()
116
+ return h('div', { id: 'user-panel' },
117
+ h('div', {}, 'User: ' + snap.username),
118
+ h(MenuButton, {
119
+ icon: 'key',
120
+ label: 'Change password',
121
+ async onClick() {
122
+ const pwd = await promptDialog('Enter new password', { type: 'password' })
123
+ if (!pwd) return
124
+ const check = await promptDialog('RE-enter new password', { type: 'password' })
125
+ if (!check) return
126
+ if (check !== pwd)
127
+ return alertDialog('The second password you entered did not match the first. Procedure aborted.', 'warning')
128
+ await apiCall('change_pwd', { newPassword: pwd })
129
+ return alertDialog('Password changed')
130
+ }
131
+ }),
132
+ h(MenuButton, {
133
+ icon: 'logout',
134
+ label: 'Logout',
135
+ onClick() {
136
+ logout().then(closeDialog)
137
+ }
138
+ })
139
+ )
140
+}
141
+