Skip to content

Commit 5b3b9ec

Browse files
committed
Add Lorem Picsum stock provider
1 parent 524e204 commit 5b3b9ec

11 files changed

Lines changed: 309 additions & 43 deletions

File tree

cspell.config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ words:
7777
- passwordless
7878
- pexels
7979
- Photoshop
80+
- Picsum
8081
- Pixabay
8182
- PKCE
8283
- plusplus

src/lib/components/assets/browser/external-assets-panel.svelte

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@
7777
upload,
7878
} = $derived(serviceProps);
7979
80-
const viewType = $derived($selectAssetsView?.type);
80+
// Use the grid view for Picsum as it doesn’t provide description for the assets, and the list
81+
// view relies on the description to show asset information.
82+
const viewType = $derived(serviceId === 'picsum' ? 'grid' : $selectAssetsView?.type);
8183
8284
const input = $state({ userName: '', password: '' });
8385
let hasConfig = $state(true);
@@ -210,7 +212,7 @@
210212
211213
apiKey = $prefs.apiKeys?.[serviceId] ?? '';
212214
[userName, password] = ($prefs.logins?.[serviceId] ?? '').split(' ');
213-
hasAuthInfo = !!apiKey || !!password;
215+
hasAuthInfo = authType === 'none' || !!apiKey || !!password;
214216
listedAssets = null;
215217
})();
216218
});
@@ -262,7 +264,7 @@
262264
</EmptyState>
263265
{:else if !listedAssets}
264266
<EmptyState>
265-
<span role="alert">{$_('searching')}</span>
267+
<span role="alert">{$_(searchTerms ? 'searching' : 'loading')}</span>
266268
</EmptyState>
267269
{:else if !listedAssets.length}
268270
<EmptyState>

