Compare commits

..

1 Commits

Author SHA1 Message Date
mertalev
e62071fda0 don't use iterator.find 2025-06-19 17:40:56 +03:00
101 changed files with 593 additions and 366 deletions

View File

@@ -644,7 +644,7 @@ jobs:
contents: read
services:
postgres:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres

6
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.71",
"version": "2.2.69",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.71",
"version": "2.2.69",
"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.0",
"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.69",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -122,7 +122,7 @@ services:
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.2-pgvectors0.2.0
env_file:
- .env
environment:

View File

@@ -63,7 +63,7 @@ services:
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.2-pgvectors0.2.0
env_file:
- .env
environment:

View File

@@ -56,7 +56,7 @@ services:
database:
container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.2-pgvectors0.2.0
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}

View File

@@ -1,12 +1,4 @@
[
{
"label": "v1.135.2",
"url": "https://v1.135.2.archive.immich.app"
},
{
"label": "v1.135.1",
"url": "https://v1.135.1.archive.immich.app"
},
{
"label": "v1.135.0",
"url": "https://v1.135.0.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.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.135.2",
"version": "1.135.0",
"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.69",
"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.0",
"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.0",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -14,11 +14,7 @@ describe('/people', () => {
let nameAlicePerson: PersonResponseDto;
let nameBobPerson: PersonResponseDto;
let nameCharliePerson: PersonResponseDto;
let nameNullPerson4Assets: PersonResponseDto;
let nameNullPerson3Assets: PersonResponseDto;
let nameNullPerson1Asset: PersonResponseDto;
let nameBillPersonFavourite: PersonResponseDto;
let nameFreddyPersonFavourite: PersonResponseDto;
let nameNullPerson: PersonResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
@@ -31,11 +27,7 @@ describe('/people', () => {
nameCharliePerson,
nameBobPerson,
nameAlicePerson,
nameNullPerson4Assets,
nameNullPerson3Assets,
nameNullPerson1Asset,
nameBillPersonFavourite,
nameFreddyPersonFavourite,
nameNullPerson,
] = await Promise.all([
utils.createPerson(admin.accessToken, {
name: 'visible_person',
@@ -60,26 +52,11 @@ describe('/people', () => {
utils.createPerson(admin.accessToken, {
name: '',
}),
utils.createPerson(admin.accessToken, {
name: '',
}),
utils.createPerson(admin.accessToken, {
name: '',
}),
utils.createPerson(admin.accessToken, {
name: 'Bill',
isFavorite: true,
}),
utils.createPerson(admin.accessToken, {
name: 'Freddy',
isFavorite: true,
}),
]);
const asset1 = await utils.createAsset(admin.accessToken);
const asset2 = await utils.createAsset(admin.accessToken);
const asset3 = await utils.createAsset(admin.accessToken);
const asset4 = await utils.createAsset(admin.accessToken);
await Promise.all([
utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }),
@@ -87,27 +64,15 @@ describe('/people', () => {
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset3.id, personId: multipleAssetsPerson.id }), // 4 assets
utils.createFace({ assetId: asset3.id, personId: multipleAssetsPerson.id }),
// Named persons
utils.createFace({ assetId: asset1.id, personId: nameCharliePerson.id }), // 1 asset
utils.createFace({ assetId: asset1.id, personId: nameBobPerson.id }),
utils.createFace({ assetId: asset2.id, personId: nameBobPerson.id }), // 2 assets
utils.createFace({ assetId: asset1.id, personId: nameAlicePerson.id }), // 1 asset
// Null-named person 4 assets
utils.createFace({ assetId: asset1.id, personId: nameNullPerson4Assets.id }),
utils.createFace({ assetId: asset2.id, personId: nameNullPerson4Assets.id }),
utils.createFace({ assetId: asset3.id, personId: nameNullPerson4Assets.id }),
utils.createFace({ assetId: asset4.id, personId: nameNullPerson4Assets.id }), // 4 assets
// Null-named person 3 assets
utils.createFace({ assetId: asset1.id, personId: nameNullPerson3Assets.id }),
utils.createFace({ assetId: asset2.id, personId: nameNullPerson3Assets.id }),
utils.createFace({ assetId: asset3.id, personId: nameNullPerson3Assets.id }), // 3 assets
// Null-named person 1 asset
utils.createFace({ assetId: asset3.id, personId: nameNullPerson1Asset.id }),
// Favourite People
utils.createFace({ assetId: asset1.id, personId: nameFreddyPersonFavourite.id }),
utils.createFace({ assetId: asset2.id, personId: nameFreddyPersonFavourite.id }),
utils.createFace({ assetId: asset1.id, personId: nameBillPersonFavourite.id }),
// Null-named person
utils.createFace({ assetId: asset1.id, personId: nameNullPerson.id }),
utils.createFace({ assetId: asset2.id, personId: nameNullPerson.id }), // 2 assets
]);
});
@@ -122,19 +87,15 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: false,
total: 11,
total: 7,
hidden: 1,
people: [
expect.objectContaining({ name: 'Freddy' }),
expect.objectContaining({ name: 'Bill' }),
expect.objectContaining({ name: 'multiple_assets_person' }),
expect.objectContaining({ name: 'Bob' }),
expect.objectContaining({ name: 'Alice' }),
expect.objectContaining({ name: 'Charlie' }),
expect.objectContaining({ name: 'visible_person' }),
expect.objectContaining({ id: nameNullPerson4Assets.id, name: '' }),
expect.objectContaining({ id: nameNullPerson3Assets.id, name: '' }),
expect.objectContaining({ name: 'hidden_person' }), // Should really be before the null names
expect.objectContaining({ name: 'hidden_person' }),
],
});
});
@@ -144,21 +105,17 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body.hasNextPage).toBe(false);
expect(body.total).toBe(11); // All persons
expect(body.total).toBe(7); // All persons
expect(body.hidden).toBe(1); // 'hidden_person'
const people = body.people as PersonResponseDto[];
expect(people.map((p) => p.id)).toEqual([
nameFreddyPersonFavourite.id, // name: 'Freddy', count: 2
nameBillPersonFavourite.id, // name: 'Bill', count: 1
multipleAssetsPerson.id, // name: 'multiple_assets_person', count: 3
nameBobPerson.id, // name: 'Bob', count: 2
nameAlicePerson.id, // name: 'Alice', count: 1
nameCharliePerson.id, // name: 'Charlie', count: 1
visiblePerson.id, // name: 'visible_person', count: 1
nameNullPerson4Assets.id, // name: '', count: 4
nameNullPerson3Assets.id, // name: '', count: 3
]);
expect(people.some((p) => p.id === hiddenPerson.id)).toBe(false);
@@ -170,18 +127,14 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: false,
total: 11,
total: 7,
hidden: 1,
people: [
expect.objectContaining({ name: 'Freddy' }),
expect.objectContaining({ name: 'Bill' }),
expect.objectContaining({ name: 'multiple_assets_person' }),
expect.objectContaining({ name: 'Bob' }),
expect.objectContaining({ name: 'Alice' }),
expect.objectContaining({ name: 'Charlie' }),
expect.objectContaining({ name: 'visible_person' }),
expect.objectContaining({ id: nameNullPerson4Assets.id, name: '' }),
expect.objectContaining({ id: nameNullPerson3Assets.id, name: '' }),
],
});
});
@@ -195,9 +148,9 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: true,
total: 11,
total: 7,
hidden: 1,
people: [expect.objectContaining({ name: 'Alice' })],
people: [expect.objectContaining({ name: 'visible_person' })],
});
});
});

View File

@@ -244,6 +244,7 @@ async def load(model: InferenceModel) -> InferenceModel:
async def idle_shutdown_task() -> None:
while True:
log.debug("Checking for inactivity...")
if (
last_called is not None
and not active_requests

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" => 201,
"android.injected.version.name" => "1.135.0",
}
)
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

@@ -649,7 +649,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -793,7 +793,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -823,7 +823,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
@@ -857,7 +857,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -865,7 +865,7 @@
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -900,7 +900,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -908,7 +908,7 @@
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -940,7 +940,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -948,7 +948,7 @@
INFOPLIST_FILE = WidgetExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -979,7 +979,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1023,7 +1023,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1064,7 +1064,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 210;
CURRENT_PROJECT_VERSION = 209;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;

View File

