homepage: show favicons in Recent History list (circular #ccc chip, 16px icon); compute /favicon.ico from entry URL; hide on error
- commit
- c91475000ce134a2c79321c42ac506a48872cd28
- author
- bamen · · 2025-09-09 18:14 UTC
- committer
- bamen · · 2025-09-09 18:28 UTC
- parent
- a863db8
- signature
- unsigned
2 files · +33 -0
M frontend/src/utils/dom.ts +20 -0
| @@ -78,6 +78,15 @@ export class DOMRenderer { | |||
| 78 | 78 | const line = document.createElement('div'); | |
| 79 | 79 | line.className = 'history-line'; | |
| 80 | 80 | ||
| 81 | + | // Favicon chip | |
| 82 | + | const icon = document.createElement('img'); | |
| 83 | + | icon.className = 'history-favicon'; | |
| 84 | + | icon.width = 16; icon.height = 16; | |
| 85 | + | icon.loading = 'lazy'; | |
| 86 | + | icon.referrerPolicy = 'no-referrer'; | |
| 87 | + | icon.src = this.buildFaviconURL(item.url); | |
| 88 | + | icon.addEventListener('error', () => { icon.style.display = 'none'; }); | |
| 89 | + | ||
| 81 | 90 | const titleEl = document.createElement('span'); | |
| 82 | 91 | titleEl.className = 'history-title'; | |
| 83 | 92 | titleEl.textContent = title; | |
| @@ -98,6 +107,7 @@ export class DOMRenderer { | |||
| 98 | 107 | urlEl.className = 'history-url'; | |
| 99 | 108 | urlEl.textContent = item.url; | |
| 100 | 109 | ||
| 110 | + | line.appendChild(icon); | |
| 101 | 111 | line.appendChild(titleEl); | |
| 102 | 112 | line.appendChild(sep1); | |
| 103 | 113 | line.appendChild(domainEl); | |
| @@ -186,4 +196,14 @@ export class DOMRenderer { | |||
| 186 | 196 | return { host: '' }; | |
| 187 | 197 | } | |
| 188 | 198 | } | |
| 199 | + | ||
| 200 | + | private buildFaviconURL(raw: string): string { | |
| 201 | + | try { | |
| 202 | + | const u = new URL(raw); | |
| 203 | + | const scheme = u.protocol && u.protocol !== ':' ? u.protocol : 'https:'; | |
| 204 | + | return `${scheme}//${u.host}/favicon.ico`; | |
| 205 | + | } catch { | |
| 206 | + | return ''; | |
| 207 | + | } | |
| 208 | + | } | |
| 189 | 209 | } | |
| @@ -78,6 +78,15 @@ export class DOMRenderer { | |||||
| 78 | const line = document.createElement('div'); | 78 | const line = document.createElement('div'); | ||
| 79 | line.className = 'history-line'; | 79 | line.className = 'history-line'; | ||
| 80 | 80 | ||||
| 81 | + | // Favicon chip | |||
| 82 | + | const icon = document.createElement('img'); | |||
| 83 | + | icon.className = 'history-favicon'; | |||
| 84 | + | icon.width = 16; icon.height = 16; | |||
| 85 | + | icon.loading = 'lazy'; | |||
| 86 | + | icon.referrerPolicy = 'no-referrer'; | |||
| 87 | + | icon.src = this.buildFaviconURL(item.url); | |||
| 88 | + | icon.addEventListener('error', () => { icon.style.display = 'none'; }); | |||
| 89 | + | ||||
| 81 | const titleEl = document.createElement('span'); | 90 | const titleEl = document.createElement('span'); | ||
| 82 | titleEl.className = 'history-title'; | 91 | titleEl.className = 'history-title'; | ||
| 83 | titleEl.textContent = title; | 92 | titleEl.textContent = title; | ||
| @@ -98,6 +107,7 @@ export class DOMRenderer { | |||||
| 98 | urlEl.className = 'history-url'; | 107 | urlEl.className = 'history-url'; | ||
| 99 | urlEl.textContent = item.url; | 108 | urlEl.textContent = item.url; | ||
| 100 | 109 | ||||
| 110 | + | line.appendChild(icon); | |||
| 101 | line.appendChild(titleEl); | 111 | line.appendChild(titleEl); | ||
| 102 | line.appendChild(sep1); | 112 | line.appendChild(sep1); | ||
| 103 | line.appendChild(domainEl); | 113 | line.appendChild(domainEl); | ||
| @@ -186,4 +196,14 @@ export class DOMRenderer { | |||||
| 186 | return { host: '' }; | 196 | return { host: '' }; | ||
| 187 | } | 197 | } | ||
| 188 | } | 198 | } | ||
| 199 | + | ||||
| 200 | + | private buildFaviconURL(raw: string): string { | |||
| 201 | + | try { | |||
| 202 | + | const u = new URL(raw); | |||
| 203 | + | const scheme = u.protocol && u.protocol !== ':' ? u.protocol : 'https:'; | |||
| 204 | + | return `${scheme}//${u.host}/favicon.ico`; | |||
| 205 | + | } catch { | |||
| 206 | + | return ''; | |||
| 207 | + | } | |||
| 208 | + | } | |||
| 189 | } | 209 | } | ||
M frontend/src/utils/html-builder.ts +13 -0
| @@ -115,6 +115,19 @@ export class HTMLBuilder { | |||
| 115 | 115 | min-width: 0; /* allow flex children to shrink */ | |
| 116 | 116 | } | |
| 117 | 117 | ||
| 118 | + | .history-favicon { | |
| 119 | + | flex: 0 0 20px; | |
| 120 | + | width: 16px; height: 16px; | |
| 121 | + | padding: 2px; | |
| 122 | + | box-sizing: content-box; | |
| 123 | + | border-radius: 50%; | |
| 124 | + | background: #ccc; | |
| 125 | + | border: 1px solid rgba(0,0,0,.12); | |
| 126 | + | box-shadow: 0 1px 2px rgba(0,0,0,.12); | |
| 127 | + | filter: brightness(1.06) contrast(1.03); | |
| 128 | + | image-rendering: -webkit-optimize-contrast; | |
| 129 | + | } | |
| 130 | + | ||
| 118 | 131 | .history-title { font-size: 0.95rem; color: #e6e6e6; flex: 0 0 auto; } | |
| 119 | 132 | .history-domain { font-size: 0.9rem; color: #a5a5a5; flex: 0 0 auto; } | |
| 120 | 133 | .history-sep { color: #666; flex: 0 0 auto; } | |
| @@ -115,6 +115,19 @@ export class HTMLBuilder { | |||||
| 115 | min-width: 0; /* allow flex children to shrink */ | 115 | min-width: 0; /* allow flex children to shrink */ | ||
| 116 | } | 116 | } | ||
| 117 | 117 | ||||
| 118 | + | .history-favicon { | |||
| 119 | + | flex: 0 0 20px; | |||
| 120 | + | width: 16px; height: 16px; | |||
| 121 | + | padding: 2px; | |||
| 122 | + | box-sizing: content-box; | |||
| 123 | + | border-radius: 50%; | |||
| 124 | + | background: #ccc; | |||
| 125 | + | border: 1px solid rgba(0,0,0,.12); | |||
| 126 | + | box-shadow: 0 1px 2px rgba(0,0,0,.12); | |||
| 127 | + | filter: brightness(1.06) contrast(1.03); | |||
| 128 | + | image-rendering: -webkit-optimize-contrast; | |||
| 129 | + | } | |||
| 130 | + | ||||
| 118 | .history-title { font-size: 0.95rem; color: #e6e6e6; flex: 0 0 auto; } | 131 | .history-title { font-size: 0.95rem; color: #e6e6e6; flex: 0 0 auto; } | ||
| 119 | .history-domain { font-size: 0.9rem; color: #a5a5a5; flex: 0 0 auto; } | 132 | .history-domain { font-size: 0.9rem; color: #a5a5a5; flex: 0 0 auto; } | ||
| 120 | .history-sep { color: #666; flex: 0 0 auto; } | 133 | .history-sep { color: #666; flex: 0 0 auto; } | ||