@bagelink/vue 1.15.78 → 1.15.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Card.vue.d.ts +8 -0
- package/dist/components/Card.vue.d.ts.map +1 -1
- package/dist/components/Image.vue.d.ts +0 -5
- package/dist/components/Image.vue.d.ts.map +1 -1
- package/dist/components/PageTitle.vue.d.ts +10 -0
- package/dist/components/PageTitle.vue.d.ts.map +1 -1
- package/dist/components/RouterWrapper.vue.d.ts +6 -2
- package/dist/components/RouterWrapper.vue.d.ts.map +1 -1
- package/dist/components/calendar/CalendarPopover.vue.d.ts +18 -0
- package/dist/components/calendar/CalendarPopover.vue.d.ts.map +1 -1
- package/dist/components/layout/AppContent.vue.d.ts +2 -0
- package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
- package/dist/components/layout/AppSidebar.vue.d.ts +5 -0
- package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
- package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
- package/dist/components/lightbox/LightboxImage.vue.d.ts +6 -0
- package/dist/components/lightbox/LightboxImage.vue.d.ts.map +1 -0
- package/dist/components/lightbox/index.d.ts.map +1 -1
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useImageSrc.d.ts +20 -0
- package/dist/composables/useImageSrc.d.ts.map +1 -0
- package/dist/index.cjs +34 -34
- package/dist/index.mjs +5102 -5057
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Card.vue +22 -2
- package/src/components/Image.vue +3 -79
- package/src/components/PageTitle.vue +14 -28
- package/src/components/RouterWrapper.vue +22 -0
- package/src/components/layout/AppContent.vue +3 -1
- package/src/components/layout/AppSidebar.vue +8 -3
- package/src/components/lightbox/Lightbox.vue +2 -1
- package/src/components/lightbox/LightboxImage.vue +14 -0
- package/src/components/lightbox/index.ts +4 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/useImageSrc.ts +88 -0
- package/src/utils/index.ts +1 -1
package/package.json
CHANGED
package/src/components/Card.vue
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
defineOptions({ name: 'BglCard' })
|
|
3
|
+
import type { GradientDirProp, GradientProp } from '@bagelink/vue'
|
|
3
4
|
import { computed } from 'vue'
|
|
5
|
+
import { useGradientVariant } from '@bagelink/vue'
|
|
4
6
|
|
|
5
7
|
const props = defineProps<{
|
|
6
8
|
label?: string
|
|
@@ -17,6 +19,13 @@ const props = defineProps<{
|
|
|
17
19
|
overflowX?: boolean
|
|
18
20
|
overflowY?: boolean
|
|
19
21
|
frame?: boolean
|
|
22
|
+
/** Theme tone used as the gradient's first stop (e.g. `color="green"`). */
|
|
23
|
+
color?: string
|
|
24
|
+
/** Paint the card with a gradient. `true` = auto (a darker shade of `color`);
|
|
25
|
+
a tone list like "green" or "blue purple pink" sets explicit stops. */
|
|
26
|
+
gradient?: GradientProp
|
|
27
|
+
/** Gradient direction — a named dir (`to-br`) or an angle in degrees. */
|
|
28
|
+
gradientDir?: GradientDirProp
|
|
20
29
|
bg?:
|
|
21
30
|
| 'gray'
|
|
22
31
|
| 'light'
|
|
@@ -31,6 +40,16 @@ const props = defineProps<{
|
|
|
31
40
|
| 'transparent'
|
|
32
41
|
}>()
|
|
33
42
|
|
|
43
|
+
const { isGradient, gradientStyle } = useGradientVariant({
|
|
44
|
+
gradient: () => props.gradient,
|
|
45
|
+
gradientDir: () => props.gradientDir,
|
|
46
|
+
color: () => props.color,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// A `pair-*` class is required to activate the `.gradient` paint rule. Mirror
|
|
50
|
+
// Btn: default to the auto case (primary) when no color is given.
|
|
51
|
+
const pairClass = computed(() => isGradient.value ? `pair-${props.color || 'primary'}` : undefined)
|
|
52
|
+
|
|
34
53
|
const bind = computed(() => {
|
|
35
54
|
const obj: { [key: string]: any } = {}
|
|
36
55
|
if (props.href !== undefined && props.href !== '') { obj.href = props.href }
|
|
@@ -47,7 +66,7 @@ const is = computed(() => {
|
|
|
47
66
|
|
|
48
67
|
<template>
|
|
49
68
|
<component
|
|
50
|
-
:is="is" v-ripple="!!to" v-bind="bind" class="bgl_card" :class="{
|
|
69
|
+
:is="is" v-ripple="!!to" v-bind="bind" class="bgl_card" :class="[{
|
|
51
70
|
thin,
|
|
52
71
|
'border bg-transparent': outline,
|
|
53
72
|
'h-100': h100,
|
|
@@ -55,7 +74,8 @@ const is = computed(() => {
|
|
|
55
74
|
'overflow-x': overflowX,
|
|
56
75
|
'overflow-y': overflowY,
|
|
57
76
|
'card_frame': frame,
|
|
58
|
-
|
|
77
|
+
'gradient': isGradient,
|
|
78
|
+
}, pairClass]" :style="gradientStyle"
|
|
59
79
|
>
|
|
60
80
|
<span v-if="label" class="card_label block label">
|
|
61
81
|
{{ label }}
|
package/src/components/Image.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
defineOptions({ name: 'BglImage', inheritAttrs: true })
|
|
3
|
-
import { Skeleton, normalizeDimension,
|
|
4
|
-
import { computed,
|
|
3
|
+
import { Skeleton, normalizeDimension, Icon, useImageSrc } from '@bagelink/vue'
|
|
4
|
+
import { computed, useSlots } from 'vue'
|
|
5
5
|
|
|
6
6
|
interface ImageProps {
|
|
7
7
|
src?: string
|
|
@@ -23,85 +23,9 @@ interface ImageProps {
|
|
|
23
23
|
rounded?: boolean | number
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
declare global {
|
|
27
|
-
interface Window {
|
|
28
|
-
heic2any: any
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
26
|
const props = defineProps<ImageProps>()
|
|
33
27
|
|
|
34
|
-
const imageSrc
|
|
35
|
-
const loadingError = ref(false)
|
|
36
|
-
|
|
37
|
-
function getImageUrl(): string | undefined {
|
|
38
|
-
if ((props.src === undefined || props.src === '') && (props.pathKey === undefined || props.pathKey === '') && (props.modelValue === undefined || props.modelValue === '')) { return }
|
|
39
|
-
return pathKeyToURL(props.src ?? props.pathKey ?? props.modelValue)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function getCachedImage(url: string): Promise<string | undefined> {
|
|
43
|
-
if (!('caches' in window)) { return undefined }
|
|
44
|
-
try {
|
|
45
|
-
const imgCache = await window.caches.open('img-cache')
|
|
46
|
-
const cachedResponse = await imgCache.match(url)
|
|
47
|
-
if (cachedResponse) {
|
|
48
|
-
return URL.createObjectURL(await cachedResponse.blob())
|
|
49
|
-
}
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.warn('Cache access error:', error)
|
|
52
|
-
}
|
|
53
|
-
return undefined
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function cacheImage(url: string, blob: Blob) {
|
|
57
|
-
if (!('caches' in window)) { return }
|
|
58
|
-
try {
|
|
59
|
-
const imgCache = await window.caches.open('img-cache')
|
|
60
|
-
await imgCache.put(url, new Response(blob))
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.warn('Cache write error:', error)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function convertHeicImage(url: string): Promise<string> {
|
|
67
|
-
await appendScript('https://cdnjs.cloudflare.com/ajax/libs/heic2any/0.0.1/index.min.js')
|
|
68
|
-
const heic2any = await awaitGlobal<(opts: { blob: Blob }) => Promise<Blob>>('heic2any')
|
|
69
|
-
const response = await fetch(normalizeURL(url))
|
|
70
|
-
const blob = await response.blob()
|
|
71
|
-
const convertedBlob = await heic2any({ blob })
|
|
72
|
-
await cacheImage(url, convertedBlob)
|
|
73
|
-
return URL.createObjectURL(convertedBlob)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function loadImage() {
|
|
77
|
-
loadingError.value = false
|
|
78
|
-
const url = getImageUrl()
|
|
79
|
-
if (url === undefined || url === '') {
|
|
80
|
-
imageSrc.value = undefined
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const ext = url.split('.').pop()?.toLowerCase().split('?')[0]
|
|
86
|
-
|
|
87
|
-
if (ext === 'heic') {
|
|
88
|
-
const cachedSrc = await getCachedImage(url)
|
|
89
|
-
if (cachedSrc !== undefined && cachedSrc !== '') {
|
|
90
|
-
imageSrc.value = cachedSrc
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
imageSrc.value = await convertHeicImage(url)
|
|
94
|
-
} else {
|
|
95
|
-
imageSrc.value = url
|
|
96
|
-
}
|
|
97
|
-
} catch (error) {
|
|
98
|
-
console.error('Image loading error:', error)
|
|
99
|
-
loadingError.value = true
|
|
100
|
-
imageSrc.value = undefined
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
watch(() => [props.src, props.pathKey, props.modelValue], loadImage, { immediate: true })
|
|
28
|
+
const { imageSrc, loadingError } = useImageSrc(() => props.src ?? props.pathKey ?? props.modelValue)
|
|
105
29
|
|
|
106
30
|
// ── Framed mode (ratio / gradient / blend / rounded / overlay slot) ──────────
|
|
107
31
|
const slots = useSlots()
|
|
@@ -4,36 +4,22 @@ defineProps({
|
|
|
4
4
|
type: String,
|
|
5
5
|
default: '',
|
|
6
6
|
},
|
|
7
|
-
|
|
7
|
+
subtitle: {
|
|
8
|
+
type: String,
|
|
9
|
+
default: '',
|
|
10
|
+
},
|
|
8
11
|
})
|
|
9
12
|
</script>
|
|
10
13
|
|
|
11
14
|
<template>
|
|
12
|
-
<div class="page-top">
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
|
|
15
|
+
<div class="page-top flex gap-1">
|
|
16
|
+
<div class="grid gap-025">
|
|
17
|
+
<h1 class="m-0 semibold txt-20 m_txt16 line-height-1">
|
|
18
|
+
<slot /> {{ value }}
|
|
19
|
+
</h1>
|
|
20
|
+
<p v-if="subtitle || $slots.subtitle" class="top-subtitle m-0 txt-13 m_txt12 align-items-center line-height-12 opacity-6">
|
|
21
|
+
<slot name="subtitle">{{ subtitle }}</slot>
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
16
24
|
</div>
|
|
17
|
-
</template>
|
|
18
|
-
|
|
19
|
-
<style>
|
|
20
|
-
|
|
21
|
-
.top-title {
|
|
22
|
-
font-weight: 600;
|
|
23
|
-
font-size: 20px;
|
|
24
|
-
line-height: 1;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.page-top {
|
|
28
|
-
display: flex;
|
|
29
|
-
align-items: center;
|
|
30
|
-
gap: 1rem;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@media screen and (max-width: 910px) {
|
|
34
|
-
.top-title {
|
|
35
|
-
font-size: 16px;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
</style>
|
|
25
|
+
</template>
|
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { nextTick, ref, watch } from 'vue'
|
|
3
|
+
import { useRoute } from 'vue-router'
|
|
2
4
|
import Loading from './Loading.vue'
|
|
5
|
+
|
|
6
|
+
// Reset the scroll position on every route change. The app's scroll container is
|
|
7
|
+
// the inner `.pageContent` element in AppContent (the window itself doesn't
|
|
8
|
+
// scroll), so a plain vue-router `scrollBehavior` wouldn't help. We scroll the
|
|
9
|
+
// nearest `.pageContent` ancestor back to the top after the new view mounts.
|
|
10
|
+
const anchor = ref<HTMLElement | null>(null)
|
|
11
|
+
const route = useRoute()
|
|
12
|
+
|
|
13
|
+
watch(
|
|
14
|
+
() => route.fullPath,
|
|
15
|
+
async () => {
|
|
16
|
+
await nextTick()
|
|
17
|
+
const container = anchor.value?.closest('.pageContent') as HTMLElement | null
|
|
18
|
+
if (container) container.scrollTop = 0
|
|
19
|
+
// Fallback: also reset the window in case a view scrolls the page itself.
|
|
20
|
+
else window.scrollTo({ top: 0 })
|
|
21
|
+
},
|
|
22
|
+
)
|
|
3
23
|
</script>
|
|
4
24
|
|
|
5
25
|
<template>
|
|
26
|
+
<!-- Zero-size anchor used to locate the scrolling `.pageContent` ancestor. -->
|
|
27
|
+
<span ref="anchor" aria-hidden="true" style="display: none" />
|
|
6
28
|
<RouterView v-slot="{ Component, route }">
|
|
7
29
|
<slot v-if="!Component">
|
|
8
30
|
<div class="w-100p h-100vh flex justify-content-center">
|
|
@@ -4,6 +4,8 @@ import { useAppLayout } from './appLayoutContext'
|
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
title?: string
|
|
7
|
+
/** Secondary line under the title (page description / context). */
|
|
8
|
+
subtitle?: string
|
|
7
9
|
showMenuButton?: boolean
|
|
8
10
|
backTo?: string | Record<string, any>
|
|
9
11
|
border?: boolean
|
|
@@ -41,7 +43,7 @@ const { isOpen, toggleMenu, sidebarCardStyle } = useAppLayout()
|
|
|
41
43
|
<Btn v-if="backTo" icon="arrow_back" thin :to="backTo" class="back-btn bg-bg m_-ms-05 m_-me-05 color-black" />
|
|
42
44
|
|
|
43
45
|
<!-- Page Title -->
|
|
44
|
-
<PageTitle v-if="title">
|
|
46
|
+
<PageTitle v-if="title || subtitle" :subtitle="subtitle">
|
|
45
47
|
{{ title }}
|
|
46
48
|
</PageTitle>
|
|
47
49
|
|
|
@@ -24,6 +24,8 @@ interface Props {
|
|
|
24
24
|
activeColor?: string
|
|
25
25
|
logoHeight?: string
|
|
26
26
|
name?: string
|
|
27
|
+
/** Small tagline rendered under the app name (hidden when collapsed). */
|
|
28
|
+
nameSubtitle?: string
|
|
27
29
|
frame?: boolean
|
|
28
30
|
activeRoutes?: string[]
|
|
29
31
|
centerlinks?: boolean
|
|
@@ -162,9 +164,12 @@ const sidebarStyles = computed(() => {
|
|
|
162
164
|
v-if="props.logo" :src="props.logo" :alt="props.logoAlt" class="contain"
|
|
163
165
|
:style="{ height: props.logoHeight }"
|
|
164
166
|
>
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
<slot name="brand" v-bind="{ isOpen: isVisuallyOpen }">
|
|
168
|
+
<span class="nav-text flex column line-height-1 gap-025 align-items-start">
|
|
169
|
+
<span>{{ props.name }}</span>
|
|
170
|
+
<span v-if="props.nameSubtitle" class="txt11 opacity-6">{{ props.nameSubtitle }}</span>
|
|
171
|
+
</span>
|
|
172
|
+
</slot>
|
|
168
173
|
</router-link>
|
|
169
174
|
|
|
170
175
|
<!-- Navigation Links -->
|
|
@@ -3,6 +3,7 @@ import type { LightboxItem } from './lightbox.types'
|
|
|
3
3
|
|
|
4
4
|
import { BglVideo, Btn, Icon, Zoomer, Image, normalizeURL, Swiper, downloadFile, useEscapeKey } from '@bagelink/vue'
|
|
5
5
|
import { computed, ref, watch } from 'vue'
|
|
6
|
+
import LightboxImage from './LightboxImage.vue'
|
|
6
7
|
|
|
7
8
|
const isOpen = ref(false)
|
|
8
9
|
const group = ref<LightboxItem[]>([])
|
|
@@ -88,7 +89,7 @@ defineExpose({ open, close })
|
|
|
88
89
|
<Zoomer v-if="item.type === 'image'" v-model:zoom="zoom" :disabled="!item?.enableZoom"
|
|
89
90
|
:mouse-wheel-to-zoom="false" :double-click-to-zoom="true" :max-scale="5" :min-scale="1"
|
|
90
91
|
:aspect-ratio="0" :limit-translation="true" @click.stop>
|
|
91
|
-
<
|
|
92
|
+
<LightboxImage :src="item?.src" />
|
|
92
93
|
</Zoomer>
|
|
93
94
|
|
|
94
95
|
<BglVideo v-else-if="item?.type === 'video' && item?.src" :src="item?.src" autoplay controls
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Plain <img> for the lightbox preview that still gets shared src resolution +
|
|
3
|
+
// HEIC conversion via useImageSrc. A bare <img> (not <Image>) is required here
|
|
4
|
+
// so the natural dimensions drive layout — <Image>'s framed mode renders an
|
|
5
|
+
// absolute-fill img that collapses the zoomer to 0×0.
|
|
6
|
+
import { useImageSrc } from '@bagelink/vue'
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{ src?: string }>()
|
|
9
|
+
const { imageSrc } = useImageSrc(() => props.src)
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<img :draggable="false" :src="imageSrc" alt="Preview" class="lightbox-image">
|
|
14
|
+
</template>
|
|
@@ -70,6 +70,10 @@ function urlToName(url: string): string {
|
|
|
70
70
|
|
|
71
71
|
function determineFileType(url: any): string {
|
|
72
72
|
if (typeof url !== 'string' || !url) { return 'unknown' }
|
|
73
|
+
// Extensionless sources (object URLs, data URLs, signed/CDN URLs) can't be sniffed
|
|
74
|
+
// by extension — fall back to MIME hints so they still preview as images/video.
|
|
75
|
+
if (url.startsWith('blob:') || /^data:image\//i.test(url)) { return 'image' }
|
|
76
|
+
if (/^data:video\//i.test(url)) { return 'video' }
|
|
73
77
|
const extension = (url.split('.').pop() || '').toLowerCase()
|
|
74
78
|
const altExtension = url.split('?')[0].split('.').pop()?.toLowerCase() || ''
|
|
75
79
|
if (IMAGE_FORMATS_REGEXP.test(extension) || IMAGE_FORMATS_REGEXP.test(altExtension)) { return 'image' }
|
package/src/composables/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { useEscapeKey } from './useEscapeKey'
|
|
|
10
10
|
export { useExcel } from './useExcel'
|
|
11
11
|
export type { GradientDir, GradientDirProp, GradientProp } from './useGradientVariant'
|
|
12
12
|
export { useGradientVariant } from './useGradientVariant'
|
|
13
|
+
export { useImageSrc } from './useImageSrc'
|
|
13
14
|
export { useLocalStore } from './useLocalStore'
|
|
14
15
|
export { usePolling } from './usePolling'
|
|
15
16
|
export { useQuery } from './useQuery'
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
2
|
+
import { appendScript, awaitGlobal, normalizeURL, pathKeyToURL } from '@bagelink/vue'
|
|
3
|
+
import { ref, toValue, watch } from 'vue'
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
heic2any: any
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function getCachedImage(url: string): Promise<string | undefined> {
|
|
12
|
+
if (!('caches' in window)) { return undefined }
|
|
13
|
+
try {
|
|
14
|
+
const imgCache = await window.caches.open('img-cache')
|
|
15
|
+
const cachedResponse = await imgCache.match(url)
|
|
16
|
+
if (cachedResponse) {
|
|
17
|
+
return URL.createObjectURL(await cachedResponse.blob())
|
|
18
|
+
}
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.warn('Cache access error:', error)
|
|
21
|
+
}
|
|
22
|
+
return undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function cacheImage(url: string, blob: Blob) {
|
|
26
|
+
if (!('caches' in window)) { return }
|
|
27
|
+
try {
|
|
28
|
+
const imgCache = await window.caches.open('img-cache')
|
|
29
|
+
await imgCache.put(url, new Response(blob))
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.warn('Cache write error:', error)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function convertHeicImage(url: string): Promise<string> {
|
|
36
|
+
await appendScript('https://cdnjs.cloudflare.com/ajax/libs/heic2any/0.0.1/index.min.js')
|
|
37
|
+
const heic2any = await awaitGlobal<(opts: { blob: Blob }) => Promise<Blob>>('heic2any')
|
|
38
|
+
const response = await fetch(normalizeURL(url))
|
|
39
|
+
const blob = await response.blob()
|
|
40
|
+
const convertedBlob = await heic2any({ blob })
|
|
41
|
+
await cacheImage(url, convertedBlob)
|
|
42
|
+
return URL.createObjectURL(convertedBlob)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve an image source for display: turns a pathKey/URL into a usable URL via
|
|
47
|
+
* `pathKeyToURL`, and transparently converts `.heic` files to a displayable blob
|
|
48
|
+
* (with a cache) since browsers can't render HEIC in <img>. Shared by <Image>
|
|
49
|
+
* and the lightbox so both get the same resolution + HEIC handling.
|
|
50
|
+
*
|
|
51
|
+
* @param source ref/getter of the raw src or pathKey
|
|
52
|
+
* @returns `{ imageSrc, loadingError }` reactive refs
|
|
53
|
+
*/
|
|
54
|
+
export function useImageSrc(source: MaybeRefOrGetter<string | undefined>) {
|
|
55
|
+
const imageSrc = ref<string | undefined>(undefined)
|
|
56
|
+
const loadingError = ref(false)
|
|
57
|
+
|
|
58
|
+
async function load() {
|
|
59
|
+
loadingError.value = false
|
|
60
|
+
const url = pathKeyToURL(toValue(source))
|
|
61
|
+
if (url === undefined || url === '') {
|
|
62
|
+
imageSrc.value = undefined
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const ext = url.split('.').pop()?.toLowerCase().split('?')[0]
|
|
68
|
+
if (ext === 'heic') {
|
|
69
|
+
const cachedSrc = await getCachedImage(url)
|
|
70
|
+
if (cachedSrc !== undefined && cachedSrc !== '') {
|
|
71
|
+
imageSrc.value = cachedSrc
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
imageSrc.value = await convertHeicImage(url)
|
|
75
|
+
} else {
|
|
76
|
+
imageSrc.value = url
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Image loading error:', error)
|
|
80
|
+
loadingError.value = true
|
|
81
|
+
imageSrc.value = undefined
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
watch(() => toValue(source), load, { immediate: true })
|
|
86
|
+
|
|
87
|
+
return { imageSrc, loadingError }
|
|
88
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -257,7 +257,7 @@ export type { ComparisonOperator, FilterCondition, LogicalOperator, QueryConditi
|
|
|
257
257
|
export { anyOf, buildQuery, evaluateQuery, parseQuery, queryFilter, range, search } from './queryFilter'
|
|
258
258
|
export type { ShowdownConverter, ShowdownOptions } from './showdown'
|
|
259
259
|
|
|
260
|
-
const URL_REGEX = /^https
|
|
260
|
+
const URL_REGEX = /^https?:\/\/|^\/\/|^blob:|^data:/
|
|
261
261
|
|
|
262
262
|
export function pathKeyToURL(pathKey?: string | null): string | undefined {
|
|
263
263
|
if (pathKey == null || pathKey === '') {
|