mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
🆙 Init V3
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { GetRoomEngine, NitroRectangle, TextureUtils } from '@nitrots/nitro-renderer';
|
||||
import { FC, useRef } from 'react';
|
||||
import { FaTimes } from 'react-icons/fa';
|
||||
import { CameraPicture, GetRoomSession, LocalizeText, PlaySound, SoundNames } from '../../../api';
|
||||
import { Button, Column, DraggableWindow } from '../../../common';
|
||||
import { useCamera, useNotification } from '../../../hooks';
|
||||
|
||||
export interface CameraWidgetCaptureViewProps
|
||||
{
|
||||
onClose: () => void;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const CAMERA_ROLL_LIMIT: number = 5;
|
||||
|
||||
export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =>
|
||||
{
|
||||
const { onClose = null, onEdit = null, onDelete = null } = props;
|
||||
const { cameraRoll = null, setCameraRoll = null, selectedPictureIndex = -1, setSelectedPictureIndex = null } = useCamera();
|
||||
const { simpleAlert = null } = useNotification();
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
|
||||
const selectedPicture = ((selectedPictureIndex > -1) ? cameraRoll[selectedPictureIndex] : null);
|
||||
|
||||
const getCameraBounds = () =>
|
||||
{
|
||||
if(!elementRef || !elementRef.current) return null;
|
||||
|
||||
const frameBounds = elementRef.current.getBoundingClientRect();
|
||||
|
||||
return new NitroRectangle(Math.floor(frameBounds.x), Math.floor(frameBounds.y), Math.floor(frameBounds.width), Math.floor(frameBounds.height));
|
||||
};
|
||||
|
||||
const takePicture = async () =>
|
||||
{
|
||||
if(selectedPictureIndex > -1)
|
||||
{
|
||||
setSelectedPictureIndex(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
const texture = GetRoomEngine().createTextureFromRoom(GetRoomSession().roomId, 1, getCameraBounds());
|
||||
|
||||
const clone = [ ...cameraRoll ];
|
||||
|
||||
if(clone.length >= CAMERA_ROLL_LIMIT)
|
||||
{
|
||||
simpleAlert(LocalizeText('camera.full.body'));
|
||||
|
||||
clone.pop();
|
||||
}
|
||||
|
||||
PlaySound(SoundNames.CAMERA_SHUTTER);
|
||||
clone.push(new CameraPicture(texture, await TextureUtils.generateImageUrl(texture)));
|
||||
|
||||
setCameraRoll(clone);
|
||||
};
|
||||
|
||||
return (
|
||||
<DraggableWindow uniqueKey="nitro-camera-capture">
|
||||
<Column center className="relative" gap={ 0 }>
|
||||
{ selectedPicture && <img alt="" className="absolute top-[37px] left-[10px] w-[320px] h-[320px]" src={ selectedPicture.imageUrl } /> }
|
||||
<div className="relative w-[340px] h-[462px] bg-[url('@/assets/images/room-widgets/camera-widget/camera-spritesheet.png')] bg-[-1px_-1px] drag-handler">
|
||||
<div className="absolute top-[8px] right-[8px] rounded-[.25rem] [box-shadow:0_0_0_1.5px_#fff] border-[2px] border-[solid] border-[#921911] bg-[repeating-linear-gradient(rgb(245,_80,_65),_rgb(245,_80,_65)_50%,_rgb(194,_48,_39)_50%,_rgb(194,_48,_39)_100%)] cursor-pointer leading-none px-[3px] py-px" onClick={ onClose }>
|
||||
<FaTimes className="fa-icon" />
|
||||
</div>
|
||||
{ !selectedPicture && <div ref={ elementRef } className="absolute top-[37px] left-[10px] w-[320px] h-[320px] bg-[url('@/assets/images/room-widgets/camera-widget/camera-spritesheet.png')] bg-[-343px_-1px]" /> }
|
||||
{ selectedPicture &&
|
||||
<div className="absolute top-[37px] left-[10px] w-[320px] h-[320px] ">
|
||||
<div className="bg-[rgba(0,_0,_0,_0.5)] w-full absolute bottom-0 py-2 text-center inline-flex justify-center">
|
||||
<Button className="me-3" title={ LocalizeText('camera.editor.button.tooltip') } variant="success" onClick={ onEdit }>{ LocalizeText('camera.editor.button.text') }</Button>
|
||||
<Button variant="danger" onClick={ onDelete }>{ LocalizeText('camera.delete.button.text') }</Button>
|
||||
</div>
|
||||
</div> }
|
||||
<div className="flex justify-center">
|
||||
<div className="w-[94px] h-[94px] cursor-pointer mt-[362px] bg-[url('@/assets/images/room-widgets/camera-widget/camera-spritesheet.png')] bg-[-343px_-321px]" title={ LocalizeText('camera.take.photo.button.tooltip') } onClick={ takePicture } />
|
||||
</div>
|
||||
</div>
|
||||
{ (cameraRoll.length > 0) &&
|
||||
<div className="w-[330px] bg-[#bab8b4] rounded-bl-[10px] rounded-br-[10px] border-[1px] border-[solid] border-[black] [box-shadow:inset_1px_0px_white,_inset_-1px_-1px_white] flex justify-center py-2">
|
||||
{ cameraRoll.map((picture, index) =>
|
||||
{
|
||||
return <img key={ index } alt="" className="w-[56px] h-[56px] border-[1px] border-[solid] border-[black] object-contain [image-rendering:initial]" src={ picture.imageUrl } onClick={ event => setSelectedPictureIndex(index) } />;
|
||||
}) }
|
||||
</div> }
|
||||
</Column>
|
||||
</DraggableWindow>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import { CameraPublishStatusMessageEvent, CameraPurchaseOKMessageEvent, CameraStorageUrlMessageEvent, CreateLinkEvent, GetRoomEngine, PublishPhotoMessageComposer, PurchasePhotoMessageComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../../api';
|
||||
import { Button, Column, LayoutCurrencyIcon, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../common';
|
||||
import { useMessageEvent } from '../../../hooks';
|
||||
|
||||
export interface CameraWidgetCheckoutViewProps
|
||||
{
|
||||
base64Url: string;
|
||||
onCloseClick: () => void;
|
||||
onCancelClick: () => void;
|
||||
price: { credits: number, duckets: number, publishDucketPrice: number };
|
||||
}
|
||||
|
||||
export const CameraWidgetCheckoutView: FC<CameraWidgetCheckoutViewProps> = props =>
|
||||
{
|
||||
const { base64Url = null, onCloseClick = null, onCancelClick = null, price = null } = props;
|
||||
const [ pictureUrl, setPictureUrl ] = useState<string>(null);
|
||||
const [ publishUrl, setPublishUrl ] = useState<string>(null);
|
||||
const [ picturesBought, setPicturesBought ] = useState(0);
|
||||
const [ wasPicturePublished, setWasPicturePublished ] = useState(false);
|
||||
const [ isWaiting, setIsWaiting ] = useState(false);
|
||||
const [ publishCooldown, setPublishCooldown ] = useState(0);
|
||||
|
||||
const publishDisabled = useMemo(() => GetConfigurationValue<boolean>('camera.publish.disabled', false), []);
|
||||
|
||||
useMessageEvent<CameraPurchaseOKMessageEvent>(CameraPurchaseOKMessageEvent, event =>
|
||||
{
|
||||
setPicturesBought(value => (value + 1));
|
||||
setIsWaiting(false);
|
||||
});
|
||||
|
||||
useMessageEvent<CameraPublishStatusMessageEvent>(CameraPublishStatusMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setPublishUrl(parser.extraDataId);
|
||||
setPublishCooldown(parser.secondsToWait);
|
||||
setWasPicturePublished(parser.ok);
|
||||
setIsWaiting(false);
|
||||
});
|
||||
|
||||
useMessageEvent<CameraStorageUrlMessageEvent>(CameraStorageUrlMessageEvent, event =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
|
||||
setPictureUrl(GetConfigurationValue<string>('camera.url') + '/' + parser.url);
|
||||
});
|
||||
|
||||
const processAction = (type: string, value: string | number = null) =>
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case 'close':
|
||||
onCloseClick();
|
||||
return;
|
||||
case 'buy':
|
||||
if(isWaiting) return;
|
||||
|
||||
setIsWaiting(true);
|
||||
SendMessageComposer(new PurchasePhotoMessageComposer(''));
|
||||
return;
|
||||
case 'publish':
|
||||
if(isWaiting) return;
|
||||
|
||||
setIsWaiting(true);
|
||||
SendMessageComposer(new PublishPhotoMessageComposer());
|
||||
return;
|
||||
case 'cancel':
|
||||
onCancelClick();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(!base64Url) return;
|
||||
|
||||
GetRoomEngine().saveBase64AsScreenshot(base64Url);
|
||||
}, [ base64Url ]);
|
||||
|
||||
if(!price) return null;
|
||||
|
||||
return (
|
||||
<NitroCardView className="nitro-camera-checkout" theme="primary-slim">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('camera.confirm_phase.title') } onCloseClick={ event => processAction('close') } />
|
||||
<NitroCardContentView>
|
||||
<div className="flex items-center justify-center">
|
||||
{ (pictureUrl && pictureUrl.length) &&
|
||||
<LayoutImage className="picture-preview border" imageUrl={ pictureUrl } /> }
|
||||
{ (!pictureUrl || !pictureUrl.length) &&
|
||||
<div className="flex items-center justify-center picture-preview border">
|
||||
<Text bold>{ LocalizeText('camera.loading') }</Text>
|
||||
</div> }
|
||||
</div>
|
||||
<div className="flex items-center bg-muted rounded p-2 justify-between">
|
||||
<Column gap={ 1 } size={ publishDisabled ? 10 : 6 }>
|
||||
<Text bold>
|
||||
{ LocalizeText('camera.purchase.header') }
|
||||
</Text>
|
||||
{ ((price.credits > 0) || (price.duckets > 0)) &&
|
||||
<div className="flex gap-1">
|
||||
<Text>{ LocalizeText('catalog.purchase.confirmation.dialog.cost') }</Text>
|
||||
{ (price.credits > 0) &&
|
||||
<div className="flex gap-1">
|
||||
<Text bold>{ price.credits }</Text>
|
||||
<LayoutCurrencyIcon type={ -1 } />
|
||||
</div> }
|
||||
{ (price.duckets > 0) &&
|
||||
<div className="flex gap-1">
|
||||
<Text bold>{ price.duckets }</Text>
|
||||
<LayoutCurrencyIcon type={ 5 } />
|
||||
</div> }
|
||||
</div> }
|
||||
{ (picturesBought > 0) &&
|
||||
<Text>
|
||||
<Text bold>{ LocalizeText('camera.purchase.count.info') }</Text> { picturesBought }
|
||||
<u className="ms-1 cursor-pointer" onClick={ () => CreateLinkEvent('inventory/toggle') }>{ LocalizeText('camera.open.inventory') }</u>
|
||||
</Text> }
|
||||
</Column>
|
||||
<div className="flex items-center">
|
||||
<Button disabled={ isWaiting } variant="success" onClick={ event => processAction('buy') }>{ LocalizeText(!picturesBought ? 'buy' : 'camera.buy.another.button.text') }</Button>
|
||||
</div>
|
||||
</div>
|
||||
{ !publishDisabled &&
|
||||
<div className="flex items-center justify-between bg-muted rounded p-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Text bold>
|
||||
{ LocalizeText(wasPicturePublished ? 'camera.publish.successful' : 'camera.publish.explanation') }
|
||||
</Text>
|
||||
<Text>
|
||||
{ LocalizeText(wasPicturePublished ? 'camera.publish.success.short.info' : 'camera.publish.detailed.explanation') }
|
||||
</Text>
|
||||
{ wasPicturePublished && <a href={ publishUrl } rel="noreferrer" target="_blank">{ LocalizeText('camera.link.to.published') }</a> }
|
||||
{ !wasPicturePublished && (price.publishDucketPrice > 0) &&
|
||||
<div className="flex gap-1">
|
||||
<Text>{ LocalizeText('catalog.purchase.confirmation.dialog.cost') }</Text>
|
||||
<div className="flex gap-1">
|
||||
<Text bold>{ price.publishDucketPrice }</Text>
|
||||
<LayoutCurrencyIcon type={ 5 } />
|
||||
</div>
|
||||
</div> }
|
||||
{ (publishCooldown > 0) && <div className="mt-1 text-center font-bold ">{ LocalizeText('camera.publish.wait', [ 'minutes' ], [ Math.ceil(publishCooldown / 60).toString() ]) }</div> }
|
||||
</div>
|
||||
{ !wasPicturePublished &&
|
||||
<div className="flex align-items-end">
|
||||
<Button disabled={ (isWaiting || (publishCooldown > 0)) } variant="success" onClick={ event => processAction('publish') }>
|
||||
{ LocalizeText('camera.publish.button.text') }
|
||||
</Button>
|
||||
</div> }
|
||||
</div> }
|
||||
<Text center>{ LocalizeText('camera.warning.disclaimer') }</Text>
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={ event => processAction('cancel') }>{ LocalizeText('generic.cancel') }</Button>
|
||||
</div>
|
||||
</NitroCardContentView>
|
||||
</NitroCardView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { GetRoomEngine, RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
|
||||
import { GetUserProfile, IPhotoData, LocalizeText } from '../../../api';
|
||||
import { Flex, Grid, Text } from '../../../common';
|
||||
|
||||
export interface CameraWidgetShowPhotoViewProps {
|
||||
currentIndex: number;
|
||||
currentPhotos: IPhotoData[];
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const CameraWidgetShowPhotoView: FC<CameraWidgetShowPhotoViewProps> = props => {
|
||||
const { currentIndex = -1, currentPhotos = null, onClick = null } = props;
|
||||
const [imageIndex, setImageIndex] = useState(0);
|
||||
|
||||
const currentImage = currentPhotos && currentPhotos.length ? currentPhotos[imageIndex] : null;
|
||||
|
||||
const next = () => {
|
||||
setImageIndex(prevValue => {
|
||||
let newIndex = prevValue + 1;
|
||||
if (newIndex >= currentPhotos.length) newIndex = 0;
|
||||
return newIndex;
|
||||
});
|
||||
};
|
||||
|
||||
const previous = () => {
|
||||
setImageIndex(prevValue => {
|
||||
let newIndex = prevValue - 1;
|
||||
if (newIndex < 0) newIndex = currentPhotos.length - 1;
|
||||
return newIndex;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setImageIndex(currentIndex);
|
||||
}, [currentIndex]);
|
||||
|
||||
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 (
|
||||
<Grid style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Flex center className="picture-preview border border-black" style={currentImage.w ? { backgroundImage: 'url(' + currentImage.w + ')' } : {}} onClick={onClick}>
|
||||
{!currentImage.w && <Text bold>{LocalizeText('camera.loading')}</Text>}
|
||||
</Flex>
|
||||
{currentImage.m && currentImage.m.length && <Text center>{currentImage.m}</Text>}
|
||||
<div className="flex items-center center justify-between">
|
||||
<Text>{currentImage.n || ''}</Text>
|
||||
<Text onClick={() => GetUserProfile(Number(getUserData(currentImage.s, Number(currentImage.u), 'id')))}> { getUserData(currentImage.s, Number(currentImage.u), 'username') } </Text>
|
||||
<Text className="cursor-pointer" onClick={() => GetUserProfile(currentImage.oi)}>{currentImage.o}</Text>
|
||||
<Text>{new Date(currentImage.t * 1000).toLocaleDateString()}</Text>
|
||||
</div>
|
||||
{currentPhotos.length > 1 && (
|
||||
<Flex className="picture-preview-buttons">
|
||||
<FaArrowLeft onClick={previous} />
|
||||
<FaArrowRight className="cursor-pointer"onClick={next} />
|
||||
</Flex>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,222 @@
|
||||
import { GetRoomCameraWidgetManager, IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, NitroLogger, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FaSave, FaSearchMinus, FaSearchPlus, FaTrash } from 'react-icons/fa';
|
||||
import ReactSlider from 'react-slider';
|
||||
import { CameraEditorTabs, CameraPicture, CameraPictureThumbnail, LocalizeText } from '../../../../api';
|
||||
import { Button, Column, Flex, Grid, LayoutImage, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView, 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>('');
|
||||
const isBusy = useRef<boolean>(false);
|
||||
|
||||
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':
|
||||
setSelectedEffectName(null);
|
||||
setSelectedEffects([]);
|
||||
return;
|
||||
case 'download': {
|
||||
(async () => {
|
||||
const image = new Image();
|
||||
image.src = currentPictureUrl;
|
||||
const newWindow = window.open('');
|
||||
newWindow.document.write(image.outerHTML);
|
||||
})();
|
||||
return;
|
||||
}
|
||||
case 'zoom':
|
||||
setIsZoomed(prev => !prev);
|
||||
return;
|
||||
}
|
||||
}, [ 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)
|
||||
)
|
||||
);
|
||||
setEffectsThumbnails(renderedEffects.map((image, index) => new CameraPictureThumbnail(availableEffects[index].name, image.src)));
|
||||
};
|
||||
processThumbnails();
|
||||
}, [ picture, availableEffects ]);
|
||||
|
||||
useEffect(() => {
|
||||
GetRoomCameraWidgetManager()
|
||||
.applyEffects(picture.texture, selectedEffects, false) // Remove isZoomed from here
|
||||
.then(imageElement => {
|
||||
setCurrentPictureUrl(imageElement.src);
|
||||
})
|
||||
.catch(error => {
|
||||
NitroLogger.error('Failed to apply effects to picture', error);
|
||||
});
|
||||
}, [ picture, selectedEffects ]); // Remove isZoomed from dependency array
|
||||
|
||||
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>
|
||||
<LayoutImage
|
||||
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 }>
|
||||
<Text>{ LocalizeText('camera.effect.name.' + selectedEffectName) }</Text>
|
||||
<ReactSlider
|
||||
className="nitro-slider"
|
||||
min={ 0 }
|
||||
max={ 1 }
|
||||
step={ 0.01 }
|
||||
value={ getCurrentEffect.strength }
|
||||
onChange={ event => setSelectedEffectAlpha(event) }
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { IRoomCameraWidgetEffect } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { FaLock, FaTimes } from 'react-icons/fa';
|
||||
import { LocalizeText } from '../../../../../api';
|
||||
import { Button, LayoutGridItem, Text } from '../../../../../common';
|
||||
|
||||
export interface CameraWidgetEffectListItemViewProps
|
||||
{
|
||||
effect: IRoomCameraWidgetEffect;
|
||||
thumbnailUrl: string;
|
||||
isActive: boolean;
|
||||
isLocked: boolean;
|
||||
selectEffect: () => void;
|
||||
removeEffect: () => void;
|
||||
}
|
||||
|
||||
export const CameraWidgetEffectListItemView: FC<CameraWidgetEffectListItemViewProps> = props =>
|
||||
{
|
||||
const { effect = null, thumbnailUrl = null, isActive = false, isLocked = false, selectEffect = null, removeEffect = null } = props;
|
||||
|
||||
return (
|
||||
<LayoutGridItem itemActive={ isActive } title={ LocalizeText(!isLocked ? (`camera.effect.name.${ effect.name }`) : `camera.effect.required.level ${ effect.minLevel }`) } onClick={ event => (!isActive && selectEffect()) }>
|
||||
{ isActive &&
|
||||
<Button className="rounded-circle remove-effect" variant="danger" onClick={ removeEffect }>
|
||||
<FaTimes className="fa-icon" />
|
||||
</Button> }
|
||||
{ !isLocked && (thumbnailUrl && thumbnailUrl.length > 0) &&
|
||||
<div className="effect-thumbnail-image border">
|
||||
<img alt="" src={ thumbnailUrl } />
|
||||
</div> }
|
||||
{ isLocked &&
|
||||
<Text bold center>
|
||||
<div>
|
||||
<FaLock className="fa-icon" />
|
||||
</div>
|
||||
{ effect.minLevel }
|
||||
</Text> }
|
||||
</LayoutGridItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
|
||||
import { FC } from 'react';
|
||||
import { CameraPictureThumbnail } from '../../../../../api';
|
||||
import { Grid } from '../../../../../common';
|
||||
import { CameraWidgetEffectListItemView } from './CameraWidgetEffectListItemView';
|
||||
|
||||
export interface CameraWidgetEffectListViewProps
|
||||
{
|
||||
myLevel: number;
|
||||
selectedEffects: IRoomCameraWidgetSelectedEffect[];
|
||||
effects: IRoomCameraWidgetEffect[];
|
||||
thumbnails: CameraPictureThumbnail[];
|
||||
processAction: (type: string, name: string) => void;
|
||||
}
|
||||
|
||||
export const CameraWidgetEffectListView: FC<CameraWidgetEffectListViewProps> = props =>
|
||||
{
|
||||
const { myLevel = 0, selectedEffects = [], effects = [], thumbnails = [], processAction = null } = props;
|
||||
|
||||
return (
|
||||
<Grid columnCount={ 3 } overflow="auto">
|
||||
{ effects && (effects.length > 0) && effects.map((effect, index) =>
|
||||
{
|
||||
const thumbnailUrl = (thumbnails.find(thumbnail => (thumbnail.effectName === effect.name)));
|
||||
const isActive = (selectedEffects.findIndex(selectedEffect => (selectedEffect.effect.name === effect.name)) > -1);
|
||||
|
||||
// return <CameraWidgetEffectListItemView key={ index } effect={ effect } isActive={ isActive } isLocked={ (effect.minLevel > myLevel) } removeEffect={ () => processAction('remove_effect', effect.name) } selectEffect={ () => processAction('select_effect', effect.name) } thumbnailUrl={ ((thumbnailUrl && thumbnailUrl.thumbnailUrl) || null) } />;
|
||||
|
||||
return <CameraWidgetEffectListItemView key={ index } effect={ effect } thumbnailUrl={ ((thumbnailUrl && thumbnailUrl.thumbnailUrl) || null) } isActive={ isActive } isLocked={ (effect.minLevel > myLevel) } selectEffect={ () => processAction('select_effect', effect.name) } removeEffect={ () => processAction('remove_effect', effect.name) } />
|
||||
}) }
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './CameraWidgetEffectListItemView';
|
||||
export * from './CameraWidgetEffectListView';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './CameraWidgetEditorView';
|
||||
export * from './effect-list';
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './CameraWidgetCaptureView';
|
||||
export * from './CameraWidgetCheckoutView';
|
||||
export * from './CameraWidgetShowPhotoView';
|
||||
export * from './editor';
|
||||
export * from './editor/effect-list';
|
||||
Reference in New Issue
Block a user