Use ImagePicker component in branding page

This commit is contained in:
Johannes Zellner
2025-03-18 23:08:29 +01:00
parent e8bd839281
commit 53aed4c7f8
+85 -118
View File
@@ -1,27 +1,24 @@
<script setup>
import { ref, onMounted, useTemplateRef } from 'vue';
import { ref, onMounted } from 'vue';
import { Button, FormGroup, TextInput } from 'pankow';
import { API_ORIGIN } from '../constants.js';
import Section from '../components/Section.vue';
import ImagePicker from '../components/ImagePicker.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;
let newBackgroundDataUrl = null;
let newAvatarDataUrl = 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;
@@ -32,115 +29,25 @@ async function onSubmit() {
[error] = await brandingModel.setFooter(footer.value);
if (error) return console.error(error);
if (backgroundChanged) {
const [error] = await brandingModel.setBackground(newBackground);
if (newBackgroundDataUrl) {
const [error] = await brandingModel.setBackground(newBackgroundDataUrl);
if (error) return console.error(error);
}
if (avatarChanged) {
const [error] = await brandingModel.setAvatar(newAvatar);
if (newAvatarDataUrl) {
const [error] = await brandingModel.setAvatar(newAvatarDataUrl);
if (error) return console.error(error);
}
backgroundChanged = false;
avatarChanged = false;
busy.value = false;
}
function onChangeAvatar() {
avatarFileInput.value.click();
function onAvatarChanged(dataUrl) {
newAvatarDataUrl = dataUrl;
}
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;
function onBackgroundChanged(dataUrl) {
newBackgroundDataUrl = dataUrl;
}
onMounted(async () => {
@@ -169,21 +76,12 @@ onMounted(async () => {
<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/*"/>
<ImagePicker :src="avatarUrl" @changed="onAvatarChanged" :size="512" display-height="100px"/>
</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/*"/>
<ImagePicker :src="backgroundUrl" @changed="onBackgroundChanged" fallback-src="/img/background-image-placeholder.svg" :max-size="4096"/>
</FormGroup>
<FormGroup>
@@ -192,11 +90,80 @@ onMounted(async () => {
<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>
<style scoped>
.cloudron-logo {
position: relative;
cursor: pointer;
width: 128px;
height: 128px;
background-position: center;
background-size: 100% 100%;
background-repeat: no-repeat;
border: 1px solid gray;
border-radius: 3px;
margin-bottom: 10px;
}
.cloudron-logo-edit-indicator {
position: absolute;
bottom: -4px;
right: -4px;
border-radius: 20px;
padding: 5px;
color: var(--pankow-text-color);
background-color: var(--pankow-input-background-color);
transition: all 250ms;
}
.cloudron-logo:hover .cloudron-logo-edit-indicator {
color: white;
background: var(--pankow-color-primary);
transform: scale(1.2);
}
.cloudron-background {
position: relative;
cursor: pointer;
/* width: 1280px;*/
width: auto;
height: 256px;
margin-bottom: 5px;
background-position: center;
background-size: 100% 100%;
background-repeat: no-repeat;
border: 1px solid gray;
border-radius: 3px;
}
.cloudron-background > img {
display: block;
/* width: 100%;*/
height: 100%;
}
.cloudron-background-edit-indicator {
position: absolute;
bottom: -4px;
right: -4px;
border-radius: 20px;
padding: 5px;
color: var(--pankow-text-color);
background-color: var(--pankow-input-background-color);
transition: all 250ms;
}
.cloudron-background:hover > .cloudron-background-edit-indicator {
color: white;
background: var(--pankow-color-primary);
transform: scale(1.2);
}
</style>