add photo picker

This commit is contained in:
MarcZierle 2022-01-16 20:52:54 +01:00
parent c97742efd7
commit e750480fd0
9 changed files with 600 additions and 11 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="https://www.zierle-training.de/images/152x152/2980389/favicon.png"> <link rel="icon" href="favicon.png">
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>
</head> </head>
<body> <body>

View File

@ -12,4 +12,40 @@ const apiClient = axios.create({
export function getPhotoLogList() { export function getPhotoLogList() {
return apiClient.get('/photologs/') return apiClient.get('/photologs/')
}
export function getPhotoLog(id) {
return apiClient.get('/photolog/'+id+'/')
}
export function addNewPhotoLog(title, date) {
return apiClient.post('/addphotolog/', {
title,
date: date,
render_date: true,
start_slide_image: null,
slides: []
})
}
export function updatePhotoLog({id, title, date, render_date, start_slide_image, slides}) {
return apiClient.put('/updatephotolog/'+id+'/', {
title,
date,
render_date,
start_slide_image,
slides
})
}
export function deletePhotoLog(id) {
return apiClient.delete('/deletephotolog/'+id+'/')
}
export function getPhotoGroups() {
return apiClient.get('/photogroups/')
}
export function getPhotosByGroup(group_id) {
return apiClient.get('/photos/?photogroup='+group_id)
} }

View File

@ -0,0 +1,157 @@
<!-- eslint-disable vue/no-mutating-props -->
<template>
<n-modal v-model:show="showSelection" :mask-closable=false>
<n-card
style="width: 75vw"
title="Select Photos"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<template #header-extra>
<p v-if="isFinite(max_select) && !or_less">
{{num_selected}}/{{max_select}} photo(s) selected
</p>
<p v-else-if="isFinite(max_select) && or_less">
maximum of {{selecions_left}} photo(s) left
</p>
<p v-else>
{{num_selected}} photo(s) selected
</p>
</template>
<n-scrollbar style="max-height: 80vh;">
<n-collapse :default-expanded-names="[first_group_id]">
<n-collapse-item v-for="group in sortedPhotoGroups"
:key="group.id"
:title="group.name"
:name="group.id">
<n-image-group>
<n-space>
<n-image
v-for="photo in photos[group.id]"
:key="photo.id"
width="100"
:src="photo.cropped_image !== null ? photo.cropped_image : photo.original_image"
@click="toggle_select_photo(photo.id)"
:class="{selected: is_photo_selected(photo.id)}"
preview-disabled />
</n-space>
</n-image-group>
<template #header-extra>{{group.date}}</template>
</n-collapse-item>
</n-collapse>
</n-scrollbar>
<template #action>
<n-space justify="end">
<n-button @click="this.$emit('closed')">Cancel</n-button>
<n-button @click="this.$emit('closed'); this.$emit('selected', this.selection)" type="primary">Select</n-button>
</n-space>
</template>
</n-card>
</n-modal>
</template>
<script>
import { useMessage } from 'naive-ui'
export default {
name: 'PhotoSelectModal',
emits: [ 'closed', 'selected' ],
props: {
showSelection: {
type: Boolean,
required: true,
default: false
},
max_select: {
type: Number,
required: false,
default: Infinity,
},
or_less: {
type: Boolean,
required: false,
default: true
},
alreadySelected: {
type: Array,
required: false,
default: () => {return []}
}
},
setup() {
const message = useMessage()
return { message }
},
data() {
return {
selection: [],
photoGroups: []
}
},
watch: {
showSelection: function () {
if (this.showSelection) {
this.selection = [...this.alreadySelected]
this.$store.dispatch('loadPhotoGroups').then(() => {
this.photoGroups = this.$store.getters.photoGroups
this.$store.dispatch('loadPhotosInAllGroups')
})
}
}
},
computed: {
photos() {
return this.$store.getters.photos
},
num_selected() {
return this.selection.length
},
selecions_left() {
return this.max_select - this.num_selected
},
first_group_id() {
if (this.sortedPhotoGroups.length > 0)
return this.sortedPhotoGroups[0].id
return -1
},
sortedPhotoGroups() {
return [...this.photoGroups].sort((a,b) => {
if (a.date === null) return 1
if (b.date === null) return -1
return a.date > b.date ? -1 : 1
})
}
},
methods: {
toggle_select_photo(photo_id) {
if (this.is_photo_selected(photo_id)) {
const index = this.selection.indexOf(photo_id)
this.selection.splice(index, 1)
} else {
if (this.selecions_left > 0) {
this.selection.push(photo_id)
} else {
this.message.error('You can only select ' + this.max_select + ' photo(s)')
}
}
},
is_photo_selected(photo_id) {
return this.selection.includes(photo_id)
}
}
}
</script>
<style scoped>
.n-image {
cursor: pointer;
}
.selected {
opacity: 0.5;
}
</style>

View File

@ -17,9 +17,16 @@ const routes = [
component: LogsList component: LogsList
}, },
{ {
path: '/logs/create', path: '/logs/create/:e?',
name: 'CreateLog', name: 'CreateLog',
component: CreateLog component: CreateLog,
props: (route) => {
const e = Number.parseInt(route.params.e)
if (Number.isNaN(e)) {
return ''
}
return {e}
}
} }
] ]

