Skip to content

Commit 0e48aee

Browse files
authored
Merge pull request #335 from decaf-dev/fix-broken-image
fix: hide broken image if image fails to load
2 parents 5267276 + 7ae81bf commit 0e48aee

File tree

3 files changed

+72
-97
lines changed

3 files changed

+72
-97
lines changed

src/svelte/app/components/grid-card.svelte

Lines changed: 70 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@
55
import store from "../../shared/services/store";
66
import Wrap from "src/svelte/shared/components/wrap.svelte";
77
import Stack from "src/svelte/shared/components/stack.svelte";
8-
import { afterUpdate, createEventDispatcher, onMount } from "svelte";
8+
import { createEventDispatcher, onMount } from "svelte";
99
import { HOVER_LINK_SOURCE_ID } from "src/constants";
1010
import EventManager from "src/event/event-manager";
1111
import Icon from "src/svelte/shared/components/icon.svelte";
1212
import { getIconIdForFile } from "../services/file-icon";
1313
import { PluginEvent } from "src/event/types";
1414
import { openContextMenu } from "../services/context-menu";
1515
import { openInCurrentTab } from "../services/open-file";
16-
import Flex from "src/svelte/shared/components/flex.svelte";
1716
import { getDomainFromUrl } from "../services/utils/url-utils";
1817
import Spacer from "src/svelte/shared/components/spacer.svelte";
1918
import Divider from "src/svelte/shared/components/divider.svelte";
@@ -25,6 +24,11 @@
2524
} from "../services/social-media-image-cache";
2625
import { CoverImageFit } from "src/types";
2726
27+
type SocialMediaImageResult = {
28+
status: "SUCCESS" | "NOT_FOUND" | "EXPIRED" | "NO_IMAGE";
29+
url?: string; // Only present when status is 'SUCCESS'
30+
};
31+
2832
export let displayName: string;
2933
export let path: string;
3034
export let baseName: string;
@@ -40,9 +44,8 @@
4044
let plugin: VaultExplorerPlugin;
4145
let enableFileIcons: boolean = false;
4246
let loadSocialMediaImage: boolean = true;
43-
let imgSrc: string | null;
44-
45-
let isCoverImageLoaded = false;
47+
let imgSrc: string | null = null;
48+
let isImageLoaded = false;
4649
4750
store.plugin.subscribe((p) => {
4851
plugin = p;
@@ -52,25 +55,10 @@
5255
5356
const dispatch = createEventDispatcher();
5457
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-
6958
onMount(() => {
7059
function handleLoadSocialMediaImageChange() {
7160
const newValue = plugin.settings.views.grid.loadSocialMediaImage;
7261
loadSocialMediaImage = newValue;
73-
renderKey++;
7462
}
7563
7664
EventManager.getInstance().on(
@@ -102,29 +90,6 @@
10290
};
10391
});
10492
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-
12893
function handleUrlClick(e: Event) {
12994
e.stopPropagation();
13095
}
@@ -164,29 +129,71 @@
164129
handleCardClick();
165130
}
166131
167-
$: hasFooterContent =
168-
tags != null || custom1 != null || custom2 != null || custom3 != null;
132+
function handleImageLoad() {
133+
isImageLoaded = true;
134+
}
169135
170136
async function handleImageError(event: Event) {
171137
const target = event.target as HTMLImageElement;
172138
target.onerror = null; // Prevent infinite loop
173139
174-
if (!imgSrc) return;
140+
let websiteUrl = target.src;
141+
if (websiteUrl.endsWith("/")) {
142+
websiteUrl = websiteUrl.slice(0, -1); // Remove the trailing slash
143+
}
175144
176145
if (loadSocialMediaImage) {
177-
const socialUrl = await fetchSocialMediaImage(imgSrc);
146+
const socialUrl = await fetchSocialMediaImage(websiteUrl);
178147
if (socialUrl) {
179-
putSocialMediaImageUrl(imgSrc, socialUrl);
148+
await putSocialMediaImageUrl(websiteUrl, socialUrl);
180149
target.src = socialUrl;
181150
} else {
182-
putSocialMediaImageUrl(imgSrc, null);
151+
await putSocialMediaImageUrl(websiteUrl, null);
183152
}
184153
}
185154
}
186155
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
189177
}
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;
190197
</script>
191198

192199
<div
@@ -207,35 +214,21 @@
207214
on:mouseover={handleCardMouseOver}
208215
>
209216
<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}
224229
{#if imageUrl === null}
225230
<div class="vault-explorer-grid-card__image"></div>
226231
{/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} -->
239232
</div>
240233
<div class="vault-explorer-grid-card__body">
241234
<div
@@ -341,16 +334,6 @@
341334
border-top-right-radius: var(--radius-m);
342335
}
343336
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-
354337
.vault-explorer-grid-card__body {
355338
padding: 8px 16px;
356339
}

src/svelte/app/services/fetch-social-media-image.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,8 @@ export const fetchSocialMediaImage = async (url: string) => {
2323

2424
const ogImage = getMetaTagContent(document, "og:image");
2525
const twitterImage = getMetaTagContent(document, "twitter:image");
26-
// const maskIcon = getLinkTagContent(document, "mask-icon");
27-
// const appleTouchIcon = getLinkTagContent(document, "apple-touch-icon");
2826

2927
let imageUrl = ogImage || twitterImage;
30-
// || appleTouchIcon || maskIcon;
3128

3229
if (imageUrl) {
3330
//Handle edge case where social media image URL has slashes at the beginning
@@ -84,11 +81,6 @@ export const fetchSocialMediaImage = async (url: string) => {
8481
}
8582
};
8683

87-
// const getLinkTagContent = (document: Document, property: string) => {
88-
// const tag = document.querySelector(`link[rel='${property}']`);
89-
// return tag ? tag.getAttribute("href") : null;
90-
// };
91-
9284
const getMetaTagContent = (document: Document, property: string) => {
9385
const tag =
9486
document.querySelector(`meta[property='${property}']`) ||

src/svelte/app/services/social-media-image-cache.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const putSocialMediaImageUrl = async (
4545
socialMediaImageUrl: string | null
4646
) => {
4747
const db = await openDatabase();
48-
db.put(STORE_NAME, {
48+
await db.put(STORE_NAME, {
4949
url,
5050
socialMediaImageUrl,
5151
timestamp: Date.now(),
@@ -76,7 +76,7 @@ export const clearSocialMediaImageCache = async () => {
7676
}
7777
};
7878

79-
const openDatabase = (): Promise<IDBPDatabase<SocialMediaImageDB>> => {
79+
const openDatabase = () => {
8080
return openDB<SocialMediaImageDB>(DATABASE_NAME, 1, {
8181
upgrade(db) {
8282
db.createObjectStore(STORE_NAME, { keyPath: "url" });

0 commit comments

Comments
 (0)