Files
cloudron-box/dashboard/src/components/app/Display.vue
T

120 lines
3.7 KiB
Vue
Raw Normal View History

2025-03-01 11:44:38 +01:00
<script setup>
import { ref, onMounted, useTemplateRef } from 'vue';
import { FormGroup, TextInput, Button, TagInput } from 'pankow';
import ImagePicker from '../ImagePicker.vue';
2025-03-01 11:44:38 +01:00
import AppsModel from '../../models/AppsModel.js';
2025-05-20 14:48:18 +02:00
import { API_ORIGIN } from '../../constants.js';
2025-03-01 11:44:38 +01:00
const props = defineProps([ 'app' ]);
const appsModel = AppsModel.create();
const imagePicker = useTemplateRef('imagePicker');
2025-03-01 11:44:38 +01:00
const busy = ref(false);
const labelError = ref('');
const tagsError = ref('');
const iconError = ref('');
const label = ref('');
const tags = ref([]);
const iconUrl = ref('');
function getDataURLFromFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
2025-03-01 11:44:38 +01:00
reader.onload = function(event) {
resolve(event.target.result);
2025-03-01 11:44:38 +01:00
};
reader.onerror = function(event) {
reject(event.target.error);
};
reader.readAsDataURL(file);
});
2025-03-01 11:44:38 +01:00
}
async function onSubmit() {
busy.value = true;
labelError.value = '';
tagsError.value = '';
iconError.value = '';
if (label.value !== props.app.label) {
const [error] = await appsModel.configure(props.app.id, 'label', { label: label.value });
if (error) {
labelError.value = error.body ? error.body.message : 'Internal error';
busy.value = false;
return console.error(error);
}
}
if (tags.value.join() !== props.app.tags.join()) {
const [error] = await appsModel.configure(props.app.id, 'tags', { tags: tags.value });
if (error) {
tagsError.value = error.body ? error.body.message : 'Internal error';
busy.value = false;
return console.error(error);
}
}
2025-05-20 14:48:18 +02:00
busy.value = false;
}
2025-03-01 11:44:38 +01:00
2025-05-20 14:48:18 +02:00
async function onIconSubmit(icon) {
const tmp = (await getDataURLFromFile(icon)).replace(/^data:image\/[a-z]+;base64,/, '');
const [error] = await appsModel.configure(props.app.id, 'icon', { icon: tmp });
if (error) {
iconError.value = error.body ? error.body.message : 'Internal error';
return error;
2025-03-01 11:44:38 +01:00
}
}
2025-05-20 14:48:18 +02:00
async function onIconUnset() {
const [error] = await appsModel.configure(props.app.id, 'icon', { icon: '' });
if (error) {
iconError.value = error.body ? error.body.message : 'Internal error';
return error;
}
iconUrl.value = props.app.iconUrl ? `${API_ORIGIN}/api/v1/apps/${props.app.id}/icon?ts=${Date.now()}` : `${API_ORIGIN}/img/appicon_fallback.png`; // calculate full icon url with cache busting
}
2025-03-01 11:44:38 +01:00
onMounted(() => {
label.value = props.app.label;
tags.value = props.app.tags;
iconUrl.value = props.app.iconUrl;
});
</script>
<template>
<div>
2025-05-20 14:48:18 +02:00
<div style="display: inline-block;">
<label>{{ $t('app.display.icon') }}</label>
<ImagePicker ref="imagePicker" :src="iconUrl" fallback-src="/img/appicon_fallback.png" :save-handler="onIconSubmit" :unset-handler="onIconUnset" :size="512" display-height="128px"/>
</div>
2025-03-01 11:44:38 +01:00
<form @submit.prevent="onSubmit()" autocomplete="off">
<fieldset :disabled="busy">
<input type="submit" style="display: none;"/>
<FormGroup>
<label for="labelInput">{{ $t('app.display.label') }}</label>
<TextInput id="labelInput" v-model="label"/>
<div class="text-error" v-if="labelError">{{ labelError }}</div>
</FormGroup>
<FormGroup>
<label for="tagsInput">{{ $t('app.display.tags') }}</label>
<TagInput id="tagsInput" :placeholder="$t('app.display.tagsPlaceholder')" v-model="tags" v-tooltip="$t('app.display.tagsTooltip')"/>
<div class="text-error" v-if="tagsError">{{ tagsError }}</div>
</FormGroup>
2025-05-20 14:48:18 +02:00
<Button @click="onSubmit()" :loading="busy" :disabled="busy">{{ $t('app.display.saveAction') }}</Button>
2025-03-01 11:44:38 +01:00
</fieldset>
</form>
</div>
</template>