@@ -78,7 +78,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.135.1</string>
<string>1.135.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -93,7 +93,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>210</string>
<string>209</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>
@@ -115,8 +115,8 @@
</dict>
<key>NSBonjourServices</key>
<array>
<string>_googlecast._tcp</string>
<string>_CC1AD845._googlecast._tcp</string>
<string>_googlecast._tcp</string>
<string>_CC1AD845._googlecast._tcp</string>
</array>
<key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string>
@@ -168,8 +168,5 @@
<true />
<key>NSFaceIDUsageDescription</key>
<string>We need to use FaceID to allow access to your locked folder</string>
<key>NSLocalNetworkUsageDescription</key>
<string>We need local network permission to connect to the local server using IP address and
allow the casting feature to work</string>
</dict>
</plist>
</plist>

View File

@@ -43,7 +43,7 @@ struct RandomConfigurationAppIntent: WidgetConfigurationIntent {
"Choose an album to show images from"
}
@Parameter(title: "Album")
@Parameter(title: "Album", default: NO_ALBUM)
var album: Album?
@Parameter(title: "Show Album Name", default: false)

View File

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

View File

@@ -10,5 +10,3 @@ enum TextSearchType {
}
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
enum SortUserBy { id }

View File

@@ -0,0 +1,18 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:openapi/api.dart';
abstract interface class ISyncStreamRepository implements IDatabaseRepository {
Future<void> updateUsersV1(Iterable<SyncUserV1> data);
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data);
Future<void> updatePartnerV1(Iterable<SyncPartnerV1> data);
Future<void> deletePartnerV1(Iterable<SyncPartnerDeleteV1> data);
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data);
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data);
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data);
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data);
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data);
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data);
}

View File

@@ -0,0 +1,22 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IUserRepository implements IDatabaseRepository {
Future<bool> insert(UserDto user);
Future<UserDto?> getByUserId(String id);
Future<List<UserDto?>> getByUserIds(List<String> ids);
Future<List<UserDto>> getAll({SortUserBy? sortBy});
Future<bool> updateAll(List<UserDto> users);
Future<UserDto> update(UserDto user);
Future<void> delete(List<String> ids);
Future<void> deleteAll();
}
enum SortUserBy { id }

View File

@@ -0,0 +1,15 @@
import 'dart:typed_data';
import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IUserApiRepository {
Future<UserDto?> getMyUser();
Future<List<UserDto>> getAll();
/// Saves the [data] in the server and uses it as the current users profile image
Future<String> createProfileImage({
required String name,
required Uint8List data,
});
}

View File

@@ -1,8 +1,8 @@
import 'dart:async';
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -10,12 +10,12 @@ class SyncStreamService {
final Logger _logger = Logger('SyncStreamService');
final ISyncApiRepository _syncApiRepository;
final SyncStreamRepository _syncStreamRepository;
final ISyncStreamRepository _syncStreamRepository;
final bool Function()? _cancelChecker;
SyncStreamService({
required ISyncApiRepository syncApiRepository,
required SyncStreamRepository syncStreamRepository,
required ISyncStreamRepository syncStreamRepository,
bool Function()? cancelChecker,
}) : _syncApiRepository = syncApiRepository,
_syncStreamRepository = syncStreamRepository,

View File

@@ -1,24 +1,24 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:logging/logging.dart';
class UserService {
final Logger _log = Logger("UserService");
final IsarUserRepository _isarUserRepository;
final UserApiRepository _userApiRepository;
final IUserRepository _userRepository;
final IUserApiRepository _userApiRepository;
final StoreService _storeService;
UserService({
required IsarUserRepository isarUserRepository,
required UserApiRepository userApiRepository,
required IUserRepository userRepository,
required IUserApiRepository userApiRepository,
required StoreService storeService,
}) : _isarUserRepository = isarUserRepository,
}) : _userRepository = userRepository,
_userApiRepository = userApiRepository,
_storeService = storeService;
@@ -38,7 +38,7 @@ class UserService {
final user = await _userApiRepository.getMyUser();
if (user == null) return null;
await _storeService.put(StoreKey.currentUser, user);
await _isarUserRepository.update(user);
await _userRepository.update(user);
return user;
}
@@ -50,7 +50,7 @@ class UserService {
);
final updatedUser = getMyUser().copyWith(profileImagePath: path);
await _storeService.put(StoreKey.currentUser, updatedUser);
await _isarUserRepository.update(updatedUser);
await _userRepository.update(updatedUser);
return path;
} catch (e) {
_log.warning("Failed to upload profile image", e);
@@ -59,10 +59,10 @@ class UserService {
}
Future<List<UserDto>> getAll() async {
return await _isarUserRepository.getAll();
return await _userRepository.getAll();
}
Future<void> deleteAll() {
return _isarUserRepository.deleteAll();
return _userRepository.deleteAll();
}
}

View File

