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)