?autoplay
Massimo Melina committed
Oct 27, 2024 at 00:55 UTC
1e4f8dade315e696bfe61cdc36fde40f82410a4f
3 files changed
+72
-12
README.md
+1
@@ -127,6 +127,7 @@ If your language is missing, please consider [translating yourself](https://gith
127
- Appending `?login=USER:PASSWORD` will automatically log in the browser
128
- Appending `?overwrite` on uploads, will override the dont_overwrite_uploading configuration, provided you also have delete permission
129
- Appending `?search=PATTERN` will trigger search at start
130
+- Appending `?autoplay=shuffle` will trigger show & play; `?autoplay` will not shuffle, but also will not start until the list is complete
131
- Right-click on "check for updates" will let you input a URL of a version to install
132
- Shift+click on a file will show & play
133
- Type the name of a file/folder to focus it, and ctrl+backspace to go to parent folder
frontend/src/show.ts
+44
-10
@@ -20,10 +20,11 @@ enum ZoomMode {
20
contain, // leave this as last
21
}
22
23
-export function fileShow(entry: DirEntry, { startPlaying=false } = {}) {
23
+export function fileShow(entry: DirEntry, { startPlaying=false, startShuffle=false } = {}) {
24
let escOnce = false
25
let onClose: any
26
let firstUri: string
27
+ let playMsgOnce = true
28
const { close } = newDialog({
29
noFrame: true,
30
className: 'file-show',
@@ -42,7 +43,8 @@ export function fileShow(entry: DirEntry, { startPlaying=false } = {}) {
43
const moving = useRef(0)
44
const lastGood = useRef(entry)
45
const [mode, setMode] = useState(ZoomMode.contain)
45
- const [shuffle, setShuffle] = useState<undefined|DirList>()
46
+ const [shuffle, setShuffle] = useState<undefined | DirList>()
47
+ useEffect(() => toggleShuffle(startShuffle), [])
48
// shuffle the rest of the list as we continue getting entries, leaving intact the part we've already played/being through
49
const shuffleIdx = useMemo(() => shuffle?.findIndex(x => x.n === cur.n), [cur])
50
useEffect(() => subscribeKey(state, 'list', list => {
@@ -102,8 +104,11 @@ export function fileShow(entry: DirEntry, { startPlaying=false } = {}) {
104
105
const { auto_play_seconds } = useSnapState()
106
const [autoPlaying, setAutoPlaying] = useState(startPlaying)
107
+ function getShowElement() {
108
+ return containerRef.current?.querySelector('.showing') // like this, we don't require component to forward ref (easier for plugins)
109
+ }
110
useEffect(() => {
106
- const showElement = containerRef.current?.querySelector('.showing') // like this, we don't require component to forward ref (easier for plugins)
111
+ const showElement = getShowElement()
112
if (!autoPlaying || !showElement) return
113
if (showElement instanceof HTMLMediaElement) {
114
showElement.play().catch(curFailed)
@@ -120,6 +125,12 @@ export function fileShow(entry: DirEntry, { startPlaying=false } = {}) {
125
const {t} = useI18N()
126
const autoPlaySecondsLabel = t('autoplay_seconds', "Seconds to wait on images")
127
const folder = dirname(cur.n)
128
+ const failOnce = useRef<typeof cur>()
129
+ useEffect(() => {
130
+ if (component || failOnce.current === cur) return
131
+ curFailed()
132
+ failOnce.current = cur
133
+ }, [cur, component])
134
return h(FlexV, {
135
gap: 0,
136
alignItems: 'stretch',
@@ -157,7 +168,7 @@ export function fileShow(entry: DirEntry, { startPlaying=false } = {}) {
168
'open','delete',
169
{ id: 'zoom', icon: 'zoom', label: t`Switch zoom mode`, onClick: switchZoomMode },
170
{ id: 'fullscreen', icon: 'fullscreen', label: t`Full screen`, onClick: toggleFullScreen },
160
- { id: 'shuffle', icon: 'shuffle', label: t`Shuffle`, toggled: Boolean(shuffle), onClick: toggleShuffle },
171
+ { id: 'shuffle', icon: 'shuffle', label: t`Shuffle`, toggled: Boolean(shuffle), onClick: () => toggleShuffle() },
172
{ id: 'repeat', icon: 'repeat', label: t`Repeat`, toggled: repeat, onClick: toggleRepeat },
173
])),
174
iconBtn('close', close),
@@ -170,8 +181,11 @@ export function fileShow(entry: DirEntry, { startPlaying=false } = {}) {
181
h('div', {}, cur.name),
182
t`Loading failed`
183
) : h('div', { className: 'showing-container', ref: containerRef },
173
- h('div', { className: 'cover ' + (cover ? '' : 'none'), style: { backgroundImage: cover && `url("${cover}")` } }),
174
- h(component || Fragment, {
184
+ h('div', {
185
+ className: 'cover ' + (cover ? '' : 'none'),
186
+ style: { backgroundImage: cover && `url("${cover}")` }
187
+ }),
188
+ component && h(component, {
189
src: cur.uri,
190
className: 'showing',
191
onLoad() {
@@ -222,7 +236,27 @@ export function fileShow(entry: DirEntry, { startPlaying=false } = {}) {
236
237
function goNext() { go(+1) }
238
225
- function curFailed() {
239
+ function curFailed(err?: Error) {
240
+ console.debug(err)
241
+ if (err?.name === 'NotAllowedError') { // browser won't allow automatic audio playing without user interaction
242
+ if (!playMsgOnce) return
243
+ playMsgOnce = false
244
+ const el = getShowElement()
245
+ if (!(el instanceof HTMLMediaElement)) return
246
+ const dlg = newDialog({ // so we offer
247
+ onClose: () => playMsgOnce = true,
248
+ Content: () => h(Btn, {
249
+ autoFocus: true,
250
+ icon: 'play',
251
+ label: "Click here to play",
252
+ onClick: () => {
253
+ el.play().catch(curFailed)
254
+ dlg.close()
255
+ }
256
+ })
257
+ })
258
+ return
259
+ }
260
const mediaError = (document.querySelector('.showing-container .showing') as any)?.error?.code // only presenti in video/audio elements
261
if (mediaError === 2) return // happens when chrome fails to fetch cover for videos. We don't skip the file for this reason. Tested on chrome129/windows
262
if (cur !== lastGood.current)
@@ -231,7 +265,7 @@ export function fileShow(entry: DirEntry, { startPlaying=false } = {}) {
265
setFailed(cur.n)
266
}
267
234
- function go(dir?: number, from=cur) {
268
+ function go(dir=1, from=cur) {
269
if (dir)
270
moving.current = dir
271
let e = from
@@ -277,8 +311,8 @@ export function fileShow(entry: DirEntry, { startPlaying=false } = {}) {
311
setMode(x => x ? x - 1 : ZoomMode.contain)
312
}
313
280
- function toggleShuffle() {
281
- setShuffle(x => x ? undefined : _.shuffle(state.list))
314
+ function toggleShuffle(force?: boolean) {
315
+ setShuffle(x => (force ?? !x) ? _.shuffle(state.list) : undefined)
316
}
317
318
function toggleRepeat() {
frontend/src/useFetchList.ts
+27
-2
@@ -11,6 +11,7 @@ import { hfsEvent, HTTP_MESSAGES, HTTP_METHOD_NOT_ALLOWED, HTTP_UNAUTHORIZED, LI
11
import { t } from './i18n'
12
import { useLocation, useNavigate } from 'react-router-dom'
13
import { closeLoginDialog } from './login'
14
+import { fileShow, getShowComponent } from './show'
15
16
export function usePath() {
17
return useLocation().pathname
@@ -20,6 +21,8 @@ export function usePath() {
21
setTimeout(() => // wait, urlParams is defined at top level
22
state.remoteSearch = urlParams.search || '')
23
24
+let autoPlayOnce: string | undefined = urlParams.autoplay // this will be consumed, so to only act once
25
+
26
export default function useFetchList() {
27
const snap = useSnapState()
28
const uri = usePath() // this api can still work removing the initial slash, but then we'll have a mixed situation that will require plugins an extra effort
@@ -55,17 +58,32 @@ export default function useFetchList() {
58
state.loading = true
59
state.error = undefined
60
state.props = undefined
61
+ // while 'play' needs to wait for the whole list to be available (and sorted) to proceed in order, 'playShuffle' can start right away, and it's important it does because a ?search may take long
62
+ let play = false
63
+ let playShuffle = false
64
// buffering entries is necessary against burst of events that will hang the browser
65
const buffer: DirList = []
66
const flush = () => {
67
const chunk = buffer.splice(0, Infinity)
62
- if (chunk.length)
63
- state.list = sort([...state.list, ...chunk])
68
+ if (!chunk.length) return
69
+ state.list = sort([...state.list, ...chunk])
70
+ if (playShuffle) // find first proper file, and play it
71
+ for (const x of chunk)
72
+ if (getShowComponent(x)) {
73
+ fileShow(x, { startPlaying: true, startShuffle: true })
74
+ playShuffle = false
75
+ break
76
+ }
77
}
78
const timer = setInterval(flush, 1000)
79
const src = apiEvents('get_file_list', params, (type, data) => {
80
if (!isMounted()) return
81
switch (type) {
82
+ case 'connected':
83
+ if (autoPlayOnce === '') play = true
84
+ if (autoPlayOnce === 'shuffle') playShuffle = true
85
+ autoPlayOnce = undefined
86
+ return
87
case 'error':
88
state.stopSearch?.()
89
state.error = t`connection error`
@@ -75,6 +93,13 @@ export default function useFetchList() {
93
flush()
94
state.stopSearch?.()
95
state.loading = false
96
+ if (play)
97
+ for (const x of state.list)
98
+ if (getShowComponent(x)) {
99
+ fileShow(x, { startPlaying: true })
100
+ play = false
101
+ break
102
+ }
103
return
104
case 'msg':
105
const showLogin = location.hash === '#LOGIN'