Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da7fbcbb46 | |||
| b471e190a0 | |||
| 7672c8c6e0 | |||
| 386a6bb377 |
@@ -12,7 +12,8 @@ services:
|
|||||||
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/data/upload
|
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/data/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- pnpm-store:/usr/src/app/.pnpm-store
|
- pnpm-store:/usr/src/app/.pnpm-store
|
||||||
- server-node_modules:/usr/src/app/server/node_modules
|
- server-node-modules:/usr/src/app/server/node_modules
|
||||||
|
- server-dist:/usr/src/app/server/dist
|
||||||
- web-node_modules:/usr/src/app/web/node_modules
|
- web-node_modules:/usr/src/app/web/node_modules
|
||||||
- github-node_modules:/usr/src/app/.github/node_modules
|
- github-node_modules:/usr/src/app/.github/node_modules
|
||||||
- cli-node_modules:/usr/src/app/cli/node_modules
|
- cli-node_modules:/usr/src/app/cli/node_modules
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ services:
|
|||||||
- ${UPLOAD_LOCATION}/photos/upload:/data/upload
|
- ${UPLOAD_LOCATION}/photos/upload:/data/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- pnpm-store:/usr/src/app/.pnpm-store
|
- pnpm-store:/usr/src/app/.pnpm-store
|
||||||
- server-node_modules:/usr/src/app/server/node_modules
|
- server-node-modules:/usr/src/app/server/node_modules
|
||||||
|
- server-dist:/usr/src/app/server/dist
|
||||||
- web-node_modules:/usr/src/app/web/node_modules
|
- web-node_modules:/usr/src/app/web/node_modules
|
||||||
- github-node_modules:/usr/src/app/.github/node_modules
|
- github-node_modules:/usr/src/app/.github/node_modules
|
||||||
- cli-node_modules:/usr/src/app/cli/node_modules
|
- cli-node_modules:/usr/src/app/cli/node_modules
|
||||||
@@ -96,7 +97,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ..:/usr/src/app
|
- ..:/usr/src/app
|
||||||
- pnpm-store:/usr/src/app/.pnpm-store
|
- pnpm-store:/usr/src/app/.pnpm-store
|
||||||
- server-node_modules:/usr/src/app/server/node_modules
|
- server-node-modules:/usr/src/app/server/node_modules
|
||||||
|
- server-dist:/usr/src/app/server/dist
|
||||||
- web-node_modules:/usr/src/app/web/node_modules
|
- web-node_modules:/usr/src/app/web/node_modules
|
||||||
- github-node_modules:/usr/src/app/.github/node_modules
|
- github-node_modules:/usr/src/app/.github/node_modules
|
||||||
- cli-node_modules:/usr/src/app/cli/node_modules
|
- cli-node_modules:/usr/src/app/cli/node_modules
|
||||||
@@ -192,7 +194,8 @@ services:
|
|||||||
command: sh -c 'for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done'
|
command: sh -c 'for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done'
|
||||||
volumes:
|
volumes:
|
||||||
- pnpm-store:/usr/src/app/.pnpm-store
|
- pnpm-store:/usr/src/app/.pnpm-store
|
||||||
- server-node_modules:/usr/src/app/server/node_modules
|
- server-node-modules:/usr/src/app/server/node_modules
|
||||||
|
- server-dist:/usr/src/app/server/dist
|
||||||
- web-node_modules:/usr/src/app/web/node_modules
|
- web-node_modules:/usr/src/app/web/node_modules
|
||||||
- github-node_modules:/usr/src/app/.github/node_modules
|
- github-node_modules:/usr/src/app/.github/node_modules
|
||||||
- cli-node_modules:/usr/src/app/cli/node_modules
|
- cli-node_modules:/usr/src/app/cli/node_modules
|
||||||
@@ -207,7 +210,8 @@ volumes:
|
|||||||
prometheus-data:
|
prometheus-data:
|
||||||
grafana-data:
|
grafana-data:
|
||||||
pnpm-store:
|
pnpm-store:
|
||||||
server-node_modules:
|
server-node-modules:
|
||||||
|
server-dist:
|
||||||
web-node_modules:
|
web-node_modules:
|
||||||
github-node_modules:
|
github-node_modules:
|
||||||
cli-node_modules:
|
cli-node_modules:
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ The `.well-known/openid-configuration` part of the url is optional and will be a
|
|||||||
## Auto Launch
|
## Auto Launch
|
||||||
|
|
||||||
When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`.
|
When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`.
|
||||||
Auto Launch can also be enabled on a per-request basis by navigating to `/auth/login?autoLaunch=1`, this can be useful in situations where Immich is called from e.g. Nextcloud using the _External sites_ app and the _oidc_ app so as to enable users to directly interact with a logged-in instance of Immich.
|
Auto Launch can also be enabled on a per-request basis by navigating to `/auth/login?authLaunch=1`, this can be useful in situations where Immich is called from e.g. Nextcloud using the _External sites_ app and the _oidc_ app so as to enable users to directly interact with a logged-in instance of Immich.
|
||||||
|
|
||||||
## Mobile Redirect URI
|
## Mobile Redirect URI
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -91,7 +91,11 @@ FROM prod-builder-base AS server-prod
|
|||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY ./package* ./pnpm* .pnpmfile.cjs ./
|
COPY ./package* ./pnpm* .pnpmfile.cjs ./
|
||||||
COPY ./server ./server/
|
COPY ./server ./server/
|
||||||
RUN SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile build && \
|
## Build server with sharp linked against system (global) libvips instead of vendored copy.
|
||||||
|
## Using SHARP_IGNORE_GLOBAL_LIBVIPS previously caused arm64 (e.g. Raspberry Pi) illegal instruction
|
||||||
|
## crashes due to the prebuilt vendored libvips targeting newer ARM features. Force global libvips
|
||||||
|
## during build so the already-present distro libvips (built with conservative flags) is used.
|
||||||
|
RUN SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile build && \
|
||||||
SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned
|
SHARP_FORCE_GLOBAL_LIBVIPS=true pnpm --filter immich --frozen-lockfile --prod --no-optional deploy /output/server-pruned
|
||||||
|
|
||||||
# web production build
|
# web production build
|
||||||
|
|||||||
@@ -38,11 +38,7 @@ from
|
|||||||
select
|
select
|
||||||
"album".*,
|
"album".*,
|
||||||
coalesce(
|
coalesce(
|
||||||
json_agg(
|
json_agg("assets") filter (
|
||||||
"assets"
|
|
||||||
order by
|
|
||||||
"assets"."fileCreatedAt" asc
|
|
||||||
) filter (
|
|
||||||
where
|
where
|
||||||
"assets"."id" is not null
|
"assets"."id" is not null
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -86,16 +86,7 @@ export class SharedLinkRepository {
|
|||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
)
|
)
|
||||||
.select((eb) =>
|
.select((eb) =>
|
||||||
eb.fn
|
eb.fn.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`).as('assets'),
|
||||||
.coalesce(
|
|
||||||
eb.fn
|
|
||||||
.jsonAgg('assets')
|
|
||||||
.orderBy('assets.fileCreatedAt', 'asc')
|
|
||||||
.filterWhere('assets.id', 'is not', null),
|
|
||||||
|
|
||||||
sql`'[]'`,
|
|
||||||
)
|
|
||||||
.as('assets'),
|
|
||||||
)
|
)
|
||||||
.select((eb) => eb.fn.toJson('owner').as('owner'))
|
.select((eb) => eb.fn.toJson('owner').as('owner'))
|
||||||
.groupBy(['album.id', sql`"owner".*`])
|
.groupBy(['album.id', sql`"owner".*`])
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import { PartnerRepository } from 'src/repositories/partner.repository';
|
|||||||
import { PersonRepository } from 'src/repositories/person.repository';
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
import { SearchRepository } from 'src/repositories/search.repository';
|
import { SearchRepository } from 'src/repositories/search.repository';
|
||||||
import { SessionRepository } from 'src/repositories/session.repository';
|
import { SessionRepository } from 'src/repositories/session.repository';
|
||||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
|
||||||
import { StackRepository } from 'src/repositories/stack.repository';
|
import { StackRepository } from 'src/repositories/stack.repository';
|
||||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository';
|
import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository';
|
||||||
@@ -287,7 +286,6 @@ const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
|
|||||||
case PersonRepository:
|
case PersonRepository:
|
||||||
case SearchRepository:
|
case SearchRepository:
|
||||||
case SessionRepository:
|
case SessionRepository:
|
||||||
case SharedLinkRepository:
|
|
||||||
case StackRepository:
|
case StackRepository:
|
||||||
case SyncRepository:
|
case SyncRepository:
|
||||||
case SyncCheckpointRepository:
|
case SyncCheckpointRepository:
|
||||||
@@ -393,7 +391,7 @@ const assetInsert = (asset: Partial<Insertable<AssetTable>> = {}) => {
|
|||||||
checksum: randomBytes(32),
|
checksum: randomBytes(32),
|
||||||
type: AssetType.Image,
|
type: AssetType.Image,
|
||||||
originalPath: '/path/to/something.jpg',
|
originalPath: '/path/to/something.jpg',
|
||||||
ownerId: 'not-a-valid-uuid',
|
ownerId: '@immich.cloud',
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
fileCreatedAt: now,
|
fileCreatedAt: now,
|
||||||
fileModifiedAt: now,
|
fileModifiedAt: now,
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import { Kysely } from 'kysely';
|
|
||||||
import { randomBytes } from 'node:crypto';
|
|
||||||
import { SharedLinkType } from 'src/enum';
|
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
|
||||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
|
||||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
|
||||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
|
||||||
import { DB } from 'src/schema';
|
|
||||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
|
||||||
import { newMediumService } from 'test/medium.factory';
|
|
||||||
import { factory } from 'test/small.factory';
|
|
||||||
import { getKyselyDB } from 'test/utils';
|
|
||||||
|
|
||||||
let defaultDatabase: Kysely<DB>;
|
|
||||||
|
|
||||||
const setup = (db?: Kysely<DB>) => {
|
|
||||||
return newMediumService(SharedLinkService, {
|
|
||||||
database: db || defaultDatabase,
|
|
||||||
real: [AccessRepository, DatabaseRepository, SharedLinkRepository],
|
|
||||||
mock: [LoggingRepository, StorageRepository],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
defaultDatabase = await getKyselyDB();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(SharedLinkService.name, () => {
|
|
||||||
describe('get', () => {
|
|
||||||
it('should return the correct dates on the shared link album', async () => {
|
|
||||||
const { sut, ctx } = setup();
|
|
||||||
|
|
||||||
const { user } = await ctx.newUser();
|
|
||||||
const auth = factory.auth({ user });
|
|
||||||
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
|
||||||
|
|
||||||
const dates = ['2021-01-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z', '2020-01-01T00:00:00.000Z'];
|
|
||||||
|
|
||||||
for (const date of dates) {
|
|
||||||
const { asset } = await ctx.newAsset({ fileCreatedAt: date, localDateTime: date, ownerId: user.id });
|
|
||||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
|
||||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
|
||||||
|
|
||||||
const sharedLink = await sharedLinkRepo.create({
|
|
||||||
key: randomBytes(16),
|
|
||||||
id: factory.uuid(),
|
|
||||||
userId: user.id,
|
|
||||||
albumId: album.id,
|
|
||||||
allowUpload: true,
|
|
||||||
type: SharedLinkType.Album,
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(sut.get(auth, sharedLink.id)).resolves.toMatchObject({
|
|
||||||
album: expect.objectContaining({
|
|
||||||
startDate: '2020-01-01T00:00:00+00:00',
|
|
||||||
endDate: '2022-01-01T00:00:00+00:00',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
|
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
|
||||||
import { timeToSeconds } from '$lib/utils/date-time';
|
import { timeToSeconds } from '$lib/utils/date-time';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk';
|
import { AssetMediaSize, AssetVisibility } from '@immich/sdk';
|
||||||
import {
|
import {
|
||||||
mdiArchiveArrowDownOutline,
|
mdiArchiveArrowDownOutline,
|
||||||
mdiCameraBurst,
|
mdiCameraBurst,
|
||||||
@@ -45,7 +45,6 @@
|
|||||||
imageClass?: ClassValue;
|
imageClass?: ClassValue;
|
||||||
brokenAssetClass?: ClassValue;
|
brokenAssetClass?: ClassValue;
|
||||||
dimmed?: boolean;
|
dimmed?: boolean;
|
||||||
albumUsers?: UserResponseDto[];
|
|
||||||
onClick?: (asset: TimelineAsset) => void;
|
onClick?: (asset: TimelineAsset) => void;
|
||||||
onSelect?: (asset: TimelineAsset) => void;
|
onSelect?: (asset: TimelineAsset) => void;
|
||||||
onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void;
|
onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void;
|
||||||
@@ -64,7 +63,6 @@
|
|||||||
readonly = false,
|
readonly = false,
|
||||||
showArchiveIcon = false,
|
showArchiveIcon = false,
|
||||||
showStackedIcon = true,
|
showStackedIcon = true,
|
||||||
albumUsers = [],
|
|
||||||
onClick = undefined,
|
onClick = undefined,
|
||||||
onSelect = undefined,
|
onSelect = undefined,
|
||||||
onMouseEvent = undefined,
|
onMouseEvent = undefined,
|
||||||
@@ -86,8 +84,6 @@
|
|||||||
let width = $derived(thumbnailSize || thumbnailWidth || 235);
|
let width = $derived(thumbnailSize || thumbnailWidth || 235);
|
||||||
let height = $derived(thumbnailSize || thumbnailHeight || 235);
|
let height = $derived(thumbnailSize || thumbnailHeight || 235);
|
||||||
|
|
||||||
let assetOwner = $derived(albumUsers?.find((user) => user.id === asset.ownerId) ?? null);
|
|
||||||
|
|
||||||
const onIconClickedHandler = (e?: MouseEvent) => {
|
const onIconClickedHandler = (e?: MouseEvent) => {
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
@@ -271,12 +267,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !!assetOwner}
|
|
||||||
<div class="absolute bottom-0 end-1">
|
|
||||||
<span class="text-white font-light text-sm">{assetOwner.name}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive}
|
{#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive}
|
||||||
<div class={['absolute start-2', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
|
<div class={['absolute start-2', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
|
||||||
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
|
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import type { UserResponseDto } from '@immich/sdk';
|
|
||||||
|
|
||||||
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@
|
|||||||
monthGroup: MonthGroup;
|
monthGroup: MonthGroup;
|
||||||
timelineManager: TimelineManager;
|
timelineManager: TimelineManager;
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction;
|
||||||
albumUsers?: UserResponseDto[];
|
|
||||||
onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void;
|
onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void;
|
||||||
onSelectAssets: (asset: TimelineAsset) => void;
|
onSelectAssets: (asset: TimelineAsset) => void;
|
||||||
onSelectAssetCandidates: (asset: TimelineAsset | null) => void;
|
onSelectAssetCandidates: (asset: TimelineAsset | null) => void;
|
||||||
@@ -41,7 +40,6 @@
|
|||||||
monthGroup = $bindable(),
|
monthGroup = $bindable(),
|
||||||
assetInteraction,
|
assetInteraction,
|
||||||
timelineManager,
|
timelineManager,
|
||||||
albumUsers = [],
|
|
||||||
onSelect,
|
onSelect,
|
||||||
onSelectAssets,
|
onSelectAssets,
|
||||||
onSelectAssetCandidates,
|
onSelectAssetCandidates,
|
||||||
@@ -191,7 +189,6 @@
|
|||||||
showStackedIcon={withStacked}
|
showStackedIcon={withStacked}
|
||||||
{showArchiveIcon}
|
{showArchiveIcon}
|
||||||
{asset}
|
{asset}
|
||||||
{albumUsers}
|
|
||||||
{groupIndex}
|
{groupIndex}
|
||||||
onClick={(asset) => onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset)}
|
onClick={(asset) => onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset)}
|
||||||
onSelect={(asset) => assetSelectHandler(timelineManager, asset, dayGroup.getAssets(), dayGroup.groupTitle)}
|
onSelect={(asset) => assetSelectHandler(timelineManager, asset, dayGroup.getAssets(), dayGroup.groupTitle)}
|
||||||
|
|||||||
@@ -30,13 +30,7 @@
|
|||||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { getTimes, toTimelineAsset, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
import { getTimes, toTimelineAsset, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||||
import {
|
import { AssetVisibility, getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||||
AssetVisibility,
|
|
||||||
getAssetInfo,
|
|
||||||
type AlbumResponseDto,
|
|
||||||
type PersonResponseDto,
|
|
||||||
type UserResponseDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { modalManager } from '@immich/ui';
|
import { modalManager } from '@immich/ui';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
@@ -65,7 +59,6 @@
|
|||||||
showArchiveIcon?: boolean;
|
showArchiveIcon?: boolean;
|
||||||
isShared?: boolean;
|
isShared?: boolean;
|
||||||
album?: AlbumResponseDto | null;
|
album?: AlbumResponseDto | null;
|
||||||
albumUsers?: UserResponseDto[];
|
|
||||||
person?: PersonResponseDto | null;
|
person?: PersonResponseDto | null;
|
||||||
isShowDeleteConfirmation?: boolean;
|
isShowDeleteConfirmation?: boolean;
|
||||||
onSelect?: (asset: TimelineAsset) => void;
|
onSelect?: (asset: TimelineAsset) => void;
|
||||||
@@ -85,7 +78,6 @@
|
|||||||
showArchiveIcon = false,
|
showArchiveIcon = false,
|
||||||
isShared = false,
|
isShared = false,
|
||||||
album = null,
|
album = null,
|
||||||
albumUsers = [],
|
|
||||||
person = null,
|
person = null,
|
||||||
isShowDeleteConfirmation = $bindable(false),
|
isShowDeleteConfirmation = $bindable(false),
|
||||||
onSelect = () => {},
|
onSelect = () => {},
|
||||||
@@ -96,12 +88,6 @@
|
|||||||
|
|
||||||
let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget, mutex } = assetViewingStore;
|
let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget, mutex } = assetViewingStore;
|
||||||
|
|
||||||
// const albumUsers = $derived(
|
|
||||||
// album?.shared && album.albumUsers.some(({ role }) => role === AlbumUserRole.Editor)
|
|
||||||
// ? [album.owner, ...album.albumUsers.map(({ user }) => user)]
|
|
||||||
// : [],
|
|
||||||
// );
|
|
||||||
|
|
||||||
let element: HTMLElement | undefined = $state();
|
let element: HTMLElement | undefined = $state();
|
||||||
|
|
||||||
let timelineElement: HTMLElement | undefined = $state();
|
let timelineElement: HTMLElement | undefined = $state();
|
||||||
@@ -950,7 +936,6 @@
|
|||||||
{isSelectionMode}
|
{isSelectionMode}
|
||||||
{singleSelect}
|
{singleSelect}
|
||||||
{monthGroup}
|
{monthGroup}
|
||||||
{albumUsers}
|
|
||||||
onSelect={({ title, assets }) => handleGroupSelect(timelineManager, title, assets)}
|
onSelect={({ title, assets }) => handleGroupSelect(timelineManager, title, assets)}
|
||||||
onSelectAssetCandidates={handleSelectAssetCandidates}
|
onSelectAssetCandidates={handleSelectAssetCandidates}
|
||||||
onSelectAssets={handleSelectAssets}
|
onSelectAssets={handleSelectAssets}
|
||||||
|
|||||||
-17
@@ -71,7 +71,6 @@
|
|||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { Button, IconButton, modalManager } from '@immich/ui';
|
import { Button, IconButton, modalManager } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiAccountEyeOutline,
|
|
||||||
mdiArrowLeft,
|
mdiArrowLeft,
|
||||||
mdiCogOutline,
|
mdiCogOutline,
|
||||||
mdiDeleteOutline,
|
mdiDeleteOutline,
|
||||||
@@ -105,7 +104,6 @@
|
|||||||
let isCreatingSharedAlbum = $state(false);
|
let isCreatingSharedAlbum = $state(false);
|
||||||
let isShowActivity = $state(false);
|
let isShowActivity = $state(false);
|
||||||
let albumOrder: AssetOrder | undefined = $state(data.album.order);
|
let albumOrder: AssetOrder | undefined = $state(data.album.order);
|
||||||
let showAlbumUsers = $state(false);
|
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
const timelineInteraction = new AssetInteraction();
|
const timelineInteraction = new AssetInteraction();
|
||||||
@@ -323,11 +321,6 @@
|
|||||||
|
|
||||||
let album = $derived(data.album);
|
let album = $derived(data.album);
|
||||||
let albumId = $derived(album.id);
|
let albumId = $derived(album.id);
|
||||||
const albumUsers = $derived(
|
|
||||||
showAlbumUsers && album?.shared && album.albumUsers.some(({ role }) => role === AlbumUserRole.Editor)
|
|
||||||
? [album.owner, ...album.albumUsers.map(({ user }) => user)]
|
|
||||||
: [],
|
|
||||||
);
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!album.isActivityEnabled && activityManager.commentCount === 0) {
|
if (!album.isActivityEnabled && activityManager.commentCount === 0) {
|
||||||
@@ -452,7 +445,6 @@
|
|||||||
<AssetGrid
|
<AssetGrid
|
||||||
enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true}
|
enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true}
|
||||||
{album}
|
{album}
|
||||||
{albumUsers}
|
|
||||||
{timelineManager}
|
{timelineManager}
|
||||||
assetInteraction={currentAssetIntersection}
|
assetInteraction={currentAssetIntersection}
|
||||||
{isShared}
|
{isShared}
|
||||||
@@ -623,15 +615,6 @@
|
|||||||
{#snippet trailing()}
|
{#snippet trailing()}
|
||||||
<CastButton />
|
<CastButton />
|
||||||
|
|
||||||
<IconButton
|
|
||||||
variant="ghost"
|
|
||||||
shape="round"
|
|
||||||
color="secondary"
|
|
||||||
aria-label="view asset owners"
|
|
||||||
icon={mdiAccountEyeOutline}
|
|
||||||
onclick={() => (showAlbumUsers = !showAlbumUsers)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if isEditor}
|
{#if isEditor}
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { handleCancel, handlePreload } from './request';
|
import { cancelRequest, handleRequest } from './request';
|
||||||
|
|
||||||
export const installBroadcastChannelListener = () => {
|
export const installBroadcastChannelListener = () => {
|
||||||
const broadcast = new BroadcastChannel('immich');
|
const broadcast = new BroadcastChannel('immich');
|
||||||
@@ -7,19 +7,12 @@ export const installBroadcastChannelListener = () => {
|
|||||||
if (!event.data) {
|
if (!event.data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const urlString = event.data.url;
|
||||||
const url = new URL(event.data.url, event.origin);
|
const url = new URL(urlString, event.origin);
|
||||||
|
if (event.data.type === 'cancel') {
|
||||||
switch (event.data.type) {
|
cancelRequest(url);
|
||||||
case 'preload': {
|
} else if (event.data.type === 'preload') {
|
||||||
handlePreload(url);
|
handleRequest(url);
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'cancel': {
|
|
||||||
handleCancel(url);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { get, put } from './cache';
|
import { get, put } from './cache';
|
||||||
|
|
||||||
const pendingRequests = new Map<string, AbortController>();
|
|
||||||
|
|
||||||
const isURL = (request: URL | RequestInfo): request is URL => (request as URL).href !== undefined;
|
const isURL = (request: URL | RequestInfo): request is URL => (request as URL).href !== undefined;
|
||||||
const isRequest = (request: RequestInfo): request is Request => (request as Request).url !== undefined;
|
const isRequest = (request: RequestInfo): request is Request => (request as Request).url !== undefined;
|
||||||
|
|
||||||
@@ -23,16 +21,11 @@ const getCacheKey = (request: URL | Request) => {
|
|||||||
throw new Error(`Invalid request: ${request}`);
|
throw new Error(`Invalid request: ${request}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handlePreload = async (request: URL | Request) => {
|
const pendingRequests = new Map<string, AbortController>();
|
||||||
try {
|
|
||||||
return await handleRequest(request);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Preload failed: ${error}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const handleRequest = async (request: URL | Request) => {
|
export const handleRequest = async (request: URL | Request) => {
|
||||||
const cacheKey = getCacheKey(request);
|
const cacheKey = getCacheKey(request);
|
||||||
|
|
||||||
const cachedResponse = await get(cacheKey);
|
const cachedResponse = await get(cacheKey);
|
||||||
if (cachedResponse) {
|
if (cachedResponse) {
|
||||||
return cachedResponse;
|
return cachedResponse;
|
||||||
@@ -48,26 +41,23 @@ export const handleRequest = async (request: URL | Request) => {
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === 'AbortError') {
|
console.log(error);
|
||||||
// dummy response avoids network errors in the console for these requests
|
return new Response(undefined, {
|
||||||
return new Response(undefined, { status: 204 });
|
status: 499,
|
||||||
}
|
statusText: `Request canceled: Instructions unclear, accidentally interrupted myself (${error})`,
|
||||||
|
});
|
||||||
console.log('Not an abort error', error);
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
} finally {
|
} finally {
|
||||||
pendingRequests.delete(cacheKey);
|
pendingRequests.delete(cacheKey);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleCancel = (url: URL) => {
|
export const cancelRequest = (url: URL) => {
|
||||||
const cacheKey = getCacheKey(url);
|
const cacheKey = getCacheKey(url);
|
||||||
const pendingRequest = pendingRequests.get(cacheKey);
|
const pending = pendingRequests.get(cacheKey);
|
||||||
if (!pendingRequest) {
|
if (!pending) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingRequest.abort();
|
pending.abort();
|
||||||
pendingRequests.delete(cacheKey);
|
pendingRequests.delete(cacheKey);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user