Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dcb7ea929 | ||
|
|
d602c573fb |
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: 'Immich Album Carousel'
|
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
|
hide_title: true
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -37,4 +37,3 @@ import screen from './img/screen.png';
|
|||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
<WidgetConfig configuration={immichAlbumCarouselWidget.configuration} />
|
<WidgetConfig configuration={immichAlbumCarouselWidget.configuration} />
|
||||||
|
|
||||||
@@ -4,15 +4,16 @@ import { IconPhoto } from "@tabler/icons-react";
|
|||||||
export const immichAlbumCarouselWidget: WidgetDefinition = {
|
export const immichAlbumCarouselWidget: WidgetDefinition = {
|
||||||
icon: IconPhoto,
|
icon: IconPhoto,
|
||||||
name: "Immich Album",
|
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",
|
path: "../../widgets/immich-album-carousel",
|
||||||
configuration: {
|
configuration: {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: "Album",
|
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" },
|
values: { type: "string" },
|
||||||
defaultValue: "",
|
defaultValue: "All photos",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Interval in seconds",
|
name: "Interval in seconds",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const immichRouter = createTRPCRouter({
|
|||||||
.concat(createImmichIntegrationMiddleware("query"))
|
.concat(createImmichIntegrationMiddleware("query"))
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
albumId: z.string(),
|
albumId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
AssetMediaSize,
|
AssetMediaSize,
|
||||||
|
AssetTypeEnum,
|
||||||
|
AssetVisibility,
|
||||||
getAlbumInfo,
|
getAlbumInfo,
|
||||||
getAllAlbums,
|
getAllAlbums,
|
||||||
getMyUser,
|
getMyUser,
|
||||||
getServerStatistics,
|
getServerStatistics,
|
||||||
init,
|
init,
|
||||||
searchAssets,
|
searchAssets,
|
||||||
|
searchRandom,
|
||||||
searchUsers,
|
searchUsers,
|
||||||
} from "@immich/sdk";
|
} from "@immich/sdk";
|
||||||
import type { AssetResponseDto, MetadataSearchDto } from "@immich/sdk";
|
import type { AssetResponseDto, MetadataSearchDto } from "@immich/sdk";
|
||||||
@@ -43,6 +46,7 @@ export interface ImmichAsset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logger = createLogger({ module: "immich-integration" });
|
const logger = createLogger({ module: "immich-integration" });
|
||||||
|
const randomPhotoCount = 50;
|
||||||
|
|
||||||
export class ImmichIntegration extends Integration {
|
export class ImmichIntegration extends Integration {
|
||||||
constructor(integration: IntegrationInput) {
|
constructor(integration: IntegrationInput) {
|
||||||
@@ -63,38 +67,36 @@ export class ImmichIntegration extends Integration {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAlbumAsync(albumId: string): Promise<ImmichAlbum> {
|
public async getAlbumAsync(albumId?: string): Promise<ImmichAlbum> {
|
||||||
this.initClient();
|
this.initClient();
|
||||||
const requestOptions = this.getRequestOptions();
|
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([
|
const [album, albumAssets] = await Promise.all([
|
||||||
getAlbumInfo({ id: albumId }, requestOptions),
|
getAlbumInfo({ id: albumId }, requestOptions),
|
||||||
this.searchAlbumAssetsAsync(albumId),
|
this.searchAlbumAssetsAsync(albumId),
|
||||||
]);
|
]);
|
||||||
const imageProxy = new ImageProxy();
|
|
||||||
return {
|
return {
|
||||||
albumName: album.albumName,
|
albumName: album.albumName,
|
||||||
assets: await Promise.all(
|
assets: await this.createAssetsAsync(albumAssets),
|
||||||
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,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
id: album.id,
|
id: album.id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -136,6 +138,32 @@ export class ImmichIntegration extends Integration {
|
|||||||
return assets;
|
return assets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async createAssetsAsync(assets: AssetResponseDto[]): Promise<ImmichAsset[]> {
|
||||||
|
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) {
|
private getRequestOptions(fetchAsync = fetchWithTrustedCertificatesAsync) {
|
||||||
return {
|
return {
|
||||||
// Undici and node types are not the same
|
// Undici and node types are not the same
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const immichAlbumsRequestHandler = createIntegrationRequestHandler<
|
|||||||
export const immichAlbumRequestHandler = createIntegrationRequestHandler<
|
export const immichAlbumRequestHandler = createIntegrationRequestHandler<
|
||||||
ImmichAlbum,
|
ImmichAlbum,
|
||||||
IntegrationKindByCategory<"photoService">,
|
IntegrationKindByCategory<"photoService">,
|
||||||
{ albumId: string }
|
{ albumId?: string }
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, input) {
|
async requestAsync(integration, input) {
|
||||||
const integrationInstance = await createIntegrationAsync(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
|
|||||||
@@ -3006,10 +3006,11 @@
|
|||||||
},
|
},
|
||||||
"immich-albumCarousel": {
|
"immich-albumCarousel": {
|
||||||
"name": "Immich Album",
|
"name": "Immich Album",
|
||||||
|
"allPhotos": "All photos",
|
||||||
"option": {
|
"option": {
|
||||||
"albumId": {
|
"albumId": {
|
||||||
"label": "Album",
|
"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": {
|
"rotationIntervalSeconds": {
|
||||||
"label": "Interval in seconds",
|
"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": "Show photos in a random order instead of always starting with the same album order."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "Slideshow of pictures in your Immich album",
|
"description": "Slideshow of pictures from an Immich album or your full library",
|
||||||
"noAlbumSelected": "No album selected, edit widget and choose an album",
|
"noPhotos": "No photos found"
|
||||||
"noPhotos": "No photos in this album"
|
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"name": "Download Client",
|
"name": "Download Client",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useI18n } from "@homarr/translation/client";
|
|||||||
import { WidgetEmptyState } from "../../common/empty-state";
|
import { WidgetEmptyState } from "../../common/empty-state";
|
||||||
import type { WidgetComponentProps } from "../../definition";
|
import type { WidgetComponentProps } from "../../definition";
|
||||||
import classes from "./component.module.css";
|
import classes from "./component.module.css";
|
||||||
|
import { ALL_PHOTOS_ALBUM_ID } from "./constants";
|
||||||
|
|
||||||
export default function ImmichAlbumCarouselWidget({
|
export default function ImmichAlbumCarouselWidget({
|
||||||
integrationIds,
|
integrationIds,
|
||||||
@@ -21,9 +22,9 @@ export default function ImmichAlbumCarouselWidget({
|
|||||||
const { data: album } = clientApi.widget.immich.getAlbum.useQuery(
|
const { data: album } = clientApi.widget.immich.getAlbum.useQuery(
|
||||||
{
|
{
|
||||||
integrationId: integrationIds[0] ?? "",
|
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(() => {
|
const photoAssets = useMemo(() => {
|
||||||
@@ -35,10 +36,6 @@ export default function ImmichAlbumCarouselWidget({
|
|||||||
setCurrentPhotoIndex(0);
|
setCurrentPhotoIndex(0);
|
||||||
}, [photoAssets]);
|
}, [photoAssets]);
|
||||||
|
|
||||||
if (!options.albumId) {
|
|
||||||
return <NoAlbumSelected />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!album) return <WidgetEmptyState />;
|
if (!album) return <WidgetEmptyState />;
|
||||||
|
|
||||||
if (album.assets.length === 0) {
|
if (album.assets.length === 0) {
|
||||||
@@ -117,20 +114,6 @@ function Carousel({ assets, currentIndex, setCurrentIndex, rotationInterval, sho
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NoAlbumSelected() {
|
|
||||||
const t = useI18n();
|
|
||||||
return (
|
|
||||||
<Center h="100%">
|
|
||||||
<Stack align="center" gap="xs">
|
|
||||||
<IconAlertCircle size={32} />
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{t("widget.immich-albumCarousel.noAlbumSelected")}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NoPhotosInAlbum() {
|
function NoPhotosInAlbum() {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const ALL_PHOTOS_ALBUM_ID = "all";
|
||||||
@@ -2,9 +2,11 @@ import { IconPhoto } from "@tabler/icons-react";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import { createWidgetDefinition } from "../../definition";
|
import { createWidgetDefinition } from "../../definition";
|
||||||
import { optionsBuilder } from "../../options";
|
import { optionsBuilder } from "../../options";
|
||||||
|
import { ALL_PHOTOS_ALBUM_ID } from "./constants";
|
||||||
|
|
||||||
export const { definition, componentLoader } = createWidgetDefinition("immich-albumCarousel", {
|
export const { definition, componentLoader } = createWidgetDefinition("immich-albumCarousel", {
|
||||||
icon: IconPhoto,
|
icon: IconPhoto,
|
||||||
@@ -15,9 +17,11 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-al
|
|||||||
createOptions() {
|
createOptions() {
|
||||||
return optionsBuilder.from((factory) => ({
|
return optionsBuilder.from((factory) => ({
|
||||||
albumId: factory.integrationSelect({
|
albumId: factory.integrationSelect({
|
||||||
|
defaultValue: ALL_PHOTOS_ALBUM_ID,
|
||||||
withDescription: true,
|
withDescription: true,
|
||||||
clearable: true,
|
clearable: true,
|
||||||
useOptions: (integrationIds: string[]) => {
|
useOptions: (integrationIds: string[]) => {
|
||||||
|
const t = useScopedI18n("widget.immich-albumCarousel");
|
||||||
const {
|
const {
|
||||||
data = [],
|
data = [],
|
||||||
isPending,
|
isPending,
|
||||||
@@ -26,7 +30,14 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-al
|
|||||||
{ integrationId: integrationIds[0] ?? "" },
|
{ integrationId: integrationIds[0] ?? "" },
|
||||||
{ enabled: integrationIds.length > 0, staleTime: 15 * 60 * 1000 },
|
{ 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({
|
rotationIntervalSeconds: factory.number({
|
||||||
|
|||||||
Reference in new issue
Block a user