View File

@ -1,22 +1,142 @@
import { createStore } from 'vuex' import { createStore } from 'vuex'
import { getPhotoLogList } from '@/api' import {
getPhotoLogList,
deletePhotoLog,
addNewPhotoLog,
getPhotoLog,
updatePhotoLog,
getPhotoGroups,
getPhotosByGroup
} from '@/api'
export default createStore({ export default createStore({
state: { state: {
photoLogList: [] photoLogList: [],
photoLogs: [],
photoGroups: [],
photos: []
}, },
mutations: { mutations: {
SET_PHOTO_LOG_LIST(state, newPhotoLogList) { SET_PHOTO_LOG_LIST(state, newPhotoLogList) {
state.photoLogList = newPhotoLogList state.photoLogList = newPhotoLogList
},
SET_PHOTO_LOG(state, photoLog) {
let log_index = state.photoLogs.findIndex(log => log.id === photoLog.id)
if (log_index > -1) {
state.photoLogs[log_index] = photoLog
} else {
state.photoLogs.push(photoLog)
}
},
REMOVE_PHOTO_LOG(state, id) {
let log_index = state.photoLogList.findIndex(log => log.id === id)
state.photoLogList.splice(log_index, 1)
},
SET_PHOTO_GROUPS(state, groups) {
state.photoGroups = groups
},
SET_PHOTOS_IN_GROUP(state, {group_id, photos}) {
if (group_id in state.photos) {
state.photos.group_id = photos
} else {
state.photos = {...state.photos, [group_id]: photos}
}
} }
}, },
actions: { actions: {
loadPhotoLogList({commit}) { loadPhotoLogList({commit}) {
getPhotoLogList().then((response) => { return new Promise((resolve, reject) => {
commit('SET_PHOTO_LOG_LIST', response.data) getPhotoLogList().then((response) => {
commit('SET_PHOTO_LOG_LIST', response.data)
resolve()
}).catch((error) => {
console.log(error)
reject()
})
})
},
loadPhotoLog({commit}, id) {
return new Promise((resolve, reject) => {
getPhotoLog(id).then((response) => {
commit('SET_PHOTO_LOG', response.data)
resolve()
}).catch((error) => {
console.log(error)
reject()
})
})
},
deletePhotoLog({commit}, id) {
deletePhotoLog(id).then(() => {
commit('REMOVE_PHOTO_LOG', id)
}).catch((error) => { }).catch((error) => {
console.log(error) console.log(error)
}) })
},
addNewPhotoLog({dispatch}, {title, date}) {
return new Promise((resolve, reject) => {
addNewPhotoLog(title, date).then((response) => {
dispatch('loadPhotoLogList')
resolve(response.data.id)
}).catch((error) => {
console.log(error)
reject(error)
})
})
},
updatePhotoLogDetails({commit}, photolog) {
if (photolog !== null) {
commit('SET_PHOTO_LOG', photolog)
return updatePhotoLog(photolog)
}
},
loadPhotoGroups({commit}) {
return new Promise((resolve, reject) => {
getPhotoGroups().then((response) => {
commit('SET_PHOTO_GROUPS', response.data)
resolve(response.data)
}).catch((error) => {
reject(error)
})
})
},
loadPhotosInGroup({commit}, group_id) {
return new Promise((resolve, reject) => {
getPhotosByGroup(group_id).then((response) => {
commit('SET_PHOTOS_IN_GROUP', {group_id, photos: response.data})
resolve(response.data)
}).catch((error) => {
reject(error)
})
})
},
loadPhotosInAllGroups({dispatch, getters}) {
for (const index in getters.photoGroups) {
const group_id = getters.photoGroups[index].id
dispatch('loadPhotosInGroup', group_id)
}
}
},
getters: {
photoLogById: (state) => (id) => {
let log = state.photoLogList.filter(log => log.id == id)
if (log.length > 0) {
return log[0]
}
return null
},
photoLogDetailsById: (state) => (id) => {
let log = state.photoLogs.filter(log => log.id == id)
if (log.length > 0) {
return log[0]
}
return null
},
photoGroups (state) {
return state.photoGroups
},
photos (state) {
return state.photos
} }
}, },
modules: {}, modules: {},

View File

@ -1,14 +1,278 @@
<template> <template>
<h1>Create Log</h1> <div>
<n-space justify="space-between">
<h1>Create Log</h1>
<n-button type="primary">Generate</n-button>
</n-space>
{{start_slide_image}}
{{slides}}
<n-space justify="space-around">
<n-input size="large" round placeholder="Photo Log title" v-model:value="title" @change="updateServerData" />
<n-date-picker v-model:value="date" type="date" @update:value="updateServerData" />
</n-space>
<n-divider title-placement="center">Slides</n-divider>
<n-space vertical>
<n-card
style="width: 80%;"
v-for="(slide, index) in editSlides" :key="index"
:title="'#'+(index+1)" >
<n-image-group>
<n-space>
<n-image
v-for="(photo_id, photoIndex) in slide" :key="photoIndex"
width="100"
:src="getPhotoSrcById(photo_id)"
/>
<n-button round dashed type="info" @click="selectPhotosForSlide(index)"> Edit </n-button>
</n-space>
</n-image-group>
</n-card>
</n-space>
<PhotoSelectModal
v-model:showSelection="selectPhotosModal"
@closed="selectPhotosModal=false"
@selected="addPhotosToSlide"
:max_select=max_photos_per_slide :or_less=true
:already-selected=editSlides[selectedSlide] />
<n-modal v-model:show="showCreateModal" :mask-closable=false>
<n-spin :show="isCreateLoading">
<n-card
style="width: 600px;"
title="Create a new Photo Log"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<n-form-item label="What's the name of the photo log?" path="title">
<n-input v-model:value="title" type="text" placeholder="Photo Log title" />
</n-form-item>
<n-form-item label="When did the training take place?" path="date">
<n-date-picker v-model:value="date" type="date" clearable />
</n-form-item>
<template #footer>
<n-space justify="end">
<n-button @click="navigateBack">Cancel</n-button>
<n-button @click="createNewPhotoLog" type="success">Create</n-button>
</n-space>
</template>
</n-card>
</n-spin>
</n-modal>
</div>
</template> </template>
<script> <script>
import { useMeta } from 'vue-meta' import { useMeta } from 'vue-meta'
import { useMessage } from 'naive-ui'
import PhotoSelectModal from '@/components/PhotoSelectModal'
export default { export default {
name: 'CreateLog', name: 'CreateLog',
components: {
PhotoSelectModal,
},
props: {
e: {
type: Number,
required: false,
default: -1,
}
},
data() {
return {
id: Number.parseInt(this.e),
title: '',
date: null,
render_date: false,
start_slide_image: null,
slides: [],
selectPhotosModal: false,
selectedSlide: null,
max_photos_per_slide: 3,
isLoadingData: false,
isCreateLoading: false,
isSavingServer: false,
}
},
setup() { setup() {
useMeta({ title: 'Create a new Photo Log' }) useMeta({ title: 'Create a new Photo Log' })
const message = useMessage()
return { message }
}, },
mounted() {
if (this.isValidId()) {
this.isLoadingData = true
this.$store.dispatch('loadPhotoGroups').then(() => {
this.photoGroups = this.$store.getters.photoGroups
this.$store.dispatch('loadPhotosInAllGroups')
})
this.findPhotoLog().then(() => {
this.updateLocalData()
})
}
},
computed: {
editSlides() {
return [...this.slides, []]
},
photos() {
return this.$store.getters.photos
},
showCreateModal () {
return !this.isValidId()
},
dateStr () {
if (this.date !== null) {
let date_obj = new Date(this.date)
let month = Number.parseInt(date_obj.getMonth())+1
month = month < 10 ? '0' + month : month
let day = Number.parseInt(date_obj.getDate())
day = day < 10 ? '0' + day : day
let date_str = date_obj.getFullYear() + '-' + month + '-' + day
return date_str
}
return '1970-01-01'
}
},
methods: {
selectPhotosForSlide(slide_index) {
this.selectedSlide = slide_index
this.selectPhotosModal = true
},
getPhotoSrcById(photo_id){
for (const index in this.photos) {
let group = this.photos[index]
let photo = group.filter((photo) => photo.id == photo_id)
if (photo.length > 0) {
photo = photo[0]
return photo.cropped_image !== null ? photo.cropped_image : photo.original_image
}
}
return null
},
addPhotosToSlide(selected_photos) {
this.slides[this.selectedSlide] = selected_photos
this.updateServerData()
},
navigateBack() {
this.$router.go(-1)
},
createNewPhotoLog() {
if (this.title.length > 0 && this.date !== null) {
this.isCreateLoading = true
this.$store.dispatch('addNewPhotoLog', {title:this.title, date:this.dateStr}).then(id => {
if (id > -1 && id !== null) {
this.id = id
this.$router.push({name: 'CreateLog', params: {e: id}})
} else {
this.message.error('Something went wrong. Please try again later.')
}
}).finally(() => {
this.isCreateLoading = false
})
} else {
if (this.title.length <= 0)
this.message.error('Please enter a title')
if (this.date === null)
this.message.error('Please enter a date')
}
},
isValidId() {
return !(
this.id == null
|| Number.isNaN(this.id)
|| this.id <= 0
|| this.id === undefined
)
},
findPhotoLog() {
return new Promise((resolve, reject) => {
let found_log = this.$store.getters.photoLogById(this.id)
if (found_log === null) {
this.$store.dispatch('loadPhotoLogList').then(() => {
found_log = this.$store.getters.photoLogById(this.id)
if (found_log === null) {
this.message.error('Photo Log could not be found')
reject()
} else {
resolve()
}
}).catch(() => {
reject()
})
} else {
resolve()
}
})
},
updateLocalData() {
let found_log = null
this.$store.dispatch('loadPhotoLog', this.id).then(() => {
found_log = this.$store.getters.photoLogDetailsById(this.id)
if (found_log !== null) {
this.title = found_log.title
this.date = new Date(found_log.date).getTime()
this.render_date = found_log.render_date
this.start_slide_image = found_log.start_slide_image
this.slides = found_log.slides
this.isLoadingData = false
} else {
this.message.error('Photo Log could not be found')
}
})
},
updateServerData() {
this.isSavingServer = true
this.$store.dispatch('updatePhotoLogDetails', {
id: this.id,
title: this.title,
date: this.dateStr,
render_date: this.render_date,
start_slide_image: this.start_slide_image,
slides: this.slides
}).then(() => {
this.message.success('Changes saved')
}).finally(() => {
this.isSavingServer = false
})
}
},
beforeRouteUpdate (to) {
this.id = Number.parseInt(to.params.e)
if (this.isValidId()) {
this.isLoadingData = true
this.findPhotoLog().then(()=> {
this.updateLocalData()
})
}
},
// eslint-disable-next-line no-unused-vars
beforeRouteLeave (to, from) {
// prevent from leaving with unsaved changes
}
} }
</script> </script>
<style scoped>
.n-input {
min-width: 30em;
}
</style>

View File

@ -9,7 +9,9 @@
</thead> </thead>
<tbody> <tbody>
<tr v-for="photolog in photoLogList" :key="photolog.id"> <tr v-for="photolog in photoLogList" :key="photolog.id">
<td>{{ photolog.title }}</td> <router-link :to="{name: 'CreateLog', params: {e: photolog.id}}">
<td>{{ photolog.title }}</td>
</router-link>
<td>{{ photolog.date }}</td> <td>{{ photolog.date }}</td>
<td> <td>
<n-button type="error" @click="askDeleteLog(photolog.id)">Delete</n-button> <n-button type="error" @click="askDeleteLog(photolog.id)">Delete</n-button>
@ -72,7 +74,10 @@ export default {
this.deleteModalContent = 'Do you want to permanently delete "' + logtitle + '"?' this.deleteModalContent = 'Do you want to permanently delete "' + logtitle + '"?'
}, },
deleteLog() { deleteLog() {
console.log('delete ' + this.deleteId) if (this.deleteId !== null) {
this.$store.dispatch('deletePhotoLog', this.deleteId)
this.deleteId = null
}
} }
} }
} }