diff --git a/public/functions/index.js b/public/functions/index.js index d20c1225..79351ec3 100644 --- a/public/functions/index.js +++ b/public/functions/index.js @@ -51,7 +51,6 @@ exports.getScene = functions const documentId = req.path .split('/') .filter(Boolean)[1] - .replace('.json', ''); if (!documentId) { res.status(400).send({ error: 'Scene ID is required' }); return; diff --git a/src/editor/api/auth.js b/src/editor/api/auth.js index 99aab55a..16394fef 100644 --- a/src/editor/api/auth.js +++ b/src/editor/api/auth.js @@ -9,9 +9,6 @@ import posthog from 'posthog-js'; const signIn = async () => { try { const { user } = await signInWithPopup(auth, new GoogleAuthProvider()); - STREET.notify.successMessage( - `Successful login with Google authentication.` - ); // first signIn to ga if (user.metadata.creationTime !== user.metadata.lastSignInTime) return; posthog.capture('user_signed_up', { @@ -32,6 +29,7 @@ const signIn = async () => { ); console.error(error); } + throw error; } }; diff --git a/src/editor/api/scene.js b/src/editor/api/scene.js index f5c145ef..5fe4fd8c 100644 --- a/src/editor/api/scene.js +++ b/src/editor/api/scene.js @@ -196,7 +196,7 @@ const saveScreenshot = async (value) => { screenshotEl.setAttribute('screentock', 'takeScreenshot', true); }; -const uploadThumbnailImage = async () => { +const uploadThumbnailImage = async (sceneDocId) => { try { // saveScreenshot('img'); @@ -238,8 +238,6 @@ const uploadThumbnailImage = async () => { const thumbnailDataUrl = resizedCanvas.toDataURL('image/jpeg', 0.5); const blobFile = await fetch(thumbnailDataUrl).then((res) => res.blob()); - const sceneDocId = STREET.utils.getCurrentSceneId(); - const thumbnailRef = ref(storage, `scenes/${sceneDocId}/files/preview.jpg`); const uploadedImg = await uploadBytes(thumbnailRef, blobFile); diff --git a/src/editor/components/Main.js b/src/editor/components/Main.js index f48a274e..3a80e89a 100644 --- a/src/editor/components/Main.js +++ b/src/editor/components/Main.js @@ -17,6 +17,7 @@ import { PaymentModal } from './modals/PaymentModal'; import { SceneEditTitle } from './components/SceneEditTitle'; import { AddLayerPanel } from './components/AddLayerPanel'; import { IntroModal } from './modals/IntroModal'; +import { NewModal } from './modals/NewModal'; import { ToolbarWrapper } from './scenegraph/ToolbarWrapper.js'; import useStore from '@/store'; @@ -42,8 +43,6 @@ export default function Main() { ); htmlEditorButton && htmlEditorButton.remove(); - handleStreetMixURL(); - window.addEventListener('hashchange', () => handleStreetMixURL()); Events.on('opentexturesmodal', function (selectedTexture, textureOnClose) { setState((prevState) => ({ ...prevState, @@ -95,15 +94,6 @@ export default function Main() { }); }, []); - const handleStreetMixURL = () => { - const isStreetMix = window.location.hash.includes('streetmix'); - if (isStreetMix) { - STREET.notify.warningMessage( - 'Hit save if you want to save changes to the scene. Otherwise changes will be lost' - ); - } - }; - const onModalTextureOnClose = (value) => { setState((prevState) => ({ ...prevState, @@ -141,6 +131,7 @@ export default function Main() { + { + const [savedScene, setSavedScene] = useState(false); + const [isSaveActionActive, setIsSaveActionActive] = useState(false); + const { isSavingScene, doSaveAs, setModal, saveScene, postSaveScene } = + useStore(); + + useEffect(() => { + if (savedScene) { + debounce(() => { + setSavedScene(false); + }, 1000); + } + }, [savedScene]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + const autoSaveScene = debounce((cmd) => { + if (cmd) { + if (currentUser && STREET.utils.getAuthorId() === currentUser.uid) { + const streetGeo = document + .getElementById('reference-layers') + ?.getAttribute('street-geo'); + if ( + !currentUser.isPro && + streetGeo && + streetGeo['latitude'] && + streetGeo['longitude'] + ) { + setModal('payment'); + return; + } + saveScene(false); + } + } + }, 1000); + Events.on('historychanged', autoSaveScene); + return () => { + Events.off('historychanged', autoSaveScene); + }; + }, [currentUser]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (isSavingScene) { + handleSave(doSaveAs); + } + }, [isSavingScene]); // eslint-disable-line react-hooks/exhaustive-deps + // if (isSavingScene) { + // // Events.on('historychanged', (cmd) => { + // // if (cmd) { + // // // Debounce the cloudSaveHandler call + + // // // this.debouncedCloudSaveHandler(); + // // } + // // }); + + const toggleSaveActionState = () => { + setIsSaveActionActive(!isSaveActionActive); + }; + + const isAuthor = () => { + return currentUser?.uid === STREET.utils.getAuthorId(); + }; + + const handleSave = async (saveAs) => { + try { + await saveSceneWithScreenshot(currentUser, saveAs); + } catch (error) { + STREET.notify.errorMessage( + `Error trying to save 3DStreet scene to cloud. Error: ${error}` + ); + console.error(error); + } finally { + postSaveScene(); + setSavedScene(true); + } + }; + + const handleUnsignedSave = () => { + setModal('signin'); + }; + + return ( +
+ {currentUser ? ( +
+ {isSavingScene ? ( + + ) : ( + + )} + {isSaveActionActive && ( +
+ + +
+ )} +
+ ) : ( + + )} +
+ ); +}; diff --git a/src/editor/components/components/Save/index.js b/src/editor/components/components/Save/index.js new file mode 100644 index 00000000..74614a72 --- /dev/null +++ b/src/editor/components/components/Save/index.js @@ -0,0 +1 @@ +export { Save } from './Save.component.jsx'; diff --git a/src/editor/components/components/SceneCard/SceneCard.module.scss b/src/editor/components/components/SceneCard/SceneCard.module.scss index 9c19bcdf..8a768c9e 100644 --- a/src/editor/components/components/SceneCard/SceneCard.module.scss +++ b/src/editor/components/components/SceneCard/SceneCard.module.scss @@ -25,12 +25,12 @@ cursor: pointer; &:hover { - border: 1px solid variables.$purple-100; + background-color: variables.$purple-100; transition: ease-out 0.3s; } &:active { - border-color: variables.$purple-200; + background-color: variables.$purple-200; transition: all 0.3s; } diff --git a/src/editor/components/modals/NewModal/NewModal.component.jsx b/src/editor/components/modals/NewModal/NewModal.component.jsx new file mode 100644 index 00000000..a521bf3d --- /dev/null +++ b/src/editor/components/modals/NewModal/NewModal.component.jsx @@ -0,0 +1,83 @@ +import Modal from '../Modal.jsx'; +import useStore from '@/store.js'; +import styles from './NewModal.module.scss'; +import ScenePlaceholder from '@/../ui_assets/ScenePlaceholder.svg'; +import { inputStreetmix } from '@/editor/lib/SceneUtils.js'; +import { Button } from '@/editor/components/components'; +import { Upload24Icon } from '@/editor/icons'; + +export const NewModal = () => { + const setModal = useStore((state) => state.setModal); + const isOpen = useStore((state) => state.modal === 'new'); + const saveScene = useStore((state) => state.saveScene); + const onClose = () => { + setModal(null); + }; + + const onClickNew = () => { + setModal(null); + AFRAME.INSPECTOR.selectEntity(null); + useStore.getState().newScene(); + STREET.utils.newScene(); + AFRAME.scenes[0].emit('newScene'); + }; + + const scenesData = [ + { + title: 'Create Blank Scene', + imagePath: '/ui_assets/cards/new-blank.jpg', + onClick: onClickNew + }, + { + title: 'Import From Streetmix', + imagePath: '/ui_assets/cards/new-streetmix-import.jpg', + onClick: inputStreetmix + } + ]; + + return ( + +
+ Create a New Scene +
+ + + } + > +
+ {scenesData?.map((scene, index) => ( +
+
{ + scene.onClick(event); + saveScene(true); + onClose(); + }} + style={{ + backgroundImage: `url(${scene.imagePath || ScenePlaceholder})`, + backgroundSize: 'cover', + backgroundPosition: 'center' + }} + /> +
+

{scene.title}

+
+
+ ))} +
+ + ); +}; diff --git a/src/editor/components/modals/NewModal/NewModal.module.scss b/src/editor/components/modals/NewModal/NewModal.module.scss new file mode 100644 index 00000000..222a87fd --- /dev/null +++ b/src/editor/components/modals/NewModal/NewModal.module.scss @@ -0,0 +1,135 @@ +@use '../../../style/variables.scss'; + +.wrapper { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + row-gap: 24px; + column-gap: 20px; + padding-bottom: 20px; + + .img { + margin-bottom: 6px; + object-fit: cover; + height: 204px; + } + + .card { + border-radius: 4px; + border: 1px solid rgba(237, 235, 239, 0.2); + padding: 8px 8px 12px 8px; + width: 272px; + height: 292px; + position: relative; + cursor: pointer; + + &:hover { + background-color: variables.$purple-100; + transition: ease-out 0.3s; + } + + &:active { + background-color: variables.$purple-200; + transition: all 0.3s; + } + + &:disabled { + border: 1px solid rgba(237, 235, 239, 0.5); + transition: all 0.3s; + } + } + + .menuBlock { + display: flex; + flex-direction: column; + width: 189px; + border-radius: 8px; + border: 1px solid variables.$purple-700; + position: absolute; + height: 70px; + width: 190px; + background-color: variables.$darkgray-300; + padding: 6px 0px; + top: 140px; + right: -75px; + z-index: 10; + + .menuItem { + font-size: 16px; + padding: 8px 8px 8px 20px; + + &:hover { + background-color: variables.$black-400; + transition: all 0.3s; + } + + &:active { + background-color: variables.$black-700; + color: variables.$lightgray-200; + transition: all 0.3s; + } + + &:disabled { + background-color: rgba(50, 50, 50, 0.6); + color: variables.$gray-500; + transition: all 0.3s; + } + } + } + + .userBlock { + display: flex; + align-items: center; + justify-content: space-between; + + .dropdown { + width: 20px; + padding: 0px; + background: none; + border-radius: 0; + } + + .userName { + display: flex; + align-items: center; + column-gap: 8px; + } + } + + .editButtons { + display: flex; + justify-content: space-between; + align-items: center; + column-gap: 8px; + + .editButton { + width: 100%; + border-radius: 10px; + } + } + + .editInput { + font-size: 16px !important; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + margin-bottom: 12px; + width: 100%; + } + + .title { + font-size: 16px !important; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + margin-bottom: 12px; + } + + .date { + margin-top: 4px; + color: rgb(116, 116, 116); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } +} diff --git a/src/editor/components/modals/NewModal/index.js b/src/editor/components/modals/NewModal/index.js new file mode 100644 index 00000000..221fc67c --- /dev/null +++ b/src/editor/components/modals/NewModal/index.js @@ -0,0 +1 @@ +export { NewModal } from './NewModal.component.jsx'; diff --git a/src/editor/components/modals/ScenesModal/ScenesModal.component.jsx b/src/editor/components/modals/ScenesModal/ScenesModal.component.jsx index 0058f6be..8c81461a 100644 --- a/src/editor/components/modals/ScenesModal/ScenesModal.component.jsx +++ b/src/editor/components/modals/ScenesModal/ScenesModal.component.jsx @@ -3,16 +3,14 @@ import { useAuthContext } from '../../../contexts'; import { Button, SceneCard, Tabs } from '../../components'; import Modal from '../Modal.jsx'; import styles from './ScenesModal.module.scss'; -import { - createElementsForScenesFromJSON, - fileJSON, - inputStreetmix -} from '@/editor/lib/SceneUtils.js'; +import { createElementsForScenesFromJSON } from '@/editor/lib/SceneUtils.js'; import { getCommunityScenes, getUserScenes } from '../../../api/scene'; -import { Load24Icon, Loader, Upload24Icon } from '../../../icons'; +import { Edit32Icon, Loader, Upload24Icon } from '../../../icons'; import { signIn } from '../../../api'; import posthog from 'posthog-js'; import useStore from '../../../../store.js'; +import { fileJSON } from '@/editor/lib/SceneUtils'; + const SCENES_PER_PAGE = 20; const tabs = [ { @@ -57,12 +55,12 @@ const ScenesModal = ({ initialTab = 'owner', delay = undefined }) => { if (event.ctrlKey || event.metaKey) { localStorage.setItem('sceneData', JSON.stringify(sceneData.data)); - const newTabUrl = `#/scenes/${scene.id}.json`; + const newTabUrl = `#/scenes/${scene.id}`; const newTab = window.open(newTabUrl, '_blank'); newTab.focus(); } else { createElementsForScenesFromJSON(sceneData.data); - window.location.hash = `#/scenes/${scene.id}.json`; + window.location.hash = `#/scenes/${scene.id}`; const sceneId = scene.id; const sceneTitle = sceneData.title; @@ -103,11 +101,9 @@ const ScenesModal = ({ initialTab = 'owner', delay = undefined }) => { useEffect(() => { const fetchData = async () => { - console.log({ scenesData, scenesDataCommunity }); if (isOpen) { let collections; setIsLoadingScenes(true); - try { if ( selectedTab === 'owner' && @@ -158,7 +154,6 @@ const ScenesModal = ({ initialTab = 'owner', delay = undefined }) => { if (selectedTab === 'owner') { const userScenes = await fetchUserScenes(); - setScenesData([...scenesData, ...userScenes]); setTotalDisplayedUserScenes(end); } else if (selectedTab === 'community') { @@ -184,7 +179,6 @@ const ScenesModal = ({ initialTab = 'owner', delay = undefined }) => { loadData(end); } }; - return renderComponent ? ( {
diff --git a/src/editor/components/modals/SignInModal/SignInModal.component.jsx b/src/editor/components/modals/SignInModal/SignInModal.component.jsx index 675d6df4..1842c747 100644 --- a/src/editor/components/modals/SignInModal/SignInModal.component.jsx +++ b/src/editor/components/modals/SignInModal/SignInModal.component.jsx @@ -3,6 +3,8 @@ import Modal from '../Modal.jsx'; import styles from './SignInModal.module.scss'; import { signIn, signInMicrosoft } from '../../../api'; import useStore from '@/store'; +import { saveSceneWithScreenshot } from '@/editor/lib/SceneUtils'; +import { auth } from '@/editor/services/firebase'; const SignInModal = () => { const setModal = useStore((state) => state.setModal); const modal = useStore((state) => state.modal); @@ -11,6 +13,17 @@ const SignInModal = () => { setModal(null); }; + const onSignInClick = async (provider = 'google') => { + if (provider === 'google') { + await signIn(); + } else if (provider === 'microsoft') { + await signInMicrosoft(); + } + if (STREET.utils.getCurrentSceneId() !== null) { + await saveSceneWithScreenshot(auth.currentUser, true); + } + onClose(); + }; return ( { onClose={onClose} >
-

Sign in to 3DStreet Cloud

+

Sign in

-

- Save and share your street scenes by clicking on a provider below to - log-in or automatically create a{' '} - - 3DStreet Cloud account - {' '} - if you don't already have one. -

+

Sign in to save your project.

{ - signIn(); - onClose(); + onSignInClick('google'); }} alt="Sign In with Google Button" className={styles.signInButton} @@ -45,8 +46,7 @@ const SignInModal = () => {
{ - signInMicrosoft(); - onClose(); + onSignInClick('microsoft'); }} alt="Sign In with Microsoft Button" className={styles.signInButton} diff --git a/src/editor/components/modals/SignInModal/SignInModal.module.scss b/src/editor/components/modals/SignInModal/SignInModal.module.scss index a3762e21..89a18bec 100644 --- a/src/editor/components/modals/SignInModal/SignInModal.module.scss +++ b/src/editor/components/modals/SignInModal/SignInModal.module.scss @@ -8,6 +8,7 @@ .contentWrapper { display: flex; + width: 100%; flex-direction: column; align-items: center; row-gap: 28px; diff --git a/src/editor/components/scenegraph/Toolbar.js b/src/editor/components/scenegraph/Toolbar.js index 36a5dbbf..c049bf00 100644 --- a/src/editor/components/scenegraph/Toolbar.js +++ b/src/editor/components/scenegraph/Toolbar.js @@ -1,385 +1,76 @@ -import React, { Component } from 'react'; -import { - createScene, - updateScene, - checkIfImagePathIsEmpty, - uploadThumbnailImage -} from '../../api/scene'; -import { - Cloud24Icon, - Save24Icon, - ScreenshotIcon, - Upload24Icon, - Edit24Icon -} from '../../icons'; -import Events from '../../lib/Events'; +import { ScreenshotIcon, Upload24Icon, Edit24Icon } from '../../icons'; import { Button, ProfileButton, Logo } from '../components'; import posthog from 'posthog-js'; import { UndoRedo } from '../components/UndoRedo'; -import debounce from 'lodash-es/debounce'; import { CameraToolbar } from '../viewport/CameraToolbar'; import useStore from '@/store'; +import { makeScreenshot } from '@/editor/lib/SceneUtils'; +import { Save } from '@/editor/components/components/Save'; -// const LOCALSTORAGE_MOCAP_UI = "aframeinspectormocapuienabled"; +function Toolbar({ currentUser }) { + const { isSavingScene, setModal, isInspectorEnabled } = useStore(); -/** - * Tools and actions. - */ -export default class Toolbar extends Component { - constructor(props) { - super(props); - this.state = { - isSaveActionActive: false, - showLoadBtn: true, - isSavingScene: false, - savedScene: false, - pendingSceneSave: false, - inspectorEnabled: true - }; - this.saveButtonRef = React.createRef(); - } - - componentDidMount() { - document.addEventListener('click', this.handleClickOutsideSave); - Events.on('historychanged', (cmd) => { - if (cmd) { - // Debounce the cloudSaveHandler call - this.debouncedCloudSaveHandler(); - } - }); - // Subscribe to store changes - this.unsubscribe = useStore.subscribe( - (state) => state.isInspectorEnabled, - (isInspectorEnabled) => { - this.setState({ inspectorEnabled: isInspectorEnabled }); - } - ); - } - - componentDidUpdate(prevProps) { - if (this.props.currentUser !== prevProps.currentUser) { - if (this.state.pendingSceneSave && this.props.currentUser) { - // Remove the flag from state, as we're going to handle the save now. - this.setState({ pendingSceneSave: false }); - this.cloudSaveHandlerWithImageUpload(); - } - } - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleClickOutsideSave); - // Unsubscribe from store changes - if (this.unsubscribe) { - this.unsubscribe(); - } - } - - isAuthor = () => { - return this.props.currentUser?.uid === STREET.utils.getAuthorId(); - }; - - handleClickOutsideSave = (event) => { - if ( - this.saveButtonRef.current && - !this.saveButtonRef.current.contains(event.target) - ) { - this.setState({ isSaveActionActive: false }); - } - }; - - cloudSaveHandlerWithImageUpload = async (doSaveAs) => { - this.makeScreenshot(); - const currentSceneId = await this.cloudSaveHandler({ doSaveAs }); - const isImagePathEmpty = await checkIfImagePathIsEmpty(currentSceneId); - if (isImagePathEmpty) { - await uploadThumbnailImage(); - } - }; - - newHandler = () => { + const newHandler = () => { posthog.capture('new_scene_clicked'); - AFRAME.INSPECTOR.selectEntity(null); - useStore.getState().newScene(); - STREET.utils.newScene(); - AFRAME.scenes[0].emit('newScene'); + useStore.getState().setModal('new'); }; - cloudSaveHandler = async ({ doSaveAs = false }) => { - try { - // if there is no current user, show sign in modal - let currentSceneId = STREET.utils.getCurrentSceneId(); - let currentSceneTitle = useStore.getState().sceneTitle; - - posthog.capture('save_scene_clicked', { - save_as: doSaveAs, - user_id: this.props.currentUser ? this.props.currentUser.uid : null, - scene_id: currentSceneId, - scene_title: currentSceneTitle - }); - - if (!this.props.currentUser) { - console.log('no user'); - useStore.getState().setModal('signin'); - return; - } - - // check if the user is not pro, and if the geospatial has array of values of mapbox - const streetGeo = document - .getElementById('reference-layers') - ?.getAttribute('street-geo'); - if ( - !this.props.currentUser.isPro && - streetGeo && - streetGeo['latitude'] && - streetGeo['longitude'] - ) { - useStore.getState().setModal('payment'); - return; - } - if (!this.isAuthor()) { - posthog.capture('not_scene_author', { - scene_id: currentSceneId, - user_id: this.props.currentUser.uid - }); - doSaveAs = true; - } - - // generate json from 3dstreet core - const entity = document.getElementById('street-container'); - const data = STREET.utils.convertDOMElToObject(entity); - const filteredData = JSON.parse(STREET.utils.filterJSONstreet(data)); - this.setState({ isSavingScene: true }); - - // we want to save, so if we *still* have no sceneID at this point, then create a new one - if (!currentSceneId || !!doSaveAs) { - // ask user for scene title here currentSceneTitle - let newSceneTitle = prompt('Scene Title:', currentSceneTitle || ''); - - if (newSceneTitle) { - currentSceneTitle = newSceneTitle; - } - - useStore.getState().setSceneTitle(currentSceneTitle); - console.log( - 'no urlSceneId or doSaveAs is true, therefore generate new one' - ); - currentSceneId = await createScene( - this.props.currentUser.uid, - filteredData.data, - currentSceneTitle, - filteredData.version - ); - console.log('newly generated currentSceneId', currentSceneId); - } else { - await updateScene( - currentSceneId, - filteredData.data, - currentSceneTitle, - filteredData.version - ); - } - - // after all those save shenanigans let's set currentSceneId in state - this.setState({ currentSceneId }); - - // save json to firebase with other metadata + const isEditor = !!isInspectorEnabled; - // make sure to update sceneId with new one in metadata component! - AFRAME.scenes[0].setAttribute('metadata', 'sceneId', currentSceneId); - AFRAME.scenes[0].setAttribute( - 'metadata', - 'authorId', - this.props.currentUser.uid - ); - - // Change the hash URL without reloading - window.location.hash = `#/scenes/${currentSceneId}.json`; - this.setState({ isSaveActionActive: false }); - this.setState({ savedScene: true }); - this.setSavedSceneFalse(); - - return currentSceneId; - } catch (error) { - STREET.notify.errorMessage( - `Error trying to save 3DStreet scene to cloud. Error: ${error}` - ); - console.error(error); - } finally { - this.setState({ isSavingScene: false }); - } - }; - - setSavedSceneFalse = debounce(() => { - this.setState({ savedScene: false }); - }, 500); - - debouncedCloudSaveHandler = debounce(() => { - if ( - this.props.currentUser && - STREET.utils.getAuthorId() === this.props.currentUser.uid - ) { - const streetGeo = document - .getElementById('reference-layers') - ?.getAttribute('street-geo'); - if ( - !this.props.currentUser.isPro && - streetGeo && - streetGeo['latitude'] && - streetGeo['longitude'] - ) { - useStore.getState().setModal('payment'); - return; - } - this.cloudSaveHandler({ doSaveAs: false }); - } - }, 1000); - - handleUnsignedSaveClick = () => { - posthog.capture('remix_scene_clicked'); - this.setState({ pendingSceneSave: true }); - useStore.getState().setModal('signin'); - }; - - makeScreenshot = () => { - const imgHTML = ''; - // Set the screenshot in local storage - localStorage.setItem('screenshot', JSON.stringify(imgHTML)); - const screenshotEl = document.getElementById('screenshot'); - screenshotEl.play(); - - screenshotEl.setAttribute('screentock', 'type', 'img'); - screenshotEl.setAttribute( - 'screentock', - 'imgElementSelector', - '#screentock-destination' - ); - // take the screenshot - screenshotEl.setAttribute('screentock', 'takeScreenshot', true); - }; - - toggleSaveActionState = () => { - this.setState((prevState) => ({ - isSaveActionActive: !prevState.isSaveActionActive - })); - }; - - render() { - const isEditor = !!this.state.inspectorEnabled; - return ( -
-
-
- -
- {isEditor && ( - <> -
- -
-
- - {this.props.currentUser ? ( -
- {this.state.savedScene ? ( - - ) : ( - - )} - {this.state.isSaveActionActive && ( -
- - -
- )} -
- ) : ( - - )} - {this.state.showLoadBtn && ( - - )} - -
- this.setState((prevState) => ({ - ...prevState, - isSignInModalActive: true - })) - } - > - -
-
- - )} + return ( +
+
+
+
{isEditor && ( -
- -
+ <> +
+ +
+
+ + + + +
setModal('profile')}> + +
+
+ )}
- ); - } + {isEditor && ( +
+ +
+ )} +
+ ); } + +export default Toolbar; diff --git a/src/editor/lib/SceneUtils.js b/src/editor/lib/SceneUtils.js index e558bbca..1c8a7096 100644 --- a/src/editor/lib/SceneUtils.js +++ b/src/editor/lib/SceneUtils.js @@ -1,3 +1,11 @@ +import posthog from 'posthog-js'; +import useStore from '@/store.js'; +import { + createScene, + updateScene, + uploadThumbnailImage +} from '@/editor/api/scene'; + export function inputStreetmix() { const streetmixURL = prompt( 'Please enter a Streetmix URL', @@ -72,3 +80,97 @@ export function convertToObject() { console.error(error); } } + +export function makeScreenshot() { + const imgHTML = ''; + // Set the screenshot in local storage + localStorage.setItem('screenshot', JSON.stringify(imgHTML)); + const screenshotEl = document.getElementById('screenshot'); + screenshotEl.play(); + + screenshotEl.setAttribute('screentock', 'type', 'img'); + screenshotEl.setAttribute( + 'screentock', + 'imgElementSelector', + '#screentock-destination' + ); + // take the screenshot + screenshotEl.setAttribute('screentock', 'takeScreenshot', true); +} + +export async function saveScene(currentUser, doSaveAs) { + const sceneTitle = useStore.getState().sceneTitle; + const authorId = STREET.utils.getAuthorId(); + let sceneId = STREET.utils.getCurrentSceneId(); + + posthog.capture('saving_scene', { + save_as: doSaveAs, + user_id: currentUser ? currentUser.uid : null, + scene_id: sceneId, + scene_title: sceneTitle + }); + + if (!currentUser) { + useStore.getState().setModal('signin'); + return; + } + + // check if the user is not pro, and if the geospatial has array of values of mapbox + const streetGeo = document + .getElementById('reference-layers') + ?.getAttribute('street-geo'); + if ( + !currentUser.isPro && + streetGeo && + streetGeo['latitude'] && + streetGeo['longitude'] + ) { + useStore.getState().setModal('payment'); + return; + } + if (authorId !== currentUser.uid) { + // posthog.capture('not_scene_author', { + // scene_id: sceneId, + // user_id: currentUser.uid + // }); + doSaveAs = true; + } + + // generate json from 3dstreet core + const entity = document.getElementById('street-container'); + const data = STREET.utils.convertDOMElToObject(entity); + const filteredData = JSON.parse(STREET.utils.filterJSONstreet(data)); + + // we want to save, so if we *still* have no sceneID at this point, then create a new one + if (!sceneId || !!doSaveAs) { + sceneId = await createScene( + currentUser.uid, + filteredData.data, + sceneTitle, + filteredData.version + ); + } else { + await updateScene( + sceneId, + filteredData.data, + sceneTitle, + filteredData.version + ); + } + + // make sure to update sceneId with new one in metadata component! + AFRAME.scenes[0].setAttribute('metadata', 'sceneId', sceneId); + AFRAME.scenes[0].setAttribute('metadata', 'authorId', currentUser.uid); + + // Change the hash URL without reloading + window.location.hash = `#/scenes/${sceneId}`; + return sceneId; +} + +export async function saveSceneWithScreenshot(currentUser, doSaveAs) { + makeScreenshot(); + const currentSceneId = await saveScene(currentUser, doSaveAs); + if (currentSceneId) { + uploadThumbnailImage(currentSceneId); + } +} diff --git a/src/index.js b/src/index.js index cdbb7b76..a217dd5b 100644 --- a/src/index.js +++ b/src/index.js @@ -29,6 +29,7 @@ require('./components/street-generated-pedestrians.js'); require('./components/street-generated-rail.js'); require('./components/street-generated-clones.js'); require('./editor/index.js'); +var firebase = require('./editor/services/firebase.js'); const state = useStore.getState(); @@ -110,6 +111,7 @@ AFRAME.registerComponent('street', { if (buildingParent) { buildingParent.remove(); } + console.log(firebase.auth.currentUser); const streetEl = streetmixParsers.processSegments( streetmixSegments.streetmixSegmentsMetric, @@ -134,8 +136,12 @@ AFRAME.registerComponent('street', { ); this.el.append(buildingsEl); } + // the scene has been loaded, set the synchronize flag this.el.setAttribute('street', 'synchronize', false); + setTimeout(() => { + state.saveScene(true); + }, 1000); } }); @@ -166,7 +172,6 @@ AFRAME.registerComponent('streetmix-loader', { // console.log('[streetmix-loader]', 'Neither streetmixStreetURL nor streetmixAPIURL have changed in this component data update, not reloading street.') return; } - // if no value for 'streetmixAPIURL' then let's see if there's a streetmixURL if (data.streetmixAPIURL.length === 0) { if (data.streetmixStreetURL.length > 0) { diff --git a/src/json-utils_1.1.js b/src/json-utils_1.1.js index ff3fe553..ebabe72b 100644 --- a/src/json-utils_1.1.js +++ b/src/json-utils_1.1.js @@ -7,7 +7,7 @@ var assetsUrl; STREET.utils = {}; function getSceneUuidFromURLHash() { const currentHash = window.location.hash; - const match = currentHash.match(/#\/scenes\/([a-zA-Z0-9-]+)\.json/); + const match = currentHash.match(/#\/scenes\/([a-zA-Z0-9-]+)/); return match && match[1] ? match[1] : null; } @@ -519,7 +519,10 @@ AFRAME.registerComponent('set-loader-from-hash', { 'Load 3DStreet scene with fetchJSON from', streetURL ); - this.fetchJSON(streetURL); + const jsonURL = streetURL.endsWith('.json') + ? streetURL + : `${streetURL}.json`; + this.fetchJSON(jsonURL); } // else { // console.log('[set-loader-from-hash]','Using default URL', this.data.defaultURL) diff --git a/src/store.js b/src/store.js index 8da62d95..7e762eea 100644 --- a/src/store.js +++ b/src/store.js @@ -6,7 +6,7 @@ const firstModal = () => { let modal = window.location.hash.includes('payment') ? 'payment' : !window.location.hash.length - ? 'scenes' + ? 'new' : null; const isStreetMix = window.location.hash.includes('streetmix'); if (isStreetMix) { @@ -21,6 +21,11 @@ const useStore = create( (set) => ({ sceneId: null, setSceneId: (newSceneId) => set({ sceneId: newSceneId }), + isSavingScene: false, + saveScene: (newDoSaveAs) => + set({ isSavingScene: true, doSaveAs: newDoSaveAs }), + postSaveScene: () => set({ isSavingScene: false, doSaveAs: false }), + doSaveAs: false, sceneTitle: null, setSceneTitle: (newSceneTitle) => set({ sceneTitle: newSceneTitle }), newScene: () => diff --git a/ui_assets/cards/new-blank.jpg b/ui_assets/cards/new-blank.jpg new file mode 100644 index 00000000..546ee1d0 Binary files /dev/null and b/ui_assets/cards/new-blank.jpg differ diff --git a/ui_assets/cards/new-streetmix-import.jpg b/ui_assets/cards/new-streetmix-import.jpg new file mode 100644 index 00000000..772256c9 Binary files /dev/null and b/ui_assets/cards/new-streetmix-import.jpg differ