mirror of
https://github.com/immich-app/immich.git
synced 2026-01-11 20:55:25 -08:00
Compare commits
1 Commits
main
...
perf/optim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee23794625 |
@@ -171,16 +171,8 @@ class RemoteAlbumService {
|
||||
|
||||
Future<List<RemoteAlbum>> _sortByNewestAsset(List<RemoteAlbum> albums) async {
|
||||
// map album IDs to their newest asset dates
|
||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {};
|
||||
for (final album in albums) {
|
||||
assetTimestampFutures[album.id] = _repository.getNewestAssetTimestamp(album.id);
|
||||
}
|
||||
|
||||
// await all database queries
|
||||
final entries = await Future.wait(
|
||||
assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)),
|
||||
);
|
||||
final assetTimestamps = Map.fromEntries(entries);
|
||||
final albumIds = albums.map((e) => e.id).toList();
|
||||
final assetTimestamps = await _repository.getNewestAssetTimestampForAlbums(albumIds);
|
||||
|
||||
final sorted = albums.sorted((a, b) {
|
||||
final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
@@ -193,15 +185,8 @@ class RemoteAlbumService {
|
||||
|
||||
Future<List<RemoteAlbum>> _sortByOldestAsset(List<RemoteAlbum> albums) async {
|
||||
// map album IDs to their oldest asset dates
|
||||
final Map<String, Future<DateTime?>> assetTimestampFutures = {
|
||||
for (final album in albums) album.id: _repository.getOldestAssetTimestamp(album.id),
|
||||
};
|
||||
|
||||
// await all database queries
|
||||
final entries = await Future.wait(
|
||||
assetTimestampFutures.entries.map((entry) async => MapEntry(entry.key, await entry.value)),
|
||||
);
|
||||
final assetTimestamps = Map.fromEntries(entries);
|
||||
final albumIds = albums.map((e) => e.id).toList();
|
||||
final assetTimestamps = await _repository.getOldestAssetTimestampForAlbums(albumIds);
|
||||
|
||||
final sorted = albums.sorted((a, b) {
|
||||
final aDate = assetTimestamps[a.id] ?? DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
@@ -321,26 +321,64 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
||||
}).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Future<DateTime?> getNewestAssetTimestamp(String albumId) {
|
||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
||||
..addColumns([_db.remoteAssetEntity.localDateTime.max()])
|
||||
..join([
|
||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
||||
]);
|
||||
Future<Map<String, DateTime?>> getNewestAssetTimestampForAlbums(List<String> albumIds) async {
|
||||
if (albumIds.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.max())).getSingleOrNull();
|
||||
final results = <String, DateTime?>{};
|
||||
|
||||
// Chunk calls to avoid SQLite limit (default 999 variables)
|
||||
const chunkSize = 900;
|
||||
for (var i = 0; i < albumIds.length; i += chunkSize) {
|
||||
final end = (i + chunkSize < albumIds.length) ? i + chunkSize : albumIds.length;
|
||||
final subList = albumIds.sublist(i, end);
|
||||
|
||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||
..where(_db.remoteAlbumAssetEntity.albumId.isIn(subList))
|
||||
..addColumns([_db.remoteAlbumAssetEntity.albumId, _db.remoteAssetEntity.localDateTime.max()])
|
||||
..join([
|
||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
||||
])
|
||||
..groupBy([_db.remoteAlbumAssetEntity.albumId]);
|
||||
|
||||
final rows = await query.get();
|
||||
for (final row in rows) {
|
||||
results[row.read(_db.remoteAlbumAssetEntity.albumId)!] = row.read(_db.remoteAssetEntity.localDateTime.max());
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<DateTime?> getOldestAssetTimestamp(String albumId) {
|
||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||
..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId))
|
||||
..addColumns([_db.remoteAssetEntity.localDateTime.min()])
|
||||
..join([
|
||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
||||
]);
|
||||
Future<Map<String, DateTime?>> getOldestAssetTimestampForAlbums(List<String> albumIds) async {
|
||||
if (albumIds.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return query.map((row) => row.read(_db.remoteAssetEntity.localDateTime.min())).getSingleOrNull();
|
||||
final results = <String, DateTime?>{};
|
||||
|
||||
// Chunk calls to avoid SQLite limit (default 999 variables)
|
||||
const chunkSize = 900;
|
||||
for (var i = 0; i < albumIds.length; i += chunkSize) {
|
||||
final end = (i + chunkSize < albumIds.length) ? i + chunkSize : albumIds.length;
|
||||
final subList = albumIds.sublist(i, end);
|
||||
|
||||
final query = _db.remoteAlbumAssetEntity.selectOnly()
|
||||
..where(_db.remoteAlbumAssetEntity.albumId.isIn(subList))
|
||||
..addColumns([_db.remoteAlbumAssetEntity.albumId, _db.remoteAssetEntity.localDateTime.min()])
|
||||
..join([
|
||||
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),
|
||||
])
|
||||
..groupBy([_db.remoteAlbumAssetEntity.albumId]);
|
||||
|
||||
final rows = await query.get();
|
||||
for (final row in rows) {
|
||||
results[row.read(_db.remoteAlbumAssetEntity.albumId)!] = row.read(_db.remoteAssetEntity.localDateTime.min());
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<int> getCount() {
|
||||
|
||||
@@ -18,30 +18,34 @@ void main() {
|
||||
mockAlbumApiRepo = MockDriftAlbumApiRepository();
|
||||
sut = RemoteAlbumService(mockRemoteAlbumRepo, mockAlbumApiRepo);
|
||||
|
||||
when(() => mockRemoteAlbumRepo.getNewestAssetTimestamp(any())).thenAnswer((invocation) {
|
||||
// Simulate a timestamp for the newest asset in the album
|
||||
final albumID = invocation.positionalArguments[0] as String;
|
||||
|
||||
if (albumID == '1') {
|
||||
return Future.value(DateTime(2023, 1, 1));
|
||||
} else if (albumID == '2') {
|
||||
return Future.value(DateTime(2023, 2, 1));
|
||||
when(() => mockRemoteAlbumRepo.getNewestAssetTimestampForAlbums(any())).thenAnswer((invocation) async {
|
||||
final albumIds = invocation.positionalArguments[0] as List<String>;
|
||||
final result = <String, DateTime?>{};
|
||||
for (final id in albumIds) {
|
||||
if (id == '1') {
|
||||
result[id] = DateTime(2023, 1, 1);
|
||||
} else if (id == '2') {
|
||||
result[id] = DateTime(2023, 2, 1);
|
||||
} else {
|
||||
result[id] = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
}
|
||||
}
|
||||
|
||||
return Future.value(DateTime.fromMillisecondsSinceEpoch(0));
|
||||
return result;
|
||||
});
|
||||
|
||||
when(() => mockRemoteAlbumRepo.getOldestAssetTimestamp(any())).thenAnswer((invocation) {
|
||||
// Simulate a timestamp for the oldest asset in the album
|
||||
final albumID = invocation.positionalArguments[0] as String;
|
||||
|
||||
if (albumID == '1') {
|
||||
return Future.value(DateTime(2019, 1, 1));
|
||||
} else if (albumID == '2') {
|
||||
return Future.value(DateTime(2019, 2, 1));
|
||||
when(() => mockRemoteAlbumRepo.getOldestAssetTimestampForAlbums(any())).thenAnswer((invocation) async {
|
||||
final albumIds = invocation.positionalArguments[0] as List<String>;
|
||||
final result = <String, DateTime?>{};
|
||||
for (final id in albumIds) {
|
||||
if (id == '1') {
|
||||
result[id] = DateTime(2019, 1, 1);
|
||||
} else if (id == '2') {
|
||||
result[id] = DateTime(2019, 2, 1);
|
||||
} else {
|
||||
result[id] = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
}
|
||||
}
|
||||
|
||||
return Future.value(DateTime.fromMillisecondsSinceEpoch(0));
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
|
||||
import { Label, Text } from '@immich/ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
@@ -29,21 +28,25 @@
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex h-6.5 place-items-center gap-1">
|
||||
<Label>{title}</Label>
|
||||
{#if isEdited}
|
||||
<div
|
||||
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
|
||||
class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900"
|
||||
>
|
||||
{$t('unsaved_change')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-cols-2">
|
||||
<div>
|
||||
<div class="flex h-6.5 place-items-center gap-1">
|
||||
<label class="font-medium text-primary text-sm" for={title}>
|
||||
{title}
|
||||
</label>
|
||||
{#if isEdited}
|
||||
<div
|
||||
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
|
||||
class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900"
|
||||
>
|
||||
{$t('unsaved_change')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Text size="small" color="muted">{subtitle}</Text>
|
||||
<div class="flex items-center mt-2 max-w-[300px]">
|
||||
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<Combobox label={title} hideLabel={true} {selectedOption} {options} placeholder={comboboxPlaceholder} {onSelect} />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import { defaultLang, langs } from '$lib/constants';
|
||||
import { lang } from '$lib/stores/preferences.store';
|
||||
import { getClosestAvailableLocale, langCodes } from '$lib/utils/i18n';
|
||||
import { Label, Text } from '@immich/ui';
|
||||
import { locale as i18nLocale, t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
@@ -35,14 +34,16 @@
|
||||
let closestLanguage = $derived(getClosestAvailableLocale([$lang], langCodes));
|
||||
</script>
|
||||
|
||||
<div class="max-w-[300px]">
|
||||
<div class={showSettingDescription ? 'grid grid-cols-2' : ''}>
|
||||
{#if showSettingDescription}
|
||||
<div>
|
||||
<div class="flex h-6.5 place-items-center gap-1">
|
||||
<Label>{$t('language')}</Label>
|
||||
<label class="font-medium text-primary text-sm" for={$t('language')}>
|
||||
{$t('language')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Text size="small" color="muted">{$t('language_setting_description')}</Text>
|
||||
<p class="text-sm dark:text-immich-dark-fg">{$t('language_setting_description')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
|
||||
import SettingCombobox from '$lib/components/shared-components/settings/setting-combobox.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SettingsLanguageSelector from '$lib/components/shared-components/settings/settings-language-selector.svelte';
|
||||
import { fallbackLocale, locales } from '$lib/constants';
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
@@ -14,7 +15,6 @@
|
||||
showDeleteModal,
|
||||
} from '$lib/stores/preferences.store';
|
||||
import { createDateFormatter, findLocale } from '$lib/utils';
|
||||
import { Field, Switch, Text } from '@immich/ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
@@ -59,55 +59,81 @@
|
||||
|
||||
<section class="my-4">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<div class="ms-8 mt-4 flex flex-col gap-4">
|
||||
<Field label={$t('theme_selection')} description={$t('theme_selection_description')}>
|
||||
<Switch checked={themeManager.theme.system} onCheckedChange={(checked) => themeManager.setSystem(checked)} />
|
||||
</Field>
|
||||
|
||||
<SettingsLanguageSelector showSettingDescription />
|
||||
|
||||
<Field label={$t('default_locale')} description={$t('default_locale_description')}>
|
||||
<Switch checked={$locale == 'default'} onCheckedChange={handleToggleLocaleBrowser} />
|
||||
<Text size="small" class="mt-2">{selectedDate}</Text>
|
||||
</Field>
|
||||
|
||||
{#if $locale !== 'default'}
|
||||
<SettingCombobox
|
||||
comboboxPlaceholder={$t('searching_locales')}
|
||||
{selectedOption}
|
||||
options={getAllLanguages()}
|
||||
title={$t('custom_locale')}
|
||||
subtitle={$t('custom_locale_description')}
|
||||
onSelect={(combobox) => handleLocaleChange(combobox?.value)}
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4">
|
||||
<SettingSwitch
|
||||
title={$t('theme_selection')}
|
||||
subtitle={$t('theme_selection_description')}
|
||||
checked={themeManager.theme.system}
|
||||
onToggle={(isChecked) => themeManager.setSystem(isChecked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ms-4">
|
||||
<SettingsLanguageSelector showSettingDescription />
|
||||
</div>
|
||||
|
||||
<div class="ms-4">
|
||||
<SettingSwitch
|
||||
title={$t('default_locale')}
|
||||
subtitle={$t('default_locale_description')}
|
||||
checked={$locale == 'default'}
|
||||
onToggle={handleToggleLocaleBrowser}
|
||||
>
|
||||
<p class="mt-2 dark:text-gray-400">{selectedDate}</p>
|
||||
</SettingSwitch>
|
||||
</div>
|
||||
{#if $locale !== 'default'}
|
||||
<div class="ms-4">
|
||||
<SettingCombobox
|
||||
comboboxPlaceholder={$t('searching_locales')}
|
||||
{selectedOption}
|
||||
options={getAllLanguages()}
|
||||
title={$t('custom_locale')}
|
||||
subtitle={$t('custom_locale_description')}
|
||||
onSelect={(combobox) => handleLocaleChange(combobox?.value)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Field label={$t('display_original_photos')} description={$t('display_original_photos_setting_description')}>
|
||||
<Switch bind:checked={$alwaysLoadOriginalFile} />
|
||||
</Field>
|
||||
|
||||
<Field label={$t('video_hover_setting')} description={$t('video_hover_setting_description')}>
|
||||
<Switch bind:checked={$playVideoThumbnailOnHover} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('setting_video_viewer_auto_play_title')}
|
||||
description={$t('setting_video_viewer_auto_play_subtitle')}
|
||||
>
|
||||
<Switch bind:checked={$autoPlayVideo} />
|
||||
</Field>
|
||||
|
||||
<Field label={$t('loop_videos')} description={$t('loop_videos_description')}>
|
||||
<Switch bind:checked={$loopVideo} />
|
||||
</Field>
|
||||
|
||||
<Field label={$t('play_original_video')} description={$t('play_original_video_setting_description')}>
|
||||
<Switch bind:checked={$alwaysLoadOriginalVideo} />
|
||||
</Field>
|
||||
|
||||
<Field label={$t('permanent_deletion_warning')} description={$t('permanent_deletion_warning_setting_description')}
|
||||
><Switch bind:checked={$showDeleteModal} />
|
||||
</Field>
|
||||
<div class="ms-4">
|
||||
<SettingSwitch
|
||||
title={$t('display_original_photos')}
|
||||
subtitle={$t('display_original_photos_setting_description')}
|
||||
bind:checked={$alwaysLoadOriginalFile}
|
||||
/>
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<SettingSwitch
|
||||
title={$t('video_hover_setting')}
|
||||
subtitle={$t('video_hover_setting_description')}
|
||||
bind:checked={$playVideoThumbnailOnHover}
|
||||
/>
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<SettingSwitch
|
||||
title={$t('setting_video_viewer_auto_play_title')}
|
||||
subtitle={$t('setting_video_viewer_auto_play_subtitle')}
|
||||
bind:checked={$autoPlayVideo}
|
||||
/>
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<SettingSwitch title={$t('loop_videos')} subtitle={$t('loop_videos_description')} bind:checked={$loopVideo} />
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<SettingSwitch
|
||||
title={$t('play_original_video')}
|
||||
subtitle={$t('play_original_video_setting_description')}
|
||||
bind:checked={$alwaysLoadOriginalVideo}
|
||||
/>
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<SettingSwitch
|
||||
title={$t('permanent_deletion_warning')}
|
||||
subtitle={$t('permanent_deletion_warning_setting_description')}
|
||||
bind:checked={$showDeleteModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user