Update camera

This commit is contained in:
Bill 2022-01-11 00:54:24 -05:00
parent 188ac1bc5d
commit 0a8e8fa67a
24 changed files with 379 additions and 391 deletions

View File

@ -62,6 +62,11 @@ $messenger-height: 370px;
$marketplace-post-offer-width: 430px; $marketplace-post-offer-width: 430px;
$marketplace-post-offer-height: 250px; $marketplace-post-offer-height: 250px;
$camera-editor-width: 600px;
$camera-editor-height: 500px;
$camera-checkout-width: 350px;
$room-info-width: 325px; $room-info-width: 325px;
.nitro-app { .nitro-app {

View File

@ -85,89 +85,41 @@
} }
.nitro-camera-editor { .nitro-camera-editor {
width: 600px; width: $camera-editor-width;
height: $camera-editor-height;
.content-area { .picture-preview {
min-height: 441px; width: 320px;
height: 441px; height: 320px;
resize: vertical; }
.picture-preview { .layout-grid-item {
width: 320px; height: 60px !important;
height: 320px; max-height: 60px !important;
}
.slider { .effect-thumbnail-image {
background: linear-gradient(180deg, transparent, black);
text-shadow: 1px 1px rgba(0, 0, 0, .5); img {
} width: 50px;
height: 50px;
image-rendering: auto;
object-fit: contain;
} }
} }
.effect-grid { .remove-effect {
position: absolute;
.grid-item-container { top: 1px;
height: 60px !important; right: 1px;
max-height: 60px !important; padding: 2px;
font-size: 10px;
.grid-item { min-height: unset;
overflow: unset;
.remove-effect {
position: absolute;
top: -1px;
right: -1px;
padding: 2px;
font-size: 11px;
line-height: 11px;
min-height: unset;
}
.effect-thumbnail-image {
img {
width: 40px;
height: 40px;
image-rendering: auto;
}
}
}
}
}
.effects {
height: 363px;
min-height: 363px;
max-height: 363px;
.btn-remove-effect {
z-index: 10;
min-height: 0px;
border-radius: 100px;
width: 20px;
}
.effect-thumbnail {
border-color: $grid-border-color !important;
background-color: $grid-bg-color;
height: 60px;
&.active {
border-color: $grid-active-border-color !important;
background-color: $grid-active-bg-color;
}
.effect-thumbnail-image {
img {
width: 40px;
height: 40px;
}
}
}
} }
} }
.nitro-camera-checkout { .nitro-camera-checkout {
width: 388px; width: $camera-checkout-width;
.picture-preview { .picture-preview {
width: 320px; width: 320px;

View File

@ -0,0 +1,5 @@
export class CameraEditorTabs
{
public static readonly COLORMATRIX: string = 'colormatrix';
public static readonly COMPOSITE: string = 'composite';
}

View File

@ -0,0 +1,24 @@
import { createContext, Dispatch, FC, ProviderProps, SetStateAction, useContext } from 'react';
import { CameraPicture } from '../common/CameraPicture';
export interface ICameraWidgetContext
{
cameraRoll: CameraPicture[],
setCameraRoll: Dispatch<SetStateAction<CameraPicture[]>>;
selectedPictureIndex: number,
setSelectedPictureIndex: Dispatch<SetStateAction<number>>;
}
const CameraWidgetContext = createContext<ICameraWidgetContext>({
cameraRoll: null,
setCameraRoll: null,
selectedPictureIndex: null,
setSelectedPictureIndex: null
});
export const CameraWidgetContextProvider: FC<ProviderProps<ICameraWidgetContext>> = props =>
{
return <CameraWidgetContext.Provider value={ props.value }>{ props.children }</CameraWidgetContext.Provider>
}
export const useCameraWidgetContext = () => useContext(CameraWidgetContext);

View File

@ -1,11 +1,21 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { NitroRectangle, TextureUtils } from '@nitrots/nitro-renderer'; import { NitroRectangle, TextureUtils } from '@nitrots/nitro-renderer';
import { FC, useCallback, useRef } from 'react'; import { FC, useCallback, useRef } from 'react';
import { GetRoomEngine, GetRoomSession, LocalizeText } from '../../../../api'; import { GetRoomEngine, GetRoomSession, LocalizeText } from '../../../../api';
import { CAMERA_SHUTTER, PlaySound } from '../../../../api/utils/PlaySound'; import { CAMERA_SHUTTER, PlaySound } from '../../../../api/utils/PlaySound';
import { Column } from '../../../../common/Column';
import { Flex } from '../../../../common/Flex';
import { DraggableWindow } from '../../../../layout'; import { DraggableWindow } from '../../../../layout';
import { NotificationUtilities } from '../../../../views/notification-center/common/NotificationUtilities';
import { CameraPicture } from '../../common/CameraPicture'; import { CameraPicture } from '../../common/CameraPicture';
import { useCameraWidgetContext } from '../../context/CameraWidgetContext'; import { useCameraWidgetContext } from '../../context/CameraWidgetContext';
import { CameraWidgetCaptureViewProps } from './CameraWidgetCaptureView.types';
export interface CameraWidgetCaptureViewProps
{
onClose: () => void;
onEdit: () => void;
onDelete: () => void;
}
const CAMERA_ROLL_LIMIT: number = 5; const CAMERA_ROLL_LIMIT: number = 5;
@ -40,7 +50,7 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
if(clone.length >= CAMERA_ROLL_LIMIT) if(clone.length >= CAMERA_ROLL_LIMIT)
{ {
alert(LocalizeText('camera.full.body')); NotificationUtilities.simpleAlert(LocalizeText('camera.full.body'));
clone.pop(); clone.pop();
} }
@ -52,12 +62,12 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
}, [ cameraRoll, selectedPictureIndex, getCameraBounds, setCameraRoll, setSelectedPictureIndex ]); }, [ cameraRoll, selectedPictureIndex, getCameraBounds, setCameraRoll, setSelectedPictureIndex ]);
return ( return (
<DraggableWindow> <DraggableWindow uniqueKey="nitro-camera-capture">
<div className="d-flex flex-column justify-content-center align-items-center nitro-camera-capture"> <Column center className="nitro-camera-capture" gap={ 0 }>
{ selectedPicture && <img alt="" className="camera-area" src={ selectedPicture.imageUrl } /> } { selectedPicture && <img alt="" className="camera-area" src={ selectedPicture.imageUrl } /> }
<div className="camera-canvas drag-handler"> <div className="camera-canvas drag-handler">
<div className="position-absolute header-close" onClick={ onClose }> <div className="position-absolute header-close" onClick={ onClose }>
<i className="fas fa-times" /> <FontAwesomeIcon icon="times" />
</div> </div>
{ !selectedPicture && <div ref={ elementRef } className="camera-area camera-view-finder" /> } { !selectedPicture && <div ref={ elementRef } className="camera-area camera-view-finder" /> }
{ selectedPicture && { selectedPicture &&
@ -72,13 +82,13 @@ export const CameraWidgetCaptureView: FC<CameraWidgetCaptureViewProps> = props =
</div> </div>
</div> </div>
{ (cameraRoll.length > 0) && { (cameraRoll.length > 0) &&
<div className="camera-roll d-flex justify-content-center py-2"> <Flex gap={ 2 } justifyContent="center" className="camera-roll d-flex justify-content-center py-2">
{ cameraRoll.map((picture, index) => { cameraRoll.map((picture, index) =>
{ {
return <img alt="" key={ index } className={ (index < (cameraRoll.length - 1) ? 'me-2' : '') } src={ picture.imageUrl } onClick={ event => setSelectedPictureIndex(index) } />; return <img alt="" key={ index } src={ picture.imageUrl } onClick={ event => setSelectedPictureIndex(index) } />;
}) } }) }
</div> } </Flex> }
</div> </Column>
</DraggableWindow> </DraggableWindow>
); );
} }

