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 +