mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
251 lines
12 KiB
TypeScript
251 lines
12 KiB
TypeScript
import { GetRoomCameraWidgetManager, IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, NitroLogger, NitroTexture, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
|
|
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { FaSave, FaSearchMinus, FaSearchPlus, FaTrash } from 'react-icons/fa';
|
|
import { CameraEditorTabs, CameraPicture, CameraPictureThumbnail, LocalizeText } from '../../../../api';
|
|
import { Button, Column, Flex, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Slider, Text } from '../../../../common';
|
|
import { CameraWidgetEffectListView } from './effect-list';
|
|
|
|
export interface CameraWidgetEditorViewProps {
|
|
picture: CameraPicture;
|
|
availableEffects: IRoomCameraWidgetEffect[];
|
|
myLevel: number;
|
|
onClose: () => void;
|
|
onCancel: () => void;
|
|
onCheckout: (pictureUrl: string) => void;
|
|
}
|
|
|
|
const TABS: string[] = [ CameraEditorTabs.COLORMATRIX, CameraEditorTabs.COMPOSITE ];
|
|
|
|
export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props => {
|
|
const { picture = null, availableEffects = null, myLevel = 1, onClose = null, onCancel = null, onCheckout = null } = props;
|
|
const [ currentTab, setCurrentTab ] = useState(TABS[0]);
|
|
const [ selectedEffectName, setSelectedEffectName ] = useState<string>(null);
|
|
const [ selectedEffects, setSelectedEffects ] = useState<IRoomCameraWidgetSelectedEffect[]>([]);
|
|
const [ effectsThumbnails, setEffectsThumbnails ] = useState<CameraPictureThumbnail[]>([]);
|
|
const [ isZoomed, setIsZoomed ] = useState(false);
|
|
const [ currentPictureUrl, setCurrentPictureUrl ] = useState<string>(picture?.imageUrl ?? '');
|
|
const [ stableTexture, setStableTexture ] = useState<NitroTexture>(null);
|
|
const debounceTimerRef = useRef<ReturnType<typeof setTimeout>>(null);
|
|
const requestIdRef = useRef<number>(0);
|
|
|
|
useEffect(() =>
|
|
{
|
|
const img = new Image();
|
|
img.onload = () => setStableTexture(NitroTexture.from(img));
|
|
img.src = picture.imageUrl;
|
|
}, [ picture ]);
|
|
|
|
const getColorMatrixEffects = useMemo(() => {
|
|
return availableEffects.filter(effect => effect.colorMatrix);
|
|
}, [ availableEffects ]);
|
|
|
|
const getCompositeEffects = useMemo(() => {
|
|
return availableEffects.filter(effect => effect.texture);
|
|
}, [ availableEffects ]);
|
|
|
|
const getEffectList = useCallback(() => {
|
|
return currentTab === CameraEditorTabs.COLORMATRIX ? getColorMatrixEffects : getCompositeEffects;
|
|
}, [ currentTab, getColorMatrixEffects, getCompositeEffects ]);
|
|
|
|
const getSelectedEffectIndex = useCallback((name: string) => {
|
|
if (!name || !name.length || !selectedEffects || !selectedEffects.length) return -1;
|
|
return selectedEffects.findIndex(effect => effect.effect.name === name);
|
|
}, [ selectedEffects ]);
|
|
|
|
const getCurrentEffectIndex = useMemo(() => {
|
|
return getSelectedEffectIndex(selectedEffectName);
|
|
}, [ selectedEffectName, getSelectedEffectIndex ]);
|
|
|
|
const getCurrentEffect = useMemo(() => {
|
|
if (!selectedEffectName) return null;
|
|
return selectedEffects[getCurrentEffectIndex] || null;
|
|
}, [ selectedEffectName, getCurrentEffectIndex, selectedEffects ]);
|
|
|
|
const setSelectedEffectAlpha = useCallback((alpha: number) => {
|
|
const index = getCurrentEffectIndex;
|
|
if (index === -1) return;
|
|
|
|
setSelectedEffects(prevValue => {
|
|
const clone = [ ...prevValue ];
|
|
const currentEffect = clone[index];
|
|
clone[index] = new RoomCameraWidgetSelectedEffect(currentEffect.effect, alpha);
|
|
return clone;
|
|
});
|
|
}, [ getCurrentEffectIndex ]);
|
|
|
|
const processAction = useCallback((type: string, effectName: string = null) => {
|
|
switch (type) {
|
|
case 'close':
|
|
onClose();
|
|
return;
|
|
case 'cancel':
|
|
onCancel();
|
|
return;
|
|
case 'checkout':
|
|
onCheckout(currentPictureUrl);
|
|
return;
|
|
case 'change_tab':
|
|
setCurrentTab(String(effectName));
|
|
return;
|
|
case 'select_effect': {
|
|
const existingIndex = getSelectedEffectIndex(effectName);
|
|
if (existingIndex >= 0) return;
|
|
|
|
const effect = availableEffects.find(effect => effect.name === effectName);
|
|
if (!effect) return;
|
|
|
|
setSelectedEffects(prevValue => [ ...prevValue, new RoomCameraWidgetSelectedEffect(effect, 1) ]);
|
|
setSelectedEffectName(effect.name);
|
|
return;
|
|
}
|
|
case 'remove_effect': {
|
|
const existingIndex = getSelectedEffectIndex(effectName);
|
|
if (existingIndex === -1) return;
|
|
|
|
setSelectedEffects(prevValue => {
|
|
const clone = [ ...prevValue ];
|
|
clone.splice(existingIndex, 1);
|
|
return clone;
|
|
});
|
|
|
|
if (selectedEffectName === effectName) setSelectedEffectName(null);
|
|
return;
|
|
}
|
|
case 'clear_effects':
|
|
onCancel();
|
|
return;
|
|
case 'download': {
|
|
if(!currentPictureUrl) return;
|
|
|
|
const parts = currentPictureUrl.split(',');
|
|
const mime = parts[0].match(/:(.*?);/)?.[1] || 'image/png';
|
|
const binary = atob(parts[1]);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for(let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
const blob = new Blob([ bytes ], { type: mime });
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
|
|
const w = window.open('', '_blank');
|
|
if(w)
|
|
{
|
|
w.document.title = 'camera_photo.png';
|
|
w.document.body.style.margin = '0';
|
|
w.document.body.innerHTML = `<img src="${ blobUrl }" style="max-width:100%"/>`;
|
|
}
|
|
|
|
return;
|
|
}
|
|
case 'zoom':
|
|
setIsZoomed(prev => !prev);
|
|
return;
|
|
}
|
|
}, [ availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose ]);
|
|
|
|
useEffect(() => {
|
|
if(!stableTexture) return;
|
|
|
|
const processThumbnails = async () => {
|
|
const renderedEffects = await Promise.all(
|
|
availableEffects.map(effect =>
|
|
GetRoomCameraWidgetManager().applyEffects(stableTexture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false)
|
|
)
|
|
);
|
|
setEffectsThumbnails(renderedEffects.map((image, index) => new CameraPictureThumbnail(availableEffects[index].name, image.src)));
|
|
};
|
|
processThumbnails();
|
|
}, [ stableTexture, availableEffects ]);
|
|
|
|
useEffect(() => {
|
|
if(!stableTexture) return;
|
|
|
|
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
|
|
debounceTimerRef.current = setTimeout(() => {
|
|
const id = ++requestIdRef.current;
|
|
|
|
GetRoomCameraWidgetManager()
|
|
.applyEffects(stableTexture, selectedEffects, false)
|
|
.then(imageElement => {
|
|
if (id !== requestIdRef.current) return;
|
|
setCurrentPictureUrl(imageElement.src);
|
|
})
|
|
.catch(error => NitroLogger.error('Failed to apply effects to picture', error));
|
|
}, 50);
|
|
|
|
return () => {
|
|
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
};
|
|
}, [ stableTexture, selectedEffects ]);
|
|
|
|
return (
|
|
<NitroCardView className="w-[600px] h-[500px]">
|
|
<NitroCardHeaderView headerText={ LocalizeText('camera.editor.button.text') } onCloseClick={ event => processAction('close') } />
|
|
<NitroCardTabsView>
|
|
{ TABS.map(tab => (
|
|
<NitroCardTabsItemView key={ tab } isActive={ currentTab === tab } onClick={ event => processAction('change_tab', tab) }>
|
|
<i className={ 'nitro-icon icon-camera-' + tab }></i>
|
|
</NitroCardTabsItemView>
|
|
)) }
|
|
</NitroCardTabsView>
|
|
<NitroCardContentView>
|
|
<Grid>
|
|
<Column overflow="hidden" size={ 5 }>
|
|
<CameraWidgetEffectListView
|
|
myLevel={ myLevel }
|
|
selectedEffects={ selectedEffects }
|
|
effects={ getEffectList() }
|
|
thumbnails={ effectsThumbnails }
|
|
processAction={ processAction }
|
|
/>
|
|
</Column>
|
|
<Column justifyContent="between" overflow="hidden" size={ 7 }>
|
|
<Column center>
|
|
<div className="w-[325px] h-[325px] overflow-hidden">
|
|
{ currentPictureUrl && <img
|
|
alt=""
|
|
src={ currentPictureUrl }
|
|
className="w-[325px] h-[325px] [image-rendering:pixelated]"
|
|
style={ isZoomed ? { transform: 'scale(2)', transformOrigin: 'center' } : undefined }
|
|
/> }
|
|
</div>
|
|
{ selectedEffectName && (
|
|
<Column center fullWidth gap={ 1 }>
|
|
<Text>{ LocalizeText('camera.effect.name.' + selectedEffectName) }</Text>
|
|
<Slider
|
|
min={ 0 }
|
|
max={ 100 }
|
|
step={ 1 }
|
|
value={ Math.round(getCurrentEffect.strength * 100) }
|
|
onChange={ event => setSelectedEffectAlpha(event / 100) }
|
|
renderThumb={ ({ key, ...props }, state) => <div key={ key } { ...props }>{ state.valueNow }</div> }
|
|
/>
|
|
</Column>
|
|
) }
|
|
</Column>
|
|
<div className="flex justify-between">
|
|
<div className="relative inline-flex align-middle">
|
|
<Button onClick={ event => processAction('clear_effects') }>
|
|
<FaTrash className="fa-icon" />
|
|
</Button>
|
|
<Button onClick={ event => processAction('download') }>
|
|
<FaSave className="fa-icon" />
|
|
</Button>
|
|
<Button onClick={ event => processAction('zoom') }>
|
|
{ isZoomed ? <FaSearchMinus className="fa-icon" /> : <FaSearchPlus className="fa-icon" /> }
|
|
</Button>
|
|
</div>
|
|
<div className="flex gap-1">
|
|
<Button onClick={ event => processAction('cancel') }>
|
|
{ LocalizeText('generic.cancel') }
|
|
</Button>
|
|
<Button onClick={ event => processAction('checkout') }>
|
|
{ LocalizeText('camera.preview.button.text') }
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Column>
|
|
</Grid>
|
|
</NitroCardContentView>
|
|
</NitroCardView>
|
|
);
|
|
}; |