Add Chapters for Galleries (#3289)

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
yoshnopa
2023-03-16 05:04:54 +01:00
committed by GitHub
parent 32c91c4855
commit 7e8f941155
58 changed files with 1685 additions and 133 deletions

View File

@@ -7,6 +7,7 @@ import {
Popover,
Form,
Row,
Dropdown,
} from "react-bootstrap";
import cx from "classnames";
import Mousetrap from "mousetrap";
@@ -30,7 +31,7 @@ import {
import * as GQL from "src/core/generated-graphql";
import { useInterfaceLocalForage } from "../LocalForage";
import { imageLightboxDisplayModeIntlMap } from "src/core/enums";
import { ILightboxImage } from "./types";
import { ILightboxImage, IChapter } from "./types";
import {
faArrowLeft,
faArrowRight,
@@ -42,6 +43,7 @@ import {
faPlay,
faSearchMinus,
faTimes,
faBars,
} from "@fortawesome/free-solid-svg-icons";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
import { useDebounce } from "../debounce";
@@ -49,6 +51,8 @@ import { useDebounce } from "../debounce";
const CLASSNAME = "Lightbox";
const CLASSNAME_HEADER = `${CLASSNAME}-header`;
const CLASSNAME_LEFT_SPACER = `${CLASSNAME_HEADER}-left-spacer`;
const CLASSNAME_CHAPTERS = `${CLASSNAME_HEADER}-chapters`;
const CLASSNAME_CHAPTER_BUTTON = `${CLASSNAME_HEADER}-chapter-button`;
const CLASSNAME_INDICATOR = `${CLASSNAME_HEADER}-indicator`;
const CLASSNAME_OPTIONS = `${CLASSNAME_HEADER}-options`;
const CLASSNAME_OPTIONS_ICON = `${CLASSNAME_OPTIONS}-icon`;
@@ -76,8 +80,11 @@ interface IProps {
initialIndex?: number;
showNavigation: boolean;
slideshowEnabled?: boolean;
pageHeader?: string;
pageCallback?: (direction: number) => void;
page?: number;
pages?: number;
pageSize?: number;
pageCallback?: (props: { direction?: number; page?: number }) => void;
chapters?: IChapter[];
hide: () => void;
}
@@ -88,12 +95,16 @@ export const LightboxComponent: React.FC<IProps> = ({
initialIndex = 0,
showNavigation,
slideshowEnabled = false,
pageHeader,
page,
pages,
pageSize: pageSize = 40,
pageCallback,
chapters = [],
hide,
}) => {
const [updateImage] = useImageUpdate();
// zero-based
const [index, setIndex] = useState<number | null>(null);
const [movingLeft, setMovingLeft] = useState(false);
const oldIndex = useRef<number | null>(null);
@@ -101,6 +112,7 @@ export const LightboxComponent: React.FC<IProps> = ({
const [isSwitchingPage, setIsSwitchingPage] = useState(true);
const [isFullscreen, setFullscreen] = useState(false);
const [showOptions, setShowOptions] = useState(false);
const [showChapters, setShowChapters] = useState(false);
const [imagesLoaded, setImagesLoaded] = useState(0);
const [navOffset, setNavOffset] = useState<React.CSSProperties | undefined>();
@@ -310,12 +322,13 @@ export const LightboxComponent: React.FC<IProps> = ({
(isUserAction = true) => {
if (isSwitchingPage || index === -1) return;
setShowChapters(false);
setMovingLeft(true);
if (index === 0) {
// go to next page, or loop back if no callback is set
if (pageCallback) {
pageCallback(-1);
pageCallback({ direction: -1 });
setIndex(-1);
oldImages.current = images;
setIsSwitchingPage(true);
@@ -334,11 +347,12 @@ export const LightboxComponent: React.FC<IProps> = ({
if (isSwitchingPage) return;
setMovingLeft(false);
setShowChapters(false);
if (index === images.length - 1) {
// go to preview page, or loop back if no callback is set
if (pageCallback) {
pageCallback(1);
pageCallback({ direction: 1 });
oldImages.current = images;
setIsSwitchingPage(true);
setIndex(0);
@@ -449,6 +463,65 @@ export const LightboxComponent: React.FC<IProps> = ({
const currentIndex = index === null ? initialIndex : index;
function gotoPage(imageIndex: number) {
const indexInPage = (imageIndex - 1) % pageSize;
if (pageCallback) {
let jumppage = Math.floor(imageIndex / pageSize) + 1;
if (page !== jumppage) {
pageCallback({ page: jumppage });
oldImages.current = images;
setIsSwitchingPage(true);
}
}
setIndex(indexInPage);
setShowChapters(false);
}
function chapterHeader() {
const imageNumber = (index ?? 0) + 1;
const globalIndex = page
? (page - 1) * pageSize + imageNumber
: imageNumber;
let chapterTitle = "";
chapters.forEach(function (chapter) {
if (chapter.image_index > globalIndex) {
return;
}
chapterTitle = chapter.title;
});
return chapterTitle ?? "";
}
const renderChapterMenu = () => {
if (chapters.length <= 0) return;
const popoverContent = chapters.map(({ id, title, image_index }) => (
<Dropdown.Item key={id} onClick={() => gotoPage(image_index)}>
{" "}
{title}
{title.length > 0 ? " - #" : "#"}
{image_index}
</Dropdown.Item>
));
return (
<Dropdown
show={showChapters}
onToggle={() => setShowChapters(!showChapters)}
>
<Dropdown.Toggle className={`minimal ${CLASSNAME_CHAPTER_BUTTON}`}>
<Icon icon={showChapters ? faTimes : faBars} />
</Dropdown.Toggle>
<Dropdown.Menu className={`${CLASSNAME_CHAPTERS}`}>
{popoverContent}
</Dropdown.Menu>
</Dropdown>
);
};
// #2451: making OptionsForm an inline component means it
// get re-rendered each time. This makes the text
// field lose focus on input. Use function instead.
@@ -634,6 +707,14 @@ export const LightboxComponent: React.FC<IProps> = ({
}
}
const pageHeader =
page && pages
? intl.formatMessage(
{ id: "dialogs.lightbox.page_header" },
{ page, total: pages }
)
: "";
return (
<div
className={CLASSNAME}
@@ -642,9 +723,11 @@ export const LightboxComponent: React.FC<IProps> = ({
onClick={handleClose}
>
<div className={CLASSNAME_HEADER}>
<div className={CLASSNAME_LEFT_SPACER} />
<div className={CLASSNAME_LEFT_SPACER}>{renderChapterMenu()}</div>
<div className={CLASSNAME_INDICATOR}>
<span>{pageHeader}</span>
<span>
{chapterHeader()} {pageHeader}
</span>
{images.length > 1 ? (
<b ref={indicatorRef}>{`${currentIndex + 1} / ${images.length}`}</b>
) : undefined}