View File

@ -0,0 +1,175 @@
import { CameraPublishStatusMessageEvent, CameraPurchaseOKMessageEvent, CameraStorageUrlMessageEvent, PublishPhotoMessageComposer, PurchasePhotoMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { GetConfiguration, GetRoomEngine, LocalizeText } from '../../../../api';
import { Button } from '../../../../common/Button';
import { Column } from '../../../../common/Column';
import { Flex } from '../../../../common/Flex';
import { LayoutImage } from '../../../../common/layout/LayoutImage';
import { Text } from '../../../../common/Text';
import { InventoryEvent } from '../../../../events';
import { BatchUpdates, CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { CurrencyIcon } from '../../../../views/shared/currency-icon/CurrencyIcon';
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 onCameraPurchaseOKMessageEvent = useCallback((event: CameraPurchaseOKMessageEvent) =>
{
BatchUpdates(() =>
{
setPicturesBought(value => (value + 1));
setIsWaiting(false);
});
}, []);
CreateMessageHook(CameraPurchaseOKMessageEvent, onCameraPurchaseOKMessageEvent);
const onCameraPublishStatusMessageEvent = useCallback((event: CameraPublishStatusMessageEvent) =>
{
const parser = event.getParser();
BatchUpdates(() =>
{
setPublishUrl(parser.extraDataId);
setPublishCooldown(parser.secondsToWait);
setWasPicturePublished(parser.ok);
setIsWaiting(false);
});
}, []);
CreateMessageHook(CameraPublishStatusMessageEvent, onCameraPublishStatusMessageEvent);
const onCameraStorageUrlMessageEvent = useCallback((event: CameraStorageUrlMessageEvent) =>
{
const parser = event.getParser();
setPictureUrl(GetConfiguration<string>('camera.url') + '/' + parser.url);
}, []);
CreateMessageHook(CameraStorageUrlMessageEvent, onCameraStorageUrlMessageEvent);
const processAction = (type: string, value: string | number = null) =>
{
switch(type)
{
case 'close':
onCloseClick();
return;
case 'buy':
if(isWaiting) return;
setIsWaiting(true);
SendMessageHook(new PurchasePhotoMessageComposer(''));
return;
case 'publish':
if(isWaiting) return;
setIsWaiting(true);
SendMessageHook(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" simple={ true }>
<NitroCardHeaderView headerText={ LocalizeText('camera.confirm_phase.title') } onCloseClick={ event => processAction('close') } />
<NitroCardContentView>
<Flex center>
{ (pictureUrl && pictureUrl.length) &&
<LayoutImage fit={ false } className="picture-preview border" imageUrl={ pictureUrl } /> }
{ (!pictureUrl || !pictureUrl.length) &&
<Flex center className="picture-preview border">
<Text bold>{ LocalizeText('camera.loading') }</Text>
</Flex> }
</Flex>
<Flex justifyContent="between" alignItems="center" className="bg-muted rounded p-2">
<Column gap={ 1 }>
<Text bold>
{ LocalizeText('camera.purchase.header') }
</Text>
{ ((price.credits > 0) || (price.duckets > 0)) &&
<Flex gap={ 1 }>
<Text>{ LocalizeText('catalog.purchase.confirmation.dialog.cost') }</Text>
{ (price.credits > 0) &&
<Flex gap={ 1 }>
<Text bold>{ price.credits }</Text>
<CurrencyIcon type={ -1 } />
</Flex> }
{ (price.duckets > 0) &&
<Flex gap={ 1 }>
<Text bold>{ price.duckets }</Text>
<CurrencyIcon type={ 5 } />
</Flex> }
</Flex> }
{ (picturesBought > 0) &&
<Text>
<Text bold>{ LocalizeText('camera.purchase.count.info') }</Text> { picturesBought }
<u className="ms-1 cursor-pointer" onClick={ () => dispatchUiEvent(new InventoryEvent(InventoryEvent.SHOW_INVENTORY)) }>{ LocalizeText('camera.open.inventory') }</u>
</Text> }
</Column>
<Flex alignItems="center">
<Button variant="success" disabled={ isWaiting } onClick={ event => processAction('buy') }>{ LocalizeText(!picturesBought ? 'buy' : 'camera.buy.another.button.text') }</Button>
</Flex>
</Flex>
<Flex justifyContent="between" alignItems="center" className="bg-muted rounded p-2">
<Column 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) &&
<Flex gap={ 1 }>
<Text>{ LocalizeText('catalog.purchase.confirmation.dialog.cost') }</Text>
<Flex gap={ 1 }>
<Text bold>{ price.publishDucketPrice }</Text>
<CurrencyIcon type={ 5 } />
</Flex>
</Flex> }
{ (publishCooldown > 0) && <div className="mt-1 text-center fw-bold">{ LocalizeText('camera.publish.wait', [ 'minutes' ], [ Math.ceil( publishCooldown / 60).toString() ]) }</div> }
</Column>
{ !wasPicturePublished &&
<Flex className="d-flex align-items-end">
<Button variant="success" size="sm" disabled={ (isWaiting || (publishCooldown > 0)) } onClick={ event => processAction('publish') }>
{ LocalizeText('camera.publish.button.text') }
</Button>
</Flex> }
</Flex>
<Text center>{ LocalizeText('camera.warning.disclaimer') }</Text>
<Flex justifyContent="end">
<Button onClick={ event => processAction('cancel') }>{ LocalizeText('generic.cancel') }</Button>
</Flex>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -1,13 +1,32 @@
import { IRoomCameraWidgetSelectedEffect, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect, RoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import ReactSlider from 'react-slider'; import ReactSlider from 'react-slider';
import { GetRoomCameraWidgetManager, LocalizeText } from '../../../../api'; import { GetRoomCameraWidgetManager, LocalizeText } from '../../../../api';
import { Button } from '../../../../common/Button';
import { ButtonGroup } from '../../../../common/ButtonGroup';
import { Column } from '../../../../common/Column';
import { Flex } from '../../../../common/Flex';
import { Grid } from '../../../../common/Grid';
import { LayoutImage } from '../../../../common/layout/LayoutImage';
import { Text } from '../../../../common/Text';
import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout'; import { NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../../../layout';
import { CameraEditorTabs } from '../../common/CameraEditorTabs';
import { CameraPicture } from '../../common/CameraPicture';
import { CameraPictureThumbnail } from '../../common/CameraPictureThumbnail'; import { CameraPictureThumbnail } from '../../common/CameraPictureThumbnail';
import { CameraWidgetEditorTabs, CameraWidgetEditorViewProps } from './CameraWidgetEditorView.types';
import { CameraWidgetEffectListView } from './effect-list/CameraWidgetEffectListView'; import { CameraWidgetEffectListView } from './effect-list/CameraWidgetEffectListView';
const TABS: string[] = [ CameraWidgetEditorTabs.COLORMATRIX, CameraWidgetEditorTabs.COMPOSITE ]; 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 => export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
{ {
@ -30,7 +49,7 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
const getEffectList = useCallback(() => const getEffectList = useCallback(() =>
{ {
if(currentTab === CameraWidgetEditorTabs.COLORMATRIX) if(currentTab === CameraEditorTabs.COLORMATRIX)
{ {
return getColorMatrixEffects; return getColorMatrixEffects;
} }
@ -170,45 +189,49 @@ export const CameraWidgetEditorView: FC<CameraWidgetEditorViewProps> = props =>
}) } }) }
</NitroCardTabsView> </NitroCardTabsView>
<NitroCardContentView> <NitroCardContentView>
<div className="row h-100"> <Grid>
<div className="col-5 d-flex flex-column h-100"> <Column size={ 5 } overflow="hidden">
<CameraWidgetEffectListView myLevel={ myLevel } selectedEffects={ selectedEffects } effects={ getEffectList() } thumbnails={ effectsThumbnails } processAction={ processAction } /> <CameraWidgetEffectListView myLevel={ myLevel } selectedEffects={ selectedEffects } effects={ getEffectList() } thumbnails={ effectsThumbnails } processAction={ processAction } />
</div> </Column>
<div className="col-7 d-flex flex-column h-100"> <Column size={ 7 } justifyContent="between" overflow="hidden">
<div className="picture-preview"> <Column center>
<img alt="" src={ getCurrentPictureUrl } /> <LayoutImage fit={ false } imageUrl={ getCurrentPictureUrl } className="picture-preview" />
</div> { selectedEffectName &&
{ selectedEffectName && <Column center fullWidth gap={ 1 }>
<div className="w-100 p-2 d-flex flex-column justify-content-center slider"> <Text>{ LocalizeText('camera.effect.name.' + selectedEffectName) }</Text>
<div className="w-100 text-center">{ LocalizeText('camera.effect.name.' + selectedEffectName) }</div> <ReactSlider
<ReactSlider className={ 'nitro-slider' }
className={ 'nitro-slider' } min={ 0 }
min={ 0 } max={ 1 }
max={ 1 } step={ 0.01 }
step={ 0.01 } value={ getCurrentEffect.alpha }
value={ getCurrentEffect.alpha } onChange={ event => setSelectedEffectAlpha(event) }
onChange={ event => setSelectedEffectAlpha(event) } renderThumb={ (props, state) => <div { ...props }>{ state.valueNow }</div> } />
renderThumb={ (props, state) => <div { ...props }>{ state.valueNow }</div> } /> </Column> }
</div> } </Column>
<div className="d-flex justify-content-between mt-2"> <Flex justifyContent="between">
<div className="btn-group"> <ButtonGroup>
<button className="btn btn-primary" onClick={ event => processAction('clear_effects') }> <Button size="sm" onClick={ event => processAction('clear_effects') }>
<i className="fas fa-trash"></i> <FontAwesomeIcon icon="trash" />
</button> </Button>
<button className="btn btn-primary" onClick={ event => processAction('download') }> <Button size="sm" onClick={ event => processAction('download') }>
<i className="fas fa-save"></i> <FontAwesomeIcon icon="save" />
</button> </Button>
<button className="btn btn-primary" onClick={ event => processAction('zoom') }> <Button size="sm" onClick={ event => processAction('zoom') }>
<i className={ `fas fa-search-${ isZoomed ? 'minus': 'plus' }` } /> <FontAwesomeIcon icon={ isZoomed ? 'search-minus' : 'search-plus' } />
</button> </Button>
</div> </ButtonGroup>
<div className="d-flex justify-content-end"> <Flex gap={ 1 }>
<button className="btn btn-primary me-2" onClick={ event => processAction('cancel') }>{ LocalizeText('generic.cancel') }</button> <Button size="sm" onClick={ event => processAction('cancel') }>
<button className="btn btn-success" onClick={ event => processAction('checkout') }>{ LocalizeText('camera.preview.button.text') }</button> { LocalizeText('generic.cancel') }
</div> </Button>
</div> <Button size="sm" onClick={ event => processAction('checkout') }>
</div> { LocalizeText('camera.preview.button.text') }
</div> </Button>
</Flex>
</Flex>
</Column>
</Grid>
</NitroCardContentView> </NitroCardContentView>
</NitroCardView> </NitroCardView>
); );

View File

@ -0,0 +1,42 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IRoomCameraWidgetEffect } from '@nitrots/nitro-renderer';
import { FC } from 'react';
import { LocalizeText } from '../../../../../api';
import { Button } from '../../../../../common/Button';
import { LayoutGridItem } from '../../../../../common/layout/LayoutGridItem';
import { Text } from '../../../../../common/Text';
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 title={ LocalizeText(!isLocked ? (`camera.effect.name.${ effect.name }`) : `camera.effect.required.level ${ effect.minLevel }`) } itemActive={ isActive } onClick={ event => (!isActive && selectEffect()) }>
{ isActive &&
<Button variant="danger" size="sm" className="rounded-circle remove-effect" onClick={ removeEffect }>
<FontAwesomeIcon icon="times" />
</Button> }
{ !isLocked && (thumbnailUrl && thumbnailUrl.length > 0) &&
<div className="effect-thumbnail-image border">
<img alt="" src={ thumbnailUrl } />
</div> }
{ isLocked &&
<Text center bold>
<div>
<FontAwesomeIcon icon="lock" />
</div>
{ effect.minLevel }
</Text> }
</LayoutGridItem>
);
}

