🆙 Fix the Zoom / Effect and layout

This commit is contained in:
DuckieTM 2025-03-18 19:35:43 +01:00
parent bcbbaf5944
commit 512ead3543
4 changed files with 143 additions and 149 deletions

View File

@ -1,71 +1,68 @@
import { GetRoomEngine, RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'; import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
import { GetUserProfile, IPhotoData, LocalizeText } from '../../../api'; import { GetUserProfile, IPhotoData, LocalizeText } from '../../../api';
import { Flex, Grid, Text } from '../../../common'; import { Flex, Grid, Text } from '../../../common';
export interface CameraWidgetShowPhotoViewProps export interface CameraWidgetShowPhotoViewProps {
{
currentIndex: number; currentIndex: number;
currentPhotos: IPhotoData[]; currentPhotos: IPhotoData[];
onClick?: () => void;
} }
export const CameraWidgetShowPhotoView: FC<CameraWidgetShowPhotoViewProps> = props => export const CameraWidgetShowPhotoView: FC<CameraWidgetShowPhotoViewProps> = props => {
{ const { currentIndex = -1, currentPhotos = null, onClick = null } = props;
const { currentIndex = -1, currentPhotos = null } = props; const [imageIndex, setImageIndex] = useState(0);
const [ imageIndex, setImageIndex ] = useState(0);
const currentImage = (currentPhotos && currentPhotos.length) ? currentPhotos[imageIndex] : null; const currentImage = currentPhotos && currentPhotos.length ? currentPhotos[imageIndex] : null;
const next = () =>
{
setImageIndex(prevValue =>
{
let newIndex = (prevValue + 1);
if(newIndex >= currentPhotos.length) newIndex = 0;
const next = () => {
setImageIndex(prevValue => {
let newIndex = prevValue + 1;
if (newIndex >= currentPhotos.length) newIndex = 0;
return newIndex; return newIndex;
}); });
}; };
const previous = () => const previous = () => {
{ setImageIndex(prevValue => {
setImageIndex(prevValue => let newIndex = prevValue - 1;
{ if (newIndex < 0) newIndex = currentPhotos.length - 1;
let newIndex = (prevValue - 1);
if(newIndex < 0) newIndex = (currentPhotos.length - 1);
return newIndex; return newIndex;
}); });
}; };
useEffect(() => useEffect(() => {
{
setImageIndex(currentIndex); setImageIndex(currentIndex);
}, [ currentIndex ]); }, [currentIndex]);
if(!currentImage) return null; if (!currentImage) return null;
const getUserData = (roomId: number, objectId: number, type: string): number | string =>
{
const roomObject = GetRoomEngine().getRoomObject(roomId, objectId, RoomObjectCategory.WALL);
if (!roomObject) return;
return type == 'username' ? roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_NAME) : roomObject.model.getValue<number>(RoomObjectVariable.FURNITURE_OWNER_ID);
}
return ( return (
<Grid style={ { display: 'flex', flexDirection: 'column' } }> <Grid style={{ display: 'flex', flexDirection: 'column' }}>
<Flex center className="picture-preview border border-black" style={ currentImage.w ? { backgroundImage: 'url(' + currentImage.w + ')' } : {} }> <Flex center className="picture-preview border border-black" style={currentImage.w ? { backgroundImage: 'url(' + currentImage.w + ')' } : {}} onClick={onClick}>
{ !currentImage.w && {!currentImage.w && <Text bold>{LocalizeText('camera.loading')}</Text>}
<Text bold>{ LocalizeText('camera.loading') }</Text> }
</Flex> </Flex>
{ currentImage.m && currentImage.m.length && {currentImage.m && currentImage.m.length && <Text center>{currentImage.m}</Text>}
<Text center>{ currentImage.m }</Text> } <div className="flex items-center center justify-between">
<div className="flex items-center justify-between"> <Text>{currentImage.n || ''}</Text>
<Text>{ (currentImage.n || '') }</Text> <Text onClick={() => GetUserProfile(Number(getUserData(currentImage.s, Number(currentImage.u), 'id')))}> { getUserData(currentImage.s, Number(currentImage.u), 'username') } </Text>
<Text>{ new Date(currentImage.t * 1000).toLocaleDateString() }</Text> <Text className="cursor-pointer" onClick={() => GetUserProfile(currentImage.oi)}>{currentImage.o}</Text>
<Text>{new Date(currentImage.t * 1000).toLocaleDateString()}</Text>
</div> </div>
{ (currentPhotos.length > 1) && {currentPhotos.length > 1 && (
<Flex className="picture-preview-buttons"> <Flex className="picture-preview-buttons">
<FaArrowLeft className="cursor-pointer picture-preview-buttons-previous fa-icon" onClick={ previous } /> <FaArrowLeft onClick={previous} />
<Text underline className="cursor-pointer" onClick={ event => GetUserProfile(currentImage.oi) }>{ currentImage.o }</Text> <FaArrowRight className="cursor-pointer"onClick={next} />
<FaArrowRight className="cursor-pointer picture-preview-buttons-next fa-icon" onClick={ next } />
</Flex> </Flex>
} )}
</Grid> </Grid>
); );
}; };

