Merge branch 'main' into feat-no-thumbhash-cache
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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('-');
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user