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 }