Uploading a character image
This commit is contained in:
@@ -4,6 +4,10 @@
|
||||
<div class="character-overview">
|
||||
<div class="character-image">
|
||||
<img :src="imageSrc" alt="Character Image"/>
|
||||
<ImageUploadCropper
|
||||
:characterId="character.id"
|
||||
@image-updated="handleImageUpdate"
|
||||
/>
|
||||
</div>
|
||||
<div class="character-stats">
|
||||
<div class="stat" v-for="(stat, index) in characterStats" :key="index">
|
||||
@@ -81,6 +85,16 @@
|
||||
margin-top: 0; /* Kein zusätzlicher oberer Margin */
|
||||
}
|
||||
|
||||
.character-image {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.character-image .image-upload-container {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.character-info {
|
||||
margin-top: 20px;
|
||||
}
|
||||
@@ -88,8 +102,13 @@
|
||||
|
||||
|
||||
<script>
|
||||
import ImageUploadCropper from './ImageUploadCropper.vue'
|
||||
|
||||
export default {
|
||||
name: "DatasheetView",
|
||||
components: {
|
||||
ImageUploadCropper
|
||||
},
|
||||
props: {
|
||||
character: {
|
||||
type: Object,
|
||||
@@ -125,6 +144,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleImageUpdate(newImage) {
|
||||
this.$emit('character-updated')
|
||||
},
|
||||
getStat(path) {
|
||||
if (path === 'git' ){
|
||||
return '64!'
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
<template>
|
||||
<div class="image-upload-container">
|
||||
<button @click="showDialog = true" class="btn-upload">
|
||||
{{ $t('character.uploadImage') }}
|
||||
</button>
|
||||
|
||||
<div v-if="showDialog" class="modal-overlay" @click.self="closeDialog">
|
||||
<div class="modal-content image-upload-modal">
|
||||
<div class="modal-header">
|
||||
<h3>{{ $t('character.uploadImage') }}</h3>
|
||||
<button @click="closeDialog" class="close-button">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div v-if="!imageLoaded" class="upload-area">
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
@change="handleFileSelect"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
/>
|
||||
<button @click="$refs.fileInput.click()" class="btn-select-file">
|
||||
{{ $t('character.selectImage') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="crop-area">
|
||||
<div class="crop-controls">
|
||||
<label>
|
||||
<input type="radio" v-model="cropShape" value="rect" />
|
||||
{{ $t('character.cropRect') }}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" v-model="cropShape" value="round" />
|
||||
{{ $t('character.cropRound') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="canvas-container">
|
||||
<canvas
|
||||
ref="canvas"
|
||||
@mousedown="startDrag"
|
||||
@mousemove="drag"
|
||||
@mouseup="endDrag"
|
||||
@mouseleave="endDrag"
|
||||
></canvas>
|
||||
</div>
|
||||
|
||||
<div class="preview-container">
|
||||
<h4>{{ $t('character.preview') }}</h4>
|
||||
<canvas ref="previewCanvas" width="400" height="400"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button @click="closeDialog" class="btn-cancel">{{ $t('cancel') }}</button>
|
||||
<button v-if="imageLoaded" @click="resetImage" class="btn-secondary">
|
||||
{{ $t('character.changeImage') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="imageLoaded"
|
||||
@click="uploadImage"
|
||||
class="btn-primary"
|
||||
:disabled="isUploading"
|
||||
>
|
||||
<span v-if="!isUploading">{{ $t('character.saveImage') }}</span>
|
||||
<span v-else>{{ $t('uploading') }}...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.btn-upload {
|
||||
padding: 8px 16px;
|
||||
background-color: var(--primary-color);
|
||||
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-upload:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.image-upload-modal {
|
||||
min-width: 700px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.btn-select-file {
|
||||
padding: 12px 24px;
|
||||
background-color: var(--primary-color);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.crop-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.crop-controls {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.crop-controls label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f5f5f5;
|
||||
overflow: auto;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.canvas-container canvas {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-container h4 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.preview-container canvas {
|
||||
border: 1px solid #ccc;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 10px 20px;
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import API from '../utils/api'
|
||||
|
||||
export default {
|
||||
name: 'ImageUploadCropper',
|
||||
props: {
|
||||
characterId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDialog: false,
|
||||
imageLoaded: false,
|
||||
isUploading: false,
|
||||
cropShape: 'rect',
|
||||
image: null,
|
||||
cropStart: null,
|
||||
cropEnd: null,
|
||||
isDragging: false,
|
||||
cropWidth: 400,
|
||||
cropHeight: 400
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFileSelect(event) {
|
||||
const file = event.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
this.image = img
|
||||
this.imageLoaded = true
|
||||
this.$nextTick(() => {
|
||||
this.initCanvas()
|
||||
})
|
||||
}
|
||||
img.src = e.target.result
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
|
||||
initCanvas() {
|
||||
const canvas = this.$refs.canvas
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
canvas.width = this.image.width
|
||||
canvas.height = this.image.height
|
||||
|
||||
ctx.drawImage(this.image, 0, 0)
|
||||
|
||||
// Initialize crop area in center
|
||||
this.cropStart = {
|
||||
x: Math.max(0, (this.image.width - this.cropWidth) / 2),
|
||||
y: Math.max(0, (this.image.height - this.cropHeight) / 2)
|
||||
}
|
||||
this.cropEnd = {
|
||||
x: this.cropStart.x + this.cropWidth,
|
||||
y: this.cropStart.y + this.cropHeight
|
||||
}
|
||||
|
||||
this.drawCropOverlay()
|
||||
this.updatePreview()
|
||||
},
|
||||
|
||||
drawCropOverlay() {
|
||||
const canvas = this.$refs.canvas
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// Redraw image
|
||||
ctx.drawImage(this.image, 0, 0)
|
||||
|
||||
// Draw semi-transparent overlay
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
const width = this.cropEnd.x - this.cropStart.x
|
||||
const height = this.cropEnd.y - this.cropStart.y
|
||||
|
||||
// Clear crop area
|
||||
ctx.clearRect(this.cropStart.x, this.cropStart.y, width, height)
|
||||
ctx.drawImage(
|
||||
this.image,
|
||||
this.cropStart.x, this.cropStart.y, width, height,
|
||||
this.cropStart.x, this.cropStart.y, width, height
|
||||
)
|
||||
|
||||
// Draw border
|
||||
ctx.strokeStyle = '#00ff00'
|
||||
ctx.lineWidth = 2
|
||||
if (this.cropShape === 'round') {
|
||||
ctx.beginPath()
|
||||
const centerX = this.cropStart.x + width / 2
|
||||
const centerY = this.cropStart.y + height / 2
|
||||
const radius = Math.min(width, height) / 2
|
||||
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI)
|
||||
ctx.stroke()
|
||||
} else {
|
||||
ctx.strokeRect(this.cropStart.x, this.cropStart.y, width, height)
|
||||
}
|
||||
},
|
||||
|
||||
updatePreview() {
|
||||
const previewCanvas = this.$refs.previewCanvas
|
||||
const ctx = previewCanvas.getContext('2d')
|
||||
|
||||
const width = this.cropEnd.x - this.cropStart.x
|
||||
const height = this.cropEnd.y - this.cropStart.y
|
||||
|
||||
ctx.clearRect(0, 0, 400, 400)
|
||||
|
||||
if (this.cropShape === 'round') {
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.arc(200, 200, 200, 0, 2 * Math.PI)
|
||||
ctx.clip()
|
||||
}
|
||||
|
||||
ctx.drawImage(
|
||||
this.image,
|
||||
this.cropStart.x, this.cropStart.y, width, height,
|
||||
0, 0, 400, 400
|
||||
)
|
||||
|
||||
if (this.cropShape === 'round') {
|
||||
ctx.restore()
|
||||
}
|
||||
},
|
||||
|
||||
startDrag(event) {
|
||||
const canvas = this.$refs.canvas
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
|
||||
// Calculate mouse position on the actual canvas (accounting for scaling)
|
||||
const scaleX = canvas.width / rect.width
|
||||
const scaleY = canvas.height / rect.height
|
||||
|
||||
this.cropStart = {
|
||||
x: (event.clientX - rect.left) * scaleX,
|
||||
y: (event.clientY - rect.top) * scaleY
|
||||
}
|
||||
this.isDragging = true
|
||||
},
|
||||
|
||||
drag(event) {
|
||||
if (!this.isDragging) return
|
||||
|
||||
const canvas = this.$refs.canvas
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
|
||||
// Calculate mouse position on the actual canvas (accounting for scaling)
|
||||
const scaleX = canvas.width / rect.width
|
||||
const scaleY = canvas.height / rect.height
|
||||
|
||||
this.cropEnd = {
|
||||
x: (event.clientX - rect.left) * scaleX,
|
||||
y: (event.clientY - rect.top) * scaleY
|
||||
}
|
||||
|
||||
// Ensure minimum size
|
||||
const minSize = 50
|
||||
if (Math.abs(this.cropEnd.x - this.cropStart.x) < minSize ||
|
||||
Math.abs(this.cropEnd.y - this.cropStart.y) < minSize) {
|
||||
return
|
||||
}
|
||||
|
||||
this.drawCropOverlay()
|
||||
this.updatePreview()
|
||||
},
|
||||
|
||||
endDrag() {
|
||||
this.isDragging = false
|
||||
},
|
||||
|
||||
async uploadImage() {
|
||||
this.isUploading = true
|
||||
|
||||
try {
|
||||
// Get cropped image as base64
|
||||
const previewCanvas = this.$refs.previewCanvas
|
||||
const croppedImage = previewCanvas.toDataURL('image/png')
|
||||
|
||||
await API.put(`/api/characters/${this.characterId}/image`, {
|
||||
image: croppedImage
|
||||
})
|
||||
|
||||
this.$emit('image-updated', croppedImage)
|
||||
this.closeDialog()
|
||||
//alert(this.$t('character.imageUploadSuccess'))
|
||||
} catch (error) {
|
||||
console.error('Failed to upload image:', error)
|
||||
alert(this.$t('character.imageUploadError') + ': ' + (error.response?.data?.error || error.message))
|
||||
} finally {
|
||||
this.isUploading = false
|
||||
}
|
||||
},
|
||||
|
||||
resetImage() {
|
||||
this.imageLoaded = false
|
||||
this.image = null
|
||||
this.cropStart = null
|
||||
this.cropEnd = null
|
||||
if (this.$refs.fileInput) {
|
||||
this.$refs.fileInput.value = ''
|
||||
}
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.showDialog = false
|
||||
this.resetImage()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
cropShape() {
|
||||
if (this.imageLoaded) {
|
||||
this.drawCropOverlay()
|
||||
this.updatePreview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+13
-1
@@ -470,5 +470,17 @@ export default {
|
||||
passwordUpdateSuccess: 'Passwort erfolgreich aktualisiert',
|
||||
passwordUpdateError: 'Fehler beim Aktualisieren des Passworts',
|
||||
currentPasswordIncorrect: 'Das aktuelle Passwort ist falsch'
|
||||
}
|
||||
},
|
||||
character: {
|
||||
uploadImage: 'Bild hochladen',
|
||||
selectImage: 'Bild auswählen',
|
||||
cropRect: 'Rechteckig',
|
||||
cropRound: 'Rund',
|
||||
preview: 'Vorschau',
|
||||
changeImage: 'Bild ändern',
|
||||
saveImage: 'Bild speichern',
|
||||
imageUploadSuccess: 'Bild erfolgreich hochgeladen',
|
||||
imageUploadError: 'Fehler beim Hochladen des Bildes'
|
||||
},
|
||||
uploading: 'Hochladen'
|
||||
}
|
||||
+13
-1
@@ -468,5 +468,17 @@ export default {
|
||||
passwordUpdateSuccess: 'Password updated successfully',
|
||||
passwordUpdateError: 'Failed to update password',
|
||||
currentPasswordIncorrect: 'Current password is incorrect'
|
||||
}
|
||||
},
|
||||
character: {
|
||||
uploadImage: 'Upload Image',
|
||||
selectImage: 'Select Image',
|
||||
cropRect: 'Rectangular',
|
||||
cropRound: 'Round',
|
||||
preview: 'Preview',
|
||||
changeImage: 'Change Image',
|
||||
saveImage: 'Save Image',
|
||||
imageUploadSuccess: 'Image uploaded successfully',
|
||||
imageUploadError: 'Failed to upload image'
|
||||
},
|
||||
uploading: 'Uploading'
|
||||
}
|
||||
Reference in New Issue
Block a user