View File

@ -1,14 +1,24 @@
import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
import { FC } from 'react'; import { FC } from 'react';
import { NitroCardGridView } from '../../../../../layout'; import { Grid } from '../../../../../common/Grid';
import { CameraWidgetEffectListItemView } from '../effect-list-item/CameraWidgetEffectListItemView'; import { CameraPictureThumbnail } from '../../../common/CameraPictureThumbnail';
import { CameraWidgetEffectListViewProps } from './CameraWidgetEffectListView.types'; 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 => export const CameraWidgetEffectListView: FC<CameraWidgetEffectListViewProps> = props =>
{ {
const { myLevel = 0, selectedEffects = [], effects = [], thumbnails = [], processAction = null } = props; const { myLevel = 0, selectedEffects = [], effects = [], thumbnails = [], processAction = null } = props;
return ( return (
<NitroCardGridView className="effect-grid" columns={ 3 }> <Grid columnCount={ 2 } overflow="auto" columnMinHeight={ 60 }>
{ effects && (effects.length > 0) && effects.map((effect, index) => { effects && (effects.length > 0) && effects.map((effect, index) =>
{ {
const thumbnailUrl = (thumbnails.find(thumbnail => (thumbnail.effectName === effect.name))); const thumbnailUrl = (thumbnails.find(thumbnail => (thumbnail.effectName === effect.name)));
@ -16,6 +26,6 @@ export const CameraWidgetEffectListView: FC<CameraWidgetEffectListViewProps> = p
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) } /> 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) } />
}) } }) }
</NitroCardGridView> </Grid>
); );
} }

