Reworking file preview buttons to be consistent across screens.

This commit is contained in:
Roland 2026-04-15 13:18:45 -06:00
parent 29133b1e56
commit 575a1b2a80
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
5 changed files with 89 additions and 56 deletions

View file

@ -122,10 +122,42 @@ input:checked + .slider:before {
color: rgba(0, 0, 0, 0.85) !important;
}
div.full-height {
/* File preview Modal.Actions toolbar.
Single layout that works at every viewport — buttons keep their order at any width.
On narrow screens, the tags selector wraps to its own line while the button groups stay together. */
.preview-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5em;
}
.preview-toolbar-group {
display: flex;
align-items: center;
gap: 0.25em;
flex: 0 0 auto;
}
.preview-toolbar-tags {
flex: 1 1 200px;
min-width: 0;
}
/* Caps preview content to fit inside the file-preview Modal.
300px reserves room for pathContent <pre>, Modal.Header, Modal.Actions row,
inner content (e.g. STL dimensions banner), and modal padding.
Use 100dvh so mobile browser chrome (URL bar) is accounted for. */
.preview-fit {
width: 100%;
height: calc(100dvh - 300px);
max-height: calc(100dvh - 300px);
min-height: 240px;
overflow: hidden;
padding-top: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
td.file-path {

View file

@ -405,6 +405,7 @@ export function AddTagsButton({
limit = null,
disabled = false,
filterByOverlap = false,
color = 'violet',
}) {
// A button which displays a modal in which the user can add or remove tags.
@ -556,7 +557,7 @@ export function AddTagsButton({
return <>
<Button
icon={active ? 'tags' : 'tag'}
color={active ? 'violet' : undefined}
color={color}
onClick={handleOpen}
type="button"
disabled={disabled}

View file

@ -109,7 +109,7 @@ export function CbzViewer({path}) {
src={pages[currentPage]}
alt={`Page ${currentPage + 1}`}
style={{
maxHeight: fullscreen ? '100vh' : '75vh',
maxHeight: fullscreen ? '100dvh' : 'calc(100dvh - 300px)',
maxWidth: '100%',
objectFit: 'contain',
}}

View file

@ -1,8 +1,7 @@
import React from "react";
import _ from "lodash";
import {Grid, Header as SHeader, Image,} from "semantic-ui-react";
import {Header as SHeader, Icon, Image,} from "semantic-ui-react";
import {TagsSelector} from "../Tags";
import {Media} from "../contexts/contexts";
import {encodeMediaPath, isSupportedArchive} from "./Common";
import {getFile, tagFileGroup, untagFileGroup} from "../api";
import {ArchivePreviewContent} from "./ArchivePreview";
@ -51,10 +50,10 @@ function getEpubViewerURL(previewFile) {
function getIframePreviewModal(previewFile, url) {
url = url || getMediaPathURL(previewFile);
return <Modal.Content>
<div className='full-height'>
<div className='preview-fit'>
<iframe title='textModal' src={url}
style={{
height: '100%', width: '100%', border: 'none', position: 'absolute', top: 0,
height: '100%', width: '100%', border: 'none',
// Use white to avoid iframe displaying with dark-theme.
backgroundColor: '#ffffff',
}}/>
@ -72,9 +71,11 @@ function getImagePreviewModal(previewFile) {
{name}
</Modal.Header>
<Modal.Content>
<a href={url}>
<Image src={url}/>
</a>
<div className='preview-fit'>
<a href={url}>
<Image src={url} style={{maxHeight: '100%', maxWidth: '100%', objectFit: 'contain'}}/>
</a>
</div>
</Modal.Content>
</React.Fragment>
}
@ -89,14 +90,16 @@ function getVideoPreviewModal(previewFile) {
</Modal.Header>
<Modal.Content>
<SHeader as='h5'>{path['path']}</SHeader>
<video controls
autoPlay={true}
id="player"
playsInline={true}
style={{maxWidth: '100%'}}
>
<source src={url}/>
</video>
<div className='preview-fit'>
<video controls
autoPlay={true}
id="player"
playsInline={true}
style={{maxHeight: '100%', maxWidth: '100%'}}
>
<source src={url}/>
</video>
</div>
</Modal.Content>
</React.Fragment>
}
@ -142,12 +145,14 @@ function STLPreviewModal({previewFile}) {
<b>Bounding Radius:</b> {dimensions.boundingRadius.toFixed(2)}
</div>}
<InlineErrorBoundary>
<StlViewer orbitControls shadows
url={url}
style={{height: '65vh', width: '100%'}}
modelProps={{color: '#7353a8'}}
onFinishLoading={(dims) => setDimensions(dims)}
/>
<div className='preview-fit'>
<StlViewer orbitControls shadows
url={url}
style={{height: '100%', width: '100%'}}
modelProps={{color: '#7353a8'}}
onFinishLoading={(dims) => setDimensions(dims)}
/>
</div>
</InlineErrorBoundary>
</Modal.Content>
</React.Fragment>
@ -275,17 +280,22 @@ export function FilePreviewProvider({children}) {
}
function setModalContent(content, url, downloadURL, path, taggable = true) {
const openButton = url ? <Button color='blue' as='a' href={url}>Open</Button> : null;
const closeButton = <Button onClick={handleClose}>Close</Button>;
// All action buttons share `size='small'` and `icon` for consistent height across viewports.
// Close is handled by the Modal's `closeIcon` (X in the top-right corner) — no dedicated button.
const openButton = url
? <Button color='blue' as='a' href={url} size='small' icon labelPosition='left'>
<Icon name='external'/>Open
</Button>
: null;
const tagsDisplay = taggable ?
<TagsSelector selectedTagNames={previewFile['tags']} onAdd={localAddTag}
onRemove={localRemoveTag}/>
: null;
const downloadButton = downloadURL ?
<Button color='yellow' as='a' href={downloadURL} floated='left' icon='download'/>
const downloadButton = downloadURL
? <Button color='yellow' as='a' href={downloadURL} size='small' icon='download'/>
: null;
const directoryURL = path ? `/files?folders=${encodeURIComponent(pathDirectory(path))}` : null;
const directoryButton = path ? <Button as='a' href={directoryURL} icon='folder'/> : null;
const directoryButton = path ? <Button as='a' href={directoryURL} size='small' icon='folder'/> : null;
const pathContent = path ? <Modal.Content>
<pre>{path}</pre>
</Modal.Content>
@ -294,6 +304,9 @@ export function FilePreviewProvider({children}) {
console.log('Preview Download URL', downloadURL);
console.log('Preview Open URL', url);
// Single flex toolbar — same button order at every viewport size.
// `flex-wrap: wrap` lets the tags selector drop to a second line on narrow screens
// while the button groups stay together and in order: [file actions] [tags] [open/close].
setPreviewModal(<Modal closeIcon
size='fullscreen'
open={true}
@ -301,30 +314,17 @@ export function FilePreviewProvider({children}) {
>
{pathContent}
<Modal.Actions>
<Media at='mobile'>
<Grid>
<Grid.Row>
<Grid.Column width={6} textAlign='left'>{downloadButton}{directoryButton}</Grid.Column>
<Grid.Column width={5}>{openButton}</Grid.Column>
<Grid.Column width={5}>{closeButton}</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column width={2} style={{paddingTop: '0.5em'}}><ShareButton/></Grid.Column>
<Grid.Column width={14}>{tagsDisplay}</Grid.Column>
</Grid.Row>
</Grid>
</Media>
<Media greaterThanOrEqual='tablet'>
<Grid>
<Grid.Row>
<Grid.Column width={2} textAlign='left'>{downloadButton}{directoryButton}</Grid.Column>
<Grid.Column width={1} style={{paddingTop: '0.5em'}}><ShareButton/></Grid.Column>
<Grid.Column width={9}>{tagsDisplay}</Grid.Column>
<Grid.Column width={2}>{openButton}</Grid.Column>
<Grid.Column width={2}>{closeButton}</Grid.Column>
</Grid.Row>
</Grid>
</Media>
<div className='preview-toolbar'>
<div className='preview-toolbar-group'>
{downloadButton}
{directoryButton}
<ShareButton/>
</div>
<div className='preview-toolbar-tags'>{tagsDisplay}</div>
<div className='preview-toolbar-group'>
{openButton}
</div>
</div>
</Modal.Actions>
{content}
</Modal>);

View file

@ -191,9 +191,9 @@ const ZimSearchEntry = ({zimId, onTag, onUntag, entry}) => {
open={open}
onClose={() => setOpen(false)}>
<Modal.Content>
<div className='full-height'>
<div className='preview-fit'>
<ZimViewer src={url} style={{
height: '100%', width: '100%', border: 'none', position: 'absolute', top: 0,
height: '100%', width: '100%', border: 'none',
// Use white to avoid iframe displaying with dark-theme.
backgroundColor: '#ffffff',
}}/>