Major new version #2
@ -1,12 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
day: Object,
|
day: Object,
|
||||||
|
dragging: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="cell"
|
class="cell"
|
||||||
|
:style="props.dragging ? 'touch-action:none;' : 'touch-action:pan-y;'"
|
||||||
:class="[
|
:class="[
|
||||||
props.day.monthClass,
|
props.day.monthClass,
|
||||||
{
|
{
|
||||||
@ -37,7 +39,6 @@ const props = defineProps({
|
|||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
touch-action: none;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
@ -47,10 +47,33 @@ const selection = ref({ startDate: null, dayCount: 0 })
|
|||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const dragAnchor = ref(null)
|
const dragAnchor = ref(null)
|
||||||
|
|
||||||
// Double-tap state
|
const DOUBLE_TAP_DELAY = 300
|
||||||
const lastTapTime = ref(0)
|
const pendingTap = ref({ date: null, time: 0, type: null })
|
||||||
const lastTapDate = ref(null)
|
const suppressMouseUntil = ref(0)
|
||||||
const DOUBLE_TAP_DELAY = 300 // milliseconds
|
|
||||||
|
function normalizeDate(val) {
|
||||||
|
if (typeof val === 'string') return val
|
||||||
|
if (val && typeof val === 'object') {
|
||||||
|
if (val.date) return String(val.date)
|
||||||
|
if (val.startDate) return String(val.startDate)
|
||||||
|
}
|
||||||
|
return String(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerTap(rawDate, type) {
|
||||||
|
const dateStr = normalizeDate(rawDate)
|
||||||
|
const now = Date.now()
|
||||||
|
const prev = pendingTap.value
|
||||||
|
const delta = now - prev.time
|
||||||
|
const isDouble =
|
||||||
|
prev.date === dateStr && prev.type === type && delta <= DOUBLE_TAP_DELAY && delta >= 35
|
||||||
|
if (isDouble) {
|
||||||
|
pendingTap.value = { date: null, time: 0, type: null }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
pendingTap.value = { date: dateStr, time: now, type }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const minVirtualWeek = computed(() => {
|
const minVirtualWeek = computed(() => {
|
||||||
const date = new Date(calendarStore.minYear, 0, 1)
|
const date = new Date(calendarStore.minYear, 0, 1)
|
||||||
@ -153,29 +176,24 @@ function createWeek(virtualWeek) {
|
|||||||
const dateStr = toLocalString(cur, DEFAULT_TZ)
|
const dateStr = toLocalString(cur, DEFAULT_TZ)
|
||||||
const storedEvents = []
|
const storedEvents = []
|
||||||
|
|
||||||
// Find all non-repeating events that occur on this date
|
|
||||||
for (const ev of calendarStore.events.values()) {
|
for (const ev of calendarStore.events.values()) {
|
||||||
if (!ev.isRepeating && dateStr >= ev.startDate && dateStr <= ev.endDate) {
|
if (!ev.isRepeating && dateStr >= ev.startDate && dateStr <= ev.endDate) {
|
||||||
storedEvents.push(ev)
|
storedEvents.push(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Build day events starting with stored (base/spanning) then virtual occurrences
|
|
||||||
const dayEvents = [...storedEvents]
|
const dayEvents = [...storedEvents]
|
||||||
|
// Expand repeating events into per-day occurrences (including virtual spans) when they cover this date.
|
||||||
for (const base of repeatingBases) {
|
for (const base of repeatingBases) {
|
||||||
// If the current date falls within the base event's original span, include the base
|
// Base event's original span: include it directly as occurrence index 0.
|
||||||
// event itself as occurrence index 0. Previously this was skipped which caused the
|
|
||||||
// first (n=0) occurrence of repeating events to be missing from the calendar.
|
|
||||||
if (dateStr >= base.startDate && dateStr <= base.endDate) {
|
if (dateStr >= base.startDate && dateStr <= base.endDate) {
|
||||||
dayEvents.push({
|
dayEvents.push({
|
||||||
...base,
|
...base,
|
||||||
// Mark explicit recurrence index for consistency with virtual occurrences
|
|
||||||
_recurrenceIndex: 0,
|
_recurrenceIndex: 0,
|
||||||
_baseId: base.id,
|
_baseId: base.id,
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any virtual occurrence spans this date
|
|
||||||
const baseStart = fromLocalString(base.startDate, DEFAULT_TZ)
|
const baseStart = fromLocalString(base.startDate, DEFAULT_TZ)
|
||||||
const baseEnd = fromLocalString(base.endDate, DEFAULT_TZ)
|
const baseEnd = fromLocalString(base.endDate, DEFAULT_TZ)
|
||||||
const spanDays = Math.max(0, differenceInCalendarDays(baseEnd, baseStart))
|
const spanDays = Math.max(0, differenceInCalendarDays(baseEnd, baseStart))
|
||||||
@ -183,19 +201,16 @@ function createWeek(virtualWeek) {
|
|||||||
|
|
||||||
let occurrenceFound = false
|
let occurrenceFound = false
|
||||||
|
|
||||||
// Walk backwards within span to find occurrence start
|
// Walk backwards within the base span to locate a matching virtual occurrence start.
|
||||||
for (let offset = 0; offset <= spanDays && !occurrenceFound; offset++) {
|
for (let offset = 0; offset <= spanDays && !occurrenceFound; offset++) {
|
||||||
const candidateStart = addDays(currentDate, -offset)
|
const candidateStart = addDays(currentDate, -offset)
|
||||||
const candidateStartStr = toLocalString(candidateStart, DEFAULT_TZ)
|
const candidateStartStr = toLocalString(candidateStart, DEFAULT_TZ)
|
||||||
|
|
||||||
const occurrenceIndex = getOccurrenceIndex(base, candidateStartStr, DEFAULT_TZ)
|
const occurrenceIndex = getOccurrenceIndex(base, candidateStartStr, DEFAULT_TZ)
|
||||||
if (occurrenceIndex !== null) {
|
if (occurrenceIndex !== null) {
|
||||||
// Calculate the end date of this occurrence
|
|
||||||
const virtualEndDate = getVirtualOccurrenceEndDate(base, candidateStartStr, DEFAULT_TZ)
|
const virtualEndDate = getVirtualOccurrenceEndDate(base, candidateStartStr, DEFAULT_TZ)
|
||||||
|
|
||||||
// Check if this occurrence spans through the current date
|
|
||||||
if (dateStr >= candidateStartStr && dateStr <= virtualEndDate) {
|
if (dateStr >= candidateStartStr && dateStr <= virtualEndDate) {
|
||||||
// Create virtual occurrence (if not already created)
|
|
||||||
const virtualId = base.id + '_v_' + candidateStartStr
|
const virtualId = base.id + '_v_' + candidateStartStr
|
||||||
const alreadyExists = dayEvents.some((ev) => ev.id === virtualId)
|
const alreadyExists = dayEvents.some((ev) => ev.id === virtualId)
|
||||||
|
|
||||||
@ -232,8 +247,6 @@ function createWeek(virtualWeek) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get holiday info once per day
|
|
||||||
// Ensure holidays initialized lazily
|
|
||||||
let holiday = null
|
let holiday = null
|
||||||
if (calendarStore.config.holidays.enabled) {
|
if (calendarStore.config.holidays.enabled) {
|
||||||
calendarStore._ensureHolidaysInitialized?.()
|
calendarStore._ensureHolidaysInitialized?.()
|
||||||
@ -308,21 +321,13 @@ function clearSelection() {
|
|||||||
selection.value = { startDate: null, dayCount: 0 }
|
selection.value = { startDate: null, dayCount: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDoubleTap(dateStr) {
|
|
||||||
const now = Date.now()
|
|
||||||
const isDouble = lastTapDate.value === dateStr && now - lastTapTime.value <= DOUBLE_TAP_DELAY
|
|
||||||
|
|
||||||
lastTapTime.value = now
|
|
||||||
lastTapDate.value = dateStr
|
|
||||||
|
|
||||||
return isDouble
|
|
||||||
}
|
|
||||||
|
|
||||||
function startDrag(dateStr) {
|
function startDrag(dateStr) {
|
||||||
|
dateStr = normalizeDate(dateStr)
|
||||||
if (calendarStore.config.select_days === 0) return
|
if (calendarStore.config.select_days === 0) return
|
||||||
isDragging.value = true
|
isDragging.value = true
|
||||||
dragAnchor.value = dateStr
|
dragAnchor.value = dateStr
|
||||||
selection.value = { startDate: dateStr, dayCount: 1 }
|
selection.value = { startDate: dateStr, dayCount: 1 }
|
||||||
|
addGlobalTouchListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDrag(dateStr) {
|
function updateDrag(dateStr) {
|
||||||
@ -338,6 +343,88 @@ function endDrag(dateStr) {
|
|||||||
selection.value = { startDate, dayCount }
|
selection.value = { startDate, dayCount }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function finalizeDragAndCreate() {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
isDragging.value = false
|
||||||
|
const eventData = createEventFromSelection()
|
||||||
|
if (eventData) {
|
||||||
|
clearSelection()
|
||||||
|
emit('create-event', eventData)
|
||||||
|
}
|
||||||
|
removeGlobalTouchListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateUnderPoint(x, y) {
|
||||||
|
const el = document.elementFromPoint(x, y)
|
||||||
|
let cur = el
|
||||||
|
while (cur) {
|
||||||
|
if (cur.dataset && cur.dataset.date) return cur.dataset.date
|
||||||
|
cur = cur.parentElement
|
||||||
|
}
|
||||||
|
return getDateFromCoordinates(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGlobalTouchMove(e) {
|
||||||
|
if (!isDragging.value) return
|
||||||
|
const t = e.touches && e.touches[0]
|
||||||
|
if (!t) return
|
||||||
|
e.preventDefault()
|
||||||
|
const dateStr = getDateUnderPoint(t.clientX, t.clientY)
|
||||||
|
if (dateStr) updateDrag(dateStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGlobalTouchEnd(e) {
|
||||||
|
if (!isDragging.value) {
|
||||||
|
removeGlobalTouchListeners()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const t = (e.changedTouches && e.changedTouches[0]) || (e.touches && e.touches[0])
|
||||||
|
if (t) {
|
||||||
|
const dateStr = getDateUnderPoint(t.clientX, t.clientY)
|
||||||
|
if (dateStr) {
|
||||||
|
const { startDate, dayCount } = calculateSelection(dragAnchor.value, dateStr)
|
||||||
|
selection.value = { startDate, dayCount }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalizeDragAndCreate()
|
||||||
|
}
|
||||||
|
|
||||||
|
function addGlobalTouchListeners() {
|
||||||
|
window.addEventListener('touchmove', onGlobalTouchMove, { passive: false })
|
||||||
|
window.addEventListener('touchend', onGlobalTouchEnd, { passive: false })
|
||||||
|
window.addEventListener('touchcancel', onGlobalTouchEnd, { passive: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeGlobalTouchListeners() {
|
||||||
|
window.removeEventListener('touchmove', onGlobalTouchMove)
|
||||||
|
window.removeEventListener('touchend', onGlobalTouchEnd)
|
||||||
|
window.removeEventListener('touchcancel', onGlobalTouchEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback hit-test if elementFromPoint doesn't find a day cell (e.g., moving between rows).
|
||||||
|
function getDateFromCoordinates(clientX, clientY) {
|
||||||
|
if (!viewport.value) return null
|
||||||
|
const vpRect = viewport.value.getBoundingClientRect()
|
||||||
|
const yOffset = clientY - vpRect.top + viewport.value.scrollTop
|
||||||
|
if (yOffset < 0) return null
|
||||||
|
const rowIndex = Math.floor(yOffset / rowHeight.value)
|
||||||
|
const virtualWeek = minVirtualWeek.value + rowIndex
|
||||||
|
if (virtualWeek < minVirtualWeek.value || virtualWeek > maxVirtualWeek.value) return null
|
||||||
|
const sampleWeek = viewport.value.querySelector('.week-row')
|
||||||
|
if (!sampleWeek) return null
|
||||||
|
const labelEl = sampleWeek.querySelector('.week-label')
|
||||||
|
const jogwheelWidth = 48
|
||||||
|
const wrRect = sampleWeek.getBoundingClientRect()
|
||||||
|
const labelRight = labelEl ? labelEl.getBoundingClientRect().right : wrRect.left
|
||||||
|
const daysAreaRight = wrRect.right - jogwheelWidth
|
||||||
|
const daysWidth = daysAreaRight - labelRight
|
||||||
|
if (clientX < labelRight || clientX > daysAreaRight) return null
|
||||||
|
const col = Math.min(6, Math.max(0, Math.floor(((clientX - labelRight) / daysWidth) * 7)))
|
||||||
|
const firstDay = getFirstDayForVirtualWeek(virtualWeek)
|
||||||
|
const targetDate = addDays(firstDay, col)
|
||||||
|
return toLocalString(targetDate, DEFAULT_TZ)
|
||||||
|
}
|
||||||
|
|
||||||
function calculateSelection(anchorStr, otherStr) {
|
function calculateSelection(anchorStr, otherStr) {
|
||||||
const limit = calendarStore.config.select_days
|
const limit = calendarStore.config.select_days
|
||||||
const anchorDate = fromLocalString(anchorStr, DEFAULT_TZ)
|
const anchorDate = fromLocalString(anchorStr, DEFAULT_TZ)
|
||||||
@ -395,59 +482,33 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleDayMouseDown = (dateStr) => {
|
const handleDayMouseDown = (d) => {
|
||||||
// Check for double tap - only start drag on second tap
|
d = normalizeDate(d)
|
||||||
if (isDoubleTap(dateStr)) {
|
if (Date.now() < suppressMouseUntil.value) return
|
||||||
startDrag(dateStr)
|
if (registerTap(d, 'mouse')) startDrag(d)
|
||||||
}
|
}
|
||||||
}
|
const handleDayMouseEnter = (d) => updateDrag(normalizeDate(d))
|
||||||
|
const handleDayMouseUp = (d) => {
|
||||||
const handleDayMouseEnter = (dateStr) => {
|
d = normalizeDate(d)
|
||||||
if (isDragging.value) {
|
if (Date.now() < suppressMouseUntil.value && !isDragging.value) return
|
||||||
updateDrag(dateStr)
|
if (!isDragging.value) return
|
||||||
}
|
endDrag(d)
|
||||||
}
|
const ev = createEventFromSelection()
|
||||||
|
if (ev) {
|
||||||
const handleDayMouseUp = (dateStr) => {
|
|
||||||
if (isDragging.value) {
|
|
||||||
endDrag(dateStr)
|
|
||||||
const eventData = createEventFromSelection()
|
|
||||||
if (eventData) {
|
|
||||||
clearSelection()
|
clearSelection()
|
||||||
emit('create-event', eventData)
|
emit('create-event', ev)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDayTouchStart = (dateStr) => {
|
|
||||||
// Check for double tap - only start drag on second tap
|
|
||||||
if (isDoubleTap(dateStr)) {
|
|
||||||
startDrag(dateStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDayTouchMove = (dateStr) => {
|
|
||||||
if (isDragging.value) {
|
|
||||||
updateDrag(dateStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDayTouchEnd = (dateStr) => {
|
|
||||||
if (isDragging.value) {
|
|
||||||
endDrag(dateStr)
|
|
||||||
const eventData = createEventFromSelection()
|
|
||||||
if (eventData) {
|
|
||||||
clearSelection()
|
|
||||||
emit('create-event', eventData)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const handleDayTouchStart = (d) => {
|
||||||
|
d = normalizeDate(d)
|
||||||
|
suppressMouseUntil.value = Date.now() + 800
|
||||||
|
if (registerTap(d, 'touch')) startDrag(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEventClick = (payload) => {
|
const handleEventClick = (payload) => {
|
||||||
emit('edit-event', payload)
|
emit('edit-event', payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle year change emitted from CalendarHeader: scroll to computed target position
|
|
||||||
const handleHeaderYearChange = ({ scrollTop: st }) => {
|
const handleHeaderYearChange = ({ scrollTop: st }) => {
|
||||||
const maxScroll = contentHeight.value - viewportHeight.value
|
const maxScroll = contentHeight.value - viewportHeight.value
|
||||||
const clamped = Math.max(0, Math.min(st, isFinite(maxScroll) ? maxScroll : st))
|
const clamped = Math.max(0, Math.min(st, isFinite(maxScroll) ? maxScroll : st))
|
||||||
@ -458,7 +519,7 @@ const handleHeaderYearChange = ({ scrollTop: st }) => {
|
|||||||
function openSettings() {
|
function openSettings() {
|
||||||
settingsDialog.value?.open()
|
settingsDialog.value?.open()
|
||||||
}
|
}
|
||||||
// Preserve approximate top visible date when first_day changes
|
// Keep roughly same visible date when first_day setting changes.
|
||||||
watch(
|
watch(
|
||||||
() => calendarStore.config.first_day,
|
() => calendarStore.config.first_day,
|
||||||
() => {
|
() => {
|
||||||
@ -504,13 +565,12 @@ watch(
|
|||||||
v-for="week in visibleWeeks"
|
v-for="week in visibleWeeks"
|
||||||
:key="week.virtualWeek"
|
:key="week.virtualWeek"
|
||||||
:week="week"
|
:week="week"
|
||||||
|
:dragging="isDragging"
|
||||||
:style="{ top: week.top + 'px' }"
|
:style="{ top: week.top + 'px' }"
|
||||||
@day-mousedown="handleDayMouseDown"
|
@day-mousedown="handleDayMouseDown"
|
||||||
@day-mouseenter="handleDayMouseEnter"
|
@day-mouseenter="handleDayMouseEnter"
|
||||||
@day-mouseup="handleDayMouseUp"
|
@day-mouseup="handleDayMouseUp"
|
||||||
@day-touchstart="handleDayTouchStart"
|
@day-touchstart="handleDayTouchStart"
|
||||||
@day-touchmove="handleDayTouchMove"
|
|
||||||
@day-touchend="handleDayTouchEnd"
|
|
||||||
@event-click="handleEventClick"
|
@event-click="handleEventClick"
|
||||||
/>
|
/>
|
||||||
<!-- Month labels positioned absolutely -->
|
<!-- Month labels positioned absolutely -->
|
||||||
|
@ -2,17 +2,13 @@
|
|||||||
import CalendarDay from './CalendarDay.vue'
|
import CalendarDay from './CalendarDay.vue'
|
||||||
import EventOverlay from './EventOverlay.vue'
|
import EventOverlay from './EventOverlay.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({ week: Object, dragging: { type: Boolean, default: false } })
|
||||||
week: Object,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'day-mousedown',
|
'day-mousedown',
|
||||||
'day-mouseenter',
|
'day-mouseenter',
|
||||||
'day-mouseup',
|
'day-mouseup',
|
||||||
'day-touchstart',
|
'day-touchstart',
|
||||||
'day-touchmove',
|
|
||||||
'day-touchend',
|
|
||||||
'event-click',
|
'event-click',
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -32,13 +28,7 @@ const handleDayTouchStart = (dateStr) => {
|
|||||||
emit('day-touchstart', dateStr)
|
emit('day-touchstart', dateStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDayTouchMove = (dateStr) => {
|
// touchmove & touchend handled globally in CalendarView
|
||||||
emit('day-touchmove', dateStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDayTouchEnd = (dateStr) => {
|
|
||||||
emit('day-touchend', dateStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEventClick = (payload) => {
|
const handleEventClick = (payload) => {
|
||||||
emit('event-click', payload)
|
emit('event-click', payload)
|
||||||
@ -53,12 +43,11 @@ const handleEventClick = (payload) => {
|
|||||||
v-for="day in props.week.days"
|
v-for="day in props.week.days"
|
||||||
:key="day.date"
|
:key="day.date"
|
||||||
:day="day"
|
:day="day"
|
||||||
|
:dragging="props.dragging"
|
||||||
@mousedown="handleDayMouseDown(day.date)"
|
@mousedown="handleDayMouseDown(day.date)"
|
||||||
@mouseenter="handleDayMouseEnter(day.date)"
|
@mouseenter="handleDayMouseEnter(day.date)"
|
||||||
@mouseup="handleDayMouseUp(day.date)"
|
@mouseup="handleDayMouseUp(day.date)"
|
||||||
@touchstart="handleDayTouchStart(day.date)"
|
@touchstart="handleDayTouchStart(day.date)"
|
||||||
@touchmove="handleDayTouchMove(day.date)"
|
|
||||||
@touchend="handleDayTouchEnd(day.date)"
|
|
||||||
/>
|
/>
|
||||||
<EventOverlay :week="props.week" @event-click="handleEventClick" />
|
<EventOverlay :week="props.week" @event-click="handleEventClick" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -165,8 +165,10 @@ function startLocalDrag(init, evt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent default to avoid text selection and other interference
|
// Prevent default for mouse/pen to avoid text selection. For touch we skip so the user can still scroll.
|
||||||
|
if (!(evt.pointerType === 'touch')) {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('pointermove', onDragPointerMove, { passive: false })
|
window.addEventListener('pointermove', onDragPointerMove, { passive: false })
|
||||||
window.addEventListener('pointerup', onDragPointerUp, { passive: false })
|
window.addEventListener('pointerup', onDragPointerUp, { passive: false })
|
||||||
|
Loading…
x
Reference in New Issue
Block a user