This commit is contained in:
mertalev 2025-05-06 21:34:59 -04:00 committed by Min Idzelis
parent 94d2025718
commit 5db3ed8412
5 changed files with 85 additions and 88 deletions

View File

@ -402,7 +402,8 @@
if (laterAsset) { if (laterAsset) {
const preloadAsset = await assetStore.getLaterAsset(laterAsset); const preloadAsset = await assetStore.getLaterAsset(laterAsset);
assetViewingStore.setAsset(laterAsset, preloadAsset ? [preloadAsset] : []); const asset = await getAssetInfo({ id: laterAsset.id, key: authManager.key });
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
await navigate({ targetRoute: 'current', assetId: laterAsset.id }); await navigate({ targetRoute: 'current', assetId: laterAsset.id });
} }
@ -413,7 +414,8 @@
const earlierAsset = await assetStore.getEarlierAsset($viewingAsset); const earlierAsset = await assetStore.getEarlierAsset($viewingAsset);
if (earlierAsset) { if (earlierAsset) {
const preloadAsset = await assetStore.getEarlierAsset(earlierAsset); const preloadAsset = await assetStore.getEarlierAsset(earlierAsset);
assetViewingStore.setAsset(earlierAsset, preloadAsset ? [preloadAsset] : []); const asset = await getAssetInfo({ id: earlierAsset.id, key: authManager.key });
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
await navigate({ targetRoute: 'current', assetId: earlierAsset.id }); await navigate({ targetRoute: 'current', assetId: earlierAsset.id });
} }
@ -424,9 +426,8 @@
const randomAsset = await assetStore.getRandomAsset(); const randomAsset = await assetStore.getRandomAsset();
if (randomAsset) { if (randomAsset) {
const preloadAsset = await assetStore.getNextAsset(randomAsset);
const asset = await getAssetInfo({ id: randomAsset.id, key: authManager.key }); const asset = await getAssetInfo({ id: randomAsset.id, key: authManager.key });
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []); assetViewingStore.setAsset(asset);
await navigate({ targetRoute: 'current', assetId: randomAsset.id }); await navigate({ targetRoute: 'current', assetId: randomAsset.id });
return asset; return asset;
} }
@ -748,8 +749,8 @@
timezoneInput={false} timezoneInput={false}
onConfirm={async (dateString: string) => { onConfirm={async (dateString: string) => {
isShowSelectDate = false; isShowSelectDate = false;
const date = DateTime.fromISO(dateString).toUTC();
const asset = await assetStore.getClosestAssetToDate(date); const asset = await assetStore.getClosestAssetToDate(new Date(dateString));
if (asset) { if (asset) {
await setFocusAsset(asset); await setFocusAsset(asset);
} }

View File

@ -8,7 +8,7 @@ import {
type CommonLayoutOptions, type CommonLayoutOptions,
type CommonPosition, type CommonPosition,
} from '$lib/utils/layout-utils'; } from '$lib/utils/layout-utils';
import { formatDateGroupTitle, toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
import { TUNABLES } from '$lib/utils/tunables'; import { TUNABLES } from '$lib/utils/tunables';
import { import {
AssetOrder, AssetOrder,
@ -76,7 +76,7 @@ export type TimelineAsset = {
ownerId: string; ownerId: string;
ratio: number; ratio: number;
thumbhash: string | null; thumbhash: string | null;
localDateTime: string; localDateTime: Date;
visibility: AssetVisibility; visibility: AssetVisibility;
isFavorite: boolean; isFavorite: boolean;
isTrashed: boolean; isTrashed: boolean;
@ -131,7 +131,8 @@ export class AssetDateGroup {
// --- public // --- public
readonly bucket: AssetBucket; readonly bucket: AssetBucket;
readonly index: number; readonly index: number;
readonly date: DateTime; readonly date: Date;
readonly groupTitle: string;
readonly dayOfMonth: number; readonly dayOfMonth: number;
intersetingAssets: IntersectingAsset[] = $state([]); intersetingAssets: IntersectingAsset[] = $state([]);
@ -146,24 +147,23 @@ export class AssetDateGroup {
col = $state(0); col = $state(0);
deferredLayout = false; deferredLayout = false;
constructor(bucket: AssetBucket, index: number, date: DateTime, dayOfMonth: number) { constructor(bucket: AssetBucket, index: number, date: Date, dayOfMonth: number) {
this.index = index; this.index = index;
this.bucket = bucket; this.bucket = bucket;
this.date = date; this.date = date;
this.dayOfMonth = dayOfMonth; this.dayOfMonth = dayOfMonth;
this.groupTitle = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()).toLocaleString(
get(locale),
{ timeZone: 'UTC', weekday: 'short', year: 'numeric', month: 'long', day: 'numeric' },
);
} }
sortAssets(sortOrder: AssetOrder = AssetOrder.Desc) { sortAssets(sortOrder: AssetOrder = AssetOrder.Desc) {
this.intersetingAssets.sort((a, b) => {
const aDate = DateTime.fromISO(a.asset!.localDateTime).toUTC();
const bDate = DateTime.fromISO(b.asset!.localDateTime).toUTC();
if (sortOrder === AssetOrder.Asc) { if (sortOrder === AssetOrder.Asc) {
return aDate.diff(bDate).milliseconds; this.intersetingAssets.sort((a, b) => a.asset!.localDateTime.valueOf() - b.asset!.localDateTime.valueOf());
} else {
this.intersetingAssets.sort((a, b) => b.asset!.localDateTime.valueOf() - a.asset!.localDateTime.valueOf());
} }
return bDate.diff(aDate).milliseconds;
});
} }
getFirstAsset() { getFirstAsset() {
@ -194,15 +194,17 @@ export class AssetDateGroup {
let changedGeometry = false; let changedGeometry = false;
for (const assetId of unprocessedIds) { for (const assetId of unprocessedIds) {
const index = this.intersetingAssets.findIndex((ia) => ia.id == assetId); const index = this.intersetingAssets.findIndex((ia) => ia.id == assetId);
if (index !== -1) { if (index === -1) {
continue;
}
const asset = this.intersetingAssets[index].asset!; const asset = this.intersetingAssets[index].asset!;
const oldTime = asset.localDateTime; const oldTime = asset.localDateTime;
let { remove } = operation(asset); let { remove } = operation(asset);
const newTime = asset.localDateTime; const newTime = asset.localDateTime;
if (oldTime !== newTime) { if (oldTime.valueOf() !== newTime.valueOf()) {
const utc = DateTime.fromISO(asset.localDateTime).toUTC().startOf('month'); const year = newTime.getUTCFullYear();
const year = utc.get('year'); const month = newTime.getUTCMonth();
const month = utc.get('month');
if (this.bucket.year !== year || this.bucket.month !== month) { if (this.bucket.year !== year || this.bucket.month !== month) {
remove = true; remove = true;
moveAssets.push({ asset, year, month }); moveAssets.push({ asset, year, month });
@ -215,7 +217,6 @@ export class AssetDateGroup {
changedGeometry = true; changedGeometry = true;
} }
} }
}
return { moveAssets, processedIds, unprocessedIds, changedGeometry }; return { moveAssets, processedIds, unprocessedIds, changedGeometry };
} }
@ -237,10 +238,6 @@ export class AssetDateGroup {
get absoluteDateGroupTop() { get absoluteDateGroupTop() {
return this.bucket.top + this.top; return this.bucket.top + this.top;
} }
get groupTitle() {
return formatDateGroupTitle(this.date);
}
} }
export interface Viewport { export interface Viewport {
@ -344,22 +341,20 @@ export class AssetBucket {
readonly month: number; readonly month: number;
readonly year: number; readonly year: number;
constructor(store: AssetStore, utcDate: DateTime, initialCount: number, order: AssetOrder = AssetOrder.Desc) { constructor(store: AssetStore, date: Date, initialCount: number, order: AssetOrder = AssetOrder.Desc) {
this.store = store; this.store = store;
this.#initialCount = initialCount; this.#initialCount = initialCount;
this.#sortOrder = order; this.#sortOrder = order;
const year = utcDate.get('year'); const bucketDateFormatted = date.toLocaleString(get(locale), {
const month = utcDate.get('month');
const bucketDateFormatted = utcDate.toJSDate().toLocaleString(get(locale), {
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
timeZone: 'UTC', timeZone: 'UTC',
}); });
this.bucketDate = utcDate.toISO()!.toString(); this.bucketDate = date.toISOString();
this.bucketDateFormatted = bucketDateFormatted; this.bucketDateFormatted = bucketDateFormatted;
this.month = month; this.month = date.getUTCMonth();
this.year = year; this.year = date.getUTCFullYear();
this.loader = new CancellableTask( this.loader = new CancellableTask(
() => { () => {
@ -411,10 +406,10 @@ export class AssetBucket {
sortDateGroups() { sortDateGroups() {
if (this.#sortOrder === AssetOrder.Asc) { if (this.#sortOrder === AssetOrder.Asc) {
return this.dateGroups.sort((a, b) => a.date.diff(b.date).milliseconds); return this.dateGroups.sort((a, b) => a.date.valueOf() - b.date.valueOf());
} }
return this.dateGroups.sort((a, b) => b.date.diff(a.date).milliseconds); return this.dateGroups.sort((a, b) => b.date.valueOf() - a.date.valueOf());
} }
runAssetOperation(ids: Set<string>, operation: AssetOperation) { runAssetOperation(ids: Set<string>, operation: AssetOperation) {
@ -459,6 +454,7 @@ export class AssetBucket {
} }
addAssets(bucketAssets: TimeBucketAssetResponseDto) { addAssets(bucketAssets: TimeBucketAssetResponseDto) {
const time1 = performance.now();
const addContext = new AddContext(); const addContext = new AddContext();
const people: string[] = []; const people: string[] = [];
for (let i = 0; i < bucketAssets.id.length; i++) { for (let i = 0; i < bucketAssets.id.length; i++) {
@ -473,7 +469,7 @@ export class AssetBucket {
isTrashed: bucketAssets.isTrashed[i], isTrashed: bucketAssets.isTrashed[i],
isVideo: !bucketAssets.isImage[i], isVideo: !bucketAssets.isImage[i],
livePhotoVideoId: bucketAssets.livePhotoVideoId[i], livePhotoVideoId: bucketAssets.livePhotoVideoId[i],
localDateTime: bucketAssets.localDateTime[i], localDateTime: new Date(bucketAssets.localDateTime[i]),
ownerId: bucketAssets.ownerId[i], ownerId: bucketAssets.ownerId[i],
people, people,
projectionType: bucketAssets.projectionType[i], projectionType: bucketAssets.projectionType[i],
@ -500,27 +496,30 @@ export class AssetBucket {
addContext.sort(this, this.#sortOrder); addContext.sort(this, this.#sortOrder);
const time2 = performance.now();
const time = time2 - time1;
console.log(`AssetBucket.addAssets took ${time}ms`);
return addContext.unprocessedAssets; return addContext.unprocessedAssets;
} }
addTimelineAsset(timelineAsset: TimelineAsset, addContext: AddContext) { addTimelineAsset(timelineAsset: TimelineAsset, addContext: AddContext) {
const { localDateTime } = timelineAsset; const { localDateTime } = timelineAsset;
const date = DateTime.fromISO(localDateTime).toUTC();
const month = date.get('month'); const month = localDateTime.getUTCMonth();
const year = date.get('year'); const year = localDateTime.getUTCFullYear();
if (this.month !== month || this.year !== year) { if (this.month !== month || this.year !== year) {
addContext.unprocessedAssets.push(timelineAsset); addContext.unprocessedAssets.push(timelineAsset);
return; return;
} }
const day = date.get('day'); const day = timelineAsset.localDateTime.getUTCDay();
let dateGroup = addContext.getDateGroup(year, month, day) || this.findDateGroupByDay(day); let dateGroup = addContext.getDateGroup(year, month, day) || this.findDateGroupByDay(day);
if (dateGroup) { if (dateGroup) {
addContext.setDateGroup(dateGroup, year, month, day); addContext.setDateGroup(dateGroup, year, month, day);
} else { } else {
dateGroup = new AssetDateGroup(this, this.dateGroups.length, date, day); dateGroup = new AssetDateGroup(this, this.dateGroups.length, localDateTime, day);
this.dateGroups.push(dateGroup); this.dateGroups.push(dateGroup);
addContext.setDateGroup(dateGroup, year, month, day); addContext.setDateGroup(dateGroup, year, month, day);
addContext.newDateGroups.add(dateGroup); addContext.newDateGroups.add(dateGroup);
@ -624,12 +623,12 @@ export class AssetBucket {
return this.assets().find((asset) => asset.id === assetDescriptor.id); return this.assets().find((asset) => asset.id === assetDescriptor.id);
} }
findClosest(target: DateTime) { findClosest(target: Date) {
let closest = undefined; let closest = undefined;
let smallestDiff = Infinity; let smallestDiff = Infinity;
for (const current of this.assets()) { for (const current of this.assets()) {
const currentDate = DateTime.fromISO(current.localDateTime).toUTC(); const currentDate = current.localDateTime;
const diff = Math.abs(target.diff(currentDate).milliseconds); const diff = Math.abs(target.valueOf() - currentDate.valueOf());
if (diff < smallestDiff) { if (diff < smallestDiff) {
smallestDiff = diff; smallestDiff = diff;
closest = current; closest = current;
@ -914,10 +913,9 @@ export class AssetStore {
} }
} }
#findBucketForDate(date: DateTime) { #findBucketForDate(date: Date) {
for (const bucket of this.buckets) { for (const bucket of this.buckets) {
const bucketDate = DateTime.fromISO(bucket.bucketDate).toUTC(); if (bucket.month === date.getUTCMonth() && bucket.year === date.getUTCFullYear()) {
if (bucketDate.hasSame(date, 'month') && bucketDate.hasSame(date, 'year')) {
return bucket; return bucket;
} }
} }
@ -1020,8 +1018,7 @@ export class AssetStore {
}); });
this.buckets = timebuckets.map((bucket) => { this.buckets = timebuckets.map((bucket) => {
const utcDate = DateTime.fromISO(bucket.timeBucket).toUTC(); return new AssetBucket(this, new Date(bucket.timeBucket), bucket.count, this.#options.order);
return new AssetBucket(this, utcDate, bucket.count, this.#options.order);
}); });
this.albumAssets.clear(); this.albumAssets.clear();
this.#updateViewportGeometry(false); this.#updateViewportGeometry(false);
@ -1055,6 +1052,7 @@ export class AssetStore {
await this.#initialiazeTimeBuckets(); await this.#initialiazeTimeBuckets();
}, true); }, true);
} }
public destroy() { public destroy() {
this.disconnect(); this.disconnect();
this.isInitialized = false; this.isInitialized = false;
@ -1203,9 +1201,9 @@ export class AssetStore {
cancelable = options.cancelable; cancelable = options.cancelable;
} }
const date = DateTime.fromISO(bucketDate).toUTC(); const date = new Date(bucketDate);
const year = date.get('year'); const year = date.getUTCFullYear();
const month = date.get('month'); const month = date.getUTCMonth();
const bucket = this.getBucketByDate(year, month); const bucket = this.getBucketByDate(year, month);
if (!bucket) { if (!bucket) {
return; return;
@ -1281,13 +1279,12 @@ export class AssetStore {
const updatedBuckets = new Set<AssetBucket>(); const updatedBuckets = new Set<AssetBucket>();
const bucketCount = this.buckets.length; const bucketCount = this.buckets.length;
for (const asset of assets) { for (const asset of assets) {
const utc = DateTime.fromISO(asset.localDateTime).toUTC().startOf('month'); const year = asset.localDateTime.getUTCFullYear();
const year = utc.get('year'); const month = asset.localDateTime.getUTCMonth();
const month = utc.get('month');
let bucket = this.getBucketByDate(year, month); let bucket = this.getBucketByDate(year, month);
if (!bucket) { if (!bucket) {
bucket = new AssetBucket(this, utc, 1, this.#options.order); bucket = new AssetBucket(this, asset.localDateTime, 1, this.#options.order);
this.buckets.push(bucket); this.buckets.push(bucket);
} }
@ -1336,14 +1333,12 @@ export class AssetStore {
} }
} }
async #loadBucketAtTime(localDateTime: string, options?: { cancelable: boolean }) { async #loadBucketAtTime(localDateTime: Date, options?: { cancelable: boolean }) {
let date = DateTime.fromISO(localDateTime).toUTC();
// Only support TimeBucketSize.Month // Only support TimeBucketSize.Month
date = date.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }); const year = localDateTime.getUTCFullYear();
const iso = date.toISO()!; const month = localDateTime.getUTCMonth();
const year = date.get('year'); localDateTime = new Date(year, month);
const month = date.get('month'); await this.loadBucket(localDateTime.toISOString(), options);
await this.loadBucket(iso, options);
return this.getBucketByDate(year, month); return this.getBucketByDate(year, month);
} }
@ -1456,7 +1451,7 @@ export class AssetStore {
return this.#getAssetWithOffset(assetDescriptor, magnitude, 'backward'); return this.#getAssetWithOffset(assetDescriptor, magnitude, 'backward');
} }
async getClosestAssetToDate(date: DateTime) { async getClosestAssetToDate(date: Date) {
let bucket = this.#findBucketForDate(date); let bucket = this.#findBucketForDate(date);
if (!bucket) { if (!bucket) {
return; return;
@ -1508,7 +1503,7 @@ export class AssetStore {
} }
async #getAssetByDayOffset(asset: TimelineAsset, bucket: AssetBucket, direction: Direction) { async #getAssetByDayOffset(asset: TimelineAsset, bucket: AssetBucket, direction: Direction) {
const currentDate = DateTime.fromISO(asset.localDateTime).toUTC(); const currentDate = DateTime.fromJSDate(asset.localDateTime).toUTC();
const targetDate = const targetDate =
direction === 'forward' direction === 'forward'
? currentDate.plus({ days: 1 }) // Moving forward in time (previous in UI) ? currentDate.plus({ days: 1 }) // Moving forward in time (previous in UI)
@ -1551,7 +1546,7 @@ export class AssetStore {
} }
async #getAssetByYearOffset(asset: TimelineAsset, bucket: AssetBucket, direction: Direction) { async #getAssetByYearOffset(asset: TimelineAsset, bucket: AssetBucket, direction: Direction) {
const currentDate = DateTime.fromISO(asset.localDateTime).toUTC(); const currentDate = DateTime.fromJSDate(asset.localDateTime).toUTC();
const targetYear = currentDate.get('year') + (direction === 'forward' ? 1 : -1); const targetYear = currentDate.get('year') + (direction === 'forward' ? 1 : -1);
const bucketIndex = this.buckets.indexOf(bucket); const bucketIndex = this.buckets.indexOf(bucket);

View File

@ -2,7 +2,6 @@ import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { derived, get } from 'svelte/store'; import { derived, get } from 'svelte/store';
import { fromLocalDateTime } from './timeline-util';
/** /**
* Calculate thumbnail size based on number of assets and viewport width * Calculate thumbnail size based on number of assets and viewport width
@ -40,7 +39,7 @@ export function getThumbnailSize(assetCount: number, viewWidth: number): number
export const getAltText = derived(t, ($t) => { export const getAltText = derived(t, ($t) => {
return (asset: TimelineAsset) => { return (asset: TimelineAsset) => {
const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }, { locale: get(locale) }); const date = asset.localDateTime.toLocaleString(get(locale), { dateStyle: 'long', timeZone: 'UTC' });
const hasPlace = asset.city && asset.country; const hasPlace = asset.city && asset.country;
const peopleCount = asset.people.length; const peopleCount = asset.people.length;

View File

@ -20,6 +20,8 @@ export const fromLocalDateTime = (localDateTime: string) =>
export const fromDateTimeOriginal = (dateTimeOriginal: string, timeZone: string) => export const fromDateTimeOriginal = (dateTimeOriginal: string, timeZone: string) =>
DateTime.fromISO(dateTimeOriginal, { zone: timeZone }); DateTime.fromISO(dateTimeOriginal, { zone: timeZone });
export const startOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth());
export function formatGroupTitle(_date: DateTime): string { export function formatGroupTitle(_date: DateTime): string {
if (!_date.isValid) { if (!_date.isValid) {
return _date.toString(); return _date.toString();
@ -77,7 +79,7 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
ownerId: assetResponse.ownerId, ownerId: assetResponse.ownerId,
ratio, ratio,
thumbhash: assetResponse.thumbhash, thumbhash: assetResponse.thumbhash,
localDateTime: assetResponse.localDateTime, localDateTime: new Date(assetResponse.localDateTime),
isFavorite: assetResponse.isFavorite, isFavorite: assetResponse.isFavorite,
visibility: assetResponse.isArchived ? AssetVisibility.Archive : AssetVisibility.Timeline, visibility: assetResponse.isArchived ? AssetVisibility.Archive : AssetVisibility.Timeline,
isTrashed: assetResponse.isTrashed, isTrashed: assetResponse.isTrashed,

View File

@ -39,7 +39,7 @@ export const timelineAssetFactory = Sync.makeFactory<TimelineAsset>({
ratio: Sync.each(() => faker.number.int()), ratio: Sync.each(() => faker.number.int()),
ownerId: Sync.each(() => faker.string.uuid()), ownerId: Sync.each(() => faker.string.uuid()),
thumbhash: Sync.each(() => faker.string.alphanumeric(28)), thumbhash: Sync.each(() => faker.string.alphanumeric(28)),
localDateTime: Sync.each(() => faker.date.past().toISOString()), localDateTime: Sync.each(() => faker.date.past()),
isFavorite: Sync.each(() => faker.datatype.boolean()), isFavorite: Sync.each(() => faker.datatype.boolean()),
visibility: AssetVisibility.Timeline, visibility: AssetVisibility.Timeline,
isTrashed: false, isTrashed: false,
@ -82,7 +82,7 @@ export const toResponseDto = (...timelineAsset: TimelineAsset[]) => {
bucketAssets.isImage.push(asset.isImage); bucketAssets.isImage.push(asset.isImage);
bucketAssets.isTrashed.push(asset.isTrashed); bucketAssets.isTrashed.push(asset.isTrashed);
bucketAssets.livePhotoVideoId.push(asset.livePhotoVideoId!); bucketAssets.livePhotoVideoId.push(asset.livePhotoVideoId!);
bucketAssets.localDateTime.push(asset.localDateTime); bucketAssets.localDateTime.push(asset.localDateTime.toISOString());
bucketAssets.ownerId.push(asset.ownerId); bucketAssets.ownerId.push(asset.ownerId);
bucketAssets.projectionType.push(asset.projectionType!); bucketAssets.projectionType.push(asset.projectionType!);
bucketAssets.ratio.push(asset.ratio); bucketAssets.ratio.push(asset.ratio);