tiles mode
Massimo Melina committed
May 21, 2023 at 22:21 UTC
1c165dd7a6c021476bea32d6c6b278d9d869e1a7
5 files changed
+150
-27
frontend/src/App.ts
+9
-6
@@ -13,17 +13,20 @@ import { Spinner } from "./components"
13
function App() {
14
useTheme()
15
const { ready } = useSnapshot(pageState) // wait for all plugins to be loaded
16
- const { messageOnly } = useSnapState()
16
+ const { messageOnly, tiles=0 } = useSnapState()
17
if (messageOnly)
18
return h('h1', { style: { textAlign: 'center'} }, messageOnly)
19
if (!ready)
20
return h(Spinner, { style: { margin: 'auto' } })
21
+ const style = { '--tile-size': tiles }
22
return h(I18Nprovider, {},
22
- h(BrowserRouter, {},
23
- h(Routes, {},
24
- h(Route, { path:'*', element: h(BrowseFiles) })
25
- ),
26
- h(Dialogs),
23
+ h('div', { className: tiles ? 'tiles-mode' : 'list-mode', style },
24
+ h(BrowserRouter, {},
25
+ h(Routes, {},
26
+ h(Route, { path:'*', element: h(BrowseFiles) })
27
+ ),
28
+ h(Dialogs),
29
+ )
30
)
31
)
32
}
frontend/src/BrowseFiles.ts
+3
-2
@@ -204,8 +204,8 @@ const Entry = memo(({ entry, midnight, separator }: EntryProps) => {
204
delete state.selected[uri]
205
},
206
}),
207
- isFolder
208
- ? h('span', { className: 'link-and-menu' }, // container to handle mouse over for both children
207
+ h('span', { className: 'link-wrapper' }, // container to handle mouse over for both children
208
+ isFolder ? h(Fragment, {},
209
h(Link, { to: base + uri }, ico, entry.n.slice(0,-1)),
210
menuOnLink && h('button', { className: 'popup-menu-button', onClick: fileMenu }, hIcon('menu'), t`Menu`)
211
)
@@ -213,6 +213,7 @@ const Entry = memo(({ entry, midnight, separator }: EntryProps) => {
213
containerDir && h(Link, { to: base + containerDir, className:'container-folder' }, ico, containerName),
214
h('a', { href: uri, onClick }, !containerDir && ico, entry.name)
215
),
216
+ ),
217
h(CustomCode, { name: 'afterEntryName', props: { entry } }),
218
h('div', { className: 'entry-panel' },
219
h(EntryDetails, { entry, midnight }),
frontend/src/index.scss
+124
-18
@@ -4,6 +4,7 @@
4
--bg: #fff;
5
--text: #555;
6
--ghost-contrast: #8882;
7
+ --ghost-contrast-alt: #eee;
8
--faint-contrast: #8884;
9
--mild-contrast: #8886;
10
--good-contrast: #000a;
@@ -15,6 +16,7 @@
16
.theme-dark {
17
--bg: #000;
18
--text: #999;
19
+ --ghost-contrast-alt: #181818;
20
--good-contrast: #fffa;
21
--button-bg: #345;
22
--button-text: #999;
@@ -23,7 +25,9 @@
25
a { color:#8ac; }
26
.dialog-closer { background: #633 }
27
.dialog-icon { color: #ccc;
26
- .icon { color: #aaa; margin-left: -1px; font-size:95%; }
28
+ .icon { color: #aaa; margin-left: -1px; font-size:95%;
29
+ margin-top: 0.4em; // useful for and affecting only img icons
30
+ }
31
}
32
.dialog-backdrop { background: #333b; }
33
.error-msg { color: #b88; background-color: #623; }
@@ -39,7 +43,7 @@ body {
43
-moz-osx-font-smoothing: grayscale;
44
}
45
body, button, select, input { font-size: 12pt; }
42
-#root {
46
+#root>div {
47
max-width: 50em;
48
margin: auto;
49
min-height: 100vh;
@@ -74,7 +78,7 @@ select { text-align: center; } // it is surely cooler on the options dialog
78
79
.icon {
80
font-size: 1.2em;
77
- height: 1.18em; min-width: 1.4em; // give similar size to <img> icons
81
+ height: 1.2em; width: 1.4em; // give similar size to <img> icons
82
}
83
.emoji-icon {
84
display: inline-block;
@@ -144,7 +148,7 @@ header {
148
position: sticky;
149
top: 0;
150
padding: .2em .1em;
147
- z-index: 1; // necessary to not be covered by checkboxes
151
+ z-index: 3; // necessary to not be covered by checkboxes
152
}
153
154
.before-sliding {
@@ -236,26 +240,26 @@ ul.dir {
240
& input[type=checkbox] {
241
margin: 0 .8em;
242
}
239
- & .link-and-menu {
243
+ & .link-wrapper {
244
&:not(:hover) .popup-menu-button { display: none; }
245
&:hover { padding: 1em; margin: -1em; } // add margin to avoid leaving the hover unintentionally
242
- }
243
- & a {
244
- &:last-of-type {
245
- word-break: break-word;
246
- padding-right: 0.3em;
247
- }
248
- & .icon {
249
- margin-right: .3em;
250
- vertical-align: text-bottom; // give similar aligning to <img> icons
251
- }
252
- &:hover {
253
- text-decoration: underline;
246
+ & a {
247
+ &:last-of-type {
248
+ word-break: break-word;
249
+ padding-right: 0.3em;
250
+ }
251
+ & .icon {
252
+ margin-right: .3em;
253
+ vertical-align: text-bottom; // give similar aligning to <img> icons
254
+ }
255
+ &:hover {
256
+ text-decoration: underline;
257
+ }
258
}
259
}
260
& .entry-panel {
261
float: right;
258
- margin-top: 0.3em;
262
+ padding-top: 0.3em;
263
display: flex;
264
align-items: center;
265
& .file-menu-button {
@@ -431,6 +435,108 @@ button label {
435
}
436
}
437
438
+#root>.tiles-mode {
439
+ max-width: none;
440
+ margin: 0 1em;
441
+ --tile-size: 5;
442
+ --name-lines: 3;
443
+ --name-height: calc(4.7em + var(--tile-size) * .4em + 1.2em * var(--name-lines));
444
+ --tile-width: calc(5em + 1em * var(--tile-size));
445
+
446
+ ul.dir {
447
+ display: grid;
448
+ grid-template-columns: repeat(auto-fill, minmax(var(--tile-width),1fr));
449
+ grid-auto-rows: calc(var(--name-height) + 1.3em);
450
+ gap: 0 20px;
451
+
452
+ li {
453
+ text-align: center;
454
+ position: relative;
455
+ display: flex;
456
+ flex-direction: column;
457
+ border-bottom: none;
458
+ overflow-x: clip;
459
+ padding: 0;
460
+
461
+ a {
462
+ max-height: var(--name-height);
463
+ display: block;
464
+ display: -webkit-box;
465
+ overflow: hidden;
466
+ -webkit-line-clamp: calc(var(--name-lines) + 1); // + icon's line
467
+ -webkit-box-orient: vertical;
468
+
469
+ &:last-of-type { padding: 0; }
470
+ &:hover {
471
+ overflow: visible;
472
+ display: block;
473
+ z-index: 1;
474
+ }
475
+ span { display: block; }
476
+ .icon.icon { font-size: calc(1.2rem + .6rem * var(--tile-size)); }
477
+ img.icon { width: auto }
478
+ .icon {
479
+ font-size: 4rem;
480
+ display: block;
481
+ margin: auto; // good for both font and img icons
482
+ }
483
+ &:hover .icon::before { text-decoration: none; }
484
+ }
485
+ .link-wrapper:hover {
486
+ padding: 0; margin: 0;
487
+ }
488
+ &:nth-of-type(odd) {
489
+ background-color: var(--bg); // to avoid checkerboard effect
490
+ }
491
+ .entry-panel {
492
+ justify-content: center;
493
+ font-size: 10pt;
494
+ }
495
+ .entry-details {
496
+ display: flex;
497
+ flex-direction: column-reverse;
498
+ font-size: 80%;
499
+ }
500
+ &.page-separator::before {
501
+ content: '';
502
+ }
503
+ input[type=checkbox] {
504
+ margin: 0;
505
+ position: absolute;
506
+ top: 0.3em;
507
+ right: 1em;
508
+ }
509
+
510
+ &:hover {
511
+ --bg: var(--ghost-contrast-alt);
512
+ background: var(--bg); // highlight whole tile
513
+ .link-wrapper, .entry-panel {
514
+ z-index: 1; background: var(--bg); // cover underlying elements of other tiles, in case it's overflowing
515
+ }
516
+ input[type=checkbox] { z-index: 2; } // over link-wrapper, but not over header
517
+ .link-wrapper a { display:inline } // avoid overlapping name and details
518
+ .entry-panel {
519
+ padding-bottom: 0.3em;
520
+ }
521
+ }
522
+ }
523
+ }
524
+ .entry-size::after {
525
+ content: none;
526
+ }
527
+ .entry-ts { display: none; }
528
+ .popup-menu-button {
529
+ position: absolute;
530
+ top: 0;
531
+ left: 0;
532
+ }
533
+ #filter-bar {
534
+ margin-bottom: 1em;
535
+ }
536
+
537
+}
538
+
539
+
540
@media (min-width: 42em) {
541
body {
542
/* Works on Firefox */
frontend/src/options.ts
+12
@@ -55,6 +55,18 @@ export function showOptions (){
55
}
56
}, t`Numeric names`),
57
58
+ h('div', {},
59
+ h('div', {}, t`Tiles mode:`, ' ', state.tiles || t`off`),
60
+ h('input', {
61
+ type: 'range',
62
+ min: 0, max: 10,
63
+ value: snap.tiles || 0,
64
+ onChange(ev: any) {
65
+ state.tiles = Number(ev.target.value)
66
+ }
67
+ }),
68
+ ),
69
+
70
h(Select, {
71
options: ['', 'light', 'dark'].map(s => ({ label: t`theme:` + ' ' + t(s || "auto"), value: s })),
72
value: snap.theme,
frontend/src/state.ts
+2
-1
@@ -30,6 +30,7 @@ export const state = proxy<{
30
can_upload?: boolean
31
can_delete?: boolean
32
accept?: string
33
+ tiles?: number
34
}>({
35
iconsReady: false,
36
username: '',
@@ -52,7 +53,7 @@ export function useSnapState() {
53
}
54
55
const SETTINGS_KEY = 'hfs_settings'
55
-const SETTINGS_TO_STORE: (keyof typeof state)[] = ['sortBy', 'sortNumerics', 'invertOrder', 'foldersFirst', 'theme']
56
+const SETTINGS_TO_STORE: (keyof typeof state)[] = ['sortBy', 'sortNumerics', 'invertOrder', 'foldersFirst', 'theme', 'tiles']
57
58
loadSettings()
59
for (const k of SETTINGS_TO_STORE)