mirror of
https://github.com/immich-app/immich
synced 2026-08-29 13:15:45 +00:00
Merge remote-tracking branch 'origin/main' into fix/gallery-app-view-intent-refresh
This commit is contained in:
commit
4ecaa76d93
24 changed files with 65 additions and 41 deletions
|
|
@ -1,7 +0,0 @@
|
|||
merge_queue:
|
||||
status_comments: outcomes
|
||||
|
||||
queue_rules:
|
||||
- name: default
|
||||
batch_size: 3
|
||||
batch_max_wait_time: 2 min
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue