|
5 | 5 | import store from "../../shared/services/store"; |
6 | 6 | import Wrap from "src/svelte/shared/components/wrap.svelte"; |
7 | 7 | import Stack from "src/svelte/shared/components/stack.svelte"; |
8 | | - import { afterUpdate, createEventDispatcher, onMount } from "svelte"; |
| 8 | + import { createEventDispatcher, onMount } from "svelte"; |
9 | 9 | import { HOVER_LINK_SOURCE_ID } from "src/constants"; |
10 | 10 | import EventManager from "src/event/event-manager"; |
11 | 11 | import Icon from "src/svelte/shared/components/icon.svelte"; |
12 | 12 | import { getIconIdForFile } from "../services/file-icon"; |
13 | 13 | import { PluginEvent } from "src/event/types"; |
14 | 14 | import { openContextMenu } from "../services/context-menu"; |
15 | 15 | import { openInCurrentTab } from "../services/open-file"; |
16 | | - import Flex from "src/svelte/shared/components/flex.svelte"; |
17 | 16 | import { getDomainFromUrl } from "../services/utils/url-utils"; |
18 | 17 | import Spacer from "src/svelte/shared/components/spacer.svelte"; |
19 | 18 | import Divider from "src/svelte/shared/components/divider.svelte"; |
|
25 | 24 | } from "../services/social-media-image-cache"; |
26 | 25 | import { CoverImageFit } from "src/types"; |
27 | 26 |
|
| 27 | + type SocialMediaImageResult = { |
| 28 | + status: "SUCCESS" | "NOT_FOUND" | "EXPIRED" | "NO_IMAGE"; |
| 29 | + url?: string; // Only present when status is 'SUCCESS' |
| 30 | + }; |
| 31 | +
|
28 | 32 | export let displayName: string; |
29 | 33 | export let path: string; |
30 | 34 | export let baseName: string; |
|
40 | 44 | let plugin: VaultExplorerPlugin; |
41 | 45 | let enableFileIcons: boolean = false; |
42 | 46 | let loadSocialMediaImage: boolean = true; |
43 | | - let imgSrc: string | null; |
44 | | -
|
45 | | - let isCoverImageLoaded = false; |
| 47 | + let imgSrc: string | null = null; |
| 48 | + let isImageLoaded = false; |
46 | 49 |
|
47 | 50 | store.plugin.subscribe((p) => { |
48 | 51 | plugin = p; |
|
52 | 55 |
|
53 | 56 | const dispatch = createEventDispatcher(); |
54 | 57 |
|
55 | | - let renderKey = 0; |
56 | | -
|
57 | | - onMount(() => { |
58 | | - updateImgSrc(); |
59 | | - }); |
60 | | -
|
61 | | - afterUpdate(() => { |
62 | | - if (imageUrl === null) { |
63 | | - imgSrc = null; |
64 | | - } else { |
65 | | - updateImgSrc(); |
66 | | - } |
67 | | - }); |
68 | | -
|
69 | 58 | onMount(() => { |
70 | 59 | function handleLoadSocialMediaImageChange() { |
71 | 60 | const newValue = plugin.settings.views.grid.loadSocialMediaImage; |
72 | 61 | loadSocialMediaImage = newValue; |
73 | | - renderKey++; |
74 | 62 | } |
75 | 63 |
|
76 | 64 | EventManager.getInstance().on( |
|
102 | 90 | }; |
103 | 91 | }); |
104 | 92 |
|
105 | | - async function updateImgSrc() { |
106 | | - if (imageUrl !== null) { |
107 | | - const entry = await getSocialMediaImageEntry(imageUrl); |
108 | | - if (entry) { |
109 | | - const isExpired = await isSocialMediaImageEntryExpired(entry); |
110 | | - if (!isExpired) { |
111 | | - const socialUrl = entry.socialMediaImageUrl; |
112 | | -
|
113 | | - //The url is null when the social media image does not exist |
114 | | - //This will always happen for sites like Twitter (x.com) |
115 | | - //To avoid fetching the same non-existent image, we will set imgSrc to null |
116 | | - if (socialUrl === null) { |
117 | | - imgSrc = null; |
118 | | - } else { |
119 | | - imgSrc = socialUrl; |
120 | | - } |
121 | | - return; |
122 | | - } |
123 | | - } |
124 | | - imgSrc = imageUrl; |
125 | | - } |
126 | | - } |
127 | | -
|
128 | 93 | function handleUrlClick(e: Event) { |
129 | 94 | e.stopPropagation(); |
130 | 95 | } |
|
164 | 129 | handleCardClick(); |
165 | 130 | } |
166 | 131 |
|
167 | | - $: hasFooterContent = |
168 | | - tags != null || custom1 != null || custom2 != null || custom3 != null; |
| 132 | + function handleImageLoad() { |
| 133 | + isImageLoaded = true; |
| 134 | + } |
169 | 135 |
|
170 | 136 | async function handleImageError(event: Event) { |
171 | 137 | const target = event.target as HTMLImageElement; |
172 | 138 | target.onerror = null; // Prevent infinite loop |
173 | 139 |
|
174 | | - if (!imgSrc) return; |
| 140 | + let websiteUrl = target.src; |
| 141 | + if (websiteUrl.endsWith("/")) { |
| 142 | + websiteUrl = websiteUrl.slice(0, -1); // Remove the trailing slash |
| 143 | + } |
175 | 144 |
|
176 | 145 | if (loadSocialMediaImage) { |
177 | | - const socialUrl = await fetchSocialMediaImage(imgSrc); |
| 146 | + const socialUrl = await fetchSocialMediaImage(websiteUrl); |
178 | 147 | if (socialUrl) { |
179 | | - putSocialMediaImageUrl(imgSrc, socialUrl); |
| 148 | + await putSocialMediaImageUrl(websiteUrl, socialUrl); |
180 | 149 | target.src = socialUrl; |
181 | 150 | } else { |
182 | | - putSocialMediaImageUrl(imgSrc, null); |
| 151 | + await putSocialMediaImageUrl(websiteUrl, null); |
183 | 152 | } |
184 | 153 | } |
185 | 154 | } |
186 | 155 |
|
187 | | - function handleImageLoad() { |
188 | | - isCoverImageLoaded = true; |
| 156 | + async function getCachedSocialMediaImageUrl( |
| 157 | + websiteUrl: string, |
| 158 | + ): Promise<SocialMediaImageResult> { |
| 159 | + const entry = await getSocialMediaImageEntry(websiteUrl); |
| 160 | +
|
| 161 | + if (entry) { |
| 162 | + const { socialMediaImageUrl } = entry; |
| 163 | +
|
| 164 | + if (socialMediaImageUrl) { |
| 165 | + const isExpired = await isSocialMediaImageEntryExpired(entry); |
| 166 | + if (!isExpired) { |
| 167 | + return { status: "SUCCESS", url: socialMediaImageUrl }; |
| 168 | + } else { |
| 169 | + return { status: "EXPIRED" }; // Image found but expired |
| 170 | + } |
| 171 | + } else { |
| 172 | + return { status: "NO_IMAGE" }; // Social image was fetched but doesn't exist |
| 173 | + } |
| 174 | + } |
| 175 | +
|
| 176 | + return { status: "NOT_FOUND" }; // Image not cached |
189 | 177 | } |
| 178 | +
|
| 179 | + $: if (imageUrl) { |
| 180 | + isImageLoaded = false; |
| 181 | + getCachedSocialMediaImageUrl(imageUrl).then((result) => { |
| 182 | + const { status, url } = result; |
| 183 | + if (status === "SUCCESS") { |
| 184 | + imgSrc = url!; |
| 185 | + } else if (status === "EXPIRED" || status === "NOT_FOUND") { |
| 186 | + imgSrc = imageUrl; |
| 187 | + } else if (status === "NO_IMAGE") { |
| 188 | + //Do nothing |
| 189 | + //This is for websites like x.com where the social image is not found |
| 190 | + //We don't want to keep trying to fetch the image |
| 191 | + } |
| 192 | + }); |
| 193 | + } |
| 194 | +
|
| 195 | + $: hasFooterContent = |
| 196 | + tags != null || custom1 != null || custom2 != null || custom3 != null; |
190 | 197 | </script> |
191 | 198 |
|
192 | 199 | <div |
|
207 | 214 | on:mouseover={handleCardMouseOver} |
208 | 215 | > |
209 | 216 | <div class="vault-explorer-grid-card__cover"> |
210 | | - {#each [renderKey] as k (k)} |
211 | | - {#if imgSrc !== null} |
212 | | - <!-- svelte-ignore a11y-missing-attribute --> |
213 | | - <img |
214 | | - class="vault-explorer-grid-card__image" |
215 | | - src={imgSrc} |
216 | | - style="display: {isCoverImageLoaded |
217 | | - ? 'block' |
218 | | - : 'none'}; object-fit: {coverImageFit};" |
219 | | - on:load={handleImageLoad} |
220 | | - on:error={handleImageError} |
221 | | - /> |
222 | | - {/if} |
223 | | - {/each} |
| 217 | + {#if imgSrc !== null} |
| 218 | + <!-- svelte-ignore a11y-missing-attribute --> |
| 219 | + <img |
| 220 | + class="vault-explorer-grid-card__image" |
| 221 | + src={imgSrc} |
| 222 | + style="display: {isImageLoaded |
| 223 | + ? 'block' |
| 224 | + : 'none'}; object-fit: {coverImageFit};" |
| 225 | + on:load={handleImageLoad} |
| 226 | + on:error={handleImageError} |
| 227 | + /> |
| 228 | + {/if} |
224 | 229 | {#if imageUrl === null} |
225 | 230 | <div class="vault-explorer-grid-card__image"></div> |
226 | 231 | {/if} |
227 | | - <!-- {#if isFavorite === true} |
228 | | - <div class="vault-explorer-grid-card__favorite"> |
229 | | - <Flex |
230 | | - justify="center" |
231 | | - align="center" |
232 | | - width="100%" |
233 | | - height="100%" |
234 | | - > |
235 | | - <Icon iconId="star" ariaLabel="Favorite" /> |
236 | | - </Flex> |
237 | | - </div> |
238 | | - {/if} --> |
239 | 232 | </div> |
240 | 233 | <div class="vault-explorer-grid-card__body"> |
241 | 234 | <div |
|
341 | 334 | border-top-right-radius: var(--radius-m); |
342 | 335 | } |
343 | 336 |
|
344 | | - /* .vault-explorer-grid-card__favorite { |
345 | | - position: absolute; |
346 | | - top: 8px; |
347 | | - right: 8px; |
348 | | - width: 20px; |
349 | | - height: 20px; |
350 | | - background-color: var(--background-primary); |
351 | | - border-radius: 50%; |
352 | | - } */ |
353 | | -
|
354 | 337 | .vault-explorer-grid-card__body { |
355 | 338 | padding: 8px 16px; |
356 | 339 | } |
|
0 commit comments