UI Fixes
Adding page component to document results Adding aside to handle notes and highlights in future
This commit is contained in:
parent
e2deaa8066
commit
1756aadaff
|
|
@ -755,29 +755,35 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
|
||||||
return matches.length
|
return matches.length
|
||||||
}
|
}
|
||||||
const isPopOverOpen = ref(false)
|
const isPopOverOpen = ref(false)
|
||||||
const anchor = ref({ x: 0, y: 0 })
|
|
||||||
const virtualElement = computed(() => ({
|
|
||||||
getBoundingClientRect: () =>
|
|
||||||
({
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
left: anchor.value.x,
|
|
||||||
right: anchor.value.x,
|
|
||||||
top: anchor.value.y,
|
|
||||||
bottom: anchor.value.y,
|
|
||||||
...anchor.value
|
|
||||||
} as DOMRect)
|
|
||||||
}))
|
|
||||||
|
|
||||||
function handleSelection(event: MouseEvent) {
|
function handleSelection(event: MouseEvent) {
|
||||||
const selection = window.getSelection()
|
const selection = window.getSelection()
|
||||||
if (selection && selection.toString().length > 0) {
|
if (selection && selection.toString().length > 3) {
|
||||||
anchor.value = { x: event.clientX, y: event.clientY }
|
|
||||||
isPopOverOpen.value = true
|
isPopOverOpen.value = true
|
||||||
} else {
|
} else {
|
||||||
isPopOverOpen.value = false
|
isPopOverOpen.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const links = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Ver en sitio',
|
||||||
|
icon: 'ph-arrow-square-out',
|
||||||
|
to: matchUrl,
|
||||||
|
target: '_blank'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Debug',
|
||||||
|
content: debugDocument(props.document)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -926,68 +932,37 @@ function handleSelection(event: MouseEvent) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="scrollContainer" class="flex-1 overflow-y-auto relative bg-gray-100">
|
<div ref="scrollContainer" class="flex-1 overflow-y-auto relative bg-gray-100">
|
||||||
<!-- Cargando párrafos -->
|
<UPage class="max-w-6xl mx-auto py-0 my-0 " @mouseup="handleSelection">
|
||||||
|
<UPageBody class="bg-white p-8 rounded-xl shadow-lg">
|
||||||
|
<UPageHeader
|
||||||
|
:title="document?.title"
|
||||||
|
:description="formatSignature(document)"
|
||||||
|
:headline="document?.draft ? 'Borrador' : ''"
|
||||||
|
:links="links"
|
||||||
|
class="py-0 pb-2"
|
||||||
|
/>
|
||||||
<div v-if="paragraphsLoading" class="flex items-center justify-center gap-2 py-16 text-sm text-muted">
|
<div v-if="paragraphsLoading" class="flex items-center justify-center gap-2 py-16 text-sm text-muted">
|
||||||
<UIcon name="i-lucide-loader-circle" class="size-5 animate-spin" />
|
<UIcon name="i-lucide-loader-circle" class="size-5 animate-spin" />
|
||||||
Cargando párrafos...
|
Cargando párrafos...
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="paragraphs.length" ref="paragraphsContainer">
|
||||||
<!-- Todos los párrafos -->
|
<div class="">
|
||||||
<div v-else-if="paragraphs.length" ref="paragraphsContainer" class="p-1 sm:p-4 max-w-4xl m-4 sm:mx-auto sm:my-6">
|
|
||||||
<UPopover
|
|
||||||
v-model:open="isPopOverOpen"
|
|
||||||
:reference="virtualElement"
|
|
||||||
class="absolute top-[90vh] left-[900px]"
|
|
||||||
>
|
|
||||||
<!-- We use the slot to bind to our moving anchor -->
|
|
||||||
<template #anchor>
|
|
||||||
<div class="hidden" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #content>
|
|
||||||
<UFieldGroup size="sm" orientation="horizontal" class="bg-white p-0 rounded-md shadow-md">
|
|
||||||
<UButton
|
|
||||||
icon="ph-note-pencil"
|
|
||||||
variant="ghost"
|
|
||||||
color="neutral"
|
|
||||||
size="lg"
|
|
||||||
@click="addNote()"
|
|
||||||
></UButton>
|
|
||||||
<UButton
|
|
||||||
icon="ph-highlighter"
|
|
||||||
variant="ghost"
|
|
||||||
color="neutral"
|
|
||||||
size="lg"
|
|
||||||
@click="highlightParagraph()"
|
|
||||||
></UButton>
|
|
||||||
<UButton
|
|
||||||
icon="ph-copy"
|
|
||||||
variant="ghost"
|
|
||||||
color="neutral"
|
|
||||||
size="lg"
|
|
||||||
@click="copyToClipboard()"
|
|
||||||
></UButton>
|
|
||||||
</UFieldGroup>
|
|
||||||
</template>
|
|
||||||
</UPopover>
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-4 pl-2 sm:p-8 sm:pl-4 mb-4 last:mb-0">
|
|
||||||
<div v-for="(hit, idx) in paragraphs" :key="idx" :data-paragraph-number="hit.document.number">
|
<div v-for="(hit, idx) in paragraphs" :key="idx" :data-paragraph-number="hit.document.number">
|
||||||
<div class="grid grid-cols-1fr items-start gap-2 mb-2" :class="(showParagraphNumbers && 'grid-cols-[32px_1fr]')">
|
<div class="grid grid-cols-1fr items-start gap-2 mb-2" :class="(showParagraphNumbers && 'grid-cols-[20px_1fr]')">
|
||||||
<div v-if="showParagraphNumbers" class="w-full select-none cursor-pointer flex justify-end">
|
<div v-if="showParagraphNumbers" class="w-full select-none cursor-pointer flex justify-end">
|
||||||
<UBadge
|
<UBadge
|
||||||
v-if="hit.document.number"
|
v-if="hit.document.number"
|
||||||
:label="`${hit.document.number}`"
|
:label="`${hit.document.number}`"
|
||||||
size="md"
|
size="md"
|
||||||
variant="ghost"
|
variant="link"
|
||||||
class="text-gray-300 hover:text-white"
|
class="text-gray-300 font-bold"
|
||||||
:class="(hit.document.type=='activities'?'hover:bg-carpagreen':'hover:bg-carpablue')"
|
:class="(hit.document.type=='activities'?'hover:text-carpagreen':'hover:text-carpablue')"
|
||||||
@click="isPopOverOpen = !isPopOverOpen"
|
@click="isPopOverOpen = !isPopOverOpen"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div
|
<div
|
||||||
class="paragraph-html text-sm leading-relaxed text-gray-800 dark:text-gray-200"
|
class="paragraph-html text-sm leading-relaxed text-gray-800 dark:text-gray-200"
|
||||||
@mouseup="handleSelection"
|
|
||||||
v-html="hit.document.html || hit.document.text"
|
v-html="hit.document.html || hit.document.text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1005,6 +980,56 @@ function handleSelection(event: MouseEvent) {
|
||||||
<ULink :to="matchUrl" target="_blank" class="text-primary">verlo en el sitio</ULink>.
|
<ULink :to="matchUrl" target="_blank" class="text-primary">verlo en el sitio</ULink>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</UPageBody>
|
||||||
|
<template #right>
|
||||||
|
<UPageAside
|
||||||
|
class="py-0 my-0"
|
||||||
|
>
|
||||||
|
<UFieldGroup orientation="horizontal" class="bg-white my-0 py-4 p-2 rounded-md shadow-md w-full justify-around">
|
||||||
|
<UTooltip
|
||||||
|
text="Crear nota"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
icon="ph-note-pencil"
|
||||||
|
:variant="isPopOverOpen ? 'solid': 'soft'"
|
||||||
|
:color="isPopOverOpen ? 'info' : 'neutral'"
|
||||||
|
size="lg"
|
||||||
|
:disabled="isPopOverOpen ? false : true"
|
||||||
|
@click="addNote()"
|
||||||
|
></UButton>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip
|
||||||
|
text="Resaltar">
|
||||||
|
<UButton
|
||||||
|
icon="ph-highlighter"
|
||||||
|
:variant="isPopOverOpen ? 'solid': 'soft'"
|
||||||
|
:color="isPopOverOpen ? 'success' : 'neutral'"
|
||||||
|
size="lg"
|
||||||
|
:disabled="isPopOverOpen ? false : true"
|
||||||
|
@click="highlightParagraph()"
|
||||||
|
></UButton>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip
|
||||||
|
text="Copiar">
|
||||||
|
<UButton
|
||||||
|
icon="ph-copy"
|
||||||
|
:variant="isPopOverOpen ? 'solid': 'soft'"
|
||||||
|
:color="isPopOverOpen ? 'warning' : 'neutral'"
|
||||||
|
size="lg"
|
||||||
|
:disabled="isPopOverOpen ? false : true"
|
||||||
|
@click="copyToClipboard()"
|
||||||
|
></UButton>
|
||||||
|
</UTooltip>
|
||||||
|
</UFieldGroup>
|
||||||
|
|
||||||
|
<UAccordion :items="items">
|
||||||
|
<template #body="{ item }">
|
||||||
|
<pre>{{ item.content }}</pre>
|
||||||
|
</template>
|
||||||
|
</UAccordion>
|
||||||
|
</UPageAside>
|
||||||
|
</template>
|
||||||
|
</UPage>
|
||||||
</div>
|
</div>
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -432,7 +432,6 @@ async function fetchDocumentWithParagraphs(docId: string) {
|
||||||
selectedDocument.value = null
|
selectedDocument.value = null
|
||||||
selectedParagraphs.value = []
|
selectedParagraphs.value = []
|
||||||
try {
|
try {
|
||||||
console.log(`[fetchDocumentWithParagraphs] Fetching document with paragraphs. collection=${props.mainCollection} docId=${docId}`)
|
|
||||||
const res = await documentsApi.multiSearch({
|
const res = await documentsApi.multiSearch({
|
||||||
multiSearchParameters: {},
|
multiSearchParameters: {},
|
||||||
multiSearchSearchesParameter: {
|
multiSearchSearchesParameter: {
|
||||||
|
|
@ -445,7 +444,6 @@ async function fetchDocumentWithParagraphs(docId: string) {
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
console.log(`[fetchDocumentWithParagraphs] collection=${props.mainCollection} docId=${docId}`, JSON.stringify(res?.results?.[0], null, 2))
|
|
||||||
const hit = (res?.results?.[0] as { hits?: Array<{ document: Record<string, unknown> }> })?.hits?.[0]
|
const hit = (res?.results?.[0] as { hits?: Array<{ document: Record<string, unknown> }> })?.hits?.[0]
|
||||||
if (hit) {
|
if (hit) {
|
||||||
const docRaw = { ...hit.document }
|
const docRaw = { ...hit.document }
|
||||||
|
|
|
||||||
|
|
@ -24,83 +24,108 @@ export function formatFiles( files:FilesObject ){
|
||||||
const { $i18n } = useNuxtApp();
|
const { $i18n } = useNuxtApp();
|
||||||
const t = $i18n.t;
|
const t = $i18n.t;
|
||||||
|
|
||||||
let items = []
|
let items = [];
|
||||||
if (files) {
|
if (files) {
|
||||||
if (files.youtube) {
|
if (files.youtube) {
|
||||||
items.push({
|
items.push({
|
||||||
to: files.youtube,
|
to: files.youtube,
|
||||||
target: '_blank',
|
target: "_blank",
|
||||||
label: 'Youtube',
|
label: "Youtube",
|
||||||
icon: 'ph:youtube-logo-thin',
|
icon: "ph:youtube-logo-thin",
|
||||||
labelClass: 'text-xs',
|
labelClass: "text-xs",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if (files.video) {
|
if (files.video) {
|
||||||
items.push({
|
items.push({
|
||||||
to: files.video,
|
to: files.video,
|
||||||
target: '_blank',
|
target: "_blank",
|
||||||
label: t('Activities.video'),
|
label: t("Activities.video"),
|
||||||
icon: 'ph:file-video-thin',
|
icon: "ph:file-video-thin",
|
||||||
labelClass: 'text-xs'
|
labelClass: "text-xs",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if (files.audio) {
|
if (files.audio) {
|
||||||
items.push({
|
items.push({
|
||||||
to: `https://actividadeswp.carpa.com/wp-content/uploads/${files.audio}`,
|
to: `https://actividadeswp.carpa.com/wp-content/uploads/${files.audio}`,
|
||||||
target: '_blank',
|
target: "_blank",
|
||||||
label: t('Activities.audio'),
|
label: t("Activities.audio"),
|
||||||
icon: 'ph:file-audio-thin',
|
icon: "ph:file-audio-thin",
|
||||||
labelClass: 'text-xs'
|
labelClass: "text-xs",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if (files.booklet) {
|
if (files.booklet) {
|
||||||
items.push({
|
items.push({
|
||||||
to: files.booklet,
|
to: files.booklet,
|
||||||
target: '_blank',
|
target: "_blank",
|
||||||
label: t('Activities.book'),
|
label: t("Activities.book"),
|
||||||
icon: 'ph:notebook-thin',
|
icon: "ph:notebook-thin",
|
||||||
labelClass: 'text-xs'
|
labelClass: "text-xs",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if (files.simple) {
|
if (files.simple) {
|
||||||
items.push({
|
items.push({
|
||||||
to: files.simple,
|
to: files.simple,
|
||||||
target: '_blank',
|
target: "_blank",
|
||||||
label: t('Activities.simple'),
|
label: t("Activities.simple"),
|
||||||
icon: 'ph:note-thin',
|
icon: "ph:note-thin",
|
||||||
labelClass: 'text-xs'
|
labelClass: "text-xs",
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(d: number) {
|
export function formatDate(d: number) {
|
||||||
const { $i18n } = useNuxtApp();
|
if (!d) {
|
||||||
const locale = $i18n.locale;
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let date = new Date(d * 1000)
|
const { $i18n } = useNuxtApp()
|
||||||
|
const locale = $i18n.locale
|
||||||
|
|
||||||
let dateString = date.toLocaleString(locale.value, { year: 'numeric', month:'long', day: 'numeric', weekday:'long', timeZone:'UTC' })
|
const date = new Date(d * 1000)
|
||||||
return capitalizeFirstLetter( dateString );
|
|
||||||
|
const dateString = date.toLocaleString(locale.value, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
weekday: 'long',
|
||||||
|
timeZone: 'UTC'
|
||||||
|
})
|
||||||
|
return capitalizeFirstLetter(dateString)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatLocation(i: ItemObject) {
|
export function formatLocation(i: ItemObject) {
|
||||||
const { $i18n } = useNuxtApp();
|
if (!i) {
|
||||||
const locale = $i18n.locale;
|
return
|
||||||
const regionNames = new Intl.DisplayNames(
|
}
|
||||||
[ locale.value], {type: 'region'}
|
const { $i18n } = useNuxtApp()
|
||||||
);
|
const locale = $i18n.locale
|
||||||
|
const regionNames = new Intl.DisplayNames([locale.value], { type: 'region' })
|
||||||
|
|
||||||
var locationParts = [];
|
const locationParts = []
|
||||||
locationParts.push(i?.place);
|
locationParts.push(i?.place)
|
||||||
locationParts.push(i?.city);
|
locationParts.push(i?.city)
|
||||||
locationParts.push(i?.state);
|
locationParts.push(i?.state)
|
||||||
if (i.country) {
|
if (i.country) {
|
||||||
locationParts.push(regionNames.of(i.country));
|
locationParts.push(regionNames.of(i.country))
|
||||||
}
|
}
|
||||||
|
|
||||||
return locationParts.filter(Boolean).join(', ');
|
return locationParts.filter(Boolean).join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSignature(i: ItemObject) {
|
||||||
|
const date = formatDate(i?.timestamp)
|
||||||
|
const location = formatLocation(i)
|
||||||
|
if (date === undefined || location === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const val = `${date} - ${location}`
|
||||||
|
|
||||||
|
if (!val) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDay(d: number) {
|
export function getDay(d: number) {
|
||||||
|
|
@ -108,7 +133,10 @@ export function getDay( d:number ){
|
||||||
const locale = $i18n.locale;
|
const locale = $i18n.locale;
|
||||||
let date = new Date(d * 1000);
|
let date = new Date(d * 1000);
|
||||||
|
|
||||||
return date.toLocaleString(locale.value, { weekday: 'long', timeZone: 'utc' });
|
return date.toLocaleString(locale.value, {
|
||||||
|
weekday: "long",
|
||||||
|
timeZone: "utc",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDayDate(d: number) {
|
export function getDayDate(d: number) {
|
||||||
|
|
@ -116,7 +144,7 @@ export function getDayDate( d:number ){
|
||||||
const locale = $i18n.locale;
|
const locale = $i18n.locale;
|
||||||
let date = new Date(d * 1000);
|
let date = new Date(d * 1000);
|
||||||
|
|
||||||
return date.toLocaleString(locale.value, { day: 'numeric', timeZone: 'utc' });
|
return date.toLocaleString(locale.value, { day: "numeric", timeZone: "utc" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMonth(d: number) {
|
export function getMonth(d: number) {
|
||||||
|
|
@ -124,47 +152,47 @@ export function getMonth( d:number ){
|
||||||
const locale = $i18n.locale;
|
const locale = $i18n.locale;
|
||||||
let date = new Date(d * 1000);
|
let date = new Date(d * 1000);
|
||||||
|
|
||||||
return date.toLocaleString(locale.value, { month: 'long', timeZone: 'utc' });
|
return date.toLocaleString(locale.value, { month: "long", timeZone: "utc" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setUrl(item: ItemObject, isSearch: boolean) {
|
export function setUrl(item: ItemObject, isSearch: boolean) {
|
||||||
const { $i18n } = useNuxtApp();
|
const { $i18n } = useNuxtApp();
|
||||||
const locale = $i18n.locale;
|
const locale = $i18n.locale;
|
||||||
const date = dayjs(item.date * 1000);
|
const date = dayjs(item.date * 1000);
|
||||||
const month = (date.month()+1).toString()
|
const month = (date.month() + 1).toString();
|
||||||
const slug = (item.slug || item.slug === 'undefined' ? item.slug : item.id);
|
const slug = item.slug || item.slug === "undefined" ? item.slug : item.id;
|
||||||
|
|
||||||
if (isSearch) {
|
if (isSearch) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return `/${locale.value}/${item.type}/${date.year()}/${month.padStart(2, '0')}/${slug}`;
|
return `/${locale.value}/${item.type}/${date.year()}/${month.padStart(2, "0")}/${slug}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getThumbnail(item: ItemObject) {
|
export function getThumbnail(item: ItemObject) {
|
||||||
console.log("ITEM", item);
|
console.log("ITEM", item);
|
||||||
let path = item.thumbnail
|
let path = item.thumbnail;
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return "https://images.carpa.com/tr:w-900,f-auto/youtube_thumbnail_46396.png"
|
return "https://images.carpa.com/tr:w-900,f-auto/youtube_thumbnail_46396.png";
|
||||||
} else {
|
} else {
|
||||||
return `https://images.carpa.com/${item.type}/${path}?tr=w-900`
|
return `https://images.carpa.com/${item.type}/${path}?tr=w-900`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAuthor(type: string) {
|
export function getAuthor(type: string) {
|
||||||
let author = ''
|
let author = "";
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "activities":
|
case "activities":
|
||||||
author = 'Dr. José Benjamín Pérez Matos'
|
author = "Dr. José Benjamín Pérez Matos";
|
||||||
break
|
break;
|
||||||
case "conferences":
|
case "conferences":
|
||||||
author = 'Dr. William Soto Santiago'
|
author = "Dr. William Soto Santiago";
|
||||||
break
|
break;
|
||||||
case "sermons":
|
case "sermons":
|
||||||
author = 'William Marrion Branham';
|
author = "William Marrion Branham";
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
return author
|
return author;
|
||||||
}
|
}
|
||||||
|
|
||||||
// removed: openItem relied on a Pinia store that is not installed in this app.
|
// removed: openItem relied on a Pinia store that is not installed in this app.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/* Copy to Clipboard */
|
/* Copy to Clipboard */
|
||||||
export async function copyToClipboard(textToCopy: string, type: string) {
|
export async function copyToClipboard(type: string) {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const selection = window.getSelection()
|
const selection = window.getSelection()
|
||||||
|
|
@ -29,3 +29,18 @@ export async function copyToClipboard(textToCopy: string, type: string) {
|
||||||
console.error('Failed to copy: ', err)
|
console.error('Failed to copy: ', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function debugDocument(json) {
|
||||||
|
let data = ''
|
||||||
|
// Iterate through keys and values
|
||||||
|
for (const key in json) {
|
||||||
|
if (json.hasOwnProperty(key)) {
|
||||||
|
data += `${key}: ${json[key]}\n\r`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addNote() {
|
||||||
|
return 'yay'
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue