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 }