Merge remote-tracking branch 'origin/main' into fix/gallery-app-view-intent-refresh

This commit is contained in:
Peter Ombodi 2026-08-04 12:08:57 +03:00
commit 4ecaa76d93
24 changed files with 65 additions and 41 deletions

View file

@ -1,7 +0,0 @@
merge_queue:
status_comments: outcomes
queue_rules:
- name: default
batch_size: 3
batch_max_wait_time: 2 min

View file

@ -33,6 +33,7 @@ final assetsActionProvider = Provider.family.autoDispose<AssetFilter<BaseAsset>,
null => const <BaseAsset>{},
},
}),
dependencies: [multiSelectProvider],
);
final clearSelectionProvider = Provider.family.autoDispose<VoidCallback, ActionSource>((ref, source) {
@ -41,10 +42,11 @@ final clearSelectionProvider = Provider.family.autoDispose<VoidCallback, ActionS
}
return () {};
});
}, dependencies: [multiSelectProvider]);
final ownedAssetsActionProvider = Provider.family.autoDispose<AssetFilter<RemoteAsset>, ActionSource>(
(ref, source) => ref.watch(assetsActionProvider(source)).owned(ref.watch(authUserProvider).id),
dependencies: [assetsActionProvider],
);
abstract class AssetActionBuilder extends ActionBuilder {

View file

@ -22,7 +22,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref,
.map((asset) => asset.id)
.toList(growable: false);
return assetIds.isEmpty ? null : (shouldArchive: shouldArchive, assetIds: assetIds);
});
}, dependencies: [ownedAssetsActionProvider]);
class ArchiveAction extends AssetActionBuilder {
const ArchiveAction({required super.source});

View file

@ -40,7 +40,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref,
final trash = trashEnabled && !ownedRemote.every((asset) => asset.isTrashed || asset.isLocked);
return (localIds: localIds, remoteIds: ownedRemote.map((asset) => asset.id).toList(growable: false), trash: trash);
});
}, dependencies: [assetsActionProvider]);
class DeleteAction extends AssetActionBuilder {
const DeleteAction({required super.source});
@ -149,7 +149,7 @@ final _cleanupStateProvider = Provider.family.autoDispose<List<String>?, ActionS
final assets = ref.watch(assetsActionProvider(source));
final assetIds = assets.backedUp().map((asset) => asset.localId).nonNulls.toList(growable: false);
return assetIds.isEmpty ? null : assetIds;
});
}, dependencies: [assetsActionProvider]);
class CleanupLocalAction extends AssetActionBuilder {
const CleanupLocalAction({required super.source});

View file

@ -14,7 +14,7 @@ final _stateProvider = Provider.family.autoDispose<List<RemoteAsset>?, ActionSou
final assets = ref.watch(assetsActionProvider(source));
final remote = assets.remote().toList(growable: false);
return remote.isEmpty ? null : remote;
});
}, dependencies: [assetsActionProvider]);
class DownloadAction extends AssetActionBuilder {
const DownloadAction({required super.source});

View file

@ -28,7 +28,7 @@ final _stateProvider = Provider.family.autoDispose<RemoteAsset?, ActionSource>((
final assets = ref.watch(ownedAssetsActionProvider(source));
return assets.where((asset) => asset.isEditable).singleOrNull;
});
}, dependencies: [ownedAssetsActionProvider]);
class EditAssetAction extends AssetActionBuilder {
const EditAssetAction({required super.source});

View file

@ -21,7 +21,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref,
}
return (assetIds: assets.map((asset) => asset.id).toList(growable: false), origin: assets.singleOrNull);
});
}, dependencies: [ownedAssetsActionProvider]);
class EditDateTimeAction extends AssetActionBuilder {
const EditDateTimeAction({required super.source});

View file

@ -21,7 +21,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref,
}
return (assetIds: assets.map((asset) => asset.id).toList(growable: false), origin: assets.singleOrNull);
});
}, dependencies: [ownedAssetsActionProvider]);
class EditLocationAction extends AssetActionBuilder {
const EditLocationAction({required super.source});

View file

@ -18,7 +18,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref,
final shouldFavorite = assets.favorite(isFavorite: false).isNotEmpty;
final assetIds = assets.favorite(isFavorite: !shouldFavorite).map((asset) => asset.id).toList(growable: false);
return (shouldFavorite: shouldFavorite, assetIds: assetIds);
});
}, dependencies: [ownedAssetsActionProvider]);
class FavoriteAction extends AssetActionBuilder {
const FavoriteAction({required super.source});

View file

@ -23,7 +23,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref,
// Only locking has an on-device copy to clean up; unlocking leaves the device alone.
localIds: shouldLock ? targets.map((asset) => asset.localId).nonNulls.toList(growable: false) : const [],
);
});
}, dependencies: [ownedAssetsActionProvider]);
class LockAction extends AssetActionBuilder {
const LockAction({required super.source});

View file

@ -11,7 +11,7 @@ final _stateProvider = Provider.family.autoDispose<List<String>?, ActionSource>(
final assets = ref.watch(assetsActionProvider(source));
final assetIds = assets.remote().map((asset) => asset.id).toList(growable: false);
return assetIds.isEmpty ? null : assetIds;
});
}, dependencies: [assetsActionProvider]);
class RemoveFromAlbumAction extends AssetActionBuilder {
final String albumId;

View file

@ -11,7 +11,7 @@ final _stateProvider = Provider.family.autoDispose<List<String>?, ActionSource>(
final assets = ref.watch(ownedAssetsActionProvider(source));
final assetIds = assets.trashed().map((asset) => asset.id).toList(growable: false);
return assetIds.isEmpty ? null : assetIds;
});
}, dependencies: [ownedAssetsActionProvider]);
class RestoreAction extends AssetActionBuilder {
const RestoreAction({required super.source});

View file

@ -11,7 +11,7 @@ import 'package:immich_mobile/utils/error_handler.dart';
final _stateProvider = Provider.family.autoDispose<String?, ActionSource>((ref, source) {
final assets = ref.watch(assetsActionProvider(source));
return assets.remote().map((asset) => asset.id).singleOrNull;
});
}, dependencies: [assetsActionProvider]);
class SetAlbumCoverAction extends AssetActionBuilder {
final String albumId;

View file

@ -16,7 +16,7 @@ final _stateProvider = Provider.family.autoDispose<List<BaseAsset>?, ActionSourc
final assets = ref.watch(assetsActionProvider(source));
final shareable = assets.toList(growable: false);
return shareable.isEmpty ? null : shareable;
});
}, dependencies: [assetsActionProvider]);
class ShareAction extends AssetActionBuilder {
const ShareAction({required super.source});

View file

@ -12,7 +12,7 @@ final _stateProvider = Provider.family.autoDispose<List<String>?, ActionSource>(
final assets = ref.watch(assetsActionProvider(source));
final remoteIds = assets.remote().map((asset) => asset.id).toList(growable: false);
return remoteIds.isEmpty ? null : remoteIds;
});
}, dependencies: [assetsActionProvider]);
class ShareLinkAction extends AssetActionBuilder {
const ShareLinkAction({required super.source});

View file

@ -23,7 +23,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref,
assetIds: assets.map((asset) => asset.id).toList(growable: false),
stackIds: assets.map((asset) => asset.stackId).nonNulls.toList(growable: false),
);
});
}, dependencies: [ownedAssetsActionProvider]);
class StackAction extends AssetActionBuilder {
const StackAction({required super.source});

View file

@ -21,7 +21,7 @@ final _stateProvider = Provider.family.autoDispose<List<String>?, ActionSource>(
final assets = ref.watch(ownedAssetsActionProvider(source));
final assetIds = assets.map((asset) => asset.id).toList(growable: false);
return assetIds.isEmpty ? null : assetIds;
});
}, dependencies: [ownedAssetsActionProvider]);
class TagAction extends AssetActionBuilder {
const TagAction({required super.source});

View file

@ -17,7 +17,7 @@ final _stateProvider = Provider.family.autoDispose<List<LocalAsset>?, ActionSour
final assets = ref.watch(assetsActionProvider(source));
final local = assets.backedUp(isBackedUp: false).local().toList(growable: false);
return local.isEmpty ? null : local;
});
}, dependencies: [assetsActionProvider]);
class UploadAction extends AssetActionBuilder {
final bool showProgress;

View file

@ -103,7 +103,8 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
socket.on('AssetUploadReadyV2', _handleSyncAssetUploadReadyV2);
socket.on('AssetEditReadyV1', _handleSyncAssetEditReadyV1);
socket.on('AssetEditReadyV2', _handleSyncAssetEditReadyV2);
socket.on('on_album_update', _handleAlbumUpdate);
socket.on('on_album_update', _handleRemoteChange);
socket.on('on_asset_stack_update', _handleRemoteChange);
socket.on('on_config_update', _handleOnConfigUpdate);
socket.on('on_new_release', _handleReleaseUpdates);
} catch (e) {
@ -185,7 +186,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV1(data));
}
void _handleAlbumUpdate(dynamic _) {
void _handleRemoteChange(dynamic _) {
unawaited(_ref.read(backgroundSyncProvider).syncRemote());
}

View file

@ -101,15 +101,18 @@ extension PumpPresentationWidget on WidgetTester {
useFallbackTranslations: true,
assetLoader: const CodegenLoader(),
child: ProviderScope(
overrides: [...context.overrides, ...overrides],
overrides: context.overrides,
child: Builder(
builder: (context) => MaterialApp(
debugShowCheckedModeBanner: false,
scaffoldMessengerKey: scaffoldMessengerKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
home: Scaffold(body: widget),
builder: (context) => ProviderScope(
overrides: overrides,
child: MaterialApp(
debugShowCheckedModeBanner: false,
scaffoldMessengerKey: scaffoldMessengerKey,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
home: Scaffold(body: widget),
),
),
),
),

View file

@ -20,7 +20,8 @@ type TagsWithWrongTypes =
| 'TagsList'
| 'Keywords'
| 'HierarchicalSubject'
| 'ISO';
| 'ISO'
| 'LensModel';
export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
ContentIdentifier?: string;
@ -43,6 +44,9 @@ export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
Description?: StringOrNumber;
ImageDescription?: StringOrNumber;
// Apparently LensModel can also be a float: https://github.com/immich-app/immich/issues/30492
LensModel?: StringOrNumber;
// Extended properties for image regions, such as faces
RegionInfo?: {
AppliedToDimensions: {

View file

@ -117,7 +117,9 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
};
const getLensModel = (exifTags: ImmichTags): string | null => {
const lensModel = (exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '').trim();
const lensModel = String(
exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '',
).trim();
if (lensModel === '----') {
return null;
}

View file

@ -152,4 +152,23 @@ describe(MetadataService.name, () => {
).resolves.toEqual({ dateTimeOriginal: new Date('4260-03-05T04:04:12.000Z') });
});
});
it('should handle float lens models (#30492)', async () => {
const { sut, ctx } = setup();
ctx.getMock(EventRepository).emit.mockResolvedValue();
const { filePath } = await createTestFile({ LensModel: 1.8 });
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ originalPath: filePath, ownerId: user.id });
await ctx.newExif({ assetId: asset.id, description: '' });
await sut.handleMetadataExtraction({ id: asset.id });
await expect(
ctx.database
.selectFrom('asset_exif')
.where('assetId', '=', asset.id)
.select('lensModel')
.executeTakeFirstOrThrow(),
).resolves.toEqual({ lensModel: '1.8' });
});
});

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { appStoreBadge, BasicModal, fdroidBadge, playStoreBadge } from '@immich/ui';
import { appStoreBadge, BasicModal, Constants, fdroidBadge, playStoreBadge } from '@immich/ui';
import { t } from 'svelte-i18n';
interface Props {
onClose: () => void;
@ -9,15 +9,15 @@
<BasicModal title={$t('app_download_links')} size="tiny" {onClose}>
<div class="mx-auto flex max-w-50 flex-col gap-4">
<a href="https://play.google.com/store/apps/details?id=app.alextran.immich" target="_blank" id="play-store-link">
<a href={Constants.Get.Android} target="_blank" id="play-store-link">
<img class="mt-2 w-full" alt="Get it on Google Play" src={playStoreBadge} />
</a>
<a href="https://apps.apple.com/us/app/immich/id1613945652" target="_blank" id="app-store-link">
<a href={Constants.Get.iOS} target="_blank" id="app-store-link">
<img class="mt-2 w-full" alt="Download on the App Store" src={appStoreBadge} />
</a>
<a href="https://f-droid.org/packages/app.alextran.immich/" target="_blank" id="fdroid-link">
<a href={Constants.Get.FDroid} target="_blank" id="fdroid-link">
<img class="mt-2 w-full" alt="Get it on F-Droid" src={fdroidBadge} />
</a>
</div>