View File

@ -1,5 +1,6 @@
@import './achievements/AchievementsView'; @import './achievements/AchievementsView';
@import './avatar-editor/AvatarEditorView'; @import './avatar-editor/AvatarEditorView';
@import './camera/CameraWidgetView';
@import './catalog/CatalogView'; @import './catalog/CatalogView';
@import './inventory/InventoryView'; @import './inventory/InventoryView';
@import './navigator/NavigatorView'; @import './navigator/NavigatorView';

View File

@ -1,5 +1,4 @@
@import "./shared/Shared"; @import "./shared/Shared";
@import "./camera/CameraWidgetView";
@import "./friends/FriendsView"; @import "./friends/FriendsView";
@import "./groups/GroupView"; @import "./groups/GroupView";
@import "./hotel-view/HotelView"; @import "./hotel-view/HotelView";

View File

@ -1,16 +0,0 @@
import { createContext, FC, useContext } from 'react';
import { CameraWidgetContextProps, ICameraWidgetContext } from './CameraWidgetContext.types';
const CameraWidgetContext = createContext<ICameraWidgetContext>({
cameraRoll: null,
setCameraRoll: null,
selectedPictureIndex: null,
setSelectedPictureIndex: null
});
export const CameraWidgetContextProvider: FC<CameraWidgetContextProps> = props =>
{
return <CameraWidgetContext.Provider value={ props.value }>{ props.children }</CameraWidgetContext.Provider>
}
export const useCameraWidgetContext = () => useContext(CameraWidgetContext);

