ImagePicker fixes and use it also in app icon settings

This commit is contained in:
Johannes Zellner
2025-03-19 01:09:17 +01:00
parent e917ae4198
commit a58b2efaf9
3 changed files with 51 additions and 167 deletions

View File

@@ -1,24 +1,25 @@
<script setup>
import { useTemplateRef, ref, onMounted } from 'vue';
import { useTemplateRef, ref } from 'vue';
const fileInput = useTemplateRef('fileInput');
const props = defineProps(['src', 'fallbackSrc', 'size', 'maxSize', 'displayHeight']);
const emits = defineEmits(['changed']);
defineExpose({
clear(originalSrc = '') {
internalSrc.value = originalSrc || props.src || props.fallbackSrc;
}
});
const image = useTemplateRef('image');
const internalSrc = ref('');
const newSrc = ref('');
const isChanged = ref(false);
function onShowIconSelector() {
fileInput.value.click();
}
// function onChanged(event) {
// emits('changed', event);
// }
function dataURLtoFile(dataURL, filename) {
// Split the data URL to get the MIME type and the base64 data
const [metadata, base64Data] = dataURL.split(',');
@@ -33,7 +34,7 @@ function dataURLtoFile(dataURL, filename) {
const arrayBuffer = new ArrayBuffer(binaryData.length);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < binaryData.length; i++) {
view[i] = binaryData.charCodeAt(i);
view[i] = binaryData.charCodeAt(i);
}
// Create a Blob from the array buffer
@@ -104,15 +105,9 @@ function onChanged(event) {
fr.readAsDataURL(event.target.files[0]);
}
// function onClear() {
// internalSrc.value = props.src || props.fallbackSrc;
// newSrc.value = null;
// isChanged.value = true;
// }
onMounted(() => {
internalSrc.value = props.src;
});
function onError() {
internalSrc.value = props.fallbackSrc;
}
</script>
@@ -121,14 +116,11 @@ onMounted(() => {
<div class="image-picker">
<input @change="onChanged($event)" type="file" ref="fileInput" style="display: none" accept="image/*"/>
<div id="previewIcon" class="image-picker" @click="onShowIconSelector()">
<img :src="internalSrc" onerror="this.src = fallbackSrc" :style="{ height: displayHeight || null }" />
<div ref="image" class="image-picker" @click="onShowIconSelector()">
<img :src="internalSrc || src" @error="onError" :style="{ height: displayHeight || null }" />
<i class="image-picker-edit-indicator fa fa-pencil-alt"></i>
</div>
</div>
<!-- TODO translate -->
<!-- <div v-show="src" class="actionable" @click="onClear()">Clear</div> -->
</div>
</template>

View File

@@ -2,14 +2,14 @@
import { ref, onMounted, useTemplateRef } from 'vue';
import { FormGroup, TextInput, Button, TagInput } from 'pankow';
import ImagePicker from '../ImagePicker.vue';
import AppsModel from '../../models/AppsModel.js';
const props = defineProps([ 'app' ]);
const appsModel = AppsModel.create();
let iconChanged = false;
const imagePicker = useTemplateRef('imagePicker');
const busy = ref(false);
const labelError = ref('');
const tagsError = ref('');
@@ -17,62 +17,27 @@ const iconError = ref('');
const label = ref('');
const tags = ref([]);
const iconUrl = ref('');
const iconFileInput = useTemplateRef('iconFileInput');
function onResetIcon() {
const accessToken = localStorage.token;
let newIcon = null;
iconChanged = true;
iconUrl.value = props.app.iconUrl + '&original=true&access_token=' + accessToken;
function onIconChanged(file) {
newIcon = file;
}
function onShowIconSelector() {
iconFileInput.value.click();
}
function getDataURLFromFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
function onIconChanged(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);
iconUrl.value = canvas.toDataURL('image/png');
iconChanged = true;
reader.onload = function(event) {
resolve(event.target.result);
};
image.src = fr.result;
};
fr.readAsDataURL(event.target.files[0]);
reader.onerror = function(event) {
reject(event.target.error);
};
reader.readAsDataURL(file);
});
}
async function onSubmit() {
@@ -99,12 +64,12 @@ async function onSubmit() {
}
}
if (iconChanged) {
if (newIcon) {
let tmp;
if (iconUrl.value.indexOf('data:image') === 0) {
tmp = iconUrl.value.replace(/^data:image\/[a-z]+;base64,/, '');
} else {
if (newIcon === 'clear') {
tmp = '';
} else {
tmp = (await getDataURLFromFile(newIcon)).replace(/^data:image\/[a-z]+;base64,/, '');
}
const [error] = await appsModel.configure(props.app.id, 'icon', { icon: tmp });
@@ -113,13 +78,16 @@ async function onSubmit() {
busy.value = false;
return console.error(error);
}
iconChanged = false;
}
busy.value = false;
}
function onClearIcon() {
imagePicker.value.clear(`${iconUrl.value}&original=true`);
newIcon = 'clear';
}
onMounted(() => {
label.value = props.app.label;
tags.value = props.app.tags;
@@ -147,14 +115,9 @@ onMounted(() => {
</FormGroup>
<FormGroup>
<label for="previewIcon">{{ $t('app.display.icon') }}</label>
<div id="previewIcon" class="app-custom-icon" @click="onShowIconSelector()">
<img :src="iconUrl" onerror="this.src = '/img/appicon_fallback.png'"/>
<i class="app-custom-icon-edit-indicator fa fa-pencil-alt"></i>
</div>
<div class="text-error" v-if="iconError">{{ iconError }}</div>
<div class="actionable" @click="onResetIcon()">{{ $t('app.display.iconResetAction') }}</div>
<input type="file" ref="iconFileInput" @change="onIconChanged($event)" style="display: none" accept="image/webp, image/png, image/gif, image/jpeg"/>
<label>{{ $t('app.display.icon') }}</label>
<ImagePicker ref="imagePicker" :src="iconUrl" fallback-src="/img/appicon_fallback.png" @changed="onIconChanged" :size="512" display-height="128px"/>
<div class="actionable" @click="onClearIcon()">{{ $t('app.display.iconResetAction') }}</div>
</FormGroup>
</fieldset>
</form>