diff --git a/apps/docs/docs/widgets/immich-album-carousel/index.mdx b/apps/docs/docs/widgets/immich-album-carousel/index.mdx
index 59b1beeb9..703bd5a57 100644
--- a/apps/docs/docs/widgets/immich-album-carousel/index.mdx
+++ b/apps/docs/docs/widgets/immich-album-carousel/index.mdx
@@ -1,6 +1,6 @@
---
title: 'Immich Album Carousel'
-description: 'Shows a slideshow of your album pictures from Immich'
+description: 'Shows a slideshow of pictures from an Immich album or your full library'
hide_title: true
---
@@ -37,4 +37,3 @@ import screen from './img/screen.png';
### Configuration
-
diff --git a/apps/docs/docs/widgets/immich-album-carousel/index.ts b/apps/docs/docs/widgets/immich-album-carousel/index.ts
index f6c4b224c..72a0b2211 100644
--- a/apps/docs/docs/widgets/immich-album-carousel/index.ts
+++ b/apps/docs/docs/widgets/immich-album-carousel/index.ts
@@ -4,15 +4,16 @@ import { IconPhoto } from "@tabler/icons-react";
export const immichAlbumCarouselWidget: WidgetDefinition = {
icon: IconPhoto,
name: "Immich Album",
- description: "Shows a slideshow of your album pictures from Immich",
+ description: "Shows a slideshow of pictures from an Immich album or your full library",
path: "../../widgets/immich-album-carousel",
configuration: {
items: [
{
name: "Album",
- description: "Select the Immich album to show in the slideshow.",
+ description:
+ "Select an Immich album or All photos. An empty selection also shows random photos from your full library.",
values: { type: "string" },
- defaultValue: "",
+ defaultValue: "All photos",
},
{
name: "Interval in seconds",
diff --git a/packages/api/src/router/widgets/immich.ts b/packages/api/src/router/widgets/immich.ts
index 7e8c54d49..aa917271f 100644
--- a/packages/api/src/router/widgets/immich.ts
+++ b/packages/api/src/router/widgets/immich.ts
@@ -25,7 +25,7 @@ export const immichRouter = createTRPCRouter({
.concat(createImmichIntegrationMiddleware("query"))
.input(
z.object({
- albumId: z.string(),
+ albumId: z.string().optional(),
}),
)
.query(async ({ ctx, input }) => {
diff --git a/packages/integrations/src/immich/immich-integration.ts b/packages/integrations/src/immich/immich-integration.ts
index 6274802a5..9496482d6 100644
--- a/packages/integrations/src/immich/immich-integration.ts
+++ b/packages/integrations/src/immich/immich-integration.ts
@@ -1,11 +1,14 @@
import {
AssetMediaSize,
+ AssetTypeEnum,
+ AssetVisibility,
getAlbumInfo,
getAllAlbums,
getMyUser,
getServerStatistics,
init,
searchAssets,
+ searchRandom,
searchUsers,
} from "@immich/sdk";
import type { AssetResponseDto, MetadataSearchDto } from "@immich/sdk";
@@ -43,6 +46,7 @@ export interface ImmichAsset {
}
const logger = createLogger({ module: "immich-integration" });
+const randomPhotoCount = 50;
export class ImmichIntegration extends Integration {
constructor(integration: IntegrationInput) {
@@ -63,38 +67,36 @@ export class ImmichIntegration extends Integration {
};
}
- public async getAlbumAsync(albumId: string): Promise {
+ public async getAlbumAsync(albumId?: string): Promise {
this.initClient();
const requestOptions = this.getRequestOptions();
+
+ if (!albumId) {
+ const assets = await searchRandom(
+ {
+ randomSearchDto: {
+ size: randomPhotoCount,
+ type: AssetTypeEnum.Image,
+ visibility: AssetVisibility.Timeline,
+ },
+ },
+ requestOptions,
+ );
+
+ return {
+ id: "",
+ albumName: "",
+ assets: await this.createAssetsAsync(assets),
+ };
+ }
+
const [album, albumAssets] = await Promise.all([
getAlbumInfo({ id: albumId }, requestOptions),
this.searchAlbumAssetsAsync(albumId),
]);
- const imageProxy = new ImageProxy();
return {
albumName: album.albumName,
- assets: await Promise.all(
- albumAssets.map(async (asset) => {
- const publicLink = await imageProxy.createImageAsync(
- // This is the URL generated by the SDK's viewAsset call. Preview images are
- // browser-compatible while avoiding the large original asset download.
- this.url(`/api/assets/${asset.id}/thumbnail`, { size: AssetMediaSize.Preview }).toString(),
- {
- "x-api-key": this.getSecretValue("apiKey"),
- },
- );
- return {
- id: asset.id,
- type: asset.type,
- thumbhash: asset.thumbhash,
- fileCreatedAt: asset.fileCreatedAt,
- fileModifiedAt: asset.fileModifiedAt,
- originalPath: asset.originalPath,
- updatedAt: asset.updatedAt,
- publicLink,
- };
- }),
- ),
+ assets: await this.createAssetsAsync(albumAssets),
id: album.id,
};
}
@@ -136,6 +138,32 @@ export class ImmichIntegration extends Integration {
return assets;
}
+ private async createAssetsAsync(assets: AssetResponseDto[]): Promise {
+ const imageProxy = new ImageProxy();
+ return await Promise.all(
+ assets.map(async (asset) => {
+ const publicLink = await imageProxy.createImageAsync(
+ // This is the URL generated by the SDK's viewAsset call. Preview images are
+ // browser-compatible while avoiding the large original asset download.
+ this.url(`/api/assets/${asset.id}/thumbnail`, { size: AssetMediaSize.Preview }).toString(),
+ {
+ "x-api-key": this.getSecretValue("apiKey"),
+ },
+ );
+ return {
+ id: asset.id,
+ type: asset.type,
+ thumbhash: asset.thumbhash,
+ fileCreatedAt: asset.fileCreatedAt,
+ fileModifiedAt: asset.fileModifiedAt,
+ originalPath: asset.originalPath,
+ updatedAt: asset.updatedAt,
+ publicLink,
+ };
+ }),
+ );
+ }
+
private getRequestOptions(fetchAsync = fetchWithTrustedCertificatesAsync) {
return {
// Undici and node types are not the same
diff --git a/packages/request-handler/src/immich.ts b/packages/request-handler/src/immich.ts
index 56fc89bd0..a6e2a996c 100644
--- a/packages/request-handler/src/immich.ts
+++ b/packages/request-handler/src/immich.ts
@@ -33,7 +33,7 @@ export const immichAlbumsRequestHandler = createIntegrationRequestHandler<
export const immichAlbumRequestHandler = createIntegrationRequestHandler<
ImmichAlbum,
IntegrationKindByCategory<"photoService">,
- { albumId: string }
+ { albumId?: string }
>({
async requestAsync(integration, input) {
const integrationInstance = await createIntegrationAsync(integration);
diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json
index 1e48e4486..ae89ad4df 100644
--- a/packages/translation/src/lang/en.json
+++ b/packages/translation/src/lang/en.json
@@ -3006,10 +3006,11 @@
},
"immich-albumCarousel": {
"name": "Immich Album",
+ "allPhotos": "All photos",
"option": {
"albumId": {
"label": "Album",
- "description": "Select the Immich album to show in the slideshow."
+ "description": "Select an Immich album or All photos. An empty selection also shows random photos from your full library."
},
"rotationIntervalSeconds": {
"label": "Interval in seconds",
@@ -3024,9 +3025,8 @@
"description": "Show photos in a random order instead of always starting with the same album order."
}
},
- "description": "Slideshow of pictures in your Immich album",
- "noAlbumSelected": "No album selected, edit widget and choose an album",
- "noPhotos": "No photos in this album"
+ "description": "Slideshow of pictures from an Immich album or your full library",
+ "noPhotos": "No photos found"
},
"downloads": {
"name": "Download Client",
diff --git a/packages/widgets/src/immich/album-carousel/component.tsx b/packages/widgets/src/immich/album-carousel/component.tsx
index ee7085757..49bb4ceec 100644
--- a/packages/widgets/src/immich/album-carousel/component.tsx
+++ b/packages/widgets/src/immich/album-carousel/component.tsx
@@ -11,6 +11,7 @@ import { useI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
import classes from "./component.module.css";
+import { ALL_PHOTOS_ALBUM_ID } from "./constants";
export default function ImmichAlbumCarouselWidget({
integrationIds,
@@ -21,9 +22,9 @@ export default function ImmichAlbumCarouselWidget({
const { data: album } = clientApi.widget.immich.getAlbum.useQuery(
{
integrationId: integrationIds[0] ?? "",
- albumId: options.albumId ?? "",
+ albumId: options.albumId && options.albumId !== ALL_PHOTOS_ALBUM_ID ? options.albumId : undefined,
},
- { enabled: Boolean(options.albumId) },
+ { enabled: integrationIds.length > 0 },
);
const photoAssets = useMemo(() => {
@@ -35,10 +36,6 @@ export default function ImmichAlbumCarouselWidget({
setCurrentPhotoIndex(0);
}, [photoAssets]);
- if (!options.albumId) {
- return ;
- }
-
if (!album) return ;
if (album.assets.length === 0) {
@@ -117,20 +114,6 @@ function Carousel({ assets, currentIndex, setCurrentIndex, rotationInterval, sho
);
}
-function NoAlbumSelected() {
- const t = useI18n();
- return (
-
-
-
-
- {t("widget.immich-albumCarousel.noAlbumSelected")}
-
-
-
- );
-}
-
function NoPhotosInAlbum() {
const t = useI18n();
return (
diff --git a/packages/widgets/src/immich/album-carousel/constants.ts b/packages/widgets/src/immich/album-carousel/constants.ts
new file mode 100644
index 000000000..0371578de
--- /dev/null
+++ b/packages/widgets/src/immich/album-carousel/constants.ts
@@ -0,0 +1 @@
+export const ALL_PHOTOS_ALBUM_ID = "all";
diff --git a/packages/widgets/src/immich/album-carousel/index.ts b/packages/widgets/src/immich/album-carousel/index.ts
index 0ae11ef79..8c4223ef3 100644
--- a/packages/widgets/src/immich/album-carousel/index.ts
+++ b/packages/widgets/src/immich/album-carousel/index.ts
@@ -2,9 +2,11 @@ import { IconPhoto } from "@tabler/icons-react";
import z from "zod";
import { clientApi } from "@homarr/api/client";
+import { useScopedI18n } from "@homarr/translation/client";
import { createWidgetDefinition } from "../../definition";
import { optionsBuilder } from "../../options";
+import { ALL_PHOTOS_ALBUM_ID } from "./constants";
export const { definition, componentLoader } = createWidgetDefinition("immich-albumCarousel", {
icon: IconPhoto,
@@ -15,9 +17,11 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-al
createOptions() {
return optionsBuilder.from((factory) => ({
albumId: factory.integrationSelect({
+ defaultValue: ALL_PHOTOS_ALBUM_ID,
withDescription: true,
clearable: true,
useOptions: (integrationIds: string[]) => {
+ const t = useScopedI18n("widget.immich-albumCarousel");
const {
data = [],
isPending,
@@ -26,7 +30,14 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-al
{ integrationId: integrationIds[0] ?? "" },
{ enabled: integrationIds.length > 0, staleTime: 15 * 60 * 1000 },
);
- return { data: data.map((album) => ({ value: album.id, label: album.albumName })), isPending, isError };
+ return {
+ data: [
+ { value: ALL_PHOTOS_ALBUM_ID, label: t("allPhotos") },
+ ...data.map((album) => ({ value: album.id, label: album.albumName })),
+ ],
+ isPending,
+ isError,
+ };
},
}),
rotationIntervalSeconds: factory.number({