mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
84e18e4954
Paste style wrote the copied fields straight onto the element, although a pasted look is meant to be the style operation the assistant sends. It now runs the engine's own restyle (bookElementStyled): a colour or face pasted by hand takes the element off the theme, so the next theme applied no longer brings the old colour back; a plain fill replaces the paint of the shape it lands on instead of staying hidden under it; and a line takes another line's look without its points, which are its course, not its look. A background picked by hand in the inspector kept the page's link to the theme, so the next theme applied set the page back to the theme's colour. The picker and the background operation now paint through one helper that takes the link away (withSpreadPaintByHand). Giving one page's background to every page carries that page's link along, or takes the others' away when it was painted by hand. A built-in layout, a book's own layout and laying a page out again from its entry ignored locks in the Studio: a locked shape was dropped and a locked photo poured into a slot unlocked, while the assistant's layout operation left both where they were. All four now lay a page out around its locked elements through one function (layoutAroundLocks), which the engine's layout operation uses too. Fill page and fill spread kept the photo in its group, so the background picture was selected and dragged with its caption and listed under the group at the front. It leaves its group as the engine's fill does, and a group left with one member goes.
132 lines
4.8 KiB
TypeScript
132 lines
4.8 KiB
TypeScript
import { bookElementId, spreadPaint, withSpreadPaintByHand, type BookPageSetup } from '@trek/shared';
|
|
import { Check, CopyCheck, ImageOff } from 'lucide-react';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { useStudioStore } from '../../store/studioStore';
|
|
import { photoSrc } from './bookRender';
|
|
import {
|
|
backgroundCandidates,
|
|
backgroundPhotoOf,
|
|
backgroundToAll,
|
|
withBackgroundPhoto,
|
|
withoutBackgroundPhoto,
|
|
} from './paint/backgroundPhoto';
|
|
import { spreadPhotoIds } from './paint/paletteModel';
|
|
import { usePaintChannel } from './paint/usePaintChannel';
|
|
import { Section } from './StudioControls';
|
|
import { StudioPaintPicker } from './StudioPaintPicker';
|
|
import type { JourneySource, StudioT } from './StudioSidebar';
|
|
|
|
/**
|
|
* The page's background, in the inspector when nothing is selected.
|
|
*
|
|
* Until now only the assistant could set it; a person had to lay a shape
|
|
* across the page and remember to send it to the back. Three things, in the
|
|
* order people reach for them: the paint itself, the same background for
|
|
* every page, and a photograph under the whole page.
|
|
*/
|
|
export function StudioBackgroundSection({
|
|
spreadIndex,
|
|
page,
|
|
source,
|
|
t,
|
|
}: {
|
|
spreadIndex: number;
|
|
page: BookPageSetup;
|
|
source: JourneySource;
|
|
t: StudioT;
|
|
}) {
|
|
const spread = useStudioStore((s) => s.doc?.spreads[spreadIndex]);
|
|
const commit = useStudioStore((s) => s.commit);
|
|
// A colour picked here is the person's, so the page stops following the theme's paper (as the assistant's does).
|
|
const channel = usePaintChannel((paint) => (doc) => ({
|
|
...doc,
|
|
spreads: doc.spreads.map((sp, i) => (i === spreadIndex ? withSpreadPaintByHand(sp, paint) : sp)),
|
|
}));
|
|
|
|
// "Applied" stands in the button for a moment, so a press that changed pages out of sight is seen to have worked.
|
|
const [applied, setApplied] = useState(false);
|
|
useEffect(() => {
|
|
if (!applied) return;
|
|
const timer = window.setTimeout(() => setApplied(false), 1600);
|
|
return () => window.clearTimeout(timer);
|
|
}, [applied]);
|
|
|
|
const current = backgroundPhotoOf(spread, page);
|
|
const candidates = useMemo(() => {
|
|
if (!spread) return [];
|
|
const entry = source.entries.find((e) => e.id === spread.entryId);
|
|
return backgroundCandidates(
|
|
spreadPhotoIds(spread),
|
|
entry?.photoIds ?? [],
|
|
source.photos.map((p) => p.photoId)
|
|
);
|
|
}, [spread, source]);
|
|
|
|
if (!spread) return null;
|
|
|
|
const setPhoto = (photoId: number) =>
|
|
commit((doc) => ({
|
|
...doc,
|
|
spreads: doc.spreads.map((sp, i) => {
|
|
if (i !== spreadIndex) return sp;
|
|
return withBackgroundPhoto(sp, page, photoId, () => bookElementId('p')) ?? sp;
|
|
}),
|
|
}));
|
|
const clearPhoto = () =>
|
|
commit((doc) => ({
|
|
...doc,
|
|
spreads: doc.spreads.map((sp, i) => (i === spreadIndex ? withoutBackgroundPhoto(sp, page) : sp)),
|
|
}));
|
|
const applyToAll = () => {
|
|
commit((doc) => backgroundToAll(doc, spreadIndex));
|
|
setApplied(true);
|
|
};
|
|
|
|
return (
|
|
<Section label={t('journey.studio.paint.background')}>
|
|
<StudioPaintPicker
|
|
value={spreadPaint(spread)}
|
|
channel={channel}
|
|
t={t}
|
|
noneLabel={t('journey.studio.paint.paper')}
|
|
/>
|
|
|
|
<button type="button" className={`st-chip st-paint-apply ${applied ? 'is-done' : ''}`} onClick={applyToAll}>
|
|
{applied ? <Check size={13} strokeWidth={2.4} /> : <CopyCheck size={13} />}
|
|
{applied ? t('journey.studio.paint.appliedAll') : t('journey.studio.paint.applyAll')}
|
|
</button>
|
|
|
|
<div className="st-paint-field">
|
|
<span className="st-paint-caption">{t('journey.studio.paint.photoBackground')}</span>
|
|
{candidates.length ? (
|
|
<>
|
|
<div className="st-bgphoto-grid">
|
|
{candidates.map((id) => (
|
|
<button
|
|
type="button"
|
|
key={id}
|
|
className={`st-bgphoto ${current?.photoId === id ? 'is-on' : ''}`}
|
|
aria-pressed={current?.photoId === id}
|
|
aria-label={t('journey.studio.paint.photoBackground')}
|
|
onClick={() => (current?.photoId === id ? clearPhoto() : setPhoto(id))}
|
|
>
|
|
<img src={photoSrc(id, false)} alt="" loading="lazy" draggable={false} />
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p className="st-hint st-paint-hint">{t('journey.studio.paint.photoBackgroundHint')}</p>
|
|
{current && (
|
|
<button type="button" className="st-chip is-danger st-paint-apply" onClick={clearPhoto}>
|
|
<ImageOff size={13} />
|
|
{t('journey.studio.paint.removePhotoBackground')}
|
|
</button>
|
|
)}
|
|
</>
|
|
) : (
|
|
<p className="st-hint st-paint-hint">{t('journey.studio.paint.noPhotos')}</p>
|
|
)}
|
|
</div>
|
|
</Section>
|
|
);
|
|
}
|