Skip to content

Commit 74fde8d

Browse files
author
Daniel Knobloch
committed
fix(perf): use cached images instead of base64 data images, fix service worker in dev
1 parent ae6d17c commit 74fde8d

File tree

16 files changed

+147
-172
lines changed

16 files changed

+147
-172
lines changed

src/lib/api/itunes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
ITunesPodcast,
66
ITunesEpisode
77
} from '$lib/types/itunes';
8-
import { resizeBase64Image } from '$lib/utils/resizeImage';
8+
import { resizeBase64Image } from '$lib/utils/imageHelpers';
99

1010
const apiUrl = 'https://itunes.apple.com/search';
1111

src/lib/components/EpisodeList.svelte

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
isPlaylist = false,
3535
isSearch = false,
3636
isShare = false,
37+
hideImages = false,
3738
onPlayNext = undefined
3839
}: {
3940
episodes: Episode[];
@@ -42,6 +43,7 @@
4243
isPlaylist?: boolean;
4344
isSearch?: boolean;
4445
isShare?: boolean;
46+
hideImages?: boolean;
4547
onPlayNext?: (episode: Episode) => void;
4648
} = $props();
4749
@@ -173,19 +175,21 @@
173175
if (!card) return;
174176
175177
const cardBottom = card.getBoundingClientRect().bottom;
176-
const padding = playingEpisode ? playerHeight + 60 : 60;
178+
const detectPadding = playingEpisode ? playerHeight + 60 : 60;
179+
const scrollPadding = playingEpisode ? 40 : 120;
177180
178-
if (cardBottom + padding > window.innerHeight) {
181+
if (cardBottom + detectPadding > window.innerHeight) {
179182
// Calculate the scroll position
180183
const targetScroll =
181184
window.scrollY +
182185
card.getBoundingClientRect().top -
183186
window.innerHeight / 2 +
184-
card.getBoundingClientRect().height / 2;
187+
card.getBoundingClientRect().height / 2 -
188+
scrollPadding;
185189
186190
const startScroll = window.scrollY;
187191
const distance = targetScroll - startScroll;
188-
const duration = 700;
192+
const duration = 350;
189193
const startTime = performance.now();
190194
191195
// Ease in for a more relaxed start
@@ -285,19 +289,28 @@
285289
onclick={() => toggleEpisodeFocus(episode)}
286290
>
287291
<div class="episode-card__content">
288-
{#if feedIconsById}
289-
{#if feedIconsById.has(episode.feedId)}
292+
{#if !hideImages}
293+
{#if feedIconsById}
294+
{#if feedIconsById.has(episode.feedId)}
295+
<img
296+
src={feedIconsById.get(episode.feedId)}
297+
loading="lazy"
298+
alt={episode.title}
299+
class="episode-card__image"
300+
/>
301+
{:else}
302+
<div class="episode-card__image">
303+
<div class="fallback">
304+
<span>{episode.title[0]?.toUpperCase() || '?'}</span>
305+
</div>
306+
</div>
307+
{/if}
308+
{:else}
290309
<img
291-
src={feedIconsById.get(episode.feedId)}
310+
src={`/icon/${episode.feedId}.png`}
292311
alt={episode.title}
293312
class="episode-card__image"
294313
/>
295-
{:else}
296-
<div class="episode-card__image">
297-
<div class="fallback">
298-
<span>{episode.title[0]?.toUpperCase() || '?'}</span>
299-
</div>
300-
</div>
301314
{/if}
302315
{/if}
303316
<div class="episode-card__heading">

src/lib/components/FeedList.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
if (feedDataById.has(feed.id.toString()) && episodeDataByFeedId.has(feed.id.toString())) {
4747
const feedToAdd = feedDataById.get(feed.id.toString())!;
4848
feedToAdd.iconData = await convertUrlToBase64(feedToAdd.iconData, feedToAdd.title);
49-
success = feedService.addFeedAndEpisodes(
49+
success = await feedService.addFeedAndEpisodes(
5050
feedToAdd,
5151
episodeDataByFeedId.get(feed.id.toString())!
5252
);

src/lib/components/Player.svelte

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import { onMount } from 'svelte';
66
import { goto } from '$app/navigation';
77
import type { ActiveEpisode } from '$lib/types/db';
8-
import type { SvelteMap } from 'svelte/reactivity';
98
import { page } from '$app/state';
109
import PlayerDetails from './PlayerDetails.svelte';
1110
import { SettingsService } from '$lib/service/SettingsService.svelte';
@@ -16,10 +15,7 @@
1615
const ICON_SIZE = '2rem';
1716
const PLAYBACK_SPEEDS = [1.0, 1.25, 1.5, 1.75, 2.0];
1817
19-
let {
20-
episode,
21-
feedIconsById
22-
}: { episode: ActiveEpisode; feedIconsById: SvelteMap<string, string> } = $props();
18+
let { episode }: { episode: ActiveEpisode } = $props();
2319
2420
let currentTime = $state(0);
2521
let lastUpdatedTime = $state(0);
@@ -55,11 +51,7 @@
5551
previousEpisodeId = episode.id;
5652
showDetailedControls = false;
5753
58-
AudioService.updateMediaSessionMetadata(
59-
episode.title,
60-
episode.feedTitle,
61-
feedIconsById.get(episode.feedId)
62-
);
54+
AudioService.updateMediaSessionMetadata(episode.title, episode.feedTitle, episode.feedId);
6355
}
6456
});
6557
@@ -219,7 +211,7 @@
219211
role="button"
220212
tabindex="0"
221213
>
222-
<img src={`data:${feedIconsById.get(episode.feedId)}`} alt="" />
214+
<img src={`/icon/${episode.feedId}.png`} alt="" />
223215
</div>
224216

225217
<button class="player__button" onclick={(e) => handleBack(e)}>

src/lib/service/AudioService.svelte.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,21 @@ export class AudioService {
3838
static updateMediaSessionMetadata(
3939
episodeTitle: string,
4040
feedTitle: string,
41-
artwork: string | undefined
41+
feedId: string
4242
) {
4343
if (!('mediaSession' in navigator)) return;
4444

4545
navigator.mediaSession.metadata = new MediaMetadata({
4646
title: episodeTitle,
4747
artist: feedTitle,
4848
album: feedTitle,
49-
artwork: artwork
50-
? [
51-
{
52-
src: `data:${artwork}`,
53-
sizes: '300x300',
54-
type: 'image/png'
55-
}
56-
]
57-
: undefined
49+
artwork: [
50+
{
51+
src: `/icon/${feedId}.png`,
52+
sizes: '300x300',
53+
type: 'image/png'
54+
}
55+
]
5856
});
5957
}
6058

src/lib/service/FeedService.svelte.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { SettingsService } from './SettingsService.svelte';
77
import type { ImportProgress } from '$lib/types/ImportProgress';
88
import { isOnline } from '$lib/utils/networkState.svelte';
99
import { findPodcastByTitleAndUrl } from '$lib/api/itunes';
10+
import { cacheBase64Image } from '$lib/utils/imageHelpers';
1011

1112
const FEED_SYNC_CHECK_INTERVAL_MS = 60 * 1000;
1213
const ONE_DAY_IN_SECONDS = 24 * 60 * 60;
@@ -159,10 +160,11 @@ export class FeedService {
159160
}
160161
}
161162

162-
addFeedAndEpisodes(feed: Feed, episodes: Episode[]): boolean {
163+
async addFeedAndEpisodes(feed: Feed, episodes: Episode[]): Promise<boolean> {
163164
try {
164165
Log.info(`Adding feed and episodes: ${feed.title}`);
165166

167+
await cacheBase64Image(feed.iconData, feed.id);
166168
db.feeds.insert(feed);
167169
db.episodes.insertMany(episodes);
168170

@@ -203,6 +205,7 @@ export class FeedService {
203205
ownerName: finderResponse.feeds[0].ownerName
204206
};
205207

208+
await cacheBase64Image(feed.iconData, feed.id);
206209
db.feeds.insert(feedWithTimestamps);
207210

208211
// feeds can be retried from feed page
@@ -234,6 +237,8 @@ export class FeedService {
234237

235238
db.feeds.insertMany(feeds);
236239

240+
await Promise.all(feeds.map(feed => cacheBase64Image(feed.iconData, feed.id)));
241+
237242
const finderRequest: EpisodeFinderRequest = {
238243
feeds,
239244
since: undefined,

src/lib/service/SettingsService.svelte.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const DefaultSettings: Settings = {
2626
inProgressEpisodeRetentionDays: 14,
2727
goBackOnResumeSeconds: 10,
2828
hugged: false,
29-
ratchet: 1,
29+
ratchet: 2,
3030
playlistView: 'upNext'
3131
};
3232

src/lib/stores/ratchets.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Log } from "$lib/service/LogService";
22
import { SettingsService } from "$lib/service/SettingsService.svelte";
3+
import { cacheBase64Image } from "$lib/utils/imageHelpers";
34
import { db, getSettings } from "./db.svelte";
45

56
interface Ratchet {
@@ -15,7 +16,17 @@ const ratchets: Ratchet[] = [
1516
run: async () => {
1617
db.feeds.updateMany({}, { $set: { isSubscribed: 1 } });
1718
}
18-
}
19+
},
20+
{
21+
name: 'Cache all feed icons',
22+
version: 2,
23+
run: async () => {
24+
const feeds = db.feeds.find({}).fetch();
25+
for (const feed of feeds) {
26+
await cacheBase64Image(feed.iconData, feed.id);
27+
}
28+
}
29+
},
1930
];
2031

2132
export async function runRatchets() {
Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,28 @@
11
import { Log } from "$lib/service/LogService";
22

3-
function createFallbackImageImage(title: string, maxWidth: number, maxHeight: number): string {
4-
const canvas = document.createElement('canvas');
5-
canvas.width = maxWidth;
6-
canvas.height = maxHeight;
7-
const ctx = canvas.getContext('2d')!;
8-
9-
// White background
10-
ctx.fillStyle = '#FFFFFF';
11-
ctx.fillRect(0, 0, canvas.width, canvas.height);
12-
13-
// Grey text
14-
const fontSize = 36;
15-
ctx.fillStyle = '#666666';
16-
ctx.font = `bold ${fontSize}px system-ui`;
17-
ctx.textAlign = 'center';
18-
ctx.textBaseline = 'middle';
19-
20-
// Word wrap
21-
const words = title.split(' ');
22-
const lines: string[] = [];
23-
let currentLine = words[0];
24-
25-
for (let i = 1; i < words.length; i++) {
26-
const testLine = currentLine + ' ' + words[i];
27-
const { width } = ctx.measureText(testLine);
28-
if (width < maxWidth - 40) { // 20px padding each side
29-
currentLine = testLine;
30-
} else {
31-
lines.push(currentLine);
32-
currentLine = words[i];
33-
}
3+
export async function cacheBase64Image(base64Image: string, cacheKey: string): Promise<void> {
4+
// Strip the data URL prefix if present
5+
const cleaned = base64Image.replace(/^data:image\/png;base64,/, '');
6+
7+
// Decode base64 into binary
8+
const binary = atob(cleaned);
9+
const byteArray = new Uint8Array(binary.length);
10+
for (let i = 0; i < binary.length; i++) {
11+
byteArray[i] = binary.charCodeAt(i);
3412
}
35-
lines.push(currentLine);
3613

37-
// Vertically center lines
38-
const lineHeight = fontSize * 1.3;
39-
const totalHeight = lines.length * lineHeight;
40-
const startY = (canvas.height - totalHeight) / 2 + lineHeight / 2;
14+
const blob = new Blob([byteArray], { type: 'image/png' });
4115

42-
lines.forEach((line, i) => {
43-
ctx.fillText(line, canvas.width / 2, startY + i * lineHeight);
16+
const cacheResponse = new Response(blob, {
17+
headers: {
18+
'Content-Type': 'image/png',
19+
'Content-Length': blob.size.toString()
20+
}
4421
});
4522

46-
return canvas.toDataURL('image/png');
23+
const cache = await caches.open('icon-cache');
24+
const absoluteUrl = new URL(`/icon/${cacheKey}.png`, location.origin).toString();
25+
await cache.put(absoluteUrl, cacheResponse);
4726
}
4827

4928
export async function resizeBase64Image(
@@ -103,5 +82,52 @@ export async function resizeBase64Image(
10382
const backupResult = await loadImage(backupUrl);
10483
if (backupResult) return backupResult;
10584
}
106-
return createFallbackImageImage(title, maxWidth, maxHeight);
85+
return createFallbackImage(title, maxWidth, maxHeight);
10786
}
87+
88+
function createFallbackImage(title: string, maxWidth: number, maxHeight: number): string {
89+
const canvas = document.createElement('canvas');
90+
canvas.width = maxWidth;
91+
canvas.height = maxHeight;
92+
const ctx = canvas.getContext('2d')!;
93+
94+
// White background
95+
ctx.fillStyle = '#FFFFFF';
96+
ctx.fillRect(0, 0, canvas.width, canvas.height);
97+
98+
// Grey text
99+
const fontSize = 36;
100+
ctx.fillStyle = '#666666';
101+
ctx.font = `bold ${fontSize}px system-ui`;
102+
ctx.textAlign = 'center';
103+
ctx.textBaseline = 'middle';
104+
105+
// Word wrap
106+
const words = title.split(' ');
107+
const lines: string[] = [];
108+
let currentLine = words[0];
109+
110+
for (let i = 1; i < words.length; i++) {
111+
const testLine = currentLine + ' ' + words[i];
112+
const { width } = ctx.measureText(testLine);
113+
if (width < maxWidth - 40) { // 20px padding each side
114+
currentLine = testLine;
115+
} else {
116+
lines.push(currentLine);
117+
currentLine = words[i];
118+
}
119+
}
120+
lines.push(currentLine);
121+
122+
// Vertically center lines
123+
const lineHeight = fontSize * 1.3;
124+
const totalHeight = lines.length * lineHeight;
125+
const startY = (canvas.height - totalHeight) / 2 + lineHeight / 2;
126+
127+
lines.forEach((line, i) => {
128+
ctx.fillText(line, canvas.width / 2, startY + i * lineHeight);
129+
});
130+
131+
return canvas.toDataURL('image/png');
132+
}
133+

src/lib/utils/storage.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ export async function requestStoragePersistence(): Promise<void> {
2121

2222
export function registerServiceWorker() {
2323
if ('serviceWorker' in navigator && !(isAppleDevice && !isPwa)) {
24-
navigator.serviceWorker.register('/service-worker.js');
24+
navigator.serviceWorker.register('/service-worker.js', {
25+
type: 'module'
26+
});
2527
}
2628
}
2729

0 commit comments

Comments
 (0)