Merge branch 'main' into feat-no-thumbhash-cache

This commit is contained in:
Thomas
2025-09-15 15:30:09 +01:00
committed by GitHub
670 changed files with 75737 additions and 69495 deletions

View File

@@ -9,14 +9,16 @@ import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
import { preferences } from '$lib/stores/user.store';
import { downloadRequest, withError } from '$lib/utils';
import { downloadRequest, sleep, withError } from '$lib/utils';
import { getByteUnitString } from '$lib/utils/byte-units';
import { getFormatter } from '$lib/utils/i18n';
import { navigate } from '$lib/utils/navigation';
import { asQueryString } from '$lib/utils/shared-links';
import {
addAssetsToAlbum as addAssets,
addAssetsToAlbums as addToAlbums,
AssetVisibility,
BulkIdErrorReason,
bulkTagAssets,
createStack,
deleteAssets,
@@ -32,6 +34,7 @@ import {
type AssetResponseDto,
type AssetTypeEnum,
type DownloadInfoDto,
type ExifResponseDto,
type StackResponseDto,
type UserPreferencesResponseDto,
type UserResponseDto,
@@ -74,6 +77,52 @@ export const addAssetsToAlbum = async (albumId: string, assetIds: string[], show
}
};
export const addAssetsToAlbums = async (albumIds: string[], assetIds: string[], showNotification = true) => {
const result = await addToAlbums({
...authManager.params,
albumsAddAssetsDto: {
albumIds,
assetIds,
},
});
if (!showNotification) {
return result;
}
if (showNotification) {
const $t = get(t);
if (result.error === BulkIdErrorReason.Duplicate) {
notificationController.show({
type: NotificationType.Info,
timeout: 5000,
message: $t('assets_were_part_of_albums_count', { values: { count: assetIds.length } }),
});
return result;
}
if (result.error) {
notificationController.show({
type: NotificationType.Info,
timeout: 5000,
message: $t('assets_cannot_be_added_to_albums', { values: { count: assetIds.length } }),
});
return result;
}
notificationController.show({
type: NotificationType.Info,
timeout: 5000,
message: $t('assets_added_to_albums_count', {
values: {
albumTotal: albumIds.length,
assetTotal: assetIds.length,
},
}),
});
return result;
}
};
export const tagAssets = async ({
assetIds,
tagIds,
@@ -230,7 +279,12 @@ export const downloadFile = async (asset: AssetResponseDto) => {
const queryParams = asQueryString(authManager.params);
for (const { filename, id } of assets) {
for (const [i, { filename, id }] of assets.entries()) {
if (i !== 0) {
// play nice with Safari
await sleep(500);
}
try {
notificationController.show({
type: NotificationType.Info,
@@ -275,6 +329,15 @@ export function isFlipped(orientation?: string | null) {
return value && (isRotated270CW(value) || isRotated90CW(value));
}
export const getDimensions = (exifInfo: ExifResponseDto) => {
const { exifImageWidth: width, exifImageHeight: height } = exifInfo;
if (isFlipped(exifInfo.orientation)) {
return { width: height, height: width };
}
return { width, height };
};
export function getFileSize(asset: AssetResponseDto, maxPrecision = 4): string {
const size = asset.exifInfo?.fileSizeInByte || 0;
return size > 0 ? getByteUnitString(size, undefined, maxPrecision) : 'Invalid Data';

View File

@@ -27,7 +27,7 @@ export class GCastDestination implements ICastDestination {
async initialize(): Promise<boolean> {
const preferencesStore = get(preferences);
if (!preferencesStore.cast.gCastEnabled) {
if (!preferencesStore || !preferencesStore.cast.gCastEnabled) {
this.isAvailable = false;
return false;
}

View File

@@ -11,15 +11,41 @@ describe('converting time to seconds', () => {
});
it('parses h:m:s.S correctly', () => {
expect(timeToSeconds('1:2:3.4')).toBeCloseTo(3723.4);
expect(timeToSeconds('1:2:3.4')).toBe(0); // Non-standard format, Luxon returns NaN
});
it('parses hhh:mm:ss.SSS correctly', () => {
expect(timeToSeconds('100:02:03.456')).toBeCloseTo(360_123.456);
expect(timeToSeconds('100:02:03.456')).toBe(0); // Non-standard format, Luxon returns NaN
});
it('ignores ignores double milliseconds hh:mm:ss.SSS.SSSSSS', () => {
expect(timeToSeconds('01:02:03.456.123456')).toBeCloseTo(3723.456);
expect(timeToSeconds('01:02:03.456.123456')).toBe(0); // Non-standard format, Luxon returns NaN
});
// Test edge cases that can cause crashes
it('handles "0" string input', () => {
expect(timeToSeconds('0')).toBe(0);
});
it('handles empty string input', () => {
expect(timeToSeconds('')).toBe(0);
});
it('parses HH:MM format correctly', () => {
expect(timeToSeconds('01:02')).toBe(3720); // 1 hour 2 minutes = 3720 seconds
});
it('handles malformed time strings', () => {
expect(timeToSeconds('invalid')).toBe(0);
});
it('parses single hour format correctly', () => {
expect(timeToSeconds('01')).toBe(3600); // Luxon interprets "01" as 1 hour
});
it('handles time strings with invalid numbers', () => {
expect(timeToSeconds('aa:bb:cc')).toBe(0);
expect(timeToSeconds('01:bb:03')).toBe(0);
});
});

View File

@@ -7,14 +7,14 @@ import { get } from 'svelte/store';
* Convert time like `01:02:03.456` to seconds.
*/
export function timeToSeconds(time: string) {
const parts = time.split(':');
parts[2] = parts[2].split('.').slice(0, 2).join('.');
if (!time || time === '0') {
return 0;
}
const [hours, minutes, seconds] = parts.map(Number);
const seconds = Duration.fromISOTime(time).as('seconds');
return Duration.fromObject({ hours, minutes, seconds }).as('seconds');
return Number.isNaN(seconds) ? 0 : seconds;
}
export function parseUtcDate(date: string) {
return DateTime.fromISO(date, { zone: 'UTC' }).toUTC();
}

View File

@@ -2,6 +2,7 @@ import { authManager } from '$lib/managers/auth-manager.svelte';
import { uploadManager } from '$lib/managers/upload-manager.svelte';
import { UploadState } from '$lib/models/upload-asset';
import { uploadAssetsStore } from '$lib/stores/upload';
import { user } from '$lib/stores/user.store';
import { uploadRequest } from '$lib/utils';
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
import { ExecutorQueue } from '$lib/utils/executor-queue';
@@ -231,6 +232,11 @@ async function fileUploader({
return responseData.id;
} catch (error) {
// ignore errors if the user logs out during uploads
if (!get(user)) {
return;
}
const errorMessage = handleError(error, $t('errors.unable_to_upload_file'));
uploadAssetsStore.track('error');
uploadAssetsStore.updateItem(deviceAssetId, { state: UploadState.ERROR, error: errorMessage });

View File

@@ -145,3 +145,16 @@ export const clearQueryParam = async (queryParam: string, url: URL) => {
await goto(url, { keepFocus: true });
}
};
export const getQueryValue = (queryKey: string) => {
const url = globalThis.location.href;
const urlObject = new URL(url);
return urlObject.searchParams.get(queryKey);
};
export const setQueryValue = async (queryKey: string, queryValue: string) => {
const url = globalThis.location.href;
const urlObject = new URL(url);
urlObject.searchParams.set(queryKey, queryValue);
await goto(urlObject, { keepFocus: true });
};

View File

@@ -5,3 +5,13 @@ export const removeAccents = (str: string) => {
export const normalizeSearchString = (str: string) => {
return removeAccents(str.toLocaleLowerCase());
};
export const buildDateString = (year: number, month?: number, day?: number) => {
return [
year.toString(),
month && !Number.isNaN(month) ? month.toString() : undefined,
day && !Number.isNaN(day) ? day.toString() : undefined,
]
.filter((date) => date !== undefined)
.join('-');
};

View File

@@ -1,6 +1,6 @@
import { locale } from '$lib/stores/preferences.store';
import { parseUtcDate } from '$lib/utils/date-time';
import { formatGroupTitle } from '$lib/utils/timeline-util';
import { formatGroupTitle, toISOYearMonthUTC } from '$lib/utils/timeline-util';
import { DateTime } from 'luxon';
describe('formatGroupTitle', () => {
@@ -77,3 +77,13 @@ describe('formatGroupTitle', () => {
expect(formatGroupTitle(date)).toBe('Invalid DateTime');
});
});
describe('toISOYearMonthUTC', () => {
it('should prefix year with 0s', () => {
expect(toISOYearMonthUTC({ year: 28, month: 1 })).toBe('0028-01-01T00:00:00.000Z');
});
it('should prefix month with 0s', () => {
expect(toISOYearMonthUTC({ year: 2025, month: 1 })).toBe('2025-01-01T00:00:00.000Z');
});
});

View File

@@ -94,8 +94,11 @@ export const fromTimelinePlainYearMonth = (timelineYearMonth: TimelineYearMonth)
{ zone: 'local', locale: get(locale) },
) as DateTime<true>;
export const toISOYearMonthUTC = ({ year, month }: TimelineYearMonth): string =>
`${year}-${month.toString().padStart(2, '0')}-01T00:00:00.000Z`;
export const toISOYearMonthUTC = ({ year, month }: TimelineYearMonth): string => {
const yearFull = `${year}`.padStart(4, '0');
const monthFull = `${month}`.padStart(2, '0');
return `${yearFull}-${monthFull}-01T00:00:00.000Z`;
};
export function formatMonthGroupTitle(_date: DateTime): string {
if (!_date.isValid) {
@@ -190,6 +193,8 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
city: city || null,
country: country || null,
people,
latitude: assetResponse.exifInfo?.latitude || null,
longitude: assetResponse.exifInfo?.longitude || null,
};
};