file-show: auto-play
Massimo Melina committed
Jan 2, 2024 at 16:14 UTC
d543524271ad4549d22e40d080b7b59aeb6ea9a7
7 files changed
+93
-31
admin/src/OptionsPage.ts
+7
-6
@@ -191,12 +191,13 @@ export default function OptionsPage() {
191
options: { "by clicking on file name": true, "by dedicated button": false }
192
},
193
{ k: 'title', md: 8, helperText: "You can see this in the tab of your browser" },
194
- { k: 'tile_size', comp: NumberField, xs: 6, sm: 4, min: 0, max: MAX_TILE_SIZE, label: "Default tiles size", helperText: "Zero = list mode" },
195
- { k: 'theme', comp: SelectField, xs: 6, sm: 4, options: THEME_OPTIONS },
196
- { k: 'sort_by', comp: SelectField, xs: 6, sm: 4, options: SORT_BY_OPTIONS },
197
- { k: 'invert_order', comp: BoolField, xs: 6, sm: 4, },
198
- { k: 'folders_first', comp: BoolField, xs: 6, sm: 4, },
199
- { k: 'sort_numerics', comp: BoolField, xs: 6, sm: 4, label: "Sort numeric names" },
194
+ { k: 'auto_play_seconds', comp: NumberField, xs: 6, sm: 3, min: 1, max: 10000, label: "Auto-play seconds delay" },
195
+ { k: 'tile_size', comp: NumberField, xs: 6, sm: 3, min: 0, max: MAX_TILE_SIZE, label: "Default tiles size", helperText: "Zero = list mode" },
196
+ { k: 'theme', comp: SelectField, xs: 6, sm: 3, options: THEME_OPTIONS },
197
+ { k: 'sort_by', comp: SelectField, xs: 6, sm: 3, options: SORT_BY_OPTIONS },
198
+ { k: 'invert_order', comp: BoolField, xs: 6, sm: 4, md: 3, },
199
+ { k: 'folders_first', comp: BoolField, xs: 6, sm: 4, md: 3, },
200
+ { k: 'sort_numerics', comp: BoolField, xs: 6, sm: 4, md: 3, label: "Sort numeric names" },
201
]
202
})
203
frontend/src/index.scss
+3
@@ -107,6 +107,9 @@ button {
107
background-color: var(--button-bg);
108
color: var(--button-text);
109
padding: .5em;
110
+ &:not(.before-sliding) { min-width: 2em; }
111
+ &.small { padding: 0 0.4em; height: 30px; }
112
+ .icon { position: relative; top: .05em; }
113
border: transparent;
114
text-decoration: none;
115
border-radius: 0.3em;
frontend/src/menu.ts
+3
-4
@@ -147,7 +147,7 @@ export function MenuPanel() {
147
}
148
149
interface MenuButtonProps extends ComponentPropsWithoutRef<"button"> {
150
- icon: string,
150
+ icon?: string,
151
label: string,
152
tooltip?: string,
153
toggled?: boolean,
@@ -167,10 +167,9 @@ export function Btn({ icon, label, tooltip, toggled, onClick, onClickAnimation,
167
setWorking(true)
168
Promise.resolve(onClick()).finally(() => setWorking(false))
169
},
170
- className: [rest.className, toggled && 'toggled', working && 'ani-working'].filter(Boolean).join(' '),
171
- ...toggled !== undefined && { 'aria-pressed': toggled },
170
...rest,
173
- }, hIcon(icon), h('span', { className: 'label' }, label) ) // don't use <label> as VoiceOver will get redundant
171
+ className: [rest.className, toggled && 'toggled', working && 'ani-working'].filter(Boolean).join(' '),
172
+ }, icon && hIcon(icon), h('span', { className: 'label' }, label) ) // don't use <label> as VoiceOver will get redundant
173
}
174
175
export function MenuLink({ href, target, confirm, confirmOptions, ...rest }: MenuButtonProps & { href: string, target?: string, confirm?: string, confirmOptions?: ConfirmOptions }) {
frontend/src/show.ts
+71
-19
@@ -1,6 +1,6 @@
1
-import { DirEntry, ext2type, state } from './state'
1
+import { DirEntry, ext2type, state, useSnapState } from './state'
2
import { createElement as h, Fragment, useEffect, useRef, useState } from 'react'
3
-import { hfsEvent, hIcon, newDialog, restartAnimation } from './misc'
3
+import { domOn, hfsEvent, hIcon, newDialog, restartAnimation } from './misc'
4
import { useEventListener, useWindowSize } from 'usehooks-ts'
5
import { EntryDetails, useMidnight } from './BrowseFiles'
6
import { Flex, FlexV, iconBtn, Spinner } from './components'
@@ -8,6 +8,7 @@ import { openFileMenu } from './fileMenu'
8
import { t, useI18N } from './i18n'
9
import { alertDialog } from './dialog'
10
import _ from 'lodash'
11
+import { Btn } from './menu'
12
13
enum ZoomMode {
14
fullWidth,
@@ -58,25 +59,53 @@ export function fileShow(entry: DirEntry) {
59
const containerRef = useRef<HTMLDivElement>()
60
const mainRef = useRef<HTMLDivElement>()
61
useEffect(() => { scrollY(-1E9) }, [cur])
62
+
63
+ const { auto_play_seconds } = useSnapState()
64
+ const [autoPlaying, setAutoPlaying] = useState(false)
65
+ const showElement = containerRef.current?.querySelector('*')
66
+ useEffect(() => {
67
+ if (!autoPlaying || !showElement) return
68
+ if (showElement instanceof HTMLMediaElement) {
69
+ showElement.play().catch(curFailed)
70
+ return domOn('ended', () => go(+1), { target: showElement as any })
71
+ }
72
+ // we are supposedly showing an image
73
+ const h = setTimeout(() => go(+1), state.auto_play_seconds * 1000)
74
+ return () => clearTimeout(h)
75
+ }, [showElement, autoPlaying, cur])
76
+
77
const {t} = useI18N()
78
return h(FlexV, {
63
- gap: 0,
64
- alignItems: 'stretch',
65
- className: ZoomMode[mode],
66
- props: {
67
- role: 'dialog',
68
- onMouseMove() {
69
- setShowNav(true)
70
- clearTimeout(timerRef.current)
71
- timerRef.current = +setTimeout(() => setShowNav(false), 1_000)
72
- }
79
+ gap: 0,
80
+ alignItems: 'stretch',
81
+ className: ZoomMode[mode],
82
+ props: {
83
+ role: 'dialog',
84
+ onMouseMove() {
85
+ setShowNav(true)
86
+ clearTimeout(timerRef.current)
87
+ timerRef.current = +setTimeout(() => setShowNav(false), 1_000)
88
}
74
- },
89
+ }
90
+ },
91
h('div', { className: 'bar' },
92
h('div', { className: 'filename' }, cur.n),
93
h(EntryDetails, { entry: cur, midnight: useMidnight() }),
94
h(Flex, {},
95
useWindowSize().width > 1280 && iconBtn('?', showHelp),
96
+ h('div', {}, // fuse buttons
97
+ h(Btn, {
98
+ className: 'small',
99
+ label: t`Auto-play`,
100
+ toggled: autoPlaying,
101
+ onClick: () => setAutoPlaying(x => !x),
102
+ }),
103
+ autoPlaying && h(Btn, {
104
+ className: 'small',
105
+ label: String(auto_play_seconds),
106
+ onClick: configAutoPlay,
107
+ }),
108
+ ),
109
iconBtn('menu', ev => openFileMenu(cur, ev, [
110
'open','delete',
111
{ id: 'zoom', icon: 'zoom', label: t`Switch zoom mode`, onClick: switchZoomMode },
@@ -99,12 +128,7 @@ export function fileShow(entry: DirEntry) {
128
lastGood.current = cur
129
setLoading(false)
130
},
102
- onError: () => {
103
- if (cur !== lastGood.current)
104
- return go()
105
- setLoading(false)
106
- setFailed(cur.n)
107
- }
131
+ onError: curFailed
132
})
133
),
134
hIcon('❮', { className: navClass, style: { left: 0 }, onClick: () => go(-1) }),
@@ -112,6 +136,13 @@ export function fileShow(entry: DirEntry) {
136
),
137
)
138
139
+ function curFailed() {
140
+ if (cur !== lastGood.current)
141
+ return go()
142
+ setLoading(false)
143
+ setFailed(cur.n)
144
+ }
145
+
146
function go(dir?: number) {
147
if (dir)
148
moving.current = dir
@@ -121,6 +152,7 @@ export function fileShow(entry: DirEntry) {
152
while (1) {
153
e = e.getSibling(moving.current)
154
if (!e) { // reached last
155
+ setAutoPlaying(false)
156
setLoading(cur !== lastGood.current)
157
setCur(lastGood.current) // revert to last known supported file
158
return restartAnimation(document.body, '.2s blink')
@@ -146,6 +178,26 @@ export function fileShow(entry: DirEntry) {
178
function scrollY(dy: number) {
179
containerRef.current?.scrollBy(0, dy * .5 * containerRef.current?.clientHeight)
180
}
181
+
182
+ function configAutoPlay() {
183
+ newDialog({
184
+ title: t`Auto-play`,
185
+ Content() {
186
+ const { auto_play_seconds } = useSnapState()
187
+ return h(FlexV, {},
188
+ t('autoplay_seconds', "Seconds to wait on images"),
189
+ h('input', {
190
+ type: 'number',
191
+ min: 1,
192
+ max: 10000,
193
+ value: auto_play_seconds,
194
+ style: { width: '4em' },
195
+ onChange: ev => state.auto_play_seconds = Number(ev.target.value)
196
+ })
197
+ )
198
+ }
199
+ })
200
+ }
201
}
202
})
203
}
src/cross.ts
+1
@@ -19,6 +19,7 @@ export const FRONTEND_OPTIONS = {
19
folders_first: true,
20
sort_numerics: false,
21
theme: '',
22
+ auto_play_seconds: 5,
23
}
24
export const SORT_BY_OPTIONS = ['name', 'extension', 'size', 'time']
25
export const THEME_OPTIONS = { auto: '', light: 'light', dark: 'dark' }
src/langs/hfs-lang-en.json
+4
-1
@@ -148,6 +148,9 @@
148
"Cancel clipboard": "Cancel clipboard",
149
"to_clipboard_source": "To folder",
150
"Paste": "Paste",
151
- "clipboard_list": "Items in clipboard:"
151
+ "clipboard_list": "Items in clipboard:",
152
+
153
+ "Auto-play": "Auto-play",
154
+ "autoplay_seconds": "Seconds to wait on images"
155
}
156
}
src/langs/hfs-lang-it.json
+4
-1
@@ -140,6 +140,9 @@
140
"Cancel clipboard": "Annulla appunti",
141
"to_clipboard_source": "Torna all'origine'",
142
"Paste": "Incolla",
143
- "clipboard_list": "Elementi negli appunti"
143
+ "clipboard_list": "Elementi negli appunti",
144
+
145
+ "Auto-play": "Auto-play",
146
+ "autoplay_seconds": "Secondi di attesa mostrando immagini"
147
}
148
}