This commit is contained in:
j3d1 2023-07-17 15:53:26 +02:00
parent f7c9d1d069
commit f3b8a3247e
8 changed files with 322 additions and 105 deletions

View file

@ -0,0 +1,76 @@
<template>
<div class="d-inline-block" v-if="show_camera">
<label for="pictures">
<slot></slot>
</label>
<input type="file" accept="image/*" multiple capture="camera" id="pictures" @change="loadFiles" class="d-none">
</div>
</template>
<style scoped>
</style>
<script>
import * as BIcons from "bootstrap-icons-vue";
export default {
name: "CameraFileSource",
components: {
...BIcons
},
emits: ["input"],
data() {
return {
show_camera: false
}
},
methods: {
loadFiles() {
const files = document.getElementById("pictures").files;
const jobs = [...files].map((file) => {
return new Promise((resolve, reject) => {
var reader = new FileReader();
reader.onload = () => {
const buffer = reader.result;
if (!(buffer instanceof ArrayBuffer)) {
console.log(buffer)
reject("Not an ArrayBuffer");
}
const data = new Uint8Array(buffer);
const hash = nacl.crypto_hash(data).reduce((a, b) => a + b.toString(16).padStart(2, "0"), "");
var base64 = btoa(
data.reduce((a, b) => a + String.fromCharCode(b), '')
);
resolve({
name: file.name,
size: file.size,
mime_type: file.type,
data: base64,
hash: hash,
});
};
reader.onerror = (e) => {
reject(e);
};
reader.readAsArrayBuffer(file)
})
});
Promise.all(jobs).then((files) => {
this.$emit("input", files)
})
}
},
mounted() {
//detect media api
if ('capture' in document.createElement('input')) {
console.log('capture supported.');
this.show_camera = true;
}
if (navigator.getUserMedia) {
console.log('getUserMedia supported.');
this.show_camera = true;
}
}
}
</script>

View file

@ -1,33 +1,33 @@
<template> <template>
<div class="input-group"> <drag-drop-file-source @input="addFiles">
<h3 v-if="create">Create New</h3>
<h3 v-else>Update</h3>
<ul> <ul>
<li v-for="file in files" :key="file.id"> <li v-for="file in whithout_images(files)" :key="file.id">
{{ file.name }}
</li>
<li v-for="file in whithout_images(item_files)" :key="file.id">
{{ file.name }} {{ file.name }}
</li> </li>
</ul> </ul>
<ul> <hr>
<img v-for="file in files" :key="file.id" :src="file.name" :alt="file.name" class="img-thumbnail"> <div>
</ul> <img v-for="file in only_images(files)" :key="file.id" :src="file.name" :alt="file.name"
<h4>Files</h4> class="img-thumbnail" :title="file.mime_type">
<div class="input-group"> <img v-for="file in whithout_images(item_files)" :key="file.id" :alt="file.name"
<input type="file" class="form-control" multiple id="files"> :src="'data:' + file.mime_type + ';base64,' + file.data" class="img-thumbnail border-info">
<button class="btn btn-outline-secondary" type="button"> <fs-file-source @input="addFiles">
<b-icon-trash></b-icon-trash> <div class="img-thumbnail btn btn-outline-primary">
</button> <b-icon-upload></b-icon-upload>
<button class="btn btn-outline-secondary" type="button" @click="uploadFiles"> </div>
<b-icon-upload></b-icon-upload> </fs-file-source>
</button> <camera-file-source @input="addFiles">
<div class="img-thumbnail btn btn-outline-primary">
<b-icon-camera></b-icon-camera>
</div>
</camera-file-source>
</div> </div>
<div class="input-group" v-if="show_camera"> </drag-drop-file-source>
<input type="file" class="form-control" accept="image/*" multiple capture="camera" id="photos">
<button class="btn btn-outline-secondary" type="button">
<b-icon-trash></b-icon-trash>
</button>
<button class="btn btn-outline-secondary" type="button" @click="uploadPhoto">
<b-icon-upload></b-icon-upload>
</button>
</div>
</div>
</template> </template>
<style scoped> <style scoped>
@ -36,108 +36,82 @@
height: 54px; height: 54px;
object-fit: cover; object-fit: cover;
} }
.img-thumbnail svg {
width: 100%;
height: 100%;
}
</style> </style>
<script> <script>
import * as BIcons from "bootstrap-icons-vue"; import * as BIcons from "bootstrap-icons-vue";
import {mapActions, mapState} from "vuex"; import {mapActions, mapState} from "vuex";
import DragDropFileSource from "@/components/DragDropFileSource.vue";
import FsFileSource from "@/components/FsFileSource.vue";
import CameraFileSource from "@/components/CameraFileSource.vue";
export default { export default {
name: "CombinedFileField", name: "CombinedFileField",
data() {
return {
show_camera: false
}
},
components: { components: {
...BIcons ...BIcons,
DragDropFileSource,
CameraFileSource,
FsFileSource
}, },
props: { props: {
value: { item_files: {
type: Array, type: Array,
required: true required: true
}, },
item_id: {
type: Number
},
create: {
type: Boolean,
default: false
}
}, },
model: { emits: ["change"],
prop: "value",
event: "input"
},
computed: { computed: {
...mapState(["files"]), ...mapState(["files"]),
}, },
methods: { methods: {
...mapActions(["fetchFiles", "pushFile"]), ...mapActions(["fetchFiles", "pushFile"]),
async uploadFiles() { async uploadFiles(files) {
const files = document.getElementById("files").files; const jobs = files.map(async file => {
for (let i = 0; i < files.length; i++) {
var reader = new FileReader();
const file = files[i];
const buffer = await new Promise((resolve, reject) => {
reader.onload = (e) => {
resolve(e.target.result);
};
reader.onerror = (e) => {
reject(e);
};
reader.readAsArrayBuffer(file)
});
const data = new Uint8Array(buffer);
const hash = nacl.crypto_hash(data);
var base64 = btoa(
data.reduce((a, b) => a + String.fromCharCode(b), '')
);
await this.pushFile({ await this.pushFile({
file: { file: file,
mime_type: file.type, item_id: this.item_id
data: base64,
},
item_id: 1
}); });
await this.fetchFiles(); });
} const responses = await Promise.all(jobs);
console.log(responses);
return responses;
}, },
async uploadPhoto() { addFiles(files) {
const files = document.getElementById("photos").files; const newfiles = files.filter(file => !this.item_files.find(f => f.hash === file.hash));
for (let i = 0; i < files.length; i++) { if (newfiles.length === 0) {
var reader = new FileReader(); console.log("no new files");
const file = files[i]; return;
const buffer = await new Promise((resolve, reject) => {
reader.onload = (e) => {
resolve(e.target.result);
};
reader.onerror = (e) => {
reject(e);
};
reader.readAsArrayBuffer(file)
});
const data = new Uint8Array(buffer);
const hash = nacl.crypto_hash(data);
var base64 = btoa(
data.reduce((a, b) => a + String.fromCharCode(b), '')
);
await this.pushFile({
file: {
mime_type: file.type,
data: base64,
},
item_id: 1
});
await this.fetchFiles();
} }
if (!this.create) {
this.uploadFiles(newfiles).then((uploaded) => {
console.log(uploaded);
})
}
this.$emit("change", [...this.item_files, ...newfiles]);
console.log(this.item_files);
},
only_images(files) {
return files.filter(file => file.mime_type.startsWith("image/"));
},
whithout_images(files) {
return files.filter(file => !file.mime_type.startsWith("image/"));
}, },
}, },
mounted() { mounted() {
this.fetchFiles(); this.fetchFiles();
//detect media api
if ('capture' in document.createElement('input')) {
console.log('capture supported.');
this.show_camera = true;
}
if (navigator.getUserMedia) {
console.log('getUserMedia supported.');
this.show_camera = true;
}
} }
} }
</script> </script>

