Compare commits

...

2 Commits

Author SHA1 Message Date
Alex
856894d581 feat(mobile): 2026 font 2026-01-12 10:23:50 -06:00
Daniel Dietzler
5e3f5f2b55 fix: unlock properties after successful sidecar write (#25168) 2026-01-12 14:01:38 +01:00
30 changed files with 130 additions and 31 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -51,4 +51,4 @@ const Map<String, Locale> locales = {
const String translationsPath = 'assets/i18n';
const List<Locale> localesNotSupportedByOverpass = [Locale('el', 'GR'), Locale('sr', 'Cyrl')];
const List<Locale> localesNotSupportedByAppFont = [Locale('el', 'GR'), Locale('sr', 'Cyrl')];

View File

@@ -100,7 +100,7 @@ class AppLogPage extends HookConsumerWidget {
minLeadingWidth: 10,
title: Text(
truncateLogMessage(logMessage.message, 4),
style: TextStyle(fontSize: 14.0, color: context.colorScheme.onSurface, fontFamily: "Inconsolata"),
style: TextStyle(fontSize: 14.0, color: context.colorScheme.onSurface, fontFamily: "GoogleSansCode"),
),
subtitle: Text(
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.logger}",

View File

@@ -57,7 +57,7 @@ class AppLogDetailPage extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0),
child: SelectableText(
text,
style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "Inconsolata"),
style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "GoogleSansCode"),
),
),
),
@@ -88,7 +88,7 @@ class AppLogDetailPage extends HookConsumerWidget {
padding: const EdgeInsets.all(8.0),
child: SelectableText(
logger.toString(),
style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "Inconsolata"),
style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "GoogleSansCode"),
),
),
),

View File

