fix: avoid duplicate manufacturer in camera name (#32657)
* fix(web,mobile): avoid duplicate manufacturer in camera name Some cameras (e.g. Canon) already include the manufacturer in the EXIF model, which made the details view show "Canon Canon EOS R6m2". Only prepend the make when the model does not already start with it. * refactor(web,mobile): address review feedback on camera name helper - Use toLowerCase instead of toLocaleLowerCase for the comparison - Hoist the whitespace regex to a static in the Dart helper - Match any word of the make, so "EASTMAN KODAK COMPANY" with "KODAK EASYSHARE C875" is deduplicated too - Compute the camera name once in DetailPanel --------- Co-authored-by: Daniel Dietzler <mail@ddietzler.dev> (cherry picked from commit cb2ea1e308d8a85f2f340f50b646f161d3528670)
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
import { getAlbumsByAssetQuery } from '#lib/queries/album.query.ts';
|
||||
import { createPreloadedQuery } from '#lib/queries/client.ts';
|
||||
import { getPublicConfigQuery } from '#lib/queries/config-public.query.ts';
|
||||
import { getCameraName } from '#lib/utils/camera-utils.ts';
|
||||
|
||||
const query = createPreloadedQuery(getPublicConfigQuery);
|
||||
const publicConfig = $derived(query.data);
|
||||
@@ -203,17 +204,17 @@
|
||||
|
||||
<div>
|
||||
{#if asset.exifInfo?.make || asset.exifInfo?.model}
|
||||
{const cameraName = getCameraName(asset.exifInfo.make, asset.exifInfo.model)}
|
||||
<p>
|
||||
<a
|
||||
href={Route.search({
|
||||
make: asset.exifInfo?.make ?? undefined,
|
||||
model: asset.exifInfo?.model ?? undefined,
|
||||
})}
|
||||
title="{$t('search_for')} {asset.exifInfo.make || ''} {asset.exifInfo.model || ''}"
|
||||
title="{$t('search_for')} {cameraName}"
|
||||
class="hover:text-primary"
|
||||
>
|
||||
{asset.exifInfo.make || ''}
|
||||
{asset.exifInfo.model || ''}
|
||||
{cameraName}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { getCameraName } from '#lib/utils/camera-utils.ts';
|
||||
|
||||
describe('getCameraName', () => {
|
||||
it('should not repeat the make when the model already starts with it', () => {
|
||||
expect(getCameraName('Canon', 'Canon EOS R6m2')).toBe('Canon EOS R6m2');
|
||||
expect(getCameraName('Canon', 'Canon EOS 5D Mark IV')).toBe('Canon EOS 5D Mark IV');
|
||||
});
|
||||
|
||||
it('should compare the make and model case-insensitively', () => {
|
||||
expect(getCameraName('NIKON', 'Nikon Z 6')).toBe('Nikon Z 6');
|
||||
expect(getCameraName('canon', 'CANON EOS R5')).toBe('CANON EOS R5');
|
||||
});
|
||||
|
||||
it('should compare against the first word of a multi-word make', () => {
|
||||
expect(getCameraName('NIKON CORPORATION', 'NIKON D850')).toBe('NIKON D850');
|
||||
expect(getCameraName('OLYMPUS IMAGING CORP.', 'OLYMPUS E-M1')).toBe('OLYMPUS E-M1');
|
||||
});
|
||||
|
||||
it('should match any word of a multi-word make', () => {
|
||||
expect(getCameraName('EASTMAN KODAK COMPANY', 'KODAK EASYSHARE C875')).toBe('KODAK EASYSHARE C875');
|
||||
expect(getCameraName('EASTMAN KODAK COMPANY', 'EASTMAN KODAK 1')).toBe('EASTMAN KODAK 1');
|
||||
});
|
||||
|
||||
it('should prepend the make when the model does not include it', () => {
|
||||
expect(getCameraName('SONY', 'ILCE-7M4')).toBe('SONY ILCE-7M4');
|
||||
expect(getCameraName('FUJIFILM', 'X-T5')).toBe('FUJIFILM X-T5');
|
||||
expect(getCameraName('Apple', 'iPhone 15 Pro')).toBe('Apple iPhone 15 Pro');
|
||||
});
|
||||
|
||||
it('should only treat the make as a prefix on a word boundary', () => {
|
||||
expect(getCameraName('Canon', 'Canonical X1')).toBe('Canon Canonical X1');
|
||||
});
|
||||
|
||||
it('should handle a missing make or model', () => {
|
||||
expect(getCameraName('Canon', null)).toBe('Canon');
|
||||
expect(getCameraName(undefined, 'EOS R6m2')).toBe('EOS R6m2');
|
||||
expect(getCameraName(null, null)).toBe('');
|
||||
expect(getCameraName('', '')).toBe('');
|
||||
});
|
||||
|
||||
it('should ignore surrounding whitespace', () => {
|
||||
expect(getCameraName(' Canon ', ' Canon EOS R6m2 ')).toBe('Canon EOS R6m2');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
const whitespace = /\s+/;
|
||||
|
||||
const isWordChar = (char: string | undefined) => char !== undefined && /\w/.test(char);
|
||||
|
||||
/**
|
||||
Builds the display name of a camera from its EXIF make and model.
|
||||
|
||||
Many cameras (e.g. Canon) already include the manufacturer in the model
|
||||
(`Canon EOS R6m2`), so prefixing the make again would produce "Canon Canon EOS R6m2".
|
||||
The make is only prepended when the model does not already start with it.
|
||||
Makes made up of several words are matched word by word, since the model usually
|
||||
repeats only part of them: `NIKON CORPORATION` / `NIKON D850` and
|
||||
`EASTMAN KODAK COMPANY` / `KODAK EASYSHARE C875`.
|
||||
*/
|
||||
export const getCameraName = (make?: string | null, model?: string | null): string => {
|
||||
const trimmedMake = make?.trim() ?? '';
|
||||
const trimmedModel = model?.trim() ?? '';
|
||||
|
||||
if (!trimmedMake) {
|
||||
return trimmedModel;
|
||||
}
|
||||
|
||||
if (!trimmedModel) {
|
||||
return trimmedMake;
|
||||
}
|
||||
|
||||
const lowerModel = trimmedModel.toLowerCase();
|
||||
const modelIncludesMake = trimmedMake
|
||||
.toLowerCase()
|
||||
.split(whitespace)
|
||||
.some((word) => lowerModel.startsWith(word) && !isWordChar(lowerModel[word.length]));
|
||||
|
||||
return modelIncludesMake ? trimmedModel : `${trimmedMake} ${trimmedModel}`;
|
||||
};
|
||||
Reference in New Issue
Block a user