View File

@ -1,13 +0,0 @@
import { Dispatch, ProviderProps, SetStateAction } from 'react';
import { CameraPicture } from '../common/CameraPicture';
export interface ICameraWidgetContext
{
cameraRoll: CameraPicture[],
setCameraRoll: Dispatch<SetStateAction<CameraPicture[]>>;
selectedPictureIndex: number,
setSelectedPictureIndex: Dispatch<SetStateAction<number>>;
}
export interface CameraWidgetContextProps extends ProviderProps<ICameraWidgetContext>
{}

View File

@ -1,6 +0,0 @@
export interface CameraWidgetCaptureViewProps
{
onClose: () => void;
onEdit: () => void;
onDelete: () => void;
}

View File

@ -1,147 +0,0 @@
import { CameraPublishStatusMessageEvent, CameraPurchaseOKMessageEvent, CameraStorageUrlMessageEvent, PublishPhotoMessageComposer, PurchasePhotoMessageComposer } from '@nitrots/nitro-renderer';
import { FC, useCallback, useEffect, useState } from 'react';
import { GetConfiguration, GetRoomEngine, LocalizeText } from '../../../../api';
import { InventoryEvent } from '../../../../events';
import { CreateMessageHook, dispatchUiEvent, SendMessageHook } from '../../../../hooks';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../../../layout';
import { CurrencyIcon } from '../../../shared/currency-icon/CurrencyIcon';
import { CameraWidgetCheckoutViewProps } from './CameraWidgetCheckoutView.types';
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);
useEffect(() =>
{
if(!base64Url) return;
GetRoomEngine().saveBase64AsScreenshot(base64Url);
}, [ base64Url ]);
const onCameraPurchaseOKMessageEvent = useCallback((event: CameraPurchaseOKMessageEvent) =>
{
setPicturesBought(value => (value + 1));
setIsWaiting(false);
}, []);
CreateMessageHook(CameraPurchaseOKMessageEvent, onCameraPurchaseOKMessageEvent);
const onCameraPublishStatusMessageEvent = useCallback((event: CameraPublishStatusMessageEvent) =>
{
const parser = event.getParser();
setPublishUrl(parser.extraDataId);
setPublishCooldown(parser.secondsToWait);
setWasPicturePublished(parser.ok);
setIsWaiting(false);
}, []);
CreateMessageHook(CameraPublishStatusMessageEvent, onCameraPublishStatusMessageEvent);
const onCameraStorageUrlMessageEvent = useCallback((event: CameraStorageUrlMessageEvent) =>
{
const parser = event.getParser();
setPictureUrl(GetConfiguration<string>('camera.url') + '/' + parser.url);
}, []);
CreateMessageHook(CameraStorageUrlMessageEvent, onCameraStorageUrlMessageEvent);
const processAction = useCallback((type: string, value: string | number = null) =>
{
switch(type)
{
case 'close':
onCloseClick();
return;
case 'buy':
if(isWaiting) return;
setIsWaiting(true);
SendMessageHook(new PurchasePhotoMessageComposer(''));
return;
case 'publish':
if(isWaiting) return;
setIsWaiting(true);
SendMessageHook(new PublishPhotoMessageComposer());
return;
case 'cancel':
onCancelClick();
return;
}
}, [onCloseClick, isWaiting, onCancelClick]);
if(!price) return null;
return (
<NitroCardView className="nitro-camera-checkout" simple={ true }>
<NitroCardHeaderView headerText={ LocalizeText('camera.confirm_phase.title') } onCloseClick={ event => processAction('close') } />
<NitroCardContentView>
<div className="d-flex justify-content-center align-items-center picture-preview border mb-2" style={ pictureUrl ? { backgroundImage: 'url(' + pictureUrl + ')' } : {} }>
{ !pictureUrl &&
<div className="text-black fw-bold">
{ LocalizeText('camera.loading') }
</div> }
</div>
{ pictureUrl && <div className="text-black mb-2">{ LocalizeText('camera.confirm_phase.info') }</div> }
<div className="d-flex justify-content-between bg-muted rounded p-2 text-black mb-2">
<div className="d-flex flex-column">
<div className="fw-bold d-flex justify-content-start">{ LocalizeText('camera.purchase.header') }</div>
{ ((price.credits > 0) || (price.duckets > 0)) &&
<div className="d-flex">
<div className="me-1">{ LocalizeText('catalog.purchase.confirmation.dialog.cost') }</div>
{ (price.credits > 0) &&
<div className="d-flex fw-bold">
{ price.credits } <CurrencyIcon type={ -1 } />
</div> }
{ (price.duckets > 0) &&
<div className="d-flex fw-bold">
{ price.duckets } <CurrencyIcon type={ 5 } />
</div> }
</div> }
{ (picturesBought > 0) &&
<div>
<span className="fw-bold">{ LocalizeText('camera.purchase.count.info') }</span> { picturesBought }
<u className="ms-1 cursor-pointer" onClick={ () => dispatchUiEvent(new InventoryEvent(InventoryEvent.SHOW_INVENTORY)) }>{ LocalizeText('camera.open.inventory') }</u>
</div> }
</div>
<div className="d-flex align-items-center">
<button className="btn btn-success" disabled={ isWaiting } onClick={ event => processAction('buy') }>{ LocalizeText(!picturesBought ? 'buy' : 'camera.buy.another.button.text') }</button>
</div>
</div>
<div className="d-flex justify-content-between bg-muted rounded p-2 text-black mb-2">
<div className="d-flex flex-column">
<div className="fw-bold d-flex justify-content-start">{ LocalizeText(wasPicturePublished ? 'camera.publish.successful' : 'camera.publish.explanation') }</div>
<div>
{ LocalizeText(wasPicturePublished ? 'camera.publish.success.short.info' : 'camera.publish.detailed.explanation') }
</div>
{ wasPicturePublished && <a href={ publishUrl } rel="noreferrer" target="_blank">{ LocalizeText('camera.link.to.published') }</a> }
{ !wasPicturePublished && (price.publishDucketPrice > 0) &&
<div className="d-flex">
<div className="me-1">{ LocalizeText('catalog.purchase.confirmation.dialog.cost') }</div>
<div className="d-flex fw-bold">
{ price.publishDucketPrice } <CurrencyIcon type={ 5 } />
</div>
</div> }
{ (publishCooldown > 0) && <div className="mt-1 text-center fw-bold">{ LocalizeText('camera.publish.wait', [ 'minutes' ], [ Math.ceil( publishCooldown / 60).toString() ]) }</div> }
</div>
{ !wasPicturePublished &&
<div className="d-flex align-items-end">
<button className="btn btn-success" disabled={ isWaiting || publishCooldown > 0 } onClick={ event => processAction('publish') }>{ LocalizeText('camera.publish.button.text') }</button>
</div> }
</div>
<div className="text-black mb-2 text-center">{ LocalizeText('camera.warning.disclaimer') }</div>
<div className="d-flex justify-content-end">
<button className="btn btn-primary" onClick={ event => processAction('cancel') }>{ LocalizeText('generic.cancel') }</button>
</div>
</NitroCardContentView>
</NitroCardView>
);
}

