Rework the ImagePicker component
This commit is contained in:
@@ -1,24 +1,20 @@
|
||||
<script setup>
|
||||
|
||||
import { useTemplateRef, ref } from 'vue';
|
||||
import { useTemplateRef, ref, watch } from 'vue';
|
||||
import { Button } from 'pankow';
|
||||
|
||||
const fileInput = useTemplateRef('fileInput');
|
||||
|
||||
const props = defineProps(['src', 'fallbackSrc', 'size', 'maxSize', 'displayHeight', 'displayWidth', 'disabled']);
|
||||
const emits = defineEmits(['changed']);
|
||||
defineExpose({
|
||||
clear(originalSrc = '') {
|
||||
internalSrc.value = originalSrc || props.src || props.fallbackSrc;
|
||||
}
|
||||
});
|
||||
const props = defineProps(['src', 'fallbackSrc', 'size', 'maxSize', 'displayHeight', 'displayWidth', 'disabled', 'saveHandler', 'unsetHandler']);
|
||||
|
||||
const image = useTemplateRef('image');
|
||||
const internalSrc = ref('');
|
||||
const isChanged = ref(false);
|
||||
const busy = ref(false);
|
||||
|
||||
function onShowIconSelector() {
|
||||
fileInput.value.click();
|
||||
}
|
||||
watch(() => {
|
||||
internalSrc.value = props.src;
|
||||
});
|
||||
|
||||
function dataURLtoFile(dataURL, filename) {
|
||||
// Split the data URL to get the MIME type and the base64 data
|
||||
@@ -46,6 +42,37 @@ function dataURLtoFile(dataURL, filename) {
|
||||
return file;
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (typeof props.saveHandler !== 'function') return console.error('saveHandler must be a function in ImagePicker');
|
||||
|
||||
busy.value = true;
|
||||
|
||||
const error = await props.saveHandler(dataURLtoFile(internalSrc.value, 'image.png'));
|
||||
if (!error) isChanged.value = false;
|
||||
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
internalSrc.value = props.src || props.fallbackSrc;
|
||||
isChanged.value = false;
|
||||
}
|
||||
|
||||
function onEdit() {
|
||||
fileInput.value.click();
|
||||
}
|
||||
|
||||
async function onUnset() {
|
||||
if (typeof props.unsetHandler !== 'function') return console.error('unsetHandler must be a function in ImagePicker');
|
||||
|
||||
busy.value = true;
|
||||
|
||||
const error = await props.unsetHandler();
|
||||
if (!error) isChanged.value = false;
|
||||
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
function onChanged(event) {
|
||||
const fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
@@ -96,8 +123,6 @@ function onChanged(event) {
|
||||
|
||||
internalSrc.value = canvas.toDataURL('image/png');
|
||||
isChanged.value = true;
|
||||
|
||||
emits('changed', dataURLtoFile(internalSrc.value, 'image.png'));
|
||||
};
|
||||
|
||||
image.src = fr.result;
|
||||
@@ -113,12 +138,19 @@ function onError() {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="image-picker-container">
|
||||
<input @change="onChanged($event)" type="file" ref="fileInput" style="display: none" accept="image/*"/>
|
||||
|
||||
<div ref="image" :disabled="disabled || null" class="image-picker" @click="!disabled && onShowIconSelector()">
|
||||
<img :src="internalSrc || src" @error="onError" :style="{ height: displayHeight || null, width: displayWidth || null }">
|
||||
<i class="image-picker-edit-indicator fa fa-pencil-alt"></i>
|
||||
<div ref="image" :disabled="disabled || null" class="image-picker" @click="!disabled && onEdit()">
|
||||
<img :src="internalSrc || src" @error="onError" class="image-picker-image" :style="{ height: displayHeight || null, width: displayWidth || null }">
|
||||
<div v-if="isChanged" class="image-picker-actions" style="visibility: visible;">
|
||||
<Button @click.stop="onCancel" secondary tool small icon="fa fa-undo" :disabled="busy"/>
|
||||
<Button @click.stop="onSave" success tool small :loading="busy" :disabled="busy" icon="fa fa-floppy-disk"/>
|
||||
</div>
|
||||
<div v-else-if="!disabled" class="image-picker-actions">
|
||||
<Button @click.stop="onEdit" tool small icon="fa fa-pencil-alt" :disabled="busy"/>
|
||||
<Button @click.stop="onUnset" tool small icon="fa fa-trash" :loading="busy" :disabled="busy" v-if="unsetHandler"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,6 +158,38 @@ function onError() {
|
||||
|
||||
<style scoped>
|
||||
|
||||
.image-picker-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.image-picker-actions {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
bottom: 6px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.image-picker-actions {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.image-picker-actions:hover,
|
||||
.image-picker:hover .image-picker-actions {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.image-picker-image {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.image-picker {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ref, onMounted, useTemplateRef } from 'vue';
|
||||
import { FormGroup, TextInput, Button, TagInput } from 'pankow';
|
||||
import ImagePicker from '../ImagePicker.vue';
|
||||
import AppsModel from '../../models/AppsModel.js';
|
||||
import { API_ORIGIN } from '../../constants.js';
|
||||
|
||||
const props = defineProps([ 'app' ]);
|
||||
|
||||
@@ -18,12 +19,6 @@ const label = ref('');
|
||||
const tags = ref([]);
|
||||
const iconUrl = ref('');
|
||||
|
||||
let newIcon = null;
|
||||
|
||||
function onIconChanged(file) {
|
||||
newIcon = file;
|
||||
}
|
||||
|
||||
function getDataURLFromFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -64,28 +59,26 @@ async function onSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
if (newIcon) {
|
||||
let tmp;
|
||||
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 });
|
||||
if (error) {
|
||||
iconError.value = error.body ? error.body.message : 'Internal error';
|
||||
busy.value = false;
|
||||
return console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
function onClearIcon() {
|
||||
imagePicker.value.clear(`${iconUrl.value}&original=true`);
|
||||
newIcon = 'clear';
|
||||
async function onIconSubmit(icon) {
|
||||
const tmp = (await getDataURLFromFile(icon)).replace(/^data:image\/[a-z]+;base64,/, '');
|
||||
const [error] = await appsModel.configure(props.app.id, 'icon', { icon: tmp });
|
||||
if (error) {
|
||||
iconError.value = error.body ? error.body.message : 'Internal error';
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
async function onIconUnset() {
|
||||
const [error] = await appsModel.configure(props.app.id, 'icon', { icon: '' });
|
||||
if (error) {
|
||||
iconError.value = error.body ? error.body.message : 'Internal error';
|
||||
return error;
|
||||
}
|
||||
|
||||
iconUrl.value = props.app.iconUrl ? `${API_ORIGIN}/api/v1/apps/${props.app.id}/icon?ts=${Date.now()}` : `${API_ORIGIN}/img/appicon_fallback.png`; // calculate full icon url with cache busting
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -98,6 +91,11 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div style="display: inline-block;">
|
||||
<label>{{ $t('app.display.icon') }}</label>
|
||||
<ImagePicker ref="imagePicker" :src="iconUrl" fallback-src="/img/appicon_fallback.png" :save-handler="onIconSubmit" :unset-handler="onIconUnset" :size="512" display-height="128px"/>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="onSubmit()" autocomplete="off">
|
||||
<fieldset :disabled="busy">
|
||||
<input type="submit" style="display: none;"/>
|
||||
@@ -114,14 +112,8 @@ onMounted(() => {
|
||||
<div class="text-error" v-if="tagsError">{{ tagsError }}</div>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<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>
|
||||
<Button @click="onSubmit()" :loading="busy" :disabled="busy">{{ $t('app.display.saveAction') }}</Button>
|
||||
</fieldset>
|
||||
</form>
|
||||
<br/>
|
||||
<Button @click="onSubmit()" :loading="busy" :disabled="busy">{{ $t('app.display.saveAction') }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user