diff --git a/src/assets/stylesheets/info-dialog.scss b/src/assets/stylesheets/info-dialog.scss index 11c79e6de8bb8a08692be706a96b4216c77ca894..7b55cdab85c45eec62cd81d61876615abe616830 100644 --- a/src/assets/stylesheets/info-dialog.scss +++ b/src/assets/stylesheets/info-dialog.scss @@ -82,6 +82,32 @@ text-align: center; margin: 0; + &__input_fields { + position: relative; + } + + &__file { + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; + } + + &__file_label { + font-size: 1.25em; + font-weight: 700; + color: white; + background-color: black; + display: inline-block; + top: 19px; + right: 24px; + bottom: 19px; + position: absolute; + line-height: 30px; + } + &__buttons { display: flex; flex-direction: row; @@ -101,7 +127,7 @@ background-color: transparent; line-height: 2.0em; padding-left: 1.25em; - padding-right: 1.25em; + padding-right: 2.25em; margin: 0.5em 0; width: 100%; } diff --git a/src/components/media-loader.js b/src/components/media-loader.js index 544d4443a4304e0ed1d3ad75523c89cafa46c979..1ae37f321a5374919e9dc5fedd51ab722b245ffa 100644 --- a/src/components/media-loader.js +++ b/src/components/media-loader.js @@ -6,6 +6,8 @@ const fetchContentType = async url => fetch(url, { method: "HEAD" }).then(r => r AFRAME.registerComponent("media-loader", { schema: { src: { type: "string" }, + token: { type: "string" }, + contentType: { type: "string" }, resize: { default: false } }, @@ -46,17 +48,23 @@ AFRAME.registerComponent("media-loader", { async update() { try { const url = this.data.src; + const token = this.data.token; - this.showLoaderTimeout = setTimeout(() => { - const loadingObj = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial()); - this.el.setObject3D("mesh", loadingObj); - this.setShapeAndScale(true); - }, 100); + this.showLoaderTimeout = + this.showLoaderTimeout || + setTimeout(() => { + const loadingObj = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial()); + this.el.setObject3D("mesh", loadingObj); + this.setShapeAndScale(true); + }, 100); + + if (!url) return; const { raw, origin, meta } = await resolveMedia(url); console.log("resolved", url, raw, origin, meta); - const contentType = (meta && meta.expected_content_type) || (await fetchContentType(raw)); + const contentType = + this.data.contentType || (meta && meta.expected_content_type) || (await fetchContentType(raw)); if (contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/")) { this.el.addEventListener( "image-loaded", @@ -65,7 +73,16 @@ AFRAME.registerComponent("media-loader", { }, { once: true } ); - this.el.setAttribute("image-plus", { src: raw, contentType }); + let blobUrl; + if (token) { + const imageResponse = await fetch(raw, { + method: "GET", + headers: { Authorization: `Token ${token}` } + }); + const blob = await imageResponse.blob(); + blobUrl = window.URL.createObjectURL(blob); + } + this.el.setAttribute("image-plus", { src: blobUrl || raw, contentType }); this.el.setAttribute("position-at-box-shape-border", { target: ".delete-button", dirs: ["forward", "back"] }); } else if (contentType.startsWith("model/gltf") || url.endsWith(".gltf") || url.endsWith(".glb")) { this.el.addEventListener( diff --git a/src/hub.js b/src/hub.js index 3b4744abf0bd38a7361d6640aa3b5a6d878ccd5d..ac0acb54b32ee72454010a78e36ff989d79f2eb8 100644 --- a/src/hub.js +++ b/src/hub.js @@ -83,7 +83,7 @@ import HubChannel from "./utils/hub-channel"; import LinkChannel from "./utils/link-channel"; import { connectToReticulum } from "./utils/phoenix-utils"; import { disableiOSZoom } from "./utils/disable-ios-zoom"; -import { addMedia } from "./utils/media-utils"; +import { addMedia} from "./utils/media-utils"; import "./systems/personal-space-bubble"; import "./systems/app-mode"; @@ -303,8 +303,8 @@ const onReady = async () => { }); const offset = { x: 0, y: 0, z: -1.5 }; - const spawnMediaInfrontOfPlayer = url => { - const entity = addMedia(url, true); + const spawnMediaInfrontOfPlayer = async (src) => { + const entity = addMedia(src, true); entity.setAttribute("offset-relative-to", { target: "#player-camera", offset @@ -330,11 +330,9 @@ const onReady = async () => { document.addEventListener("drop", e => { e.preventDefault(); - const imgUrl = e.dataTransfer.getData("url"); - if (imgUrl) { - console.log("Dropped: ", imgUrl); - spawnMediaInfrontOfPlayer(imgUrl); - } + const url = e.dataTransfer.getData("url"); + const file = e.dataTransfer.files && e.dataTransfer.files[0]; + spawnMediaInfrontOfPlayer(url || file); }); } diff --git a/src/react-components/create-object-dialog.js b/src/react-components/create-object-dialog.js index f450f6f478e0907e8ebebcbde204be52260c5b1b..a7223d468668825d7c8771168e9703c427711cdd 100644 --- a/src/react-components/create-object-dialog.js +++ b/src/react-components/create-object-dialog.js @@ -13,7 +13,9 @@ let lastUrl = ""; export default class CreateObjectDialog extends Component { state = { - url: "" + url: "", + file: null, + text: "" }; static propTypes = { @@ -35,13 +37,25 @@ export default class CreateObjectDialog extends Component { if (e && e.target.value && e.target.value !== "") { this.setState({ url: e.target.value, + text: e.target.value, attributionImage: e.target.validity.valid && attributionHostnames[new URL(e.target.value).hostname] }); + } else { + this.setState({ + text: "" + }); } }; + onFileChange = (e) =>{ + this.setState({ + file: e.target.files[0], + text: e.target.files[0].name + }); + }; + onCreateClicked = () => { - this.props.onCreateObject(this.state.url || DEFAULT_OBJECT_URL); + this.props.onCreateObject(this.state.file || this.state.url || DEFAULT_OBJECT_URL); this.props.onCloseDialog(); }; @@ -60,14 +74,22 @@ export default class CreateObjectDialog extends Component { <form onSubmit={this.onCreateClicked}> <div className="add-media-form"> - <input - ref={el => (this.input = el)} - type="url" - placeholder="Image, Video, or GLTF URL" - className="add-media-form__link_field" - value={this.state.url} - onChange={this.onUrlChange} - /> + <div className="add-media-form__input_fields"> + <input + ref={el => (this.input = el)} + type={this.state.file ? "text" : "url"} + placeholder="Image, Video, or GLTF URL" + className="add-media-form__link_field" + value={this.state.text} + onChange={this.onUrlChange} + /> + <input className="add-media-form__file" + id="file" + type="file" + onChange={this.onFileChange} + /> + <label className="add-media-form__file_label" htmlFor="file">Choose a file</label> + </div> <div className="add-media-form__buttons"> <button className="add-media-form__action-button"> <span>create</span> diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js index e864bfc9fd979ef42b4fd4083cd67325e3465b3a..b21095e772d09954a4e880d31def490c2efad233 100644 --- a/src/utils/media-utils.js +++ b/src/utils/media-utils.js @@ -29,7 +29,31 @@ export const addMedia = (src, resize = false) => { const entity = document.createElement("a-entity"); entity.id = "interactable-media-" + interactableId++; entity.setAttribute("networked", { template: "#interactable-media" }); - entity.setAttribute("media-loader", { src, resize }); + entity.setAttribute("media-loader", { resize, src: typeof src === "string" ? src : "" }); scene.appendChild(entity); + + if (typeof src === "object") { + const uploadResponse = upload(src).then(response => { + const src = response.raw; + const contentType = response.meta.expected_content_type; + const token = response.meta.access_token; + entity.setAttribute("media-loader", { src, contentType, token }); + }); + } return entity; }; + +export const upload = file => { + const formData = new FormData(); + formData.append("media", file); + return fetch(mediaAPIEndpoint, { + method: "POST", + body: formData + + // We do NOT specify a Content-Type header like so + // headers: { "Content-Type" : "multipart/form-data" }, + // because we want the browser to automatically add + // "Content-Type" : "multipart/form-data; boundary=...--------------<boundary_size>", + // See https://stanko.github.io/uploading-files-using-fetch-multipart-form-data/ for details. + }).then(r => r.json()); +};