Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
CI: true
- run: sed -i 's|""|"https://unpkg.com/@workadventure/scripting-api-extra@${{ env.release_tag }}/dist"|g' src/Features/default_assets_url.ts
- run: sed -i 's|http://workadventure.localhost|https://workadventu.re|g' src/Features/default_api_url.ts
- run: npm run build --if-present
- run: npm run test
- run: curl -s https://codecov.io/bash | bash
Expand Down
5 changes: 5 additions & 0 deletions src/Features/default_api_url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* The base URL for the default API.
* This file is updated dynamically during the package build process.
*/
export const defaultApiUrl = "http://workadventure.localhost/api";
42 changes: 38 additions & 4 deletions src/Features/properties_templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,51 @@ export async function initPropertiesTemplates(): Promise<void> {
) {
continue;
}

const template = new TemplateValue(property.value, WA.state);
if (template.isPureString()) {
continue;
}
const newValue = template.getValue();
setProperty(layerName, property.name, newValue);
setLayerProperty(layerName, property.name, newValue);

template.onChange((newValue) => {
setProperty(layerName, property.name, newValue);
setLayerProperty(layerName, property.name, newValue);
});
}

// Parse the URL of the integrated websites (for example if mustache is used)
// Here we want to select the Tiled object layers with the type 'website' and the property 'url'
let promises = []
if (layer.type === "objectgroup") {
for (const object of layer.objects) {
if (object.type === "website") {
for (const property of object.properties) {
if (property.name === "url") {
const template = new TemplateValue(property.value, WA.state);
if (template.isPureString()) {
continue;
}
const newValue = template.getValue();
promises.push(setWebsiteProperty(object.name, newValue));

template.onChange((newValue) => {
setWebsiteProperty(object.name, newValue);
});
}
}
}
}
}
await Promise.all(promises);
}
}

