feat(homepage): add DuckDuckGo favicon fallback for history and favorites
- commit
- 25a9d2e629292facf19b40a46cb28d694b745253
- author
- brice · · 2025-12-17 17:14 UTC
- parent
- e30e9ed
- signature
- unsigned
2 files · +40 -32
M webui/src/pages/homepage/favorites/FavoriteCard.svelte +20 -16
| @@ -18,6 +18,14 @@ | |||
| 18 | 18 | } | |
| 19 | 19 | }; | |
| 20 | 20 | ||
| 21 | + | const getFaviconUrl = (fav: Favorite): string => { | |
| 22 | + | // Use provided favicon_url if available | |
| 23 | + | if (fav.favicon_url) return fav.favicon_url; | |
| 24 | + | // Construct DuckDuckGo favicon URL from domain | |
| 25 | + | const domain = getDomain(fav.url); | |
| 26 | + | return `https://icons.duckduckgo.com/ip3/${encodeURIComponent(domain)}.ico`; | |
| 27 | + | }; | |
| 28 | + | ||
| 21 | 29 | const getDisplayTitle = (fav: Favorite): string => { | |
| 22 | 30 | const trimmed = fav.title?.trim(); | |
| 23 | 31 | if (trimmed) return trimmed; | |
| @@ -55,22 +63,18 @@ | |||
| 55 | 63 | onkeydown={handleKeyDown} | |
| 56 | 64 | > | |
| 57 | 65 | <div class="card-favicon"> | |
| 58 | - | {#if favorite.favicon_url} | |
| 59 | - | <img | |
| 60 | - | src={favorite.favicon_url} | |
| 61 | - | alt="" | |
| 62 | - | class="favicon-img" | |
| 63 | - | onerror={(e) => { | |
| 64 | - | const target = e.target as HTMLImageElement; | |
| 65 | - | target.style.display = 'none'; | |
| 66 | - | const fallback = target.nextElementSibling as HTMLElement; | |
| 67 | - | if (fallback) fallback.style.display = 'flex'; | |
| 68 | - | }} | |
| 69 | - | /> | |
| 70 | - | <div class="favicon-fallback" style="display: none;"></div> | |
| 71 | - | {:else} | |
| 72 | - | <div class="favicon-fallback"></div> | |
| 73 | - | {/if} | |
| 66 | + | <img | |
| 67 | + | src={getFaviconUrl(favorite)} | |
| 68 | + | alt="" | |
| 69 | + | class="favicon-img" | |
| 70 | + | onerror={(e) => { | |
| 71 | + | const target = e.target as HTMLImageElement; | |
| 72 | + | target.style.display = 'none'; | |
| 73 | + | const fallback = target.nextElementSibling as HTMLElement; | |
| 74 | + | if (fallback) fallback.style.display = 'flex'; | |
| 75 | + | }} | |
| 76 | + | /> | |
| 77 | + | <div class="favicon-fallback" style="display: none;"></div> | |
| 74 | 78 | {#if favorite.shortcut_key} | |
| 75 | 79 | <div class="shortcut-badge">{favorite.shortcut_key}</div> | |
| 76 | 80 | {/if} | |
| @@ -18,6 +18,14 @@ | |||||
| 18 | } | 18 | } | ||
| 19 | }; | 19 | }; | ||
| 20 | 20 | ||||
| 21 | + | const getFaviconUrl = (fav: Favorite): string => { | |||
| 22 | + | // Use provided favicon_url if available | |||
| 23 | + | if (fav.favicon_url) return fav.favicon_url; | |||
| 24 | + | // Construct DuckDuckGo favicon URL from domain | |||
| 25 | + | const domain = getDomain(fav.url); | |||
| 26 | + | return `https://icons.duckduckgo.com/ip3/${encodeURIComponent(domain)}.ico`; | |||
| 27 | + | }; | |||
| 28 | + | ||||
| 21 | const getDisplayTitle = (fav: Favorite): string => { | 29 | const getDisplayTitle = (fav: Favorite): string => { | ||
| 22 | const trimmed = fav.title?.trim(); | 30 | const trimmed = fav.title?.trim(); | ||
| 23 | if (trimmed) return trimmed; | 31 | if (trimmed) return trimmed; | ||
| @@ -55,22 +63,18 @@ | |||||
| 55 | onkeydown={handleKeyDown} | 63 | onkeydown={handleKeyDown} | ||
| 56 | > | 64 | > | ||
| 57 | <div class="card-favicon"> | 65 | <div class="card-favicon"> | ||
| 58 | - | {#if favorite.favicon_url} | 66 | + | <img |
| 59 | - | <img | 67 | + | src={getFaviconUrl(favorite)} |
| 60 | - | src={favorite.favicon_url} | 68 | + | alt="" |
| 61 | - | alt="" | 69 | + | class="favicon-img" |
| 62 | - | class="favicon-img" | 70 | + | onerror={(e) => { |
| 63 | - | onerror={(e) => { | 71 | + | const target = e.target as HTMLImageElement; |
| 64 | - | const target = e.target as HTMLImageElement; | 72 | + | target.style.display = 'none'; |
| 65 | - | target.style.display = 'none'; | 73 | + | const fallback = target.nextElementSibling as HTMLElement; |
| 66 | - | const fallback = target.nextElementSibling as HTMLElement; | 74 | + | if (fallback) fallback.style.display = 'flex'; |
| 67 | - | if (fallback) fallback.style.display = 'flex'; | 75 | + | }} |
| 68 | - | }} | 76 | + | /> |
| 69 | - | /> | 77 | + | <div class="favicon-fallback" style="display: none;"></div> |
| 70 | - | <div class="favicon-fallback" style="display: none;"></div> | |||
| 71 | - | {:else} | |||
| 72 | - | <div class="favicon-fallback"></div> | |||
| 73 | - | {/if} | |||
| 74 | {#if favorite.shortcut_key} | 78 | {#if favorite.shortcut_key} | ||
| 75 | <div class="shortcut-badge">{favorite.shortcut_key}</div> | 79 | <div class="shortcut-badge">{favorite.shortcut_key}</div> | ||
| 76 | {/if} | 80 | {/if} | ||
M webui/src/pages/homepage/history/HistoryItem.svelte +20 -16
| @@ -20,6 +20,14 @@ | |||
| 20 | 20 | } | |
| 21 | 21 | }; | |
| 22 | 22 | ||
| 23 | + | const getFaviconUrl = (item: HistoryEntry): string => { | |
| 24 | + | // Use provided favicon_url if available | |
| 25 | + | if (item.favicon_url) return item.favicon_url; | |
| 26 | + | // Construct DuckDuckGo favicon URL from domain | |
| 27 | + | const domain = getDomain(item.url).replace(/^www\./, ''); | |
| 28 | + | return `https://icons.duckduckgo.com/ip3/${encodeURIComponent(domain)}.ico`; | |
| 29 | + | }; | |
| 30 | + | ||
| 23 | 31 | const getDisplayTitle = (item: HistoryEntry): string => { | |
| 24 | 32 | const trimmed = item.title?.trim(); | |
| 25 | 33 | if (trimmed) return trimmed; | |
| @@ -75,22 +83,18 @@ | |||
| 75 | 83 | onkeydown={handleKeyDown} | |
| 76 | 84 | > | |
| 77 | 85 | <div class="item-favicon"> | |
| 78 | - | {#if entry.favicon_url} | |
| 79 | - | <img | |
| 80 | - | src={entry.favicon_url} | |
| 81 | - | alt="" | |
| 82 | - | class="favicon-img" | |
| 83 | - | onerror={(e) => { | |
| 84 | - | const target = e.target as HTMLImageElement; | |
| 85 | - | target.style.display = 'none'; | |
| 86 | - | const fallback = target.nextElementSibling as HTMLElement; | |
| 87 | - | if (fallback) fallback.style.display = 'flex'; | |
| 88 | - | }} | |
| 89 | - | /> | |
| 90 | - | <div class="favicon-fallback" style="display: none;"></div> | |
| 91 | - | {:else} | |
| 92 | - | <div class="favicon-fallback"></div> | |
| 93 | - | {/if} | |
| 86 | + | <img | |
| 87 | + | src={getFaviconUrl(entry)} | |
| 88 | + | alt="" | |
| 89 | + | class="favicon-img" | |
| 90 | + | onerror={(e) => { | |
| 91 | + | const target = e.target as HTMLImageElement; | |
| 92 | + | target.style.display = 'none'; | |
| 93 | + | const fallback = target.nextElementSibling as HTMLElement; | |
| 94 | + | if (fallback) fallback.style.display = 'flex'; | |
| 95 | + | }} | |
| 96 | + | /> | |
| 97 | + | <div class="favicon-fallback" style="display: none;"></div> | |
| 94 | 98 | </div> | |
| 95 | 99 | ||
| 96 | 100 | <div class="item-content"> | |
| @@ -20,6 +20,14 @@ | |||||
| 20 | } | 20 | } | ||
| 21 | }; | 21 | }; | ||
| 22 | 22 | ||||
| 23 | + | const getFaviconUrl = (item: HistoryEntry): string => { | |||
| 24 | + | // Use provided favicon_url if available | |||
| 25 | + | if (item.favicon_url) return item.favicon_url; | |||
| 26 | + | // Construct DuckDuckGo favicon URL from domain | |||
| 27 | + | const domain = getDomain(item.url).replace(/^www\./, ''); | |||
| 28 | + | return `https://icons.duckduckgo.com/ip3/${encodeURIComponent(domain)}.ico`; | |||
| 29 | + | }; | |||
| 30 | + | ||||
| 23 | const getDisplayTitle = (item: HistoryEntry): string => { | 31 | const getDisplayTitle = (item: HistoryEntry): string => { | ||
| 24 | const trimmed = item.title?.trim(); | 32 | const trimmed = item.title?.trim(); | ||
| 25 | if (trimmed) return trimmed; | 33 | if (trimmed) return trimmed; | ||
| @@ -75,22 +83,18 @@ | |||||
| 75 | onkeydown={handleKeyDown} | 83 | onkeydown={handleKeyDown} | ||
| 76 | > | 84 | > | ||
| 77 | <div class="item-favicon"> | 85 | <div class="item-favicon"> | ||
| 78 | - | {#if entry.favicon_url} | 86 | + | <img |
| 79 | - | <img | 87 | + | src={getFaviconUrl(entry)} |
| 80 | - | src={entry.favicon_url} | 88 | + | alt="" |
| 81 | - | alt="" | 89 | + | class="favicon-img" |
| 82 | - | class="favicon-img" | 90 | + | onerror={(e) => { |
| 83 | - | onerror={(e) => { | 91 | + | const target = e.target as HTMLImageElement; |
| 84 | - | const target = e.target as HTMLImageElement; | 92 | + | target.style.display = 'none'; |
| 85 | - | target.style.display = 'none'; | 93 | + | const fallback = target.nextElementSibling as HTMLElement; |
| 86 | - | const fallback = target.nextElementSibling as HTMLElement; | 94 | + | if (fallback) fallback.style.display = 'flex'; |
| 87 | - | if (fallback) fallback.style.display = 'flex'; | 95 | + | }} |
| 88 | - | }} | 96 | + | /> |
| 89 | - | /> | 97 | + | <div class="favicon-fallback" style="display: none;"></div> |
| 90 | - | <div class="favicon-fallback" style="display: none;"></div> | |||
| 91 | - | {:else} | |||
| 92 | - | <div class="favicon-fallback"></div> | |||
| 93 | - | {/if} | |||
| 94 | </div> | 98 | </div> | ||
| 95 | 99 | ||||
| 96 | <div class="item-content"> | 100 | <div class="item-content"> | ||