file-show: shuffle
Massimo Melina committed
Feb 2, 2024 at 23:56 UTC
740d08a50b387901055965aa44aafcae621ce075
9 files changed
+51
-18
frontend/fontello-config.json
+6
@@ -251,6 +251,12 @@
251
"css": "copy",
252
"code": 61637,
253
"src": "fontawesome"
254
+ },
255
+ {
256
+ "uid": "37c5ab63f10d7ad0b84d0978dcd0c7a8",
257
+ "css": "shuffle",
258
+ "code": 59418,
259
+ "src": "fontawesome"
260
}
261
]
262
}
\ No newline at end of file
frontend/public/fontello.css
+3
-2
@@ -1,6 +1,6 @@
1
@font-face {
2
font-family: 'fontello';
3
- src: url('fontello.woff2?39711600') format('woff2');
3
+ src: url('fontello.woff2?39711601') format('woff2');
4
font-weight: normal;
5
font-style: normal;
6
}
@@ -65,6 +65,7 @@
65
.fa-lock:before { content: '\e817'; } /* '' */
66
.fa-reload:before { content: '\e818'; } /* '' */
67
.fa-cut:before { content: '\e819'; } /* '' */
68
+.fa-shuffle:before { content: '\e81a'; } /* '' */
69
.fa-spin6:before { content: '\e839'; } /* '' */
70
.fa-crown:before { content: '\e844'; } /* '' */
71
.fa-download:before { content: '\f02e'; } /* '' */
@@ -72,7 +73,7 @@
73
.fa-link:before { content: '\f08e'; } /* '' */
74
.fa-filter:before { content: '\f0b0'; } /* '' */
75
.fa-zoom:before { content: '\f0b2'; } /* '' */
75
-.copy:before { content: '\f0c5'; } /* '' */
76
+.fa-copy:before { content: '\f0c5'; } /* '' */
77
.fa-menu:before { content: '\f0c9'; } /* '' */
78
.fa-comment:before { content: '\f0e5'; } /* '' */
79
.fa-paste:before { content: '\f0ea'; } /* '' */
frontend/public/fontello.woff2
Binary files a/frontend/public/fontello.woff2 and b/frontend/public/fontello.woff2 differ
frontend/src/fileMenu.ts
+3
-1
@@ -19,6 +19,7 @@ interface FileMenuEntry {
19
subLabel?: ReactNode
20
href?: string
21
icon?: string
22
+ toggled?: boolean
23
onClick?: (ev:MouseEvent<Element>) => any
24
}
25
@@ -92,7 +93,8 @@ export function openFileMenu(entry: DirEntry, ev: MouseEvent, addToMenu: (FileMe
93
: e?.label && h('a', {
94
key: i,
95
href: '#',
95
- ..._.omit(e, ['label', 'icon', 'onClick']),
96
+ ..._.omit(e, ['label', 'icon', 'toggled']),
97
+ className: e.toggled ? 'toggled' : undefined,
98
async onClick(event: MouseEvent) {
99
if (!e.href)
100
event.preventDefault()
frontend/src/icons.ts
+1
@@ -41,6 +41,7 @@ const SYS_ICONS: Record<string, [string] | [string, string | false]> = { // fals
41
info: ['ⓘ', false],
42
cut: ['✄'],
43
paste: ['📋'],
44
+ shuffle: ['🔀'],
45
}
46
47
document.fonts.ready.then(async ()=> {
frontend/src/index.scss
+8
-2
@@ -35,7 +35,9 @@
35
}
36
.dialog-backdrop { background: #333b; }
37
.error-msg { color: #b88; background-color: #623; }
38
- button.toggled { color: #eee; }
38
+ button.toggled, a.toggled { color: #eee;
39
+ text-shadow: 0 0 3px #fff;
40
+ }
41
}
42
}
43
body {
@@ -128,6 +130,10 @@ button.toggled {
130
color: #fff;
131
text-shadow: 0 0 3px #fff;
132
}
133
+a.toggled {
134
+ color: #223;
135
+ text-shadow: 0 0 5px #223;
136
+}
137
button, .breadcrumb { /* consistent focus color */
138
&:focus-visible {
139
outline: 3px solid var(--focus-color);
@@ -382,7 +388,7 @@ ul.dir {
388
}
389
}
390
385
-button .label {
391
+button .icon + .label {
392
cursor: inherit;
393
margin-left: .4em;
394
}
frontend/src/show.ts
+26
-11
@@ -1,9 +1,9 @@
1
-import { DirEntry, ext2type, state, useSnapState } from './state'
1
+import { DirEntry, DirList, ext2type, state, useSnapState } from './state'
2
import { createElement as h, Fragment, useEffect, useRef, useState } from 'react'
3
import { domOn, hfsEvent, hIcon, newDialog, restartAnimation } from './misc'
4
import { useEventListener, useWindowSize } from 'usehooks-ts'
5
import { EntryDetails, useMidnight } from './BrowseFiles'
6
-import { Btn, Flex, FlexV, iconBtn, Spinner } from './components'
6
+import { Btn, FlexV, iconBtn, Spinner } from './components'
7
import { openFileMenu } from './fileMenu'
8
import { t, useI18N } from './i18n'
9
import { alertDialog } from './dialog'
@@ -24,6 +24,11 @@ export function fileShow(entry: DirEntry) {
24
const moving = useRef(0)
25
const lastGood = useRef(entry)
26
const [mode, setMode] = useState(ZoomMode.contain)
27
+ const [shuffle, setShuffle] = useState<undefined|DirList>()
28
+ useEffect(() => {
29
+ if (shuffle)
30
+ goTo(shuffle[0])
31
+ }, [shuffle])
32
useEventListener('keydown', ({ key }) => {
33
if (key === 'ArrowLeft')
34
return goPrev()
@@ -39,6 +44,8 @@ export function fileShow(entry: DirEntry) {
44
return switchZoomMode()
45
if (key === 'f')
46
return toggleFullScreen()
47
+ if (key === 's')
48
+ return toggleShuffle()
49
if (key === ' ') {
50
const sel = state.selected
51
if (sel[cur.uri])
@@ -113,6 +120,7 @@ export function fileShow(entry: DirEntry) {
120
'open','delete',
121
{ id: 'zoom', icon: 'zoom', label: t`Switch zoom mode`, onClick: switchZoomMode },
122
{ id: 'fullscreen', icon: 'fullscreen', label: t`Full screen`, onClick: toggleFullScreen },
123
+ { id: 'shuffle', icon: 'shuffle', label: t`Shuffle`, toggled: Boolean(shuffle), onClick: toggleShuffle },
124
])),
125
iconBtn('close', close),
126
),
@@ -133,11 +141,11 @@ export function fileShow(entry: DirEntry) {
141
onError: curFailed,
142
onPlay() {
143
const folder = cur.n.slice(0, -cur.name)
144
+ const covers = state.list.filter(x => folder === x.n.slice(0, -x.name) // same folder
145
+ && x.name.match(/(?:folder|cover|albumart.*)\.jpe?g$/i))
146
navigator.mediaSession.metadata = new MediaMetadata({
147
title: cur.name,
138
- artwork: state.list.filter(x => folder === x.n.slice(0, -x.name) // same folder
139
- && x.name.match(/(?:folder|cover|albumart.*)\.jpe?g$/i))
140
- .map(x => ({ src: x.n }))
148
+ artwork: covers.map(x => ({ src: x.n }))
149
})
150
}
151
})
@@ -162,19 +170,22 @@ export function fileShow(entry: DirEntry) {
170
if (dir)
171
moving.current = dir
172
let e = cur
165
- setFailed(false)
166
- setLoading(true)
173
while (1) {
168
- e = e.getSibling(moving.current)
174
+ e = e.getSibling(moving.current, shuffle)
175
if (!e) { // reached last
176
if (dir! > 0) setAutoPlaying(false)
171
- setLoading(cur !== lastGood.current)
172
- setCur(lastGood.current) // revert to last known supported file
177
+ goTo(lastGood.current) // revert to last known supported file
178
return restartAnimation(document.body, '.2s blink')
179
}
180
if (!e.isFolder && getShowType(e)) break // give it a chance
181
}
177
- setCur(e)
182
+ goTo(e)
183
+ }
184
+
185
+ function goTo(to: typeof cur) {
186
+ setFailed(false)
187
+ setLoading(to !== lastGood.current)
188
+ setCur(to)
189
}
190
191
function toggleFullScreen() {
@@ -190,6 +201,10 @@ export function fileShow(entry: DirEntry) {
201
setMode(x => x ? x - 1 : ZoomMode.contain)
202
}
203
204
+ function toggleShuffle() {
205
+ setShuffle(x => x ? undefined : _.shuffle(state.list))
206
+ }
207
+
208
function scrollY(dy: number) {
209
containerRef.current?.scrollBy(0, dy * .5 * containerRef.current?.clientHeight)
210
}
src/langs/hfs-lang-en.json
+2
-1
@@ -158,6 +158,7 @@
158
"autoplay_seconds": "Seconds to wait on images",
159
"Select all": "Select all",
160
"go_first": "Go to first item",
161
- "go_last": "Go to last item"
161
+ "go_last": "Go to last item",
162
+ "Shuffle": "Shuffle"
163
}
164
}
src/langs/hfs-lang-it.json
+2
-1
@@ -150,6 +150,7 @@
150
"autoplay_seconds": "Secondi di attesa mostrando immagini",
151
"Select all": "Seleziona tutto",
152
"go_first": "Vai all'inizio",
153
- "go_last": "Vai alla fine"
153
+ "go_last": "Vai alla fine",
154
+ "Shuffle": "Riproduzione casuale"
155
}
156
}