@samitouri / QOSami-HFS / commits / a6dada5e

type to focus

Massimo Melina committed Sep 24, 2024 at 15:51 UTC a6dada5ec62bcc5940942558c23c3481c46b21d2
3 files changed +63 -6
README.md
+3 -2
@@ -148,12 +148,13 @@ Some actions you can take for improved security:
148
149 - Appending `#LOGIN` to address will bring up the login dialog
150 - Appending ?lang=CODE to address will force a specific language
151 -- right/ctrl/command click on toggle-all checkbox will invert each checkbox state
151 +- Right/ctrl/command click on toggle-all checkbox will invert each checkbox state
152 - Appending `?login=USER:PASSWORD` will automatically log in the browser
153 - Appending `?overwrite` on uploads, will override the dont_overwrite_uploading configuration, provided you also have delete permission
154 - Appending `?search=PATTERN` will trigger search at start
155 - Right-click on "check for updates" will let you input a URL of a version to install
156 -- shift+click on a file will show & play
156 +- Shift+click on a file will show & play
157 +- Type the name of a file/folder to focus it, and ctrl+backspace to go to parent folder
158
159 ## Contribute
160
frontend/src/BrowseFiles.ts
+46 -4
@@ -1,10 +1,10 @@
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 { Link } from 'react-router-dom'
3 +import { Link, useNavigate } from 'react-router-dom'
4 import { createElement as h, Fragment, memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState,
5 useId} from 'react'
6 -import { useMediaQuery, useWindowSize } from 'usehooks-ts'
7 -import { domOn, formatBytes, ErrorMsg, hIcon, onlyTruthy, noAriaTitle, prefix, isMac, getHFS } from './misc'
6 +import { useEventListener, useMediaQuery, useWindowSize } from 'usehooks-ts'
7 +import { domOn, formatBytes, ErrorMsg, hIcon, onlyTruthy, noAriaTitle, prefix, isMac, isCtrlKey } from './misc'
8 import { Checkbox, CustomCode, iconBtn, Spinner } from './components'
9 import { Head } from './Head'
10 import { DirEntry, state, useSnapState } from './state'
@@ -63,6 +63,8 @@ function FilesList() {
63 const theList = filteredList || list
64 const total = theList.length
65 const nPages = Math.ceil(total / pageSize)
66 + const pageEnd = offset + pageSize * (1+extraPages)
67 + const thisPage = theList.slice(offset, pageEnd)
68
69 useEffect(() => setPage(0), [theList[0]]) // reset page if the list changes
70 // reset scrolling if the page changes
@@ -89,6 +91,44 @@ function FilesList() {
91 calcScrolledPages()
92 }), [page, extraPages, nPages])
93
94 + // type to focus
95 + const [focus, setFocus] = useState('')
96 + useEffect(() => setFocus(''), [theList]) // reset
97 + const navigate = useNavigate()
98 + const timeout = useRef()
99 + useEventListener('keydown', ev => {
100 + if (isCtrlKey(ev as any) === 'Backspace' && location.pathname > '/')
101 + return navigate(location.pathname + '..')
102 + const { key } = ev
103 + if (ev.metaKey || ev.ctrlKey || ev.altKey) return
104 + setFocus(was => {
105 + const will = key === 'Backspace' ? was.slice(0, -1)
106 + : key === 'Escape' || key === 'Tab' ? ''
107 + : key.length === 1 ? was + key.toLocaleLowerCase()
108 + : was
109 + if (will !== was) {
110 + clearTimeout(timeout.current)
111 + timeout.current = setTimeout(() => setFocus(''), 5_000) as any
112 + }
113 + return will
114 + })
115 + })
116 + const focusIndex = useMemo(() => {
117 + if (!focus) return -1
118 + const match = (x: typeof theList[0]) => x.name.toLocaleLowerCase().startsWith(focus)
119 + const inThisPage = thisPage.findIndex(match) // first attempt within this page
120 + if (inThisPage >= 0)
121 + return inThisPage + offset
122 + const i = theList.findIndex(match) // search again on whole list
123 + if (i >= 0)
124 + setPage(Math.floor(i / pageSize))
125 + return i
126 + }, [focus])
127 + useEffect(() => { // wait for possible page-change before focusing
128 + if (focusIndex >= 0)
129 + (document.querySelector(`a[href="${theList[focusIndex]?.uri}"]`) as HTMLElement)?.focus()
130 + }, [focusIndex])
131 +
132 const ref = useRef<HTMLElement>()
133
134 const [goBottom, setGoBottom] = useState(false)
@@ -98,6 +138,7 @@ function FilesList() {
138 window.scrollTo(0, document.body.scrollHeight)
139 }, [goBottom])
140 const changePage = useCallback((i: number, pleaseGoBottom?: boolean) => {
141 + setFocus('')
142 if (pleaseGoBottom)
143 setGoBottom(true)
144 if (i < page || i > page + extraPages)
@@ -114,9 +155,10 @@ function FilesList() {
155 : filteredList && !filteredList.length && t('filter_none', "No match for this filter")
156
157 return h(Fragment, {},
158 + focus && h('div', { id: 'focus-typing', className: focusIndex < 0 ? 'focus-typing-mismatch' : '' }, focus, hIcon('info', { style: { cursor: 'default' }, title: `ESC: ${t`Cancel`}` })),
159 h('ul', { ref, className: 'dir' },
160 msgInstead ? h('p', {}, msgInstead)
119 - : theList.slice(offset, offset + pageSize * (1+extraPages)).map((entry, idx) =>
161 + : thisPage.map((entry, idx) =>
162 h(Entry, {
163 key: entry.key || entry.n,
164 midnight,
frontend/src/index.scss
+14
@@ -544,6 +544,20 @@ form label+input { margin-top: .2em; }
544 li:first-of-type { margin-top: .5em }
545 }
546
547 +#focus-typing {
548 + position: fixed;
549 + top: 0;
550 + right: 0;
551 + background: var(--bg);
552 + z-index: 3;
553 + padding: .1em .5em;
554 + border: 1px solid currentColor;
555 + margin: -1px;
556 +}
557 +.focus-typing-mismatch {
558 + color: var(--error);
559 +}
560 +
561 .tiles-mode {
562 max-width: none;
563 @media (min-width: 42em) {