mirror of
https://github.com/immich-app/immich.git
synced 2026-03-01 11:20:12 +03:00
Compare commits
6 Commits
v2.2.1
...
fix-remote
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f72d47942 | ||
|
|
c76324c611 | ||
|
|
0ddb92e1ec | ||
|
|
d08a520aa2 | ||
|
|
7bdf0f6c50 | ||
|
|
2b33a58448 |
2
.github/workflows/fix-format.yml
vendored
2
.github/workflows/fix-format.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
|
||||
- name: Fix formatting
|
||||
run: make install-all && make format-all
|
||||
run: pnpm --recursive install && pnpm run --recursive --parallel fix:format
|
||||
|
||||
- name: Commit and push
|
||||
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
|
||||
|
||||
@@ -22,7 +22,6 @@ dependencies = [
|
||||
"rich>=13.4.2",
|
||||
"tokenizers>=0.15.0,<1.0",
|
||||
"uvicorn[standard]>=0.22.0,<1.0",
|
||||
"setuptools>=78.1.0",
|
||||
"rapidocr>=3.1.0",
|
||||
]
|
||||
|
||||
|
||||
2
machine-learning/uv.lock
generated
2
machine-learning/uv.lock
generated
@@ -1100,7 +1100,6 @@ dependencies = [
|
||||
{ name = "python-multipart" },
|
||||
{ name = "rapidocr" },
|
||||
{ name = "rich" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "tokenizers" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
@@ -1188,7 +1187,6 @@ requires-dist = [
|
||||
{ name = "rapidocr", specifier = ">=3.1.0" },
|
||||
{ name = "rich", specifier = ">=13.4.2" },
|
||||
{ name = "rknn-toolkit-lite2", marker = "extra == 'rknn'", specifier = ">=2.3.0,<3" },
|
||||
{ name = "setuptools", specifier = ">=78.1.0" },
|
||||
{ name = "tokenizers", specifier = ">=0.15.0,<1.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.22.0,<1.0" },
|
||||
]
|
||||
|
||||
@@ -133,11 +133,15 @@
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
B231F52D2E93A44A00BC45D1 /* Core */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = Sync;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -526,14 +530,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
@@ -562,14 +562,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
@@ -718,7 +714,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CURRENT_PROJECT_VERSION = 233;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -862,7 +858,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CURRENT_PROJECT_VERSION = 233;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -892,7 +888,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CURRENT_PROJECT_VERSION = 233;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -926,7 +922,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CURRENT_PROJECT_VERSION = 233;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -969,7 +965,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CURRENT_PROJECT_VERSION = 233;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -1009,7 +1005,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CURRENT_PROJECT_VERSION = 233;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -1048,7 +1044,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CURRENT_PROJECT_VERSION = 233;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1092,7 +1088,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CURRENT_PROJECT_VERSION = 233;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
@@ -1133,7 +1129,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CURRENT_PROJECT_VERSION = 233;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.1.0</string>
|
||||
<string>2.2.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -107,7 +107,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>231</string>
|
||||
<string>233</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -15,18 +15,18 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
|
||||
|
||||
## iOS
|
||||
|
||||
### ios release_dev
|
||||
### ios gha_testflight_dev
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios release_dev
|
||||
[bundle exec] fastlane ios gha_testflight_dev
|
||||
```
|
||||
|
||||
iOS Development Build to TestFlight (requires separate bundle ID)
|
||||
|
||||
### ios release_ci
|
||||
### ios gha_release_prod
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios release_ci
|
||||
[bundle exec] fastlane ios gha_release_prod
|
||||
```
|
||||
|
||||
iOS Release to TestFlight
|
||||
|
||||
@@ -612,12 +612,15 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
|
||||
final validUsers = {currentUserId, ...partnerIds.nonNulls};
|
||||
|
||||
// Asset is not owned by the current user or any of their partners and is not part of any (shared) album
|
||||
// Asset is not owned by the current user or any of their partners and is not part of any (shared) album or memory
|
||||
// Likely a stale asset that was previously shared but has been removed
|
||||
await _db.remoteAssetEntity.deleteWhere((asset) {
|
||||
return asset.ownerId.isNotIn(validUsers) &
|
||||
asset.id.isNotInQuery(
|
||||
_db.remoteAlbumAssetEntity.selectOnly()..addColumns([_db.remoteAlbumAssetEntity.assetId]),
|
||||
) &
|
||||
asset.id.isNotInQuery(
|
||||
_db.memoryAssetEntity.selectOnly()..addColumns([_db.memoryAssetEntity.assetId]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/people.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
@@ -108,8 +107,6 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
|
||||
}
|
||||
|
||||
void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
||||
ref.read(currentTabIndexProvider.notifier).state = index;
|
||||
|
||||
// On Photos page menu tapped
|
||||
if (router.activeIndex == kPhotoTabIndex && index == kPhotoTabIndex) {
|
||||
EventStream.shared.emit(const ScrollToTopEvent());
|
||||
|
||||
@@ -73,27 +73,29 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
search() async {
|
||||
if (filter.value.isEmpty) {
|
||||
searchFilter(SearchFilter filter) async {
|
||||
if (filter.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (preFilter == null && filter.value == previousFilter.value) {
|
||||
if (preFilter == null && filter == previousFilter.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSearching.value = true;
|
||||
ref.watch(paginatedSearchProvider.notifier).clear();
|
||||
final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
|
||||
final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter);
|
||||
|
||||
if (!hasResult) {
|
||||
context.showSnackBar(searchInfoSnackBar('search_no_result'.t(context: context)));
|
||||
}
|
||||
|
||||
previousFilter.value = filter.value;
|
||||
previousFilter.value = filter;
|
||||
isSearching.value = false;
|
||||
}
|
||||
|
||||
search() => searchFilter(filter.value);
|
||||
|
||||
loadMoreSearchResult() async {
|
||||
isSearching.value = true;
|
||||
final hasResult = await ref.watch(paginatedSearchProvider.notifier).search(filter.value);
|
||||
@@ -108,7 +110,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
searchPreFilter() {
|
||||
if (preFilter != null) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
search();
|
||||
searchFilter(preFilter);
|
||||
|
||||
if (preFilter.location.city != null) {
|
||||
locationCurrentFilterWidget.value = Text(preFilter.location.city!, style: context.textTheme.labelLarge);
|
||||
@@ -122,7 +124,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
||||
searchPreFilter();
|
||||
|
||||
return null;
|
||||
}, []);
|
||||
}, [preFilter]);
|
||||
|
||||
showPeoplePicker() {
|
||||
handleOnSelect(Set<PersonDto> value) {
|
||||
|
||||
@@ -3,14 +3,12 @@ import 'dart:async';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class SimilarPhotosActionButton extends ConsumerWidget {
|
||||
@@ -38,20 +36,7 @@ class SimilarPhotosActionButton extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
|
||||
/// Using and currentTabIndex to make sure we are using the correct
|
||||
/// navigation behavior. We want to be able to navigate back to the
|
||||
/// main timline using View In Timeline button without the need of
|
||||
/// waiting for the timeline to be rebuild. At the same time, we want
|
||||
/// to refresh the search page when tapping the Similar Photos button
|
||||
/// while already in the Search tab.
|
||||
final currentTabIndex = (ref.read(currentTabIndexProvider.notifier).state);
|
||||
|
||||
if (currentTabIndex != kSearchTabIndex) {
|
||||
unawaited(context.router.navigate(const DriftSearchRoute()));
|
||||
ref.read(currentTabIndexProvider.notifier).state = kSearchTabIndex;
|
||||
} else {
|
||||
unawaited(context.router.popAndPush(const DriftSearchRoute()));
|
||||
}
|
||||
unawaited(context.navigateTo(const DriftSearchRoute()));
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -5,4 +5,3 @@ final inLockedViewProvider = StateProvider<bool>((ref) => false);
|
||||
final currentRouteNameProvider = StateProvider<String?>((ref) => null);
|
||||
final previousRouteNameProvider = StateProvider<String?>((ref) => null);
|
||||
final previousRouteDataProvider = StateProvider<RouteSettings?>((ref) => null);
|
||||
final currentTabIndexProvider = StateProvider<int>((ref) => 0);
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
/// This test reproduces the bug where pruneAssets() deletes assets that are part of memories,
|
||||
/// causing foreign key constraint failures when trying to insert memory-asset relationships.
|
||||
void main() {
|
||||
late DbRepository db;
|
||||
late SyncStreamRepository sut;
|
||||
|
||||
setUp(() async {
|
||||
db = DbRepository(NativeDatabase.memory());
|
||||
sut = SyncStreamRepository(db);
|
||||
|
||||
// Set up test data: Create a user and a partner
|
||||
await sut.updateAuthUsersV1([
|
||||
SyncAuthUserV1(
|
||||
email: 'current-user@test.com',
|
||||
id: 'user-1',
|
||||
isAdmin: false,
|
||||
name: 'Current User',
|
||||
avatarColor: null,
|
||||
hasProfileImage: false,
|
||||
profileChangedAt: DateTime(2025),
|
||||
),
|
||||
]);
|
||||
|
||||
await sut.updateUsersV1([
|
||||
SyncUserV1(
|
||||
deletedAt: null,
|
||||
email: 'partner@test.com',
|
||||
id: 'partner-1',
|
||||
name: 'Partner User',
|
||||
avatarColor: null,
|
||||
hasProfileImage: false,
|
||||
profileChangedAt: DateTime(2025),
|
||||
),
|
||||
]);
|
||||
|
||||
await sut.updatePartnerV1([
|
||||
SyncPartnerV1(
|
||||
inTimeline: true,
|
||||
sharedById: 'partner-1',
|
||||
sharedWithId: 'user-1',
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
group('pruneAssets - Memory Asset Bug', () {
|
||||
test('BEFORE FIX: pruneAssets() should NOT delete assets that are part of memories', () async {
|
||||
// Step 1: Create an asset owned by someone else (not current user or partner)
|
||||
await sut.updateAssetsV1([
|
||||
SyncAssetV1(
|
||||
checksum: 'checksum-1'.codeUnits,
|
||||
deletedAt: null,
|
||||
deviceAssetId: 'device-1',
|
||||
deviceId: 'device-1',
|
||||
duplicateId: null,
|
||||
duration: null,
|
||||
fileCreatedAt: DateTime(2025, 1, 1),
|
||||
fileModifiedAt: DateTime(2025, 1, 1),
|
||||
id: 'asset-shared-memory',
|
||||
isArchived: false,
|
||||
isFavorite: false,
|
||||
isOffline: false,
|
||||
isTrashed: false,
|
||||
libraryId: null,
|
||||
livePhotoVideoId: null,
|
||||
localDateTime: DateTime(2025, 1, 1),
|
||||
originalFileName: 'shared-memory.jpg',
|
||||
// Asset owned by someone else - should be pruned if not in album/memory
|
||||
ownerId: 'other-user-not-partner',
|
||||
resized: true,
|
||||
stackId: null,
|
||||
thumbhash: null,
|
||||
type: AssetTypeEnum.IMAGE,
|
||||
updatedAt: DateTime(2025, 1, 1),
|
||||
visibility: AssetVisibility.public_,
|
||||
),
|
||||
]);
|
||||
|
||||
// Step 2: Create a memory owned by current user
|
||||
await sut.updateMemoriesV1([
|
||||
SyncMemoryV1(
|
||||
createdAt: DateTime(2025, 1, 1),
|
||||
data: {'year': 2025, 'title': 'Test Memory'},
|
||||
deletedAt: null,
|
||||
hideAt: null,
|
||||
id: 'memory-1',
|
||||
isSaved: false,
|
||||
memoryAt: DateTime(2025, 1, 1),
|
||||
ownerId: 'user-1',
|
||||
seenAt: null,
|
||||
showAt: DateTime(2025, 1, 1),
|
||||
type: MemoryType.onThisDay,
|
||||
updatedAt: DateTime(2025, 1, 1),
|
||||
),
|
||||
]);
|
||||
|
||||
// Step 3: Link the shared asset to the memory
|
||||
await sut.updateMemoryAssetsV1([
|
||||
SyncMemoryAssetV1(
|
||||
assetId: 'asset-shared-memory',
|
||||
memoryId: 'memory-1',
|
||||
),
|
||||
]);
|
||||
|
||||
// Verify the asset and memory-asset relationship exist
|
||||
final assetsBefore = await db.remoteAssetEntity.select().get();
|
||||
final memoryAssetsBefore = await db.memoryAssetEntity.select().get();
|
||||
expect(assetsBefore.length, 1);
|
||||
expect(assetsBefore.first.id, 'asset-shared-memory');
|
||||
expect(memoryAssetsBefore.length, 1);
|
||||
|
||||
// Step 4: Call pruneAssets() - This is where the bug happens
|
||||
await sut.pruneAssets();
|
||||
|
||||
// Step 5: Verify the asset is NOT deleted (because it's in a memory)
|
||||
final assetsAfter = await db.remoteAssetEntity.select().get();
|
||||
expect(
|
||||
assetsAfter.length,
|
||||
1,
|
||||
reason: 'Asset should NOT be pruned because it is part of a memory',
|
||||
);
|
||||
expect(assetsAfter.first.id, 'asset-shared-memory');
|
||||
|
||||
// Step 6: Verify we can still work with memory-asset relationships
|
||||
// This simulates receiving more sync events after pruning
|
||||
await expectLater(
|
||||
sut.updateMemoryAssetsV1([
|
||||
SyncMemoryAssetV1(
|
||||
assetId: 'asset-shared-memory',
|
||||
memoryId: 'memory-1',
|
||||
),
|
||||
]),
|
||||
completes,
|
||||
reason: 'Should not throw foreign key constraint error',
|
||||
);
|
||||
});
|
||||
|
||||
test('pruneAssets() SHOULD delete assets not in albums or memories', () async {
|
||||
// Step 1: Create an asset that's truly orphaned (not in album or memory)
|
||||
await sut.updateAssetsV1([
|
||||
SyncAssetV1(
|
||||
checksum: 'checksum-2'.codeUnits,
|
||||
deletedAt: null,
|
||||
deviceAssetId: 'device-2',
|
||||
deviceId: 'device-2',
|
||||
duplicateId: null,
|
||||
duration: null,
|
||||
fileCreatedAt: DateTime(2025, 1, 1),
|
||||
fileModifiedAt: DateTime(2025, 1, 1),
|
||||
id: 'asset-orphaned',
|
||||
isArchived: false,
|
||||
isFavorite: false,
|
||||
isOffline: false,
|
||||
isTrashed: false,
|
||||
libraryId: null,
|
||||
livePhotoVideoId: null,
|
||||
localDateTime: DateTime(2025, 1, 1),
|
||||
originalFileName: 'orphaned.jpg',
|
||||
ownerId: 'other-user-not-partner',
|
||||
resized: true,
|
||||
stackId: null,
|
||||
thumbhash: null,
|
||||
type: AssetTypeEnum.IMAGE,
|
||||
updatedAt: DateTime(2025, 1, 1),
|
||||
visibility: AssetVisibility.public_,
|
||||
),
|
||||
]);
|
||||
|
||||
// Verify the asset exists
|
||||
final assetsBefore = await db.remoteAssetEntity.select().get();
|
||||
expect(assetsBefore.length, 1);
|
||||
|
||||
// Call pruneAssets()
|
||||
await sut.pruneAssets();
|
||||
|
||||
// Verify the orphaned asset IS deleted
|
||||
final assetsAfter = await db.remoteAssetEntity.select().get();
|
||||
expect(
|
||||
assetsAfter.length,
|
||||
0,
|
||||
reason: 'Orphaned asset should be pruned',
|
||||
);
|
||||
});
|
||||
|
||||
test('pruneAssets() should NOT delete assets in albums', () async {
|
||||
// Step 1: Create an asset and an album
|
||||
await sut.updateAssetsV1([
|
||||
SyncAssetV1(
|
||||
checksum: 'checksum-3'.codeUnits,
|
||||
deletedAt: null,
|
||||
deviceAssetId: 'device-3',
|
||||
deviceId: 'device-3',
|
||||
duplicateId: null,
|
||||
duration: null,
|
||||
fileCreatedAt: DateTime(2025, 1, 1),
|
||||
fileModifiedAt: DateTime(2025, 1, 1),
|
||||
id: 'asset-in-album',
|
||||
isArchived: false,
|
||||
isFavorite: false,
|
||||
isOffline: false,
|
||||
isTrashed: false,
|
||||
libraryId: null,
|
||||
livePhotoVideoId: null,
|
||||
localDateTime: DateTime(2025, 1, 1),
|
||||
originalFileName: 'in-album.jpg',
|
||||
ownerId: 'other-user-not-partner',
|
||||
resized: true,
|
||||
stackId: null,
|
||||
thumbhash: null,
|
||||
type: AssetTypeEnum.IMAGE,
|
||||
updatedAt: DateTime(2025, 1, 1),
|
||||
visibility: AssetVisibility.public_,
|
||||
),
|
||||
]);
|
||||
|
||||
await sut.updateAlbumsV1([
|
||||
SyncAlbumV1(
|
||||
albumName: 'Test Album',
|
||||
albumThumbnailAssetId: null,
|
||||
createdAt: DateTime(2025, 1, 1),
|
||||
deletedAt: null,
|
||||
description: 'Test',
|
||||
id: 'album-1',
|
||||
isActivityEnabled: false,
|
||||
lastModifiedAssetTimestamp: DateTime(2025, 1, 1),
|
||||
order: AlbumUserRole.editor,
|
||||
ownerId: 'user-1',
|
||||
startDate: DateTime(2025, 1, 1),
|
||||
endDate: DateTime(2025, 1, 2),
|
||||
updatedAt: DateTime(2025, 1, 1),
|
||||
),
|
||||
]);
|
||||
|
||||
await sut.updateAlbumToAssetsV1([
|
||||
SyncAlbumToAssetV1(
|
||||
albumId: 'album-1',
|
||||
assetId: 'asset-in-album',
|
||||
),
|
||||
]);
|
||||
|
||||
// Verify setup
|
||||
final assetsBefore = await db.remoteAssetEntity.select().get();
|
||||
expect(assetsBefore.length, 1);
|
||||
|
||||
// Call pruneAssets()
|
||||
await sut.pruneAssets();
|
||||
|
||||
// Verify asset is NOT deleted (protected by album membership)
|
||||
final assetsAfter = await db.remoteAssetEntity.select().get();
|
||||
expect(
|
||||
assetsAfter.length,
|
||||
1,
|
||||
reason: 'Asset should NOT be pruned because it is in an album',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -234,6 +234,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
await this.initTask.reset();
|
||||
await this.#init(options);
|
||||
this.updateViewportGeometry(false);
|
||||
this.#createScrubberMonths();
|
||||
}
|
||||
|
||||
async #init(options: TimelineManagerOptions) {
|
||||
|
||||
Reference in New Issue
Block a user