View File

@ -3,11 +3,10 @@ import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FaSave, FaSearchMinus, FaSearchPlus, FaTrash } from 'react-icons/fa'; import { FaSave, FaSearchMinus, FaSearchPlus, FaTrash } from 'react-icons/fa';
import ReactSlider from 'react-slider'; import ReactSlider from 'react-slider';
import { CameraEditorTabs, CameraPicture, CameraPictureThumbnail, LocalizeText } from '../../../../api'; import { CameraEditorTabs, CameraPicture, CameraPictureThumbnail, LocalizeText } from '../../../../api';
import { Button, ButtonGroup, Column, Flex, Grid, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../../../common'; import { Button, Column, Flex, Grid, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, Text } from '../../../../common';
import { CameraWidgetEffectListView } from './effect-list'; import { CameraWidgetEffectListView } from './effect-list';
export interface CameraWidgetEditorViewProps export interface CameraWidgetEditorViewProps {
{
picture: CameraPicture; picture: CameraPicture;
availableEffects: IRoomCameraWidgetEffect[]; availableEffects: IRoomCameraWidgetEffect[];
myLevel: number; myLevel: number;
@ -18,8 +17,7 @@ export interface CameraWidgetEditorViewProps
const TABS: string[] = [ CameraEditorTabs.COLORMATRIX, CameraEditorTabs.COMPOSITE ]; const TABS: string[] = [ CameraEditorTabs.COLORMATRIX, CameraEditorTabs.COMPOSITE ];
export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props => export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props => {
{
const { picture = null, availableEffects = null, myLevel = 1, onClose = null, onCancel = null, onCheckout = null } = props; const { picture = null, availableEffects = null, myLevel = 1, onClose = null, onCancel = null, onCheckout = null } = props;
const [ currentTab, setCurrentTab ] = useState(TABS[0]); const [ currentTab, setCurrentTab ] = useState(TABS[0]);
const [ selectedEffectName, setSelectedEffectName ] = useState<string>(null); const [ selectedEffectName, setSelectedEffectName ] = useState<string>(null);
@ -29,66 +27,46 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
const [ currentPictureUrl, setCurrentPictureUrl ] = useState<string>(''); const [ currentPictureUrl, setCurrentPictureUrl ] = useState<string>('');
const isBusy = useRef<boolean>(false); const isBusy = useRef<boolean>(false);
const getColorMatrixEffects = useMemo(() => const getColorMatrixEffects = useMemo(() => {
{
return availableEffects.filter(effect => effect.colorMatrix); return availableEffects.filter(effect => effect.colorMatrix);
}, [ availableEffects ]); }, [ availableEffects ]);
const getCompositeEffects = useMemo(() => const getCompositeEffects = useMemo(() => {
{
return availableEffects.filter(effect => effect.texture); return availableEffects.filter(effect => effect.texture);
}, [ availableEffects ]); }, [ availableEffects ]);
const getEffectList = useCallback(() => const getEffectList = useCallback(() => {
{ return currentTab === CameraEditorTabs.COLORMATRIX ? getColorMatrixEffects : getCompositeEffects;
if(currentTab === CameraEditorTabs.COLORMATRIX)
{
return getColorMatrixEffects;
}
return getCompositeEffects;
}, [ currentTab, getColorMatrixEffects, getCompositeEffects ]); }, [ currentTab, getColorMatrixEffects, getCompositeEffects ]);
const getSelectedEffectIndex = useCallback((name: string) => const getSelectedEffectIndex = useCallback((name: string) => {
{ if (!name || !name.length || !selectedEffects || !selectedEffects.length) return -1;
if(!name || !name.length || !selectedEffects || !selectedEffects.length) return -1; return selectedEffects.findIndex(effect => effect.effect.name === name);
return selectedEffects.findIndex(effect => (effect.effect.name === name));
}, [ selectedEffects ]); }, [ selectedEffects ]);
const getCurrentEffectIndex = useMemo(() => const getCurrentEffectIndex = useMemo(() => {
{
return getSelectedEffectIndex(selectedEffectName); return getSelectedEffectIndex(selectedEffectName);
}, [ selectedEffectName, getSelectedEffectIndex ]); }, [ selectedEffectName, getSelectedEffectIndex ]);
const getCurrentEffect = useMemo(() => const getCurrentEffect = useMemo(() => {
{ if (!selectedEffectName) return null;
if(!selectedEffectName) return null; return selectedEffects[getCurrentEffectIndex] || null;
return (selectedEffects[getCurrentEffectIndex] || null);
}, [ selectedEffectName, getCurrentEffectIndex, selectedEffects ]); }, [ selectedEffectName, getCurrentEffectIndex, selectedEffects ]);
const setSelectedEffectAlpha = useCallback((alpha: number) => const setSelectedEffectAlpha = useCallback((alpha: number) => {
{
const index = getCurrentEffectIndex; const index = getCurrentEffectIndex;
if (index === -1) return;
if(index === -1) return; setSelectedEffects(prevValue => {
setSelectedEffects(prevValue =>
{
const clone = [ ...prevValue ]; const clone = [ ...prevValue ];
const currentEffect = clone[index]; const currentEffect = clone[index];
clone[index] = new RoomCameraWidgetSelectedEffect(currentEffect.effect, alpha);
clone[getCurrentEffectIndex] = new RoomCameraWidgetSelectedEffect(currentEffect.effect, alpha);
return clone; return clone;
}); });
}, [ getCurrentEffectIndex, setSelectedEffects ]); }, [ getCurrentEffectIndex ]);
const processAction = useCallback((type: string, effectName: string = null) => const processAction = useCallback((type: string, effectName: string = null) => {
{ switch (type) {
switch(type)
{
case 'close': case 'close':
onClose(); onClose();
return; return;
@ -102,37 +80,27 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
setCurrentTab(String(effectName)); setCurrentTab(String(effectName));
return; return;
case 'select_effect': { case 'select_effect': {
let existingIndex = getSelectedEffectIndex(effectName); const existingIndex = getSelectedEffectIndex(effectName);
if (existingIndex >= 0) return;
if(existingIndex >= 0) return; const effect = availableEffects.find(effect => effect.name === effectName);
if (!effect) return;
const effect = availableEffects.find(effect => (effect.name === effectName));
if(!effect) return;
setSelectedEffects(prevValue =>
{
return [ ...prevValue, new RoomCameraWidgetSelectedEffect(effect, 1) ];
});
setSelectedEffects(prevValue => [ ...prevValue, new RoomCameraWidgetSelectedEffect(effect, 1) ]);
setSelectedEffectName(effect.name); setSelectedEffectName(effect.name);
return; return;
} }
case 'remove_effect': { case 'remove_effect': {
let existingIndex = getSelectedEffectIndex(effectName); const existingIndex = getSelectedEffectIndex(effectName);
if (existingIndex === -1) return;
if(existingIndex === -1) return; setSelectedEffects(prevValue => {
setSelectedEffects(prevValue =>
{
const clone = [ ...prevValue ]; const clone = [ ...prevValue ];
clone.splice(existingIndex, 1); clone.splice(existingIndex, 1);
return clone; return clone;
}); });
if(selectedEffectName === effectName) setSelectedEffectName(null); if (selectedEffectName === effectName) setSelectedEffectName(null);
return; return;
} }
case 'clear_effects': case 'clear_effects':
@ -140,78 +108,90 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
setSelectedEffects([]); setSelectedEffects([]);
return; return;
case 'download': { case 'download': {
(async () => (async () => {
{
const image = new Image(); const image = new Image();
image.src = currentPictureUrl; image.src = currentPictureUrl;
const newWindow = window.open(''); const newWindow = window.open('');
newWindow.document.write(image.outerHTML); newWindow.document.write(image.outerHTML);
})(); })();
return; return;
} }
case 'zoom': case 'zoom':
setIsZoomed(!isZoomed); setIsZoomed(prev => !prev);
return; return;
} }
}, [ isZoomed, availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose, setIsZoomed, setSelectedEffects ]); }, [ availableEffects, selectedEffectName, currentPictureUrl, getSelectedEffectIndex, onCancel, onCheckout, onClose ]);
useEffect(() =>
{
const processThumbnails = async () =>
{
const renderedEffects = await Promise.all(availableEffects.map(effect => GetRoomCameraWidgetManager().applyEffects(picture.texture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false)));
useEffect(() => {
const processThumbnails = async () => {
const renderedEffects = await Promise.all(
availableEffects.map(effect =>
GetRoomCameraWidgetManager().applyEffects(picture.texture, [ new RoomCameraWidgetSelectedEffect(effect, 1) ], false)
)
);
setEffectsThumbnails(renderedEffects.map((image, index) => new CameraPictureThumbnail(availableEffects[index].name, image.src))); setEffectsThumbnails(renderedEffects.map((image, index) => new CameraPictureThumbnail(availableEffects[index].name, image.src)));
}; };
processThumbnails(); processThumbnails();
}, [ picture, availableEffects ]); }, [ picture, availableEffects ]);
useEffect(() => useEffect(() => {
{
GetRoomCameraWidgetManager() GetRoomCameraWidgetManager()
.applyEffects(picture.texture, selectedEffects, isZoomed) .applyEffects(picture.texture, selectedEffects, false) // Remove isZoomed from here
.then(imageElement => .then(imageElement => {
{
setCurrentPictureUrl(imageElement.src); setCurrentPictureUrl(imageElement.src);
}) })
.catch(error => .catch(error => {
{
NitroLogger.error('Failed to apply effects to picture', error); NitroLogger.error('Failed to apply effects to picture', error);
}); });
}, [ picture, selectedEffects, isZoomed ]); }, [ picture, selectedEffects ]); // Remove isZoomed from dependency array
return ( return (
<NitroCardView className="w-[600px] h-[500px]"> <NitroCardView className="w-[600px] h-[500px]">
<NitroCardHeaderView headerText={ LocalizeText('camera.editor.button.text') } onCloseClick={ event => processAction('close') } /> <NitroCardHeaderView headerText={ LocalizeText('camera.editor.button.text') } onCloseClick={ event => processAction('close') } />
<NitroCardTabsView> <NitroCardTabsView>
{ TABS.map(tab => { TABS.map(tab => (
{ <NitroCardTabsItemView key={ tab } isActive={ currentTab === tab } onClick={ event => processAction('change_tab', tab) }>
return <NitroCardTabsItemView key={ tab } isActive={ currentTab === tab } onClick={ event => processAction('change_tab', tab) }><i className={ 'nitro-icon icon-camera-' + tab }></i></NitroCardTabsItemView>; <i className={ 'nitro-icon icon-camera-' + tab }></i>
}) } </NitroCardTabsItemView>
)) }
</NitroCardTabsView> </NitroCardTabsView>
<NitroCardContentView> <NitroCardContentView>
<Grid> <Grid>
<Column overflow="hidden" size={ 5 }> <Column overflow="hidden" size={ 5 }>
<CameraWidgetEffectListView myLevel={ myLevel } selectedEffects={ selectedEffects } effects={ getEffectList() } thumbnails={ effectsThumbnails } processAction={ processAction } /> <CameraWidgetEffectListView
myLevel={ myLevel }
selectedEffects={ selectedEffects }
effects={ getEffectList() }
thumbnails={ effectsThumbnails }
processAction={ processAction }
/>
</Column> </Column>
<Column justifyContent="between" overflow="hidden" size={ 7 }> <Column justifyContent="between" overflow="hidden" size={ 7 }>
<Column center> <Column center>
<LayoutImage className="w-[320px] h-[320px]" imageUrl={ currentPictureUrl } /> <LayoutImage
{ selectedEffectName && style={{
width: '320px',
height: '320px',
backgroundImage: `url(${currentPictureUrl})`,
backgroundPosition: isZoomed ? 'center' : 'top left',
backgroundSize: isZoomed ? 'contain' : 'auto', // Zoom only affects display
backgroundRepeat: 'no-repeat'
}}
/>
{ selectedEffectName && (
<Column center fullWidth gap={ 1 }> <Column center fullWidth gap={ 1 }>
<Text>{ LocalizeText('camera.effect.name.' + selectedEffectName) }</Text> <Text>{ LocalizeText('camera.effect.name.' + selectedEffectName) }</Text>
<ReactSlider <ReactSlider
className={ 'nitro-slider' } className="nitro-slider"
min={ 0 } min={ 0 }
max={ 1 } max={ 1 }
step={ 0.01 } step={ 0.01 }
value={ getCurrentEffect.strength } value={ getCurrentEffect.strength }
onChange={ event => setSelectedEffectAlpha(event) } onChange={ event => setSelectedEffectAlpha(event) }
renderThumb={ ({ key, ...props }, state) => <div key={ key } { ...props }>{ state.valueNow }</div> } /> renderThumb={ ({ key, ...props }, state) => <div key={ key } { ...props }>{ state.valueNow }</div> }
</Column> } />
</Column>
) }
</Column> </Column>
<div className="flex justify-between"> <div className="flex justify-between">
<div className="relative inline-flex align-middle"> <div className="relative inline-flex align-middle">
@ -222,8 +202,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
<FaSave className="fa-icon" /> <FaSave className="fa-icon" />
</Button> </Button>
<Button onClick={ event => processAction('zoom') }> <Button onClick={ event => processAction('zoom') }>
{ isZoomed && <FaSearchMinus className="fa-icon" /> } { isZoomed ? <FaSearchMinus className="fa-icon" /> : <FaSearchPlus className="fa-icon" /> }
{ !isZoomed && <FaSearchPlus className="fa-icon" /> }
</Button> </Button>
</div> </div>
<div className="flex gap-1"> <div className="flex gap-1">
@ -240,4 +219,4 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );
}; };

View File

@ -1,23 +1,34 @@
import { GetSessionDataManager } from '@nitrots/nitro-renderer';
import { FC } from 'react'; import { FC } from 'react';
import { ReportType } from '../../../../api'; import { GetConfigurationValue, LocalizeText, ReportType } from '../../../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common'; import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../common';
import { useFurnitureExternalImageWidget, useHelp } from '../../../../hooks'; import { useFurnitureExternalImageWidget, useHelp } from '../../../../hooks';
import { CameraWidgetShowPhotoView } from '../../../camera/views/CameraWidgetShowPhotoView'; import { CameraWidgetShowPhotoView } from '../../../camera/views/CameraWidgetShowPhotoView';
export const FurnitureExternalImageView: FC<{}> = props => export const FurnitureExternalImageView: FC<{}> = props => {
{
const { objectId = -1, currentPhotoIndex = -1, currentPhotos = null, onClose = null } = useFurnitureExternalImageWidget(); const { objectId = -1, currentPhotoIndex = -1, currentPhotos = null, onClose = null } = useFurnitureExternalImageWidget();
const { report = null } = useHelp(); const { report = null } = useHelp();
if((objectId === -1) || (currentPhotoIndex === -1)) return null; if (objectId === -1 || currentPhotoIndex === -1) return null;
const handleOpenFullPhoto = () => {
const photoUrl = currentPhotos[currentPhotoIndex].w.replace('_small.png', '.png');
if (photoUrl) {
console.log("Opened photo URL:", photoUrl);
window.open(photoUrl, '_blank');
}
};
return ( return (
<NitroCardView className="nitro-external-image-widget" theme="primary-slim"> <NitroCardView className="nitro-external-image-widget no-resize" uniqueKey="photo-viewer" theme="primary-slim">
<NitroCardHeaderView headerText="" isGalleryPhoto={ true } onCloseClick={ onClose } onReportPhoto={ () => report(ReportType.PHOTO, { extraData: currentPhotos[currentPhotoIndex].w, roomId: currentPhotos[currentPhotoIndex].s, reportedUserId: GetSessionDataManager().userId, roomObjectId: Number(currentPhotos[currentPhotoIndex].u) }) } /> <NitroCardHeaderView
headerText={ LocalizeText('camera.interface.title') }
isGalleryPhoto={true}
onCloseClick={onClose}
onReportPhoto={() => report(ReportType.PHOTO, { extraData: currentPhotos[currentPhotoIndex].w, roomId: currentPhotos[currentPhotoIndex].s, reportedUserId: GetSessionDataManager().userId, roomObjectId: Number(currentPhotos[currentPhotoIndex].u) })}
/>
<NitroCardContentView> <NitroCardContentView>
<CameraWidgetShowPhotoView currentIndex={ currentPhotoIndex } currentPhotos={ currentPhotos } /> <CameraWidgetShowPhotoView currentIndex={currentPhotoIndex} currentPhotos={currentPhotos} onClick={handleOpenFullPhoto} />
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );
}; };

View File

@ -2,6 +2,14 @@
pointer-events: none; pointer-events: none;
} }
.no-resize {
resize: none !important;
min-width: 340px !important;
max-width: 340px !important;
min-height: 430px !important;
max-height: 430px !important;
}
.nitro-widget-custom-stack-height { .nitro-widget-custom-stack-height {
width: 275px; width: 275px;
height: 220px; height: 220px;
@ -53,7 +61,7 @@
width: 320px; width: 320px;
height: 320px; height: 320px;
} }
.picture-preview-buttons { .picture-preview-buttons {
display: flex; display: flex;
align-items: center; align-items: center;
@ -64,7 +72,6 @@
.picture-preview-buttons-previous, .picture-preview-buttons-previous,
.picture-preview-buttons-next { .picture-preview-buttons-next {
color: #222; color: #222;
background-color: white;
padding: 10px; padding: 10px;
border-radius: 50%; border-radius: 50%;
} }