From 0e8f7d384d7440dffecb8edb84d7f4c8cb116489 Mon Sep 17 00:00:00 2001 From: netpro2k Date: Tue, 26 Jan 2021 16:19:52 -0800 Subject: [PATCH 1/4] Add support for creating scenes directly from GLBs --- src/api/Api.js | 68 ++++++ src/ui/App.js | 2 + src/ui/inputs/FileInput.js | 29 ++- src/ui/projects/CreateProjectPage.js | 3 + src/ui/projects/CreateScenePage.js | 344 +++++++++++++++++++++++++++ src/ui/projects/ProjectGridItem.js | 28 ++- src/ui/projects/ProjectsPage.js | 5 +- 7 files changed, 471 insertions(+), 8 deletions(-) create mode 100644 src/ui/projects/CreateScenePage.js diff --git a/src/api/Api.js b/src/api/Api.js index 848ff105c..9d313cedd 100644 --- a/src/api/Api.js +++ b/src/api/Api.js @@ -955,6 +955,74 @@ export default class Project extends EventEmitter { return project; } + async publishGLBScene(screenshotFile, glbFile, params, signal, sceneId) { + let screenshotId, screenshotToken; + if (screenshotFile) { + const { + file_id, + meta: { access_token } + } = await this.upload(screenshotFile, null, signal); + screenshotId = file_id; + screenshotToken = access_token; + } + + let glbId, glbToken; + if (glbFile) { + const { + file_id, + meta: { access_token } + } = await this.upload(glbFile, null, signal); + glbId = file_id; + glbToken = access_token; + } + + const headers = { + "content-type": "application/json", + authorization: `Bearer ${this.getToken()}` + }; + + // HACK: Create a dummy project to add this to project listings + let project_id; + if (!sceneId) { + const body = JSON.stringify({ + project: { name: "GLB Only Project" } + }); + const resp = await this.fetch(`https://${RETICULUM_SERVER}/api/v1/projects`, { + method: "POST", + headers, + body, + signal + }); + const project = await resp.json(); + project_id = project.project_id; + } + + const sceneParams = { + screenshot_file_id: screenshotId, + screenshot_file_token: screenshotToken, + model_file_id: glbId, + model_file_token: glbToken, + allow_remixing: params.allowRemixing, + allow_promotion: params.allowPromotion, + name: params.name, + project_id, + attributions: { + creator: params.creatorAttribution, + content: [] + } + }; + + const body = JSON.stringify({ scene: sceneParams }); + + const resp = await this.fetch(`https://${RETICULUM_SERVER}/api/v1/scenes${sceneId ? "/" + sceneId : ""}`, { + method: sceneId ? "PUT" : "POST", + headers, + body + }); + + return resp.json(); + } + async upload(blob, onUploadProgress, signal) { // Use direct upload API, see: https://github.com/mozilla/reticulum/pull/319 const { phx_host: uploadHost } = await (await this.fetch(`https://${RETICULUM_SERVER}/api/v1/meta`)).json(); diff --git a/src/ui/App.js b/src/ui/App.js index dfb0ff6b0..04b5b6b09 100644 --- a/src/ui/App.js +++ b/src/ui/App.js @@ -20,6 +20,7 @@ import LoginPage from "./auth/LoginPage"; import LogoutPage from "./auth/LogoutPage"; import ProjectsPage from "./projects/ProjectsPage"; import CreateProjectPage from "./projects/CreateProjectPage"; +import CreateScenePage from "./projects/CreateScenePage"; import { ThemeProvider } from "styled-components"; @@ -82,6 +83,7 @@ export default class App extends Component { + } /> diff --git a/src/ui/inputs/FileInput.js b/src/ui/inputs/FileInput.js index 3b8907c61..8b35e6636 100644 --- a/src/ui/inputs/FileInput.js +++ b/src/ui/inputs/FileInput.js @@ -1,18 +1,33 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; +import styled from "styled-components"; + import { Button } from "./Button"; -import Hidden from "../layout/Hidden"; let nextId = 0; +export const FileInputContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +// We do this instead of actually hiding it so that form validation can still display tooltips correctly +export const StyledInput = styled.input` + opacity: 0; + position: absolute; +`; + export default class FileInput extends Component { static propTypes = { label: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired + onChange: PropTypes.func.isRequired, + showSelectedFile: PropTypes.bool }; static defaultProps = { - label: "Upload..." + label: "Upload...", + showSelectedFile: false }; constructor(props) { @@ -24,6 +39,7 @@ export default class FileInput extends Component { } onChange = e => { + this.setState({ filename: e.target.files[0].name }); this.props.onChange(e.target.files, e); }; @@ -31,12 +47,13 @@ export default class FileInput extends Component { const { label, onChange, ...rest } = this.props; return ( -
+ - -
+ + {this.props.showSelectedFile && {this.state.filename ? this.state.filename : "No File chosen"}} + ); } } diff --git a/src/ui/projects/CreateProjectPage.js b/src/ui/projects/CreateProjectPage.js index d35dca192..a5238c1d8 100644 --- a/src/ui/projects/CreateProjectPage.js +++ b/src/ui/projects/CreateProjectPage.js @@ -122,6 +122,9 @@ export default function CreateProjectPage({ history, location }) { + diff --git a/src/ui/projects/CreateScenePage.js b/src/ui/projects/CreateScenePage.js new file mode 100644 index 000000000..bb89e0622 --- /dev/null +++ b/src/ui/projects/CreateScenePage.js @@ -0,0 +1,344 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import configs from "../../configs"; +import { withApi } from "../contexts/ApiContext"; +import NavBar from "../navigation/NavBar"; +import Footer from "../navigation/Footer"; +import styled from "styled-components"; + +import StringInput from "../inputs/StringInput"; +import BooleanInput from "../inputs/BooleanInput"; +import FileInput from "../inputs/FileInput"; +import FormField from "../inputs/FormField"; +import { Button } from "../inputs/Button"; +import ProgressBar from "../inputs/ProgressBar"; + +export const SceneUploadFormContainer = styled.div` + display: flex; + flex: 1; + flex-direction: row; + background-color: ${props => props.theme.panel2}; + border-radius: 3px; +`; + +export const UploadSceneSection = styled.form` + padding-bottom: 100px; + display: flex; + + &:first-child { + padding-top: 100px; + } + + h1 { + font-size: 36px; + } + + h2 { + font-size: 16px; + } +`; + +export const UploadSceneContainer = styled.form` + display: flex; + flex: 1; + flex-direction: column; + margin: 0 auto; + max-width: 800px; + min-width: 400px; + padding: 0 20px; +`; + +export const SceneUploadHeader = styled.div` + margin-bottom: 36px; + display: flex; + justify-content: space-between; + align-items: center; +`; + +const LeftContent = styled.div` + display: flex; + width: 360px; + border-top-left-radius: inherit; + align-items: flex-start; + padding: 30px; + position: relative; + + img, + div { + width: 300px; + height: 168px; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + background-color: ${props => props.theme.panel}; + border-radius: 6px; + } + input { + opacity: 0; + position: absolute; + } +`; + +const RightContent = styled.div` + display: flex; + flex-direction: column; + flex: 1; + padding: 30px 30px; + + label[type="button"] { + display: flex; + margin-bottom: 0; + margin-right: 5px; + } +`; + +class CreateScenePage extends Component { + static propTypes = { + api: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + match: PropTypes.object + }; + + constructor(props) { + super(props); + + const isAuthenticated = this.props.api.isAuthenticated(); + + this.state = { + isAuthenticated, + isUploading: false, + isLoading: true, + + sceneId: null, + + error: null, + + name: "", + creatorAttribution: "", + allowRemixing: false, + allowPromotion: false, + glbFile: null, + thumbnailFile: null, + + thumbnailUrl: null, + sceneUrl: null + }; + } + + async componentDidMount() { + const { match } = this.props; + const sceneId = match.params.sceneId; + const isNew = sceneId === "new"; + + const scene = isNew ? {} : await this.props.api.getScene(sceneId); + this.setState({ + name: scene.name || "", + creatorAttribution: scene.creatorAttribution || "", + allowRemixing: scene.allowRemixing, + allowPromotion: scene.allowPromotion, + thumbnailUrl: scene.screenshot_url, + sceneId: scene.scene_id, + sceneUrl: scene.url, + isLoading: false + }); + + console.log(sceneId, scene); + } + + onChangeName = name => this.setState({ name }); + + onChangeCreatorAttribution = creatorAttribution => this.setState({ creatorAttribution }); + + onChangeAllowRemixing = allowRemixing => this.setState({ allowRemixing }); + + onChangeAllowPromotion = allowPromotion => this.setState({ allowPromotion }); + + onChangeGlbFile = ([glbFile]) => this.setState({ glbFile }); + + onChangeThumbnailFile = ([thumbnailFile]) => { + if (this.state.thumbnailUrl && this.state.thumbnailUrl.indexOf("data:") === 0) { + URL.revokeObjectURL(this.state.thumbnailUrl); + } + + this.setState({ thumbnailFile }); + + // For preview + const reader = new FileReader(); + reader.onload = e => { + this.setState({ + thumbnailUrl: e.target.result + }); + }; + reader.readAsDataURL(thumbnailFile); + }; + + onPublish = async e => { + const API = this.props.api; + + e.preventDefault(); + console.log(this.state); + + this.setState({ isUploading: true }); + + const abortController = new AbortController(); + + const resp = await API.publishGLBScene( + this.state.thumbnailFile, + this.state.glbFile, + { + name: this.state.name, + allow_remixing: this.state.allowRemixing, + allow_promotion: this.state.allowPromotion, + attributions: { + creator: this.state.creatorAttribution, + content: [] + } + }, + abortController.signal, + this.state.sceneId + ); + + console.log(resp); + const scene = resp.scenes[0]; + this.setState({ + isUploading: false, + sceneId: scene.scene_id, + sceneUrl: scene.url, + glbFile: null, + thumbnailFile: null + }); + }; + + openScene = () => { + window.open(this.state.sceneUrl); + }; + + render() { + const { sceneId, sceneUrl, isLoading, isUploading } = this.state; + + const isNew = !sceneId; + + const { creatorAttribution, name, allowRemixing, allowPromotion } = this.state; + + const maxSize = this.props.api.maxUploadSize; + + const content = isLoading ? ( + + ) : ( + <> + +

{isNew ? "Publish Scene From GLB" : "Update GLB Scene"}

+ + {sceneUrl && ( + + )} +
+ + + + this.onChangeThumbnailFile(e.target.files)} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + {isUploading ? : } + + + + ); + + return ( + <> + +
+ + {content} + +
+