@samitouri / QOSami-HFS / commits / d5435242

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 }