View file

@ -0,0 +1,88 @@
<template>
<div @dragenter.prevent="dragenter" @dragover.prevent="dragover" @dragleave.prevent="dragleave"
@drop.prevent="drop" ref="dropzone">
<slot></slot>
</div>
</template>
<style scoped>
.dragover {
opacity: 0.5;
}
</style>
<script>
import * as BIcons from "bootstrap-icons-vue";
export default {
name: "DragDropFileSource",
components: {
...BIcons
},
emits: ["input"],
methods: {
dragenter(e) {
if (e.dataTransfer.types.includes("Files")) {
e.preventDefault()
e.stopPropagation()
this.$refs.dropzone.classList.add("dragover")
}else{
console.log(e.dataTransfer.types) //TODO text/uri-list?
}
},
dragover(e) {
if (e.dataTransfer.types.includes("Files")) {
e.preventDefault()
e.stopPropagation()
this.$refs.dropzone.classList.add("dragover")
}
},
dragleave(e) {
if (e.dataTransfer.types.includes("Files")) {
e.preventDefault()
e.stopPropagation()
this.$refs.dropzone.classList.remove("dragover")
}
},
drop(e) {
if (e.dataTransfer.types.includes("Files")) {
e.preventDefault()
e.stopPropagation()
this.$refs.dropzone.classList.remove("dragover")
const jobs = [...e.dataTransfer.files].map((file) => {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.readAsArrayBuffer(file)
reader.onloadend = () => {
const buffer = reader.result;
if (!(buffer instanceof ArrayBuffer)) {
console.log(buffer)
reject("Not an ArrayBuffer");
}
const data = new Uint8Array(buffer);
const hash = nacl.crypto_hash(data).reduce((a, b) => a + b.toString(16).padStart(2, "0"), "");
var base64 = btoa(
data.reduce((a, b) => a + String.fromCharCode(b), '')
);
resolve({
name: file.name,
size: file.size,
mime_type: file.type,
data: base64,
hash: hash,
});
}
reader.onerror = (e) => {
reject(e)
}
})
})
Promise.all(jobs).then((files) => {
this.$emit("input", files)
console.log("drop", files)
})
}
}
}
}
</script>

View file

