Commit bab3529f authored by stahl's avatar stahl
Browse files

src

parent c1bfa9a7
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="data:,">
<link href="https://fonts.googleapis.com/css?family=Lato|Vollkorn:400,600,700" rel="stylesheet"> <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
**/node_modules
**/dist
<template>
<StereometrieEditor />
<br />
<br />
<br />
<footer>
<img alt="ZHdK logo" width="200" src="./assets/logo.png">
</footer>
</template>
<script>
import StereometrieEditor from './components/StereometrieEditor.vue'
export default {
name: 'App',
components: {
StereometrieEditor
}
}
</script>
<style>
html {
font-family: "Lato", sans-serif;
}
body {
color: #aaa;
background: #444;
}
footer {
bottom: 0px;
width: 88vw;
color: #111;
background: #fff;
padding: 30px;
}
</style>
FROM node:latest as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY ./ .
RUN npm run build
FROM nginx as production-stage
RUN mkdir /app
COPY --from=build-stage /app/dist /app
COPY nginx.conf /etc/nginx/nginx.conf
<template>
<div class="dialogue">
<div
v-if=selection.length
class="files"
>
<div
v-for="(file, index) in selection"
:key=index
@click="onClickFile(file)"
:class="fileStyle"
class="file"
>
<div>{{ file }}</div>
<img
:src="imgUrl(file)"
class="image"
/>
</div>
</div>
<div
v-else
>
Keine Daten vorhanden.
</div>
<button @click="cancel">Abbrechen</button>
</div>
</template>
<script>
import {
computed,
//ref,
} from "vue";
export default {
emits: ['submit', 'cancel'],
props: {
files: {
type: Array,
default: null,
},
originalFiles: {
type: Array,
default: null,
},
placeholder: {
type: String,
default: null,
},
index: {
type: Number,
default: null,
},
},
setup: function(props) {
const selection = computed(() => {
const placeholder = props.placeholder;
const index = props.index;
const files = props.files;
const originalFiles = props.originalFiles;
// find thumbs range at index
let backwardSlice = files.slice(0, index+1);
backwardSlice = backwardSlice.filter(item => item !== placeholder);
let forwardSlice = files.slice(index+1);
forwardSlice = forwardSlice.filter(item => item !== placeholder);
const a = originalFiles.indexOf(backwardSlice.at(-1));
const b = originalFiles.indexOf(forwardSlice.at(0));
return originalFiles.slice(a+1,b);
});
const imgUrl = (item) => {
return `${document.settings.MEDIA_URL}/${item}`;
};
const fileStyle = computed(() => {
return {
'grid-template-rows': `repeat(${props.files.length}, auto)`
}
});
return {
selection,
imgUrl,
fileStyle,
};
},
methods: {
onClickFile(file) {
this.$emit("submit", file);
},
cancel() {
this.$emit("cancel");
},
}
};
</script>
<style scoped>
.dialogue {
position: absolute;
padding: 20px;
right: 20px;
background: rgba(1.0,1.0,1.0,1.0);
}
.files {
margin: 10px;
padding: 10px;
}
.file {
padding: 10px;
cursor: pointer;
display: grid;
grid-template-columns: repeat(2, auto);
grid-gap: 20px;
border: 1px solid grey;
align-items: center;
}
.file:hover {
border: 1px solid yellow;
}
.image {
width: 200px;
}
</style>
<template>
<header>
<h1>Stereometrie</h1>
<div class="actions top">
<div>
Index: <text-reader once @load="load"></text-reader>
</div>
<div>
Öffnen: <text-reader @load="importData"></text-reader>
</div>
<button @click="save">Speichern ...</button>&nbsp;&nbsp;
<div>
Seite
<select
v-model="page"
>
<option
v-for="(page, index) in Array.from(Array(numPages).keys())"
:key=index
:value=page
>
{{ page+1 }}
</option>
</select>
</div>
<div>
Zoom
<select
v-model="zoomLevel"
>
<option
v-for="(level, index) in Array.from(Array(20).keys())"
:key=index
:value=level+1
>
{{ level+1 }}
</option>
</select>
</div>
</div>
<div class="actions options">
<label style="color: magenta;">
Bilder-Lücken anzeigen
<input type=checkbox v-model=showGaps />
</label>
<label style="color: red;">
Fehler anzeigen
<input type=checkbox v-model=showLeftRightMarker />
</label>
<label>
Vorschau
<input type=checkbox v-model=showPreview />
</label>
</div>
<div class="actions editor">
<button @click="replaceWithPlaceholder()">Mit Platzhalter ersetzen [r]</button>
<button @click="addPlaceholder()">Platzhalter einfügen [p]</button>
<button @click="toggleAddThumbsDialogue()">Bild einfügen [a]</button>
<button @click="removeThumb()">Löschen [delete/backspace]</button>
</div>
<div class="actions info">
<div>
{{ x+1 }} / {{ y+1 }}
</div>
<div>
{{ filenameIndexAtIndex(index) }}
</div>
<div>
{{ filenameDateAtIndex(index) }}
</div>
<div>
{{ filenameTimeAtIndex(index) }}
</div>
<div>
{{ files[index] }}
</div>
</div>
</header>
<div class="thumb-preview">
<img
v-lazy="imgUrl(files[index])"
/>
</div>
<div
class="thumbs"
>
<div
v-if="files && files.length && originalFiles && originalFiles.length"
>
<select-thumb-dialogue v-if="isAddThumbsDialogueActive" @cancel="cancelSelect" @submit="insertFile" :files="files" :originalFiles="originalFiles" :index="index" :placeholder="placeholder"></select-thumb-dialogue>
<div
:style="pageStyle"
>
<div
v-for="(item, index) in filesForPage"
:key=index
>
<img
v-lazy="imgUrl(item)"
@click="thumbClicked(index)"
:title="item"
:style="[thumbStyle(item, index), checkLeftRight(item, index), cursorStyle(index)]"
/>
</div>
</div>
</div>
<div
v-else
>
Keine Daten vorhanden.
</div>
</div>
</template>
<script>
import { useKeypress } from 'vue3-keypress';
import {
computed,
ref,
inject,
} from "vue";
import TextReader from "./TextReader";
import SelectThumbDialogue from "./SelectThumbDialogue";
export default {
name: 'StereometrieEditor',
components: {
TextReader,
SelectThumbDialogue,
},
setup: function() {
const Lazyload = inject('Lazyload');
const keyboardIsActive = ref(true);
Lazyload.$on('error', function ({el, src}) {
//console.table(Lazyload.performance())
console.log(el, src);
});
const placeholder = 'placeholder.jpg';
const originalFiles = ref([]);
const files = ref([]);
/*
const files = ref(
Array(
'2019/0207906-20191130-12-31-54.58-L.jpg',
placeholder,
'2019/0207908-20191130-13-01-46.25-R.jpg',
'2019/0207909-20191130-13-31-52.48-L.jpg',
'2019/0207910-20191130-13-31-52.73-R.jpg',
'2019/0207911-20191130-14-02-18.33-L.jpg',
'2019/0207912-20191130-14-02-18.59-R.jpg',
'2019/0207913-20191130-14-31-34.88-L.jpg',
'2019/0207914-20191130-14-31-35.15-R.jpg',
'2019/0207915-20191130-15-01-50.00-L.jpg',
'2019/0207916-20191130-15-01-50.27-R.jpg',
'2019/0207917-20191130-15-31-43.16-L.jpg',
'2019/0207918-20191130-15-31-43.44-R.jpg',
'2019/0207919-20191130-16-01-31.35-L.jpg',
'2019/0207920-20191130-16-01-31.64-R.jpg',
'2019/0207921-20191130-16-31-37.58-L.jpg',
'2019/0207922-20191130-16-31-37.87-R.jpg',
'2019/0207923-20191130-17-01-45.27-L.jpg',
'2019/0207924-20191130-17-01-45.57-R.jpg',
'2019/0207925-20191130-17-31-53.68-L.jpg',
'2019/0207926-20191130-18-01-33.63-L.jpg',
'2019/0207927-20191130-18-01-33.94-R.jpg',
'2019/0207928-20191130-18-31-31.46-L.jpg',
'2019/0207929-20191130-18-31-31.78-R.jpg',
'2019/0207930-20191130-19-01-38.28-L.jpg',
'2019/0207931-20191130-19-01-38.60-R.jpg',
)
);
*/
const filesToInsert = ref(null);
const isAddThumbsDialogueActive = ref(false);
const page = ref(0);
const zoomLevel = ref(8);
const numPages = 88;
const rows = 48;
const cols = 32;
const pageSize = rows * cols;
const x = ref(0); // cursor x
const y = ref(0); // cursor y
const showGaps = ref(true);
const showLeftRightMarker = ref(true);
const showPreview = ref(false);
const pageIndex = computed(() => {
return x.value + y.value * cols;
});
const index = computed(() => {
return page.value * pageSize + pageIndex.value;
});
const removeThumb = () => {
files.value.splice(index.value, 1);
};
const replaceWithPlaceholder = () => {
files.value[index.value] = placeholder;
};
const addPlaceholder = () => {
files.value[index.value+1] = placeholder;
};
const toggleAddThumbsDialogue = () => {
if (isAddThumbsDialogueActive.value == true) {
isAddThumbsDialogueActive.value = false;
//keyboardIsActive.value = true;
} else {
isAddThumbsDialogueActive.value = true;
//keyboardIsActive.value = false;
}
}
const insertFile = (file) => {
console.log('insert', file);
files.value.splice(index.value+1, 0, file);
toggleAddThumbsDialogue();
};
const cancelSelect = () => {
toggleAddThumbsDialogue();
};
const filesForPage = computed(() => {
return files.value.slice(page.value*pageSize, page.value*pageSize+pageSize);
});
const imgUrl = (item) => {
if (item == placeholder) {
return `${document.settings.MEDIA_URL}/${placeholder}`;
}
return `${document.settings.MEDIA_URL}/${item}`;
}
const mod = (a, n) => {
return ((a % n ) + n ) % n;
}
const onKeyDown = ({ event }) => {
switch(event.keyCode) {
case 37: // left
case 72: // h
x.value = mod(x.value-1, cols);
break;
case 39: // right
case 76: // l
x.value = mod(x.value+1, cols);
break;
case 38: // up
case 75: // k
y.value = mod(y.value-1, rows);
break;
case 40: // down
case 74: // j
y.value = mod(y.value+1, rows);
break;
case 46: // delete
removeThumb()
break;
case 8: // backspace
removeThumb()
x.value = mod(x.value-1, cols);
break;
case 82: // r
replaceWithPlaceholder()
break;
case 80: //p
addPlaceholder()
x.value = mod(x.value+1, cols);
break;
case 65: // a
toggleAddThumbsDialogue()
break;
}
}
useKeypress({
keyEvent: "keydown",
onAnyKey: onKeyDown,
keyBinds: [],
isActive: keyboardIsActive,
})
return {
originalFiles,
files,
filesToInsert,
textFileToSave: null,
x,
y,
rows,
cols,
page,
pageIndex,
index,
numPages,
pageSize,
filesForPage,
thumbWidth: 407,
thumbHeight: 273,
zoomLevel,
placeholder,
imgUrl,
keyboardIsActive,
cancelSelect,
insertFile,
removeThumb,
replaceWithPlaceholder,
addPlaceholder,
toggleAddThumbsDialogue,
isAddThumbsDialogueActive,
showGaps,
showLeftRightMarker,
showPreview,
mod,
};
},
computed: {
pageStyle() {
const width = this.thumbWidth / this.zoomLevel;
const height = this.thumbHeight / this.zoomLevel;
let style = {
'display': 'grid',
'grid-template-columns': `repeat(${this.cols}, ${width}px)`,
'grid-template-rows': `repeat(${this.rows}, ${height}px)`,
};
if (!this.showPreview) {
style['grid-gap'] = '4px';
}
return style;
},
itemsWithGaps() {
const thumbsWithoutPlaceholder = this.files.filter(item => item !== this.placeholder);
const items = thumbsWithoutPlaceholder.filter((item, index) => {
const difference = Math.abs(this.filenameIndexAtIndex(index) - this.filenameIndexAtIndex(index+1));
return difference != 1;
});
console.log(items);
return items;
},
},
methods: {
thumbClicked(i) {
this.y = parseInt(i / this.cols);
this.x = i % this.cols;
},
filenameIndexAtIndex(i) {
if (this.files && this.files[i]) {
const index = this.files[i].slice(-34, -34+7);
if (index) {
return index;
} else {
return 'no valid index';
}
}
},
filenameDateAtIndex(i) {
if (this.files && this.files[i]) {
const d = this.files[i].slice(-26, -26+8);
if (!d) {
return 'no valid date';
}
const year = d.slice(0,4);
const month = d.slice(4,6);
const day = d.slice(6,8);
return `${day}.${month}.${year}`
}
},
filenameTimeAtIndex(i) {
if (this.files && this.files[i]) {
const d = this.files[i].slice(-17, -17+8);
if (!d) {
return 'no valid date';
}
const hour = d.slice(0,2);
const minute = d.slice(3,5);
const seconds = d.slice(6,8);
return `${hour}:${minute}:${seconds}`
}
},
cursorStyle(index) {
if (this.showPreview) {
return {};
}
let style = {};
if (index === this.pageIndex) {
style.border = '2px solid yellow';
}
return style;
},
// eslint-disable-next-line
thumbStyle(item, index) {
let style = {};
const width = this.thumbWidth / this.zoomLevel;
const height = this.thumbHeight / this.zoomLevel;
style.width = `${width}px`;
style.height = `${height}px`;
if (!this.showPreview && this.showGaps) {
if (this.itemsWithGaps.includes(item)) {