Port branding view to vue

This commit is contained in:
Johannes Zellner
2025-02-10 18:42:02 +01:00
parent 58fcca58fc
commit c193a86a4c
4 changed files with 287 additions and 2 deletions
+203
View File
@@ -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>