View File

@ -1,7 +0,0 @@
export interface CameraWidgetCheckoutViewProps
{
base64Url: string;
onCloseClick: () => void;
onCancelClick: () => void;
price: { credits: number, duckets: number, publishDucketPrice: number };
}

View File

@ -1,18 +0,0 @@
import { IRoomCameraWidgetEffect } from '@nitrots/nitro-renderer';
import { CameraPicture } from '../../common/CameraPicture';
export interface CameraWidgetEditorViewProps
{
picture: CameraPicture;
availableEffects: IRoomCameraWidgetEffect[];
myLevel: number;
onClose: () => void;
onCancel: () => void;
onCheckout: (pictureUrl: string) => void;
}
export class CameraWidgetEditorTabs
{
public static readonly COLORMATRIX: string = 'colormatrix';
public static readonly COMPOSITE: string = 'composite';
}

View File

@ -1,29 +0,0 @@
import { FC } from 'react';
import { LocalizeText } from '../../../../../api';
import { NitroCardGridItemView } from '../../../../../layout';
import { CameraWidgetEffectListItemViewProps } from './CameraWidgetEffectListItemView.types';
export const CameraWidgetEffectListItemView: FC<CameraWidgetEffectListItemViewProps> = props =>
{
const { effect = null, thumbnailUrl = null, isActive = false, isLocked = false, selectEffect = null, removeEffect = null } = props;
return (
<NitroCardGridItemView title={ LocalizeText(!isLocked ? (`camera.effect.name.${ effect.name }`) : `camera.effect.required.level ${ effect.minLevel }`) } itemActive={ isActive } onClick={ event => (!isActive && selectEffect()) }>
{ isActive &&
<button className="btn btn-danger btn-sm rounded-circle remove-effect" onClick={ removeEffect }>
<i className="fas fa-times" />
</button> }
{ !isLocked && (thumbnailUrl && thumbnailUrl.length > 0) &&
<div className="effect-thumbnail-image border">
<img alt="" src={ thumbnailUrl } />
</div> }
{ isLocked &&
<div className="text-center text-black">
<div>
<i className="fas fa-lock" />
</div>
<div className="fw-bold">{ effect.minLevel }</div>
</div> }
</NitroCardGridItemView>
);
}

