Merge branch 'typed-fields' into dev
This commit is contained in:
commit
a63da41435
8 changed files with 290 additions and 94 deletions
|
@ -1,100 +1,47 @@
|
|||
<template>
|
||||
<div>
|
||||
<img
|
||||
v-if="!capturing"
|
||||
class="img-fluid rounded mx-auto d-block mb-3"
|
||||
:src="item.dataImage || `https://c3lf.de/api/1/thumbs/${item.file}`"
|
||||
alt="Image not available."
|
||||
/>
|
||||
<video
|
||||
v-if="capturing"
|
||||
ref="video"
|
||||
class="img-fluid rounded mx-auto d-block mb-3"
|
||||
>
|
||||
Video stream not available.
|
||||
</video>
|
||||
<canvas ref="canvas" class="img-fluid d-none"/>
|
||||
<div class="row" v-if="capturing && !streaming">
|
||||
<div class="spinner-grow text-danger mx-auto" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-auto">
|
||||
<button v-if="!capturing" class="btn my-2 ml-auto btn-secondary" @click="openStream()">
|
||||
<font-awesome-icon icon="camera"/>
|
||||
</button>
|
||||
<div v-if="capturing" class="btn-group my-2 ml-auto">
|
||||
<button class="btn btn-success" @click="captureVideoImage()">
|
||||
<font-awesome-icon icon="camera"/> Capture
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
:class="(item.file || item.dataImage) ? 'btn-danger' : 'btn-secondary disabled'"
|
||||
@click="(item.file || item.dataImage) && closeStream()"
|
||||
>
|
||||
<font-awesome-icon icon="stop"/> Abort
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h6>Editing Item <span class="badge badge-secondary">#{{ item.item_uid }}</span></h6>
|
||||
<form>
|
||||
<div class="form-group" v-for="field in ['description', 'box']" :key="field">
|
||||
<label>{{ field }}</label>
|
||||
<input type="text" class="form-control" v-model="item[field]">
|
||||
</div>
|
||||
</form>
|
||||
<InputPhoto
|
||||
:model="item"
|
||||
field="file"
|
||||
:on-capture="storeImage"
|
||||
/>
|
||||
<InputString
|
||||
label="description"
|
||||
:model="item"
|
||||
field="description"
|
||||
:validation-fn="str => str && str.length > 0"
|
||||
/>
|
||||
<InputCombo
|
||||
label="box"
|
||||
:model="item"
|
||||
nameKey="box"
|
||||
uniqueKey="cid"
|
||||
:options="boxes"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import InputString from './inputs/InputString';
|
||||
import InputCombo from './inputs/InputCombo';
|
||||
import { mapGetters } from 'vuex';
|
||||
import InputPhoto from './inputs/InputPhoto';
|
||||
|
||||
export default {
|
||||
name: 'Add',
|
||||
name: 'EditItem',
|
||||
components: {InputPhoto, InputCombo, InputString },
|
||||
props: ['item'],
|
||||
data: () => ({
|
||||
capturing: false,
|
||||
streaming: false,
|
||||
stream: undefined
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters(['getBoxes']),
|
||||
boxes({ getBoxes }) {
|
||||
return getBoxes.map(obj => ({cid: obj.cid, box: obj.name}));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openStream() {
|
||||
if (!this.capturing) {
|
||||
this.capturing = true;
|
||||
this.streaming = false;
|
||||
navigator.mediaDevices.getUserMedia({video: true, audio: false}).then(stream => {
|
||||
this.stream = stream;
|
||||
const { video } = this.$refs;
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
video.addEventListener('canplay', () => {
|
||||
this.streaming = true;
|
||||
}, false);
|
||||
}).catch(err => console.log(err)); // todo: toast error
|
||||
storeImage(image) {
|
||||
this.item.dataImage = image;
|
||||
}
|
||||
},
|
||||
captureVideoImage() {
|
||||
const { video, canvas } = this.$refs;
|
||||
const context = canvas.getContext('2d');
|
||||
const { videoWidth, videoHeight } = video;
|
||||
canvas.width = videoWidth;
|
||||
canvas.height = videoHeight;
|
||||
context.drawImage(video, 0, 0, videoWidth, videoHeight);
|
||||
this.item.dataImage = canvas.toDataURL('image/png');
|
||||
this.closeStream();
|
||||
},
|
||||
closeStream() {
|
||||
if (this.capturing) {
|
||||
this.stream.getTracks().forEach(s => s.stop());
|
||||
this.capturing = false;
|
||||
this.streaming = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.item.file)
|
||||
this.openStream();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.closeStream();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -65,8 +65,8 @@ export default {
|
|||
}
|
||||
|
||||
/* Hacky Fix, for this Issue: https://hannover.ccc.de/gitlab/c3lf/lffrontend/issues/26 */
|
||||
.modal-body {
|
||||
/*.modal-body {
|
||||
max-height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}*/
|
||||
</style>
|
50
src/components/inputs/Addon.vue
Normal file
50
src/components/inputs/Addon.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<div class="input-group-append">
|
||||
<span
|
||||
class="input-group-text"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
:title="type"
|
||||
>{{ type[0] }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
:class="'btn-' + color"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
:title="tooltip"
|
||||
>
|
||||
<font-awesome-icon :icon="icon"/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from 'jquery';
|
||||
import 'bootstrap/js/dist/tooltip';
|
||||
|
||||
export default {
|
||||
name: 'Addon',
|
||||
props: [ 'type', 'isValid' ],
|
||||
mounted() {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
},
|
||||
computed: {
|
||||
icon() {
|
||||
if (this.isValid == undefined) return 'pen';
|
||||
if (this.isValid == false) return 'times';
|
||||
return 'check';
|
||||
},
|
||||
color() {
|
||||
if (this.isValid == undefined) return 'info';
|
||||
if (this.isValid == false) return 'danger';
|
||||
return 'success';
|
||||
},
|
||||
tooltip() {
|
||||
if (this.isValid == undefined) return 'no data validation';
|
||||
if (this.isValid == false) return 'data invalid';
|
||||
return 'data valid';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
75
src/components/inputs/InputCombo.vue
Normal file
75
src/components/inputs/InputCombo.vue
Normal file
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<div class="form-group">
|
||||
<label :for="label">{{ label }}</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<button
|
||||
class="btn btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
>Search</button>
|
||||
<div class="dropdown-menu">
|
||||
<a
|
||||
v-for="option in options"
|
||||
:key="option[uniqueKey]"
|
||||
class="dropdown-item"
|
||||
@click="setInternalValue(option)"
|
||||
>
|
||||
{{ option[nameKey] }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" class="form-control" :id="label" v-model="internalName">
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
class="btn"
|
||||
:class="{ 'btn-info disabled': isValid, 'btn-success': !isValid }"
|
||||
v-if="!isValid"
|
||||
@click="addOption()"
|
||||
>
|
||||
<font-awesome-icon icon="plus"/>
|
||||
</button>
|
||||
</div>
|
||||
<Addon type="Combo Box" :is-valid="isValid"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Addon from './Addon';
|
||||
|
||||
export default {
|
||||
name: 'InputCombo',
|
||||
components: {Addon},
|
||||
props: [ 'label', 'model', 'nameKey', 'uniqueKey', 'options', 'onOptionAdd' ],
|
||||
data: ({ options, model, nameKey, uniqueKey }) => ({
|
||||
internalName: model[nameKey],
|
||||
selectedOption: options.filter(e => e[uniqueKey] === model[uniqueKey])[0],
|
||||
addingOption: false
|
||||
}),
|
||||
computed: {
|
||||
isValid: ({options, nameKey, internalName}) => options.some(e => e[nameKey] === internalName)
|
||||
},
|
||||
watch: {
|
||||
internalName(newValue, oldValue) {
|
||||
if (this.isValid) {
|
||||
if(newValue!=this.selectedOption[this.nameKey]){
|
||||
this.selectedOption = this.options.filter(e => e[this.nameKey] === this.internalName)[0];
|
||||
}
|
||||
this.model[this.nameKey] = this.selectedOption[this.nameKey];
|
||||
this.model[this.uniqueKey] = this.selectedOption[this.uniqueKey];
|
||||
}
|
||||
console.log(oldValue, newValue, this.isValid);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setInternalValue(option) {
|
||||
this.selectedOption = option;
|
||||
this.internalName = option[this.nameKey];
|
||||
},
|
||||
addOption() {
|
||||
this.onOptionAdd({[this.nameKey]: this.internalName});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
95
src/components/inputs/InputPhoto.vue
Normal file
95
src/components/inputs/InputPhoto.vue
Normal file
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<div>
|
||||
<img
|
||||
v-if="!capturing"
|
||||
class="img-fluid rounded mx-auto d-block mb-3"
|
||||
:src="dataImage || `https://c3lf.de/api/1/thumbs/${model[field]}`"
|
||||
alt="Image not available."
|
||||
/>
|
||||
<video
|
||||
v-if="capturing"
|
||||
ref="video"
|
||||
class="img-fluid rounded mx-auto d-block mb-3"
|
||||
>
|
||||
Video stream not available.
|
||||
</video>
|
||||
<canvas ref="canvas" class="img-fluid d-none"/>
|
||||
<div class="row" v-if="capturing && !streaming">
|
||||
<div class="spinner-grow text-danger mx-auto" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-auto">
|
||||
<button v-if="!capturing" class="btn my-2 ml-auto btn-secondary" @click="openStream()">
|
||||
<font-awesome-icon icon="camera"/>
|
||||
</button>
|
||||
<div v-if="capturing" class="btn-group my-2 ml-auto">
|
||||
<button class="btn btn-success" @click="captureVideoImage()">
|
||||
<font-awesome-icon icon="camera"/> Capture
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
:class="(model[field] || dataImage) ? 'btn-danger' : 'btn-secondary disabled'"
|
||||
@click="(model[field] || dataImage) && closeStream()"
|
||||
>
|
||||
<font-awesome-icon icon="stop"/> Abort
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'InputPhoto',
|
||||
props: [ 'model', 'field', 'onCapture' ],
|
||||
data: () => ({
|
||||
capturing: false,
|
||||
streaming: false,
|
||||
stream: undefined,
|
||||
dataImage: undefined
|
||||
}),
|
||||
methods: {
|
||||
openStream() {
|
||||
if (!this.capturing) {
|
||||
this.capturing = true;
|
||||
this.streaming = false;
|
||||
navigator.mediaDevices.getUserMedia({video: true, audio: false}).then(stream => {
|
||||
this.stream = stream;
|
||||
const { video } = this.$refs;
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
video.addEventListener('canplay', () => {
|
||||
this.streaming = true;
|
||||
}, false);
|
||||
}).catch(err => console.log(err)); // todo: toast error
|
||||
}
|
||||
},
|
||||
captureVideoImage() {
|
||||
const { video, canvas } = this.$refs;
|
||||
const context = canvas.getContext('2d');
|
||||
const { videoWidth, videoHeight } = video;
|
||||
canvas.width = videoWidth;
|
||||
canvas.height = videoHeight;
|
||||
context.drawImage(video, 0, 0, videoWidth, videoHeight);
|
||||
this.dataImage = canvas.toDataURL('image/png');
|
||||
this.onCapture(this.dataImage);
|
||||
this.closeStream();
|
||||
},
|
||||
closeStream() {
|
||||
if (this.capturing) {
|
||||
this.stream.getTracks().forEach(s => s.stop());
|
||||
this.capturing = false;
|
||||
this.streaming = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.model[this.field])
|
||||
this.openStream();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.closeStream();
|
||||
}
|
||||
};
|
||||
</script>
|
29
src/components/inputs/InputString.vue
Normal file
29
src/components/inputs/InputString.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="form-group">
|
||||
<label :for="label">{{ label }}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" :id="label" v-model="model[field]">
|
||||
<Addon v-if="!hasValidationFn" type="String"/>
|
||||
<Addon v-if="hasValidationFn" type="String" :is-valid="isValid"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Addon from './Addon';
|
||||
|
||||
export default {
|
||||
name: 'InputString',
|
||||
components: { Addon },
|
||||
props: [ 'label', 'model', 'field', 'validationFn' ],
|
||||
computed: {
|
||||
hasValidationFn({ validationFn }) {
|
||||
return validationFn != undefined;
|
||||
},
|
||||
isValid({ model, field, validationFn, hasValidationFn }) {
|
||||
if (!hasValidationFn) return true;
|
||||
return validationFn(model[field]);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -11,10 +11,10 @@ import 'bootstrap/dist/js/bootstrap.min.js';
|
|||
|
||||
// fontawesome
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList, faWindowClose, faCamera, faStop } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList, faWindowClose, faCamera, faStop, faPen, faCheck, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
|
||||
library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList, faWindowClose, faCamera, faStop);
|
||||
library.add(faPlus, faCheckCircle, faEdit, faTrash, faCat, faSyncAlt, faSort, faSortUp, faSortDown, faTh, faList, faWindowClose, faCamera, faStop, faPen, faCheck, faTimes);
|
||||
|
||||
Vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ const store = new Vuex.Store({
|
|||
getEventSlug: state => state.route && state.route.params.event? state.route.params.event : state.events.length ? state.events[0].slug : '36C3',
|
||||
getActiveView: state => state.route.name || 'items',
|
||||
getFilters: state => state.route.query,
|
||||
getBoxes: state => state.loadedBoxes
|
||||
},
|
||||
mutations: {
|
||||
replaceEvents(state, events) {
|
||||
|
@ -91,7 +92,6 @@ const store = new Vuex.Store({
|
|||
let file = new File([blob], 'dot.png', blob);
|
||||
delete item.dataImage;
|
||||
item.image = file;
|
||||
item.cid = item.cid || 1;
|
||||
var formData = new FormData();
|
||||
for ( var key in item ) formData.append(key, item[key]);
|
||||
const { data } = await axios.post(`/1/${getters.getEventSlug}/item`, formData, {
|
||||
|
|
Loading…
Reference in a new issue