@ -0,0 +1,59 @@
<template>
<div class="d-inline-block">
<label for="files">
<slot></slot>
</label>
<input type="file" multiple id="files" @change="loadFiles" class="d-none">
</div>
</template>
<style scoped>
</style>
<script>
import * as BIcons from "bootstrap-icons-vue";
export default {
name: "FsFileSource",
components: {
...BIcons
},
emits: ["input"],
methods: {
loadFiles() {
const files = document.getElementById("files").files;
const jobs = [...files].map((file) => {
return new Promise((resolve, reject) => {
var reader = new FileReader();
reader.onload = () => {
const buffer = reader.result;
if (!(buffer instanceof ArrayBuffer)) {
console.log(buffer)
reject("Not an ArrayBuffer");
}
const data = new Uint8Array(buffer);
const hash = nacl.crypto_hash(data).reduce((a, b) => a + b.toString(16).padStart(2, "0"), "");
var base64 = btoa(
data.reduce((a, b) => a + String.fromCharCode(b), '')
);
resolve({
name: file.name,
size: file.size,
mime_type: file.type,
data: base64,
hash: hash,
});
};
reader.onerror = (e) => {
reject(e);
};
reader.readAsArrayBuffer(file)
})
});
Promise.all(jobs).then((files) => {
this.$emit("input", files)
})
}
},
}
</script>

View file

@ -253,7 +253,7 @@ export default createStore({
async pushFile({state, dispatch, getters}, {item_id, file}) { async pushFile({state, dispatch, getters}, {item_id, file}) {
const servers = await dispatch('getHomeServers') const servers = await dispatch('getHomeServers')
const data = await servers.post(getters.signAuth, '/api/item_files/'+item_id+'/', file) const data = await servers.post(getters.signAuth, '/api/item_files/'+item_id+'/', file)
if (data.mime_type) { if (data.hash) {
state.files.push(data) state.files.push(data)
return data return data
} }

View file

@ -36,10 +36,10 @@
</div> </div>
<!-- actions --> <!-- actions -->
<div class="card"> <div class="card">
<a class="btn btn-primary" :href="'/inventory/' + id + '/edit'"> <button class="btn btn-primary" @click="$router.push('/inventory/' + id + '/edit')">
<b-icon-pencil-square></b-icon-pencil-square> <b-icon-pencil-square></b-icon-pencil-square>
Edit Edit
</a> </button>
<button type="submit" class="btn btn-danger" <button type="submit" class="btn btn-danger"
@click="deleteInventoryItem(item).then(() => $router.push('/inventory'))"> @click="deleteInventoryItem(item).then(() => $router.push('/inventory'))">
<b-icon-trash></b-icon-trash> <b-icon-trash></b-icon-trash>

View file

@ -6,6 +6,13 @@
<div class="card"> <div class="card">
<div class="card-header">Edit Item</div> <div class="card-header">Edit Item</div>
<div class="card-body"> <div class="card-body">
<div class="mb-3">
<ul>
<li v-for="file in item.files" :key="file.name">
{{ file.name }} ({{ file.hash }})
</li>
</ul>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Name</label> <label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" <input type="text" class="form-control" id="name" name="name"
@ -31,7 +38,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="image" class="form-label">Image</label> <label for="image" class="form-label">Image</label>
<combined-file-field :value="item.files"></combined-file-field> <combined-file-field :item_files="item.files" :item_id="item.id" @change="addFiles"></combined-file-field>
</div> </div>
</div> </div>
<div class="card"> <div class="card">
@ -78,12 +85,16 @@ export default {
description: "", description: "",
owned_quantity: 0, owned_quantity: 0,
image: "", image: "",
files: [],
...this.inventory_items.find(item => item.id === parseInt(this.id)) ...this.inventory_items.find(item => item.id === parseInt(this.id))
} }
} }
}, },
methods: { methods: {
...mapActions(["fetchInventoryItems", "updateInventoryItem"]) ...mapActions(["fetchInventoryItems", "updateInventoryItem"]),
addFiles(files) {
this.inventory_items.find(item => item.id === parseInt(this.id)).files = files
},
}, },
async mounted() { async mounted() {
await this.fetchInventoryItems() await this.fetchInventoryItems()

View file

@ -6,6 +6,13 @@
<div class="card"> <div class="card">
<div class="card-header">Create New Item</div> <div class="card-header">Create New Item</div>
<div class="card-body"> <div class="card-body">
<div class="mb-3">
<ul>
<li v-for="file in item.files" :key="file.name">
{{ file.name }} ({{ file.hash }})
</li>
</ul>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Name</label> <label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" <input type="text" class="form-control" id="name" name="name"
@ -31,7 +38,8 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="image" class="form-label">Image</label> <label for="image" class="form-label">Image</label>
<combined-file-field :value="item.files"></combined-file-field> <combined-file-field :item_files="item.files" create
@change="item.files = $event"></combined-file-field>
</div> </div>
</div> </div>
<div class="card"> <div class="card">
@ -71,7 +79,8 @@ export default {
owned_quantity: 0, owned_quantity: 0,
image: "", image: "",
tags: [], tags: [],
properties: [] properties: [],
files: []
} }
} }
}, },