fix(gallery): keep canvas rendering in the lightbox, render tiles as <img>
Every tile, the hero and the folder covers switched to a <canvas> when the per-event toggle was on or the protection level was `maximum`. A canvas pins a backing store of naturalWidth × naturalHeight × 4 bytes that the browser is not allowed to evict, and iOS Safari has a hard budget for canvas memory that fails silently when exceeded — blank tiles, no error, on exactly the browser the large-gallery report came from. A gallery is several hundred tiles and one lightbox image. What canvas buys on a thumbnail is a slightly harder right-click. What actually protects the images is server-side: the served file is watermarked and the download route refuses when downloads are off. The photographer who reported the large-gallery case, shipping to real clients, said the same and turned the global toggle off once it was about to reach their next gallery. So: tiles, hero and folder covers always render <img>. The lightbox keeps both the per-event toggle and the `maximum` implication — one image, where the calculus is different. The toggle is now wired to the lightbox for the first time; before this it reached only the tiles, so the label that said "canvas rendering" turned every grid into canvases and left the lightbox alone. Labels in all four locales now say where it applies. `protectionLevel` was destructured in seven tile components only to feed that OR; those props and their pass-throughs go with it. The shared layout props keep it, since the story layout still hands it to its lightbox. A source-level test pins that only PhotoLightbox passes useCanvasRendering to AuthenticatedImage or turns it on for `maximum`. Relates to issue 1287
This commit is contained in:
@@ -22,9 +22,7 @@ interface GalleryFolderTilesProps {
|
|||||||
* so a gallery configured for canvas rendering or maximum protection must not
|
* so a gallery configured for canvas rendering or maximum protection must not
|
||||||
* get an ordinary blob-backed <img> here just because it is a cover.
|
* get an ordinary blob-backed <img> here just because it is a cover.
|
||||||
*/
|
*/
|
||||||
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
|
||||||
useEnhancedProtection?: boolean;
|
useEnhancedProtection?: boolean;
|
||||||
useCanvasRendering?: boolean;
|
|
||||||
allowDownloads?: boolean;
|
allowDownloads?: boolean;
|
||||||
/**
|
/**
|
||||||
* Compact chip row instead of cover cards. Used by the full-bleed layouts
|
* Compact chip row instead of cover cards. Used by the full-bleed layouts
|
||||||
@@ -40,8 +38,6 @@ export const GalleryFolderTiles: React.FC<GalleryFolderTilesProps> = ({
|
|||||||
onOpen,
|
onOpen,
|
||||||
compact = false,
|
compact = false,
|
||||||
slug,
|
slug,
|
||||||
protectionLevel,
|
|
||||||
useCanvasRendering,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -95,10 +91,6 @@ export const GalleryFolderTiles: React.FC<GalleryFolderTilesProps> = ({
|
|||||||
isGallery
|
isGallery
|
||||||
slug={slug}
|
slug={slug}
|
||||||
// Same rule as every other gallery image path: maximum
|
// Same rule as every other gallery image path: maximum
|
||||||
// protection implies canvas rendering even when the separate
|
|
||||||
// toggle is off (its default), otherwise a cover silently
|
|
||||||
// falls back to a blob-backed <img>.
|
|
||||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
|||||||
@@ -1311,9 +1311,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event, requiresP
|
|||||||
onOpen={openFolderBySlug}
|
onOpen={openFolderBySlug}
|
||||||
compact={compact}
|
compact={compact}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
protectionLevel={protectionLevel}
|
|
||||||
useEnhancedProtection={protectionLevel !== 'basic'}
|
useEnhancedProtection={protectionLevel !== 'basic'}
|
||||||
useCanvasRendering={useCanvasRendering}
|
|
||||||
allowDownloads={allowDownloads}
|
allowDownloads={allowDownloads}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ interface HeroHeaderProps {
|
|||||||
heroLogoPosition?: 'top' | 'center' | 'bottom';
|
heroLogoPosition?: 'top' | 'center' | 'bottom';
|
||||||
dividerStyle?: HeroDividerStyle;
|
dividerStyle?: HeroDividerStyle;
|
||||||
allowDownloads?: boolean;
|
allowDownloads?: boolean;
|
||||||
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
|
||||||
useEnhancedProtection?: boolean;
|
useEnhancedProtection?: boolean;
|
||||||
useCanvasRendering?: boolean;
|
|
||||||
onScrollToContent?: () => void;
|
onScrollToContent?: () => void;
|
||||||
// Hero image anchor position (#162) – keyword or "X% Y%" focal point
|
// Hero image anchor position (#162) – keyword or "X% Y%" focal point
|
||||||
heroImageAnchor?: string;
|
heroImageAnchor?: string;
|
||||||
@@ -43,8 +41,6 @@ export const HeroHeader: React.FC<HeroHeaderProps> = ({
|
|||||||
heroLogoSize = 'medium',
|
heroLogoSize = 'medium',
|
||||||
heroLogoPosition = 'top',
|
heroLogoPosition = 'top',
|
||||||
dividerStyle = 'wave',
|
dividerStyle = 'wave',
|
||||||
protectionLevel = 'standard',
|
|
||||||
useCanvasRendering = false,
|
|
||||||
onScrollToContent,
|
onScrollToContent,
|
||||||
heroImageAnchor = 'center'
|
heroImageAnchor = 'center'
|
||||||
}) => {
|
}) => {
|
||||||
@@ -142,7 +138,6 @@ export const HeroHeader: React.FC<HeroHeaderProps> = ({
|
|||||||
style={{ objectPosition: heroImageAnchor }}
|
style={{ objectPosition: heroImageAnchor }}
|
||||||
isGallery={true}
|
isGallery={true}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Overlay */}
|
{/* Overlay */}
|
||||||
|
|||||||
@@ -196,7 +196,6 @@ export const PhotoGrid: React.FC<PhotoGridProps> = ({
|
|||||||
allowDownloads={allowDownloads}
|
allowDownloads={allowDownloads}
|
||||||
protectionLevel={protectionLevel}
|
protectionLevel={protectionLevel}
|
||||||
useEnhancedProtection={useEnhancedProtection}
|
useEnhancedProtection={useEnhancedProtection}
|
||||||
useCanvasRendering={useCanvasRendering}
|
|
||||||
slug={slug}
|
slug={slug}
|
||||||
feedbackEnabled={feedbackEnabled}
|
feedbackEnabled={feedbackEnabled}
|
||||||
/>
|
/>
|
||||||
@@ -214,6 +213,7 @@ export const PhotoGrid: React.FC<PhotoGridProps> = ({
|
|||||||
allowDownloads={allowDownloads}
|
allowDownloads={allowDownloads}
|
||||||
protectionLevel={protectionLevel}
|
protectionLevel={protectionLevel}
|
||||||
useEnhancedProtection={useEnhancedProtection}
|
useEnhancedProtection={useEnhancedProtection}
|
||||||
|
useCanvasRendering={useCanvasRendering}
|
||||||
disableRightClick={disableRightClick}
|
disableRightClick={disableRightClick}
|
||||||
enableDevtoolsProtection={enableDevtoolsProtection}
|
enableDevtoolsProtection={enableDevtoolsProtection}
|
||||||
/>
|
/>
|
||||||
@@ -231,7 +231,6 @@ interface PhotoThumbnailProps {
|
|||||||
allowDownloads?: boolean;
|
allowDownloads?: boolean;
|
||||||
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
||||||
useEnhancedProtection?: boolean;
|
useEnhancedProtection?: boolean;
|
||||||
useCanvasRendering?: boolean;
|
|
||||||
slug: string; // Add slug as required prop
|
slug: string; // Add slug as required prop
|
||||||
feedbackEnabled?: boolean;
|
feedbackEnabled?: boolean;
|
||||||
}
|
}
|
||||||
@@ -244,7 +243,6 @@ const PhotoThumbnail: React.FC<PhotoThumbnailProps> = ({
|
|||||||
onDownload,
|
onDownload,
|
||||||
allowDownloads = true,
|
allowDownloads = true,
|
||||||
protectionLevel = 'standard',
|
protectionLevel = 'standard',
|
||||||
useCanvasRendering = false,
|
|
||||||
slug,
|
slug,
|
||||||
feedbackEnabled = false
|
feedbackEnabled = false
|
||||||
}) => {
|
}) => {
|
||||||
@@ -268,7 +266,6 @@ const PhotoThumbnail: React.FC<PhotoThumbnailProps> = ({
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
isGallery={true}
|
isGallery={true}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
|
||||||
onProtectionViolation={(violationType) => {
|
onProtectionViolation={(violationType) => {
|
||||||
// Track analytics
|
// Track analytics
|
||||||
if (typeof window !== 'undefined' && (window as any).umami) {
|
if (typeof window !== 'undefined' && (window as any).umami) {
|
||||||
|
|||||||
@@ -361,9 +361,7 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
|
|||||||
heroLogoPosition={heroLogoPosition}
|
heroLogoPosition={heroLogoPosition}
|
||||||
dividerStyle={heroDividerStyle}
|
dividerStyle={heroDividerStyle}
|
||||||
allowDownloads={allowDownloads}
|
allowDownloads={allowDownloads}
|
||||||
protectionLevel={protectionLevel}
|
|
||||||
useEnhancedProtection={useEnhancedProtection}
|
useEnhancedProtection={useEnhancedProtection}
|
||||||
useCanvasRendering={useCanvasRendering}
|
|
||||||
heroImageAnchor={heroImageAnchor}
|
heroImageAnchor={heroImageAnchor}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Canvas rendering is a lightbox concern, not a tile concern.
|
||||||
|
*
|
||||||
|
* A canvas pins a backing store of naturalWidth × naturalHeight × 4 bytes
|
||||||
|
* that the browser is not allowed to evict, and iOS Safari has a hard
|
||||||
|
* budget for canvas memory that fails silently when exceeded — blank
|
||||||
|
* tiles, no error. A gallery is hundreds of tiles and one lightbox image,
|
||||||
|
* so the tiles render <img> whatever the protection level says, and the
|
||||||
|
* lightbox keeps the per-event toggle and the `maximum` implication.
|
||||||
|
*
|
||||||
|
* Source-level pin: nothing under components/gallery except PhotoLightbox
|
||||||
|
* may hand `useCanvasRendering` to AuthenticatedImage.
|
||||||
|
*/
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
function walk(dir: string): string[] {
|
||||||
|
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||||
|
const file = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) return entry.name === '__tests__' ? [] : walk(file);
|
||||||
|
return file.endsWith('.tsx') ? [file] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('canvas rendering stays in the lightbox', () => {
|
||||||
|
const files = walk(root);
|
||||||
|
const lightbox = path.join(root, 'PhotoLightbox.tsx');
|
||||||
|
|
||||||
|
/** The JSX props of every <AuthenticatedImage> in a file, plus every
|
||||||
|
* `imageProps={{ ... }}` object a layout hands to PhotoCard to spread in. */
|
||||||
|
const imageProps = (source: string) => [
|
||||||
|
...source.split('<AuthenticatedImage').slice(1).map((chunk) => chunk.split('/>')[0]),
|
||||||
|
...source.split('imageProps={{').slice(1).map((chunk) => chunk.split('}}')[0]),
|
||||||
|
];
|
||||||
|
|
||||||
|
it('only PhotoLightbox passes useCanvasRendering to AuthenticatedImage', () => {
|
||||||
|
const offenders = files.filter((file) => file !== lightbox
|
||||||
|
&& imageProps(fs.readFileSync(file, 'utf8')).some((props) => props.includes('useCanvasRendering')));
|
||||||
|
expect(offenders.map((f) => path.relative(root, f))).toEqual([]);
|
||||||
|
// The pin has teeth: the lightbox itself is caught by the same probe.
|
||||||
|
expect(imageProps(fs.readFileSync(lightbox, 'utf8')).some((props) => props.includes('useCanvasRendering'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only PhotoLightbox turns canvas on for protection level maximum', () => {
|
||||||
|
const offenders = files.filter((file) => file !== lightbox
|
||||||
|
&& /useCanvasRendering[^\n]*protectionLevel === 'maximum'/.test(fs.readFileSync(file, 'utf8')));
|
||||||
|
expect(offenders.map((f) => path.relative(root, f))).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the lightbox still does both', () => {
|
||||||
|
const source = fs.readFileSync(lightbox, 'utf8');
|
||||||
|
expect(source).toMatch(/useCanvasRendering=\{useCanvasRendering \|\| protectionLevel === 'maximum'\}/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -47,6 +47,7 @@ export interface BaseGalleryLayoutProps {
|
|||||||
onPickResolution?: (photoIds: number[]) => void;
|
onPickResolution?: (photoIds: number[]) => void;
|
||||||
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
||||||
useEnhancedProtection?: boolean;
|
useEnhancedProtection?: boolean;
|
||||||
|
/** Canvas rendering applies to the lightbox only; tiles always render <img>. */
|
||||||
useCanvasRendering?: boolean;
|
useCanvasRendering?: boolean;
|
||||||
feedbackEnabled?: boolean;
|
feedbackEnabled?: boolean;
|
||||||
feedbackOptions?: {
|
feedbackOptions?: {
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ interface PhotoCardProps {
|
|||||||
isLiked: boolean;
|
isLiked: boolean;
|
||||||
slug: string;
|
slug: string;
|
||||||
allowDownloads?: boolean;
|
allowDownloads?: boolean;
|
||||||
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
|
||||||
useEnhancedProtection?: boolean;
|
useEnhancedProtection?: boolean;
|
||||||
useCanvasRendering?: boolean;
|
|
||||||
feedbackEnabled?: boolean;
|
feedbackEnabled?: boolean;
|
||||||
// #506: track the per-event "allow likes" toggle so the per-photo
|
// #506: track the per-event "allow likes" toggle so the per-photo
|
||||||
// Like button respects it. `feedbackEnabled` alone isn't enough —
|
// Like button respects it. `feedbackEnabled` alone isn't enough —
|
||||||
@@ -68,8 +66,6 @@ const PhotoCard: React.FC<PhotoCardProps> = ({
|
|||||||
isSelectionMode,
|
isSelectionMode,
|
||||||
isLiked,
|
isLiked,
|
||||||
slug,
|
slug,
|
||||||
protectionLevel = 'standard',
|
|
||||||
useCanvasRendering = false,
|
|
||||||
feedbackEnabled = false,
|
feedbackEnabled = false,
|
||||||
allowLikes = false,
|
allowLikes = false,
|
||||||
index
|
index
|
||||||
@@ -122,7 +118,6 @@ const PhotoCard: React.FC<PhotoCardProps> = ({
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
isGallery={true}
|
isGallery={true}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Colour label (#1044) — same badge every layout uses. */}
|
{/* Colour label (#1044) — same badge every layout uses. */}
|
||||||
@@ -208,9 +203,7 @@ export const GalleryPremiumLayout: React.FC<GalleryPremiumLayoutProps> = ({
|
|||||||
allowDownloads = true,
|
allowDownloads = true,
|
||||||
downloadChoices,
|
downloadChoices,
|
||||||
onPickResolution,
|
onPickResolution,
|
||||||
protectionLevel = 'standard',
|
|
||||||
useEnhancedProtection = false,
|
useEnhancedProtection = false,
|
||||||
useCanvasRendering = false,
|
|
||||||
feedbackEnabled = false,
|
feedbackEnabled = false,
|
||||||
feedbackOptions,
|
feedbackOptions,
|
||||||
heroPhotoOverride,
|
heroPhotoOverride,
|
||||||
@@ -629,9 +622,7 @@ export const GalleryPremiumLayout: React.FC<GalleryPremiumLayoutProps> = ({
|
|||||||
isLiked={likedPhotoIds.has(originalPhoto.id)}
|
isLiked={likedPhotoIds.has(originalPhoto.id)}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
allowDownloads={allowDownloads}
|
allowDownloads={allowDownloads}
|
||||||
protectionLevel={protectionLevel}
|
|
||||||
useEnhancedProtection={useEnhancedProtection}
|
useEnhancedProtection={useEnhancedProtection}
|
||||||
useCanvasRendering={useCanvasRendering}
|
|
||||||
feedbackEnabled={feedbackEnabled}
|
feedbackEnabled={feedbackEnabled}
|
||||||
allowLikes={!!feedbackOptions?.allowLikes}
|
allowLikes={!!feedbackOptions?.allowLikes}
|
||||||
index={photoIndex}
|
index={photoIndex}
|
||||||
|
|||||||
@@ -255,9 +255,7 @@ export const GalleryStoryLayout: React.FC<GalleryStoryLayoutProps> = ({
|
|||||||
photo={heroPhoto}
|
photo={heroPhoto}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
allowDownloads={allowDownloads}
|
allowDownloads={allowDownloads}
|
||||||
protectionLevel={protectionLevel}
|
|
||||||
useEnhancedProtection={useEnhancedProtection}
|
useEnhancedProtection={useEnhancedProtection}
|
||||||
useCanvasRendering={useCanvasRendering}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main Content - Scenes */}
|
{/* Main Content - Scenes */}
|
||||||
@@ -281,9 +279,7 @@ export const GalleryStoryLayout: React.FC<GalleryStoryLayoutProps> = ({
|
|||||||
onPhotoClick={handleOpenLightbox}
|
onPhotoClick={handleOpenLightbox}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
allowDownloads={allowDownloads}
|
allowDownloads={allowDownloads}
|
||||||
protectionLevel={protectionLevel}
|
|
||||||
useEnhancedProtection={useEnhancedProtection}
|
useEnhancedProtection={useEnhancedProtection}
|
||||||
useCanvasRendering={useCanvasRendering}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div id={`gallery-${scene.id}`} className="story-gallery-grid">
|
<div id={`gallery-${scene.id}`} className="story-gallery-grid">
|
||||||
@@ -298,9 +294,7 @@ export const GalleryStoryLayout: React.FC<GalleryStoryLayoutProps> = ({
|
|||||||
slug={slug}
|
slug={slug}
|
||||||
galleryId={`gallery-${scene.id}`}
|
galleryId={`gallery-${scene.id}`}
|
||||||
allowDownloads={allowDownloads}
|
allowDownloads={allowDownloads}
|
||||||
protectionLevel={protectionLevel}
|
|
||||||
useEnhancedProtection={useEnhancedProtection}
|
useEnhancedProtection={useEnhancedProtection}
|
||||||
useCanvasRendering={useCanvasRendering}
|
|
||||||
// Mark first photo in each grid as featured
|
// Mark first photo in each grid as featured
|
||||||
featured={index === 0 && scene.photos.length > 4}
|
featured={index === 0 && scene.photos.length > 4}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ interface GridPhotoProps {
|
|||||||
animationType?: string;
|
animationType?: string;
|
||||||
allowDownloads?: boolean;
|
allowDownloads?: boolean;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
|
||||||
useEnhancedProtection?: boolean;
|
useEnhancedProtection?: boolean;
|
||||||
useCanvasRendering?: boolean;
|
|
||||||
feedbackEnabled?: boolean;
|
feedbackEnabled?: boolean;
|
||||||
feedbackOptions?: {
|
feedbackOptions?: {
|
||||||
allowLikes?: boolean;
|
allowLikes?: boolean;
|
||||||
@@ -47,8 +45,6 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
|
|||||||
animationType = 'fade',
|
animationType = 'fade',
|
||||||
allowDownloads = true,
|
allowDownloads = true,
|
||||||
slug,
|
slug,
|
||||||
protectionLevel = 'standard',
|
|
||||||
useCanvasRendering = false,
|
|
||||||
feedbackEnabled = false,
|
feedbackEnabled = false,
|
||||||
feedbackOptions,
|
feedbackOptions,
|
||||||
savedIdentity,
|
savedIdentity,
|
||||||
@@ -130,7 +126,6 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
|
|||||||
loading: 'lazy',
|
loading: 'lazy',
|
||||||
isGallery: true,
|
isGallery: true,
|
||||||
slug,
|
slug,
|
||||||
useCanvasRendering: useCanvasRendering || protectionLevel === 'maximum',
|
|
||||||
onProtectionViolation: (violationType: string) => {
|
onProtectionViolation: (violationType: string) => {
|
||||||
console.warn(`Protection violation on grid photo ${photo.id}: ${violationType}`);
|
console.warn(`Protection violation on grid photo ${photo.id}: ${violationType}`);
|
||||||
},
|
},
|
||||||
@@ -200,9 +195,7 @@ export const GridGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
|||||||
isSelectionMode = false,
|
isSelectionMode = false,
|
||||||
onPhotoSelect,
|
onPhotoSelect,
|
||||||
allowDownloads = true,
|
allowDownloads = true,
|
||||||
protectionLevel = 'standard',
|
|
||||||
useEnhancedProtection = false,
|
useEnhancedProtection = false,
|
||||||
useCanvasRendering = false,
|
|
||||||
feedbackEnabled = false,
|
feedbackEnabled = false,
|
||||||
feedbackOptions,
|
feedbackOptions,
|
||||||
isClient = false,
|
isClient = false,
|
||||||
@@ -255,9 +248,7 @@ export const GridGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
|||||||
animationType={animation}
|
animationType={animation}
|
||||||
allowDownloads={allowDownloads}
|
allowDownloads={allowDownloads}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
protectionLevel={protectionLevel}
|
|
||||||
useEnhancedProtection={useEnhancedProtection}
|
useEnhancedProtection={useEnhancedProtection}
|
||||||
useCanvasRendering={useCanvasRendering}
|
|
||||||
feedbackEnabled={feedbackEnabled}
|
feedbackEnabled={feedbackEnabled}
|
||||||
feedbackOptions={feedbackOptions}
|
feedbackOptions={feedbackOptions}
|
||||||
savedIdentity={savedIdentity}
|
savedIdentity={savedIdentity}
|
||||||
|
|||||||
@@ -40,9 +40,7 @@ interface JustifiedPhotoProps {
|
|||||||
animationType?: string;
|
animationType?: string;
|
||||||
allowDownloads?: boolean;
|
allowDownloads?: boolean;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
|
||||||
useEnhancedProtection?: boolean;
|
useEnhancedProtection?: boolean;
|
||||||
useCanvasRendering?: boolean;
|
|
||||||
feedbackEnabled?: boolean;
|
feedbackEnabled?: boolean;
|
||||||
feedbackOptions?: {
|
feedbackOptions?: {
|
||||||
allowLikes?: boolean;
|
allowLikes?: boolean;
|
||||||
@@ -69,8 +67,6 @@ const JustifiedPhoto: React.FC<JustifiedPhotoProps> = ({
|
|||||||
animationType = 'fade',
|
animationType = 'fade',
|
||||||
allowDownloads = true,
|
allowDownloads = true,
|
||||||
slug,
|
slug,
|
||||||
protectionLevel = 'standard',
|
|
||||||
useCanvasRendering = false,
|
|
||||||
feedbackEnabled = false,
|
feedbackEnabled = false,
|
||||||
feedbackOptions,
|
feedbackOptions,
|
||||||
savedIdentity,
|
savedIdentity,
|
||||||
@@ -133,7 +129,6 @@ const JustifiedPhoto: React.FC<JustifiedPhotoProps> = ({
|
|||||||
loading: 'lazy',
|
loading: 'lazy',
|
||||||
isGallery: true,
|
isGallery: true,
|
||||||
slug,
|
slug,
|
||||||
useCanvasRendering: useCanvasRendering || protectionLevel === 'maximum',
|
|
||||||
onProtectionViolation: (violationType: string) => {
|
onProtectionViolation: (violationType: string) => {
|
||||||
console.warn(`Protection violation on justified photo ${photo.id}: ${violationType}`);
|
console.warn(`Protection violation on justified photo ${photo.id}: ${violationType}`);
|
||||||
},
|
},
|
||||||
@@ -214,9 +209,7 @@ export const JustifiedGalleryLayout: React.FC<JustifiedGalleryLayoutProps> = ({
|
|||||||
isSelectionMode = false,
|
isSelectionMode = false,
|
||||||
onPhotoSelect,
|
onPhotoSelect,
|
||||||
allowDownloads = true,
|
allowDownloads = true,
|
||||||
protectionLevel = 'standard',
|
|
||||||
useEnhancedProtection = false,
|
useEnhancedProtection = false,
|
||||||
useCanvasRendering = false,
|
|
||||||
feedbackEnabled = false,
|
feedbackEnabled = false,
|
||||||
feedbackOptions,
|
feedbackOptions,
|
||||||
// Hero props
|
// Hero props
|
||||||
@@ -399,7 +392,6 @@ export const JustifiedGalleryLayout: React.FC<JustifiedGalleryLayoutProps> = ({
|
|||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
isGallery={true}
|
isGallery={true}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Overlay */}
|
{/* Overlay */}
|
||||||
@@ -523,9 +515,7 @@ export const JustifiedGalleryLayout: React.FC<JustifiedGalleryLayoutProps> = ({
|
|||||||
animationType={animation}
|
animationType={animation}
|
||||||
allowDownloads={allowDownloads}
|
allowDownloads={allowDownloads}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
protectionLevel={protectionLevel}
|
|
||||||
useEnhancedProtection={useEnhancedProtection}
|
useEnhancedProtection={useEnhancedProtection}
|
||||||
useCanvasRendering={useCanvasRendering}
|
|
||||||
feedbackEnabled={feedbackEnabled}
|
feedbackEnabled={feedbackEnabled}
|
||||||
feedbackOptions={feedbackOptions}
|
feedbackOptions={feedbackOptions}
|
||||||
savedIdentity={savedIdentity}
|
savedIdentity={savedIdentity}
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ interface StoryCarouselProps {
|
|||||||
slug: string;
|
slug: string;
|
||||||
id: string;
|
id: string;
|
||||||
allowDownloads?: boolean;
|
allowDownloads?: boolean;
|
||||||
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
|
||||||
useEnhancedProtection?: boolean;
|
useEnhancedProtection?: boolean;
|
||||||
useCanvasRendering?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StoryCarousel: React.FC<StoryCarouselProps> = ({
|
export const StoryCarousel: React.FC<StoryCarouselProps> = ({
|
||||||
@@ -27,9 +25,7 @@ export const StoryCarousel: React.FC<StoryCarouselProps> = ({
|
|||||||
slug,
|
slug,
|
||||||
id,
|
id,
|
||||||
allowDownloads = true,
|
allowDownloads = true,
|
||||||
protectionLevel = 'standard',
|
|
||||||
useEnhancedProtection = false,
|
useEnhancedProtection = false,
|
||||||
useCanvasRendering = false
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div id={id} className="story-carousel">
|
<div id={id} className="story-carousel">
|
||||||
@@ -53,9 +49,7 @@ export const StoryCarousel: React.FC<StoryCarouselProps> = ({
|
|||||||
slug={slug}
|
slug={slug}
|
||||||
galleryId={id}
|
galleryId={id}
|
||||||
allowDownloads={allowDownloads}
|
allowDownloads={allowDownloads}
|
||||||
protectionLevel={protectionLevel}
|
|
||||||
useEnhancedProtection={useEnhancedProtection}
|
useEnhancedProtection={useEnhancedProtection}
|
||||||
useCanvasRendering={useCanvasRendering}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ interface StoryHeroProps {
|
|||||||
photo?: Photo | null;
|
photo?: Photo | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
allowDownloads?: boolean;
|
allowDownloads?: boolean;
|
||||||
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
|
||||||
useEnhancedProtection?: boolean;
|
useEnhancedProtection?: boolean;
|
||||||
useCanvasRendering?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StoryHero: React.FC<StoryHeroProps> = ({
|
export const StoryHero: React.FC<StoryHeroProps> = ({
|
||||||
@@ -22,8 +20,6 @@ export const StoryHero: React.FC<StoryHeroProps> = ({
|
|||||||
stats,
|
stats,
|
||||||
photo,
|
photo,
|
||||||
slug,
|
slug,
|
||||||
protectionLevel = 'standard',
|
|
||||||
useCanvasRendering = false
|
|
||||||
}) => {
|
}) => {
|
||||||
const formattedDate = date
|
const formattedDate = date
|
||||||
? new Date(date).toLocaleDateString('en-US', {
|
? new Date(date).toLocaleDateString('en-US', {
|
||||||
@@ -49,7 +45,6 @@ export const StoryHero: React.FC<StoryHeroProps> = ({
|
|||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
isGallery={true}
|
isGallery={true}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full bg-gray-900" />
|
<div className="w-full h-full bg-gray-900" />
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ interface StoryPhotoCardProps {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
slug: string;
|
slug: string;
|
||||||
allowDownloads?: boolean;
|
allowDownloads?: boolean;
|
||||||
protectionLevel?: 'basic' | 'standard' | 'enhanced' | 'maximum';
|
|
||||||
useEnhancedProtection?: boolean;
|
useEnhancedProtection?: boolean;
|
||||||
useCanvasRendering?: boolean;
|
|
||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
galleryId: string;
|
galleryId: string;
|
||||||
}
|
}
|
||||||
@@ -28,8 +26,6 @@ export const StoryPhotoCard: React.FC<StoryPhotoCardProps> = ({
|
|||||||
onToggleFavorite,
|
onToggleFavorite,
|
||||||
onClick,
|
onClick,
|
||||||
slug,
|
slug,
|
||||||
protectionLevel = 'standard',
|
|
||||||
useCanvasRendering = false,
|
|
||||||
featured = false,
|
featured = false,
|
||||||
galleryId: _galleryId
|
galleryId: _galleryId
|
||||||
}) => {
|
}) => {
|
||||||
@@ -103,7 +99,6 @@ export const StoryPhotoCard: React.FC<StoryPhotoCardProps> = ({
|
|||||||
}`}
|
}`}
|
||||||
isGallery={true}
|
isGallery={true}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
useCanvasRendering={useCanvasRendering || protectionLevel === 'maximum'}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export const ImageSecurityTab: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<Image className="w-4 h-4 ml-2 mr-1 text-neutral-500" />
|
<Image className="w-4 h-4 ml-2 mr-1 text-neutral-500" />
|
||||||
<span className="text-sm text-neutral-700 dark:text-neutral-300">
|
<span className="text-sm text-neutral-700 dark:text-neutral-300">
|
||||||
{t('settings.imageSecurity.enableCanvas', 'Enable canvas rendering by default (advanced protection)')}
|
{t('settings.imageSecurity.enableCanvas', 'Enable canvas rendering in the lightbox by default (advanced protection)')}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1934,7 +1934,7 @@
|
|||||||
"disableRightClick": "Rechtsklick-Menü blockieren",
|
"disableRightClick": "Rechtsklick-Menü blockieren",
|
||||||
"watermarkDownloads": "Wasserzeichen bei Downloads hinzufügen",
|
"watermarkDownloads": "Wasserzeichen bei Downloads hinzufügen",
|
||||||
"enableDevtoolsProtection": "Entwicklertools erkennen",
|
"enableDevtoolsProtection": "Entwicklertools erkennen",
|
||||||
"useCanvasRendering": "Canvas-Rendering (erweiterter Schutz)",
|
"useCanvasRendering": "Canvas-Rendering in der Lightbox (erweiterter Schutz)",
|
||||||
"protectionInfo": "Schutzfunktionen helfen, unerlaubte Downloads zu verhindern, können jedoch nicht alle Methoden blockieren.",
|
"protectionInfo": "Schutzfunktionen helfen, unerlaubte Downloads zu verhindern, können jedoch nicht alle Methoden blockieren.",
|
||||||
"protectionLevelBasic": "Einfach - Nur Rechtsklick-Sperre",
|
"protectionLevelBasic": "Einfach - Nur Rechtsklick-Sperre",
|
||||||
"protectionLevelStandard": "Standard - Tastenkombinationen blockiert",
|
"protectionLevelStandard": "Standard - Tastenkombinationen blockiert",
|
||||||
@@ -2374,7 +2374,7 @@
|
|||||||
"protectionLevel": "Standard-Schutzstufe",
|
"protectionLevel": "Standard-Schutzstufe",
|
||||||
"imageQuality": "Standard-Bildqualität",
|
"imageQuality": "Standard-Bildqualität",
|
||||||
"enableDevtools": "DevTools-Erkennung standardmäßig aktivieren",
|
"enableDevtools": "DevTools-Erkennung standardmäßig aktivieren",
|
||||||
"enableCanvas": "Canvas-Rendering standardmäßig aktivieren (erweiterter Schutz)",
|
"enableCanvas": "Canvas-Rendering in der Lightbox standardmäßig aktivieren (erweiterter Schutz)",
|
||||||
"rateLimiting": "Ratenbegrenzung",
|
"rateLimiting": "Ratenbegrenzung",
|
||||||
"rateLimitingHelp": "Begrenzen Sie, wie viele Bilder angefordert werden können, um Scraping zu verhindern.",
|
"rateLimitingHelp": "Begrenzen Sie, wie viele Bilder angefordert werden können, um Scraping zu verhindern.",
|
||||||
"requestsPerMinute": "Anfragen pro Minute",
|
"requestsPerMinute": "Anfragen pro Minute",
|
||||||
|
|||||||
@@ -1430,7 +1430,7 @@
|
|||||||
"disableRightClick": "Block right-click menu",
|
"disableRightClick": "Block right-click menu",
|
||||||
"watermarkDownloads": "Add watermark to downloads",
|
"watermarkDownloads": "Add watermark to downloads",
|
||||||
"enableDevtoolsProtection": "Detect developer tools",
|
"enableDevtoolsProtection": "Detect developer tools",
|
||||||
"useCanvasRendering": "Canvas rendering (advanced protection)",
|
"useCanvasRendering": "Canvas rendering in the lightbox (advanced protection)",
|
||||||
"protectionInfo": "Protection features help prevent unauthorized downloads but cannot block all methods.",
|
"protectionInfo": "Protection features help prevent unauthorized downloads but cannot block all methods.",
|
||||||
"protectionLevelBasic": "Basic - Right-click blocking only",
|
"protectionLevelBasic": "Basic - Right-click blocking only",
|
||||||
"protectionLevelStandard": "Standard - Keyboard shortcuts blocked",
|
"protectionLevelStandard": "Standard - Keyboard shortcuts blocked",
|
||||||
@@ -1937,7 +1937,7 @@
|
|||||||
"protectionLevel": "Default Protection Level",
|
"protectionLevel": "Default Protection Level",
|
||||||
"imageQuality": "Default Image Quality",
|
"imageQuality": "Default Image Quality",
|
||||||
"enableDevtools": "Enable DevTools detection by default",
|
"enableDevtools": "Enable DevTools detection by default",
|
||||||
"enableCanvas": "Enable canvas rendering by default (advanced protection)",
|
"enableCanvas": "Enable canvas rendering in the lightbox by default (advanced protection)",
|
||||||
"rateLimiting": "Rate Limiting",
|
"rateLimiting": "Rate Limiting",
|
||||||
"rateLimitingHelp": "Limit how many images can be requested to prevent scraping.",
|
"rateLimitingHelp": "Limit how many images can be requested to prevent scraping.",
|
||||||
"requestsPerMinute": "Requests per minute",
|
"requestsPerMinute": "Requests per minute",
|
||||||
|
|||||||
@@ -498,7 +498,7 @@
|
|||||||
"disableRightClick": "Bloquer le clic droit",
|
"disableRightClick": "Bloquer le clic droit",
|
||||||
"watermarkDownloads": "Ajouter un filigrane aux téléchargements",
|
"watermarkDownloads": "Ajouter un filigrane aux téléchargements",
|
||||||
"enableDevtoolsProtection": "Détecter les outils de développement",
|
"enableDevtoolsProtection": "Détecter les outils de développement",
|
||||||
"useCanvasRendering": "Rendu canvas (protection avancée)",
|
"useCanvasRendering": "Rendu canvas dans la visionneuse (protection avancée)",
|
||||||
"protectionInfo": "Les fonctionnalités de protection aident à prévenir les téléchargements non autorisés mais ne peuvent pas bloquer toutes les méthodes.",
|
"protectionInfo": "Les fonctionnalités de protection aident à prévenir les téléchargements non autorisés mais ne peuvent pas bloquer toutes les méthodes.",
|
||||||
"protectionLevelBasic": "Basique - Blocage du clic droit uniquement",
|
"protectionLevelBasic": "Basique - Blocage du clic droit uniquement",
|
||||||
"protectionLevelStandard": "Standard - Blocage des raccourcis clavier",
|
"protectionLevelStandard": "Standard - Blocage des raccourcis clavier",
|
||||||
@@ -912,7 +912,7 @@
|
|||||||
"protectionLevel": "Niveau de protection par défaut",
|
"protectionLevel": "Niveau de protection par défaut",
|
||||||
"imageQuality": "Qualité d'image par défaut",
|
"imageQuality": "Qualité d'image par défaut",
|
||||||
"enableDevtools": "Activer la détection des outils DevTools par défaut",
|
"enableDevtools": "Activer la détection des outils DevTools par défaut",
|
||||||
"enableCanvas": "Activer le rendu canvas par défaut (protection avancée)",
|
"enableCanvas": "Activer le rendu canvas dans la visionneuse par défaut (protection avancée)",
|
||||||
"rateLimiting": "Limitation de débit",
|
"rateLimiting": "Limitation de débit",
|
||||||
"rateLimitingHelp": "Limitez le nombre d'images qui peuvent être demandées pour éviter le scraping.",
|
"rateLimitingHelp": "Limitez le nombre d'images qui peuvent être demandées pour éviter le scraping.",
|
||||||
"requestsPerMinute": "Requêtes par minute",
|
"requestsPerMinute": "Requêtes par minute",
|
||||||
|
|||||||
@@ -498,7 +498,7 @@
|
|||||||
"disableRightClick": "Blokiraj meni desnega klika",
|
"disableRightClick": "Blokiraj meni desnega klika",
|
||||||
"watermarkDownloads": "Dodaj vodni žig prenosom",
|
"watermarkDownloads": "Dodaj vodni žig prenosom",
|
||||||
"enableDevtoolsProtection": "Zaznaj orodja za razvijalce",
|
"enableDevtoolsProtection": "Zaznaj orodja za razvijalce",
|
||||||
"useCanvasRendering": "Izris prek canvas (napredna zaščita)",
|
"useCanvasRendering": "Izris prek canvas v pregledovalniku (napredna zaščita)",
|
||||||
"protectionInfo": "Zaščitne funkcije pomagajo preprečiti nepooblaščene prenose, vendar ne morejo blokirati vseh metod.",
|
"protectionInfo": "Zaščitne funkcije pomagajo preprečiti nepooblaščene prenose, vendar ne morejo blokirati vseh metod.",
|
||||||
"protectionLevelBasic": "Osnovno - samo blokada desnega klika",
|
"protectionLevelBasic": "Osnovno - samo blokada desnega klika",
|
||||||
"protectionLevelStandard": "Standardno - blokirane bližnjice na tipkovnici",
|
"protectionLevelStandard": "Standardno - blokirane bližnjice na tipkovnici",
|
||||||
@@ -912,7 +912,7 @@
|
|||||||
"protectionLevel": "Privzeta raven zaščite",
|
"protectionLevel": "Privzeta raven zaščite",
|
||||||
"imageQuality": "Privzeta kakovost slike",
|
"imageQuality": "Privzeta kakovost slike",
|
||||||
"enableDevtools": "Privzeto omogoči zaznavanje DevTools",
|
"enableDevtools": "Privzeto omogoči zaznavanje DevTools",
|
||||||
"enableCanvas": "Privzeto omogoči izris prek canvas (napredna zaščita)",
|
"enableCanvas": "Privzeto omogoči izris prek canvas v pregledovalniku (napredna zaščita)",
|
||||||
"rateLimiting": "Omejevanje hitrosti",
|
"rateLimiting": "Omejevanje hitrosti",
|
||||||
"rateLimitingHelp": "Omejite, koliko slik je mogoče zahtevati, da preprečite množično pobiranje.",
|
"rateLimitingHelp": "Omejite, koliko slik je mogoče zahtevati, da preprečite množično pobiranje.",
|
||||||
"requestsPerMinute": "Zahtev na minuto",
|
"requestsPerMinute": "Zahtev na minuto",
|
||||||
|
|||||||
@@ -669,7 +669,7 @@ export const EventInformationCard: React.FC<EventInformationCardProps> = ({
|
|||||||
className="w-4 h-4 text-accent border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
|
className="w-4 h-4 text-accent border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
|
||||||
/>
|
/>
|
||||||
<Image className="w-4 h-4 ml-2 mr-1 text-neutral-500 dark:text-neutral-400" />
|
<Image className="w-4 h-4 ml-2 mr-1 text-neutral-500 dark:text-neutral-400" />
|
||||||
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('events.useCanvasRendering', 'Canvas rendering (advanced protection)')}</span>
|
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('events.useCanvasRendering', 'Canvas rendering in the lightbox (advanced protection)')}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
|
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user