Compare commits

..

11 Commits

Author SHA1 Message Date
github-actions
6563fa608a chore: version v1.135.3 2025-06-20 19:48:18 +00:00
Jason Rasmussen
1a90fc8e58 feat: test for non-standard database name (#19386) 2025-06-20 19:31:16 +00:00
Alex
c707f9cef4 refactor(mobile): partner.interface.dart (#19338) 2025-06-20 18:37:59 +00:00
dotlambda
6fda863c08 fix(server): don't hardcode database name in migration (#19376) 2025-06-20 21:33:34 +03:00
Daniel Dietzler
373b654156 chore: migrate profile picture cropper modal (#19378) 2025-06-20 18:16:10 +00:00
Daniel Dietzler
a5d84ba552 chore: consistent modal footer spacing (#19377) 2025-06-20 18:05:39 +00:00
Daniel Dietzler
1dc8fa2979 chore: rename edit album form modal (#19375) 2025-06-20 13:51:14 -04:00
Alex
0426699f13 refactor(mobile): partner_api.interface.dart (#19337)
* refactor(mobile): partner_api.interface.dart

* merge main
2025-06-20 17:04:15 +00:00
Alex
8154ec29df refactor(mobile): person_api.interface.dart (#19336) 2025-06-20 11:45:31 -05:00
Alex
3024cd343b refactor(mobile): timeline.interface.dart (#19331)
refactor(mobile): timeline.repository.dart
2025-06-20 11:44:02 -05:00
Zack Pollard
0b44d4b6f2 fix: partner and album backfill acks (#19371)
fix: partner sync being entirely broken
2025-06-20 16:14:36 +00:00
65 changed files with 247 additions and 264 deletions

6
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.71",
"version": "2.2.72",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.71",
"version": "2.2.72",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"chokidar": "^4.0.3",
@@ -54,7 +54,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.135.2",
"version": "1.135.3",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.71",
"version": "2.2.72",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -1,4 +1,8 @@
[
{
"label": "v1.135.3",
"url": "https://v1.135.3.archive.immich.app"
},
{
"label": "v1.135.2",
"url": "https://v1.135.2.archive.immich.app"

8
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.135.2",
"version": "1.135.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.135.2",
"version": "1.135.3",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -44,7 +44,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.71",
"version": "2.2.72",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -93,7 +93,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.135.2",
"version": "1.135.3",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.135.2",
"version": "1.135.3",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 203,
"android.injected.version.name" => "1.135.2",
"android.injected.version.code" => 204,
"android.injected.version.name" => "1.135.3",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -22,7 +22,7 @@ platform :ios do
path: "./Runner.xcodeproj",
)
increment_version_number(
version_number: "1.135.2"
version_number: "1.135.3"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -1,11 +1,5 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
abstract interface class IPersonApiRepository {
Future<List<Person>> getAll();
Future<Person> update(String id, {String? name});
}
class Person {
Person({
required this.id,

View File

@@ -1,8 +0,0 @@
import 'package:immich_mobile/domain/models/user.model.dart';
abstract class IPartnerRepository {
Future<List<UserDto>> getSharedWith();
Future<List<UserDto>> getSharedBy();
Stream<List<UserDto>> watchSharedWith();
Stream<List<UserDto>> watchSharedBy();
}

View File

@@ -1,13 +0,0 @@
import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IPartnerApiRepository {
Future<List<UserDto>> getAll(Direction direction);
Future<UserDto> create(String id);
Future<UserDto> update(String id, {required bool inTimeline});
Future<void> delete(String id);
}
enum Direction {
sharedWithMe,
sharedByMe,
}

View File

@@ -1,39 +0,0 @@
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
abstract class ITimelineRepository {
Future<List<String>> getTimelineUserIds(String id);
Stream<List<String>> watchTimelineUsers(String id);
Stream<RenderList> watchArchiveTimeline(String userId);
Stream<RenderList> watchFavoriteTimeline(String userId);
Stream<RenderList> watchTrashTimeline(String userId);
Stream<RenderList> watchAlbumTimeline(
Album album,
GroupAssetsBy groupAssetsBy,
);
Stream<RenderList> watchAllVideosTimeline(String userId);
Stream<RenderList> watchHomeTimeline(
String userId,
GroupAssetsBy groupAssetsBy,
);
Stream<RenderList> watchMultiUsersTimeline(
List<String> userIds,
GroupAssetsBy groupAssetsBy,
);
Future<RenderList> getTimelineFromAssets(
List<Asset> assets,
GroupAssetsBy getGroupByOption,
);
Stream<RenderList> watchAssetSelectionTimeline(String userId);
Stream<RenderList> watchLockedTimeline(
String userId,
GroupAssetsBy groupAssetsBy,
);
}

View File

@@ -1,8 +1,8 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
class SearchLocationFilter {
String? country;

View File

@@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';

View File

@@ -1,5 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/services/person.service.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';

View File

@@ -2,7 +2,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/interfaces/partner.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:isar/isar.dart';
@@ -11,11 +10,9 @@ final partnerRepositoryProvider = Provider(
(ref) => PartnerRepository(ref.watch(dbProvider)),
);
class PartnerRepository extends DatabaseRepository
implements IPartnerRepository {
class PartnerRepository extends DatabaseRepository {
PartnerRepository(super.db);
@override
Future<List<UserDto>> getSharedBy() async {
return (await db.users
.filter()
@@ -26,7 +23,6 @@ class PartnerRepository extends DatabaseRepository
.toList();
}
@override
Future<List<UserDto>> getSharedWith() async {
return (await db.users
.filter()
@@ -37,13 +33,11 @@ class PartnerRepository extends DatabaseRepository
.toList();
}
@override
Stream<List<UserDto>> watchSharedBy() {
return (db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch())
.map((users) => users.map((u) => u.toDto()).toList());
}
@override
Stream<List<UserDto>> watchSharedWith() {
return (db.users
.filter()

View File

@@ -1,24 +1,26 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart';
enum Direction {
sharedWithMe,
sharedByMe,
}
final partnerApiRepositoryProvider = Provider(
(ref) => PartnerApiRepository(
ref.watch(apiServiceProvider).partnersApi,
),
);
class PartnerApiRepository extends ApiRepository
implements IPartnerApiRepository {
class PartnerApiRepository extends ApiRepository {
final PartnersApi _api;
PartnerApiRepository(this._api);
@override
Future<List<UserDto>> getAll(Direction direction) async {
final response = await checkNull(
_api.getPartners(
@@ -30,16 +32,13 @@ class PartnerApiRepository extends ApiRepository
return response.map(UserConverter.fromPartnerDto).toList();
}
@override
Future<UserDto> create(String id) async {
final dto = await checkNull(_api.createPartner(id));
return UserConverter.fromPartnerDto(dto);
}
@override
Future<void> delete(String id) => _api.removePartner(id);
@override
Future<UserDto> update(String id, {required bool inTimeline}) async {
final dto = await checkNull(
_api.updatePartner(

View File

@@ -1,5 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart';
@@ -8,19 +8,16 @@ final personApiRepositoryProvider = Provider(
(ref) => PersonApiRepository(ref.watch(apiServiceProvider).peopleApi),
);
class PersonApiRepository extends ApiRepository
implements IPersonApiRepository {
class PersonApiRepository extends ApiRepository {
final PeopleApi _api;
PersonApiRepository(this._api);
@override
Future<List<Person>> getAll() async {
final dto = await checkNull(_api.getAllPeople());
return dto.people.map(_toPerson).toList();
}
@override
Future<Person> update(String id, {String? name}) async {
final dto = await checkNull(
_api.updatePerson(id, PersonUpdateDto(name: name)),

View File

@@ -3,7 +3,6 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/timeline.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:immich_mobile/utils/hash.dart';
@@ -13,11 +12,9 @@ import 'package:isar/isar.dart';
final timelineRepositoryProvider =
Provider((ref) => TimelineRepository(ref.watch(dbProvider)));
class TimelineRepository extends DatabaseRepository
implements ITimelineRepository {
class TimelineRepository extends DatabaseRepository {
TimelineRepository(super.db);
@override
Future<List<String>> getTimelineUserIds(String id) {
return db.users
.filter()
@@ -28,7 +25,6 @@ class TimelineRepository extends DatabaseRepository
.findAll();
}
@override
Stream<List<String>> watchTimelineUsers(String id) {
return db.users
.filter()
@@ -39,7 +35,6 @@ class TimelineRepository extends DatabaseRepository
.watch();
}
@override
Stream<RenderList> watchArchiveTimeline(String userId) {
final query = db.assets
.where()
@@ -52,7 +47,6 @@ class TimelineRepository extends DatabaseRepository
return _watchRenderList(query, GroupAssetsBy.none);
}
@override
Stream<RenderList> watchFavoriteTimeline(String userId) {
final query = db.assets
.where()
@@ -67,7 +61,6 @@ class TimelineRepository extends DatabaseRepository
return _watchRenderList(query, GroupAssetsBy.none);
}
@override
Stream<RenderList> watchAlbumTimeline(
Album album,
GroupAssetsBy groupAssetByOption,
@@ -86,7 +79,6 @@ class TimelineRepository extends DatabaseRepository
return _watchRenderList(withSortedOption, groupAssetByOption);
}
@override
Stream<RenderList> watchTrashTimeline(String userId) {
final query = db.assets
.filter()
@@ -97,7 +89,6 @@ class TimelineRepository extends DatabaseRepository
return _watchRenderList(query, GroupAssetsBy.none);
}
@override
Stream<RenderList> watchAllVideosTimeline(String userId) {
final query = db.assets
.where()
@@ -111,7 +102,6 @@ class TimelineRepository extends DatabaseRepository
return _watchRenderList(query, GroupAssetsBy.none);
}
@override
Stream<RenderList> watchHomeTimeline(
String userId,
GroupAssetsBy groupAssetByOption,
@@ -128,7 +118,6 @@ class TimelineRepository extends DatabaseRepository
return _watchRenderList(query, groupAssetByOption);
}
@override
Stream<RenderList> watchMultiUsersTimeline(
List<String> userIds,
GroupAssetsBy groupAssetByOption,
@@ -145,7 +134,6 @@ class TimelineRepository extends DatabaseRepository
return _watchRenderList(query, groupAssetByOption);
}
@override
Future<RenderList> getTimelineFromAssets(
List<Asset> assets,
GroupAssetsBy getGroupByOption,
@@ -153,7 +141,6 @@ class TimelineRepository extends DatabaseRepository
return RenderList.fromAssets(assets, getGroupByOption);
}
@override
Stream<RenderList> watchAssetSelectionTimeline(String userId) {
final query = db.assets
.where()
@@ -168,7 +155,6 @@ class TimelineRepository extends DatabaseRepository
return _watchRenderList(query, GroupAssetsBy.none);
}
@override
Stream<RenderList> watchLockedTimeline(
String userId,
GroupAssetsBy getGroupByOption,

View File

@@ -1,8 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/interfaces/partner.interface.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/partner.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
@@ -17,8 +15,8 @@ final partnerServiceProvider = Provider(
);
class PartnerService {
final IPartnerApiRepository _partnerApiRepository;
final IPartnerRepository _partnerRepository;
final PartnerApiRepository _partnerApiRepository;
final PartnerRepository _partnerRepository;
final IsarUserRepository _isarUserRepository;
final Logger _log = Logger("PartnerService");

View File

@@ -1,8 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/person_api.repository.dart';
@@ -20,7 +20,7 @@ PersonService personService(Ref ref) => PersonService(
class PersonService {
final Logger _log = Logger("PersonService");
final IPersonApiRepository _personApiRepository;
final PersonApiRepository _personApiRepository;
final IAssetApiRepository _assetApiRepository;
final IAssetRepository _assetRepository;

View File

@@ -17,8 +17,6 @@ import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
import 'package:immich_mobile/interfaces/partner.interface.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
@@ -69,9 +67,9 @@ class SyncService {
final IExifInfoRepository _exifInfoRepository;
final IsarUserRepository _isarUserRepository;
final UserService _userService;
final IPartnerRepository _partnerRepository;
final PartnerRepository _partnerRepository;
final IETagRepository _eTagRepository;
final IPartnerApiRepository _partnerApiRepository;
final PartnerApiRepository _partnerApiRepository;
final UserApiRepository _userApiRepository;
final AsyncMutex _lock = AsyncMutex();
final Logger _log = Logger('SyncService');

View File

@@ -2,7 +2,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/timeline.interface.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/timeline.repository.dart';
@@ -18,7 +17,7 @@ final timelineServiceProvider = Provider<TimelineService>((ref) {
});
class TimelineService {
final ITimelineRepository _timelineRepository;
final TimelineRepository _timelineRepository;
final AppSettingsService _appSettingsService;
final UserService _userService;

View File

@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/person.model.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/providers/search/people.provider.dart';
import 'package:immich_mobile/services/api.service.dart';

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.135.2
- API version: 1.135.3
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.135.2+203
version: 1.135.3+204
environment:
sdk: '>=3.3.0 <4.0.0'

View File

@@ -11,7 +11,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:mocktail/mocktail.dart';

View File

@@ -9,10 +9,10 @@ import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
import 'package:immich_mobile/interfaces/partner.interface.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/partner.repository.dart';
import 'package:mocktail/mocktail.dart';
class MockAlbumRepository extends Mock implements IAlbumRepository {}
@@ -42,9 +42,9 @@ class MockAuthApiRepository extends Mock implements IAuthApiRepository {}
class MockAuthRepository extends Mock implements IAuthRepository {}
class MockPartnerRepository extends Mock implements IPartnerRepository {}
class MockPartnerRepository extends Mock implements PartnerRepository {}
class MockPartnerApiRepository extends Mock implements IPartnerApiRepository {}
class MockPartnerApiRepository extends Mock implements PartnerApiRepository {}
class MockLocalFilesManagerRepository extends Mock
implements ILocalFilesManager {}

View File

@@ -8503,7 +8503,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.135.2",
"version": "1.135.3",
"contact": {}
},
"tags": [],

View File

@@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
"version": "1.135.2",
"version": "1.135.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.135.2",
"version": "1.135.3",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "1.135.2",
"version": "1.135.3",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.135.2
* 1.135.3
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.135.2",
"version": "1.135.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.135.2",
"version": "1.135.3",
"hasInstallScript": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.135.2",
"version": "1.135.3",
"description": "",
"author": "",
"private": true,

View File

@@ -130,7 +130,7 @@ from
where
"ownerId" = $1
and "updatedAt" < now() - interval '1 millisecond'
and "updateId" < $2
and "updateId" <= $2
and "updateId" >= $3
order by
"updateId" asc
@@ -274,7 +274,7 @@ from
where
"assets"."ownerId" = $1
and "exif"."updatedAt" < now() - interval '1 millisecond'
and "exif"."updateId" < $2
and "exif"."updateId" <= $2
and "exif"."updateId" >= $3
order by
"exif"."updateId" asc
@@ -418,7 +418,7 @@ from
where
"albumsId" = $1
and "updatedAt" < now() - interval '1 millisecond'
and "updateId" < $2
and "updateId" <= $2
and "updateId" >= $3
order by
"updateId" asc

View File

@@ -111,7 +111,7 @@ export class SyncRepository {
.select(columns.syncAsset)
.where('ownerId', '=', partnerId)
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
.where('updateId', '<', beforeUpdateId)
.where('updateId', '<=', beforeUpdateId)
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
.orderBy('updateId', 'asc')
.stream();
@@ -169,7 +169,7 @@ export class SyncRepository {
.innerJoin('assets', 'assets.id', 'exif.assetId')
.where('assets.ownerId', '=', partnerId)
.where('exif.updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
.where('exif.updateId', '<', beforeUpdateId)
.where('exif.updateId', '<=', beforeUpdateId)
.$if(!!afterUpdateId, (eb) => eb.where('exif.updateId', '>=', afterUpdateId!))
.orderBy('exif.updateId', 'asc')
.stream();
@@ -273,7 +273,7 @@ export class SyncRepository {
.select(columns.syncAlbumUser)
.where('albumsId', '=', albumId)
.where('updatedAt', '<', sql.raw<Date>("now() - interval '1 millisecond'"))
.where('updateId', '<', beforeUpdateId)
.where('updateId', '<=', beforeUpdateId)
.$if(!!afterUpdateId, (eb) => eb.where('updateId', '>=', afterUpdateId!))
.orderBy('updateId', 'asc')
.stream();

View File

@@ -7,8 +7,8 @@ export async function up(qb: Kysely<any>): Promise<void> {
return;
}
await sql`alter database immich reset all;`.execute(qb);
const { db, guc } = res.rows[0];
await sql.raw(`alter database "${db}" reset all;`).execute(qb);
for (const parameter of guc) {
const [key, value] = parameter.split('=');
if (key === 'vchordrq.prewarm_dim') {

View File

@@ -38,11 +38,11 @@ const mapSyncAssetV1 = ({ checksum, thumbhash, ...data }: AssetLike): SyncAssetV
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
});
const isEntityBackfillComplete = (entity: { createId: string }, checkpoint: SyncAck | undefined): boolean =>
entity.createId === checkpoint?.updateId && checkpoint.extraId === COMPLETE_ID;
const isEntityBackfillComplete = (createId: string, checkpoint: SyncAck | undefined): boolean =>
createId === checkpoint?.updateId && checkpoint.extraId === COMPLETE_ID;
const getStartId = (entity: { createId: string }, checkpoint: SyncAck | undefined): string | undefined =>
checkpoint?.updateId === entity.createId ? checkpoint?.extraId : undefined;
const getStartId = (createId: string, checkpoint: SyncAck | undefined): string | undefined =>
createId === checkpoint?.updateId ? checkpoint?.extraId : undefined;
const send = <T extends keyof SyncItem, D extends SyncItem[T]>(response: Writable, item: SerializeOptions<T, D>) => {
response.write(serialize(item));
@@ -235,22 +235,23 @@ export class SyncService extends BaseService {
const endId = upsertCheckpoint.updateId;
for (const partner of partners) {
if (isEntityBackfillComplete(partner, backfillCheckpoint)) {
const createId = partner.createId;
if (isEntityBackfillComplete(createId, backfillCheckpoint)) {
continue;
}
const startId = getStartId(partner, backfillCheckpoint);
const startId = getStartId(createId, backfillCheckpoint);
const backfill = this.syncRepository.getPartnerAssetsBackfill(partner.sharedById, startId, endId);
for await (const { updateId, ...data } of backfill) {
send(response, {
type: backfillType,
ids: [updateId],
ids: [createId, updateId],
data: mapSyncAssetV1(data),
});
}
sendEntityBackfillCompleteAck(response, backfillType, partner.sharedById);
sendEntityBackfillCompleteAck(response, backfillType, createId);
}
} else if (partners.length > 0) {
await this.upsertBackfillCheckpoint({
@@ -291,18 +292,19 @@ export class SyncService extends BaseService {
const endId = upsertCheckpoint.updateId;
for (const partner of partners) {
if (isEntityBackfillComplete(partner, backfillCheckpoint)) {
const createId = partner.createId;
if (isEntityBackfillComplete(createId, backfillCheckpoint)) {
continue;
}
const startId = getStartId(partner, backfillCheckpoint);
const startId = getStartId(createId, backfillCheckpoint);
const backfill = this.syncRepository.getPartnerAssetExifsBackfill(partner.sharedById, startId, endId);
for await (const { updateId, ...data } of backfill) {
send(response, { type: backfillType, ids: [updateId], data });
send(response, { type: backfillType, ids: [partner.createId, updateId], data });
}
sendEntityBackfillCompleteAck(response, backfillType, partner.sharedById);
sendEntityBackfillCompleteAck(response, backfillType, partner.createId);
}
} else if (partners.length > 0) {
await this.upsertBackfillCheckpoint({
@@ -350,18 +352,19 @@ export class SyncService extends BaseService {
const endId = upsertCheckpoint.updateId;
for (const album of albums) {
if (isEntityBackfillComplete(album, backfillCheckpoint)) {
const createId = album.createId;
if (isEntityBackfillComplete(createId, backfillCheckpoint)) {
continue;
}
const startId = getStartId(album, backfillCheckpoint);
const startId = getStartId(createId, backfillCheckpoint);
const backfill = this.syncRepository.getAlbumUsersBackfill(album.id, startId, endId);
for await (const { updateId, ...data } of backfill) {
send(response, { type: backfillType, ids: [updateId], data });
send(response, { type: backfillType, ids: [createId, updateId], data });
}
sendEntityBackfillCompleteAck(response, backfillType, album.id);
sendEntityBackfillCompleteAck(response, backfillType, createId);
}
} else if (albums.length > 0) {
await this.upsertBackfillCheckpoint({

View File

@@ -7,12 +7,13 @@ import { getKyselyConfig } from 'src/utils/database';
import { GenericContainer, Wait } from 'testcontainers';
const globalSetup = async () => {
const templateName = 'mich';
const postgresContainer = await new GenericContainer('ghcr.io/immich-app/postgres:14-vectorchord0.4.3')
.withExposedPorts(5432)
.withEnvironment({
POSTGRES_PASSWORD: 'postgres',
POSTGRES_USER: 'postgres',
POSTGRES_DB: 'immich',
POSTGRES_DB: templateName,
})
.withCommand([
'postgres',
@@ -35,7 +36,7 @@ const globalSetup = async () => {
.start();
const postgresPort = postgresContainer.getMappedPort(5432);
const postgresUrl = `postgres://postgres:postgres@localhost:${postgresPort}/immich`;
const postgresUrl = `postgres://postgres:postgres@localhost:${postgresPort}/${templateName}`;
process.env.IMMICH_TEST_POSTGRES_URL = postgresUrl;

View File

@@ -19,7 +19,7 @@ beforeAll(async () => {
defaultDatabase = await getKyselyDB();
});
describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
describe(SyncRequestType.PartnerAssetExifsV1, () => {
it('should detect and sync the first partner asset exif', async () => {
const { auth, sut, getRepository, testSync } = await setup();
@@ -78,7 +78,6 @@ describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
await sut.setAcks(auth, { acks });
const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(ackSyncResponse).toHaveLength(0);
});
@@ -196,6 +195,79 @@ describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
expect(finalAcks).toEqual([]);
});
it('should handle partners with users ids lower than a uuidv7', async () => {
const { auth, sut, getRepository, testSync } = await setup();
const userRepo = getRepository('user');
const user2 = mediumFactory.userInsert({ id: '00d4c0af-7695-4cf2-85e6-415eeaf449cb' });
const user3 = mediumFactory.userInsert({ id: '00e4c0af-7695-4cf2-85e6-415eeaf449cb' });
await userRepo.create(user2);
await userRepo.create(user3);
const assetRepo = getRepository('asset');
const assetUser3 = mediumFactory.assetInsert({ ownerId: user3.id });
await assetRepo.create(assetUser3);
await assetRepo.upsertExif({ assetId: assetUser3.id, make: 'assetUser3' });
await wait(2);
const assetUser2 = mediumFactory.assetInsert({ ownerId: user2.id });
await assetRepo.create(assetUser2);
await assetRepo.upsertExif({ assetId: assetUser2.id, make: 'assetUser2' });
const partnerRepo = getRepository('partner');
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
const response = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(response).toHaveLength(1);
expect(response).toEqual(
expect.arrayContaining([
{
ack: expect.any(String),
data: expect.objectContaining({
assetId: assetUser2.id,
}),
type: SyncEntityType.PartnerAssetExifV1,
},
]),
);
const acks = response.map(({ ack }) => ack);
await sut.setAcks(auth, { acks });
// This checks that our ack upsert is correct
const ackUpsertResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(ackUpsertResponse).toEqual([]);
await partnerRepo.create({ sharedById: user3.id, sharedWithId: auth.user.id });
const syncAckResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(syncAckResponse).toHaveLength(2);
expect(syncAckResponse).toEqual(
expect.arrayContaining([
{
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
data: expect.objectContaining({
assetId: assetUser3.id,
}),
type: SyncEntityType.PartnerAssetExifBackfillV1,
},
{
ack: expect.stringContaining(SyncEntityType.PartnerAssetExifBackfillV1),
data: {},
type: SyncEntityType.SyncAckV1,
},
]),
);
const syncAckResponseAcks = syncAckResponse.map(({ ack }) => ack);
await sut.setAcks(auth, { acks: [syncAckResponseAcks[1]] });
const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
expect(finalResponse).toEqual([]);
});
it('should only backfill partner assets created prior to the current partner asset checkpoint', async () => {
const { auth, sut, getRepository, testSync } = await setup();
@@ -210,13 +282,13 @@ describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
const assetUser2 = mediumFactory.assetInsert({ ownerId: user2.id });
const asset2User3 = mediumFactory.assetInsert({ ownerId: user3.id });
await assetRepo.create(assetUser3);
await assetRepo.upsertExif({ assetId: assetUser3.id, make: 'Canon' });
await assetRepo.upsertExif({ assetId: assetUser3.id, make: 'assetUser3' });
await wait(2);
await assetRepo.create(assetUser2);
await assetRepo.upsertExif({ assetId: assetUser2.id, make: 'Canon' });
await assetRepo.upsertExif({ assetId: assetUser2.id, make: 'assetUser2' });
await wait(2);
await assetRepo.create(asset2User3);
await assetRepo.upsertExif({ assetId: asset2User3.id, make: 'Canon' });
await assetRepo.upsertExif({ assetId: asset2User3.id, make: 'asset2User3' });
const partnerRepo = getRepository('partner');
await partnerRepo.create({ sharedById: user2.id, sharedWithId: auth.user.id });
@@ -246,7 +318,7 @@ describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
expect(backfillResponse).toEqual(
expect.arrayContaining([
{
ack: expect.any(String),
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
data: expect.objectContaining({
assetId: assetUser3.id,
}),
@@ -270,7 +342,6 @@ describe.concurrent(SyncRequestType.PartnerAssetExifsV1, () => {
const backfillAck = backfillResponse[1].ack;
const partnerAssetAck = backfillResponse[2].ack;
await sut.setAcks(auth, { acks: [backfillAck, partnerAssetAck] });
const finalResponse = await testSync(auth, [SyncRequestType.PartnerAssetExifsV1]);
const finalAcks = finalResponse.map(({ ack }) => ack);

View File

@@ -373,18 +373,23 @@ function* newPngFactory() {
const pngFactory = newPngFactory();
const withDatabase = (url: string, name: string) => url.replace('/immich', `/${name}`);
const templateName = 'mich';
const withDatabase = (url: string, name: string) => url.replace(`/${templateName}`, `/${name}`);
export const getKyselyDB = async (suffix?: string): Promise<Kysely<DB>> => {
const testUrl = process.env.IMMICH_TEST_POSTGRES_URL!;
const sql = postgres({
...asPostgresConnectionConfig({ connectionType: 'url', url: withDatabase(testUrl, 'postgres') }),
...asPostgresConnectionConfig({
connectionType: 'url',
url: withDatabase(testUrl, 'postgres'),
}),
max: 1,
});
const randomSuffix = Math.random().toString(36).slice(2, 7);
const dbName = `immich_${suffix ?? randomSuffix}`;
await sql.unsafe(`CREATE DATABASE ${dbName} WITH TEMPLATE immich OWNER postgres;`);
await sql.unsafe(`CREATE DATABASE ${dbName} WITH TEMPLATE ${templateName} OWNER postgres;`);
return new Kysely<DB>(getKyselyConfig({ connectionType: 'url', url: withDatabase(testUrl, dbName) }));
};

14
web/package-lock.json generated
View File

@@ -1,17 +1,17 @@
{
"name": "immich-web",
"version": "1.135.2",
"version": "1.135.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.135.2",
"version": "1.135.3",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.22.7",
"@immich/ui": "^0.22.8",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.11.5",
@@ -87,7 +87,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.135.2",
"version": "1.135.3",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
@@ -1333,9 +1333,9 @@
"link": true
},
"node_modules/@immich/ui": {
"version": "0.22.7",
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.22.7.tgz",
"integrity": "sha512-FdA0RDSOO1IDSTQmCbW9u5yXFl59EHu++tYonDR/FEZUKrMwfmQEanePSW5g5KofdumKEuxBN1fWFym3NbB0jQ==",
"version": "0.22.8",
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.22.8.tgz",
"integrity": "sha512-DVhDgz6drx7vfNhAssX4yYgOC3JpLm8uovLvz3n36skCNU6pm8GoSgH6gMGTM36sx5go3fvhHw5N3KR+A/7bjg==",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@mdi/js": "^7.4.47",

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.135.2",
"version": "1.135.3",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {
@@ -28,7 +28,7 @@
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.22.7",
"@immich/ui": "^0.22.8",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.11.5",

View File

@@ -2,7 +2,6 @@
import { goto } from '$app/navigation';
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
import AlbumsTable from '$lib/components/album-page/albums-table.svelte';
import EditAlbumForm from '$lib/components/forms/edit-album-form.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import RightClickContextMenu from '$lib/components/shared-components/context-menu/right-click-context-menu.svelte';
import {
@@ -11,6 +10,7 @@
} from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants';
import { modalManager } from '$lib/managers/modal-manager.svelte';
import AlbumEditModal from '$lib/modals/AlbumEditModal.svelte';
import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
@@ -258,7 +258,7 @@
const handleEdit = async (album: AlbumResponseDto) => {
closeAlbumContextMenu();
const editedAlbum = await modalManager.show(EditAlbumForm, {
const editedAlbum = await modalManager.show(AlbumEditModal, {
album,
});
if (editedAlbum) {

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import ProfileImageCropper from '$lib/components/shared-components/profile-image-cropper.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte';
import ProfileImageCropperModal from '$lib/modals/ProfileImageCropperModal.svelte';
import type { AssetResponseDto } from '@immich/sdk';
import { mdiAccountCircleOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -11,18 +11,10 @@
}
let { asset }: Props = $props();
let showProfileImageCrop = $state(false);
</script>
<MenuOption
icon={mdiAccountCircleOutline}
onClick={() => (showProfileImageCrop = true)}
onClick={() => modalManager.show(ProfileImageCropperModal, { asset })}
text={$t('set_as_profile_picture')}
/>
{#if showProfileImageCrop}
<Portal target="body">
<ProfileImageCropper {asset} onClose={() => (showProfileImageCrop = false)} />
</Portal>
{/if}

View File

@@ -2,7 +2,7 @@
import AlbumCover from '$lib/components/album-page/album-cover.svelte';
import { handleError } from '$lib/utils/handle-error';
import { updateAlbumInfo, type AlbumResponseDto } from '@immich/sdk';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiRenameOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -68,9 +68,9 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth disabled={isSubmitting} form="edit-album-form">{$t('save')}</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -5,7 +5,7 @@
} from '$lib/components/shared-components/notification/notification';
import ApiKeyGrid from '$lib/components/user-settings-page/user-api-key-grid.svelte';
import { Permission } from '@immich/sdk';
import { Button, Checkbox, Label, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, Checkbox, HStack, Label, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiKeyVariant } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@@ -219,9 +219,9 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{cancelText}</Button>
<Button shape="round" type="submit" fullWidth form="api-key-form">{submitText}</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { copyToClipboard } from '$lib/utils';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiKeyVariant } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -27,9 +27,9 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<HStack fullWidth>
<Button shape="round" onclick={() => copyToClipboard(secret)} fullWidth>{$t('copy_to_clipboard')}</Button>
<Button shape="round" onclick={onClose} fullWidth>{$t('done')}</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -2,7 +2,7 @@
import Icon from '$lib/components/elements/icon.svelte';
import { tagAssets } from '$lib/utils/asset-utils';
import { getAllTags, upsertTags, type TagResponseDto } from '@immich/sdk';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiClose, mdiTag } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@@ -99,9 +99,9 @@
</ModalBody>
<ModalFooter>
<div class="flex w-full gap-2">
<HStack fullWidth>
<Button shape="round" fullWidth color="secondary" onclick={() => onClose()}>{$t('cancel')}</Button>
<Button type="submit" shape="round" fullWidth form="create-tag-form" {disabled}>{$t('tag_assets')}</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiCancel } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -32,13 +32,13 @@
</div>
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose(false)}>
{$t('cancel')}
</Button>
<Button shape="round" color="danger" fullWidth onclick={() => onClose(true)}>
{$t('confirm')}
</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Button, Modal, ModalBody, ModalFooter, type Color } from '@immich/ui';
import { Button, HStack, Modal, ModalBody, ModalFooter, type Color } from '@immich/ui';
import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n';
@@ -38,13 +38,13 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose(false)}>
{$t('cancel')}
</Button>
<Button shape="round" color={confirmColor} fullWidth onclick={handleConfirm} {disabled}>
{confirmText}
</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiFolderRemove } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@@ -63,7 +63,7 @@
</form>
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
{#if isEditing}
<Button shape="round" color="danger" fullWidth onclick={() => onClose({ action: 'delete' })}
@@ -73,6 +73,6 @@
<Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="add-exclusion-pattern-form">
{submitText}
</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiFolderSync } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@@ -60,7 +60,7 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{cancelText}</Button>
{#if isEditing}
<Button shape="round" color="danger" fullWidth onclick={() => onClose({ action: 'delete' })}>
@@ -70,6 +70,6 @@
<Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="library-import-path-form">
{submitText}
</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { LibraryResponseDto } from '@immich/sdk';
import { Button, Field, Input, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiRenameOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -29,9 +29,9 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<HStack fullWidth>
<Button shape="round" fullWidth color="secondary" onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" fullWidth type="submit" form="rename-library-form">{$t('save')}</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -2,7 +2,7 @@
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { user } from '$lib/stores/user.store';
import { searchUsersAdmin } from '@immich/sdk';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiFolderSync } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@@ -38,9 +38,9 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth form="select-library-owner-form">{$t('create')}</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import type { MapSettings } from '$lib/stores/preferences.store';
import { Button, Field, Modal, ModalBody, ModalFooter, Stack, Switch } from '@immich/ui';
import { Button, Field, HStack, Modal, ModalBody, ModalFooter, Stack, Switch } from '@immich/ui';
import { Duration } from 'luxon';
import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition';
@@ -127,9 +127,9 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<HStack fullWidth>
<Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button type="submit" shape="round" fullWidth form="map-settings-form">{$t('save')}</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { copyToClipboard } from '$lib/utils';
import { Button, Code, IconButton, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
import { Button, Code, HStack, IconButton, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
import { mdiCheck, mdiContentCopy } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -35,10 +35,10 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<HStack fullWidth>
<Button shape="round" color="primary" fullWidth onclick={() => onClose()}>
{$t('done')}
</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -5,7 +5,7 @@
} from '$lib/components/shared-components/notification/notification';
import { handleError } from '$lib/utils/handle-error';
import { updatePerson, type PersonResponseDto } from '@immich/sdk';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiCake } from '@mdi/js';
import { t } from 'svelte-i18n';
import DateInput from '../components/elements/date-input.svelte';
@@ -65,13 +65,13 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>
{$t('cancel')}
</Button>
<Button type="submit" shape="round" color="primary" fullWidth form="set-birth-date-form">
{$t('save')}
</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -7,7 +7,7 @@
import { getPeopleThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { mergePerson, type PersonResponseDto } from '@immich/sdk';
import { Button, IconButton, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, HStack, IconButton, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiArrowLeft, mdiMerge } from '@mdi/js';
import { onMount, tick } from 'svelte';
import { t } from 'svelte-i18n';
@@ -139,11 +139,11 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<HStack fullWidth>
<Button fullWidth shape="round" color="secondary" onclick={() => onClose()}>{$t('no')}</Button>
<Button id="merge-confirm-button" fullWidth shape="round" onclick={handleMergePerson}>
{$t('yes')}
</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -6,8 +6,8 @@
import domtoimage from 'dom-to-image';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import PhotoViewer from '../asset-viewer/photo-viewer.svelte';
import { NotificationType, notificationController } from './notification/notification';
import PhotoViewer from '../components/asset-viewer/photo-viewer.svelte';
import { NotificationType, notificationController } from '../components/shared-components/notification/notification';
interface Props {
asset: AssetResponseDto;

View File

@@ -34,7 +34,7 @@
import { parseUtcDate } from '$lib/utils/date-time';
import { generateId } from '$lib/utils/generate-id';
import { AssetTypeEnum, AssetVisibility, type MetadataSearchDto, type SmartSearchDto } from '@immich/sdk';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiTune } from '@mdi/js';
import { t } from 'svelte-i18n';
import { SvelteSet } from 'svelte/reactivity';
@@ -204,11 +204,11 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<HStack fullWidth>
<Button shape="round" size="large" type="reset" color="secondary" fullWidth form={formId}
>{$t('clear_all')}</Button
>
<Button shape="round" size="large" type="submit" fullWidth form={formId}>{$t('search')}</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -2,7 +2,7 @@
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { SettingInputFieldType } from '$lib/constants';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import {
mdiArrowDownThin,
mdiArrowUpThin,
@@ -105,9 +105,9 @@
</div>
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<HStack fullWidth>
<Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button fullWidth color="primary" shape="round" onclick={applyChanges}>{$t('confirm')}</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -9,6 +9,7 @@
Button,
Field,
HelperText,
HStack,
Input,
Modal,
ModalBody,
@@ -131,10 +132,11 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<HStack fullWidth>
<Button color="secondary" fullWidth onclick={() => onClose()} shape="round">{$t('cancel')}</Button>
<Button type="submit" disabled={!valid} fullWidth shape="round" form="create-new-user-form">{$t('create')}</Button
>
</div>
<Button type="submit" disabled={!valid} fullWidth shape="round" form="create-new-user-form"
>{$t('create')}
</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -5,7 +5,7 @@
import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units';
import { handleError } from '$lib/utils/handle-error';
import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
import { Button, Field, Modal, ModalBody, ModalFooter, Switch } from '@immich/ui';
import { Button, Field, HStack, Modal, ModalBody, ModalFooter, Switch } from '@immich/ui';
import { mdiAccountEditOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -115,11 +115,11 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth form="edit-user-form" onclick={() => onClose()}
>{$t('cancel')}</Button
>
<Button type="submit" shape="round" fullWidth form="edit-user-form">{$t('confirm')}</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -2,7 +2,7 @@
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { handleError } from '$lib/utils/handle-error';
import { restoreUserAdmin, type UserAdminResponseDto, type UserResponseDto } from '@immich/sdk';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiDeleteRestore } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -35,13 +35,13 @@
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>
{$t('cancel')}
</Button>
<Button shape="round" color="primary" fullWidth onclick={() => handleRestoreUser()}>
{$t('restore')}
</Button>
</div>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -191,10 +191,10 @@
</ModalBody>
<ModalFooter>
<div class="flex w-full gap-2">
<HStack fullWidth>
<Button color="secondary" fullWidth shape="round" onclick={() => handleCancel()}>{$t('cancel')}</Button>
<Button type="submit" fullWidth shape="round" form="create-tag-form">{$t('create')}</Button>
</div>
</HStack>
</ModalFooter>
</Modal>
{/if}
@@ -214,10 +214,10 @@
</ModalBody>
<ModalFooter>
<div class="flex w-full gap-2">
<HStack fullWidth>
<Button color="secondary" fullWidth shape="round" onclick={() => handleCancel()}>{$t('cancel')}</Button>
<Button type="submit" fullWidth shape="round" form="edit-tag-form">{$t('save')}</Button>
</div>
</HStack>
</ModalFooter>
</Modal>
{/if}