@@ -1,4 +1,5 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
@@ -9,12 +10,14 @@ import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility;
import 'package:openapi/api.dart' hide AssetVisibility;
class SyncStreamRepository extends DriftDatabaseRepository {
class DriftSyncStreamRepository extends DriftDatabaseRepository
implements ISyncStreamRepository {
final Logger _logger = Logger('DriftSyncStreamRepository');
final Drift _db;
SyncStreamRepository(super.db) : _db = db;
DriftSyncStreamRepository(super.db) : _db = db;
@override
Future<void> deleteUsersV1(Iterable<SyncUserDeleteV1> data) async {
try {
await _db.batch((batch) {
@@ -31,6 +34,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updateUsersV1(Iterable<SyncUserV1> data) async {
try {
await _db.batch((batch) {
@@ -53,6 +57,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> deletePartnerV1(Iterable<SyncPartnerDeleteV1> data) async {
try {
await _db.batch((batch) {
@@ -72,6 +77,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updatePartnerV1(Iterable<SyncPartnerV1> data) async {
try {
await _db.batch((batch) {
@@ -95,6 +101,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
try {
await _deleteAssetsV1(data);
@@ -104,6 +111,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data) async {
try {
await _updateAssetsV1(data);
@@ -113,6 +121,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
try {
await _deleteAssetsV1(data);
@@ -122,6 +131,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data) async {
try {
await _updateAssetsV1(data);
@@ -131,6 +141,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
try {
await _updateAssetExifV1(data);
@@ -140,6 +151,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
}
}
@override
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
try {
await _updateAssetExifV1(data);

View File

@@ -1,26 +1,30 @@
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.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/infrastructure/repositories/db.repository.dart';
import 'package:isar/isar.dart';
class IsarUserRepository extends IsarDatabaseRepository {
class IsarUserRepository extends IsarDatabaseRepository
implements IUserRepository {
final Isar _db;
const IsarUserRepository(super.db) : _db = db;
@override
Future<void> delete(List<String> ids) async {
await transaction(() async {
await _db.users.deleteAllById(ids);
});
}
@override
Future<void> deleteAll() async {
await transaction(() async {
await _db.users.clear();
});
}
@override
Future<List<UserDto>> getAll({SortUserBy? sortBy}) async {
return (await _db.users
.where()
@@ -35,14 +39,17 @@ class IsarUserRepository extends IsarDatabaseRepository {
.toList();
}
@override
Future<UserDto?> getByUserId(String id) async {
return (await _db.users.getById(id))?.toDto();
}
@override
Future<List<UserDto?>> getByUserIds(List<String> ids) async {
return (await _db.users.getAllById(ids)).map((u) => u?.toDto()).toList();
}
@override
Future<bool> insert(UserDto user) async {
await transaction(() async {
await _db.users.put(entity.User.fromDto(user));
@@ -50,6 +57,7 @@ class IsarUserRepository extends IsarDatabaseRepository {
return true;
}
@override
Future<UserDto> update(UserDto user) async {
await transaction(() async {
await _db.users.put(entity.User.fromDto(user));
@@ -57,6 +65,7 @@ class IsarUserRepository extends IsarDatabaseRepository {
return user;
}
@override
Future<bool> updateAll(List<UserDto> users) async {
await transaction(() async {
await _db.users.putAll(users.map(entity.User.fromDto).toList());

View File

@@ -1,15 +1,17 @@
import 'dart:typed_data';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/api.repository.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:openapi/api.dart';
class UserApiRepository extends ApiRepository {
class UserApiRepository extends ApiRepository implements IUserApiRepository {
final UsersApi _api;
const UserApiRepository(this._api);
@override
Future<UserDto?> getMyUser() async {
final (adminDto, preferenceDto) =
await (_api.getMyUser(), _api.getMyPreferences()).wait;
@@ -18,6 +20,7 @@ class UserApiRepository extends ApiRepository {
return UserConverter.fromAdminDto(adminDto, preferenceDto);
}
@override
Future<String> createProfileImage({
required String name,
required Uint8List data,
@@ -30,6 +33,7 @@ class UserApiRepository extends ApiRepository {
return res.profileImagePath;
}
@override
Future<List<UserDto>> getAll() async {
final dto = await checkNull(_api.searchUsers());
return dto.map(UserConverter.fromSimpleUserDto).toList();

View File

@@ -0,0 +1,16 @@
import 'package:immich_mobile/models/activities/activity.model.dart';
abstract interface class IActivityApiRepository {
Future<List<Activity>> getAll(
String albumId, {
String? assetId,
});
Future<Activity> create(
String albumId,
ActivityType type, {
String? assetId,
String? comment,
});
Future<void> delete(String id);
Future<ActivityStats> getStats(String albumId, {String? assetId});
}

View File

@@ -0,0 +1,42 @@
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/album.entity.dart';
abstract interface class IAlbumApiRepository {
Future<Album> get(String id);
Future<List<Album>> getAll({bool? shared});
Future<Album> create(
String name, {
required Iterable<String> assetIds,
Iterable<String> sharedUserIds = const [],
});
Future<Album> update(
String albumId, {
String? name,
String? thumbnailAssetId,
String? description,
bool? activityEnabled,
SortOrder? sortOrder,
});
Future<void> delete(String albumId);
Future<({List<String> added, List<String> duplicates})> addAssets(
String albumId,
Iterable<String> assetIds,
);
Future<({List<String> removed, List<String> failed})> removeAssets(
String albumId,
Iterable<String> assetIds,
);
Future<Album> addUsers(
String albumId,
Iterable<String> userIds,
);
Future<void> removeUser(String albumId, {required String userId});
}

View File

@@ -0,0 +1,21 @@
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
abstract interface class IAlbumMediaRepository {
Future<List<Album>> getAll();
Future<List<String>> getAssetIds(String albumId);
Future<int> getAssetCount(String albumId);
Future<List<Asset>> getAssets(
String albumId, {
int start = 0,
int end = 0x7fffffffffffffff,
DateTime? modifiedFrom,
DateTime? modifiedUntil,
bool orderByModificationDate = false,
});
Future<Album> get(String id);
}

View File

@@ -0,0 +1,6 @@
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
abstract interface class IBiometricRepository {
Future<BiometricStatus> getStatus();
Future<bool> authenticate(String? message);
}

View File

@@ -0,0 +1,15 @@
import 'package:background_downloader/background_downloader.dart';
abstract interface class IDownloadRepository {
void Function(TaskStatusUpdate)? onImageDownloadStatus;
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
void Function(TaskProgressUpdate)? onTaskProgress;
Future<List<TaskRecord>> getLiveVideoTasks();
Future<List<bool>> downloadAll(List<DownloadTask> tasks);
Future<bool> cancel(String id);
Future<void> deleteAllTrackingRecords();
Future<void> deleteRecordsWithIds(List<String> id);
}

View File

@@ -0,0 +1,4 @@
abstract interface class INetworkRepository {
Future<String?> getWifiName();
Future<String?> getWifiIp();
}

View File

@@ -0,0 +1,5 @@
abstract interface class ISecureStorageRepository {
Future<String?> read(String key);
Future<void> write(String key, String value);
Future<void> delete(String key);
}

View File

@@ -0,0 +1,9 @@
import 'package:immich_mobile/models/sessions/session_create_response.model.dart';
abstract interface class ISessionAPIRepository {
Future<SessionCreateResponse> createSession(
String deviceName,
String deviceOS, {
int? duration,
});
}

View File

@@ -0,0 +1,7 @@
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
abstract interface class IShareHandlerRepository {
void Function(List<ShareIntentAttachment>)? onSharedMedia;
Future<void> init();
}

View File

@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/models/auth/auth_state.model.dart';
@@ -107,7 +108,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
final AuthState _authState;
final BackgroundService _backgroundService;
final GalleryPermissionNotifier _galleryPermissionNotifier;
final AlbumMediaRepository _albumMediaRepository;
final IAlbumMediaRepository _albumMediaRepository;
final IFileMediaRepository _fileMediaRepository;
final BackupAlbumService _backupAlbumService;
final Ref ref;

View File

@@ -26,7 +26,7 @@ final syncApiRepositoryProvider = Provider(
);
final syncStreamRepositoryProvider = Provider(
(ref) => SyncStreamRepository(ref.watch(driftProvider)),
(ref) => DriftSyncStreamRepository(ref.watch(driftProvider)),
);
final localSyncServiceProvider = Provider(

View File

@@ -1,4 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
@@ -10,16 +12,16 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user.provider.g.dart';
@Riverpod(keepAlive: true)
IsarUserRepository userRepository(Ref ref) =>
IUserRepository userRepository(Ref ref) =>
IsarUserRepository(ref.watch(isarProvider));
@Riverpod(keepAlive: true)
UserApiRepository userApiRepository(Ref ref) =>
IUserApiRepository userApiRepository(Ref ref) =>
UserApiRepository(ref.watch(apiServiceProvider).usersApi);
@Riverpod(keepAlive: true)
UserService userService(Ref ref) => UserService(
isarUserRepository: ref.watch(userRepositoryProvider),
userRepository: ref.watch(userRepositoryProvider),
userApiRepository: ref.watch(userApiRepositoryProvider),
storeService: ref.watch(storeServiceProvider),
);

View File

@@ -6,11 +6,11 @@ part of 'user.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$userRepositoryHash() => r'538791a4ad126ed086c9db682c67fc5c654d54f3';
String _$userRepositoryHash() => r'1a2ac726bcc44397dcaecf449084fefd336696d4';
/// See also [userRepository].
@ProviderFor(userRepository)
final userRepositoryProvider = Provider<IsarUserRepository>.internal(
final userRepositoryProvider = Provider<IUserRepository>.internal(
userRepository,
name: r'userRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -22,12 +22,12 @@ final userRepositoryProvider = Provider<IsarUserRepository>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef UserRepositoryRef = ProviderRef<IsarUserRepository>;
String _$userApiRepositoryHash() => r'8a7340ca4544c8c6b20225c65bff2abb9e96baa2';
typedef UserRepositoryRef = ProviderRef<IUserRepository>;
String _$userApiRepositoryHash() => r'6b19f2c99fb83162a5ceb91adb8589eaae01bc92';
/// See also [userApiRepository].
@ProviderFor(userApiRepository)
final userApiRepositoryProvider = Provider<UserApiRepository>.internal(
final userApiRepositoryProvider = Provider<IUserApiRepository>.internal(
userApiRepository,
name: r'userApiRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -39,8 +39,8 @@ final userApiRepositoryProvider = Provider<UserApiRepository>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef UserApiRepositoryRef = ProviderRef<UserApiRepository>;
String _$userServiceHash() => r'181414dddc7891be6237e13d568c287a804228d1';
typedef UserApiRepositoryRef = ProviderRef<IUserApiRepository>;
String _$userServiceHash() => r'4a0873357b7115b4d6bfa8e89b847c0b74ce0d93';
/// See also [userService].
@ProviderFor(userService)

View File

@@ -1,5 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/interfaces/activity_api.interface.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
@@ -9,17 +10,20 @@ final activityApiRepositoryProvider = Provider(
(ref) => ActivityApiRepository(ref.watch(apiServiceProvider).activitiesApi),
);
class ActivityApiRepository extends ApiRepository {
class ActivityApiRepository extends ApiRepository
implements IActivityApiRepository {
final ActivitiesApi _api;
ActivityApiRepository(this._api);
@override
Future<List<Activity>> getAll(String albumId, {String? assetId}) async {
final response =
await checkNull(_api.getActivities(albumId, assetId: assetId));
return response.map(_toActivity).toList();
}
@override
Future<Activity> create(
String albumId,
ActivityType type, {
@@ -38,10 +42,12 @@ class ActivityApiRepository extends ApiRepository {
return _toActivity(response);
}
@override
Future<void> delete(String id) {
return checkNull(_api.deleteActivity(id));
}
@override
Future<ActivityStats> getStats(String albumId, {String? assetId}) async {
final response =
await checkNull(_api.getActivityStatistics(albumId, assetId: assetId));

View File

@@ -5,6 +5,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart';
@@ -13,21 +14,24 @@ final albumApiRepositoryProvider = Provider(
(ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
);
class AlbumApiRepository extends ApiRepository {
class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
final AlbumsApi _api;
AlbumApiRepository(this._api);
@override
Future<Album> get(String id) async {
final dto = await checkNull(_api.getAlbumInfo(id));
return _toAlbum(dto);
}
@override
Future<List<Album>> getAll({bool? shared}) async {
final dtos = await checkNull(_api.getAllAlbums(shared: shared));
return dtos.map(_toAlbum).toList();
}
@override
Future<Album> create(
String name, {
required Iterable<String> assetIds,
@@ -50,6 +54,7 @@ class AlbumApiRepository extends ApiRepository {
return _toAlbum(responseDto);
}
@override
Future<Album> update(
String albumId, {
String? name,
@@ -79,10 +84,12 @@ class AlbumApiRepository extends ApiRepository {
return _toAlbum(response);
}
@override
Future<void> delete(String albumId) {
return _api.deleteAlbum(albumId);
}
@override
Future<({List<String> added, List<String> duplicates})> addAssets(
String albumId,
Iterable<String> assetIds,
@@ -107,6 +114,7 @@ class AlbumApiRepository extends ApiRepository {
return (added: added, duplicates: duplicates);
}
@override
Future<({List<String> removed, List<String> failed})> removeAssets(
String albumId,
Iterable<String> assetIds,
@@ -128,6 +136,7 @@ class AlbumApiRepository extends ApiRepository {
return (removed: removed, failed: failed);
}
@override
Future<Album> addUsers(String albumId, Iterable<String> userIds) async {
final albumUsers =
userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList();
@@ -140,6 +149,7 @@ class AlbumApiRepository extends ApiRepository {
return _toAlbum(response);
}
@override
Future<void> removeUser(String albumId, {required String userId}) {
return _api.removeUserFromAlbum(albumId, userId);
}

View File

@@ -4,13 +4,14 @@ import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:photo_manager/photo_manager.dart' hide AssetType;
final albumMediaRepositoryProvider =
Provider((ref) => const AlbumMediaRepository());
class AlbumMediaRepository {
class AlbumMediaRepository implements IAlbumMediaRepository {
const AlbumMediaRepository();
bool get useCustomFilter =>
@@ -40,6 +41,7 @@ class AlbumMediaRepository {
)
: null;
@override
Future<List<Album>> getAll() async {
final filter = useCustomFilter
? CustomFilter.sql(where: '${CustomColumns.base.width} > 0')
@@ -50,6 +52,7 @@ class AlbumMediaRepository {
return assetPathEntities.map(_toAlbum).toList();
}
@override
Future<List<String>> getAssetIds(String albumId) async {
final album =
await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter());
@@ -58,12 +61,14 @@ class AlbumMediaRepository {
return assets.map((e) => e.id).toList();
}
@override
Future<int> getAssetCount(String albumId) async {
final album =
await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter());
return album.assetCountAsync;
}
@override
Future<List<Asset>> getAssets(
String albumId, {
int start = 0,
@@ -92,7 +97,12 @@ class AlbumMediaRepository {
return assets.map(AssetMediaRepository.toAsset).toList().cast();
}
Future<Album> get(String id) async {
@override
Future<Album> get(
String id, {
DateTime? modifiedFrom,
DateTime? modifiedUntil,
}) async {
final assetPathEntity = await AssetPathEntity.fromId(
id,
filterOption: _getAlbumFilter(containsPathModified: true),

View File

@@ -1,16 +1,18 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/biometric.interface.dart';
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
import 'package:local_auth/local_auth.dart';
final biometricRepositoryProvider =
Provider((ref) => BiometricRepository(LocalAuthentication()));
class BiometricRepository {
class BiometricRepository implements IBiometricRepository {
final LocalAuthentication _localAuth;
BiometricRepository(this._localAuth);
@override
Future<BiometricStatus> getStatus() async {
final bool canAuthenticateWithBiometrics =
await _localAuth.canCheckBiometrics;
@@ -24,6 +26,7 @@ class BiometricRepository {
);
}
@override
Future<bool> authenticate(String? message) async {
return _localAuth.authenticate(
localizedReason: message ?? 'please_auth_to_access'.tr(),

View File

@@ -1,16 +1,21 @@
import 'package:background_downloader/background_downloader.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/download.interface.dart';
import 'package:immich_mobile/utils/download.dart';
final downloadRepositoryProvider = Provider((ref) => DownloadRepository());
class DownloadRepository {
class DownloadRepository implements IDownloadRepository {
@override
void Function(TaskStatusUpdate)? onImageDownloadStatus;
@override
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
@override
void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
@override
void Function(TaskProgressUpdate)? onTaskProgress;
DownloadRepository() {
@@ -33,18 +38,22 @@ class DownloadRepository {
);
}
@override
Future<List<bool>> downloadAll(List<DownloadTask> tasks) {
return FileDownloader().enqueueAll(tasks);
}
@override
Future<void> deleteAllTrackingRecords() {
return FileDownloader().database.deleteAllRecords();
}
@override
Future<bool> cancel(String id) {
return FileDownloader().cancelTaskWithId(id);
}
@override
Future<List<TaskRecord>> getLiveVideoTasks() {
return FileDownloader().database.allRecordsWithStatus(
TaskStatus.complete,
@@ -52,6 +61,7 @@ class DownloadRepository {
);
}
@override
Future<void> deleteRecordsWithIds(List<String> ids) {
return FileDownloader().database.deleteRecordsWithIds(ids);
}

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/network.interface.dart';
import 'package:network_info_plus/network_info_plus.dart';
final networkRepositoryProvider = Provider((_) {
@@ -9,11 +10,12 @@ final networkRepositoryProvider = Provider((_) {
return NetworkRepository(networkInfo);
});
class NetworkRepository {
class NetworkRepository implements INetworkRepository {
final NetworkInfo _networkInfo;
NetworkRepository(this._networkInfo);
@override
Future<String?> getWifiName() {
if (Platform.isAndroid) {
// remove quote around the return value on Android
@@ -28,6 +30,7 @@ class NetworkRepository {
return _networkInfo.getWifiName();
}
@override
Future<String?> getWifiIp() {
return _networkInfo.getWifiIP();
}

View File

@@ -1,22 +1,26 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/secure_storage.interface.dart';
final secureStorageRepositoryProvider =
Provider((ref) => SecureStorageRepository(const FlutterSecureStorage()));
class SecureStorageRepository {
class SecureStorageRepository implements ISecureStorageRepository {
final FlutterSecureStorage _secureStorage;
SecureStorageRepository(this._secureStorage);
@override
Future<String?> read(String key) {
return _secureStorage.read(key: key);
}
@override
Future<void> write(String key, String value) {
return _secureStorage.write(key: key, value: value);
}
@override
Future<void> delete(String key) {
return _secureStorage.delete(key: key);
}

View File

@@ -1,4 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/sessions_api.interface.dart';
import 'package:immich_mobile/models/sessions/session_create_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
@@ -10,11 +11,13 @@ final sessionsAPIRepositoryProvider = Provider(
),
);
class SessionsAPIRepository extends ApiRepository {
class SessionsAPIRepository extends ApiRepository
implements ISessionAPIRepository {
final SessionsApi _api;
SessionsAPIRepository(this._api);
@override
Future<SessionCreateResponse> createSession(
String deviceType,
String deviceOS, {

View File

@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/share_handler.interface.dart';
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
import 'package:share_handler/share_handler.dart';
@@ -8,11 +9,13 @@ final shareHandlerRepositoryProvider = Provider(
(ref) => ShareHandlerRepository(),
);
class ShareHandlerRepository {
class ShareHandlerRepository implements IShareHandlerRepository {
ShareHandlerRepository();
@override
void Function(List<ShareIntentAttachment> attachments)? onSharedMedia;
@override
Future<void> init() async {
final handler = ShareHandlerPlatform.instance;
final media = await handler.getInitialSharedMedia();

View File

@@ -1,10 +1,10 @@
import 'package:immich_mobile/interfaces/activity_api.interface.dart';
import 'package:immich_mobile/mixins/error_logger.mixin.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/repositories/activity_api.repository.dart';
import 'package:logging/logging.dart';
class ActivityService with ErrorLoggerMixin {
final ActivityApiRepository _activityApiRepository;
final IActivityApiRepository _activityApiRepository;
@override
final Logger logger = Logger("ActivityService");

View File

@@ -14,6 +14,8 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
@@ -49,8 +51,8 @@ class AlbumService {
final IAlbumRepository _albumRepository;
final IAssetRepository _assetRepository;
final IBackupAlbumRepository _backupAlbumRepository;
final AlbumMediaRepository _albumMediaRepository;
final AlbumApiRepository _albumApiRepository;
final IAlbumMediaRepository _albumMediaRepository;
final IAlbumApiRepository _albumApiRepository;
final Logger _log = Logger('AlbumService');
Completer<bool> _localCompleter = Completer()..complete(false);
Completer<bool> _remoteCompleter = Completer()..complete(false);

View File

@@ -5,11 +5,11 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
@@ -53,7 +53,7 @@ class AssetService {
final IAssetApiRepository _assetApiRepository;
final IAssetRepository _assetRepository;
final IExifInfoRepository _exifInfoRepository;
final IsarUserRepository _isarUserRepository;
final IUserRepository _userRepository;
final IETagRepository _etagRepository;
final IBackupAlbumRepository _backupRepository;
final ApiService _apiService;
@@ -68,7 +68,7 @@ class AssetService {
this._assetApiRepository,
this._assetRepository,
this._exifInfoRepository,
this._isarUserRepository,
this._userRepository,
this._etagRepository,
this._backupRepository,
this._apiService,
@@ -85,9 +85,7 @@ class AssetService {
final syncedUserIds = await _etagRepository.getAllIds();
final List<UserDto> syncedUsers = syncedUserIds.isEmpty
? []
: (await _isarUserRepository.getByUserIds(syncedUserIds))
.nonNulls
.toList();
: (await _userRepository.getByUserIds(syncedUserIds)).nonNulls.toList();
final Stopwatch sw = Stopwatch()..start();
final bool changes = await _syncService.syncRemoteAssetsToDb(
users: syncedUsers,

View File

@@ -11,6 +11,7 @@ import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
@@ -51,7 +52,7 @@ class BackupService {
final Logger _log = Logger("BackupService");
final AppSettingsService _appSetting;
final AlbumService _albumService;
final AlbumMediaRepository _albumMediaRepository;
final IAlbumMediaRepository _albumMediaRepository;
final IFileMediaRepository _fileMediaRepository;
final IAssetRepository _assetRepository;
final IAssetMediaRepository _assetMediaRepository;

View File

@@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/download.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
import 'package:immich_mobile/repositories/download.repository.dart';
@@ -22,7 +23,7 @@ final downloadServiceProvider = Provider(
);
class DownloadService {
final DownloadRepository _downloadRepository;
final IDownloadRepository _downloadRepository;
final IFileMediaRepository _fileMediaRepository;
final Logger _log = Logger("DownloadService");
void Function(TaskStatusUpdate)? onImageDownloadStatus;

View File

@@ -1,24 +1,24 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
class EntityService {
final IAssetRepository _assetRepository;
final IsarUserRepository _isarUserRepository;
final IUserRepository _userRepository;
EntityService(
this._assetRepository,
this._isarUserRepository,
this._userRepository,
);
Future<Album> fillAlbumWithDatabaseEntities(Album album) async {
final ownerId = album.ownerId;
if (ownerId != null) {
// replace owner with user from database
final user = await _isarUserRepository.getByUserId(ownerId);
final user = await _userRepository.getByUserId(ownerId);
album.owner.value = user == null ? null : User.fromDto(user);
}
final thumbnailAssetId =
@@ -30,7 +30,7 @@ class EntityService {
}
if (album.remoteUsers.isNotEmpty) {
// replace all users with users from database
final users = await _isarUserRepository
final users = await _userRepository
.getByUserIds(album.remoteUsers.map((user) => user.id).toList());
album.sharedUsers.clear();
album.sharedUsers.addAll(users.nonNulls.map(User.fromDto));

View File

@@ -1,4 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/biometric.interface.dart';
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
import 'package:immich_mobile/repositories/biometric.repository.dart';
@@ -9,7 +10,9 @@ final localAuthServiceProvider = Provider(
);
class LocalAuthService {
final BiometricRepository _biometricRepository;
// final _log = Logger("LocalAuthService");
final IBiometricRepository _biometricRepository;
LocalAuthService(this._biometricRepository);

View File

@@ -1,4 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/network.interface.dart';
import 'package:immich_mobile/repositories/network.repository.dart';
import 'package:immich_mobile/repositories/permission.repository.dart';
@@ -10,7 +11,7 @@ final networkServiceProvider = Provider((ref) {
});
class NetworkService {
final NetworkRepository _repository;
final INetworkRepository _repository;
final IPermissionRepository _permissionRepository;
NetworkService(this._repository, this._permissionRepository);

View File

@@ -1,6 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.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';
@@ -19,12 +19,12 @@ final partnerServiceProvider = Provider(
class PartnerService {
final IPartnerApiRepository _partnerApiRepository;
final IPartnerRepository _partnerRepository;
final IsarUserRepository _isarUserRepository;
final IUserRepository _userRepository;
final Logger _log = Logger("PartnerService");
PartnerService(
this._partnerApiRepository,
this._isarUserRepository,
this._userRepository,
this._partnerRepository,
);
@@ -47,8 +47,7 @@ class PartnerService {
Future<bool> removePartner(UserDto partner) async {
try {
await _partnerApiRepository.delete(partner.id);
await _isarUserRepository
.update(partner.copyWith(isPartnerSharedBy: false));
await _userRepository.update(partner.copyWith(isPartnerSharedBy: false));
} catch (e) {
_log.warning("Failed to remove partner ${partner.id}", e);
return false;
@@ -59,8 +58,7 @@ class PartnerService {
Future<bool> addPartner(UserDto partner) async {
try {
await _partnerApiRepository.create(partner.id);
await _isarUserRepository
.update(partner.copyWith(isPartnerSharedBy: true));
await _userRepository.update(partner.copyWith(isPartnerSharedBy: true));
return true;
} catch (e) {
_log.warning("Failed to add partner ${partner.id}", e);
@@ -77,7 +75,7 @@ class PartnerService {
partner.id,
inTimeline: inTimeline,
);
await _isarUserRepository
await _userRepository
.update(partner.copyWith(inTimeline: dto.inTimeline));
return true;
} catch (e) {

View File

@@ -1,4 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/secure_storage.interface.dart';
import 'package:immich_mobile/repositories/secure_storage.repository.dart';
final secureStorageServiceProvider = Provider(
@@ -8,7 +9,9 @@ final secureStorageServiceProvider = Provider(
);
class SecureStorageService {
final SecureStorageRepository _secureStorageRepository;
// final _log = Logger("LocalAuthService");
final ISecureStorageRepository _secureStorageRepository;
SecureStorageService(this._secureStorageRepository);

View File

@@ -3,17 +3,18 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/models/user.model.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/entities/etag.entity.dart';
import 'package:immich_mobile/extensions/collection_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.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';
@@ -62,17 +63,17 @@ final syncServiceProvider = Provider(
class SyncService {
final HashService _hashService;
final EntityService _entityService;
final AlbumMediaRepository _albumMediaRepository;
final AlbumApiRepository _albumApiRepository;
final IAlbumMediaRepository _albumMediaRepository;
final IAlbumApiRepository _albumApiRepository;
final IAlbumRepository _albumRepository;
final IAssetRepository _assetRepository;
final IExifInfoRepository _exifInfoRepository;
final IsarUserRepository _isarUserRepository;
final IUserRepository _userRepository;
final UserService _userService;
final IPartnerRepository _partnerRepository;
final IETagRepository _eTagRepository;
final IPartnerApiRepository _partnerApiRepository;
final UserApiRepository _userApiRepository;
final IUserApiRepository _userApiRepository;
final AsyncMutex _lock = AsyncMutex();
final Logger _log = Logger('SyncService');
final AppSettingsService _appSettingsService;
@@ -87,7 +88,7 @@ class SyncService {
this._assetRepository,
this._exifInfoRepository,
this._partnerRepository,
this._isarUserRepository,
this._userRepository,
this._userService,
this._eTagRepository,
this._appSettingsService,
@@ -164,7 +165,7 @@ class SyncService {
/// Returns `true`if there were any changes
Future<bool> _syncUsersFromServer(List<UserDto> users) async {
users.sortBy((u) => u.id);
final dbUsers = await _isarUserRepository.getAll(sortBy: SortUserBy.id);
final dbUsers = await _userRepository.getAll(sortBy: SortUserBy.id);
final List<String> toDelete = [];
final List<UserDto> toUpsert = [];
final changes = diffSortedListsSync(
@@ -185,9 +186,9 @@ class SyncService {
onlySecond: (UserDto b) => toDelete.add(b.id),
);
if (changes) {
await _isarUserRepository.transaction(() async {
await _isarUserRepository.delete(toDelete);
await _isarUserRepository.updateAll(toUpsert);
await _userRepository.transaction(() async {
await _userRepository.delete(toDelete);
await _userRepository.updateAll(toUpsert);
});
}
return changes;
@@ -447,7 +448,7 @@ class SyncService {
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
await upsertAssetsWithExif(updated);
final assetsToLink = existingInDb + updated;
final usersToLink = await _isarUserRepository.getByUserIds(userIdsToAdd);
final usersToLink = await _userRepository.getByUserIds(userIdsToAdd);
album.name = dto.name;
album.description = dto.description;

View File

@@ -57,13 +57,7 @@ Future<void> migrateDatabaseIfNeeded(Isar db) async {
}
final shouldTruncate = version < 8 || version < targetVersion;
if (shouldTruncate) {
if (targetVersion == 12) {
await Store.put(StoreKey.version, targetVersion);
return;
}
await _migrateTo(db, targetVersion);
}
}

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.0
- 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.0+201
environment:
sdk: '>=3.3.0 <4.0.0'

View File

@@ -4,9 +4,9 @@ import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:mocktail/mocktail.dart';
import '../../fixtures/sync_stream.stub.dart';
@@ -30,7 +30,7 @@ class _MockCancellationWrapper extends Mock implements _CancellationWrapper {}
void main() {
late SyncStreamService sut;
late SyncStreamRepository mockSyncStreamRepo;
late ISyncStreamRepository mockSyncStreamRepo;
late ISyncApiRepository mockSyncApiRepo;
late Function(List<SyncEvent>, Function()) handleEventsCallback;
late _MockAbortCallbackWrapper mockAbortCallbackWrapper;

View File

@@ -1,11 +1,11 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:mocktail/mocktail.dart';
import '../../fixtures/user.stub.dart';
@@ -14,16 +14,16 @@ import '../service.mock.dart';
void main() {
late UserService sut;
late IsarUserRepository mockUserRepo;
late UserApiRepository mockUserApiRepo;
late IUserRepository mockUserRepo;
late IUserApiRepository mockUserApiRepo;
late StoreService mockStoreService;
setUp(() {
mockUserRepo = MockIsarUserRepository();
mockUserRepo = MockUserRepository();
mockUserApiRepo = MockUserApiRepository();
mockStoreService = MockStoreService();
sut = UserService(
isarUserRepository: mockUserRepo,
userRepository: mockUserRepo,
userApiRepository: mockUserApiRepo,
storeService: mockStoreService,
);

View File

@@ -5,21 +5,21 @@ import 'package:immich_mobile/domain/interfaces/log.interface.dart';
import 'package:immich_mobile/domain/interfaces/storage.interface.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.interface.dart';
import 'package:mocktail/mocktail.dart';
class MockStoreRepository extends Mock implements IStoreRepository {}
class MockLogRepository extends Mock implements ILogRepository {}
class MockIsarUserRepository extends Mock implements IsarUserRepository {}
class MockUserRepository extends Mock implements IUserRepository {}
class MockDeviceAssetRepository extends Mock
implements IDeviceAssetRepository {}
class MockSyncStreamRepository extends Mock implements SyncStreamRepository {}
class MockSyncStreamRepository extends Mock implements ISyncStreamRepository {}
class MockLocalAlbumRepository extends Mock implements ILocalAlbumRepository {}
@@ -28,6 +28,6 @@ class MockLocalAssetRepository extends Mock implements ILocalAssetRepository {}
class MockStorageRepository extends Mock implements IStorageRepository {}
// API Repos
class MockUserApiRepository extends Mock implements UserApiRepository {}
class MockUserApiRepository extends Mock implements IUserApiRepository {}
class MockSyncApiRepository extends Mock implements ISyncApiRepository {}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
@@ -55,7 +55,7 @@ void main() {
final MockAlbumRepository albumRepository = MockAlbumRepository();
final MockAssetRepository assetRepository = MockAssetRepository();
final MockExifInfoRepository exifInfoRepository = MockExifInfoRepository();
final MockIsarUserRepository userRepository = MockIsarUserRepository();
final MockUserRepository userRepository = MockUserRepository();
final MockETagRepository eTagRepository = MockETagRepository();
final MockAlbumMediaRepository albumMediaRepository =
MockAlbumMediaRepository();

View File

@@ -1,5 +1,7 @@
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
@@ -11,8 +13,6 @@ 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/album_media.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:mocktail/mocktail.dart';
class MockAlbumRepository extends Mock implements IAlbumRepository {}
@@ -25,7 +25,7 @@ class MockExifInfoRepository extends Mock implements IExifInfoRepository {}
class MockETagRepository extends Mock implements IETagRepository {}
class MockAlbumMediaRepository extends Mock implements AlbumMediaRepository {}
class MockAlbumMediaRepository extends Mock implements IAlbumMediaRepository {}
class MockBackupAlbumRepository extends Mock
implements IBackupAlbumRepository {}
@@ -36,7 +36,7 @@ class MockAssetMediaRepository extends Mock implements IAssetMediaRepository {}
class MockFileMediaRepository extends Mock implements IFileMediaRepository {}
class MockAlbumApiRepository extends Mock implements AlbumApiRepository {}
class MockAlbumApiRepository extends Mock implements IAlbumApiRepository {}
class MockAuthApiRepository extends Mock implements IAuthApiRepository {}

View File

@@ -22,7 +22,7 @@ void main() {
late MockExifInfoRepository exifInfoRepository;
late MockETagRepository eTagRepository;
late MockBackupAlbumRepository backupAlbumRepository;
late MockIsarUserRepository userRepository;
late MockUserRepository userRepository;
late MockAssetMediaRepository assetMediaRepository;
late MockApiService apiService;
@@ -35,7 +35,7 @@ void main() {
assetRepository = MockAssetRepository();
assetApiRepository = MockAssetApiRepository();
exifInfoRepository = MockExifInfoRepository();
userRepository = MockIsarUserRepository();
userRepository = MockUserRepository();
eTagRepository = MockETagRepository();
backupAlbumRepository = MockBackupAlbumRepository();
apiService = MockApiService();

View File

@@ -12,11 +12,11 @@ import '../repository.mocks.dart';
void main() {
late EntityService sut;
late MockAssetRepository assetRepository;
late MockIsarUserRepository userRepository;
late MockUserRepository userRepository;
setUp(() {
assetRepository = MockAssetRepository();
userRepository = MockIsarUserRepository();
userRepository = MockUserRepository();
sut = EntityService(assetRepository, userRepository);
});

View File

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

View File

@@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
"version": "1.135.2",
"version": "1.135.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.135.2",
"version": "1.135.0",
"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.0",
"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.0
* 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.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.135.2",
"version": "1.135.0",
"hasInstallScript": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -42,6 +42,7 @@
"handlebars": "^4.7.8",
"i18n-iso-countries": "^7.6.0",
"ioredis": "^5.3.2",
"joi": "^17.10.0",
"js-yaml": "^4.1.0",
"kysely": "^0.28.0",
"kysely-postgres-js": "^2.0.0",
@@ -1348,6 +1349,21 @@
"node": ">=6"
}
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -5036,6 +5052,27 @@
"url": "https://ko-fi.com/killymxi"
}
},
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
"integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"license": "BSD-3-Clause"
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"license": "BSD-3-Clause"
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@@ -11296,6 +11333,19 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
"@sideway/address": "^4.1.5",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/jose": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.135.2",
"version": "1.135.0",
"description": "",
"author": "",
"private": true,
@@ -68,6 +68,7 @@
"handlebars": "^4.7.8",
"i18n-iso-countries": "^7.6.0",
"ioredis": "^5.3.2",
"joi": "^17.10.0",
"js-yaml": "^4.1.0",
"kysely": "^0.28.0",
"kysely-postgres-js": "^2.0.0",

View File

@@ -224,7 +224,7 @@ limit
with
"assets" as (
select
date_trunc('MONTH', "localDateTime" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' as "timeBucket"
date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' as "timeBucket"
from
"assets"
where
@@ -232,7 +232,7 @@ with
and "assets"."visibility" in ('archive', 'timeline')
)
select
("timeBucket" AT TIME ZONE 'UTC')::date::text as "timeBucket",
"timeBucket"::date::text as "timeBucket",
count(*) as "count"
from
"assets"
@@ -300,7 +300,7 @@ with
where
"assets"."deletedAt" is null
and "assets"."visibility" in ('archive', 'timeline')
and date_trunc('MONTH', "localDateTime" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' = $2
and date_trunc('MONTH', "localDateTime" at time zone 'UTC') at time zone 'UTC' = $2
and not exists (
select
from

View File

@@ -12,38 +12,6 @@ delete from "person"
where
"person"."id" in ($1)
-- PersonRepository.getAllForUser
select
"person".*
from
"person"
inner join "asset_faces" on "asset_faces"."personId" = "person"."id"
inner join "assets" on "asset_faces"."assetId" = "assets"."id"
and "assets"."visibility" = 'timeline'
and "assets"."deletedAt" is null
where
"person"."ownerId" = $1
and "asset_faces"."deletedAt" is null
and "person"."isHidden" = $2
group by
"person"."id"
having
(
"person"."name" != $3
or count("asset_faces"."assetId") >= $4
)
order by
"person"."isHidden" asc,
"person"."isFavorite" desc,
NULLIF(person.name, '') is null asc,
count("asset_faces"."assetId") desc,
NULLIF(person.name, '') asc nulls last,
"person"."createdAt"
limit
$5
offset
$6
-- PersonRepository.getAllWithoutFaces
select
"person".*

View File

@@ -42,6 +42,11 @@ interface LivePhotoSearchOptions {
type: AssetType;
}
export enum TimeBucketSize {
DAY = 'DAY',
MONTH = 'MONTH',
}
interface AssetBuilderOptions {
isFavorite?: boolean;
isTrashed?: boolean;
@@ -485,13 +490,13 @@ export class AssetRepository {
.execute();
}
@GenerateSql({ params: [{}] })
@GenerateSql({ params: [{ size: TimeBucketSize.MONTH }] })
async getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]> {
return this.db
.with('assets', (qb) =>
qb
.selectFrom('assets')
.select(truncatedDate<Date>().as('timeBucket'))
.select(truncatedDate<Date>(TimeBucketSize.MONTH).as('timeBucket'))
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
.$if(options.visibility === undefined, withDefaultVisibility)
@@ -520,7 +525,7 @@ export class AssetRepository {
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)),
)
.selectFrom('assets')
.select(sql<string>`("timeBucket" AT TIME ZONE 'UTC')::date::text`.as('timeBucket'))
.select(sql<string>`"timeBucket"::date::text`.as('timeBucket'))
.select((eb) => eb.fn.countAll<number>().as('count'))
.groupBy('timeBucket')
.orderBy('timeBucket', options.order ?? 'desc')
@@ -571,7 +576,7 @@ export class AssetRepository {
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
.$if(options.visibility == undefined, withDefaultVisibility)
.$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!))
.where(truncatedDate(), '=', timeBucket.replace(/^[+-]/, ''))
.where(truncatedDate(TimeBucketSize.MONTH), '=', timeBucket.replace(/^[+-]/, ''))
.$if(!!options.albumId, (qb) =>
qb.where((eb) =>
eb.exists(

View File

@@ -138,7 +138,6 @@ export class PersonRepository {
.stream();
}
@GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] })
async getAllForUser(pagination: PaginationOptions, userId: string, options?: PersonSearchOptions) {
const items = await this.db
.selectFrom('person')
@@ -180,9 +179,8 @@ export class PersonRepository {
)
.$if(!options?.closestFaceAssetId, (qb) =>
qb
.orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
.orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc')
.orderBy(sql`NULLIF(person.name, '')`, (om) => om.asc().nullsLast())
.orderBy(sql`NULLIF(person.name, '') asc nulls last`)
.orderBy('person.createdAt'),
)
.$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false))

View File

@@ -1,21 +1,15 @@
import { Kysely, sql } from 'kysely';
export async function up(qb: Kysely<any>): Promise<void> {
type Conf = { db: string; guc: string[] };
const res = await sql<Conf>`select current_database() db, to_json(setconfig) guc from pg_db_role_setting`.execute(qb);
if (res.rows.length === 0) {
return;
}
await sql`alter database immich reset all;`.execute(qb);
const { db, guc } = res.rows[0];
for (const parameter of guc) {
const [key, value] = parameter.split('=');
if (key === 'vchordrq.prewarm_dim') {
continue;
}
await sql.raw(`alter database "${db}" set ${key} to ${value};`).execute(qb);
}
export async function up(db: Kysely<any>): Promise<void> {
const { rows } = await sql<{ db: string }>`SELECT current_database() as db;`.execute(db);
const databaseName = rows[0].db;
await sql.raw(`ALTER DATABASE "${databaseName}" RESET vchordrq.prewarm_dim;`).execute(db);
}
export async function down(): Promise<void> {}
export async function down(db: Kysely<any>): Promise<void> {
const { rows } = await sql<{ db: string }>`SELECT current_database() as db;`.execute(db);
const databaseName = rows[0].db;
await sql
.raw(`ALTER DATABASE "${databaseName}" SET vchordrq.prewarm_dim = '512,640,768,1024,1152,1536';`)
.execute(db);
}

View File

@@ -151,11 +151,11 @@ describe(SearchService.name, () => {
},
});
const id = assetStub.livePhotoMotionAsset.id;
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
const result = await sut.handleSearchDuplicates({ id });
expect(result).toBe(JobStatus.SKIPPED);
expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled();
});
it('should skip if duplicate detection is disabled', async () => {
@@ -168,11 +168,11 @@ describe(SearchService.name, () => {
},
});
const id = assetStub.livePhotoMotionAsset.id;
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset);
const result = await sut.handleSearchDuplicates({ id });
expect(result).toBe(JobStatus.SKIPPED);
expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled();
});
it('should fail if asset is not found', async () => {

View File

@@ -1113,6 +1113,8 @@ describe(LibraryService.name, () => {
mocks.library.get.mockResolvedValue(library);
mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.image1]));
mocks.asset.getById.mockResolvedValue(assetStub.image1);
await expect(sut.handleDeleteLibrary({ id: library.id })).resolves.toBe(JobStatus.SUCCESS);
});
});

View File

@@ -268,7 +268,7 @@ describe(NotificationService.name, () => {
mocks.album.getById.mockResolvedValue(albumStub.empty);
await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SKIPPED);
expect(mocks.job.queue).not.toHaveBeenCalled();
expect(mocks.asset.getById).not.toHaveBeenCalled();
});
it('should skip if the recipient has email notifications disabled', async () => {

View File

@@ -18,6 +18,7 @@ import postgres, { Notice } from 'postgres';
import { columns, Exif, Person } from 'src/database';
import { DB } from 'src/db';
import { AssetFileType, AssetVisibility, DatabaseExtension, DatabaseSslMode } from 'src/enum';
import { TimeBucketSize } from 'src/repositories/asset.repository';
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
import { DatabaseConnectionParams, VectorExtension } from 'src/types';
@@ -278,8 +279,8 @@ export function withTags(eb: ExpressionBuilder<DB, 'assets'>) {
).as('tags');
}
export function truncatedDate<O>() {
return sql<O>`date_trunc(${sql.lit('MONTH')}, "localDateTime" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'`;
export function truncatedDate<O>(size: TimeBucketSize) {
return sql<O>`date_trunc(${sql.lit(size)}, "localDateTime" at time zone 'UTC') at time zone 'UTC'`;
}
export function withTagId<O>(qb: SelectQueryBuilder<DB, 'assets', O>, tagId: string) {

View File

@@ -7,7 +7,7 @@ import { getKyselyConfig } from 'src/utils/database';
import { GenericContainer, Wait } from 'testcontainers';
const globalSetup = async () => {
const postgresContainer = await new GenericContainer('ghcr.io/immich-app/postgres:14-vectorchord0.4.3')
const postgresContainer = await new GenericContainer('ghcr.io/immich-app/postgres:14-vectorchord0.4.1')
.withExposedPorts(5432)
.withEnvironment({
POSTGRES_PASSWORD: 'postgres',

6
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-web",
"version": "1.135.2",
"version": "1.135.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.135.2",
"version": "1.135.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
@@ -87,7 +87,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.135.2",
"version": "1.135.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.135.2",
"version": "1.135.0",
"license": "GNU Affero General Public License version 3",
"type": "module",
"scripts": {

View File

@@ -1,16 +0,0 @@
<script lang="ts">
import { Icon, Text } from '@immich/ui';
import { mdiCheck, mdiClose } from '@mdi/js';
interface Props {
title: string;
state: boolean;
}
let { title, state }: Props = $props();
</script>
<div class="flex justify-between items-center">
<Text class="text-sm font-medium">{title}</Text>
<Icon icon={state ? mdiCheck : mdiClose} class={state ? 'text-primary' : 'text-danger'} size="24" />
</div>

View File

@@ -546,8 +546,11 @@
{#if stack && withStacked}
{@const stackedAssets = stack.assets}
<div id="stack-slideshow" class="absolute bottom-0 w-full col-span-4 col-start-1">
<div class="relative flex flex-row no-wrap overflow-x-auto overflow-y-hidden horizontal-scrollbar">
<div
id="stack-slideshow"
class="flex place-item-center place-content-center absolute bottom-0 w-full col-span-4 col-start-1 overflow-x-auto overflow-y-hidden horizontal-scrollbar"
>
<div class="relative flex flex-row no-wrap">
{#each stackedAssets as stackedAsset (stackedAsset.id)}
<div
class={['inline-block px-1 relative transition-all pb-2']}

View File

@@ -375,7 +375,7 @@
<Icon path={mdiCheckCircle} size="24" class="text-zinc-800" />
{:else if selected}
<div class="rounded-full bg-[#D9DCEF] dark:bg-[#232932]">
<Icon path={mdiCheckCircle} size="24" class="text-primary" />
<Icon path={mdiCheckCircle} size="24" class="text-immich-primary" />
</div>
{:else}
<Icon path={mdiCheckCircle} size="24" class="text-white/80 hover:text-white" />

View File

@@ -35,7 +35,7 @@
children,
}: Props = $props();
let scrollbarClass = $derived(scrollbar ? 'immich-scrollbar' : 'scrollbar-hidden');
let scrollbarClass = $derived(scrollbar ? 'immich-scrollbar p-2' : 'scrollbar-hidden');
let hasTitleClass = $derived(title ? 'top-16 h-[calc(100%-(--spacing(16)))]' : 'top-0 h-full');
</script>
@@ -60,7 +60,7 @@
{/if}
<main class="relative">
<div class="{scrollbarClass} absolute {hasTitleClass} w-full overflow-y-auto p-2" use:useActions={use}>
<div class="{scrollbarClass} absolute {hasTitleClass} w-full overflow-y-auto" use:useActions={use}>
{@render children?.()}
</div>

View File

@@ -145,19 +145,19 @@
{#if !singleSelect && ((hoveredDayGroup === dayGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dayGroup.groupTitle))}
<div
transition:fly={{ x: -24, duration: 200, opacity: 0.5 }}
class="inline-block pe-2 hover:cursor-pointer"
class="inline-block px-2 hover:cursor-pointer"
onclick={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))}
onkeydown={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))}
>
{#if assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
<Icon path={mdiCheckCircle} size="24" class="text-primary" />
<Icon path={mdiCheckCircle} size="24" color="#4250af" />
{:else}
<Icon path={mdiCircleOutline} size="24" color="#757575" />
{/if}
</div>
{/if}
<span class="w-full truncate first-letter:capitalize" title={dayGroup.groupTitle}>
<span class="w-full truncate first-letter:capitalize ms-2.5" title={dayGroup.groupTitle}>
{dayGroup.groupTitle}
</span>
</div>

View File

@@ -39,7 +39,7 @@
<section
id="memory-lane"
bind:this={memoryLaneElement}
class="relative mt-3 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all"
class="relative mt-5 mx-2 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all"
style="scrollbar-width:none"
use:resizeObserver={({ width }) => (offsetWidth = width)}
onscroll={onScroll}

View File

@@ -32,7 +32,7 @@ export async function getAssetWithOffset(
export function findMonthGroupForAsset(timelineManager: TimelineManager, id: string) {
for (const month of timelineManager.months) {
const asset = month.findAssetById({ id });
const asset = month.findAssetById(id);
if (asset) {
return { monthGroup: month, asset };
}

View File

@@ -20,7 +20,7 @@ import { get } from 'svelte/store';
import { DayGroup } from './day-group.svelte';
import { GroupInsertionCache } from './group-insertion-cache.svelte';
import type { TimelineManager } from './timeline-manager.svelte';
import type { AssetDescriptor, AssetOperation, Direction, MoveAsset, TimelineAsset } from './types';
import type { AssetOperation, Direction, MoveAsset, TimelineAsset } from './types';
import { ViewerAsset } from './viewer-asset.svelte';
export class MonthGroup {
@@ -342,9 +342,9 @@ export class MonthGroup {
}
}
findAssetById(assetDescriptor: AssetDescriptor) {
findAssetById(id: string) {
for (const asset of this.assetsIterator()) {
if (asset.id === assetDescriptor.id) {
if (asset.id === id) {
return asset;
}
}

View File

@@ -80,15 +80,15 @@ describe('TimelineManager', () => {
expect(plainMonths).toEqual(
expect.arrayContaining([
expect.objectContaining({ year: 2024, month: 3, height: 165.5 }),
expect.objectContaining({ year: 2024, month: 2, height: 11_996 }),
expect.objectContaining({ year: 2024, month: 3, height: 185.5 }),
expect.objectContaining({ year: 2024, month: 2, height: 12_016 }),
expect.objectContaining({ year: 2024, month: 1, height: 286 }),
]),
);
});
it('calculates timeline height', () => {
expect(timelineManager.timelineHeight).toBe(12_447.5);
expect(timelineManager.timelineHeight).toBe(12_487.5);
});
});

View File

@@ -428,7 +428,7 @@ export class TimelineManager {
return;
}
monthGroup = await this.#loadMonthGroupAtTime(asset.localDateTime, { cancelable: false });
if (monthGroup?.findAssetById({ id })) {
if (monthGroup?.findAssetById(id)) {
return monthGroup;
}
}

View File

@@ -96,7 +96,6 @@ export function justifiedLayout(assets: (TimelineAsset | AssetResponseDto)[], op
containerWidth: options.rowWidth,
boxSpacing: options.spacing,
targetRowHeightTolerange: options.heightTolerance,
containerPadding: 0,
};
const result = createJustifiedLayout(

View File

@@ -443,7 +443,7 @@
<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}>
<div class="relative w-full shrink">
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
<main class="relative h-dvh overflow-hidden px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
<AssetGrid
enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true}
{album}

View File

@@ -42,28 +42,28 @@
};
</script>
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
<main class="grid h-dvh pt-18">
<AssetGrid enableRouting={true} {timelineManager} {assetInteraction} onEscape={handleEscape} />
</main>
{#if assetInteraction.selectionActive}
<AssetSelectControlBar
assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()}
>
<CreateSharedLink />
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
<AddToAlbum />
<AddToAlbum shared />
</ButtonContextMenu>
<DownloadAction />
</AssetSelectControlBar>
{:else}
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(AppRoute.SHARING)}>
{#snippet leading()}
<p class="whitespace-nowrap text-immich-fg dark:text-immich-dark-fg">
{data.partner.name}'s photos
</p>
{/snippet}
</ControlAppBar>
{/if}
{#if assetInteraction.selectionActive}
<AssetSelectControlBar
assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()}
>
<CreateSharedLink />
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
<AddToAlbum />
<AddToAlbum shared />
</ButtonContextMenu>
<DownloadAction />
</AssetSelectControlBar>
{:else}
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(AppRoute.SHARING)}>
{#snippet leading()}
<p class="whitespace-nowrap text-immich-fg dark:text-immich-dark-fg">
{data.partner.name}'s photos
</p>
{/snippet}
</ControlAppBar>
{/if}
</main>

View File

@@ -374,7 +374,7 @@
</script>
<main
class="relative z-0 h-dvh overflow-hidden px-2 md:px-6 md:pt-(--navbar-height-md) pt-(--navbar-height)"
class="relative z-0 h-dvh overflow-hidden tall:ms-4 md:pt-(--navbar-height-md) pt-(--navbar-height)"
use:scrollMemoryClearer={{
routeStartsWith: AppRoute.PEOPLE,
beforeClear: () => {

Some files were not shown because too many files have changed in this diff Show More