2025-02-10 18:42:02 +01:00
|
|
|
<script setup>
|
|
|
|
|
|
|
|
|
|
import { ref, onMounted, useTemplateRef } from 'vue';
|
|
|
|
|
import { Button, FormGroup, TextInput } from 'pankow';
|
2025-03-03 11:22:56 +01:00
|
|
|
import { API_ORIGIN } from '../constants.js';
|
2025-02-10 18:42:02 +01:00
|
|
|
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>
|