1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-26 15:26:19 +00:00

[image file view] add a new version of 'image zoomer'(zoom in/out icons, and a scale menu) (#8059)

This commit is contained in:
llj
2025-07-19 17:00:55 +08:00
committed by GitHub
parent fb3a5d3f6c
commit 6451547c45
3 changed files with 167 additions and 33 deletions

View File

@@ -82,7 +82,7 @@ class FileContent extends React.Component {
}
return (
<div className="file-view-content flex-1 image-file-view">
<div className="file-view-content flex-1 image-file-view d-flex align-items-center justify-content-center">
{previousImage && (
<a href={previousImageUrl} id="img-prev" title={gettext('you can also press ← ')}><span className="sf3-font sf3-font-down rotate-90 d-inline-block"></span></a>
)}

View File

@@ -1,57 +1,167 @@
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { Button, Input } from 'reactstrap';
import Icon from '../../components/icon';
import { Input, Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap';
import IconButton from '../../components/icon-button';
import { gettext } from '../../utils/constants';
import '../../metadata/components/data-process-setter/gallery-slider-setter/index.css';
const SCALE_OPTIONS = [0.25, 0.5, 1, 1.5, 2];
const SCALE_OPTIONS = [0.15, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4];
const SCALE_MIN = SCALE_OPTIONS[0];
const SCALE_MAX = SCALE_OPTIONS[SCALE_OPTIONS.length - 1];
const SCALE_OPTIONS_2 = [
{ value: 'page-fit', text: gettext('Page Fit') },
{ value: 'actual-size', text: gettext('Actual Size') }
];
const ImageZoomer = ({ setImageScale }) => {
const [curScale, setScale] = useState(1);
const [curScaleText, setScaleText] = useState(`${curScale * 100}%`); // for the text shown in the input
const [selectedScale, setSelectedScale] = useState(1); // for the scale menu
const [isScaleMenuOpen, setScaleMenuOpen] = useState(false);
const toggleMenu = useCallback(() => {
setScaleMenuOpen(!isScaleMenuOpen);
}, [isScaleMenuOpen, setScaleMenuOpen]);
const scaleImage = useCallback((scale) => {
setImageScale(scale);
}, [setImageScale]);
const changeScale = useCallback((e) => {
const scale = Number(e.target.value);
const zoomInOrOut = useCallback((scale) => {
setScale(scale);
scaleImage(scale);
setScaleText(`${Math.round(scale * 100)}%`); // handle the result of floating point arithmetic
if (SCALE_OPTIONS.indexOf(scale) == -1) {
setSelectedScale(null);
} else {
setSelectedScale(scale);
}
}, [scaleImage]);
const zoomIn = useCallback(() => {
const scale = SCALE_OPTIONS[SCALE_OPTIONS.indexOf(curScale) + 1];
setScale(scale);
scaleImage(scale);
}, [curScale, scaleImage]);
const offset = Math.ceil(curScale) * 0.1; // round up
const normalizedScale = Number((curScale + offset).toFixed(2)); // handle the result of floating point arithmetic
const scale = Math.min(normalizedScale, SCALE_MAX);
zoomInOrOut(scale);
}, [curScale, zoomInOrOut]);
const zoomOut = useCallback(() => {
const scale = SCALE_OPTIONS[SCALE_OPTIONS.indexOf(curScale) - 1];
const offset = Math.ceil(curScale) * 0.1; // round up
const normalizedScale = Number((curScale - offset).toFixed(2)); // handle the result of floating point arithmetic
const scale = Math.max(normalizedScale, SCALE_MIN);
zoomInOrOut(scale);
}, [curScale, zoomInOrOut]);
const scaleImageToPageFit = useCallback(() => {
const imageElement = document.getElementById('image-view');
const borderWidth = 1;
const width = imageElement.clientWidth + borderWidth * 2;
const height = imageElement.clientHeight + borderWidth * 2;
const imageContainer = imageElement.parentNode;
const hPadding = 0; // horizontal padding
const vPadding = 30; // vertical padding
const maxWidth = imageContainer.clientWidth - hPadding * 2;
const maxHeight = imageContainer.clientHeight - vPadding * 2;
const hScale = maxWidth / width;
const vScale = maxHeight / height;
const scale = Math.min(hScale, vScale);
setScale(scale);
scaleImage(scale);
}, [curScale, scaleImage]);
}, [setScale, scaleImage]);
const onMenuItemClick = useCallback((value) => {
setSelectedScale(value);
if (SCALE_OPTIONS.indexOf(value) != -1) {
const scale = value;
setScale(scale);
scaleImage(scale);
setScaleText(`${scale * 100}%`);
} else {
if (value == 'actual-size') {
const scale = 1;
setScale(scale);
scaleImage(scale);
} else {
// 'page-fit'
scaleImageToPageFit();
}
setScaleText(SCALE_OPTIONS_2.filter(item => item.value == value)[0].text);
}
setScaleMenuOpen(false);
}, [scaleImage, setScaleMenuOpen, scaleImageToPageFit]);
const onMenuItemKeyDown = useCallback((e, value) => {
if (e.key == 'Enter' || e.key == 'Space') {
onMenuItemClick(value);
}
}, [onMenuItemClick]);
return (
<div className='metadata-slider-container image-zoomer ml-0'>
<Button className="metadata-slider-icon-button" onClick={zoomOut} disabled={curScale == SCALE_MIN}>
<Icon symbol='minus_sign' className='metadata-slider-icon' />
</Button>
<Input
type="range"
min={SCALE_MIN}
max={SCALE_MAX}
step="any"
value={curScale}
onChange={changeScale}
className="metadata-slider"
<div className='d-flex align-items-center image-zoomer'>
<IconButton
id="zoom-out-image"
icon="minus_sign"
text={gettext('Zoom out')}
onClick={zoomOut}
disabled={curScale == SCALE_MIN}
/>
<Dropdown
isOpen={isScaleMenuOpen}
toggle={toggleMenu}
className="vam"
direction='down'
>
<DropdownToggle
tag='div'
className="position-relative"
data-toggle="dropdown"
aria-expanded={isScaleMenuOpen}
>
<Input id="cur-scale-input" type="text" value={curScaleText} readOnly={true} />
<i id="scale-menu-caret" className="sf3-font-down sf3-font"></i>
</DropdownToggle>
<DropdownMenu id="scale-menu">
{SCALE_OPTIONS.map((item, index) => {
return (
<DropdownItem
key={index}
className="position-relative pl-5"
onClick={() => {onMenuItemClick(item);}}
onKeyDown={(e) => {onMenuItemKeyDown(e, item);}}
>
{selectedScale == item && (
<i className="dropdown-item-tick sf2-icon-tick"></i>
)}
<span>{`${item * 100}%`}</span>
</DropdownItem>
);
})}
{SCALE_OPTIONS_2.map((item, index) => {
return (
<DropdownItem
key={index}
className="position-relative pl-5"
onClick={() => {onMenuItemClick(item.value);}}
onKeyDown={(e) => {onMenuItemKeyDown(e, item.value);}}
>
{selectedScale == item.value && (
<i className="dropdown-item-tick sf2-icon-tick"></i>
)}
<span>{item.text}</span>
</DropdownItem>
);
})}
</DropdownMenu>
</Dropdown>
<IconButton
id="zoom-in-image"
icon="plus_sign"
text={gettext('Zoom in')}
onClick={zoomIn}
disabled={curScale == SCALE_MAX}
/>
<Button className="metadata-slider-icon-button" onClick={zoomIn} disabled={curScale == SCALE_MAX}>
<Icon symbol='plus_sign' className='metadata-slider-icon' />
</Button>
</div>
);
};

View File

@@ -18,8 +18,6 @@
border: 1px solid #eee;
width: auto;
height: auto;
max-width: calc(100% - 4px);
max-height: calc(100% - 4px);
font-size: 0;
line-height: 0;
}
@@ -51,6 +49,32 @@
color: var(--bs-body-color);
}
.image-zoomer {
height: 24px;
.image-zoomer #zoom-out-image {
margin-right: 2px;
font-size: 18px; /* the actual icon is smaller than the others */
}
.image-zoomer #zoom-in-image {
margin-left: 2px;
font-size: 18px; /* the actual icon is smaller than the others */
}
.image-zoomer #cur-scale-input {
height: 28px;
width: 120px;
user-select: none;
}
.image-zoomer #scale-menu {
min-width: 120px; /* overwrite css from seahub_react.css */
width: 120px;
}
.image-zoomer #scale-menu-caret {
position: absolute;
right: 12px;
top: 9px;
font-size: 9px;
line-height: 1;
color: #666;
}