mirror of
https://github.com/elk-zone/elk.git
synced 2024-11-07 09:29:59 +00:00
feat: preview multiple images
This commit is contained in:
parent
568a333d7c
commit
cf7cd1fd6c
7 changed files with 92 additions and 23 deletions
|
@ -45,11 +45,21 @@ function getFieldNameIcon(fieldName: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewHeader() {
|
function previewHeader() {
|
||||||
openImagePreviewDialog({ src: account.header, alt: `${account.username}'s profile header` })
|
openMediaPreview([{
|
||||||
|
id: `${account.acct}:header`,
|
||||||
|
type: 'image',
|
||||||
|
previewUrl: account.header,
|
||||||
|
description: `${account.username}'s profile header`,
|
||||||
|
}])
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewAvatar() {
|
function previewAvatar() {
|
||||||
openImagePreviewDialog({ src: account.avatar, alt: account.username })
|
openMediaPreview([{
|
||||||
|
id: `${account.acct}:avatar`,
|
||||||
|
type: 'image',
|
||||||
|
previewUrl: account.avatar,
|
||||||
|
description: `${account.username}'s avatar`,
|
||||||
|
}])
|
||||||
}
|
}
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
isEditHistoryDialogOpen,
|
isEditHistoryDialogOpen,
|
||||||
isImagePreviewDialogOpen,
|
isMediaPreviewOpen,
|
||||||
isPreviewHelpOpen,
|
isPreviewHelpOpen,
|
||||||
isPublishDialogOpen,
|
isPublishDialogOpen,
|
||||||
isSigninDialogOpen,
|
isSigninDialogOpen,
|
||||||
|
@ -18,8 +18,8 @@ import {
|
||||||
<ModalDialog v-model="isPublishDialogOpen">
|
<ModalDialog v-model="isPublishDialogOpen">
|
||||||
<PublishWidget :draft-key="dialogDraftKey" expanded min-w-180 />
|
<PublishWidget :draft-key="dialogDraftKey" expanded min-w-180 />
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
<ModalDialog v-model="isImagePreviewDialogOpen" type="preview">
|
<ModalDialog v-model="isMediaPreviewOpen" close-button>
|
||||||
<img :src="imagePreview.src" :alt="imagePreview.alt" max-w-95vw max-h-95vh>
|
<ModalMediaPreview v-if="isMediaPreviewOpen" @close="closeMediaPreview()" />
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
<ModalDialog v-model="isEditHistoryDialogOpen">
|
<ModalDialog v-model="isEditHistoryDialogOpen">
|
||||||
<StatusEditPreview :edit="statusEdit" />
|
<StatusEditPreview :edit="statusEdit" />
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
||||||
|
|
||||||
type DialogType = 'top' | 'right' | 'bottom' | 'left' | 'dialog' | 'preview'
|
type DialogType = 'top' | 'right' | 'bottom' | 'left' | 'dialog'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
type = 'dialog',
|
type = 'dialog',
|
||||||
|
closeButton = false,
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
type?: DialogType
|
type?: DialogType
|
||||||
|
closeButton?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { modelValue } = defineModel<{
|
const { modelValue } = defineModel<{
|
||||||
|
@ -21,8 +23,6 @@ const positionClass = computed(() => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'dialog':
|
case 'dialog':
|
||||||
return 'border rounded top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2'
|
return 'border rounded top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2'
|
||||||
case 'preview':
|
|
||||||
return 'border rounded top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2'
|
|
||||||
case 'bottom':
|
case 'bottom':
|
||||||
return 'bottom-0 left-0 right-0 border-t'
|
return 'bottom-0 left-0 right-0 border-t'
|
||||||
case 'top':
|
case 'top':
|
||||||
|
@ -41,8 +41,6 @@ const transformClass = computed(() => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'dialog':
|
case 'dialog':
|
||||||
return 'op0'
|
return 'op0'
|
||||||
case 'preview':
|
|
||||||
return 'op0'
|
|
||||||
case 'bottom':
|
case 'bottom':
|
||||||
return 'translate-y-[100%]'
|
return 'translate-y-[100%]'
|
||||||
case 'top':
|
case 'top':
|
||||||
|
@ -123,7 +121,13 @@ function onTransitionEnd() {
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<button v-if="type === 'preview'" btn-action-icon bg="black/20" aria-label="Close" hover:bg="black/40" dark:bg="white/10" dark:hover:bg="white/20" absolute top-0 right-0 m1 @click="close">
|
<button
|
||||||
|
v-if="closeButton"
|
||||||
|
btn-action-icon bg="black/20" aria-label="Close"
|
||||||
|
hover:bg="black/40" dark:bg="white/10" dark:hover:bg="white/20"
|
||||||
|
absolute top-0 right-0 m1
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
<div i-ri:close-fill text-white />
|
<div i-ri:close-fill text-white />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
44
components/modal/ModalMediaPreview.vue
Normal file
44
components/modal/ModalMediaPreview.vue
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const current = computed(() => mediaPreviewList.value[mediaPreviewIndex.value])
|
||||||
|
const hasNext = computed(() => mediaPreviewIndex.value < mediaPreviewList.value.length - 1)
|
||||||
|
const hasPrev = computed(() => mediaPreviewIndex.value > 0)
|
||||||
|
|
||||||
|
const keys = useMagicKeys()
|
||||||
|
|
||||||
|
whenever(keys.arrowLeft, prev)
|
||||||
|
whenever(keys.arrowRight, next)
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
if (hasNext.value)
|
||||||
|
mediaPreviewIndex.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
function prev() {
|
||||||
|
if (hasPrev.value)
|
||||||
|
mediaPreviewIndex.value--
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(e: MouseEvent) {
|
||||||
|
const path = e.composedPath() as HTMLElement[]
|
||||||
|
const el = path.find(el => ['A', 'BUTTON', 'IMG', 'VIDEO'].includes(el.tagName?.toUpperCase()))
|
||||||
|
if (!el)
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div relative h-screen w-screen flex select-none @click="onClick">
|
||||||
|
<div absolute top-0 left-0 right-0 text-center>
|
||||||
|
{{ mediaPreviewIndex + 1 }} / {{ mediaPreviewList.length }}
|
||||||
|
</div>
|
||||||
|
<button v-if="hasNext" btn-action-icon absolute top="1/2" right-1 title="Next" @click="next">
|
||||||
|
<div i-ri:arrow-right-s-line />
|
||||||
|
</button>
|
||||||
|
<button v-if="hasPrev" btn-action-icon absolute top="1/2" left-1 title="Next" @click="prev">
|
||||||
|
<div i-ri:arrow-left-s-line />
|
||||||
|
</button>
|
||||||
|
<img :src="current.url || current.previewUrl" :alt="current.description || ''" max-w-95vw max-h-95vh ma>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -4,6 +4,7 @@ import type { Attachment } from 'masto'
|
||||||
|
|
||||||
const { attachment } = defineProps<{
|
const { attachment } = defineProps<{
|
||||||
attachment: Attachment
|
attachment: Attachment
|
||||||
|
attachments?: Attachment[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const src = $computed(() => attachment.remoteUrl || attachment.url || attachment.previewUrl!)
|
const src = $computed(() => attachment.remoteUrl || attachment.url || attachment.previewUrl!)
|
||||||
|
@ -62,10 +63,7 @@ const aspectRatio = computed(() => {
|
||||||
focus:ring="2 primary inset"
|
focus:ring="2 primary inset"
|
||||||
rounded-lg
|
rounded-lg
|
||||||
aria-label="Open image preview dialog"
|
aria-label="Open image preview dialog"
|
||||||
@click="openImagePreviewDialog({
|
@click="openMediaPreview(attachments ? attachments : [attachment], attachments?.indexOf(attachment) || 0)"
|
||||||
src,
|
|
||||||
alt: attachment.description!,
|
|
||||||
})"
|
|
||||||
>
|
>
|
||||||
<CommonBlurhash
|
<CommonBlurhash
|
||||||
:blurhash="attachment.blurhash"
|
:blurhash="attachment.blurhash"
|
||||||
|
|
|
@ -9,7 +9,11 @@ const { status } = defineProps<{
|
||||||
<template>
|
<template>
|
||||||
<div class="status-media-container" :class="`status-media-container-${status.mediaAttachments.length}`">
|
<div class="status-media-container" :class="`status-media-container-${status.mediaAttachments.length}`">
|
||||||
<template v-for="attachment of status.mediaAttachments" :key="attachment.id">
|
<template v-for="attachment of status.mediaAttachments" :key="attachment.id">
|
||||||
<StatusAttachment :attachment="attachment" class="w-full h-full" />
|
<StatusAttachment
|
||||||
|
:attachment="attachment"
|
||||||
|
:attachments="status.mediaAttachments"
|
||||||
|
class="w-full h-full"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
import type { StatusEdit } from 'masto'
|
import type { Attachment, StatusEdit } from 'masto'
|
||||||
import type { Draft } from './statusDrafts'
|
import type { Draft } from './statusDrafts'
|
||||||
import { STORAGE_KEY_FIRST_VISIT, STORAGE_KEY_ZEN_MODE } from '~/constants'
|
import { STORAGE_KEY_FIRST_VISIT, STORAGE_KEY_ZEN_MODE } from '~/constants'
|
||||||
|
|
||||||
export const imagePreview = ref({ src: '', alt: '' })
|
export const mediaPreviewList = ref<Attachment[]>([])
|
||||||
|
export const mediaPreviewIndex = ref(0)
|
||||||
|
|
||||||
export const statusEdit = ref<StatusEdit>()
|
export const statusEdit = ref<StatusEdit>()
|
||||||
export const dialogDraftKey = ref<string>()
|
export const dialogDraftKey = ref<string>()
|
||||||
|
|
||||||
export const isFirstVisit = useLocalStorage(STORAGE_KEY_FIRST_VISIT, !process.mock)
|
export const isFirstVisit = useLocalStorage(STORAGE_KEY_FIRST_VISIT, !process.mock)
|
||||||
export const isZenMode = useLocalStorage(STORAGE_KEY_ZEN_MODE, false)
|
export const isZenMode = useLocalStorage(STORAGE_KEY_ZEN_MODE, false)
|
||||||
export const toggleZenMode = useToggle(isZenMode)
|
|
||||||
|
|
||||||
export const isSigninDialogOpen = ref(false)
|
export const isSigninDialogOpen = ref(false)
|
||||||
export const isPublishDialogOpen = ref(false)
|
export const isPublishDialogOpen = ref(false)
|
||||||
export const isImagePreviewDialogOpen = ref(false)
|
export const isMediaPreviewOpen = ref(false)
|
||||||
export const isEditHistoryDialogOpen = ref(false)
|
export const isEditHistoryDialogOpen = ref(false)
|
||||||
export const isPreviewHelpOpen = ref(isFirstVisit.value)
|
export const isPreviewHelpOpen = ref(isFirstVisit.value)
|
||||||
|
|
||||||
|
export const toggleZenMode = useToggle(isZenMode)
|
||||||
|
|
||||||
export function openSigninDialog() {
|
export function openSigninDialog() {
|
||||||
isSigninDialogOpen.value = true
|
isSigninDialogOpen.value = true
|
||||||
}
|
}
|
||||||
|
@ -46,9 +50,14 @@ if (isPreviewHelpOpen.value) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openImagePreviewDialog(image: { src: string; alt: string }) {
|
export function openMediaPreview(attachments: Attachment[], index = 0) {
|
||||||
imagePreview.value = image
|
mediaPreviewList.value = attachments
|
||||||
isImagePreviewDialogOpen.value = true
|
mediaPreviewIndex.value = index
|
||||||
|
isMediaPreviewOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeMediaPreview() {
|
||||||
|
isMediaPreviewOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openEditHistoryDialog(edit: StatusEdit) {
|
export function openEditHistoryDialog(edit: StatusEdit) {
|
||||||
|
|
Loading…
Reference in a new issue