View File

@ -1,11 +0,0 @@
import { IRoomCameraWidgetEffect } from '@nitrots/nitro-renderer';
export interface CameraWidgetEffectListItemViewProps
{
effect: IRoomCameraWidgetEffect;
thumbnailUrl: string;
isActive: boolean;
isLocked: boolean;
selectEffect: () => void;
removeEffect: () => void;
}

View File

@ -1,11 +0,0 @@
import { IRoomCameraWidgetEffect, IRoomCameraWidgetSelectedEffect } from '@nitrots/nitro-renderer';
import { CameraPictureThumbnail } from '../../../common/CameraPictureThumbnail';
export interface CameraWidgetEffectListViewProps
{
myLevel: number;
selectedEffects: IRoomCameraWidgetSelectedEffect[];
effects: IRoomCameraWidgetEffect[];
thumbnails: CameraPictureThumbnail[];
processAction: (type: string, name: string) => void;
}

View File

@ -3,13 +3,13 @@ import { FC, useCallback, useEffect, useState } from 'react';
import { AddEventLinkTracker, GetCommunication, RemoveLinkEventTracker } from '../../api'; import { AddEventLinkTracker, GetCommunication, RemoveLinkEventTracker } from '../../api';
import { AchievementsView } from '../../components/achievements/AchievementsView'; import { AchievementsView } from '../../components/achievements/AchievementsView';
import { AvatarEditorView } from '../../components/avatar-editor/AvatarEditorView'; import { AvatarEditorView } from '../../components/avatar-editor/AvatarEditorView';
import { CameraWidgetView } from '../../components/camera/CameraWidgetView';
import { CatalogView } from '../../components/catalog/CatalogView'; import { CatalogView } from '../../components/catalog/CatalogView';
import { InventoryView } from '../../components/inventory/InventoryView'; import { InventoryView } from '../../components/inventory/InventoryView';
import { NavigatorView } from '../../components/navigator/NavigatorView'; import { NavigatorView } from '../../components/navigator/NavigatorView';
import { ToolbarView } from '../../components/toolbar/ToolbarView'; import { ToolbarView } from '../../components/toolbar/ToolbarView';
import { useRoomSessionManagerEvent } from '../../hooks/events/nitro/session/room-session-manager-event'; import { useRoomSessionManagerEvent } from '../../hooks/events/nitro/session/room-session-manager-event';
import { TransitionAnimation, TransitionAnimationTypes } from '../../layout'; import { TransitionAnimation, TransitionAnimationTypes } from '../../layout';
import { CameraWidgetView } from '../camera/CameraWidgetView';
import { CampaignView } from '../campaign/CampaignView'; import { CampaignView } from '../campaign/CampaignView';
import { ChatHistoryView } from '../chat-history/ChatHistoryView'; import { ChatHistoryView } from '../chat-history/ChatHistoryView';
import { FloorplanEditorView } from '../floorplan-editor/FloorplanEditorView'; import { FloorplanEditorView } from '../floorplan-editor/FloorplanEditorView';