mirror of
https://github.com/immich-app/immich.git
synced 2025-12-14 01:10:38 +03:00
Compare commits
1 Commits
feature/re
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6b0f0c76a |
@@ -1604,6 +1604,7 @@
|
|||||||
"read_changelog": "Read Changelog",
|
"read_changelog": "Read Changelog",
|
||||||
"readonly_mode_disabled": "Read-only mode disabled",
|
"readonly_mode_disabled": "Read-only mode disabled",
|
||||||
"readonly_mode_enabled": "Read-only mode enabled",
|
"readonly_mode_enabled": "Read-only mode enabled",
|
||||||
|
"plugins": "Plugins",
|
||||||
"ready_for_upload": "Ready for upload",
|
"ready_for_upload": "Ready for upload",
|
||||||
"reassign": "Reassign",
|
"reassign": "Reassign",
|
||||||
"reassigned_assets_to_existing_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}",
|
"reassigned_assets_to_existing_person": "Re-assigned {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}",
|
||||||
|
|||||||
@@ -5717,6 +5717,154 @@
|
|||||||
"description": "This endpoint requires the `person.read` permission."
|
"description": "This endpoint requires the `person.read` permission."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/plugins": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "searchPlugins",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "isEnabled",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isInstalled",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isTrusted",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/PluginResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Plugin"
|
||||||
|
],
|
||||||
|
"x-immich-admin-only": true,
|
||||||
|
"x-immich-permission": "plugin.read",
|
||||||
|
"description": "This endpoint is an admin-only route, and requires the `plugin.read` permission."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/plugins/{id}": {
|
||||||
|
"delete": {
|
||||||
|
"operationId": "deletePlugin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Plugin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"operationId": "updatePlugin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PluginUpdateDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PluginResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Plugin"
|
||||||
|
],
|
||||||
|
"x-immich-admin-only": true,
|
||||||
|
"x-immich-permission": "plugin.update",
|
||||||
|
"description": "This endpoint is an admin-only route, and requires the `plugin.update` permission."
|
||||||
|
}
|
||||||
|
},
|
||||||
"/search/cities": {
|
"/search/cities": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getAssetsByCity",
|
"operationId": "getAssetsByCity",
|
||||||
@@ -13211,6 +13359,9 @@
|
|||||||
"person.statistics",
|
"person.statistics",
|
||||||
"person.merge",
|
"person.merge",
|
||||||
"person.reassign",
|
"person.reassign",
|
||||||
|
"plugin.read",
|
||||||
|
"plugin.update",
|
||||||
|
"plugin.delete",
|
||||||
"pinCode.create",
|
"pinCode.create",
|
||||||
"pinCode.update",
|
"pinCode.update",
|
||||||
"pinCode.delete",
|
"pinCode.delete",
|
||||||
@@ -13498,6 +13649,66 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"PluginResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"createdAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isInstalled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isTrusted": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"packageId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"createdAt",
|
||||||
|
"description",
|
||||||
|
"id",
|
||||||
|
"isEnabled",
|
||||||
|
"isInstalled",
|
||||||
|
"isTrusted",
|
||||||
|
"name",
|
||||||
|
"packageId",
|
||||||
|
"updatedAt",
|
||||||
|
"version"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"PluginUpdateDto": {
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"isEnabled"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"PurchaseResponse": {
|
"PurchaseResponse": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"hideBuyButtonUntil": {
|
"hideBuyButtonUntil": {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { NotificationController } from 'src/controllers/notification.controller'
|
|||||||
import { OAuthController } from 'src/controllers/oauth.controller';
|
import { OAuthController } from 'src/controllers/oauth.controller';
|
||||||
import { PartnerController } from 'src/controllers/partner.controller';
|
import { PartnerController } from 'src/controllers/partner.controller';
|
||||||
import { PersonController } from 'src/controllers/person.controller';
|
import { PersonController } from 'src/controllers/person.controller';
|
||||||
|
import { PluginController } from 'src/controllers/plugin.controller';
|
||||||
import { SearchController } from 'src/controllers/search.controller';
|
import { SearchController } from 'src/controllers/search.controller';
|
||||||
import { ServerController } from 'src/controllers/server.controller';
|
import { ServerController } from 'src/controllers/server.controller';
|
||||||
import { SessionController } from 'src/controllers/session.controller';
|
import { SessionController } from 'src/controllers/session.controller';
|
||||||
@@ -54,6 +55,7 @@ export const controllers = [
|
|||||||
OAuthController,
|
OAuthController,
|
||||||
PartnerController,
|
PartnerController,
|
||||||
PersonController,
|
PersonController,
|
||||||
|
PluginController,
|
||||||
SearchController,
|
SearchController,
|
||||||
ServerController,
|
ServerController,
|
||||||
SessionController,
|
SessionController,
|
||||||
|
|||||||
36
server/src/controllers/plugin.controller.ts
Normal file
36
server/src/controllers/plugin.controller.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Put, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { PluginResponseDto, PluginSearchDto, PluginUpdateDto } from 'src/dtos/plugin.dto';
|
||||||
|
import { Permission } from 'src/enum';
|
||||||
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { PluginService } from 'src/services/plugin.service';
|
||||||
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
|
@ApiTags('Plugin')
|
||||||
|
@Controller('plugins')
|
||||||
|
export class PluginController {
|
||||||
|
constructor(private service: PluginService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Authenticated({ admin: true, permission: Permission.PluginRead })
|
||||||
|
searchPlugins(@Auth() auth: AuthDto, @Query() dto: PluginSearchDto): Promise<PluginResponseDto[]> {
|
||||||
|
return this.service.search(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
@Authenticated({ admin: true, permission: Permission.PluginUpdate })
|
||||||
|
updatePlugin(
|
||||||
|
@Auth() auth: AuthDto,
|
||||||
|
@Param() { id }: UUIDParamDto,
|
||||||
|
@Body() dto: PluginUpdateDto,
|
||||||
|
): Promise<PluginResponseDto> {
|
||||||
|
return this.service.update(auth, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
deletePlugin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||||
|
return this.service.delete(auth, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -144,6 +144,19 @@ export type UserAdmin = User & {
|
|||||||
metadata: UserMetadataItem[];
|
metadata: UserMetadataItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Plugin = {
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
packageId: string;
|
||||||
|
version: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
isEnabled: boolean;
|
||||||
|
isInstalled: boolean;
|
||||||
|
isTrusted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type StorageAsset = {
|
export type StorageAsset = {
|
||||||
id: string;
|
id: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
|
|||||||
58
server/src/dtos/plugin.dto.ts
Normal file
58
server/src/dtos/plugin.dto.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsBoolean, IsString } from 'class-validator';
|
||||||
|
import { Plugin } from 'src/database';
|
||||||
|
import { Optional, ValidateBoolean } from 'src/validation';
|
||||||
|
|
||||||
|
export class PluginSearchDto {
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
isEnabled?: boolean;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
isTrusted?: boolean;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
isInstalled?: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Optional()
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginImportDto {
|
||||||
|
url!: string;
|
||||||
|
install!: boolean;
|
||||||
|
isEnabled!: boolean;
|
||||||
|
isTrusted!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginUpdateDto {
|
||||||
|
@IsBoolean()
|
||||||
|
isEnabled!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginResponseDto {
|
||||||
|
id!: string;
|
||||||
|
createdAt!: Date;
|
||||||
|
updatedAt!: Date;
|
||||||
|
packageId!: string;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
version!: number;
|
||||||
|
name!: string;
|
||||||
|
description!: string;
|
||||||
|
isEnabled!: boolean;
|
||||||
|
isInstalled!: boolean;
|
||||||
|
isTrusted!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapPlugin = (plugin: Plugin): PluginResponseDto => ({
|
||||||
|
id: plugin.id,
|
||||||
|
createdAt: plugin.createdAt,
|
||||||
|
updatedAt: plugin.updatedAt,
|
||||||
|
packageId: plugin.packageId,
|
||||||
|
version: plugin.version,
|
||||||
|
name: plugin.name,
|
||||||
|
description: plugin.description,
|
||||||
|
isEnabled: plugin.isEnabled,
|
||||||
|
isInstalled: plugin.isInstalled,
|
||||||
|
isTrusted: plugin.isTrusted,
|
||||||
|
});
|
||||||
@@ -164,6 +164,10 @@ export enum Permission {
|
|||||||
PersonMerge = 'person.merge',
|
PersonMerge = 'person.merge',
|
||||||
PersonReassign = 'person.reassign',
|
PersonReassign = 'person.reassign',
|
||||||
|
|
||||||
|
PluginRead = 'plugin.read',
|
||||||
|
PluginUpdate = 'plugin.update',
|
||||||
|
PluginDelete = 'plugin.delete',
|
||||||
|
|
||||||
PinCodeCreate = 'pinCode.create',
|
PinCodeCreate = 'pinCode.create',
|
||||||
PinCodeUpdate = 'pinCode.update',
|
PinCodeUpdate = 'pinCode.update',
|
||||||
PinCodeDelete = 'pinCode.delete',
|
PinCodeDelete = 'pinCode.delete',
|
||||||
|
|||||||
91
server/src/interfaces/plugin.interface.ts
Normal file
91
server/src/interfaces/plugin.interface.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
export type PluginFactory = {
|
||||||
|
register: () => MaybePromise<Plugin>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PluginLike = Plugin | PluginFactory | { default: Plugin } | { plugin: Plugin };
|
||||||
|
|
||||||
|
export interface Plugin<T extends PluginConfig | undefined = undefined> {
|
||||||
|
version: 1;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
actions: PluginAction<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginAction<T extends PluginConfig | undefined = undefined> = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
events?: EventType[];
|
||||||
|
config?: T;
|
||||||
|
} & (
|
||||||
|
| { type: ActionType.ASSET; onAction: OnAction<T, AssetDto> }
|
||||||
|
| { type: ActionType.ALBUM; onAction: OnAction<T, AlbumDto> }
|
||||||
|
| { type: ActionType.ALBUM_ASSET; onAction: OnAction<T, { asset: AssetDto; album: AlbumDto }> }
|
||||||
|
);
|
||||||
|
|
||||||
|
export type OnAction<T extends PluginConfig | undefined, D = PluginActionData> = T extends undefined
|
||||||
|
? (ctx: PluginContext, data: D) => MaybePromise<void>
|
||||||
|
: (ctx: PluginContext, data: D, config: InferConfig<T>) => MaybePromise<void>;
|
||||||
|
|
||||||
|
export interface PluginContext {
|
||||||
|
updateAsset: (asset: { id: string; isArchived: boolean }) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginActionData = { data: { asset?: AssetDto; album?: AlbumDto } } & (
|
||||||
|
| { type: EventType.ASSET_UPLOAD; data: { asset: AssetDto } }
|
||||||
|
| { type: EventType.ASSET_UPDATE; data: { asset: AssetDto } }
|
||||||
|
| { type: EventType.ASSET_TRASH; data: { asset: AssetDto } }
|
||||||
|
| { type: EventType.ASSET_DELETE; data: { asset: AssetDto } }
|
||||||
|
| { type: EventType.ALBUM_CREATE; data: { album: AlbumDto } }
|
||||||
|
| { type: EventType.ALBUM_UPDATE; data: { album: AlbumDto } }
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PluginConfig = Record<string, ConfigItem>;
|
||||||
|
|
||||||
|
export type ConfigItem = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
required?: boolean;
|
||||||
|
} & { [K in Types]: { type: K; default?: InferType<K> } }[Types];
|
||||||
|
|
||||||
|
export type InferType<T extends Types> = T extends 'string'
|
||||||
|
? string
|
||||||
|
: T extends 'date'
|
||||||
|
? Date
|
||||||
|
: T extends 'number'
|
||||||
|
? number
|
||||||
|
: T extends 'boolean'
|
||||||
|
? boolean
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type Types = 'string' | 'boolean' | 'number' | 'date';
|
||||||
|
type MaybePromise<T = void> = Promise<T> | T;
|
||||||
|
type IfRequired<T extends ConfigItem, Type> = T['required'] extends true ? Type : Type | undefined;
|
||||||
|
type InferConfig<T> = T extends PluginConfig
|
||||||
|
? {
|
||||||
|
[K in keyof T]: IfRequired<T[K], InferType<T[K]['type']>>;
|
||||||
|
}
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export enum ActionType {
|
||||||
|
ASSET = 'asset',
|
||||||
|
ALBUM = 'album',
|
||||||
|
ALBUM_ASSET = 'album-asset',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EventType {
|
||||||
|
ASSET_UPLOAD = 'asset.upload',
|
||||||
|
ASSET_UPDATE = 'asset.update',
|
||||||
|
ASSET_TRASH = 'asset.trash',
|
||||||
|
ASSET_DELETE = 'asset.delete',
|
||||||
|
ASSET_ARCHIVE = 'asset.archive',
|
||||||
|
ASSET_UNARCHIVE = 'asset.unarchive',
|
||||||
|
|
||||||
|
ALBUM_CREATE = 'album.create',
|
||||||
|
ALBUM_UPDATE = 'album.update',
|
||||||
|
ALBUM_DELETE = 'album.delete',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AssetDto = { id: string; type: 'asset' };
|
||||||
|
export type AlbumDto = { id: string; type: 'album' };
|
||||||
92
server/src/plugin_types.ts
Normal file
92
server/src/plugin_types.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import sdk from '../../open-api/typescript-sdk';
|
||||||
|
|
||||||
|
export type Plugin = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
filters: Filter[];
|
||||||
|
// actions: Action[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum EntityType {
|
||||||
|
Asset = 'asset',
|
||||||
|
Album = 'album',
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
type: EntityType;
|
||||||
|
configuration?: Config[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type FilterContext<C = Record<string, any>, D = any> = {
|
||||||
|
api: {
|
||||||
|
getAssetAlbums: (assetId: string) => Promise<any[]>;
|
||||||
|
};
|
||||||
|
sdk: typeof sdk;
|
||||||
|
config: C;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AssetFilter = {
|
||||||
|
type: EntityType.Asset;
|
||||||
|
filter: (ctx: FilterContext, input: { asset: { id: string } }) => Promise<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AlbumFilter = {
|
||||||
|
type: EntityType.Album;
|
||||||
|
filter: (ctx: FilterContext, input: { album: { id: string; name: string } }) => Promise<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Filter = PluginItem & (AssetFilter | AlbumFilter);
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
key: string;
|
||||||
|
type: PluginConfigType;
|
||||||
|
required?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PluginConfigType = 'string' | 'number' | 'boolean' | 'date' | 'albumId' | 'assetId';
|
||||||
|
|
||||||
|
const authenticate = (ctx: FilterContext) => {
|
||||||
|
const
|
||||||
|
sdk.init()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const corePlugin: Plugin = {
|
||||||
|
id: 'immich',
|
||||||
|
name: 'Immich Core Plugin',
|
||||||
|
description: 'Core actions and filters for workflows',
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
id: 'core.notInAnyAlbum',
|
||||||
|
name: 'Not in any album',
|
||||||
|
description: 'Filters assets that are not in any album',
|
||||||
|
type: EntityType.Asset,
|
||||||
|
async filter(ctx, { asset }) {
|
||||||
|
const albums = await ctx.sdk.getAllAlbums({ assetId: asset.id });
|
||||||
|
return albums.length === 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'core.notInAlbum',
|
||||||
|
name: 'Not in an album',
|
||||||
|
description: 'Run on assets not in the specified album',
|
||||||
|
type: EntityType.Asset,
|
||||||
|
configuration: [
|
||||||
|
{
|
||||||
|
key: 'albumId',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
async filter(ctx, { asset }) {
|
||||||
|
// missing api to check if an asset is in an album
|
||||||
|
const albums = await ctx.sdk.getAllAlbums({ assetId: asset.id });
|
||||||
|
return !!albums.find((album) => album.id === ctx.config.albumId);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
55
server/src/plugins/asset.plugin.ts
Normal file
55
server/src/plugins/asset.plugin.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { ActionType, AssetDto, Plugin, PluginContext } from 'src/interfaces/plugin.interface';
|
||||||
|
|
||||||
|
const onAsset = async (ctx: PluginContext, asset: AssetDto) => {
|
||||||
|
await ctx.updateAsset({ id: asset.id, isArchived: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const plugin: Plugin = {
|
||||||
|
version: 1,
|
||||||
|
id: 'immich-plugins',
|
||||||
|
name: 'Asset Plugin',
|
||||||
|
description: 'Immich asset plugin',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 'asset.favorite',
|
||||||
|
name: '',
|
||||||
|
type: ActionType.ASSET,
|
||||||
|
description: '',
|
||||||
|
onAction: async (ctx, asset) => {
|
||||||
|
await ctx.updateAsset({ id: asset.id, isArchived: false });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'asset.unfavorite',
|
||||||
|
name: '',
|
||||||
|
type: ActionType.ASSET,
|
||||||
|
description: '',
|
||||||
|
onAction: () => {
|
||||||
|
console.log('Unfavorite');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'asset.action',
|
||||||
|
name: '',
|
||||||
|
type: ActionType.ASSET,
|
||||||
|
description: '',
|
||||||
|
onAction: (ctx, asset) => onAsset(ctx, asset),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'album-asset.action',
|
||||||
|
name: '',
|
||||||
|
type: ActionType.ALBUM_ASSET,
|
||||||
|
description: '',
|
||||||
|
onAction: (ctx, { asset }) => onAsset(ctx, asset),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'asset.unarchive',
|
||||||
|
name: '',
|
||||||
|
type: ActionType.ASSET,
|
||||||
|
description: '',
|
||||||
|
onAction: () => {
|
||||||
|
console.log('Unarchive');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -27,6 +27,7 @@ import { NotificationRepository } from 'src/repositories/notification.repository
|
|||||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
import { PersonRepository } from 'src/repositories/person.repository';
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
|
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||||
import { SearchRepository } from 'src/repositories/search.repository';
|
import { SearchRepository } from 'src/repositories/search.repository';
|
||||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||||
@@ -44,7 +45,6 @@ import { TrashRepository } from 'src/repositories/trash.repository';
|
|||||||
import { UserRepository } from 'src/repositories/user.repository';
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||||
import { ViewRepository } from 'src/repositories/view-repository';
|
import { ViewRepository } from 'src/repositories/view-repository';
|
||||||
|
|
||||||
export const repositories = [
|
export const repositories = [
|
||||||
AccessRepository,
|
AccessRepository,
|
||||||
ActivityRepository,
|
ActivityRepository,
|
||||||
@@ -76,6 +76,7 @@ export const repositories = [
|
|||||||
PartnerRepository,
|
PartnerRepository,
|
||||||
PersonRepository,
|
PersonRepository,
|
||||||
ProcessRepository,
|
ProcessRepository,
|
||||||
|
PluginRepository,
|
||||||
SearchRepository,
|
SearchRepository,
|
||||||
SessionRepository,
|
SessionRepository,
|
||||||
ServerInfoRepository,
|
ServerInfoRepository,
|
||||||
|
|||||||
83
server/src/repositories/plugin.repository.ts
Normal file
83
server/src/repositories/plugin.repository.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||||
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { writeFile } from 'node:fs/promises';
|
||||||
|
import { PluginLike } from 'src/interfaces/plugin.interface';
|
||||||
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { DB } from 'src/schema';
|
||||||
|
import { PluginTable } from 'src/schema/tables/plugin.table';
|
||||||
|
|
||||||
|
type PluginSearchOptions = {
|
||||||
|
id?: string;
|
||||||
|
namespace?: string;
|
||||||
|
version?: number;
|
||||||
|
name?: string;
|
||||||
|
isEnabled?: boolean;
|
||||||
|
isInstalled?: boolean;
|
||||||
|
isTrusted?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PluginRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectKysely() private db: Kysely<DB>,
|
||||||
|
private logger: LoggingRepository,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(PluginRepository.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
search(options: PluginSearchOptions) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('plugin')
|
||||||
|
.select([
|
||||||
|
'id',
|
||||||
|
'packageId',
|
||||||
|
'version',
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'isEnabled',
|
||||||
|
'isInstalled',
|
||||||
|
'isTrusted',
|
||||||
|
'requirePath',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'deletedAt',
|
||||||
|
])
|
||||||
|
.$if(!!options.id, (qb) => qb.where('id', '=', options.id!))
|
||||||
|
.$if(!!options.version, (qb) => qb.where('version', '=', options.version!))
|
||||||
|
.$if(!!options.name, (qb) => qb.where('name', '=', options.name!))
|
||||||
|
.$if(!!options.isEnabled, (qb) => qb.where('isEnabled', '=', options.isEnabled!))
|
||||||
|
.$if(!!options.isInstalled, (qb) => qb.where('isInstalled', '=', options.isInstalled!))
|
||||||
|
.$if(!!options.isTrusted, (qb) => qb.where('isTrusted', '=', options.isTrusted!))
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
create(dto: Insertable<PluginTable>) {
|
||||||
|
return this.db.insertInto('plugin').values(dto).returningAll().executeTakeFirstOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id: string) {
|
||||||
|
return this.db.selectFrom('plugin').where('id', '=', id).executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(dto: Updateable<PluginTable>) {
|
||||||
|
return this.db.updateTable('plugin').set(dto).returningAll().executeTakeFirstOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
await this.db.deleteFrom('plugin').where('id', '=', id).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(url: string, downloadPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { json } = await fetch(url);
|
||||||
|
await writeFile(downloadPath, await json());
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error downloading the plugin from ${url}. ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load(pluginPath: string): Promise<PluginLike> {
|
||||||
|
return import(pluginPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
|
|||||||
import { PartnerTable } from 'src/schema/tables/partner.table';
|
import { PartnerTable } from 'src/schema/tables/partner.table';
|
||||||
import { PersonAuditTable } from 'src/schema/tables/person-audit.table';
|
import { PersonAuditTable } from 'src/schema/tables/person-audit.table';
|
||||||
import { PersonTable } from 'src/schema/tables/person.table';
|
import { PersonTable } from 'src/schema/tables/person.table';
|
||||||
|
import { PluginTable } from 'src/schema/tables/plugin.table';
|
||||||
import { SessionTable } from 'src/schema/tables/session.table';
|
import { SessionTable } from 'src/schema/tables/session.table';
|
||||||
import { SharedLinkAssetTable } from 'src/schema/tables/shared-link-asset.table';
|
import { SharedLinkAssetTable } from 'src/schema/tables/shared-link-asset.table';
|
||||||
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
|
||||||
@@ -105,6 +106,7 @@ export class ImmichDatabase {
|
|||||||
PartnerTable,
|
PartnerTable,
|
||||||
PersonTable,
|
PersonTable,
|
||||||
PersonAuditTable,
|
PersonAuditTable,
|
||||||
|
PluginTable,
|
||||||
SessionTable,
|
SessionTable,
|
||||||
SharedLinkAssetTable,
|
SharedLinkAssetTable,
|
||||||
SharedLinkTable,
|
SharedLinkTable,
|
||||||
@@ -202,6 +204,8 @@ export interface DB {
|
|||||||
person: PersonTable;
|
person: PersonTable;
|
||||||
person_audit: PersonAuditTable;
|
person_audit: PersonAuditTable;
|
||||||
|
|
||||||
|
plugin: PluginTable;
|
||||||
|
|
||||||
session: SessionTable;
|
session: SessionTable;
|
||||||
session_sync_checkpoint: SessionSyncCheckpointTable;
|
session_sync_checkpoint: SessionSyncCheckpointTable;
|
||||||
|
|
||||||
|
|||||||
61
server/src/schema/tables/plugin.table.ts
Normal file
61
server/src/schema/tables/plugin.table.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Insertable } from 'kysely';
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
|
Generated,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Table,
|
||||||
|
Timestamp,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
|
const plugin: Insertable<PluginTable> = {
|
||||||
|
version: 1,
|
||||||
|
id: '123',
|
||||||
|
name: 'Immich Core Plugin',
|
||||||
|
description: 'Core plugins for Immich',
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
deletedAt: null,
|
||||||
|
packageId: 'immich-plugin-',
|
||||||
|
};
|
||||||
|
|
||||||
|
@Table('plugins')
|
||||||
|
export class PluginTable {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: Generated<string>;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt!: Generated<Timestamp>;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt!: Generated<Timestamp>;
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt!: Date | null;
|
||||||
|
|
||||||
|
@Column({ unique: true })
|
||||||
|
packageId!: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
version!: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: true })
|
||||||
|
isEnabled!: Generated<boolean>;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
isInstalled!: Generated<boolean>;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
isTrusted!: Generated<boolean>;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
requirePath!: string | null;
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ import { NotificationRepository } from 'src/repositories/notification.repository
|
|||||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
import { PersonRepository } from 'src/repositories/person.repository';
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
|
import { PluginRepository } from 'src/repositories/plugin.repository';
|
||||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||||
import { SearchRepository } from 'src/repositories/search.repository';
|
import { SearchRepository } from 'src/repositories/search.repository';
|
||||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||||
@@ -138,6 +139,7 @@ export class BaseService {
|
|||||||
protected oauthRepository: OAuthRepository,
|
protected oauthRepository: OAuthRepository,
|
||||||
protected partnerRepository: PartnerRepository,
|
protected partnerRepository: PartnerRepository,
|
||||||
protected personRepository: PersonRepository,
|
protected personRepository: PersonRepository,
|
||||||
|
protected pluginRepository: PluginRepository,
|
||||||
protected processRepository: ProcessRepository,
|
protected processRepository: ProcessRepository,
|
||||||
protected searchRepository: SearchRepository,
|
protected searchRepository: SearchRepository,
|
||||||
protected serverInfoRepository: ServerInfoRepository,
|
protected serverInfoRepository: ServerInfoRepository,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { NotificationAdminService } from 'src/services/notification-admin.servic
|
|||||||
import { NotificationService } from 'src/services/notification.service';
|
import { NotificationService } from 'src/services/notification.service';
|
||||||
import { PartnerService } from 'src/services/partner.service';
|
import { PartnerService } from 'src/services/partner.service';
|
||||||
import { PersonService } from 'src/services/person.service';
|
import { PersonService } from 'src/services/person.service';
|
||||||
|
import { PluginService } from 'src/services/plugin.service';
|
||||||
import { SearchService } from 'src/services/search.service';
|
import { SearchService } from 'src/services/search.service';
|
||||||
import { ServerService } from 'src/services/server.service';
|
import { ServerService } from 'src/services/server.service';
|
||||||
import { SessionService } from 'src/services/session.service';
|
import { SessionService } from 'src/services/session.service';
|
||||||
@@ -40,6 +41,7 @@ import { UserAdminService } from 'src/services/user-admin.service';
|
|||||||
import { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
import { VersionService } from 'src/services/version.service';
|
import { VersionService } from 'src/services/version.service';
|
||||||
import { ViewService } from 'src/services/view.service';
|
import { ViewService } from 'src/services/view.service';
|
||||||
|
import { WorkflowService } from 'src/services/workflow.service';
|
||||||
|
|
||||||
export const services = [
|
export const services = [
|
||||||
ApiKeyService,
|
ApiKeyService,
|
||||||
@@ -66,6 +68,7 @@ export const services = [
|
|||||||
NotificationAdminService,
|
NotificationAdminService,
|
||||||
PartnerService,
|
PartnerService,
|
||||||
PersonService,
|
PersonService,
|
||||||
|
PluginService,
|
||||||
SearchService,
|
SearchService,
|
||||||
ServerService,
|
ServerService,
|
||||||
SessionService,
|
SessionService,
|
||||||
@@ -84,4 +87,5 @@ export const services = [
|
|||||||
UserService,
|
UserService,
|
||||||
VersionService,
|
VersionService,
|
||||||
ViewService,
|
ViewService,
|
||||||
|
WorkflowService,
|
||||||
];
|
];
|
||||||
|
|||||||
42
server/src/services/plugin.service.ts
Normal file
42
server/src/services/plugin.service.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Plugin } from 'src/database';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { mapPlugin, PluginSearchDto, PluginUpdateDto } from 'src/dtos/plugin.dto';
|
||||||
|
import { Permission } from 'src/enum';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
|
||||||
|
const plugins: Plugin[] = [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
name: 'Immich Core Plugin',
|
||||||
|
description: 'Core plugins for Immich',
|
||||||
|
version: 1,
|
||||||
|
isEnabled: true,
|
||||||
|
isInstalled: true,
|
||||||
|
isTrusted: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
packageId: 'immich-plugin-',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export class PluginService extends BaseService {
|
||||||
|
async search(auth: AuthDto, dto: PluginSearchDto) {
|
||||||
|
await this.requireAccess({ auth, permission: Permission.PluginRead, ids: [] });
|
||||||
|
// return this.pluginRepository.search(dto);
|
||||||
|
|
||||||
|
return plugins.map(mapPlugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(auth: AuthDto, id: string, dto: PluginUpdateDto) {
|
||||||
|
await this.requireAccess({ auth, permission: Permission.PluginUpdate, ids: [id] });
|
||||||
|
return this.pluginRepository.update({
|
||||||
|
id,
|
||||||
|
isEnabled: dto.isEnabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(auth: AuthDto, id: string) {
|
||||||
|
await this.requireAccess({ auth, permission: Permission.PluginUpdate, ids: [id] });
|
||||||
|
await this.pluginRepository.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
server/src/services/workflow.service.ts
Normal file
31
server/src/services/workflow.service.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PluginLike } from 'src/interfaces/plugin.interface';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WorkflowService extends BaseService {
|
||||||
|
private plugins?: PluginLike[];
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
const activePlugins = await this.pluginRepository.search({ isEnabled: true });
|
||||||
|
const installPaths = activePlugins.map((p) => p.requirePath).filter(Boolean) as string[];
|
||||||
|
this.plugins = await Promise.all(installPaths.map((path) => this.pluginRepository.load(path!)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// async register() {
|
||||||
|
// const plugins = ['/src/abc'];
|
||||||
|
// for (const pluginModule of plugins) {
|
||||||
|
// // eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
// try {
|
||||||
|
// const plugin: Plugin = ;
|
||||||
|
// const actions = await plugin.register();
|
||||||
|
// for (const action of actions) {
|
||||||
|
// this.actions[action.id] = action;
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error(`Unable to load module: ${pluginModule}`, error);
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
12
server/src/utils/plugin.ts
Normal file
12
server/src/utils/plugin.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { AssetDto, EventType, OnAction, PluginConfig } from 'src/interfaces/plugin.interface';
|
||||||
|
|
||||||
|
export const createPluginAction = <T extends PluginConfig | undefined = undefined>(options: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
events?: EventType[];
|
||||||
|
config?: T;
|
||||||
|
}) => ({
|
||||||
|
addHandler: (onAction: OnAction<T>) => ({ ...options, onAction }),
|
||||||
|
onAsset: (onAction: OnAction<T, AssetDto>) => ({ ...options, onAction }),
|
||||||
|
});
|
||||||
@@ -25,6 +25,7 @@ export enum AppRoute {
|
|||||||
ADMIN_STATS = '/admin/server-status',
|
ADMIN_STATS = '/admin/server-status',
|
||||||
ADMIN_JOBS = '/admin/jobs-status',
|
ADMIN_JOBS = '/admin/jobs-status',
|
||||||
ADMIN_REPAIR = '/admin/repair',
|
ADMIN_REPAIR = '/admin/repair',
|
||||||
|
ADMIN_PLUGINS = '/admin/plugins',
|
||||||
|
|
||||||
ALBUMS = '/albums',
|
ALBUMS = '/albums',
|
||||||
LIBRARIES = '/libraries',
|
LIBRARIES = '/libraries',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
|
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { NavbarItem } from '@immich/ui';
|
import { NavbarItem } from '@immich/ui';
|
||||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync } from '@mdi/js';
|
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiConnection, mdiServer, mdiSync } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
<div class="flex flex-col pt-8 pe-4 gap-1">
|
<div class="flex flex-col pt-8 pe-4 gap-1">
|
||||||
<NavbarItem title={$t('users')} href={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} />
|
<NavbarItem title={$t('users')} href={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} />
|
||||||
<NavbarItem title={$t('jobs')} href={AppRoute.ADMIN_JOBS} icon={mdiSync} />
|
<NavbarItem title={$t('jobs')} href={AppRoute.ADMIN_JOBS} icon={mdiSync} />
|
||||||
|
<NavbarItem title={$t('plugins')} href={AppRoute.ADMIN_PLUGINS} icon={mdiConnection} />
|
||||||
<NavbarItem title={$t('settings')} href={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
|
<NavbarItem title={$t('settings')} href={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
|
||||||
<NavbarItem title={$t('external_libraries')} href={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />
|
<NavbarItem title={$t('external_libraries')} href={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />
|
||||||
<NavbarItem title={$t('server_stats')} href={AppRoute.ADMIN_STATS} icon={mdiServer} />
|
<NavbarItem title={$t('server_stats')} href={AppRoute.ADMIN_STATS} icon={mdiServer} />
|
||||||
|
|||||||
54
web/src/routes/admin/plugins/+page.svelte
Normal file
54
web/src/routes/admin/plugins/+page.svelte
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||||
|
import { Button, Icon, Switch } from '@immich/ui';
|
||||||
|
import { mdiCheckDecagram, mdiWrench } from '@mdi/js';
|
||||||
|
import { range } from 'lodash-es';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: PageData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data }: Props = $props();
|
||||||
|
|
||||||
|
const plugins = range(0, 8).map((index) => ({
|
||||||
|
name: `Plugin-${index}`,
|
||||||
|
description: `Plugin ${index} is awesome because it can do x and even y!`,
|
||||||
|
isEnabled: Math.random() < 0.5,
|
||||||
|
isInstalled: Math.random() < 0.5,
|
||||||
|
isOfficial: Math.random() < 0.5,
|
||||||
|
version: 1,
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AdminPageLayout title={data.meta.title}>
|
||||||
|
{#snippet buttons()}
|
||||||
|
<div class="flex gap-2 items-center justify-center">
|
||||||
|
<Button leadingIcon={mdiWrench} onclick={() => console.log('clicked')}>Test</Button>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
||||||
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||||
|
{#each plugins as plugin, i (i)}
|
||||||
|
<section
|
||||||
|
class="flex flex-col gap-4 justify-between dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl p-4"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<h1 class="m-0 items-start flex gap-2">
|
||||||
|
{plugin.name}
|
||||||
|
{#if plugin.isOfficial}
|
||||||
|
<Icon icon={mdiCheckDecagram} size="18" class="text-success" />
|
||||||
|
{/if}
|
||||||
|
<div class="place-self-end justify-self-end justify-end self-end">Version {plugin.version}</div>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="m-0 text-sm text-gray-600 dark:text-gray-300">{plugin.description}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex">Is {plugin.isInstalled ? '' : 'not '}installed</div>
|
||||||
|
<Switch checked={plugin.isEnabled} id={plugin.name} title="Enabled" />
|
||||||
|
</section>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</AdminPageLayout>
|
||||||
16
web/src/routes/admin/plugins/+page.ts
Normal file
16
web/src/routes/admin/plugins/+page.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { authenticate } from '$lib/utils/auth';
|
||||||
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ url }) => {
|
||||||
|
await authenticate(url, { admin: true });
|
||||||
|
const plugins = [];
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins,
|
||||||
|
meta: {
|
||||||
|
title: $t('plugins'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) satisfies PageLoad;
|
||||||
Reference in New Issue
Block a user