Merge branch 'typed-fields' into dev

This commit is contained in:
j3d1 2019-12-23 22:25:17 +01:00
commit a63da41435
8 changed files with 290 additions and 94 deletions

View file

@ -1,100 +1,47 @@
<template> <template>
<div> <div>
<img <h6>Editing Item <span class="badge badge-secondary">#{{ item.item_uid }}</span></h6>
v-if="!capturing" <InputPhoto
class="img-fluid rounded mx-auto d-block mb-3" :model="item"
:src="item.dataImage || `https://c3lf.de/api/1/thumbs/${item.file}`" field="file"
alt="Image not available." :on-capture="storeImage"
/> />
<video <InputString
v-if="capturing" label="description"
ref="video" :model="item"
class="img-fluid rounded mx-auto d-block mb-3" field="description"
> :validation-fn="str => str && str.length > 0"
Video stream not available. />
</video> <InputCombo
<canvas ref="canvas" class="img-fluid d-none"/> label="box"
<div class="row" v-if="capturing && !streaming"> :model="item"
<div class="spinner-grow text-danger mx-auto" role="status"> nameKey="box"
<span class="sr-only">Loading...</span> uniqueKey="cid"
</div> :options="boxes"
</div> />
<div class="row m-auto"> </div>
<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"/>&nbsp;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"/>&nbsp;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>
</div>
</template> </template>
<script> <script>
import InputString from './inputs/InputString';
import InputCombo from './inputs/InputCombo';
import { mapGetters } from 'vuex';
import InputPhoto from './inputs/InputPhoto';
export default { export default {
name: 'Add', name: 'EditItem',
components: {InputPhoto, InputCombo, InputString },
props: ['item'], props: ['item'],
data: () => ({ computed: {
capturing: false, ...mapGetters(['getBoxes']),
streaming: false, boxes({ getBoxes }) {
stream: undefined 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
}
},
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() { methods: {
if (!this.item.file) storeImage(image) {
this.openStream(); this.item.dataImage = image;
}, }
beforeDestroy() {
this.closeStream();
} }
}; };
</script> </script>

View file

@ -65,8 +65,8 @@ export default {
} }
/* Hacky Fix, for this Issue: https://hannover.ccc.de/gitlab/c3lf/lffrontend/issues/26 */ /* Hacky Fix, for this Issue: https://hannover.ccc.de/gitlab/c3lf/lffrontend/issues/26 */
.modal-body { /*.modal-body {
max-height: calc(100vh - 200px); max-height: calc(100vh - 200px);
overflow-y: auto; overflow-y: auto;
} }*/
</style> </style>

View 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>

View 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>

View 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"/>&nbsp;Capture
</button>
<button
class="btn"
:class="(model[field] || dataImage) ? 'btn-danger' : 'btn-secondary disabled'"
@click="(model[field] || dataImage) && closeStream()"
>
<font-awesome-icon icon="stop"/>&nbsp;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>

View 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>

View file

@ -11,10 +11,10 @@ import 'bootstrap/dist/js/bootstrap.min.js';
// fontawesome // fontawesome
import { library } from '@fortawesome/fontawesome-svg-core'; 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'; 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); Vue.component('font-awesome-icon', FontAwesomeIcon);

View file

@ -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', 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', getActiveView: state => state.route.name || 'items',
getFilters: state => state.route.query, getFilters: state => state.route.query,
getBoxes: state => state.loadedBoxes
}, },
mutations: { mutations: {
replaceEvents(state, events) { replaceEvents(state, events) {
@ -91,7 +92,6 @@ const store = new Vuex.Store({
let file = new File([blob], 'dot.png', blob); let file = new File([blob], 'dot.png', blob);
delete item.dataImage; delete item.dataImage;
item.image = file; item.image = file;
item.cid = item.cid || 1;
var formData = new FormData(); var formData = new FormData();
for ( var key in item ) formData.append(key, item[key]); for ( var key in item ) formData.append(key, item[key]);
const { data } = await axios.post(`/1/${getters.getEventSlug}/item`, formData, { const { data } = await axios.post(`/1/${getters.getEventSlug}/item`, formData, {