folder-size: nicer
Massimo Melina committed
Jan 15, 2025 at 23:32 UTC
02fc57f2b14f0627052a3216c05615e440c6f8a7
6 files changed
+36
-15
frontend/src/BrowseFiles.ts
+4
-4
@@ -5,9 +5,9 @@ import { createElement as h, Fragment, memo, MouseEvent, useCallback, useEffect,
5
useId} from 'react'
6
import { useEventListener, useMediaQuery, useWindowSize } from 'usehooks-ts'
7
import {
8
- domOn, formatBytes, ErrorMsg, hIcon, onlyTruthy, noAriaTitle, prefix, isMac, isCtrlKey, hfsEvent, formatTimestamp
8
+ domOn, ErrorMsg, hIcon, onlyTruthy, prefix, isMac, isCtrlKey, hfsEvent, formatTimestamp
9
} from './misc'
10
-import { Checkbox, CustomCode, iconBtn, Spinner } from './components'
10
+import { Checkbox, CustomCode, Bytes, iconBtn, Spinner } from './components'
11
import { Head } from './Head'
12
import { DirEntry, state, useSnapState } from './state'
13
import { alertDialog } from './dialog'
@@ -350,5 +350,5 @@ export const EntryDetails = memo(({ entry, midnight }: { entry: DirEntry, midnig
350
})
351
352
const EntrySize = memo(({ s }: { s: DirEntry['s'] }) =>
353
- s === undefined ? null
354
- : h('span', { className: 'entry-size', ...noAriaTitle(s.toLocaleString()) }, formatBytes(s)))
353
+ s === undefined ? null : h(Bytes, { className: 'entry-size', bytes: s }) )
354
+
frontend/src/components.ts
+11
-4
@@ -1,10 +1,12 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { Callback, getHFS, hfsEvent, hIcon, Html, isPrimitive, onlyTruthy, prefix } from './misc'
3
+import {
4
+ Callback, getHFS, hfsEvent, hIcon, Html, isPrimitive, onlyTruthy, prefix, noAriaTitle, formatBytes
5
+} from './misc'
6
import {
7
ButtonHTMLAttributes, ChangeEvent, createElement as h, CSSProperties, forwardRef, Fragment,
8
HTMLAttributes, InputHTMLAttributes, isValidElement, MouseEventHandler, ReactNode, SelectHTMLAttributes,
7
- useMemo, useState, ComponentPropsWithoutRef, LabelHTMLAttributes, useRef
9
+ useMemo, useState, ComponentPropsWithoutRef, LabelHTMLAttributes, useRef, ReactElement
10
} from 'react'
11
import _ from 'lodash'
12
import i18n from './i18n'
@@ -111,7 +113,7 @@ export function iconBtn(icon: string, onClick: MouseEventHandler, { title, ...pr
113
}
114
115
export interface BtnProps extends ComponentPropsWithoutRef<"button"> {
114
- icon?: string,
116
+ icon?: string | ReactElement,
117
label: string,
118
tooltip?: string,
119
toggled?: boolean,
@@ -147,5 +149,10 @@ export function Btn({ icon, label, tooltip, toggled, onClick, onClickAnimation,
149
...rest,
150
...asText ? { role: 'button', style: { cursor: 'pointer', ...rest.style } } : undefined,
151
className: [rest.className, toggled && 'toggled', working && 'ani-working', success && 'success'].filter(Boolean).join(' '),
150
- }, icon && hIcon(icon), h('span', { className: 'label' }, label) ) // don't use <label> as VoiceOver will get redundant
152
+ }, icon && (isValidElement(icon) ? icon : hIcon(icon)),
153
+ h('span', { className: 'label' }, label) ) // don't use <label> as VoiceOver will get redundant
154
}
155
+
156
+export function Bytes({ bytes, ...props }: { bytes: number } & HTMLAttributes<HTMLSpanElement>) {
157
+ return h('span', { ...noAriaTitle(bytes.toLocaleString()), ...props }, formatBytes(bytes))
158
+}
\ No newline at end of file
frontend/src/fileMenu.ts
+12
-5
@@ -2,7 +2,8 @@ import {
2
dontBotherWithKeys, formatBytes, getHFS, hfsEvent, hIcon, newDialog, prefix, with_, working,
3
pathEncode, closeDialog, anyDialogOpen, Falsy, operationSuccessful
4
} from './misc'
5
-import { createElement as h, Fragment, isValidElement, MouseEvent, ReactNode } from 'react'
5
+import { createElement as h, Fragment, isValidElement, MouseEvent, ReactNode, useState } from 'react'
6
+import { Btn, Bytes, Spinner } from './components'
7
import _ from 'lodash'
8
import { getEntryIcon, MISSING_PERM } from './BrowseFiles'
9
import { DirEntry, state } from './state'
@@ -67,7 +68,6 @@ export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: (Falsy
68
}),
69
state.props?.can_delete && { id: 'rename', label: t`Rename`, icon: 'edit', onClick: () => rename(entry) },
70
state.props?.can_delete && { id: 'cut', label: t`Cut`, icon: 'cut', onClick: () => close(cut([entry])) },
70
- isFolder && { id: 'folderSize', label: t`Folder size`, icon: 'total', onClick: () => folderSize() },
71
isFolder && !entry.web && !entry.cantOpen && { id: 'list', label: t`Get list`, href: uri + '?get=list&folders=*', icon: 'list' },
72
].filter(Boolean)
73
const folder = entry.n.slice(0, -entry.name.length - (entry.isFolder ? 2 : 1))
@@ -84,6 +84,7 @@ export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: (Falsy
84
onClick: () => closeDialog(null, true)
85
}, folder.replaceAll('/', ' / '))
86
},
87
+ isFolder && { id: 'folderSize', label: t`Size`, value: h(FolderSize) },
88
].filter(Boolean)
89
const res = hfsEvent('fileMenu', { entry, menu, props })
90
menu.push(...res.flat()) // flat because each plugin may return an array of entries
@@ -143,9 +144,15 @@ export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: (Falsy
144
}
145
})
146
146
- async function folderSize() {
147
- const { bytes } = await apiCall('get_folder_size', { uri: entry.uri }, { modal: working, timeout: false })
148
- await alertDialog(formatBytes(bytes), 'info', t`Folder size`)
147
+ function FolderSize() {
148
+ const [size, setSize] = useState<any>(null)
149
+ useApi(size !== null && 'get_folder_size', { uri: entry.uri }, {
150
+ timeout: false,
151
+ onResponse: (res, data) => setSize(data.bytes)
152
+ })
153
+ return size === null ? h(Btn, { asText: true, label: t`Calculate`, onClick() { setSize(true) } })
154
+ : size === true ? h(Btn, { asText: true, label: t`Cancel`, icon: h(Spinner), onClick() { setSize(null) } })
155
+ : h(Bytes, { bytes: size })
156
}
157
}
158
frontend/src/index.scss
+3
@@ -108,6 +108,9 @@ img.file-icon { height: 1em; }
108
vertical-align: text-bottom;
109
}
110
111
+[role=button] .icon {
112
+ margin-right: 0.5em;
113
+}
114
.icon.mirror:before { transform: scaleX(-1); }
115
a {
116
text-decoration: none;
src/langs/hfs-lang-en.json
+3
-1
@@ -177,6 +177,8 @@
177
"allow_session_ip_change": "Allow IP change during this session",
178
179
"focus_hint": "By typing on your keyboard, you search and focus elements of the list.",
180
- "copy_links": "Copy links"
180
+ "copy_links": "Copy links",
181
+
182
+ "Calculate": "Calculate"
183
}
184
}
src/langs/hfs-lang-it.json
+3
-1
@@ -169,6 +169,8 @@
169
"allow_session_ip_change": "Permetti cambio IP in questa sessione",
170
171
"focus_hint": "Scrivendo sulla tastiera cerchi tra gli elementi della lista.",
172
- "copy_links": "Copia i link"
172
+ "copy_links": "Copia i link",
173
+
174
+ "Calculate": "Calcola"
175
}
176
}