mirror of
https://github.com/immich-app/immich.git
synced 2026-05-29 11:02:38 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e35610d0a7 |
@@ -95,6 +95,7 @@ describe('/server', () => {
|
||||
major: expect.any(Number),
|
||||
minor: expect.any(Number),
|
||||
patch: expect.any(Number),
|
||||
prerelease: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,18 +21,18 @@ describe('/system-config', () => {
|
||||
const response1 = await request(app)
|
||||
.put('/system-config')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ ...config, newVersionCheck: { enabled: false } });
|
||||
.send({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } });
|
||||
|
||||
expect(response1.status).toBe(200);
|
||||
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false } });
|
||||
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } });
|
||||
|
||||
const response2 = await request(app)
|
||||
.put('/system-config')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ ...config, newVersionCheck: { enabled: true } });
|
||||
.send({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } });
|
||||
|
||||
expect(response2.status).toBe(200);
|
||||
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true } });
|
||||
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } });
|
||||
});
|
||||
|
||||
it('should reject an invalid config entry', async () => {
|
||||
|
||||
+4
-1
@@ -305,6 +305,8 @@
|
||||
"refreshing_all_libraries": "Refreshing all libraries",
|
||||
"registration": "Admin Registration",
|
||||
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
|
||||
"release_channel_release_candidate": "Release candidate",
|
||||
"release_channel_stable": "Stable",
|
||||
"remove_failed_jobs": "Remove failed jobs",
|
||||
"require_password_change_on_login": "Require user to change password on first login",
|
||||
"reset_settings_to_default": "Reset settings to default",
|
||||
@@ -442,6 +444,8 @@
|
||||
"user_settings_description": "Manage user settings",
|
||||
"user_successfully_removed": "User {email} has been successfully removed.",
|
||||
"users_page_description": "Admin users page",
|
||||
"version_check_channel": "Release channel",
|
||||
"version_check_channel_description": "Pick the release channel you want to get version announcements for",
|
||||
"version_check_enabled_description": "Enable version check",
|
||||
"version_check_implications": "The version check feature relies on periodic communication with {server}",
|
||||
"version_check_settings": "Version Check",
|
||||
@@ -2233,7 +2237,6 @@
|
||||
"slideshow_repeat": "Repeat slideshow",
|
||||
"slideshow_repeat_description": "Loop back to beginning when slideshow ends",
|
||||
"slideshow_settings": "Slideshow settings",
|
||||
"smart_album": "Smart album",
|
||||
"sort_albums_by": "Sort albums by...",
|
||||
"sort_created": "Date created",
|
||||
"sort_items": "Number of items",
|
||||
|
||||
@@ -64,7 +64,6 @@ class TextRecognizer(InferenceModel):
|
||||
rec_batch_num=max_batch_size if max_batch_size else 6,
|
||||
rec_img_shape=(3, 48, 320),
|
||||
lang_type=self.language,
|
||||
model_root_dir=self.cache_dir,
|
||||
)
|
||||
)
|
||||
return session
|
||||
|
||||
@@ -1028,12 +1028,7 @@ class TestOcr:
|
||||
text_recognizer.load()
|
||||
|
||||
rapid_recognizer.assert_called_once_with(
|
||||
OcrOptions(
|
||||
session=ort_session.return_value,
|
||||
rec_batch_num=6,
|
||||
rec_img_shape=(3, 48, 320),
|
||||
model_root_dir=text_recognizer.cache_dir,
|
||||
)
|
||||
OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320))
|
||||
)
|
||||
|
||||
def test_set_custom_max_batch_size(self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture) -> None:
|
||||
@@ -1046,12 +1041,7 @@ class TestOcr:
|
||||
text_recognizer.load()
|
||||
|
||||
rapid_recognizer.assert_called_once_with(
|
||||
OcrOptions(
|
||||
session=ort_session.return_value,
|
||||
rec_batch_num=4,
|
||||
rec_img_shape=(3, 48, 320),
|
||||
model_root_dir=text_recognizer.cache_dir,
|
||||
)
|
||||
OcrOptions(session=ort_session.return_value, rec_batch_num=4, rec_img_shape=(3, 48, 320))
|
||||
)
|
||||
|
||||
def test_ignore_other_custom_max_batch_size(
|
||||
@@ -1066,12 +1056,7 @@ class TestOcr:
|
||||
text_recognizer.load()
|
||||
|
||||
rapid_recognizer.assert_called_once_with(
|
||||
OcrOptions(
|
||||
session=ort_session.return_value,
|
||||
rec_batch_num=6,
|
||||
rec_img_shape=(3, 48, 320),
|
||||
model_root_dir=text_recognizer.cache_dir,
|
||||
)
|
||||
OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320))
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@ lockfile = true
|
||||
|
||||
[tasks.plugins]
|
||||
run = [
|
||||
"pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile",
|
||||
"pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core build",
|
||||
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile",
|
||||
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build",
|
||||
]
|
||||
|
||||
[tasks.open-api-typescript]
|
||||
@@ -108,7 +108,7 @@ depends = "//:plugins"
|
||||
dir = "docker"
|
||||
interactive = true
|
||||
env = { COMPOSE_BAKE = true }
|
||||
run = "docker compose -f ./docker-compose.prod.yml up --build --remove-orphans"
|
||||
run = "docker compose -f ./docker-compose.prod.yml up --remove-orphans"
|
||||
depends_post = "//:prod-down"
|
||||
|
||||
[tasks.prod-scale]
|
||||
|
||||
@@ -143,8 +143,13 @@ class AppConfig {
|
||||
})
|
||||
as T;
|
||||
|
||||
factory AppConfig.fromEntries(Map<MetadataKey<Object>, Object> overrides) =>
|
||||
overrides.entries.fold(const AppConfig(), (config, entry) => config.write(entry.key, entry.value));
|
||||
factory AppConfig.fromEntries(Map<MetadataKey<Object>, Object> entries) {
|
||||
var config = const AppConfig();
|
||||
for (final MapEntry(key: key, value: value) in entries.entries) {
|
||||
config = config.write(key, value);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
AppConfig write<T extends Object>(MetadataKey<T> key, T value) {
|
||||
return switch (key) {
|
||||
|
||||
@@ -7,6 +7,13 @@ import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
|
||||
enum MetadataScope {
|
||||
user, // keys with this scope are deleted on logout
|
||||
system;
|
||||
|
||||
const MetadataScope();
|
||||
}
|
||||
|
||||
enum MetadataKey<T extends Object> {
|
||||
// Theme
|
||||
themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)),
|
||||
@@ -25,11 +32,14 @@ enum MetadataKey<T extends Object> {
|
||||
viewerTapToNavigate<bool>(),
|
||||
|
||||
// Network
|
||||
networkAutoEndpointSwitching<bool>(),
|
||||
networkPreferredWifiName<String>(),
|
||||
networkLocalEndpoint<String>(),
|
||||
networkExternalEndpointList<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
|
||||
networkCustomHeaders<Map<String, String>>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)),
|
||||
networkAutoEndpointSwitching<bool>(scope: .system),
|
||||
networkPreferredWifiName<String>(scope: .system),
|
||||
networkLocalEndpoint<String>(scope: .system),
|
||||
networkExternalEndpointList<List<String>>(scope: .system, codec: _ListCodec(_PrimitiveCodec.string)),
|
||||
networkCustomHeaders<Map<String, String>>(
|
||||
scope: .system,
|
||||
codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string),
|
||||
),
|
||||
|
||||
// Album
|
||||
albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)),
|
||||
@@ -50,7 +60,7 @@ enum MetadataKey<T extends Object> {
|
||||
timelineStorageIndicator<bool>(),
|
||||
|
||||
// Log
|
||||
logLevel<LogLevel>(codec: _EnumCodec(LogLevel.values)),
|
||||
logLevel<LogLevel>(scope: .system, codec: _EnumCodec(LogLevel.values)),
|
||||
|
||||
// Map
|
||||
mapShowFavoriteOnly<bool>(),
|
||||
@@ -73,9 +83,10 @@ enum MetadataKey<T extends Object> {
|
||||
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)),
|
||||
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values));
|
||||
|
||||
final MetadataScope scope;
|
||||
final _MetadataCodec<T>? _codecOverride;
|
||||
|
||||
const MetadataKey({_MetadataCodec<T>? codec}) : _codecOverride = codec;
|
||||
const MetadataKey({this.scope = .user, _MetadataCodec<T>? codec}) : _codecOverride = codec;
|
||||
|
||||
_MetadataCodec<T> get _codec => _codecOverride ?? _MetadataCodec.forType(T);
|
||||
|
||||
|
||||
@@ -34,33 +34,21 @@ class MetadataRepository extends DriftDatabaseRepository {
|
||||
|
||||
Future<void> refresh() async => _applyOverrides(await _db.select(_db.metadataEntity).get());
|
||||
|
||||
Future<void> clear(Iterable<MetadataKey> keys) async {
|
||||
if (keys.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final names = keys.map((key) => key.name).toList();
|
||||
await (_db.delete(_db.metadataEntity)..where((row) => row.key.isIn(names))).go();
|
||||
|
||||
for (final key in keys) {
|
||||
_appConfig = _appConfig.write(key, defaultConfig.read(key));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> write<T extends Object, U extends T>(MetadataKey<T> key, U value) async {
|
||||
if (value == _appConfig.read(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value == defaultConfig.read(key)) {
|
||||
return clear([key]);
|
||||
await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.name))).go();
|
||||
} else {
|
||||
await _db
|
||||
.into(_db.metadataEntity)
|
||||
.insertOnConflictUpdate(
|
||||
MetadataEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())),
|
||||
);
|
||||
}
|
||||
|
||||
await _db
|
||||
.into(_db.metadataEntity)
|
||||
.insertOnConflictUpdate(
|
||||
MetadataEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())),
|
||||
);
|
||||
_appConfig = _appConfig.write(key, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class ApiService {
|
||||
final ApiClient _apiClient = ApiClient(basePath: '');
|
||||
late ApiClient _apiClient;
|
||||
|
||||
late UsersApi usersApi;
|
||||
late AuthenticationApi authenticationApi;
|
||||
@@ -54,7 +54,7 @@ class ApiService {
|
||||
}
|
||||
|
||||
setEndpoint(String endpoint) {
|
||||
_apiClient.basePath = endpoint;
|
||||
_apiClient = ApiClient(basePath: endpoint);
|
||||
_apiClient.client = NetworkRepository.client;
|
||||
usersApi = UsersApi(_apiClient);
|
||||
authenticationApi = AuthenticationApi(_apiClient);
|
||||
|
||||
@@ -110,7 +110,7 @@ class AuthService {
|
||||
/// - Authentication repository data
|
||||
/// - Current user information
|
||||
/// - Access token
|
||||
/// - Server-specific endpoint configuration
|
||||
/// - Asset ETag
|
||||
///
|
||||
/// All deletions are executed in parallel using [Future.wait].
|
||||
Future<void> clearLocalData() async {
|
||||
@@ -120,12 +120,6 @@ class AuthService {
|
||||
_authRepository.clearLocalData(),
|
||||
Store.delete(StoreKey.currentUser),
|
||||
Store.delete(StoreKey.accessToken),
|
||||
MetadataRepository.instance.clear(const [
|
||||
.networkAutoEndpointSwitching,
|
||||
.networkPreferredWifiName,
|
||||
.networkLocalEndpoint,
|
||||
.networkExternalEndpointList,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Generated
+3
@@ -513,6 +513,9 @@ Class | Method | HTTP request | Description
|
||||
- [RatingsUpdate](doc//RatingsUpdate.md)
|
||||
- [ReactionLevel](doc//ReactionLevel.md)
|
||||
- [ReactionType](doc//ReactionType.md)
|
||||
- [ReleaseChannel](doc//ReleaseChannel.md)
|
||||
- [ReleaseEventV1](doc//ReleaseEventV1.md)
|
||||
- [ReleaseType](doc//ReleaseType.md)
|
||||
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
|
||||
- [RotateParameters](doc//RotateParameters.md)
|
||||
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
|
||||
|
||||
Generated
+3
@@ -258,6 +258,9 @@ part 'model/ratings_response.dart';
|
||||
part 'model/ratings_update.dart';
|
||||
part 'model/reaction_level.dart';
|
||||
part 'model/reaction_type.dart';
|
||||
part 'model/release_channel.dart';
|
||||
part 'model/release_event_v1.dart';
|
||||
part 'model/release_type.dart';
|
||||
part 'model/reverse_geocoding_state_response_dto.dart';
|
||||
part 'model/rotate_parameters.dart';
|
||||
part 'model/search_album_response_dto.dart';
|
||||
|
||||
Generated
+3
-21
@@ -508,18 +508,12 @@ class AlbumsApi {
|
||||
/// * [String] assetId:
|
||||
/// Filter albums containing this asset ID (ignores other parameters)
|
||||
///
|
||||
/// * [String] id:
|
||||
/// Album ID
|
||||
///
|
||||
/// * [bool] isOwned:
|
||||
/// Filter by ownership: true = only owned, false = only shared-with-me, undefined = no filter
|
||||
///
|
||||
/// * [bool] isShared:
|
||||
/// Filter by shared status: true = only shared, false = not shared, undefined = no filter
|
||||
///
|
||||
/// * [String] name:
|
||||
/// Album name (exact match)
|
||||
Future<Response> getAllAlbumsWithHttpInfo({ String? assetId, String? id, bool? isOwned, bool? isShared, String? name, }) async {
|
||||
Future<Response> getAllAlbumsWithHttpInfo({ String? assetId, bool? isOwned, bool? isShared, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/albums';
|
||||
|
||||
@@ -533,18 +527,12 @@ class AlbumsApi {
|
||||
if (assetId != null) {
|
||||
queryParams.addAll(_queryParams('', 'assetId', assetId));
|
||||
}
|
||||
if (id != null) {
|
||||
queryParams.addAll(_queryParams('', 'id', id));
|
||||
}
|
||||
if (isOwned != null) {
|
||||
queryParams.addAll(_queryParams('', 'isOwned', isOwned));
|
||||
}
|
||||
if (isShared != null) {
|
||||
queryParams.addAll(_queryParams('', 'isShared', isShared));
|
||||
}
|
||||
if (name != null) {
|
||||
queryParams.addAll(_queryParams('', 'name', name));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
@@ -569,19 +557,13 @@ class AlbumsApi {
|
||||
/// * [String] assetId:
|
||||
/// Filter albums containing this asset ID (ignores other parameters)
|
||||
///
|
||||
/// * [String] id:
|
||||
/// Album ID
|
||||
///
|
||||
/// * [bool] isOwned:
|
||||
/// Filter by ownership: true = only owned, false = only shared-with-me, undefined = no filter
|
||||
///
|
||||
/// * [bool] isShared:
|
||||
/// Filter by shared status: true = only shared, false = not shared, undefined = no filter
|
||||
///
|
||||
/// * [String] name:
|
||||
/// Album name (exact match)
|
||||
Future<List<AlbumResponseDto>?> getAllAlbums({ String? assetId, String? id, bool? isOwned, bool? isShared, String? name, }) async {
|
||||
final response = await getAllAlbumsWithHttpInfo( assetId: assetId, id: id, isOwned: isOwned, isShared: isShared, name: name, );
|
||||
Future<List<AlbumResponseDto>?> getAllAlbums({ String? assetId, bool? isOwned, bool? isShared, }) async {
|
||||
final response = await getAllAlbumsWithHttpInfo( assetId: assetId, isOwned: isOwned, isShared: isShared, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
Generated
+7
-1
@@ -13,7 +13,7 @@ part of openapi.api;
|
||||
class ApiClient {
|
||||
ApiClient({this.basePath = '/api', this.authentication,});
|
||||
|
||||
String basePath;
|
||||
final String basePath;
|
||||
final Authentication? authentication;
|
||||
|
||||
var _client = Client();
|
||||
@@ -562,6 +562,12 @@ class ApiClient {
|
||||
return ReactionLevelTypeTransformer().decode(value);
|
||||
case 'ReactionType':
|
||||
return ReactionTypeTypeTransformer().decode(value);
|
||||
case 'ReleaseChannel':
|
||||
return ReleaseChannelTypeTransformer().decode(value);
|
||||
case 'ReleaseEventV1':
|
||||
return ReleaseEventV1.fromJson(value);
|
||||
case 'ReleaseType':
|
||||
return ReleaseTypeTypeTransformer().decode(value);
|
||||
case 'ReverseGeocodingStateResponseDto':
|
||||
return ReverseGeocodingStateResponseDto.fromJson(value);
|
||||
case 'RotateParameters':
|
||||
|
||||
Generated
+6
@@ -157,6 +157,12 @@ String parameterToString(dynamic value) {
|
||||
if (value is ReactionType) {
|
||||
return ReactionTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is ReleaseChannel) {
|
||||
return ReleaseChannelTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is ReleaseType) {
|
||||
return ReleaseTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is SearchSuggestionType) {
|
||||
return SearchSuggestionTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -77,7 +77,7 @@ class JobName {
|
||||
static const versionCheck = JobName._(r'VersionCheck');
|
||||
static const ocrQueueAll = JobName._(r'OcrQueueAll');
|
||||
static const ocr = JobName._(r'Ocr');
|
||||
static const workflowAssetTrigger = JobName._(r'WorkflowAssetTrigger');
|
||||
static const workflowAssetCreate = JobName._(r'WorkflowAssetCreate');
|
||||
|
||||
/// List of all possible values in this [enum][JobName].
|
||||
static const values = <JobName>[
|
||||
@@ -135,7 +135,7 @@ class JobName {
|
||||
versionCheck,
|
||||
ocrQueueAll,
|
||||
ocr,
|
||||
workflowAssetTrigger,
|
||||
workflowAssetCreate,
|
||||
];
|
||||
|
||||
static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value);
|
||||
@@ -228,7 +228,7 @@ class JobNameTypeTransformer {
|
||||
case r'VersionCheck': return JobName.versionCheck;
|
||||
case r'OcrQueueAll': return JobName.ocrQueueAll;
|
||||
case r'Ocr': return JobName.ocr;
|
||||
case r'WorkflowAssetTrigger': return JobName.workflowAssetTrigger;
|
||||
case r'WorkflowAssetCreate': return JobName.workflowAssetCreate;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
||||
+3
-14
@@ -18,7 +18,6 @@ class PluginTemplateResponseDto {
|
||||
this.steps = const [],
|
||||
required this.title,
|
||||
required this.trigger,
|
||||
this.uiHints = const [],
|
||||
});
|
||||
|
||||
/// Template description
|
||||
@@ -35,17 +34,13 @@ class PluginTemplateResponseDto {
|
||||
|
||||
WorkflowTrigger trigger;
|
||||
|
||||
/// Ui hints, for example \"smart-album\"
|
||||
List<String> uiHints;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PluginTemplateResponseDto &&
|
||||
other.description == description &&
|
||||
other.key == key &&
|
||||
_deepEquality.equals(other.steps, steps) &&
|
||||
other.title == title &&
|
||||
other.trigger == trigger &&
|
||||
_deepEquality.equals(other.uiHints, uiHints);
|
||||
other.trigger == trigger;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -54,11 +49,10 @@ class PluginTemplateResponseDto {
|
||||
(key.hashCode) +
|
||||
(steps.hashCode) +
|
||||
(title.hashCode) +
|
||||
(trigger.hashCode) +
|
||||
(uiHints.hashCode);
|
||||
(trigger.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PluginTemplateResponseDto[description=$description, key=$key, steps=$steps, title=$title, trigger=$trigger, uiHints=$uiHints]';
|
||||
String toString() => 'PluginTemplateResponseDto[description=$description, key=$key, steps=$steps, title=$title, trigger=$trigger]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -67,7 +61,6 @@ class PluginTemplateResponseDto {
|
||||
json[r'steps'] = this.steps;
|
||||
json[r'title'] = this.title;
|
||||
json[r'trigger'] = this.trigger;
|
||||
json[r'uiHints'] = this.uiHints;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -85,9 +78,6 @@ class PluginTemplateResponseDto {
|
||||
steps: PluginTemplateStepResponseDto.listFromJson(json[r'steps']),
|
||||
title: mapValueOfType<String>(json, r'title')!,
|
||||
trigger: WorkflowTrigger.fromJson(json[r'trigger'])!,
|
||||
uiHints: json[r'uiHints'] is Iterable
|
||||
? (json[r'uiHints'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -140,7 +130,6 @@ class PluginTemplateResponseDto {
|
||||
'steps',
|
||||
'title',
|
||||
'trigger',
|
||||
'uiHints',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
/// Release channel
|
||||
class ReleaseChannel {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const ReleaseChannel._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const stable = ReleaseChannel._(r'stable');
|
||||
static const releaseCandidate = ReleaseChannel._(r'releaseCandidate');
|
||||
|
||||
/// List of all possible values in this [enum][ReleaseChannel].
|
||||
static const values = <ReleaseChannel>[
|
||||
stable,
|
||||
releaseCandidate,
|
||||
];
|
||||
|
||||
static ReleaseChannel? fromJson(dynamic value) => ReleaseChannelTypeTransformer().decode(value);
|
||||
|
||||
static List<ReleaseChannel> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <ReleaseChannel>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = ReleaseChannel.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [ReleaseChannel] to String,
|
||||
/// and [decode] dynamic data back to [ReleaseChannel].
|
||||
class ReleaseChannelTypeTransformer {
|
||||
factory ReleaseChannelTypeTransformer() => _instance ??= const ReleaseChannelTypeTransformer._();
|
||||
|
||||
const ReleaseChannelTypeTransformer._();
|
||||
|
||||
String encode(ReleaseChannel data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a ReleaseChannel.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
ReleaseChannel? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'stable': return ReleaseChannel.stable;
|
||||
case r'releaseCandidate': return ReleaseChannel.releaseCandidate;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [ReleaseChannelTypeTransformer] instance.
|
||||
static ReleaseChannelTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class ReleaseEventV1 {
|
||||
/// Returns a new [ReleaseEventV1] instance.
|
||||
ReleaseEventV1({
|
||||
required this.checkedAt,
|
||||
required this.isAvailable,
|
||||
required this.releaseVersion,
|
||||
required this.serverVersion,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
/// When the server last checked for a latest version. As an ISO timestamp
|
||||
String checkedAt;
|
||||
|
||||
/// Whether a new version is available
|
||||
bool isAvailable;
|
||||
|
||||
ServerVersionResponseDto releaseVersion;
|
||||
|
||||
ServerVersionResponseDto serverVersion;
|
||||
|
||||
ReleaseType type;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is ReleaseEventV1 &&
|
||||
other.checkedAt == checkedAt &&
|
||||
other.isAvailable == isAvailable &&
|
||||
other.releaseVersion == releaseVersion &&
|
||||
other.serverVersion == serverVersion &&
|
||||
other.type == type;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(checkedAt.hashCode) +
|
||||
(isAvailable.hashCode) +
|
||||
(releaseVersion.hashCode) +
|
||||
(serverVersion.hashCode) +
|
||||
(type.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ReleaseEventV1[checkedAt=$checkedAt, isAvailable=$isAvailable, releaseVersion=$releaseVersion, serverVersion=$serverVersion, type=$type]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'checkedAt'] = this.checkedAt;
|
||||
json[r'isAvailable'] = this.isAvailable;
|
||||
json[r'releaseVersion'] = this.releaseVersion;
|
||||
json[r'serverVersion'] = this.serverVersion;
|
||||
json[r'type'] = this.type;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [ReleaseEventV1] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ReleaseEventV1? fromJson(dynamic value) {
|
||||
upgradeDto(value, "ReleaseEventV1");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return ReleaseEventV1(
|
||||
checkedAt: mapValueOfType<String>(json, r'checkedAt')!,
|
||||
isAvailable: mapValueOfType<bool>(json, r'isAvailable')!,
|
||||
releaseVersion: ServerVersionResponseDto.fromJson(json[r'releaseVersion'])!,
|
||||
serverVersion: ServerVersionResponseDto.fromJson(json[r'serverVersion'])!,
|
||||
type: ReleaseType.fromJson(json[r'type'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<ReleaseEventV1> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <ReleaseEventV1>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = ReleaseEventV1.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, ReleaseEventV1> mapFromJson(dynamic json) {
|
||||
final map = <String, ReleaseEventV1>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = ReleaseEventV1.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of ReleaseEventV1-objects as value to a dart map
|
||||
static Map<String, List<ReleaseEventV1>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<ReleaseEventV1>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = ReleaseEventV1.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'checkedAt',
|
||||
'isAvailable',
|
||||
'releaseVersion',
|
||||
'serverVersion',
|
||||
'type',
|
||||
};
|
||||
}
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class ReleaseType {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const ReleaseType._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const major = ReleaseType._(r'major');
|
||||
static const premajor = ReleaseType._(r'premajor');
|
||||
static const minor = ReleaseType._(r'minor');
|
||||
static const preminor = ReleaseType._(r'preminor');
|
||||
static const patch_ = ReleaseType._(r'patch');
|
||||
static const prepatch = ReleaseType._(r'prepatch');
|
||||
static const prerelease = ReleaseType._(r'prerelease');
|
||||
static const release = ReleaseType._(r'release');
|
||||
|
||||
/// List of all possible values in this [enum][ReleaseType].
|
||||
static const values = <ReleaseType>[
|
||||
major,
|
||||
premajor,
|
||||
minor,
|
||||
preminor,
|
||||
patch_,
|
||||
prepatch,
|
||||
prerelease,
|
||||
release,
|
||||
];
|
||||
|
||||
static ReleaseType? fromJson(dynamic value) => ReleaseTypeTypeTransformer().decode(value);
|
||||
|
||||
static List<ReleaseType> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <ReleaseType>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = ReleaseType.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [ReleaseType] to String,
|
||||
/// and [decode] dynamic data back to [ReleaseType].
|
||||
class ReleaseTypeTypeTransformer {
|
||||
factory ReleaseTypeTypeTransformer() => _instance ??= const ReleaseTypeTypeTransformer._();
|
||||
|
||||
const ReleaseTypeTypeTransformer._();
|
||||
|
||||
String encode(ReleaseType data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a ReleaseType.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
ReleaseType? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'major': return ReleaseType.major;
|
||||
case r'premajor': return ReleaseType.premajor;
|
||||
case r'minor': return ReleaseType.minor;
|
||||
case r'preminor': return ReleaseType.preminor;
|
||||
case r'patch': return ReleaseType.patch_;
|
||||
case r'prepatch': return ReleaseType.prepatch;
|
||||
case r'prerelease': return ReleaseType.prerelease;
|
||||
case r'release': return ReleaseType.release;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [ReleaseTypeTypeTransformer] instance.
|
||||
static ReleaseTypeTypeTransformer? _instance;
|
||||
}
|
||||
|
||||
+22
-6
@@ -16,47 +16,61 @@ class ServerVersionResponseDto {
|
||||
required this.major,
|
||||
required this.minor,
|
||||
required this.patch_,
|
||||
required this.prerelease,
|
||||
});
|
||||
|
||||
/// Major version number
|
||||
///
|
||||
/// Minimum value: -9007199254740991
|
||||
/// Minimum value: 0
|
||||
/// Maximum value: 9007199254740991
|
||||
int major;
|
||||
|
||||
/// Minor version number
|
||||
///
|
||||
/// Minimum value: -9007199254740991
|
||||
/// Minimum value: 0
|
||||
/// Maximum value: 9007199254740991
|
||||
int minor;
|
||||
|
||||
/// Patch version number
|
||||
///
|
||||
/// Minimum value: -9007199254740991
|
||||
/// Minimum value: 0
|
||||
/// Maximum value: 9007199254740991
|
||||
int patch_;
|
||||
|
||||
/// Pre-release version number
|
||||
///
|
||||
/// Minimum value: 0
|
||||
/// Maximum value: 9007199254740991
|
||||
int? prerelease;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is ServerVersionResponseDto &&
|
||||
other.major == major &&
|
||||
other.minor == minor &&
|
||||
other.patch_ == patch_;
|
||||
other.patch_ == patch_ &&
|
||||
other.prerelease == prerelease;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(major.hashCode) +
|
||||
(minor.hashCode) +
|
||||
(patch_.hashCode);
|
||||
(patch_.hashCode) +
|
||||
(prerelease == null ? 0 : prerelease!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ServerVersionResponseDto[major=$major, minor=$minor, patch_=$patch_]';
|
||||
String toString() => 'ServerVersionResponseDto[major=$major, minor=$minor, patch_=$patch_, prerelease=$prerelease]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'major'] = this.major;
|
||||
json[r'minor'] = this.minor;
|
||||
json[r'patch'] = this.patch_;
|
||||
if (this.prerelease != null) {
|
||||
json[r'prerelease'] = this.prerelease;
|
||||
} else {
|
||||
// json[r'prerelease'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -72,6 +86,7 @@ class ServerVersionResponseDto {
|
||||
major: mapValueOfType<int>(json, r'major')!,
|
||||
minor: mapValueOfType<int>(json, r'minor')!,
|
||||
patch_: mapValueOfType<int>(json, r'patch')!,
|
||||
prerelease: mapValueOfType<int>(json, r'prerelease'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -122,6 +137,7 @@ class ServerVersionResponseDto {
|
||||
'major',
|
||||
'minor',
|
||||
'patch',
|
||||
'prerelease',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,26 +13,32 @@ part of openapi.api;
|
||||
class SystemConfigNewVersionCheckDto {
|
||||
/// Returns a new [SystemConfigNewVersionCheckDto] instance.
|
||||
SystemConfigNewVersionCheckDto({
|
||||
required this.channel,
|
||||
required this.enabled,
|
||||
});
|
||||
|
||||
ReleaseChannel channel;
|
||||
|
||||
/// Enabled
|
||||
bool enabled;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigNewVersionCheckDto &&
|
||||
other.channel == channel &&
|
||||
other.enabled == enabled;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(channel.hashCode) +
|
||||
(enabled.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigNewVersionCheckDto[enabled=$enabled]';
|
||||
String toString() => 'SystemConfigNewVersionCheckDto[channel=$channel, enabled=$enabled]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'channel'] = this.channel;
|
||||
json[r'enabled'] = this.enabled;
|
||||
return json;
|
||||
}
|
||||
@@ -46,6 +52,7 @@ class SystemConfigNewVersionCheckDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SystemConfigNewVersionCheckDto(
|
||||
channel: ReleaseChannel.fromJson(json[r'channel'])!,
|
||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||
);
|
||||
}
|
||||
@@ -94,6 +101,7 @@ class SystemConfigNewVersionCheckDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'channel',
|
||||
'enabled',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1627,17 +1627,6 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Album ID",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "isOwned",
|
||||
"required": false,
|
||||
@@ -1655,15 +1644,6 @@
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Album name (exact match)",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -18171,7 +18151,7 @@
|
||||
"VersionCheck",
|
||||
"OcrQueueAll",
|
||||
"Ocr",
|
||||
"WorkflowAssetTrigger"
|
||||
"WorkflowAssetCreate"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -20219,13 +20199,6 @@
|
||||
"trigger": {
|
||||
"$ref": "#/components/schemas/WorkflowTrigger",
|
||||
"description": "Workflow trigger"
|
||||
},
|
||||
"uiHints": {
|
||||
"description": "Ui hints, for example \"smart-album\"",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -20233,8 +20206,7 @@
|
||||
"key",
|
||||
"steps",
|
||||
"title",
|
||||
"trigger",
|
||||
"uiHints"
|
||||
"trigger"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -20828,6 +20800,58 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReleaseChannel": {
|
||||
"description": "Release channel",
|
||||
"enum": [
|
||||
"stable",
|
||||
"releaseCandidate"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReleaseEventV1": {
|
||||
"properties": {
|
||||
"checkedAt": {
|
||||
"description": "When the server last checked for a latest version. As an ISO timestamp",
|
||||
"type": "string"
|
||||
},
|
||||
"isAvailable": {
|
||||
"description": "Whether a new version is available",
|
||||
"type": "boolean"
|
||||
},
|
||||
"releaseVersion": {
|
||||
"$ref": "#/components/schemas/ServerVersionResponseDto"
|
||||
},
|
||||
"serverVersion": {
|
||||
"$ref": "#/components/schemas/ServerVersionResponseDto"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/ReleaseType",
|
||||
"description": "Release type",
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"checkedAt",
|
||||
"isAvailable",
|
||||
"releaseVersion",
|
||||
"serverVersion",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ReleaseType": {
|
||||
"enum": [
|
||||
"major",
|
||||
"premajor",
|
||||
"minor",
|
||||
"preminor",
|
||||
"patch",
|
||||
"prepatch",
|
||||
"prerelease",
|
||||
"release"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReverseGeocodingStateResponseDto": {
|
||||
"properties": {
|
||||
"lastImportFileName": {
|
||||
@@ -21497,26 +21521,40 @@
|
||||
"major": {
|
||||
"description": "Major version number",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": -9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"minor": {
|
||||
"description": "Minor version number",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": -9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"patch": {
|
||||
"description": "Patch version number",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": -9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"prerelease": {
|
||||
"description": "Pre-release version number",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v3.0.0",
|
||||
"state": "Added"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"major",
|
||||
"minor",
|
||||
"patch"
|
||||
"patch",
|
||||
"prerelease"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -24537,12 +24575,16 @@
|
||||
},
|
||||
"SystemConfigNewVersionCheckDto": {
|
||||
"properties": {
|
||||
"channel": {
|
||||
"$ref": "#/components/schemas/ReleaseChannel"
|
||||
},
|
||||
"enabled": {
|
||||
"description": "Enabled",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"channel",
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
@@ -13,7 +13,7 @@
|
||||
class ApiClient {
|
||||
ApiClient({this.basePath = '/api', this.authentication,});
|
||||
|
||||
- final String basePath;
|
||||
+ String basePath;
|
||||
final Authentication? authentication;
|
||||
|
||||
var _client = Client();
|
||||
@@ -143,19 +143,19 @@
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,20 +20,19 @@
|
||||
"caseSensitive": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "immich-plugin-core#assetArchive",
|
||||
"config": {
|
||||
"inverse": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "immich-plugin-core#assetAddToAlbums",
|
||||
"config": {
|
||||
"albumIds": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "immich-plugin-core#assetArchive",
|
||||
"config": {
|
||||
"inverse": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"uiHints": ["SmartAlbum"]
|
||||
]
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
@@ -66,7 +65,7 @@
|
||||
},
|
||||
"required": ["pattern"]
|
||||
},
|
||||
"uiHints": ["Filter"]
|
||||
"uiHints": ["filter"]
|
||||
},
|
||||
{
|
||||
"name": "filterFileType",
|
||||
@@ -86,7 +85,7 @@
|
||||
},
|
||||
"required": ["fileTypes"]
|
||||
},
|
||||
"uiHints": ["Filter"]
|
||||
"uiHints": ["filter"]
|
||||
},
|
||||
{
|
||||
"name": "filterPerson",
|
||||
@@ -100,7 +99,7 @@
|
||||
"array": true,
|
||||
"title": "Person IDs",
|
||||
"description": "List of person to match",
|
||||
"uiHint": "personId"
|
||||
"uiHint": "personI"
|
||||
},
|
||||
"matchAny": {
|
||||
"type": "boolean",
|
||||
@@ -111,7 +110,7 @@
|
||||
},
|
||||
"required": ["personIds"]
|
||||
},
|
||||
"uiHints": ["Filter"]
|
||||
"uiHints": ["filter"]
|
||||
},
|
||||
{
|
||||
"name": "assetArchive",
|
||||
@@ -188,7 +187,7 @@
|
||||
"title": "Album IDs",
|
||||
"array": true,
|
||||
"description": "Target album IDs",
|
||||
"uiHint": "AlbumId"
|
||||
"uiHint": "albumId"
|
||||
}
|
||||
},
|
||||
"required": ["albumIds"]
|
||||
@@ -273,14 +272,14 @@
|
||||
"type": "string",
|
||||
"title": "Album ID",
|
||||
"description": "Target album ID",
|
||||
"uiHint": "AlbumId"
|
||||
"uiHint": "albumId"
|
||||
},
|
||||
"albumIds": {
|
||||
"type": "string",
|
||||
"title": "Album IDs",
|
||||
"description": "Target album IDs",
|
||||
"array": true,
|
||||
"uiHint": "AlbumId"
|
||||
"uiHint": "albumId"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export const assetTrash = () => {
|
||||
changes: {
|
||||
asset: config.inverse
|
||||
? { deletedAt: null, status: AssetStatus.Active }
|
||||
: { deletedAt: new Date().toISOString(), status: AssetStatus.Trashed },
|
||||
: { deletedAt: new Date(), status: AssetStatus.Trashed },
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"packageManager": "pnpm@10.33.4",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.1.1",
|
||||
"@immich/sdk": "workspace:*",
|
||||
"@types/node": "^24.12.4",
|
||||
"esbuild": "^0.28.0",
|
||||
"tsc-alias": "^1.8.16",
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { type BulkIdResponseDto, type BulkIdsDto } from '@immich/sdk';
|
||||
|
||||
// keep in sync with plugin-core/src/index.d.ts';
|
||||
declare module 'extism:host' {
|
||||
interface user {
|
||||
albumAddAssets(ptr: PTR): I64;
|
||||
@@ -48,11 +45,7 @@ type AlbumsToAssets = {
|
||||
|
||||
export const hostFunctions = (authToken: string) => ({
|
||||
albumAddAssets: (albumId: string, assetIds: string[]) =>
|
||||
call<[string, BulkIdsDto], BulkIdResponseDto[]>(
|
||||
'albumAddAssets',
|
||||
authToken,
|
||||
[albumId, { ids: assetIds }],
|
||||
),
|
||||
call('albumAddAssets', authToken, [albumId, { ids: assetIds }]),
|
||||
addAssetsToAlbums: ({ assetIds, albumIds }: AlbumsToAssets) =>
|
||||
call('addAssetsToAlbums', authToken, [{ albumIds, assetIds }]),
|
||||
});
|
||||
|
||||
@@ -68,19 +68,19 @@ export type AssetV1 = {
|
||||
ownerId: string;
|
||||
type: AssetType;
|
||||
originalPath: string;
|
||||
fileCreatedAt: string;
|
||||
fileModifiedAt: string;
|
||||
fileCreatedAt: Date;
|
||||
fileModifiedAt: Date;
|
||||
isFavorite: boolean;
|
||||
checksum: Buffer; // sha1 checksum
|
||||
livePhotoVideoId: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: Date;
|
||||
createdAt: Date;
|
||||
originalFileName: string;
|
||||
isOffline: boolean;
|
||||
libraryId: string | null;
|
||||
isExternal: boolean;
|
||||
deletedAt: string | null;
|
||||
localDateTime: string;
|
||||
deletedAt: Date | null;
|
||||
localDateTime: Date;
|
||||
stackId: string | null;
|
||||
duplicateId: string | null;
|
||||
status: AssetStatus;
|
||||
@@ -93,8 +93,8 @@ export type AssetV1 = {
|
||||
exifImageHeight: number | null;
|
||||
fileSizeInByte: number | null;
|
||||
orientation: string | null;
|
||||
dateTimeOriginal: string | null;
|
||||
modifyDate: string | null;
|
||||
dateTimeOriginal: Date | null;
|
||||
modifyDate: Date | null;
|
||||
lensModel: string | null;
|
||||
fNumber: number | null;
|
||||
focalLength: number | null;
|
||||
@@ -116,7 +116,7 @@ export type AssetV1 = {
|
||||
autoStackId: string | null;
|
||||
rating: number | null;
|
||||
tags: string[] | null;
|
||||
updatedAt: string | null;
|
||||
updatedAt: Date | null;
|
||||
} | null;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1535,8 +1535,6 @@ export type PluginTemplateResponseDto = {
|
||||
title: string;
|
||||
/** Workflow trigger */
|
||||
trigger: WorkflowTrigger;
|
||||
/** Ui hints, for example "smart-album" */
|
||||
uiHints: string[];
|
||||
};
|
||||
export type QueueResponseDto = {
|
||||
/** Whether the queue is paused */
|
||||
@@ -2076,6 +2074,8 @@ export type ServerVersionResponseDto = {
|
||||
minor: number;
|
||||
/** Patch version number */
|
||||
patch: number;
|
||||
/** Pre-release version number */
|
||||
prerelease: number | null;
|
||||
};
|
||||
export type VersionCheckStateResponseDto = {
|
||||
/** Last check timestamp */
|
||||
@@ -2423,6 +2423,7 @@ export type SystemConfigMetadataDto = {
|
||||
faces: SystemConfigFacesDto;
|
||||
};
|
||||
export type SystemConfigNewVersionCheckDto = {
|
||||
channel: ReleaseChannel;
|
||||
/** Enabled */
|
||||
enabled: boolean;
|
||||
};
|
||||
@@ -2768,6 +2769,16 @@ export type WorkflowShareResponseDto = {
|
||||
trigger: WorkflowTrigger;
|
||||
};
|
||||
export type LicenseResponseDto = UserLicense;
|
||||
export type ReleaseEventV1 = {
|
||||
/** When the server last checked for a latest version. As an ISO timestamp */
|
||||
checkedAt: string;
|
||||
/** Whether a new version is available */
|
||||
isAvailable: boolean;
|
||||
releaseVersion: ServerVersionResponseDto;
|
||||
serverVersion: ServerVersionResponseDto;
|
||||
/** Release type */
|
||||
"type": ReleaseType;
|
||||
};
|
||||
export type SyncAckV1 = {};
|
||||
export type SyncAlbumDeleteV1 = {
|
||||
/** Album ID */
|
||||
@@ -3599,22 +3610,18 @@ export function getUserStatisticsAdmin({ id, isFavorite, isTrashed, visibility }
|
||||
/**
|
||||
* List all albums
|
||||
*/
|
||||
export function getAllAlbums({ assetId, id, isOwned, isShared, name }: {
|
||||
export function getAllAlbums({ assetId, isOwned, isShared }: {
|
||||
assetId?: string;
|
||||
id?: string;
|
||||
isOwned?: boolean;
|
||||
isShared?: boolean;
|
||||
name?: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: AlbumResponseDto[];
|
||||
}>(`/albums${QS.query(QS.explode({
|
||||
assetId,
|
||||
id,
|
||||
isOwned,
|
||||
isShared,
|
||||
name
|
||||
isShared
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
@@ -7146,7 +7153,7 @@ export enum JobName {
|
||||
VersionCheck = "VersionCheck",
|
||||
OcrQueueAll = "OcrQueueAll",
|
||||
Ocr = "Ocr",
|
||||
WorkflowAssetTrigger = "WorkflowAssetTrigger"
|
||||
WorkflowAssetCreate = "WorkflowAssetCreate"
|
||||
}
|
||||
export enum SearchSuggestionType {
|
||||
Country = "country",
|
||||
@@ -7311,6 +7318,10 @@ export enum LogLevel {
|
||||
Error = "error",
|
||||
Fatal = "fatal"
|
||||
}
|
||||
export enum ReleaseChannel {
|
||||
Stable = "stable",
|
||||
ReleaseCandidate = "releaseCandidate"
|
||||
}
|
||||
export enum OAuthTokenEndpointAuthMethod {
|
||||
ClientSecretPost = "client_secret_post",
|
||||
ClientSecretBasic = "client_secret_basic"
|
||||
@@ -7319,6 +7330,16 @@ export enum AssetOrderBy {
|
||||
TakenAt = "takenAt",
|
||||
CreatedAt = "createdAt"
|
||||
}
|
||||
export enum ReleaseType {
|
||||
Major = "major",
|
||||
Premajor = "premajor",
|
||||
Minor = "minor",
|
||||
Preminor = "preminor",
|
||||
Patch = "patch",
|
||||
Prepatch = "prepatch",
|
||||
Prerelease = "prerelease",
|
||||
Release = "release"
|
||||
}
|
||||
export enum UserMetadataKey {
|
||||
Preferences = "preferences",
|
||||
License = "license",
|
||||
|
||||
Generated
+33
-31
@@ -10,7 +10,7 @@ overrides:
|
||||
sharp: ^0.34.5
|
||||
webpackbar: ^7.0.0
|
||||
|
||||
packageExtensionsChecksum: sha256-W6pFzyf+6QXnV91iA6oob0OGVkergPXDN1afLgoF53k=
|
||||
packageExtensionsChecksum: sha256-3l4AQg4iuprBDup+q+2JaPvbPg/7XodWCE0ZteH+s54=
|
||||
|
||||
pnpmfileChecksum: sha256-un98do36L0wZyqsjcLozQ3YUadCAn2yz5bXcBbOuyDA=
|
||||
|
||||
@@ -332,9 +332,6 @@ importers:
|
||||
'@extism/js-pdk':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
'@immich/sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../sdk
|
||||
'@types/node':
|
||||
specifier: ^24.12.4
|
||||
version: 24.12.4
|
||||
@@ -392,7 +389,7 @@ importers:
|
||||
version: 6.1.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)
|
||||
'@nestjs/swagger':
|
||||
specifier: ^11.4.2
|
||||
version: 11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3)
|
||||
version: 11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)
|
||||
'@nestjs/websockets':
|
||||
specifier: ^11.0.4
|
||||
version: 11.1.21(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(@nestjs/platform-socket.io@11.1.21)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
@@ -539,7 +536,7 @@ importers:
|
||||
version: 8.0.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)
|
||||
nestjs-zod:
|
||||
specifier: ^5.3.0
|
||||
version: 5.4.0(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3))(rxjs@7.8.2)(zod@4.3.6)
|
||||
version: 5.4.0(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6)
|
||||
nodemailer:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.7
|
||||
@@ -574,8 +571,8 @@ importers:
|
||||
specifier: ^1.6.3
|
||||
version: 1.6.4
|
||||
semver:
|
||||
specifier: ^7.6.2
|
||||
version: 7.8.0
|
||||
specifier: ^7.8.1
|
||||
version: 7.8.1
|
||||
sharp:
|
||||
specifier: ^0.34.5
|
||||
version: 0.34.5
|
||||
@@ -3798,7 +3795,6 @@ packages:
|
||||
class-transformer: '*'
|
||||
class-validator: '*'
|
||||
reflect-metadata: ^0.1.12 || ^0.2.0
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
'@fastify/static':
|
||||
optional: true
|
||||
@@ -11247,6 +11243,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.8.1:
|
||||
resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
send@0.19.2:
|
||||
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -16304,7 +16305,7 @@ snapshots:
|
||||
nopt: 5.0.0
|
||||
npmlog: 5.0.1
|
||||
rimraf: 3.0.2
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
tar: 6.2.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
@@ -16574,7 +16575,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- chokidar
|
||||
|
||||
'@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3)':
|
||||
'@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)':
|
||||
dependencies:
|
||||
'@microsoft/tsdoc': 0.16.0
|
||||
'@nestjs/common': 11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
@@ -16585,7 +16586,6 @@ snapshots:
|
||||
path-to-regexp: 8.4.2
|
||||
reflect-metadata: 0.2.2
|
||||
swagger-ui-dist: 5.32.6
|
||||
typescript: 6.0.3
|
||||
|
||||
'@nestjs/testing@11.1.21(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(@nestjs/platform-express@11.1.21)':
|
||||
dependencies:
|
||||
@@ -17762,7 +17762,7 @@ snapshots:
|
||||
'@testing-library/dom@10.4.1':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.29.7
|
||||
'@types/aria-query': 5.0.4
|
||||
aria-query: 5.3.0
|
||||
dom-accessibility-api: 0.5.16
|
||||
@@ -18466,7 +18466,7 @@ snapshots:
|
||||
'@typescript-eslint/visitor-keys': 8.59.4
|
||||
debug: 4.4.3
|
||||
minimatch: 10.2.5
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
tinyglobby: 0.2.16
|
||||
ts-api-utils: 2.5.0(typescript@6.0.3)
|
||||
typescript: 6.0.3
|
||||
@@ -19566,7 +19566,7 @@ snapshots:
|
||||
dot-prop: 10.1.0
|
||||
env-paths: 3.0.0
|
||||
json-schema-typed: 8.0.2
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
uint8array-extras: 1.5.0
|
||||
|
||||
config-chain@1.1.13:
|
||||
@@ -19738,7 +19738,7 @@ snapshots:
|
||||
postcss-modules-scope: 3.2.1(postcss@8.5.15)
|
||||
postcss-modules-values: 4.0.0(postcss@8.5.15)
|
||||
postcss-value-parser: 4.2.0
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
optionalDependencies:
|
||||
webpack: 5.107.0(postcss@8.5.15)
|
||||
|
||||
@@ -20606,7 +20606,7 @@ snapshots:
|
||||
find-up: 5.0.0
|
||||
globals: 15.15.0
|
||||
lodash.memoize: 4.1.2
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
|
||||
eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.4.0(jiti@2.7.0)))(eslint@10.4.0(jiti@2.7.0))(prettier@3.8.3):
|
||||
dependencies:
|
||||
@@ -20629,7 +20629,7 @@ snapshots:
|
||||
postcss: 8.5.15
|
||||
postcss-load-config: 3.1.4(postcss@8.5.15)
|
||||
postcss-safe-parser: 7.0.1(postcss@8.5.15)
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
svelte-eslint-parser: 1.6.1(svelte@5.55.8(@typescript-eslint/types@8.59.4))
|
||||
optionalDependencies:
|
||||
svelte: 5.55.8(@typescript-eslint/types@8.59.4)
|
||||
@@ -21107,7 +21107,7 @@ snapshots:
|
||||
minimatch: 3.1.5
|
||||
node-abort-controller: 3.1.1
|
||||
schema-utils: 3.3.0
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
tapable: 2.3.3
|
||||
typescript: 5.9.3
|
||||
webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)
|
||||
@@ -21543,7 +21543,7 @@ snapshots:
|
||||
|
||||
history@4.10.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@babel/runtime': 7.29.7
|
||||
loose-envify: 1.4.0
|
||||
resolve-pathname: 3.0.0
|
||||
tiny-invariant: 1.3.3
|
||||
@@ -22131,7 +22131,7 @@ snapshots:
|
||||
lodash.isstring: 4.0.1
|
||||
lodash.once: 4.1.1
|
||||
ms: 2.1.3
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
|
||||
just-compare@2.3.0: {}
|
||||
|
||||
@@ -22417,7 +22417,7 @@ snapshots:
|
||||
|
||||
make-dir@4.0.0:
|
||||
dependencies:
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
|
||||
maplibre-gl@5.24.0:
|
||||
dependencies:
|
||||
@@ -23234,14 +23234,14 @@ snapshots:
|
||||
'@opentelemetry/host-metrics': 0.38.3(@opentelemetry/api@1.9.1)
|
||||
tslib: 2.8.1
|
||||
|
||||
nestjs-zod@5.4.0(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3))(rxjs@7.8.2)(zod@4.3.6):
|
||||
nestjs-zod@5.4.0(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6):
|
||||
dependencies:
|
||||
'@nestjs/common': 11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
deepmerge: 4.3.1
|
||||
rxjs: 7.8.2
|
||||
zod: 4.3.6
|
||||
optionalDependencies:
|
||||
'@nestjs/swagger': 11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)(typescript@6.0.3)
|
||||
'@nestjs/swagger': 11.4.3(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(reflect-metadata@0.2.2)
|
||||
|
||||
next-tick@1.1.0: {}
|
||||
|
||||
@@ -23252,7 +23252,7 @@ snapshots:
|
||||
|
||||
node-abi@3.92.0:
|
||||
dependencies:
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
optional: true
|
||||
|
||||
node-abort-controller@3.1.1: {}
|
||||
@@ -23293,7 +23293,7 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
nopt: 9.0.0
|
||||
proc-log: 6.1.0
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
tar: 7.5.15
|
||||
tinyglobby: 0.2.16
|
||||
undici: 6.25.0
|
||||
@@ -23531,7 +23531,7 @@ snapshots:
|
||||
got: 12.6.1
|
||||
registry-auth-token: 5.1.1
|
||||
registry-url: 6.0.1
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
|
||||
package-manager-detector@1.6.0: {}
|
||||
|
||||
@@ -23919,7 +23919,7 @@ snapshots:
|
||||
cosmiconfig: 8.3.6(typescript@6.0.3)
|
||||
jiti: 1.21.7
|
||||
postcss: 8.5.15
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
webpack: 5.107.0(postcss@8.5.15)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
@@ -24974,12 +24974,14 @@ snapshots:
|
||||
|
||||
semver-diff@4.0.0:
|
||||
dependencies:
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.8.0: {}
|
||||
|
||||
semver@7.8.1: {}
|
||||
|
||||
send@0.19.2:
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
@@ -25514,7 +25516,7 @@ snapshots:
|
||||
postcss: 8.5.15
|
||||
postcss-scss: 4.0.9(postcss@8.5.15)
|
||||
postcss-selector-parser: 7.1.1
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
optionalDependencies:
|
||||
svelte: 5.55.8(@typescript-eslint/types@8.59.4)
|
||||
|
||||
@@ -26222,7 +26224,7 @@ snapshots:
|
||||
is-yarn-global: 0.4.1
|
||||
latest-version: 7.0.0
|
||||
pupa: 3.3.0
|
||||
semver: 7.8.0
|
||||
semver: 7.8.1
|
||||
semver-diff: 4.0.0
|
||||
xdg-basedir: 5.1.0
|
||||
|
||||
|
||||
@@ -60,9 +60,6 @@ packageExtensions:
|
||||
dependencies:
|
||||
node-addon-api: '*'
|
||||
node-gyp: '*'
|
||||
'@nestjs/swagger':
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
dedupePeerDependents: false
|
||||
preferWorkspacePackages: true
|
||||
injectWorkspacePackages: true
|
||||
|
||||
+2
-4
@@ -13,15 +13,14 @@ FROM builder AS server
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY ./server ./server/
|
||||
COPY ./packages/sdk ./packages/sdk/
|
||||
COPY ./packages/plugin-sdk ./packages/plugin-sdk/
|
||||
RUN --mount=type=cache,id=pnpm-server,target=/buildcache/pnpm-store \
|
||||
--mount=type=bind,source=package.json,target=package.json \
|
||||
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
|
||||
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
|
||||
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter immich --frozen-lockfile build && \
|
||||
SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned
|
||||
SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich --filter @immich/plugin-sdk --frozen-lockfile build && \
|
||||
SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned
|
||||
|
||||
FROM builder AS web
|
||||
|
||||
@@ -67,7 +66,6 @@ ENV MISE_DISABLE_TOOLS=flutter
|
||||
RUN --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
mise install
|
||||
|
||||
COPY ./packages/sdk ./packages/sdk/
|
||||
COPY ./packages/plugin-core ./packages/plugin-core/
|
||||
COPY ./packages/plugin-sdk ./packages/plugin-sdk/
|
||||
|
||||
|
||||
@@ -68,7 +68,6 @@ run = [
|
||||
[tasks.ci-medium]
|
||||
run = [
|
||||
{ task = ":install" },
|
||||
{ task = "//:plugins" },
|
||||
{ task = "//packages/plugin-core:build" },
|
||||
{ task = ":test-medium --run" },
|
||||
]
|
||||
|
||||
+1
-1
@@ -106,7 +106,7 @@
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"semver": "^7.6.2",
|
||||
"semver": "^7.8.1",
|
||||
"sharp": "^0.34.5",
|
||||
"sirv": "^3.0.0",
|
||||
"socket.io": "^4.8.1",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CronExpression } from '@nestjs/schedule';
|
||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
||||
import {
|
||||
AudioCodec,
|
||||
Colorspace,
|
||||
@@ -135,6 +136,7 @@ export type SystemConfig = {
|
||||
};
|
||||
newVersionCheck: {
|
||||
enabled: boolean;
|
||||
channel: ReleaseChannel;
|
||||
};
|
||||
nightlyTasks: {
|
||||
startTime: string;
|
||||
@@ -344,6 +346,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
},
|
||||
newVersionCheck: {
|
||||
enabled: true,
|
||||
channel: ReleaseChannel.Stable,
|
||||
},
|
||||
nightlyTasks: {
|
||||
startTime: '00:00',
|
||||
|
||||
@@ -265,3 +265,13 @@ export class HistoryBuilder {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
export const extraModels: Function[] = [];
|
||||
|
||||
export const ExtraModel = (): ClassDecorator => {
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping, @typescript-eslint/no-unsafe-function-type
|
||||
return (object: Function) => {
|
||||
extraModels.push(object);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -65,8 +65,6 @@ const UpdateAlbumSchema = z
|
||||
|
||||
const GetAlbumsSchema = z
|
||||
.object({
|
||||
id: z.uuidv4().optional().describe('Album ID'),
|
||||
name: z.string().optional().describe('Album name (exact match)'),
|
||||
isOwned: stringToBool
|
||||
.optional()
|
||||
.describe('Filter by ownership: true = only owned, false = only shared-with-me, undefined = no filter'),
|
||||
|
||||
@@ -38,7 +38,6 @@ const PluginManifestTemplateSchema = z
|
||||
description: z.string().min(1).describe('Template description'),
|
||||
trigger: WorkflowTriggerSchema.describe('Workflow trigger'),
|
||||
steps: z.array(PluginManifestTemplateStepSchema).describe('Workflow steps'),
|
||||
uiHints: z.array(z.string()).optional().default([]).describe('Ui hints, for example "smart-album"'),
|
||||
})
|
||||
.meta({ id: 'PluginManifestTemplateDto' });
|
||||
|
||||
|
||||
@@ -58,7 +58,6 @@ const PluginTemplateResponseSchema = z
|
||||
description: z.string().describe('Template description'),
|
||||
trigger: WorkflowTriggerSchema.describe('Workflow trigger'),
|
||||
steps: z.array(PluginTemplateStepResponseSchema).describe('Workflow steps'),
|
||||
uiHints: z.array(z.string()).describe('Ui hints, for example "smart-album"'),
|
||||
})
|
||||
.meta({ id: 'PluginTemplateResponseDto' });
|
||||
|
||||
@@ -92,7 +91,6 @@ export type PluginTemplate = {
|
||||
config?: Record<string, unknown> | null;
|
||||
enabled?: boolean;
|
||||
}>;
|
||||
uiHints: string[];
|
||||
};
|
||||
|
||||
export const mapTemplate = (plugin: { name: string }, template: PluginTemplate): PluginTemplateResponseDto => {
|
||||
@@ -106,7 +104,6 @@ export const mapTemplate = (plugin: { name: string }, template: PluginTemplate):
|
||||
config: step.config ?? null,
|
||||
enabled: step.enabled,
|
||||
})),
|
||||
uiHints: template.uiHints ?? [],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import type { SemVer } from 'semver';
|
||||
import { ExtraModel, HistoryBuilder } from 'src/decorators';
|
||||
import { isoDatetimeToDate } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
@@ -58,9 +59,15 @@ const ServerStorageResponseSchema = z
|
||||
|
||||
const ServerVersionResponseSchema = z
|
||||
.object({
|
||||
major: z.int().describe('Major version number'),
|
||||
minor: z.int().describe('Minor version number'),
|
||||
patch: z.int().describe('Patch version number'),
|
||||
major: z.int().min(0).describe('Major version number'),
|
||||
minor: z.int().min(0).describe('Minor version number'),
|
||||
patch: z.int().min(0).describe('Patch version number'),
|
||||
prerelease: z
|
||||
.int()
|
||||
.min(0)
|
||||
.nullable()
|
||||
.meta(HistoryBuilder.v3().getExtensions())
|
||||
.describe('Pre-release version number'),
|
||||
})
|
||||
.meta({ id: 'ServerVersionResponseDto' });
|
||||
|
||||
@@ -140,6 +147,27 @@ const ServerFeaturesSchema = z
|
||||
})
|
||||
.meta({ id: 'ServerFeaturesDto' });
|
||||
|
||||
export enum ReleaseType {
|
||||
Major = 'major',
|
||||
Premajor = 'premajor',
|
||||
Minor = 'minor',
|
||||
Preminor = 'preminor',
|
||||
Patch = 'patch',
|
||||
Prepatch = 'prepatch',
|
||||
Prerelease = 'prerelease',
|
||||
Release = 'release',
|
||||
}
|
||||
|
||||
const ReleaseTypeSchema = z.enum(ReleaseType).meta({ id: 'ReleaseType' }).describe('Release type');
|
||||
|
||||
const ReleaseEventV1Schema = z.object({
|
||||
isAvailable: z.boolean().describe('Whether a new version is available'),
|
||||
checkedAt: z.string().describe('When the server last checked for a latest version. As an ISO timestamp'),
|
||||
serverVersion: ServerVersionResponseSchema,
|
||||
releaseVersion: ServerVersionResponseSchema,
|
||||
type: ReleaseTypeSchema.nullable(),
|
||||
});
|
||||
|
||||
export class ServerPingResponse extends createZodDto(ServerPingResponseSchema) {}
|
||||
export class ServerAboutResponseDto extends createZodDto(ServerAboutResponseSchema) {}
|
||||
export class ServerApkLinksDto extends createZodDto(ServerApkLinksSchema) {}
|
||||
@@ -147,7 +175,12 @@ export class ServerStorageResponseDto extends createZodDto(ServerStorageResponse
|
||||
|
||||
export class ServerVersionResponseDto extends createZodDto(ServerVersionResponseSchema) {
|
||||
static fromSemVer(value: SemVer): z.infer<typeof ServerVersionResponseSchema> {
|
||||
return { major: value.major, minor: value.minor, patch: value.patch };
|
||||
return {
|
||||
major: value.major,
|
||||
minor: value.minor,
|
||||
patch: value.patch,
|
||||
prerelease: (value.prerelease[1] as number) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,10 +191,5 @@ export class ServerMediaTypesResponseDto extends createZodDto(ServerMediaTypesRe
|
||||
export class ServerConfigDto extends createZodDto(ServerConfigSchema) {}
|
||||
export class ServerFeaturesDto extends createZodDto(ServerFeaturesSchema) {}
|
||||
|
||||
export interface ReleaseNotification {
|
||||
isAvailable: boolean;
|
||||
/** ISO8601 */
|
||||
checkedAt: string;
|
||||
serverVersion: ServerVersionResponseDto;
|
||||
releaseVersion: ServerVersionResponseDto;
|
||||
}
|
||||
@ExtraModel()
|
||||
export class ReleaseEventV1 extends createZodDto(ReleaseEventV1Schema) {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import { ExtraModel } from 'src/decorators';
|
||||
import { AssetEditActionSchema } from 'src/dtos/editing.dto';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
@@ -17,15 +17,6 @@ import {
|
||||
import { isoDatetimeToDate } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
export const extraSyncModels: Function[] = [];
|
||||
|
||||
const ExtraModel = (): ClassDecorator => {
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
return (object: Function) => {
|
||||
extraSyncModels.push(object);
|
||||
};
|
||||
};
|
||||
|
||||
const SyncUserV1Schema = z
|
||||
.object({
|
||||
id: z.string().describe('User ID'),
|
||||
|
||||
@@ -151,8 +151,15 @@ const SystemConfigMapSchema = z
|
||||
})
|
||||
.meta({ id: 'SystemConfigMapDto' });
|
||||
|
||||
export enum ReleaseChannel {
|
||||
Stable = 'stable',
|
||||
ReleaseCandidate = 'releaseCandidate',
|
||||
}
|
||||
|
||||
const ReleaseChannelSchema = z.enum(ReleaseChannel).describe('Release channel').meta({ id: 'ReleaseChannel' });
|
||||
|
||||
const SystemConfigNewVersionCheckSchema = z
|
||||
.object({ enabled: configBool.describe('Enabled') })
|
||||
.object({ enabled: configBool.describe('Enabled'), channel: ReleaseChannelSchema })
|
||||
.meta({ id: 'SystemConfigNewVersionCheckDto' });
|
||||
|
||||
const SystemConfigNightlyTasksSchema = z
|
||||
|
||||
+1
-1
@@ -866,7 +866,7 @@ export enum JobName {
|
||||
Ocr = 'Ocr',
|
||||
|
||||
// Workflow
|
||||
WorkflowAssetTrigger = 'WorkflowAssetTrigger',
|
||||
WorkflowAssetCreate = 'WorkflowAssetCreate',
|
||||
}
|
||||
|
||||
export const JobNameSchema = z.enum(JobName).describe('Job name').meta({ id: 'JobName' });
|
||||
|
||||
@@ -209,16 +209,11 @@ export class AlbumRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, { isOwned: true, isShared: true }] })
|
||||
getAll(
|
||||
ownerId: string,
|
||||
options: { id?: string; isOwned?: boolean; isShared?: boolean; name?: string } = {},
|
||||
): Promise<MapAlbumDto[]> {
|
||||
getAll(ownerId: string, options: { isOwned?: boolean; isShared?: boolean } = {}): Promise<MapAlbumDto[]> {
|
||||
return this.buildAlbumBaseQuery(ownerId, options)
|
||||
.selectAll('album')
|
||||
.select(withAlbumUsers(ownerId))
|
||||
.select(withSharedLink)
|
||||
.$if(!!options.id, (qb) => qb.where('album.id', '=', options.id!))
|
||||
.$if(!!options.name, (qb) => qb.where('album.albumName', '=', options.name!))
|
||||
.orderBy('album.createdAt', 'desc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { exec as execCallback } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
@@ -64,10 +65,12 @@ export class ServerInfoRepository {
|
||||
this.logger.setContext(ServerInfoRepository.name);
|
||||
}
|
||||
|
||||
async getLatestRelease(): Promise<VersionResponse> {
|
||||
async getLatestRelease(channel: ReleaseChannel): Promise<VersionResponse> {
|
||||
try {
|
||||
const { versionCheck } = this.configRepository.getEnv();
|
||||
const response = await fetch(versionCheck.url);
|
||||
const url = new URL(versionCheck.url);
|
||||
url.searchParams.append('channel', channel);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Version check request failed with status ${response.status}: ${await response.text()}`);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Server, Socket } from 'socket.io';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { NotificationDto } from 'src/dtos/notification.dto';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { ReleaseEventV1, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { SyncAssetEditV1, SyncAssetExifV1, SyncAssetV2 } from 'src/dtos/sync.dto';
|
||||
import { AppRestartEvent, ArgsOf, EventRepository } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
@@ -31,7 +31,7 @@ export interface ClientEventMap {
|
||||
on_person_thumbnail: [string];
|
||||
on_server_version: [ServerVersionResponseDto];
|
||||
on_config_update: [];
|
||||
on_new_release: [ReleaseNotification];
|
||||
on_new_release: [ReleaseEventV1];
|
||||
on_notification: [NotificationDto];
|
||||
on_session_delete: [string];
|
||||
|
||||
|
||||
@@ -45,10 +45,10 @@ export class WorkflowRepository {
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
search(dto: WorkflowSearchDto & { userId?: string }) {
|
||||
search(dto: WorkflowSearchDto & { ownerId?: string }) {
|
||||
return this.queryBuilder()
|
||||
.$if(!!dto.id, (qb) => qb.where('id', '=', dto.id!))
|
||||
.$if(!!dto.userId, (qb) => qb.where('ownerId', '=', dto.userId!))
|
||||
.$if(!!dto.ownerId, (qb) => qb.where('ownerId', '=', dto.ownerId!))
|
||||
.$if(!!dto.trigger, (qb) => qb.where('trigger', '=', dto.trigger!))
|
||||
.$if(dto.enabled !== undefined, (qb) => qb.where('enabled', '=', dto.enabled!))
|
||||
.orderBy('createdAt', 'desc')
|
||||
|
||||
@@ -37,12 +37,15 @@ export class AlbumService extends BaseService {
|
||||
};
|
||||
}
|
||||
|
||||
async getAll({ user: { id: ownerId } }: AuthDto, { assetId, ...rest }: GetAlbumsDto): Promise<AlbumResponseDto[]> {
|
||||
async getAll(
|
||||
{ user: { id: ownerId } }: AuthDto,
|
||||
{ assetId, isOwned, isShared }: GetAlbumsDto,
|
||||
): Promise<AlbumResponseDto[]> {
|
||||
await this.albumRepository.updateThumbnails();
|
||||
|
||||
const albums = assetId
|
||||
? await this.albumRepository.getByAssetId(ownerId, assetId)
|
||||
: await this.albumRepository.getAll(ownerId, rest);
|
||||
: await this.albumRepository.getAll(ownerId, { isOwned, isShared });
|
||||
|
||||
if (albums.length === 0) {
|
||||
return [];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
||||
import {
|
||||
AudioCodec,
|
||||
Colorspace,
|
||||
@@ -184,6 +185,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||
},
|
||||
newVersionCheck: {
|
||||
enabled: true,
|
||||
channel: ReleaseChannel.Stable,
|
||||
},
|
||||
trash: {
|
||||
enabled: true,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DateTime } from 'luxon';
|
||||
import { SemVer } from 'semver';
|
||||
import { defaults } from 'src/config';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
||||
import { CronJob, JobName, JobStatus, SystemMetadataKey } from 'src/enum';
|
||||
import { VersionService } from 'src/services/version.service';
|
||||
import { factory } from 'test/small.factory';
|
||||
@@ -22,6 +23,17 @@ describe(VersionService.name, () => {
|
||||
mocks.cron.update.mockResolvedValue();
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
vitest.mock(import('src/constants.js'), async () => ({
|
||||
...(await vitest.importActual<typeof import('src/constants.js')>('src/constants.js')),
|
||||
serverVersion: new SemVer('v3.0.0'),
|
||||
}));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vitest.unmock(import('src/constants.js'));
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
@@ -66,9 +78,10 @@ describe(VersionService.name, () => {
|
||||
describe('getVersion', () => {
|
||||
it('should respond the server version', () => {
|
||||
expect(sut.getVersion()).toEqual({
|
||||
major: serverVersion.major,
|
||||
minor: serverVersion.minor,
|
||||
patch: serverVersion.patch,
|
||||
major: 3,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -143,24 +156,24 @@ describe(VersionService.name, () => {
|
||||
describe('onConfigUpdate', () => {
|
||||
it('should queue a version check job when newVersionCheck is enabled', async () => {
|
||||
await sut.onConfigUpdate({
|
||||
oldConfig: { ...defaults, newVersionCheck: { enabled: false } },
|
||||
newConfig: { ...defaults, newVersionCheck: { enabled: true } },
|
||||
oldConfig: { ...defaults, newVersionCheck: { enabled: false, channel: ReleaseChannel.Stable } },
|
||||
newConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } },
|
||||
});
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.VersionCheck, data: {} });
|
||||
});
|
||||
|
||||
it('should not queue a version check job when newVersionCheck is disabled', async () => {
|
||||
await sut.onConfigUpdate({
|
||||
oldConfig: { ...defaults, newVersionCheck: { enabled: true } },
|
||||
newConfig: { ...defaults, newVersionCheck: { enabled: false } },
|
||||
oldConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } },
|
||||
newConfig: { ...defaults, newVersionCheck: { enabled: false, channel: ReleaseChannel.Stable } },
|
||||
});
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not queue a version check job when newVersionCheck was already enabled', async () => {
|
||||
await sut.onConfigUpdate({
|
||||
oldConfig: { ...defaults, newVersionCheck: { enabled: true } },
|
||||
newConfig: { ...defaults, newVersionCheck: { enabled: true } },
|
||||
oldConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } },
|
||||
newConfig: { ...defaults, newVersionCheck: { enabled: true, channel: ReleaseChannel.Stable } },
|
||||
});
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -169,21 +182,36 @@ describe(VersionService.name, () => {
|
||||
describe('onWebsocketConnection', () => {
|
||||
it('should send on_server_version client event', async () => {
|
||||
await sut.onWebsocketConnection({ userId: '42' });
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', {
|
||||
major: 3,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: null,
|
||||
});
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should also send a new release notification', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue({ checkedAt: '2024-01-01', releaseVersion: 'v1.42.0' });
|
||||
await sut.onWebsocketConnection({ userId: '42' });
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', {
|
||||
major: 3,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: null,
|
||||
});
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||
});
|
||||
|
||||
it('should not send a release notification when the version check is disabled', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValueOnce({ newVersionCheck: { enabled: false } });
|
||||
await sut.onWebsocketConnection({ userId: '42' });
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', {
|
||||
major: 3,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
prerelease: null,
|
||||
});
|
||||
expect(mocks.websocket.clientSend).not.toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,19 +3,27 @@ import { DateTime } from 'luxon';
|
||||
import semver, { SemVer } from 'semver';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { ReleaseEventV1, ReleaseType, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { ReleaseChannel } from 'src/dtos/system-config.dto';
|
||||
import { CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { VersionCheckMetadata } from 'src/types';
|
||||
import { handlePromiseError } from 'src/utils/misc';
|
||||
|
||||
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
|
||||
const asNotification = (
|
||||
channel: ReleaseChannel,
|
||||
{ checkedAt, releaseVersion }: VersionCheckMetadata,
|
||||
): ReleaseEventV1 => {
|
||||
return {
|
||||
isAvailable: semver.gt(releaseVersion, serverVersion),
|
||||
// can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483
|
||||
isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion.toString(), {
|
||||
includePrerelease: channel === ReleaseChannel.ReleaseCandidate,
|
||||
}),
|
||||
checkedAt,
|
||||
serverVersion: ServerVersionResponseDto.fromSemVer(serverVersion),
|
||||
releaseVersion: ServerVersionResponseDto.fromSemVer(new SemVer(releaseVersion)),
|
||||
type: semver.diff(serverVersion, releaseVersion) as ReleaseType,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -98,14 +106,21 @@ export class VersionService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease();
|
||||
const { version: releaseVersion, published_at: publishedAt } = await this.serverInfoRepository.getLatestRelease(
|
||||
newVersionCheck.channel,
|
||||
);
|
||||
const metadata: VersionCheckMetadata = { checkedAt: DateTime.utc().toISO(), releaseVersion };
|
||||
|
||||
await this.systemMetadataRepository.set(SystemMetadataKey.VersionCheckState, metadata);
|
||||
|
||||
if (semver.gt(releaseVersion, serverVersion)) {
|
||||
// can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483
|
||||
if (
|
||||
semver.intersects(`>${serverVersion}`, releaseVersion.toString(), {
|
||||
includePrerelease: newVersionCheck.channel === ReleaseChannel.ReleaseCandidate,
|
||||
})
|
||||
) {
|
||||
this.logger.log(`Found ${releaseVersion}, released at ${new Date(publishedAt).toLocaleString()}`);
|
||||
this.websocketRepository.clientBroadcast('on_new_release', asNotification(metadata));
|
||||
this.websocketRepository.clientBroadcast('on_new_release', asNotification(newVersionCheck.channel, metadata));
|
||||
}
|
||||
} catch (error: Error | any) {
|
||||
this.logger.warn(`Unable to run version check: ${error}\n${error?.stack}`);
|
||||
@@ -117,7 +132,11 @@ export class VersionService extends BaseService {
|
||||
|
||||
@OnEvent({ name: 'WebsocketConnect' })
|
||||
async onWebsocketConnection({ userId }: ArgOf<'WebsocketConnect'>) {
|
||||
this.websocketRepository.clientSend('on_server_version', userId, serverVersion);
|
||||
this.websocketRepository.clientSend(
|
||||
'on_server_version',
|
||||
userId,
|
||||
ServerVersionResponseDto.fromSemVer(serverVersion),
|
||||
);
|
||||
|
||||
const { newVersionCheck } = await this.getConfig({ withCache: true });
|
||||
if (!newVersionCheck.enabled) {
|
||||
@@ -126,7 +145,7 @@ export class VersionService extends BaseService {
|
||||
|
||||
const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState);
|
||||
if (metadata) {
|
||||
this.websocketRepository.clientSend('on_new_release', userId, asNotification(metadata));
|
||||
this.websocketRepository.clientSend('on_new_release', userId, asNotification(newVersionCheck.channel, metadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { CurrentPlugin } from '@extism/extism';
|
||||
import { WorkflowChanges, WorkflowEventData, WorkflowEventPayload, WorkflowResponse } from '@immich/plugin-sdk';
|
||||
import { HttpException, UnauthorizedException } from '@nestjs/common';
|
||||
import _ from 'lodash';
|
||||
import { join } from 'node:path';
|
||||
import { DummyValue, OnEvent, OnJob } from 'src/decorators';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { AlbumsAddAssetsDto } from 'src/dtos/album.dto';
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
} from 'src/enum';
|
||||
import { ArgOf } from 'src/repositories/event.repository';
|
||||
import { AlbumService } from 'src/services/album.service';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { JobOf } from 'src/types';
|
||||
|
||||
@@ -32,11 +32,9 @@ const dummy = () => {
|
||||
|
||||
type ExecuteOptions<T extends WorkflowType> = {
|
||||
read: (type: T) => Promise<{ authUserId: string; data: WorkflowEventData<T> }>;
|
||||
write: (auth: AuthDto, changes: WorkflowChanges<T>) => Promise<void>;
|
||||
write: (changes: WorkflowChanges<T>) => Promise<void>;
|
||||
};
|
||||
|
||||
type AssetTrigger = { userId: string; assetId: string; trigger: WorkflowTrigger };
|
||||
|
||||
export class WorkflowExecutionService extends BaseService {
|
||||
private jwtSecret!: string;
|
||||
|
||||
@@ -64,6 +62,7 @@ export class WorkflowExecutionService extends BaseService {
|
||||
const albumAddAssets = this.wrap<[id: string, dto: BulkIdsDto]>((authDto, args) =>
|
||||
albumService.addAssets(authDto, ...args),
|
||||
);
|
||||
|
||||
const addAssetsToAlbums = this.wrap<[dto: AlbumsAddAssetsDto]>((authDto, args) =>
|
||||
albumService.addAssetsToAlbums(authDto, ...args),
|
||||
);
|
||||
@@ -248,25 +247,20 @@ export class WorkflowExecutionService extends BaseService {
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AssetCreate' })
|
||||
onAssetCreate({ asset: { ownerId: userId, id: assetId } }: ArgOf<'AssetCreate'>) {
|
||||
return this.onAssetTrigger({ userId, assetId, trigger: WorkflowTrigger.AssetCreate });
|
||||
}
|
||||
|
||||
private async onAssetTrigger({ userId, assetId, trigger }: AssetTrigger) {
|
||||
const items = await this.workflowRepository.search({ userId, trigger });
|
||||
async onAssetCreate({ asset }: ArgOf<'AssetCreate'>) {
|
||||
const dto = { ownerId: asset.ownerId, trigger: WorkflowTrigger.AssetCreate };
|
||||
const items = await this.workflowRepository.search(dto);
|
||||
await this.jobRepository.queueAll(
|
||||
items.map((workflow) => ({
|
||||
name: JobName.WorkflowAssetTrigger,
|
||||
data: { workflowId: workflow.id, assetId, trigger },
|
||||
name: JobName.WorkflowAssetCreate,
|
||||
data: { workflowId: workflow.id, assetId: asset.id },
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.WorkflowAssetTrigger, queue: QueueName.Workflow })
|
||||
handleAssetTrigger({ workflowId, assetId }: JobOf<JobName.WorkflowAssetTrigger>) {
|
||||
@OnJob({ name: JobName.WorkflowAssetCreate, queue: QueueName.Workflow })
|
||||
handleAssetCreate({ workflowId, assetId }: JobOf<JobName.WorkflowAssetCreate>) {
|
||||
return this.execute(workflowId, (type) => {
|
||||
const assetService = BaseService.create(AssetService, this);
|
||||
|
||||
switch (type) {
|
||||
case WorkflowType.AssetV1: {
|
||||
return {
|
||||
@@ -277,16 +271,19 @@ export class WorkflowExecutionService extends BaseService {
|
||||
authUserId: asset.ownerId,
|
||||
};
|
||||
},
|
||||
write: async (auth, changes) => {
|
||||
const asset = changes.asset;
|
||||
if (!asset) {
|
||||
return;
|
||||
write: async (changes) => {
|
||||
if (changes.asset) {
|
||||
await this.assetRepository.update({
|
||||
id: assetId,
|
||||
..._.omitBy(
|
||||
{
|
||||
isFavorite: changes.asset?.isFavorite,
|
||||
visibility: changes.asset?.visibility,
|
||||
},
|
||||
_.isUndefined,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
await assetService.update(auth, assetId, {
|
||||
isFavorite: asset.isFavorite,
|
||||
visibility: asset.visibility,
|
||||
});
|
||||
},
|
||||
} satisfies ExecuteOptions<typeof type>;
|
||||
}
|
||||
@@ -304,19 +301,7 @@ export class WorkflowExecutionService extends BaseService {
|
||||
}
|
||||
|
||||
// TODO infer from steps
|
||||
let type: T | undefined;
|
||||
for (const targetType of Object.values(WorkflowType)) {
|
||||
const missing = workflow.steps.some((step) => !step.types.includes(targetType));
|
||||
if (!missing) {
|
||||
type = targetType as unknown as T;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
throw new Error('Unable to infer workflow event type from steps');
|
||||
}
|
||||
|
||||
const type = 'AssetV1' as T;
|
||||
const handler = getHandler(type);
|
||||
if (!handler) {
|
||||
this.logger.error(`Misconfigured workflow ${workflowId}: no handler for type ${type}`);
|
||||
@@ -352,18 +337,7 @@ export class WorkflowExecutionService extends BaseService {
|
||||
payload,
|
||||
);
|
||||
if (result?.changes) {
|
||||
await write(
|
||||
{
|
||||
user: {
|
||||
id: readResult.authUserId,
|
||||
},
|
||||
session: {
|
||||
id: DummyValue.UUID,
|
||||
hasElevatedPermission: true,
|
||||
},
|
||||
} as AuthDto,
|
||||
result.changes,
|
||||
);
|
||||
await write(result.changes);
|
||||
({ data } = await read(type));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export class WorkflowService extends BaseService {
|
||||
}
|
||||
|
||||
async search(auth: AuthDto, dto: WorkflowSearchDto): Promise<WorkflowResponseDto[]> {
|
||||
const workflows = await this.workflowRepository.search({ ...dto, userId: auth.user.id });
|
||||
const workflows = await this.workflowRepository.search({ ...dto, ownerId: auth.user.id });
|
||||
return workflows.map((workflow) => mapWorkflow(workflow));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -404,7 +404,7 @@ export type JobItem =
|
||||
| { name: JobName.Ocr; data: IEntityJob }
|
||||
|
||||
// Workflow
|
||||
| { name: JobName.WorkflowAssetTrigger; data: { workflowId: string; assetId: string } }
|
||||
| { name: JobName.WorkflowAssetCreate; data: { workflowId: string; assetId: string } }
|
||||
|
||||
// Editor
|
||||
| { name: JobName.AssetEditThumbnailGeneration; data: IEntityJob };
|
||||
|
||||
@@ -15,7 +15,7 @@ import picomatch from 'picomatch';
|
||||
import parse from 'picomatch/lib/parse';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { CLIP_MODEL_INFO, endpointTags, serverVersion } from 'src/constants';
|
||||
import { extraSyncModels } from 'src/dtos/sync.dto';
|
||||
import { extraModels } from 'src/decorators';
|
||||
import { ApiCustomExtension, ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
|
||||
@@ -289,7 +289,7 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean })
|
||||
|
||||
const options: SwaggerDocumentOptions = {
|
||||
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
|
||||
extraModels: extraSyncModels,
|
||||
extraModels,
|
||||
ignoreGlobalPrefix: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ describe('core plugin', () => {
|
||||
steps: [{ method: 'immich-plugin-core#assetArchive' }],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({
|
||||
visibility: AssetVisibility.Archive,
|
||||
@@ -154,7 +154,7 @@ describe('core plugin', () => {
|
||||
steps: [{ method: 'immich-plugin-core#assetArchive', config: { inverse: true } }],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({
|
||||
visibility: AssetVisibility.Timeline,
|
||||
@@ -173,7 +173,7 @@ describe('core plugin', () => {
|
||||
steps: [{ method: 'immich-plugin-core#assetLock' }],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({
|
||||
visibility: AssetVisibility.Locked,
|
||||
@@ -190,7 +190,7 @@ describe('core plugin', () => {
|
||||
steps: [{ method: 'immich-plugin-core#assetLock', config: { inverse: true } }],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({
|
||||
visibility: AssetVisibility.Timeline,
|
||||
@@ -209,7 +209,7 @@ describe('core plugin', () => {
|
||||
steps: [{ method: 'immich-plugin-core#assetFavorite' }],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: true });
|
||||
});
|
||||
@@ -224,7 +224,7 @@ describe('core plugin', () => {
|
||||
steps: [{ method: 'immich-plugin-core#assetFavorite', config: { inverse: true } }],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toMatchObject({ isFavorite: false });
|
||||
});
|
||||
@@ -242,7 +242,7 @@ describe('core plugin', () => {
|
||||
steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album.id] } }],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
|
||||
await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.toContain(asset.id);
|
||||
});
|
||||
@@ -261,7 +261,7 @@ describe('core plugin', () => {
|
||||
steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album1.id, album2.id] } }],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeUndefined();
|
||||
|
||||
await expect(ctx.get(AlbumRepository).getAssetIds(album1.id, [asset.id])).resolves.toContain(asset.id);
|
||||
await expect(ctx.get(AlbumRepository).getAssetIds(album2.id, [asset.id])).resolves.toContain(asset.id);
|
||||
@@ -279,7 +279,7 @@ describe('core plugin', () => {
|
||||
steps: [{ method: 'immich-plugin-core#assetAddToAlbums', config: { albumIds: [album.id] } }],
|
||||
});
|
||||
|
||||
await expect(ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeTruthy();
|
||||
await expect(ctx.sut.handleAssetCreate({ workflowId: workflow.id, assetId: asset.id })).resolves.toBeTruthy();
|
||||
|
||||
await expect(ctx.get(AlbumRepository).getAssetIds(album.id, [asset.id])).resolves.not.toContain(asset.id);
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<Self schema={childSchema} key={childKey} bind:config={getValue, setValue} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if schema.uiHint === 'AlbumId'}
|
||||
{:else if schema.uiHint === 'albumId'}
|
||||
<SchemaAlbumPicker {label} {description} array={schema.array} bind:albumIds={getUiHintValue, setUiHintValue} />
|
||||
{:else if schema.enum && schema.array}
|
||||
<Field {label} {description}>
|
||||
|
||||
@@ -205,7 +205,11 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={['group flex overflow-hidden focus-visible:outline-none', backgroundColorClass, { 'rounded-xl': selected }]}
|
||||
class={[
|
||||
'group flex overflow-hidden transition-[background-color,border-radius] focus-visible:outline-none',
|
||||
backgroundColorClass,
|
||||
{ 'rounded-xl': selected },
|
||||
]}
|
||||
style:width="{width}px"
|
||||
style:height="{height}px"
|
||||
onmouseenter={onMouseEnter}
|
||||
@@ -245,8 +249,16 @@
|
||||
]}
|
||||
>
|
||||
<ImageThumbnail
|
||||
class={['absolute group-focus-visible:rounded-lg', { 'rounded-xl': selected }, imageClass]}
|
||||
brokenAssetClass={['z-1 absolute group-focus-visible:rounded-lg', { 'rounded-xl': selected }, brokenAssetClass]}
|
||||
class={[
|
||||
'absolute transition-[border-radius] group-focus-visible:rounded-lg',
|
||||
{ 'rounded-xl': selected },
|
||||
imageClass,
|
||||
]}
|
||||
brokenAssetClass={[
|
||||
'z-1 absolute group-focus-visible:rounded-lg transition-[border-radius]',
|
||||
{ 'rounded-xl': selected },
|
||||
brokenAssetClass,
|
||||
]}
|
||||
url={getAssetMediaUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })}
|
||||
altText={$getAltText(asset)}
|
||||
widthStyle="{width}px"
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
import ServerAboutModal from '$lib/modals/ServerAboutModal.svelte';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { websocketStore } from '$lib/stores/websocket';
|
||||
import type { ReleaseEvent } from '$lib/types';
|
||||
import { semverToName } from '$lib/utils';
|
||||
import { requestServerInfo } from '$lib/utils/auth';
|
||||
import {
|
||||
getAboutInfo,
|
||||
getVersionHistory,
|
||||
type ReleaseEventV1,
|
||||
type ServerAboutResponseDto,
|
||||
type ServerVersionHistoryResponseDto,
|
||||
} from '@immich/sdk';
|
||||
@@ -35,11 +35,9 @@
|
||||
userInteraction.versions = versions;
|
||||
});
|
||||
let isMain = $derived(info?.sourceRef === 'main' && info.repository === 'immich-app/immich');
|
||||
let version = $derived(
|
||||
$serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null,
|
||||
);
|
||||
let version = $derived($serverVersion ? semverToName($serverVersion) : null);
|
||||
|
||||
const getReleaseInfo = (release?: ReleaseEvent) => {
|
||||
const getReleaseInfo = (release?: ReleaseEventV1) => {
|
||||
if (!release || !release?.isAvailable || !authManager.user.isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ import type {
|
||||
LoginResponseDto,
|
||||
PersonResponseDto,
|
||||
QueueResponseDto,
|
||||
ReleaseEventV1,
|
||||
SharedLinkResponseDto,
|
||||
SystemConfigDto,
|
||||
TagResponseDto,
|
||||
UserAdminResponseDto,
|
||||
WorkflowResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import type { ReleaseEvent } from '$lib/types';
|
||||
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
|
||||
import type { TreeNode } from '$lib/utils/tree-utils';
|
||||
|
||||
@@ -86,7 +86,7 @@ export type Events = {
|
||||
WorkflowUpdate: [WorkflowResponseDto];
|
||||
WorkflowDelete: [WorkflowResponseDto];
|
||||
|
||||
ReleaseEvent: [ReleaseEvent];
|
||||
ReleaseEvent: [ReleaseEventV1];
|
||||
|
||||
WebsocketConnect: [];
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ReleaseEventV1 } from '@immich/sdk';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { type ReleaseEvent } from '$lib/types';
|
||||
|
||||
class ReleaseManager {
|
||||
value = $state<ReleaseEvent | undefined>();
|
||||
value = $state<ReleaseEventV1 | undefined>();
|
||||
|
||||
constructor() {
|
||||
eventManager.on({
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="grow text-start">
|
||||
<Text fontWeight="medium" class="flex items-center gap-1"
|
||||
>{method.title}
|
||||
{#if method.uiHints.includes('Filter')}
|
||||
{#if method.uiHints.includes('filter')}
|
||||
<Badge size="tiny" color="info" title={$t('plugin_method_filter_type_description')}
|
||||
>{$t('plugin_method_filter_type')}</Badge
|
||||
>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { pluginManager } from '$lib/managers/plugin-manager.svelte';
|
||||
import { handleUpdateWorkflow } from '$lib/services/workflow.service';
|
||||
import { getTriggerDescription, getTriggerName } from '$lib/utils/workflow';
|
||||
import { type WorkflowResponseDto } from '@immich/sdk';
|
||||
import { FormModal, ListButton, Text } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
workflow: WorkflowResponseDto;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const { workflow, onClose }: Props = $props();
|
||||
|
||||
let selected = $state(pluginManager.getTrigger(workflow.trigger));
|
||||
|
||||
const onSubmit = async () => {
|
||||
const success = await handleUpdateWorkflow(workflow.id, { trigger: selected.trigger });
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<FormModal title={$t('trigger')} {onClose} {onSubmit} size="small">
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each pluginManager.triggers as item (item.trigger)}
|
||||
<ListButton selected={item.trigger === selected.trigger} onclick={() => (selected = item)}>
|
||||
<div class="grow text-start">
|
||||
<Text fontWeight="medium">{getTriggerName($t, item.trigger)}</Text>
|
||||
<Text size="tiny" color="muted">{getTriggerDescription($t, item.trigger)}</Text>
|
||||
</div>
|
||||
</ListButton>
|
||||
{/each}
|
||||
</div>
|
||||
</FormModal>
|
||||
+1
-6
@@ -2,7 +2,7 @@
|
||||
import { pluginManager } from '$lib/managers/plugin-manager.svelte';
|
||||
import { handleCreateWorkflow } from '$lib/services/workflow.service';
|
||||
import { type PluginTemplateResponseDto } from '@immich/sdk';
|
||||
import { Badge, FormModal, Icon, ListButton, Text } from '@immich/ui';
|
||||
import { FormModal, Icon, ListButton, Text } from '@immich/ui';
|
||||
import { mdiFlashOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -59,11 +59,6 @@
|
||||
<Text fontWeight="medium">{template.title}</Text>
|
||||
<Text size="tiny" color="muted">{template.description}</Text>
|
||||
</div>
|
||||
{#if template.uiHints.includes('SmartAlbum')}
|
||||
<div class="shrink-0">
|
||||
<Badge size="small">{$t('smart_album')}</Badge>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</ListButton>
|
||||
{/each}
|
||||
@@ -26,7 +26,7 @@ import type { MessageFormatter } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import WorkflowDuplicateModal from '$lib/modals/WorkflowDuplicateModal.svelte';
|
||||
import WorkflowTemplatePickerModal from '$lib/modals/WorkflowTemplatePickerModal.svelte';
|
||||
import WorkflowTemplatePicker from '$lib/modals/WorkflowTemplatePicker.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { copyToClipboard, downloadJson } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@@ -50,7 +50,7 @@ export const getWorkflowsActions = ($t: MessageFormatter) => {
|
||||
const UseTemplate: ActionItem = {
|
||||
title: $t('browse_templates'),
|
||||
icon: mdiFileDocumentMultipleOutline,
|
||||
onAction: () => modalManager.show(WorkflowTemplatePickerModal, {}),
|
||||
onAction: () => modalManager.show(WorkflowTemplatePicker, {}),
|
||||
};
|
||||
|
||||
return { Create, UseTemplate };
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type AssetResponseDto,
|
||||
type MaintenanceStatusResponseDto,
|
||||
type NotificationDto,
|
||||
type ReleaseEventV1,
|
||||
type ServerVersionResponseDto,
|
||||
type SyncAssetEditV1,
|
||||
type SyncAssetV2,
|
||||
@@ -15,7 +16,6 @@ import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { maintenanceStore } from '$lib/stores/maintenance.store';
|
||||
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
||||
import type { ReleaseEvent } from '$lib/types';
|
||||
import { createEventEmitter } from '$lib/utils/eventemitter';
|
||||
|
||||
interface AppRestartEvent {
|
||||
@@ -34,7 +34,7 @@ export interface Events {
|
||||
on_person_thumbnail: (personId: string) => void;
|
||||
on_server_version: (serverVersion: ServerVersionResponseDto) => void;
|
||||
on_config_update: () => void;
|
||||
on_new_release: (event: ReleaseEvent) => void;
|
||||
on_new_release: (event: ReleaseEventV1) => void;
|
||||
on_session_delete: (sessionId: string) => void;
|
||||
on_notification: (notification: NotificationDto) => void;
|
||||
|
||||
|
||||
+2
-10
@@ -1,4 +1,4 @@
|
||||
import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk';
|
||||
import type { QueueResponseDto } from '@immich/sdk';
|
||||
import type { ActionItem } from '@immich/ui';
|
||||
import type { DateTime } from 'luxon';
|
||||
import type { SvelteSet } from 'svelte/reactivity';
|
||||
@@ -7,14 +7,6 @@ import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
|
||||
export type LatLng = { lng: number; lat: number };
|
||||
|
||||
export interface ReleaseEvent {
|
||||
isAvailable: boolean;
|
||||
/** ISO8601 */
|
||||
checkedAt: string;
|
||||
serverVersion: ServerVersionResponseDto;
|
||||
releaseVersion: ServerVersionResponseDto;
|
||||
}
|
||||
|
||||
export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] };
|
||||
|
||||
export type HeaderButtonActionItem = ActionItem & { data?: { title?: string } };
|
||||
@@ -104,7 +96,7 @@ export type JSONSchemaProperty = {
|
||||
array?: boolean;
|
||||
properties?: Record<string, JSONSchemaProperty>;
|
||||
required?: string[];
|
||||
uiHint?: 'AlbumId' | 'AssetId' | 'PersonId';
|
||||
uiHint?: 'albumId' | 'assetId' | 'personId';
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AssetTypeEnum } from '@immich/sdk';
|
||||
import { getAssetUrl, getReleaseType } from '$lib/utils';
|
||||
import { getAssetUrl } from '$lib/utils';
|
||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
||||
import { sharedLinkFactory } from '@test-data/factories/shared-link-factory';
|
||||
|
||||
@@ -161,26 +161,4 @@ describe('utils', () => {
|
||||
expect(url).toContain(asset.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe(getReleaseType.name, () => {
|
||||
it('should return "major" for major version changes', () => {
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 2, minor: 0, patch: 0 })).toBe('major');
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 3, minor: 2, patch: 1 })).toBe('major');
|
||||
});
|
||||
|
||||
it('should return "minor" for minor version changes', () => {
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 1, patch: 0 })).toBe('minor');
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 2, patch: 1 })).toBe('minor');
|
||||
});
|
||||
|
||||
it('should return "patch" for patch version changes', () => {
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 1 })).toBe('patch');
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 5 })).toBe('patch');
|
||||
});
|
||||
|
||||
it('should return "none" for matching versions', () => {
|
||||
expect(getReleaseType({ major: 1, minor: 0, patch: 0 }, { major: 1, minor: 0, patch: 0 })).toBe('none');
|
||||
expect(getReleaseType({ major: 1, minor: 2, patch: 3 }, { major: 1, minor: 2, patch: 3 })).toBe('none');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-20
@@ -411,26 +411,8 @@ export function createDateFormatter(localeCode: string | undefined): DateFormatt
|
||||
};
|
||||
}
|
||||
|
||||
export const getReleaseType = (
|
||||
current: ServerVersionResponseDto,
|
||||
newVersion: ServerVersionResponseDto,
|
||||
): 'major' | 'minor' | 'patch' | 'none' => {
|
||||
if (current.major !== newVersion.major) {
|
||||
return 'major';
|
||||
}
|
||||
|
||||
if (current.minor !== newVersion.minor) {
|
||||
return 'minor';
|
||||
}
|
||||
|
||||
if (current.patch !== newVersion.patch) {
|
||||
return 'patch';
|
||||
}
|
||||
|
||||
return 'none';
|
||||
};
|
||||
|
||||
export const semverToName = ({ major, minor, patch }: ServerVersionResponseDto) => `v${major}.${minor}.${patch}`;
|
||||
export const semverToName = ({ major, minor, patch, prerelease }: ServerVersionResponseDto) =>
|
||||
`v${major}.${minor}.${patch}${prerelease ? `-rc.${prerelease}` : ''}`;
|
||||
|
||||
export const withoutIcons = (actions: ActionItem[]): ActionItem[] =>
|
||||
actions.map((action) => ({ ...action, icon: undefined }));
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
<script module lang="ts">
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { getAlbumInfo } from '@immich/sdk';
|
||||
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const albumNameCache = new Map<string, Promise<string>>();
|
||||
|
||||
const getAlbumName = (id: string): Promise<string> => {
|
||||
let albumName = albumNameCache.get(id);
|
||||
if (!albumName) {
|
||||
albumName = getAlbumInfo({ ...authManager.params, id })
|
||||
.then((album) => album.albumName)
|
||||
.catch(() => id);
|
||||
albumNameCache.set(id, albumName);
|
||||
}
|
||||
return albumName;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { pluginManager } from '$lib/managers/plugin-manager.svelte';
|
||||
import type { JSONSchemaProperty } from '$lib/types';
|
||||
import type { WorkflowStepDto } from '@immich/sdk';
|
||||
import { Badge, Card, CardBody, CardDescription, CardHeader, CardTitle, Icon, IconButton } from '@immich/ui';
|
||||
import {
|
||||
@@ -46,14 +26,10 @@
|
||||
let { step, index, onEdit, onDelete, onInsertBefore, onDrop }: Props = $props();
|
||||
|
||||
const method = $derived(pluginManager.getMethod(step.method));
|
||||
const isFilter = $derived(method?.uiHints?.includes('Filter') ?? false);
|
||||
const schema = $derived(method?.schema as JSONSchemaProperty | undefined);
|
||||
const isFilter = $derived(method?.uiHints?.includes('filter') ?? false);
|
||||
const configEntries = $derived(
|
||||
Object.entries(step.config ?? {}).filter(([, value]) => value !== null && value !== undefined && value !== ''),
|
||||
);
|
||||
|
||||
const getUiHint = (key: string) => schema?.properties?.[key]?.uiHint;
|
||||
const toIds = (value: unknown): string[] => (Array.isArray(value) ? value.map(String) : [String(value)]);
|
||||
let dragImage = $state<Element>();
|
||||
let isDropTarget = $state(false);
|
||||
let hoverDrag = $state(false);
|
||||
@@ -99,7 +75,7 @@
|
||||
target: document.body,
|
||||
props: {
|
||||
description: method?.description,
|
||||
isFilter: method?.uiHints?.includes('Filter') ?? false,
|
||||
isFilter: method?.uiHints?.includes('filter') ?? false,
|
||||
label: step ? pluginManager.getMethodLabel(step.method) : '',
|
||||
stepNumber: index + 1,
|
||||
},
|
||||
@@ -228,28 +204,15 @@
|
||||
{#if configEntries.length > 0}
|
||||
<CardBody class="py-3">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
{#snippet badge(key: string, content: string)}
|
||||
{#each configEntries as [key, value] (key)}
|
||||
<Badge
|
||||
color={isFilter ? 'info' : 'warning'}
|
||||
shape="round"
|
||||
size="small"
|
||||
class="border font-mono {isFilter ? 'border-primary-200' : 'border-warning-200'}"
|
||||
>
|
||||
<span class="opacity-60">{key}</span>{content}
|
||||
<span class="opacity-60">{key}</span>{formatConfigValue(value)}
|
||||
</Badge>
|
||||
{/snippet}
|
||||
{#each configEntries as [key, value] (key)}
|
||||
{#if getUiHint(key) === 'AlbumId'}
|
||||
{#each toIds(value) as albumId (albumId)}
|
||||
{#await getAlbumName(albumId)}
|
||||
{@render badge($t('album'), '…')}
|
||||
{:then albumName}
|
||||
{@render badge($t('album'), `"${truncate(albumName)}"`)}
|
||||
{/await}
|
||||
{/each}
|
||||
{:else}
|
||||
{@render badge(key, formatConfigValue(value))}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</CardBody>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
for (const [i, step] of workflow.steps.entries()) {
|
||||
const method = pluginManager.getMethod(step.method);
|
||||
const isFilter = method?.uiHints?.includes('Filter') ?? false;
|
||||
const isFilter = method?.uiHints?.includes('filter') ?? false;
|
||||
const type = isFilter ? $t('filter') : $t('action');
|
||||
const label = pluginManager.getMethodLabel(step.method);
|
||||
lines.push(` [${i + 1}] ${type.toUpperCase()} · ${label}`);
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte';
|
||||
import type { ReleaseEvent } from '$lib/types';
|
||||
import { getReleaseType, semverToName } from '$lib/utils';
|
||||
import { semverToName } from '$lib/utils';
|
||||
import { ReleaseType, type ReleaseEventV1 } from '@immich/sdk';
|
||||
import { modalManager } from '@immich/ui';
|
||||
|
||||
let modal = $state<{
|
||||
@@ -11,16 +11,20 @@
|
||||
close: () => Promise<void>;
|
||||
}>();
|
||||
|
||||
const onReleaseEvent = async (release: ReleaseEvent) => {
|
||||
const onReleaseEvent = async (release: ReleaseEventV1) => {
|
||||
if (!release.isAvailable || !authManager.user.isAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const releaseVersion = semverToName(release.releaseVersion);
|
||||
const serverVersion = semverToName(release.serverVersion);
|
||||
const type = getReleaseType(release.serverVersion, release.releaseVersion);
|
||||
|
||||
if (type === 'none' || type === 'patch' || localStorage.getItem('appVersion') === releaseVersion) {
|
||||
if (
|
||||
!release.type ||
|
||||
release.type === ReleaseType.Patch ||
|
||||
release.type === ReleaseType.Prepatch ||
|
||||
localStorage.getItem('appVersion') === releaseVersion
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingSelect from './SettingSelect.svelte';
|
||||
import { ReleaseChannel } from '@immich/sdk';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
@@ -20,6 +23,20 @@
|
||||
bind:checked={configToEdit.newVersionCheck.enabled}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingSelect
|
||||
label={$t('admin.version_check_channel')}
|
||||
desc={$t('admin.version_check_channel_description')}
|
||||
bind:value={configToEdit.newVersionCheck.channel}
|
||||
options={[
|
||||
{
|
||||
value: ReleaseChannel.Stable,
|
||||
text: $t('admin.release_channel_stable'),
|
||||
},
|
||||
{ value: ReleaseChannel.ReleaseCandidate, text: $t('admin.release_channel_release_candidate') },
|
||||
]}
|
||||
isEdited={configToEdit.newVersionCheck.channel !== config.newVersionCheck.channel}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingButtonsRow bind:configToEdit keys={['newVersionCheck']} {disabled} />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user