Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e0005acfd | |||
| 55a196bfa0 | |||
| d04675fb41 | |||
| acfd40b77a | |||
| 840e43430c | |||
| a3e0c6cef5 | |||
| d0b49846dc | |||
| 8896b2dbf5 | |||
| 251e644b2a | |||
| a02635f9a5 | |||
| 104f3dfcc3 | |||
| b7e3b48a44 |
@@ -12,8 +12,7 @@ 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,8 +33,7 @@ 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
|
||||||
@@ -97,8 +96,7 @@ 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
|
||||||
@@ -194,8 +192,7 @@ 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
|
||||||
@@ -210,8 +207,7 @@ 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?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.
|
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.
|
||||||
|
|
||||||
## Mobile Redirect URI
|
## Mobile Redirect URI
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ from
|
|||||||
select
|
select
|
||||||
"album".*,
|
"album".*,
|
||||||
coalesce(
|
coalesce(
|
||||||
json_agg("assets") filter (
|
json_agg(
|
||||||
|
"assets"
|
||||||
|
order by
|
||||||
|
"assets"."fileCreatedAt" asc
|
||||||
|
) filter (
|
||||||
where
|
where
|
||||||
"assets"."id" is not null
|
"assets"."id" is not null
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -86,7 +86,16 @@ export class SharedLinkRepository {
|
|||||||
(join) => join.onTrue(),
|
(join) => join.onTrue(),
|
||||||
)
|
)
|
||||||
.select((eb) =>
|
.select((eb) =>
|
||||||
eb.fn.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`).as('assets'),
|
eb.fn
|
||||||
|
.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,6 +33,7 @@ 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';
|
||||||
@@ -286,6 +287,7 @@ 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:
|
||||||
@@ -391,7 +393,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: '@immich.cloud',
|
ownerId: 'not-a-valid-uuid',
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
fileCreatedAt: now,
|
fileCreatedAt: now,
|
||||||
fileModifiedAt: now,
|
fileModifiedAt: now,
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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 } from '@immich/sdk';
|
import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk';
|
||||||
import {
|
import {
|
||||||
mdiArchiveArrowDownOutline,
|
mdiArchiveArrowDownOutline,
|
||||||
mdiCameraBurst,
|
mdiCameraBurst,
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
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;
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
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,
|
||||||
@@ -84,6 +86,8 @@
|
|||||||
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();
|
||||||
@@ -267,6 +271,12 @@
|
|||||||
</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,6 +9,7 @@
|
|||||||
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';
|
||||||
|
|
||||||
@@ -25,7 +26,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;
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
monthGroup = $bindable(),
|
monthGroup = $bindable(),
|
||||||
assetInteraction,
|
assetInteraction,
|
||||||
timelineManager,
|
timelineManager,
|
||||||
|
albumUsers = [],
|
||||||
onSelect,
|
onSelect,
|
||||||
onSelectAssets,
|
onSelectAssets,
|
||||||
onSelectAssetCandidates,
|
onSelectAssetCandidates,
|
||||||
@@ -189,6 +191,7 @@
|
|||||||
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,7 +30,13 @@
|
|||||||
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 { AssetVisibility, getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
import {
|
||||||
|
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';
|
||||||
@@ -59,6 +65,7 @@
|
|||||||
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;
|
||||||
@@ -78,6 +85,7 @@
|
|||||||
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 = () => {},
|
||||||
@@ -88,6 +96,12 @@
|
|||||||
|
|
||||||
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();
|
||||||
@@ -936,6 +950,7 @@
|
|||||||
{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,6 +71,7 @@
|
|||||||
} 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,
|
||||||
@@ -104,6 +105,7 @@
|
|||||||
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();
|
||||||
@@ -321,6 +323,11 @@
|
|||||||
|
|
||||||
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) {
|
||||||
@@ -445,6 +452,7 @@
|
|||||||
<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}
|
||||||
@@ -615,6 +623,15 @@
|
|||||||
{#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 { cancelRequest, handleRequest } from './request';
|
import { handleCancel, handlePreload } from './request';
|
||||||
|
|
||||||
export const installBroadcastChannelListener = () => {
|
export const installBroadcastChannelListener = () => {
|
||||||
const broadcast = new BroadcastChannel('immich');
|
const broadcast = new BroadcastChannel('immich');
|
||||||
@@ -7,12 +7,19 @@ export const installBroadcastChannelListener = () => {
|
|||||||
if (!event.data) {
|
if (!event.data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const urlString = event.data.url;
|
|
||||||
const url = new URL(urlString, event.origin);
|
const url = new URL(event.data.url, event.origin);
|
||||||
if (event.data.type === 'cancel') {
|
|
||||||
cancelRequest(url);
|
switch (event.data.type) {
|
||||||
} else if (event.data.type === 'preload') {
|
case 'preload': {
|
||||||
handleRequest(url);
|
handlePreload(url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'cancel': {
|
||||||
|
handleCancel(url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -21,11 +23,16 @@ const getCacheKey = (request: URL | Request) => {
|
|||||||
throw new Error(`Invalid request: ${request}`);
|
throw new Error(`Invalid request: ${request}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pendingRequests = new Map<string, AbortController>();
|
export const handlePreload = async (request: URL | Request) => {
|
||||||
|
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;
|
||||||
@@ -41,23 +48,26 @@ export const handleRequest = async (request: URL | Request) => {
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
if (error.name === 'AbortError') {
|
||||||
return new Response(undefined, {
|
// dummy response avoids network errors in the console for these requests
|
||||||
status: 499,
|
return new Response(undefined, { status: 204 });
|
||||||
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 cancelRequest = (url: URL) => {
|
export const handleCancel = (url: URL) => {
|
||||||
const cacheKey = getCacheKey(url);
|
const cacheKey = getCacheKey(url);
|
||||||
const pending = pendingRequests.get(cacheKey);
|
const pendingRequest = pendingRequests.get(cacheKey);
|
||||||
if (!pending) {
|
if (!pendingRequest) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pending.abort();
|
pendingRequest.abort();
|
||||||
pendingRequests.delete(cacheKey);
|
pendingRequests.delete(cacheKey);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user