extract PiP drag/snap/enlarge logic into shared createPip() utility
All checks were successful
CI / update (push) Successful in 1m32s
All checks were successful
CI / update (push) Successful in 1m32s
Both StickyImage and rosary page now use the same pip.svelte.ts factory for mobile drag-to-corner, snap, and double-tap enlarge behavior.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { createPip } from '$lib/js/pip.svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {'layout' | 'overlay'} mode
|
* @param {'layout' | 'overlay'} mode
|
||||||
@@ -10,17 +11,9 @@
|
|||||||
|
|
||||||
let pipEl = $state(null);
|
let pipEl = $state(null);
|
||||||
let contentEl = $state(null);
|
let contentEl = $state(null);
|
||||||
let corner = $state('bottom-right');
|
|
||||||
let dragging = $state(false);
|
|
||||||
let enlarged = $state(false);
|
|
||||||
let inView = $state(false);
|
let inView = $state(false);
|
||||||
let dragOffset = { x: 0, y: 0 };
|
|
||||||
let dragPos = $state({ x: 0, y: 0 });
|
const pip = createPip();
|
||||||
let dragMoved = false;
|
|
||||||
let lastTapTime = 0;
|
|
||||||
const MARGIN = 16;
|
|
||||||
const TAP_THRESHOLD = 10;
|
|
||||||
const DOUBLE_TAP_MS = 400;
|
|
||||||
|
|
||||||
function isMobile() {
|
function isMobile() {
|
||||||
return !window.matchMedia('(min-width: 1024px)').matches;
|
return !window.matchMedia('(min-width: 1024px)').matches;
|
||||||
@@ -31,121 +24,14 @@
|
|||||||
return isMobile();
|
return isMobile();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether the image visibility is controlled by IntersectionObserver
|
|
||||||
function isObserverControlled() {
|
|
||||||
return mode === 'overlay';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCornerPos(c, el) {
|
|
||||||
const vw = window.innerWidth;
|
|
||||||
const vh = window.innerHeight;
|
|
||||||
const r = el.getBoundingClientRect();
|
|
||||||
return {
|
|
||||||
'top-left': { x: MARGIN, y: MARGIN },
|
|
||||||
'top-right': { x: vw - r.width - MARGIN, y: MARGIN },
|
|
||||||
'bottom-left': { x: MARGIN, y: vh - r.height - MARGIN },
|
|
||||||
'bottom-right': { x: vw - r.width - MARGIN, y: vh - r.height - MARGIN },
|
|
||||||
}[c];
|
|
||||||
}
|
|
||||||
|
|
||||||
function snapToCorner(el, c) {
|
|
||||||
const pos = getCornerPos(c, el);
|
|
||||||
corner = c;
|
|
||||||
dragPos = pos;
|
|
||||||
el.style.transition = 'transform 0.25s ease';
|
|
||||||
el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
|
||||||
el.addEventListener('transitionend', () => { el.style.transition = ''; }, { once: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function nearestCorner(x, y, el) {
|
|
||||||
const vw = window.innerWidth;
|
|
||||||
const vh = window.innerHeight;
|
|
||||||
const r = el.getBoundingClientRect();
|
|
||||||
const cx = x + r.width / 2;
|
|
||||||
const cy = y + r.height / 2;
|
|
||||||
const left = cx < vw / 2;
|
|
||||||
const top = cy < vh / 2;
|
|
||||||
return `${top ? 'top' : 'bottom'}-${left ? 'left' : 'right'}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPointerDown(e) {
|
|
||||||
if (!pipEl || !isPipActive()) return;
|
|
||||||
dragging = true;
|
|
||||||
dragMoved = false;
|
|
||||||
const r = pipEl.getBoundingClientRect();
|
|
||||||
dragOffset = { x: e.clientX - r.left, y: e.clientY - r.top };
|
|
||||||
pipEl.setPointerCapture(e.pointerId);
|
|
||||||
pipEl.style.transition = '';
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPointerMove(e) {
|
|
||||||
if (!dragging || !pipEl) return;
|
|
||||||
const x = e.clientX - dragOffset.x;
|
|
||||||
const y = e.clientY - dragOffset.y;
|
|
||||||
if (!dragMoved) {
|
|
||||||
const dx = Math.abs(x - dragPos.x);
|
|
||||||
const dy = Math.abs(y - dragPos.y);
|
|
||||||
if (dx > TAP_THRESHOLD || dy > TAP_THRESHOLD) dragMoved = true;
|
|
||||||
}
|
|
||||||
dragPos = { x, y };
|
|
||||||
pipEl.style.transform = `translate(${x}px, ${y}px)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleEnlarged() {
|
|
||||||
if (!pipEl) return;
|
|
||||||
const rect = pipEl.getBoundingClientRect();
|
|
||||||
const vh = window.innerHeight / 100;
|
|
||||||
const currentH = enlarged ? 37.5 * vh : 25 * vh;
|
|
||||||
const targetH = enlarged ? 25 * vh : 37.5 * vh;
|
|
||||||
const ratio = targetH / currentH;
|
|
||||||
|
|
||||||
enlarged = !enlarged;
|
|
||||||
|
|
||||||
const newW = rect.width * ratio;
|
|
||||||
const newH = rect.height * ratio;
|
|
||||||
let newX = rect.left;
|
|
||||||
let newY = rect.top;
|
|
||||||
if (corner.includes('right')) newX = rect.right - newW;
|
|
||||||
if (corner.includes('bottom')) newY = rect.bottom - newH;
|
|
||||||
|
|
||||||
dragPos = { x: newX, y: newY };
|
|
||||||
pipEl.style.transition = 'transform 0.25s ease';
|
|
||||||
pipEl.style.transform = `translate(${newX}px, ${newY}px)`;
|
|
||||||
pipEl.addEventListener('transitionend', () => {
|
|
||||||
pipEl.style.transition = '';
|
|
||||||
}, { once: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPointerUp(e) {
|
|
||||||
if (!dragging || !pipEl) return;
|
|
||||||
dragging = false;
|
|
||||||
|
|
||||||
if (!dragMoved) {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastTapTime < DOUBLE_TAP_MS) {
|
|
||||||
lastTapTime = 0;
|
|
||||||
toggleEnlarged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastTapTime = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
const r = pipEl.getBoundingClientRect();
|
|
||||||
snapToCorner(pipEl, nearestCorner(r.left, r.top, pipEl));
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateVisibility() {
|
function updateVisibility() {
|
||||||
if (!pipEl) return;
|
if (!pipEl) return;
|
||||||
if (isPipActive()) {
|
if (isPipActive()) {
|
||||||
// Mobile PiP mode
|
// Mobile PiP mode
|
||||||
if (inView) {
|
if (inView) {
|
||||||
const pos = getCornerPos(corner, pipEl);
|
pip.show(pipEl);
|
||||||
dragPos = pos;
|
|
||||||
pipEl.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
|
||||||
pipEl.style.opacity = '1';
|
|
||||||
} else {
|
} else {
|
||||||
pipEl.style.opacity = '0';
|
pip.hide();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Desktop (both modes): CSS handles everything
|
// Desktop (both modes): CSS handles everything
|
||||||
@@ -161,7 +47,11 @@
|
|||||||
|
|
||||||
function onResize() {
|
function onResize() {
|
||||||
if (!pipEl) return;
|
if (!pipEl) return;
|
||||||
updateVisibility();
|
if (isPipActive() && inView) {
|
||||||
|
pip.reposition();
|
||||||
|
} else {
|
||||||
|
updateVisibility();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -193,11 +83,11 @@
|
|||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
class="image-wrap"
|
class="image-wrap"
|
||||||
class:enlarged
|
class:enlarged={pip.enlarged}
|
||||||
bind:this={pipEl}
|
bind:this={pipEl}
|
||||||
onpointerdown={onPointerDown}
|
onpointerdown={pip.onpointerdown}
|
||||||
onpointermove={onPointerMove}
|
onpointermove={pip.onpointermove}
|
||||||
onpointerup={onPointerUp}
|
onpointerup={pip.onpointerup}
|
||||||
>
|
>
|
||||||
<img {src} {alt}>
|
<img {src} {alt}>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
164
src/lib/js/pip.svelte.ts
Normal file
164
src/lib/js/pip.svelte.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* Shared PiP (Picture-in-Picture) drag/snap/enlarge composable.
|
||||||
|
* Extracts duplicated mobile PiP logic from StickyImage and rosary page.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface PipOptions {
|
||||||
|
margin?: number;
|
||||||
|
tapThreshold?: number;
|
||||||
|
doubleTapMs?: number;
|
||||||
|
initialCorner?: Corner;
|
||||||
|
smallHeight?: number;
|
||||||
|
largeHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
||||||
|
|
||||||
|
export function createPip(opts: PipOptions = {}) {
|
||||||
|
const margin = opts.margin ?? 16;
|
||||||
|
const tapThreshold = opts.tapThreshold ?? 10;
|
||||||
|
const doubleTapMs = opts.doubleTapMs ?? 400;
|
||||||
|
const smallVh = opts.smallHeight ?? 25;
|
||||||
|
const largeVh = opts.largeHeight ?? 37.5;
|
||||||
|
|
||||||
|
let corner: Corner = $state(opts.initialCorner ?? 'bottom-right');
|
||||||
|
let dragging = $state(false);
|
||||||
|
let enlarged = $state(false);
|
||||||
|
|
||||||
|
let dragOffset = { x: 0, y: 0 };
|
||||||
|
let dragPos = { x: 0, y: 0 };
|
||||||
|
let dragMoved = false;
|
||||||
|
let lastTapTime = 0;
|
||||||
|
let el: HTMLElement | null = null;
|
||||||
|
|
||||||
|
function getCornerPos(c: Corner, target: HTMLElement) {
|
||||||
|
const vw = window.innerWidth;
|
||||||
|
const vh = window.innerHeight;
|
||||||
|
const r = target.getBoundingClientRect();
|
||||||
|
const positions: Record<Corner, { x: number; y: number }> = {
|
||||||
|
'top-left': { x: margin, y: margin },
|
||||||
|
'top-right': { x: vw - r.width - margin, y: margin },
|
||||||
|
'bottom-left': { x: margin, y: vh - r.height - margin },
|
||||||
|
'bottom-right': { x: vw - r.width - margin, y: vh - r.height - margin },
|
||||||
|
};
|
||||||
|
return positions[c];
|
||||||
|
}
|
||||||
|
|
||||||
|
function nearestCorner(x: number, y: number, target: HTMLElement): Corner {
|
||||||
|
const vw = window.innerWidth;
|
||||||
|
const vh = window.innerHeight;
|
||||||
|
const r = target.getBoundingClientRect();
|
||||||
|
const cx = x + r.width / 2;
|
||||||
|
const cy = y + r.height / 2;
|
||||||
|
const left = cx < vw / 2;
|
||||||
|
const top = cy < vh / 2;
|
||||||
|
return `${top ? 'top' : 'bottom'}-${left ? 'left' : 'right'}` as Corner;
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapToCorner(target: HTMLElement, c: Corner) {
|
||||||
|
const pos = getCornerPos(c, target);
|
||||||
|
corner = c;
|
||||||
|
dragPos = pos;
|
||||||
|
target.style.transition = 'transform 0.25s ease';
|
||||||
|
target.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||||
|
target.addEventListener('transitionend', () => { target.style.transition = ''; }, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEnlarged() {
|
||||||
|
if (!el) return;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const vh = window.innerHeight / 100;
|
||||||
|
const currentH = enlarged ? largeVh * vh : smallVh * vh;
|
||||||
|
const targetH = enlarged ? smallVh * vh : largeVh * vh;
|
||||||
|
const ratio = targetH / currentH;
|
||||||
|
|
||||||
|
enlarged = !enlarged;
|
||||||
|
|
||||||
|
const newW = rect.width * ratio;
|
||||||
|
const newH = rect.height * ratio;
|
||||||
|
let newX = rect.left;
|
||||||
|
let newY = rect.top;
|
||||||
|
if (corner.includes('right')) newX = rect.right - newW;
|
||||||
|
if (corner.includes('bottom')) newY = rect.bottom - newH;
|
||||||
|
|
||||||
|
dragPos = { x: newX, y: newY };
|
||||||
|
el.style.transition = 'transform 0.25s ease';
|
||||||
|
el.style.transform = `translate(${newX}px, ${newY}px)`;
|
||||||
|
el.addEventListener('transitionend', () => {
|
||||||
|
el!.style.transition = '';
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(target: HTMLElement) {
|
||||||
|
el = target;
|
||||||
|
const pos = getCornerPos(corner, target);
|
||||||
|
dragPos = pos;
|
||||||
|
target.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||||
|
target.style.opacity = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
if (el) el.style.opacity = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function reposition() {
|
||||||
|
if (!el) return;
|
||||||
|
const pos = getCornerPos(corner, el);
|
||||||
|
dragPos = pos;
|
||||||
|
el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onpointerdown(e: PointerEvent) {
|
||||||
|
if (!el) return;
|
||||||
|
dragging = true;
|
||||||
|
dragMoved = false;
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
dragOffset = { x: e.clientX - r.left, y: e.clientY - r.top };
|
||||||
|
el.setPointerCapture(e.pointerId);
|
||||||
|
el.style.transition = '';
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onpointermove(e: PointerEvent) {
|
||||||
|
if (!dragging || !el) return;
|
||||||
|
const x = e.clientX - dragOffset.x;
|
||||||
|
const y = e.clientY - dragOffset.y;
|
||||||
|
if (!dragMoved) {
|
||||||
|
const dx = Math.abs(x - dragPos.x);
|
||||||
|
const dy = Math.abs(y - dragPos.y);
|
||||||
|
if (dx > tapThreshold || dy > tapThreshold) dragMoved = true;
|
||||||
|
}
|
||||||
|
dragPos = { x, y };
|
||||||
|
el.style.transform = `translate(${x}px, ${y}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onpointerup(_e: PointerEvent) {
|
||||||
|
if (!dragging || !el) return;
|
||||||
|
dragging = false;
|
||||||
|
|
||||||
|
if (!dragMoved) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastTapTime < doubleTapMs) {
|
||||||
|
lastTapTime = 0;
|
||||||
|
toggleEnlarged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastTapTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
snapToCorner(el, nearestCorner(r.left, r.top, el));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get corner() { return corner; },
|
||||||
|
get dragging() { return dragging; },
|
||||||
|
get enlarged() { return enlarged; },
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
reposition,
|
||||||
|
onpointerdown,
|
||||||
|
onpointermove,
|
||||||
|
onpointerup
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { createLanguageContext } from "$lib/contexts/languageContext.js";
|
import { createLanguageContext } from "$lib/contexts/languageContext.js";
|
||||||
|
import { createPip } from "$lib/js/pip.svelte";
|
||||||
import "$lib/css/christ.css";
|
import "$lib/css/christ.css";
|
||||||
import "$lib/css/action_button.css";
|
import "$lib/css/action_button.css";
|
||||||
import Kreuzzeichen from "$lib/components/prayers/Kreuzzeichen.svelte";
|
import Kreuzzeichen from "$lib/components/prayers/Kreuzzeichen.svelte";
|
||||||
@@ -352,6 +353,25 @@ function getMysteryImage(mystery, section) {
|
|||||||
}
|
}
|
||||||
const mysteryPipSrc = $derived(getMysteryImage(selectedMystery, activeSection));
|
const mysteryPipSrc = $derived(getMysteryImage(selectedMystery, activeSection));
|
||||||
|
|
||||||
|
// Mobile PiP drag/enlarge
|
||||||
|
const pip = createPip();
|
||||||
|
let rosaryPipEl = $state(null);
|
||||||
|
let lastPipSrc = $state(null);
|
||||||
|
|
||||||
|
function isMobilePip() {
|
||||||
|
return !window.matchMedia('(min-width: 900px)').matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (mysteryPipSrc) lastPipSrc = mysteryPipSrc;
|
||||||
|
if (!rosaryPipEl || !isMobilePip()) return;
|
||||||
|
if (mysteryPipSrc) {
|
||||||
|
pip.show(rosaryPipEl);
|
||||||
|
} else {
|
||||||
|
pip.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mysteryImageContainer;
|
let mysteryImageContainer;
|
||||||
let mysteryScrollRaf = null;
|
let mysteryScrollRaf = null;
|
||||||
|
|
||||||
@@ -491,6 +511,14 @@ onMount(() => {
|
|||||||
// Now allow saving to localStorage
|
// Now allow saving to localStorage
|
||||||
hasLoadedFromStorage = true;
|
hasLoadedFromStorage = true;
|
||||||
|
|
||||||
|
// PiP resize handler
|
||||||
|
const onPipResize = () => {
|
||||||
|
if (rosaryPipEl && isMobilePip() && mysteryPipSrc) {
|
||||||
|
pip.reposition();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', onPipResize);
|
||||||
|
|
||||||
let scrollLock = null; // Track which side initiated the scroll: 'prayer', 'svg', or 'click'
|
let scrollLock = null; // Track which side initiated the scroll: 'prayer', 'svg', or 'click'
|
||||||
let scrollLockTimeout = null;
|
let scrollLockTimeout = null;
|
||||||
|
|
||||||
@@ -835,6 +863,7 @@ onMount(() => {
|
|||||||
clearTimeout(scrollLockTimeout);
|
clearTimeout(scrollLockTimeout);
|
||||||
clearTimeout(svgScrollTimeout);
|
clearTimeout(svgScrollTimeout);
|
||||||
window.removeEventListener('scroll', handleWindowScroll);
|
window.removeEventListener('scroll', handleWindowScroll);
|
||||||
|
window.removeEventListener('resize', onPipResize);
|
||||||
if (svgContainer) {
|
if (svgContainer) {
|
||||||
svgContainer.removeEventListener('scroll', handleSvgScroll);
|
svgContainer.removeEventListener('scroll', handleSvgScroll);
|
||||||
}
|
}
|
||||||
@@ -1392,15 +1421,17 @@ h1 {
|
|||||||
/* Mobile PiP for mystery images */
|
/* Mobile PiP for mystery images */
|
||||||
.mystery-pip {
|
.mystery-pip {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 16px;
|
top: 0;
|
||||||
right: 16px;
|
left: 0;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s ease;
|
touch-action: none;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
transition: opacity 0.25s ease;
|
||||||
}
|
}
|
||||||
.mystery-pip.visible {
|
.mystery-pip:active {
|
||||||
opacity: 1;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
.mystery-pip img {
|
.mystery-pip img {
|
||||||
height: 25vh;
|
height: 25vh;
|
||||||
@@ -1408,6 +1439,11 @@ h1 {
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: height 0.25s ease;
|
||||||
|
}
|
||||||
|
.mystery-pip.enlarged img {
|
||||||
|
height: 37.5vh;
|
||||||
}
|
}
|
||||||
@media (min-width: 900px) {
|
@media (min-width: 900px) {
|
||||||
.mystery-pip {
|
.mystery-pip {
|
||||||
@@ -1819,9 +1855,18 @@ h1 {
|
|||||||
|
|
||||||
<!-- Mobile PiP for mystery images -->
|
<!-- Mobile PiP for mystery images -->
|
||||||
{#if hasMysteryImages}
|
{#if hasMysteryImages}
|
||||||
<div class="mystery-pip" class:visible={!!mysteryPipSrc}>
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
{#if mysteryPipSrc}
|
<div
|
||||||
<img src={mysteryPipSrc} alt="">
|
class="mystery-pip"
|
||||||
|
class:visible={!!mysteryPipSrc}
|
||||||
|
class:enlarged={pip.enlarged}
|
||||||
|
bind:this={rosaryPipEl}
|
||||||
|
onpointerdown={pip.onpointerdown}
|
||||||
|
onpointermove={pip.onpointermove}
|
||||||
|
onpointerup={pip.onpointerup}
|
||||||
|
>
|
||||||
|
{#if lastPipSrc}
|
||||||
|
<img src={lastPipSrc} alt="">
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user