move components where they belong
This commit is contained in:
45
components/Completed.vue
Normal file
45
components/Completed.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template lang="pug">
|
||||
v-container
|
||||
v-card-title.d-block.text-h5.text-center(v-text="$t('setup.completed')")
|
||||
v-card-text(v-html="$t('setup.completed_description', user)")
|
||||
v-alert.mb-3.mt-1(v-if='isHttp' outlined type='warning' color='red' show-icon :icon='mdiAlert') {{$t('setup.https_warning')}}
|
||||
v-alert.mb-3.mt-1(outlined type='warning' color='red' show-icon :icon='mdiAlert') {{$t('setup.copy_password_dialog')}}
|
||||
v-card-actions
|
||||
v-btn(text @click='next' color='primary' :loading='loading' :disabled='loading') {{$t('setup.start')}}
|
||||
v-icon(v-text='mdiArrowRight')
|
||||
</template>
|
||||
<script>
|
||||
import { mdiArrowRight, mdiAlert } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
isHttp: { type: Boolean, default: false },
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
mdiArrowRight, mdiAlert,
|
||||
loading: false,
|
||||
user: {
|
||||
email: 'admin',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
next () {
|
||||
window.location='/admin'
|
||||
},
|
||||
async start (user) {
|
||||
this.user = { ...user }
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
await this.$axios.$get('/ping')
|
||||
this.loading = false
|
||||
} catch (e) {
|
||||
setTimeout(() => this.start(user), 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
256
components/DateInput.vue
Normal file
256
components/DateInput.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<template lang="pug">
|
||||
v-col(cols=12)
|
||||
.text-center
|
||||
v-btn-toggle.v-col-6.flex-column.flex-sm-row(v-model='type' color='primary' @change='type => change("type", type)')
|
||||
v-btn(value='normal' label="normal") {{$t('event.normal')}}
|
||||
v-btn(value='multidate' label='multidate') {{$t('event.multidate')}}
|
||||
v-btn(v-if='settings.allow_recurrent_event' value='recurrent' label="recurrent") {{$t('event.recurrent')}}
|
||||
|
||||
p {{$t(`event.${type}_description`)}}
|
||||
|
||||
v-btn-toggle.v-col-6.flex-column.flex-sm-row(v-if='type === "recurrent"' color='primary' :value='value.recurrent.frequency' @change='fq => change("frequency", fq)')
|
||||
v-btn(v-for='f in frequencies' :key='f.value' :value='f.value') {{f.text}}
|
||||
|
||||
client-only
|
||||
.datePicker.mt-3
|
||||
v-input(:value='fromDate'
|
||||
:rules="[$validators.required('common.when')]")
|
||||
vc-date-picker(
|
||||
:value='fromDate'
|
||||
@input="date => change('date', date)"
|
||||
:is-range='type === "multidate"'
|
||||
:attributes='attributes'
|
||||
:locale='$i18n.locale'
|
||||
:from-page.sync='page'
|
||||
:is-dark="settings['theme.is_dark']"
|
||||
is-inline
|
||||
is-expanded
|
||||
:min-date='type !== "recurrent" && new Date()')
|
||||
|
||||
div.text-center.mb-2(v-if='type === "recurrent"')
|
||||
span(v-if='value.recurrent.frequency !== "1m" && value.recurrent.frequency !== "2m"') {{whenPatterns}}
|
||||
v-btn-toggle.mt-1.flex-column.flex-sm-row(v-else :value='value.recurrent.type' color='primary' @change='fq => change("recurrentType", fq)')
|
||||
v-btn(v-for='whenPattern in whenPatterns' :value='whenPattern.key' :key='whenPatterns.key' small) {{whenPattern.label}}
|
||||
|
||||
v-row.mt-3.col-md-6.mx-auto
|
||||
v-col.col-12.col-sm-6
|
||||
v-select(dense :label="$t('event.from')" :value='fromHour' clearable
|
||||
:disabled='!value.from'
|
||||
:rules="[$validators.required('event.from')]"
|
||||
:items='hourList' @change='hr => change("fromHour", hr)')
|
||||
|
||||
v-col.col-12.col-sm-6
|
||||
v-select(dense :label="$t('event.due')"
|
||||
:disabled='!fromHour'
|
||||
:value='dueHour' clearable
|
||||
:items='hourList' @change='hr => change("dueHour", hr)')
|
||||
|
||||
List(v-if='type==="normal" && todayEvents.length' :events='todayEvents' :title='$t("event.same_day")')
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import dayjs from 'dayjs'
|
||||
import { mapState } from 'vuex'
|
||||
import List from '@/components/List'
|
||||
import { attributesFromEvents } from '../assets/helper'
|
||||
|
||||
export default {
|
||||
name: 'DateInput',
|
||||
components: { List },
|
||||
props: {
|
||||
value: { type: Object, default: () => ({ from: null, due: null, recurrent: null }) },
|
||||
event: { type: Object, default: () => null }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
type: 'normal',
|
||||
page: null,
|
||||
events: [],
|
||||
frequencies: [
|
||||
{ value: '1w', text: this.$t('event.each_week') },
|
||||
{ value: '2w', text: this.$t('event.each_2w') },
|
||||
{ value: '1m', text: this.$t('event.each_month') }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['settings', 'tags']),
|
||||
todayEvents () {
|
||||
const start = dayjs(this.value.from).startOf('day').unix()
|
||||
const end = dayjs(this.value.from).endOf('day').unix()
|
||||
return this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end)
|
||||
},
|
||||
attributes () {
|
||||
return attributesFromEvents(this.events, this.tags)
|
||||
},
|
||||
fromDate () {
|
||||
if (this.value.multidate) {
|
||||
return ({ start: dayjs(this.value.from).toDate(), end: dayjs(this.value.due).toDate() })
|
||||
}
|
||||
return this.value.from ? dayjs(this.value.from).toDate() : null
|
||||
},
|
||||
|
||||
fromHour () {
|
||||
return this.value.from && this.value.fromHour ? dayjs.tz(this.value.from).format('HH:mm') : null
|
||||
},
|
||||
dueHour () {
|
||||
return this.value.due && this.value.dueHour ? dayjs.tz(this.value.due).format('HH:mm') : null
|
||||
},
|
||||
hourList () {
|
||||
const hourList = []
|
||||
const leftPad = h => ('00' + h).slice(-2)
|
||||
for (let h = 0; h < 24; h++) {
|
||||
const textHour = leftPad(h < 13 ? h : h - 12)
|
||||
hourList.push({ text: textHour + ':00 ' + (h <= 12 ? 'AM' : 'PM'), value: leftPad(h) + ':00' })
|
||||
hourList.push({ text: textHour + ':30 ' + (h <= 12 ? 'AM' : 'PM'), value: leftPad(h) + ':30' })
|
||||
}
|
||||
|
||||
return hourList
|
||||
},
|
||||
whenPatterns () {
|
||||
if (!this.value.from) { return }
|
||||
const date = dayjs(this.value.from)
|
||||
|
||||
const freq = this.value.recurrent.frequency
|
||||
const weekDay = date.format('dddd')
|
||||
if (freq === '1w' || freq === '2w') {
|
||||
return this.$t(`event.recurrent_${freq}_days`, { days: weekDay }).toUpperCase()
|
||||
} else if (freq === '1m' || freq === '2m') {
|
||||
const monthDay = date.format('D')
|
||||
const n = Math.floor((monthDay - 1) / 7) + 1
|
||||
|
||||
const patterns = [
|
||||
{ label: this.$t(`event.recurrent_${freq}_days`, { days: monthDay }), key: 'ordinal' }
|
||||
// { label: this.$tc(`event.recurrent_${freq}_ordinal`, { n, days: weekDay }), key: 'weekday' }
|
||||
]
|
||||
|
||||
if (n < 5) {
|
||||
patterns.push(
|
||||
{
|
||||
label: this.$t(`event.recurrent_${freq}_ordinal`, { n: this.$t(`ordinal.${n}`), days: weekDay }),
|
||||
key: n
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// if selected day is in last week, propose also this type of selection
|
||||
const lastWeek = date.daysInMonth() - monthDay < 7
|
||||
if (lastWeek) {
|
||||
patterns.push(
|
||||
{
|
||||
label: this.$t(`event.recurrent_${freq}_ordinal`, { n: this.$t('ordinal.-1'), days: weekDay }),
|
||||
key: -1
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return patterns
|
||||
} else if (freq === '1d') {
|
||||
return this.$t('event.recurrent_each_day')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
if (this.value.multidate) {
|
||||
this.type = 'multidate'
|
||||
} else if (this.value.recurrent) {
|
||||
this.type = 'recurrent'
|
||||
} else {
|
||||
this.type = 'normal'
|
||||
}
|
||||
this.events = await this.$api.getEvents({
|
||||
start: dayjs().unix(),
|
||||
show_recurrent: true
|
||||
})
|
||||
this.events = this.events.filter(e => e.id !== this.event.id)
|
||||
},
|
||||
methods: {
|
||||
updateRecurrent (value) {
|
||||
this.$emit('input', { ...this.value, recurrent: value || null })
|
||||
},
|
||||
change (what, value) {
|
||||
// change event's type
|
||||
if (what === 'type') {
|
||||
if (typeof value === 'undefined') { this.type = 'normal' }
|
||||
if (value === 'recurrent') {
|
||||
this.$emit('input', { ...this.value, recurrent: { frequency: '1w' }, multidate: false })
|
||||
} else if (value === 'multidate') {
|
||||
this.$emit('input', { ...this.value, recurrent: null, multidate: true })
|
||||
} else {
|
||||
let from = this.value.from
|
||||
if (from && from.start) {
|
||||
from = from.start
|
||||
}
|
||||
let due = this.value.due
|
||||
if (due && due.start) {
|
||||
due = due.start
|
||||
}
|
||||
this.$emit('input', { ...this.value, from, due, recurrent: null, multidate: false })
|
||||
}
|
||||
} else if (what === 'frequency') {
|
||||
this.$emit('input', { ...this.value, recurrent: { ...this.value.recurrent, frequency: value } })
|
||||
} else if (what === 'recurrentType') {
|
||||
this.$emit('input', { ...this.value, recurrent: { ...this.value.recurrent, type: value } })
|
||||
} else if (what === 'fromHour') {
|
||||
if (value) {
|
||||
const [hour, minute] = value.split(':')
|
||||
const from = dayjs.tz(this.value.from).hour(hour).minute(minute).second(0)
|
||||
this.$emit('input', { ...this.value, from, fromHour: true })
|
||||
} else {
|
||||
this.$emit('input', { ...this.value, fromHour: false })
|
||||
}
|
||||
} else if (what === 'dueHour') {
|
||||
if (value) {
|
||||
const [hour, minute] = value.split(':')
|
||||
const fromHour = dayjs.tz(this.value.from).hour()
|
||||
|
||||
// add a day
|
||||
let due = dayjs(this.value.from)
|
||||
if (fromHour > Number(hour) && !this.value.multidate) {
|
||||
due = due.add(1, 'day')
|
||||
}
|
||||
due = due.hour(hour).minute(minute).second(0)
|
||||
this.$emit('input', { ...this.value, due, dueHour: true })
|
||||
} else {
|
||||
this.$emit('input', { ...this.value, due: null, dueHour: false })
|
||||
}
|
||||
// change date in calendar (could be a range or a recurrent event...)
|
||||
} else if (what === 'date') {
|
||||
if (value === null) {
|
||||
this.$emit('input', { ...this.value, from: null, fromHour: false })
|
||||
return
|
||||
}
|
||||
if (this.value.multidate) {
|
||||
let from = value.start
|
||||
let due = value.end
|
||||
if (this.value.fromHour) {
|
||||
from = dayjs.tz(value.start).hour(dayjs.tz(this.value.from).hour())
|
||||
}
|
||||
if (this.value.dueHour) {
|
||||
due = dayjs.tz(value.end).hour(dayjs.tz(this.value.due).hour())
|
||||
}
|
||||
this.$emit('input', { ...this.value, from, due })
|
||||
} else {
|
||||
let from = value
|
||||
let due = this.value.due
|
||||
if (this.value.fromHour) {
|
||||
from = dayjs.tz(value).hour(dayjs.tz(this.value.from).hour())
|
||||
}
|
||||
if (this.value.dueHour && this.value.due) {
|
||||
due = dayjs.tz(value).hour(dayjs.tz(this.value.due).hour())
|
||||
}
|
||||
this.$emit('input', { ...this.value, from, due })
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.datePicker {
|
||||
max-width: 500px !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
52
components/DbStep.vue
Normal file
52
components/DbStep.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template lang="pug">
|
||||
v-container
|
||||
v-card-title.text-h5 Database
|
||||
v-card-text
|
||||
v-form
|
||||
v-btn-toggle(text color='primary' v-model='db.dialect')
|
||||
v-btn(value='sqlite' text) sqlite
|
||||
v-btn(value='postgres' text) postgres
|
||||
v-btn(value='mariadb' text) mariadb
|
||||
template(v-if='db.dialect === "sqlite"')
|
||||
v-text-field(v-model='db.storage' label='Path')
|
||||
template(v-if='db.dialect !== "sqlite"')
|
||||
v-text-field(v-model='db.host' label='Hostname' :rules="[$validators.required('hostname')]")
|
||||
v-text-field(v-model='db.database' label='Database' :rules="[$validators.required('database')]")
|
||||
v-text-field(v-model='db.username' label='Username' :rules="[$validators.required('username')]")
|
||||
v-text-field(type='password' v-model='db.password' label='Password' :rules="[$validators.required('password')]")
|
||||
|
||||
v-card-actions
|
||||
v-btn(text @click='checkDb' color='primary' :loading='loading' :disabled='loading') {{$t('common.next')}}
|
||||
v-icon(v-text='mdiArrowRight')
|
||||
</template>
|
||||
<script>
|
||||
import { mdiArrowRight } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
mdiArrowRight,
|
||||
db: {
|
||||
dialect: 'sqlite',
|
||||
storage: './gancio.sqlite',
|
||||
host: 'localhost',
|
||||
database: 'gancio'
|
||||
},
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async checkDb () {
|
||||
this.loading = true
|
||||
try {
|
||||
await this.$axios.$post('/setup/db', { db: this.db })
|
||||
this.$root.$message('DB Connection OK!', { color: 'success' })
|
||||
this.$emit('complete', this.db)
|
||||
} catch (e) {
|
||||
this.$root.$message(e.response.data, { color: 'error' })
|
||||
}
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
103
components/ImportDialog.vue
Normal file
103
components/ImportDialog.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template lang="pug">
|
||||
v-card
|
||||
v-card-title {{$t('common.import')}}
|
||||
v-card-text
|
||||
p(v-html="$t('event.import_description')")
|
||||
v-form(v-model='valid' ref='form' lazy-validation @submit.prevent='importGeneric')
|
||||
v-row
|
||||
.col-xl-5.col-lg-5.col-md-7.col-sm-12.col-xs-12
|
||||
v-text-field(v-model='URL'
|
||||
:label="$t('common.url')"
|
||||
:hint="$t('event.import_URL')"
|
||||
persistent-hint
|
||||
:loading='loading' :error='error'
|
||||
:error-messages='errorMessage')
|
||||
.col
|
||||
v-file-input(
|
||||
v-model='file'
|
||||
accept=".ics"
|
||||
:label="$t('event.ics')"
|
||||
:hint="$t('event.import_ICS')"
|
||||
persistent-hint)
|
||||
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(text @click='$emit("close")' color='warning') {{$t('common.cancel')}}
|
||||
v-btn(text @click='importGeneric' :loading='loading' :disabled='loading'
|
||||
color='primary') {{$t('common.import')}}
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import ical from 'ical.js'
|
||||
import get from 'lodash/get'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'ImportDialog',
|
||||
data () {
|
||||
return {
|
||||
file: null,
|
||||
errorMessage: '',
|
||||
error: false,
|
||||
loading: false,
|
||||
valid: false,
|
||||
URL: '',
|
||||
event: {}
|
||||
}
|
||||
},
|
||||
computed: mapState(['places']),
|
||||
methods: {
|
||||
importGeneric () {
|
||||
if (this.file) {
|
||||
this.importICS()
|
||||
} else {
|
||||
this.importURL()
|
||||
}
|
||||
},
|
||||
importICS () {
|
||||
const reader = new FileReader()
|
||||
reader.readAsText(this.file)
|
||||
reader.onload = () => {
|
||||
const ret = ical.parse(reader.result)
|
||||
const component = new ical.Component(ret)
|
||||
const events = component.getAllSubcomponents('vevent')
|
||||
const event = new ical.Event(events[0])
|
||||
this.event = {
|
||||
title: get(event, 'summary', ''),
|
||||
description: get(event, 'description', ''),
|
||||
place: { name: get(event, 'location', '') },
|
||||
start_datetime: get(event, 'startDate', '').toUnixTime(),
|
||||
end_datetime: get(event, 'endDate', '').toUnixTime()
|
||||
}
|
||||
|
||||
this.$emit('imported', this.event)
|
||||
}
|
||||
},
|
||||
async importURL () {
|
||||
if (!this.URL) {
|
||||
this.errorMessage = this.$validators.required('common.url')('')
|
||||
this.error = true
|
||||
return
|
||||
}
|
||||
if (!this.URL.match(/^https?:\/\//)) {
|
||||
this.URL = `https://${this.URL}`
|
||||
}
|
||||
this.error = false
|
||||
this.errorMessage = ''
|
||||
this.loading = true
|
||||
|
||||
try {
|
||||
const ret = await this.$axios.$get('/event/import', { params: { URL: this.URL } })
|
||||
this.events = ret
|
||||
// check if contain an h-event
|
||||
this.$emit('imported', ret[0])
|
||||
} catch (e) {
|
||||
this.error = true
|
||||
this.errorMessage = String(e)
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
177
components/MediaInput.vue
Normal file
177
components/MediaInput.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template lang="pug">
|
||||
span
|
||||
v-dialog(v-model='openMediaDetails' :fullscreen="$vuetify.breakpoint.xsOnly" width='1000px')
|
||||
v-card
|
||||
v-card-title {{$t('common.media')}}
|
||||
v-card-text
|
||||
v-row.mt-1
|
||||
v-col#focalPointSelector(
|
||||
@mousedown='handleStart' @touchstart='handleStart'
|
||||
@mousemove='handleMove' @touchmove='handleMove'
|
||||
@mouseup='handleStop' @touchend='handleStop'
|
||||
)
|
||||
div.focalPoint(:style="{ top, left }")
|
||||
img(v-if='mediaPreview' :src='mediaPreview')
|
||||
|
||||
v-col.col-12.col-sm-4
|
||||
p {{$t('event.choose_focal_point')}}
|
||||
img.mediaPreview.d-none.d-sm-block(v-if='mediaPreview'
|
||||
:src='mediaPreview' :style="{ 'object-position': position }")
|
||||
|
||||
v-textarea.mt-4(type='text'
|
||||
label='Alternative text'
|
||||
persistent-hint
|
||||
@input='v => name=v'
|
||||
:value='value.name' filled
|
||||
:hint='$t("event.alt_text_description")')
|
||||
br
|
||||
v-card-actions.justify-space-between
|
||||
v-btn(text @click='openMediaDetails=false' color='warning') Cancel
|
||||
v-btn(text color='primary' @click='save') Save
|
||||
|
||||
h3.mb-3.font-weight-regular(v-if='mediaPreview') {{$t('common.media')}}
|
||||
v-card-actions(v-if='mediaPreview')
|
||||
v-spacer
|
||||
v-btn(text color='primary' @click='openMediaDetails = true') {{$t('common.edit')}}
|
||||
v-btn(text color='error' @click='remove') {{$t('common.remove')}}
|
||||
div(v-if='mediaPreview')
|
||||
img.mediaPreview.col-12.ml-3(:src='mediaPreview' :style="{ 'object-position': savedPosition }")
|
||||
span.float-right {{event.media[0].name}}
|
||||
v-file-input(
|
||||
v-else
|
||||
:label="$t('common.media')"
|
||||
:hint="$t('event.media_description')"
|
||||
:prepend-icon="mdiCamera"
|
||||
:value='value.image'
|
||||
@change="selectMedia"
|
||||
persistent-hint
|
||||
accept='image/*')
|
||||
</template>
|
||||
<script>
|
||||
import { mdiCamera } from '@mdi/js'
|
||||
export default {
|
||||
name: 'MediaInput',
|
||||
props: {
|
||||
value: { type: Object, default: () => ({ image: null }) },
|
||||
event: { type: Object, default: () => ({}) }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
mdiCamera,
|
||||
openMediaDetails: false,
|
||||
name: this.value.name || '',
|
||||
focalpoint: this.value.focalpoint || [0, 0],
|
||||
dragging: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
mediaPreview () {
|
||||
if (!this.value.url && !this.value.image) {
|
||||
return false
|
||||
}
|
||||
const url = this.value.image ? URL.createObjectURL(this.value.image) : /^https?:\/\//.test(this.value.url) ? this.value.url : `/media/thumb/${this.value.url}`
|
||||
return url
|
||||
},
|
||||
top () {
|
||||
return ((this.focalpoint[1] + 1) * 50) + '%'
|
||||
},
|
||||
left () {
|
||||
return ((this.focalpoint[0] + 1) * 50) + '%'
|
||||
},
|
||||
savedPosition () {
|
||||
const focalpoint = this.value.focalpoint || [0, 0]
|
||||
return `${(focalpoint[0] + 1) * 50}% ${(focalpoint[1] + 1) * 50}%`
|
||||
},
|
||||
position () {
|
||||
const focalpoint = this.focalpoint || [0, 0]
|
||||
return `${(focalpoint[0] + 1) * 50}% ${(focalpoint[1] + 1) * 50}%`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save () {
|
||||
this.$emit('input', { url: this.value.url, image: this.value.image, name: this.name || (this.event.title) || '', focalpoint: [...this.focalpoint] })
|
||||
this.openMediaDetails = false
|
||||
},
|
||||
async remove () {
|
||||
const ret = await this.$root.$confirm('event.remove_media_confirmation')
|
||||
if (!ret) { return }
|
||||
this.$emit('remove')
|
||||
},
|
||||
selectMedia (v) {
|
||||
this.$emit('input', { image: v, name: this.event.title, focalpoint: [0, 0] })
|
||||
},
|
||||
handleStart (ev) {
|
||||
ev.preventDefault()
|
||||
this.dragging = true
|
||||
this.handleMove(ev, true)
|
||||
return false
|
||||
},
|
||||
handleStop (ev) {
|
||||
this.dragging = false
|
||||
},
|
||||
handleMove (ev, manual = false) {
|
||||
if (!this.dragging && !manual) return
|
||||
ev.stopPropagation()
|
||||
const boundingClientRect = document.getElementById('focalPointSelector').getBoundingClientRect()
|
||||
|
||||
const clientX = ev.changedTouches ? ev.changedTouches[0].clientX : ev.clientX
|
||||
const clientY = ev.changedTouches ? ev.changedTouches[0].clientY : ev.clientY
|
||||
|
||||
// get relative coordinate
|
||||
let x = Math.ceil(clientX - boundingClientRect.left)
|
||||
let y = Math.ceil(clientY - boundingClientRect.top)
|
||||
|
||||
// snap to border
|
||||
x = x < 30 ? 0 : x > boundingClientRect.width - 30 ? boundingClientRect.width : x
|
||||
y = y < 30 ? 0 : y > boundingClientRect.height - 30 ? boundingClientRect.height : y
|
||||
|
||||
// this.relativeFocalpoint = [x + 'px', y + 'px']
|
||||
|
||||
// map to real image coordinate
|
||||
const posY = -1 + (y / boundingClientRect.height) * 2
|
||||
const posX = -1 + (x / boundingClientRect.width) * 2
|
||||
|
||||
this.focalpoint = [posX, posY]
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.cursorPointer {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.mediaPreview {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
aspect-ratio: 1.7778;
|
||||
}
|
||||
|
||||
#focalPointSelector {
|
||||
position: relative;
|
||||
cursor: move;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-self: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#focalPointSelector img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.focalPoint {
|
||||
position: absolute;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
top: 100px;
|
||||
left: 100px;
|
||||
transform: translate(-25px, -25px);
|
||||
border-radius: 50%;
|
||||
border: 1px solid #ff6d408e;
|
||||
box-shadow: 0 0 0 9999em rgba(0, 0, 0, .65);
|
||||
}
|
||||
</style>
|
||||
106
components/WhereInput.vue
Normal file
106
components/WhereInput.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template lang="pug">
|
||||
v-row
|
||||
v-col(cols=12 md=6)
|
||||
v-combobox(ref='place'
|
||||
:rules="[$validators.required('common.where')]"
|
||||
:label="$t('common.where')"
|
||||
:hint="$t('event.where_description')"
|
||||
:search-input.sync="placeName"
|
||||
:prepend-icon='mdiMapMarker'
|
||||
persistent-hint
|
||||
:value="value.name"
|
||||
:items="filteredPlaces"
|
||||
no-filter
|
||||
item-text='name'
|
||||
@change='selectPlace')
|
||||
template(v-slot:item="{ item, attrs, on }")
|
||||
v-list-item(v-bind='attrs' v-on='on')
|
||||
v-list-item-content(two-line v-if='item.create')
|
||||
v-list-item-title <v-icon color='primary' v-text='mdiPlus' :aria-label='$t("common.add")'></v-icon> {{item.name}}
|
||||
v-list-item-content(two-line v-else)
|
||||
v-list-item-title(v-text='item.name')
|
||||
v-list-item-subtitle(v-text='item.address')
|
||||
|
||||
v-col(cols=12 md=6)
|
||||
v-text-field(ref='address'
|
||||
:prepend-icon='mdiMap'
|
||||
:disabled='disableAddress'
|
||||
:rules="[ v => disableAddress ? true : $validators.required('common.address')(v)]"
|
||||
:label="$t('common.address')"
|
||||
@change="changeAddress"
|
||||
:value="value.address")
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { mdiMap, mdiMapMarker, mdiPlus } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
name: 'WhereInput',
|
||||
props: {
|
||||
value: { type: Object, default: () => {} }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
mdiMap, mdiMapMarker, mdiPlus,
|
||||
place: { },
|
||||
placeName: '',
|
||||
disableAddress: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['places']),
|
||||
filteredPlaces () {
|
||||
if (!this.placeName) { return this.places }
|
||||
const placeName = this.placeName.trim().toLowerCase()
|
||||
let nameMatch = false
|
||||
const matches = this.places.filter(p => {
|
||||
const tmpName = p.name.toLowerCase()
|
||||
const tmpAddress = p.address.toLowerCase()
|
||||
if (tmpName.includes(placeName)) {
|
||||
if (tmpName === placeName) { nameMatch = true }
|
||||
return true
|
||||
}
|
||||
return tmpAddress.includes(placeName)
|
||||
})
|
||||
if (!nameMatch) {
|
||||
matches.unshift({ create: true, name: this.placeName })
|
||||
}
|
||||
return matches
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectPlace (p) {
|
||||
if (!p) { return }
|
||||
if (typeof p === 'object' && !p.create) {
|
||||
this.place.name = p.name.trim()
|
||||
this.place.address = p.address
|
||||
this.place.id = p.id
|
||||
this.disableAddress = true
|
||||
} else { // this is a new place
|
||||
this.place.name = p.name || p
|
||||
const tmpPlace = this.place.name.trim().toLowerCase()
|
||||
// search for a place with the same name
|
||||
const place = this.places.find(p => p.name.toLowerCase() === tmpPlace)
|
||||
if (place) {
|
||||
this.place.name = place.name
|
||||
this.place.id = place.id
|
||||
this.place.address = place.address
|
||||
this.disableAddress = true
|
||||
} else {
|
||||
delete this.place.id
|
||||
this.place.address = ''
|
||||
this.disableAddress = false
|
||||
this.$refs.place.blur()
|
||||
this.$refs.address.focus()
|
||||
}
|
||||
}
|
||||
this.$emit('input', { ...this.place })
|
||||
},
|
||||
changeAddress (v) {
|
||||
this.place.address = v
|
||||
this.$emit('input', { ...this.place })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
38
components/embedEvent.vue
Normal file
38
components/embedEvent.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template lang='pug'>
|
||||
v-card
|
||||
v-card-title(v-text="$t('common.embed_title')")
|
||||
v-card-text
|
||||
v-alert.mb-3.mt-1(type='info' show-icon :icon='mdiInformation') {{$t('common.embed_help')}}
|
||||
v-alert.pa-5.my-4.blue-grey.darken-4.text-body-1.lime--text.text--lighten-3 <pre>{{code}}</pre>
|
||||
v-btn.float-end(text color='primary' @click='clipboard(code)') {{$t("common.copy")}}
|
||||
v-icon.ml-1(v-text='mdiContentCopy')
|
||||
p.mx-auto
|
||||
.mx-auto
|
||||
gancio-event(:id='event.id' :baseurl='settings.baseurl')
|
||||
v-card-actions
|
||||
v-spacer
|
||||
v-btn(text color='warning' @click="$emit('close')") {{$t("common.cancel")}}
|
||||
v-btn(text @click='clipboard(code)' color="primary") {{$t("common.copy")}}
|
||||
</template>
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import clipboard from '../assets/clipboard'
|
||||
import { mdiContentCopy, mdiInformation } from '@mdi/js'
|
||||
|
||||
export default {
|
||||
name: 'EmbedEvent',
|
||||
data() {
|
||||
return { mdiContentCopy, mdiInformation }
|
||||
},
|
||||
mixins: [clipboard],
|
||||
props: {
|
||||
event: { type: Object, default: () => ({}) }
|
||||
},
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
code () {
|
||||
return `<script src='${this.settings.baseurl}\/gancio-events.es.js'><\/script>\n<gancio-event baseurl='${this.settings.baseurl}' id=${this.event.id}></gancio-event>\n\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
54
components/eventAdmin.vue
Normal file
54
components/eventAdmin.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template lang='pug'>
|
||||
div
|
||||
v-btn(text color='primary' v-if='event.is_visible' @click='toggle(false)') {{$t(`common.${event.parentId?'skip':'hide'}`)}}
|
||||
v-btn(text color='success' v-else @click='toggle(false)') <v-icon color='yellow' v-text='mdiAlert'></v-icon> {{$t('common.confirm')}}
|
||||
v-btn(text color='primary' @click='$router.push(`/add/${event.id}`)') {{$t('common.edit')}}
|
||||
v-btn(text color='primary' v-if='!event.parentId' @click='remove(false)') {{$t('common.remove')}}
|
||||
|
||||
template(v-if='event.parentId')
|
||||
v-divider
|
||||
span.mr-1 <v-icon v-text='mdiRepeat'></v-icon> {{$t('event.edit_recurrent')}}
|
||||
v-btn(text color='primary' v-if='event.parent.is_visible' @click='toggle(true)') {{$t('common.pause')}}
|
||||
v-btn(text color='primary' v-else @click='toggle(true)') {{$t('common.start')}}
|
||||
v-btn(text color='primary' @click='$router.push(`/add/${event.parentId}`)') {{$t('common.edit')}}
|
||||
v-btn(text color='primary' @click='remove(true)') {{$t('common.remove')}}
|
||||
</template>
|
||||
<script>
|
||||
import { mdiAlert, mdiRepeat } from '@mdi/js'
|
||||
export default {
|
||||
name: 'EventAdmin',
|
||||
data () {
|
||||
return { mdiAlert, mdiRepeat }
|
||||
},
|
||||
props: {
|
||||
event: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async remove (parent = false) {
|
||||
const ret = await this.$root.$confirm(`event.remove_${parent ? 'recurrent_' : ''}confirmation`)
|
||||
if (!ret) { return }
|
||||
const id = parent ? this.event.parentId : this.event.id
|
||||
await this.$axios.delete(`/event/${id}`)
|
||||
this.$router.replace('/')
|
||||
},
|
||||
async toggle (parent = false) {
|
||||
const id = parent ? this.event.parentId : this.event.id
|
||||
const is_visible = parent ? this.event.parent.is_visible : this.event.is_visible
|
||||
const method = is_visible ? 'unconfirm' : 'confirm'
|
||||
try {
|
||||
await this.$axios.$put(`/event/${method}/${id}`)
|
||||
if (parent) {
|
||||
this.event.parent.is_visible = !is_visible
|
||||
} else {
|
||||
this.event.is_visible = !is_visible
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user