@@ -234,7 +234,7 @@ class FolderPath extends StatelessWidget {
Text(
currentFolder.path,
style: TextStyle(
fontFamily: 'Inconsolata',
fontFamily: 'GoogleSansCode',
fontWeight: FontWeight.bold,
fontSize: 14,
color: context.colorScheme.onSurface.withAlpha(175),

View File

@@ -41,7 +41,7 @@ class LoginPage extends HookConsumerWidget {
style: TextStyle(
color: context.colorScheme.onSurfaceSecondary,
fontWeight: FontWeight.bold,
fontFamily: "Inconsolata",
fontFamily: "GoogleSansCode",
),
),
const Text(' '),
@@ -51,7 +51,7 @@ class LoginPage extends HookConsumerWidget {
style: TextStyle(
color: context.primaryColor,
fontWeight: FontWeight.bold,
fontFamily: "Inconsolata",
fontFamily: "GoogleSansCode",
),
),
onTap: () {

View File

@@ -450,7 +450,7 @@ class _SegmentWidget extends StatelessWidget {
alignment: Alignment.center,
child: Text(
_segment.date.year.toString(),
style: context.textTheme.labelMedium?.copyWith(fontFamily: "OverpassMono", fontWeight: FontWeight.w600),
style: context.textTheme.labelMedium?.copyWith(fontFamily: "GoogleSansCode", fontWeight: FontWeight.w600),
),
),
),

View File

@@ -147,9 +147,9 @@ ImmichTheme decolorizeSurfaces({required ImmichTheme theme}) {
}
String? _getFontFamilyFromLocale(Locale locale) {
if (localesNotSupportedByOverpass.contains(locale)) {
if (localesNotSupportedByAppFont.contains(locale)) {
// Let Flutter use the default font
return null;
}
return 'Overpass';
return 'GoogleSans';
}

View File

@@ -58,7 +58,7 @@ class AdvancedBottomSheet extends HookConsumerWidget {
style: const TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
fontFamily: "Inconsolata",
fontFamily: "GoogleSansCode",
),
showCursor: true,
),

View File

@@ -36,7 +36,7 @@ class BackupUploadProgressBar extends ConsumerWidget {
),
Text(
" ${uploadProgress.toStringAsFixed(0)}%",
style: const TextStyle(fontSize: 12, fontFamily: "OverpassMono"),
style: const TextStyle(fontSize: 12, fontFamily: "GoogleSansCode"),
),
],
),

View File

@@ -26,10 +26,10 @@ class BackupUploadStats extends ConsumerWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(uploadFileProgress, style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono")),
Text(uploadFileProgress, style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode")),
Text(
_formatUploadFileSpeed(uploadFileSpeed),
style: const TextStyle(fontSize: 10, fontFamily: "OverpassMono"),
style: const TextStyle(fontSize: 10, fontFamily: "GoogleSansCode"),
),
],
),

View File

@@ -43,7 +43,7 @@ class PinInput extends StatelessWidget {
final defaultPinTheme = PinTheme(
width: getPinSize().width,
height: getPinSize().height,
textStyle: TextStyle(fontSize: 24, color: context.colorScheme.onSurface, fontFamily: 'Overpass Mono'),
textStyle: TextStyle(fontSize: 24, color: context.colorScheme.onSurface, fontFamily: 'GoogleSansCode'),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(19)),
border: Border.all(color: context.colorScheme.surfaceBright),

View File

@@ -50,7 +50,7 @@ class EntityCountTile extends StatelessWidget {
const Spacer(),
RichText(
text: TextSpan(
style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600),
style: const TextStyle(fontSize: 18, fontFamily: 'GoogleSansCode', fontWeight: FontWeight.w600),
children: [
TextSpan(
text: zeroPadding(count, maxDigits),

View File

@@ -117,7 +117,7 @@ class EndpointInputState extends ConsumerState<EndpointInput> {
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: validateUrl,
keyboardType: TextInputType.url,
style: const TextStyle(fontFamily: 'Inconsolata', fontWeight: FontWeight.w600, fontSize: 14),
style: const TextStyle(fontFamily: 'GoogleSansCode', fontWeight: FontWeight.w600, fontSize: 14),
decoration: InputDecoration(
hintText: 'http(s)://immich.domain.com',
contentPadding: const EdgeInsets.all(16),

View File

@@ -155,7 +155,7 @@ class LocalNetworkPreference extends HookConsumerWidget {
style: context.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100),
fontFamily: 'Inconsolata',
fontFamily: 'GoogleSansCode',
),
),
trailing: IconButton(
@@ -175,7 +175,7 @@ class LocalNetworkPreference extends HookConsumerWidget {
style: context.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
color: enabled ? context.primaryColor : context.colorScheme.onSurface.withAlpha(100),
fontFamily: 'Inconsolata',
fontFamily: 'GoogleSansCode',
),
),
trailing: IconButton(

View File

@@ -110,7 +110,7 @@ class NetworkingSettings extends HookConsumerWidget {
currentEndpoint ?? "--",
style: TextStyle(
fontSize: 16,
fontFamily: 'Inconsolata',
fontFamily: 'GoogleSansCode',
fontWeight: FontWeight.bold,
color: context.primaryColor,
),

View File

@@ -127,24 +127,26 @@ flutter:
assets:
- assets/
fonts:
- family: Inconsolata
- family: GoogleSans
fonts:
- asset: fonts/Inconsolata-Regular.ttf
- family: Overpass
fonts:
- asset: fonts/overpass/Overpass-Regular.ttf
- asset: fonts/GoogleSans/GoogleSans-Regular.ttf
weight: 400
- asset: fonts/overpass/Overpass-Italic.ttf
- asset: fonts/GoogleSans/GoogleSans-Italic.ttf
style: italic
- asset: fonts/overpass/Overpass-Medium.ttf
- asset: fonts/GoogleSans/GoogleSans-Medium.ttf
weight: 500
- asset: fonts/overpass/Overpass-SemiBold.ttf
- asset: fonts/GoogleSans/GoogleSans-SemiBold.ttf
weight: 600
- asset: fonts/overpass/Overpass-Bold.ttf
- asset: fonts/GoogleSans/GoogleSans-Bold.ttf
weight: 700
- family: OverpassMono
- family: GoogleSansCode
fonts:
- asset: fonts/overpass/OverpassMono.ttf
- asset: fonts/GoogleSansCode/GoogleSansCode-Regular.ttf
weight: 400
- asset: fonts/GoogleSansCode/GoogleSansCode-Medium.ttf
weight: 500
- asset: fonts/GoogleSansCode/GoogleSansCode-SemiBold.ttf
weight: 600
flutter_launcher_icons:
image_path_android: 'assets/immich-logo.png'
adaptive_icon_background: '#ffffff'

View File

@@ -49,6 +49,23 @@ returning
"dateTimeOriginal",
"timeZone"
-- AssetRepository.unlockProperties
update "asset_exif"
set
"lockedProperties" = nullif(
array(
select distinct
property
from
unnest("asset_exif"."lockedProperties") property
where
not property = any ($1)
),
'{}'
)
where
"assetId" = $2
-- AssetRepository.getMetadata
select
"key",

View File

@@ -223,6 +223,17 @@ export class AssetRepository {
.execute();
}
@GenerateSql({ params: [DummyValue.UUID, ['description']] })
unlockProperties(assetId: string, properties: LockableProperty[]) {
return this.db
.updateTable('asset_exif')
.where('assetId', '=', assetId)
.set((eb) => ({
lockedProperties: sql`nullif(array(select distinct property from unnest(${eb.ref('asset_exif.lockedProperties')}) property where not property = any(${properties})), '{}')`,
}))
.execute();
}
async upsertJobStatus(...jobStatus: Insertable<AssetJobStatusTable>[]): Promise<void> {
if (jobStatus.length === 0) {
return;

View File

@@ -1758,6 +1758,12 @@ describe(MetadataService.name, () => {
GPSLatitude: gps,
GPSLongitude: gps,
});
expect(mocks.asset.unlockProperties).toHaveBeenCalledWith(asset.id, [
'description',
'latitude',
'longitude',
'dateTimeOriginal',
]);
});
});

View File

@@ -461,6 +461,8 @@ export class MetadataService extends BaseService {
await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Sidecar, path: sidecarPath });
}
await this.assetRepository.unlockProperties(asset.id, lockedProperties);
return JobStatus.Success;
}

View File

@@ -87,4 +87,64 @@ describe(AssetRepository.name, () => {
).resolves.toEqual({ lockedProperties: ['description', 'dateTimeOriginal'] });
});
});
describe('unlockProperties', () => {
it('should unlock one property', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({
assetId: asset.id,
dateTimeOriginal: '2023-11-19T18:11:00',
lockedProperties: ['dateTimeOriginal', 'description'],
});
await expect(
ctx.database
.selectFrom('asset_exif')
.select('lockedProperties')
.where('assetId', '=', asset.id)
.executeTakeFirstOrThrow(),
).resolves.toEqual({ lockedProperties: ['dateTimeOriginal', 'description'] });
await sut.unlockProperties(asset.id, ['dateTimeOriginal']);
await expect(
ctx.database
.selectFrom('asset_exif')
.select('lockedProperties')
.where('assetId', '=', asset.id)
.executeTakeFirstOrThrow(),
).resolves.toEqual({ lockedProperties: ['description'] });
});
it('should unlock all properties', async () => {
const { ctx, sut } = setup();
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({
assetId: asset.id,
dateTimeOriginal: '2023-11-19T18:11:00',
lockedProperties: ['dateTimeOriginal', 'description'],
});
await expect(
ctx.database
.selectFrom('asset_exif')
.select('lockedProperties')
.where('assetId', '=', asset.id)
.executeTakeFirstOrThrow(),
).resolves.toEqual({ lockedProperties: ['dateTimeOriginal', 'description'] });
await sut.unlockProperties(asset.id, ['description', 'dateTimeOriginal']);
await expect(
ctx.database
.selectFrom('asset_exif')
.select('lockedProperties')
.where('assetId', '=', asset.id)
.executeTakeFirstOrThrow(),
).resolves.toEqual({ lockedProperties: null });
});
});
});

View File

@@ -9,6 +9,7 @@ export const newAssetRepositoryMock = (): Mocked<RepositoryInterface<AssetReposi
upsertExif: vitest.fn(),
updateAllExif: vitest.fn(),
updateDateTimeOriginal: vitest.fn().mockResolvedValue([]),
unlockProperties: vitest.fn().mockResolvedValue([]),
upsertJobStatus: vitest.fn(),
getForCopy: vitest.fn(),
getByDayOfYear: vitest.fn(),