/**
* Sets the property value on the map.
* Sets the property value of a layer on the map.
* Furthermore, if the property name is "visible", modify the visibility of the layer.
*/
function setProperty(layerName: string, propertyName: string, value: string): void {
function setLayerProperty(layerName: string, propertyName: string, value: string): void {
WA.room.setProperty(layerName, propertyName, value);
if (propertyName === "visible") {
if (value) {
Expand All @@ -43,3 +69,11 @@ function setProperty(layerName: string, propertyName: string, value: string): vo
}
}
}

/**
* Sets the property value of an object of type 'website' on the map.
*/
async function setWebsiteProperty(objectName: string, value: string): Promise<void> {
const website = await WA.room.website.get(objectName);
website.url = value;
}
71 changes: 70 additions & 1 deletion src/Iframes/Configuration/Components/Field.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script lang="ts">
import type {VariableDescriptor} from "../../../VariablesExtra";
import {createStoreFromVariable} from "../../../VariableMapper";
import {prepareUpload, uploadFile} from "../../../Uploader";
import type {Writable} from "svelte/store";
import {formStore} from "../Stores/form";

export let variable: VariableDescriptor;

Expand All @@ -14,13 +16,24 @@
const stringVariableStore = variableStore as Writable<string>;
const boolVariableStore = variableStore as Writable<boolean>;

let container: HTMLElement;
let fileInput: HTMLInputElement;

function getAllowedValues() {
const allowedValuesStr = variable.properties.mustGetString('allowed_values');
return JSON.parse(allowedValuesStr) as {[key: string]: string | number | undefined};
}

function onChange(event: Event) {
$variableStore = (event.target as HTMLInputElement).value;
if (type === 'upload') {
prepareUpload(event, variable)
} else {
$variableStore = (event.target as HTMLInputElement).value;
}
}

async function onUpload() {
$variableStore = await uploadFile()
}
</script>

Expand Down Expand Up @@ -49,6 +62,30 @@
</label>
{/each}
</div>
{:else if type === 'upload' }
<div class="nes-field field upload">
<span>{label}</span>
<div class="field">
<input type="file" accept="image/*"
name={variable.name}
id="upload_{variable.name}"
bind:this={fileInput}
on:change={onChange}
class="nes-btn">

<div>
<div bind:this={container} class="image-preview">
{#if $formStore.showImage}
<img bind:this={$formStore.image} src="" alt="Preview" />
{:else}
<span>/</span>
{/if}
</div>

<button class="nes-btn is-primary upload-btn" on:click={onUpload}>Upload & Replace</button>
</div>
</div>
</div>
{:else}
<div class="nes-field field">
<label for="input_{variable.name}">{label}</label>
Expand All @@ -59,6 +96,9 @@
<div class="description">{ description }</div>
{/if}

{#if $formStore.error }
<div class="error"><p>{ $formStore.error }</p></div>
{/if}

<style lang="scss">
.field {
Expand All @@ -70,4 +110,33 @@
color: #777;
margin-bottom: 30px;
}

.upload {
height: 128px;

.image-preview {
image-rendering: -webkit-crisp-edges;
image-rendering: crisp-edges;
width: 256px;
min-height: 128px;
border: 2px solid #ddd;
margin-top: 15px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: #ccc;
}

.upload-btn {
display: flex;
margin-top: 20px;
}
}

.error {
margin-top: 25px;
color: #cb2525;
display: inline-block;
}
</style>
76 changes: 76 additions & 0 deletions src/Iframes/Configuration/Stores/form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { writable } from "svelte/store";

interface Form {
error: string,
image: HTMLImageElement,
showImage: boolean,
formData: FormData,
}

const defaultFormState: Form = {
error: '',
image: new Image(0, 0),
showImage: false,
formData: new FormData()
}

function createFormAction() {
const { subscribe, set, update } = writable<Form>(defaultFormState);

return {
subscribe,
setError: (error: string): void => {
update((form: Form) => {
form.error = error
return form
})
},
clearError: (): void => {
update((form: Form) => {
form.error = ''
return form
})
},
setImagePreviewMaxWidth: (width: string): void => {
update((form: Form) => {
form.image.style.maxWidth = "" + width + "px";
return form
})
},
setImagePreviewMaxHeight: (height: string): void => {
update((form: Form) => {
form.image.style.maxHeight = "" + height + "px";
return form
})
},
setImageSource: (source: string): void => {
update((form: Form) => {
form.image.setAttribute("src", source);
return form
})
},
changeImageVisibility: (mustShowImage: boolean): void => {
update((form: Form) => {
form.showImage = mustShowImage
return form
})
},
setFormDataFile: (file: File): void => {
update((form: Form) => {
form.formData.append('file', file)
return form
})
},
setFormDataProperty: (propertyName: string, propertyValue: any): void => {
update((form: Form) => {
form.formData.set(propertyName, propertyValue)
return form
})
},
clearForm: (): void => {
set(defaultFormState);
},
}
}

export const formStore = createFormAction();
71 changes: 71 additions & 0 deletions src/Uploader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/// <reference path="../node_modules/@workadventure/iframe-api-typings/iframe_api.d.ts" />

import {defaultApiUrl} from "./Features/default_api_url";
import { formStore } from "./Iframes/Configuration/Stores/form"
import type {VariableDescriptor} from "./VariablesExtra";

let formData: FormData
const unsubscribe = formStore.subscribe(form => {
formData = form.formData;
})

/**
* Builds the image preview and the form data
*/
export function prepareUpload(event: Event, variable: VariableDescriptor) {
const files = (event.target as HTMLInputElement).files;
const file = files ? files[0] : null;
formStore.clearError()

if (file) {
formStore.changeImageVisibility(true)
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result === "string") {
formStore.setFormDataFile(file)
formStore.setFormDataProperty('identifier', variable.name)
if (variable.properties.getString('imageWidth')) {
const width = variable.properties.mustGetString('imageWidth')
// Just for the rendering, doesn't resize the actual file
formStore.setImagePreviewMaxWidth(width)
// This will resize the file, on server side
formStore.setFormDataProperty('imageWidth', width)
}
if (variable.properties.getString('imageHeight')) {
const height = variable.properties.mustGetString('imageHeight')
// Just for the rendering, doesn't resize the actual file
formStore.setImagePreviewMaxHeight(height)
// This will resize the file, on server side
formStore.setFormDataProperty('imageHeight', height)
}
formStore.setImageSource(reader.result)
}
};
reader.readAsDataURL(file);
return;
}
formStore.changeImageVisibility(false)
}

/**
* Performs the upload and update the url variable
*/
export async function uploadFile(): Promise<string> {
const token = WA.player.userRoomToken
formStore.clearError()
//!\ if you are self-hosting WA, this API must be added by your side
const response = await fetch(defaultApiUrl + '/upload-file', {
method: 'POST',
body: formData,
headers: {
'Authorization': `Bearer ${token}`,
},
}).catch(e => {
formStore.setError('Upload error. Please contact the support.')
throw new Error(e)
})

unsubscribe()
const data = await response.json()
return data.url
}
Loading