src/lib/components/assets/browser/select-assets-dialog.svelte

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,19 @@
9999
/** @type {ExternalAssetsPanel | undefined} */
100100
let externalAssetsPanel = $state();
101101
102+
/**
103+
* Sort services by their label in alphabetical order.
104+
* @param {[string, { serviceLabel: string }]} a First service entry.
105+
* @param {[string, { serviceLabel: string }]} b Second service entry.
106+
* @returns {number} Sorting order value.
107+
*/
108+
const sortServicesByName = (a, b) => {
109+
const nameA = a[1].serviceLabel.toLowerCase();
110+
const nameB = b[1].serviceLabel.toLowerCase();
111+
112+
return nameA.localeCompare(nameB);
113+
};
114+
102115
const title = $derived(
103116
kind === 'image' ? $_('assets_dialog.title.image') : $_('assets_dialog.title.file'),
104117
);
@@ -142,27 +155,28 @@
142155
const enabledStockAssetProviderEntries = $derived.by(() => {
143156
const { providers = [] } = getStockAssetMediaLibraryOptions({ fieldConfig });
144157
145-
return Object.entries(allStockAssetProviders).filter(
146-
([serviceId, { hotlinking }]) =>
147-
providers.includes(/** @type {StockAssetProviderName} */ (serviceId)) &&
148-
// When hotlinking is not required, files are downloaded and then uploaded to the
149-
// repository, so the default library has to be configured.
150-
(hotlinking || isDefaultLibraryEnabled),
151-
);
158+
return Object.entries(allStockAssetProviders)
159+
.filter(
160+
([serviceId, { hotlinking }]) =>
161+
providers.includes(/** @type {StockAssetProviderName} */ (serviceId)) &&
162+
// When hotlinking is not required, files are downloaded and then uploaded to the
163+
// repository, so the default library has to be configured.
164+
(hotlinking || isDefaultLibraryEnabled),
165+
)
166+
.sort(sortServicesByName);
152167
});
153168
const isEnabledMediaService = $derived(
154-
enabledStockAssetProviderEntries
155-
.map(([serviceId]) => serviceId)
156-
.includes(/** @type {StockAssetProviderName} */ (libraryName)) &&
157-
!!$prefs?.apiKeys?.[libraryName],
169+
enabledStockAssetProviderEntries.some(
170+
([serviceId, { authType }]) =>
171+
serviceId === libraryName && (authType === 'none' || !!$prefs?.apiKeys?.[libraryName]),
172+
),
158173
);
159174
const enabledCloudServiceEntries = $derived(
160175
Object.entries(allCloudStorageServices).filter(([, { isEnabled }]) => isEnabled?.() ?? true),
161176
);
162-
const enabledExternalServiceEntries = $derived([
163-
...enabledCloudServiceEntries,
164-
...enabledStockAssetProviderEntries,
165-
]);
177+
const enabledExternalServiceEntries = $derived(
178+
[...enabledCloudServiceEntries, ...enabledStockAssetProviderEntries].sort(sortServicesByName),
179+
);
166180
const isCloudLibrary = $derived(
167181
enabledCloudServiceEntries.map(([serviceId]) => serviceId).includes(libraryName),
168182
);
@@ -312,7 +326,7 @@
312326
</script>
313327
314328
{#snippet headerItems()}
315-
{#if isDefaultLibrary || (isCloudLibrary && libraryName !== 'cloudinary') || isStockLibrary}
329+
{#if isDefaultLibrary || (isCloudLibrary && libraryName !== 'cloudinary') || (isStockLibrary && libraryName !== 'picsum')}
316330
{#if $selectAssetsView}
317331
<ViewSwitcher
318332
currentView={(() => /** @type {Writable<SelectAssetsView>} */ (selectAssetsView))()}

src/lib/components/assets/browser/simple-image-grid.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@
106106
padding: 0 8px;
107107
width: 100%;
108108
min-height: calc(var(--sui-font-size-default) * 3);
109+
110+
&:empty {
111+
display: none;
112+
}
109113
}
110114
}
111115
}

src/lib/components/settings/panels/media-panel.svelte

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -95,22 +95,25 @@
9595
'https://sveltiacms.app/en/docs/integrations/stock-photos',
9696
)}
9797
</p>
98-
{#each enabledStockAssetProviderEntries as [serviceId, { serviceLabel }] (serviceId)}
99-
<section>
100-
<h4>{serviceLabel}</h4>
101-
<div role="none">
102-
<PasswordInput
103-
bind:value={$prefs.apiKeys[serviceId]}
104-
flex
105-
autocomplete="off"
106-
spellcheck="false"
107-
aria-label={$_('prefs.media.stock_photos.field_label', {
108-
values: { service: serviceLabel },
109-
})}
110-
onchange={() => onchange(serviceId)}
111-
/>
112-
</div>
113-
</section>
98+
{#each enabledStockAssetProviderEntries as [serviceId, service] (serviceId)}
99+
{@const { serviceLabel, authType } = service}
100+
{#if authType !== 'none'}
101+
<section>
102+
<h4>{serviceLabel}</h4>
103+
<div role="none">
104+
<PasswordInput
105+
bind:value={$prefs.apiKeys[serviceId]}
106+
flex
107+
autocomplete="off"
108+
spellcheck="false"
109+
aria-label={$_('prefs.media.stock_photos.field_label', {
110+
values: { service: serviceLabel },
111+
})}
112+
onchange={() => onchange(serviceId)}
113+
/>
114+
</div>
115+
</section>
116+
{/if}
114117
{/each}
115118
</section>
116119
{/if}

src/lib/services/integrations/media-libraries/stock/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getMediaLibraryOptions } from '$lib/services/integrations/media-libraries';
22

33
import pexels from './pexels';
4+
import picsum from './picsum';
45
import pixabay from './pixabay';
56
import unsplash from './unsplash';
67

@@ -15,6 +16,7 @@ import unsplash from './unsplash';
1516
*/
1617
export const allStockAssetProviders = {
1718
pexels,
19+
picsum,
1820
pixabay,
1921
unsplash,
2022
};

src/lib/services/integrations/media-libraries/stock/index.test.js

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/* eslint-disable no-await-in-loop */
2+
3+
/**
4+
* @import {
5+
* ExternalAsset,
6+
* MediaLibraryService,
7+
* } from '$lib/types/private';
8+
*/
9+
10+
/**
11+
* @typedef {object} FetchResult
12+
* @property {string} id Asset ID.
13+
* @property {string} author Photographer name.
14+
* @property {number} width Image width.
15+
* @property {number} height Image height.
16+
* @property {string} download_url Full-size image URL.
17+
*/
18+
19+
const ENDPOINT = 'https://picsum.photos/v2/list';
20+
const LIMIT = 100;
21+
const TOTAL_PAGES = 10;
22+
const FETCH_PAGES = 3;
23+
24+
/**
25+
* Parse API results into ExternalAsset format.
26+
* @param {FetchResult[]} results API results.
27+
* @returns {ExternalAsset[]} Assets.
28+
*/
29+
export const parseResults = (results) =>
30+
results.map(({ id, download_url: downloadURL }) => ({
31+
id,
32+
// The service doesn’t provide descriptions
33+
description: '',
34+
previewURL: `https://picsum.photos/id/${id}/400/300`,
35+
downloadURL,
36+
fileName: `picsum-${id}.jpg`,
37+
kind: 'image',
38+
// No credit is required as the photos are licensed under CC0
39+
// https://github.com/DMarby/picsum-photos/issues/81#issuecomment-1340068800
40+
}));
41+
42+
/**
43+
* Fetch all available pictures across all pages.
44+
* @returns {Promise<ExternalAsset[]>} Assets.
45+
* @see https://picsum.photos/#list-images
46+
*/
47+
export const list = async () => {
48+
/** @type {FetchResult[]} */
49+
const results = [];
50+
51+
// Pick random pages from the 10 available (100 images each).
52+
const pages = Array.from({ length: TOTAL_PAGES }, (_, i) => i + 1)
53+
.sort(() => Math.random() - 0.5)
54+
.slice(0, FETCH_PAGES);
55+
56+
for (let i = 0; i < pages.length; i += 1) {
57+
const params = new URLSearchParams({ page: String(pages[i]), limit: String(LIMIT) });
58+
const response = await fetch(`${ENDPOINT}?${params}`);
59+
60+
if (!response.ok) {
61+
return Promise.reject();
62+
}
63+
64+
/** @type {FetchResult[]} */
65+
const pageResults = await response.json();
66+
67+
results.push(...pageResults);
68+
}
69+
70+
// Randomize the results for variety, as the API returns them in the same order for the same page.
71+
return parseResults(results.sort(() => Math.random() - 0.5));
72+
};
73+
74+
/**
75+
* @type {MediaLibraryService}
76+
*/
77+
export default {
78+
serviceType: 'stock_assets',
79+
serviceId: 'picsum',
80+
serviceLabel: 'Lorem Picsum',
81+
serviceURL: 'https://picsum.photos/',
82+
showServiceLink: true,
83+
hotlinking: true,
84+
authType: 'none',
85+
list,
86+
};

0 commit comments

Comments
 (0)