Port branding view to vue
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
<script setup>
|
||||
|
||||
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN || window.location.origin;
|
||||
|
||||
import { ref, onMounted, useTemplateRef } from 'vue';
|
||||
import { Button, FormGroup, TextInput } from 'pankow';
|
||||
import Section from '../components/Section.vue';
|
||||
import BrandingModel from '../models/BrandingModel.js';
|
||||
import DashboardModel from '../models/DashboardModel.js';
|
||||
|
||||
const brandingModel = BrandingModel.create();
|
||||
const dashboardModel = DashboardModel.create();
|
||||
|
||||
let backgroundChanged = false;
|
||||
let newBackground = null;
|
||||
let avatarChanged = false;
|
||||
let newAvatar = null;
|
||||
|
||||
const busy = ref(false);
|
||||
const name = ref('');
|
||||
const footer = ref('');
|
||||
const avatarUrl = ref(`${API_ORIGIN}/api/v1/cloudron/avatar?${String(Math.random()).slice(2)}`);
|
||||
const avatarFileInput = useTemplateRef('avatarFileInput');
|
||||
const backgroundUrl = ref(`${API_ORIGIN}/api/v1/cloudron/background?${String(Math.random()).slice(2)}`);
|
||||
const backgroundFileInput = useTemplateRef('backgroundFileInput');
|
||||
|
||||
async function onSubmit() {
|
||||
busy.value = true;
|
||||
|
||||
let [error] = await brandingModel.setName(name.value);
|
||||
if (error) return console.error(error);
|
||||
|
||||
[error] = await brandingModel.setFooter(footer.value);
|
||||
if (error) return console.error(error);
|
||||
|
||||
if (backgroundChanged) {
|
||||
const [error] = await brandingModel.setBackground(newBackground);
|
||||
if (error) return console.error(error);
|
||||
}
|
||||
|
||||
if (avatarChanged) {
|
||||
const [error] = await brandingModel.setAvatar(newAvatar);
|
||||
if (error) return console.error(error);
|
||||
}
|
||||
|
||||
backgroundChanged = false;
|
||||
avatarChanged = false;
|
||||
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
function onChangeAvatar() {
|
||||
avatarFileInput.value.click();
|
||||
}
|
||||
|
||||
function onAvatarChanged(event) {
|
||||
const fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
const image = new Image();
|
||||
image.onload = function () {
|
||||
const size = 512;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
|
||||
const imageDimensionRatio = image.width / image.height;
|
||||
const canvasDimensionRatio = canvas.width / canvas.height;
|
||||
let renderableHeight, renderableWidth, xStart, yStart;
|
||||
|
||||
if (imageDimensionRatio > canvasDimensionRatio) {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = image.width * (renderableHeight / image.height);
|
||||
xStart = (canvas.width - renderableWidth) / 2;
|
||||
yStart = 0;
|
||||
} else if (imageDimensionRatio < canvasDimensionRatio) {
|
||||
renderableWidth = canvas.width;
|
||||
renderableHeight = image.height * (renderableWidth / image.width);
|
||||
xStart = 0;
|
||||
yStart = (canvas.height - renderableHeight) / 2;
|
||||
} else {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = canvas.width;
|
||||
xStart = 0;
|
||||
yStart = 0;
|
||||
}
|
||||
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(image, xStart, yStart, renderableWidth, renderableHeight);
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
newAvatar = new File([blob], 'avatar.png', { type: blob.type });
|
||||
|
||||
avatarUrl.value = URL.createObjectURL(newAvatar);
|
||||
avatarChanged = true;
|
||||
}, 'image/png');
|
||||
};
|
||||
|
||||
image.src = fr.result;
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
}
|
||||
|
||||
function onChangeBackgroud() {
|
||||
backgroundFileInput.value.click();
|
||||
}
|
||||
|
||||
function onBackgroundChanged(event) {
|
||||
const fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
const image = new Image();
|
||||
image.onload = function () {
|
||||
// convert and scale to webp max 4k
|
||||
const maxWidth = 4096;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
|
||||
if (image.naturalWidth > maxWidth) {
|
||||
canvas.width = maxWidth;
|
||||
canvas.height = (image.naturalHeight / image.naturalWidth) * maxWidth;
|
||||
} else {
|
||||
canvas.width = image.naturalWidth;
|
||||
canvas.height = image.naturalHeight;
|
||||
}
|
||||
|
||||
canvas.getContext('2d').drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
canvas.toBlob((blob) => {
|
||||
newBackground = new File([blob], 'background.webp', { type: blob.type });
|
||||
|
||||
backgroundUrl.value = URL.createObjectURL(newBackground);
|
||||
backgroundChanged = true;
|
||||
}, 'image/webp');
|
||||
};
|
||||
|
||||
image.src = fr.result;
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
}
|
||||
|
||||
function onBackgroundClear() {
|
||||
backgroundUrl.value = `${API_ORIGIN}/img/background-image-placeholder.svg`;
|
||||
newBackground = null;
|
||||
backgroundChanged = true;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
let [error, result] = await dashboardModel.getConfig();
|
||||
if (error) return console.error(error);
|
||||
name.value = result.cloudronName;
|
||||
|
||||
[error, result] = await brandingModel.getFooter();
|
||||
if (error) return console.error(error);
|
||||
footer.value = result;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="content">
|
||||
<Section :title="$t('branding.title')">
|
||||
<form @submit.prevent="onSubmit()">
|
||||
<fieldset :disabled="busy">
|
||||
<input type="submit" style="display: none;" />
|
||||
|
||||
<FormGroup>
|
||||
<label for="nameInput">{{ $t('branding.cloudronName') }}</label>
|
||||
<TextInput id="nameInput" v-model="name" minlength="1" maxlength="64" required/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label>{{ $t('branding.logo') }}</label>
|
||||
<div class="branding-avatar" @click="onChangeAvatar()">
|
||||
<img :src="avatarUrl"/>
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
<input @change="onAvatarChanged($event)" type="file" ref="avatarFileInput" style="display: none" accept="image/*"/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label class="control-label">{{ $t('branding.backgroundImage') }}</label>
|
||||
<div class="branding-background" @click="onChangeBackgroud()">
|
||||
<img :src="backgroundUrl" onerror="this.src = '/img/background-image-placeholder.svg'"/>
|
||||
<i class="picture-edit-indicator fa fa-pencil-alt"></i>
|
||||
</div>
|
||||
<div v-show="backgroundUrl" class="actionable" @click="onBackgroundClear()">{{ $t('branding.clearBackgroundImage') }}</div>
|
||||
<input @change="onBackgroundChanged($event)" type="file" ref="backgroundFileInput" style="display: none" accept="image/*"/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="footerInput">{{ $t('branding.footer.title') }} </label>
|
||||
<p>{{ $t('branding.footer.description') }} <sup><a href="https://docs.cloudron.io/branding/#footer" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></p>
|
||||
<textarea id="footerInput" v-model="footer" :disabled="busy" style="display: block; width: 100%" rows="1"></textarea>
|
||||
</FormGroup>
|
||||
|
||||
<br/>
|
||||
|
||||
<Button @click="onSubmit()" :loading="busy" :disabled="busy">{{ $t('main.dialog.save') }}</Button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Section>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user