mirror of https://github.com/immich-app/immich.git
feat(web) Individual assets shared mechanism (#1317)
* Create shared link modal for individual asset * Added API to create asset shared link * Added viewer for individual shared link * Added multiselection app bar * Refactor gallery viewer to its own component * Refactor * Refactor * Add and remove asset from shared link * Fixed test * Fixed notification card doesn't wrap * Add check asset access when created asset shared link * pr feedbackpull/1330/head
parent
b9b2b559a1
commit
e9fda40b2b
@ -0,0 +1,18 @@
|
||||
# openapi.model.CreateAssetsShareLinkDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**assetIds** | **List<String>** | | [default to const []]
|
||||
**expiredAt** | **String** | | [optional]
|
||||
**allowUpload** | **bool** | | [optional]
|
||||
**description** | **String** | | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
# openapi.model.UpdateAssetsToSharedLinkDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**assetIds** | **List<String>** | | [default to const []]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
@ -0,0 +1,164 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class CreateAssetsShareLinkDto {
|
||||
/// Returns a new [CreateAssetsShareLinkDto] instance.
|
||||
CreateAssetsShareLinkDto({
|
||||
this.assetIds = const [],
|
||||
this.expiredAt,
|
||||
this.allowUpload,
|
||||
this.description,
|
||||
});
|
||||
|
||||
List<String> assetIds;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? expiredAt;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? allowUpload;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? description;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is CreateAssetsShareLinkDto &&
|
||||
other.assetIds == assetIds &&
|
||||
other.expiredAt == expiredAt &&
|
||||
other.allowUpload == allowUpload &&
|
||||
other.description == description;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetIds.hashCode) +
|
||||
(expiredAt == null ? 0 : expiredAt!.hashCode) +
|
||||
(allowUpload == null ? 0 : allowUpload!.hashCode) +
|
||||
(description == null ? 0 : description!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'CreateAssetsShareLinkDto[assetIds=$assetIds, expiredAt=$expiredAt, allowUpload=$allowUpload, description=$description]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetIds'] = this.assetIds;
|
||||
if (this.expiredAt != null) {
|
||||
json[r'expiredAt'] = this.expiredAt;
|
||||
} else {
|
||||
// json[r'expiredAt'] = null;
|
||||
}
|
||||
if (this.allowUpload != null) {
|
||||
json[r'allowUpload'] = this.allowUpload;
|
||||
} else {
|
||||
// json[r'allowUpload'] = null;
|
||||
}
|
||||
if (this.description != null) {
|
||||
json[r'description'] = this.description;
|
||||
} else {
|
||||
// json[r'description'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [CreateAssetsShareLinkDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static CreateAssetsShareLinkDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
// Ensure that the map contains the required keys.
|
||||
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||
// Note 2: this code is stripped in release mode!
|
||||
assert(() {
|
||||
requiredKeys.forEach((key) {
|
||||
assert(json.containsKey(key), 'Required key "CreateAssetsShareLinkDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "CreateAssetsShareLinkDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return CreateAssetsShareLinkDto(
|
||||
assetIds: json[r'assetIds'] is List
|
||||
? (json[r'assetIds'] as List).cast<String>()
|
||||
: const [],
|
||||
expiredAt: mapValueOfType<String>(json, r'expiredAt'),
|
||||
allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<CreateAssetsShareLinkDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <CreateAssetsShareLinkDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = CreateAssetsShareLinkDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, CreateAssetsShareLinkDto> mapFromJson(dynamic json) {
|
||||
final map = <String, CreateAssetsShareLinkDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = CreateAssetsShareLinkDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of CreateAssetsShareLinkDto-objects as value to a dart map
|
||||
static Map<String, List<CreateAssetsShareLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<CreateAssetsShareLinkDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = CreateAssetsShareLinkDto.listFromJson(entry.value, growable: growable,);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetIds',
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,113 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class UpdateAssetsToSharedLinkDto {
|
||||
/// Returns a new [UpdateAssetsToSharedLinkDto] instance.
|
||||
UpdateAssetsToSharedLinkDto({
|
||||
this.assetIds = const [],
|
||||
});
|
||||
|
||||
List<String> assetIds;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetsToSharedLinkDto &&
|
||||
other.assetIds == assetIds;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetIds.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UpdateAssetsToSharedLinkDto[assetIds=$assetIds]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetIds'] = this.assetIds;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [UpdateAssetsToSharedLinkDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static UpdateAssetsToSharedLinkDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
// Ensure that the map contains the required keys.
|
||||
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||
// Note 2: this code is stripped in release mode!
|
||||
assert(() {
|
||||
requiredKeys.forEach((key) {
|
||||
assert(json.containsKey(key), 'Required key "UpdateAssetsToSharedLinkDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "UpdateAssetsToSharedLinkDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return UpdateAssetsToSharedLinkDto(
|
||||
assetIds: json[r'assetIds'] is List
|
||||
? (json[r'assetIds'] as List).cast<String>()
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<UpdateAssetsToSharedLinkDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UpdateAssetsToSharedLinkDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UpdateAssetsToSharedLinkDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, UpdateAssetsToSharedLinkDto> mapFromJson(dynamic json) {
|
||||
final map = <String, UpdateAssetsToSharedLinkDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = UpdateAssetsToSharedLinkDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of UpdateAssetsToSharedLinkDto-objects as value to a dart map
|
||||
static Map<String, List<UpdateAssetsToSharedLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<UpdateAssetsToSharedLinkDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = UpdateAssetsToSharedLinkDto.listFromJson(entry.value, growable: growable,);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetIds',
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for CreateAssetsShareLinkDto
|
||||
void main() {
|
||||
// final instance = CreateAssetsShareLinkDto();
|
||||
|
||||
group('test CreateAssetsShareLinkDto', () {
|
||||
// List<String> assetIds (default value: const [])
|
||||
test('to test the property `assetIds`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String expiredAt
|
||||
test('to test the property `expiredAt`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool allowUpload
|
||||
test('to test the property `allowUpload`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String description
|
||||
test('to test the property `description`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for UpdateAssetsToSharedLinkDto
|
||||
void main() {
|
||||
// final instance = UpdateAssetsToSharedLinkDto();
|
||||
|
||||
group('test UpdateAssetsToSharedLinkDto', () {
|
||||
// List<String> assetIds (default value: const [])
|
||||
test('to test the property `assetIds`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class UpdateAssetsToSharedLinkDto {
|
||||
@IsNotEmpty()
|
||||
assetIds!: string[];
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateAssetsShareLinkDto {
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
@ApiProperty({
|
||||
isArray: true,
|
||||
type: String,
|
||||
title: 'Array asset IDs to be shared',
|
||||
example: [
|
||||
'bf973405-3f2a-48d2-a687-2ed4167164be',
|
||||
'dd41870b-5d00-46d2-924e-1d8489a0aa0f',
|
||||
'fad77c3f-deef-4e7e-9608-14c1aa4e559a',
|
||||
],
|
||||
})
|
||||
assetIds!: string[];
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
expiredAt?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
allowUpload?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
}
|
||||
@ -0,0 +1,150 @@
|
||||
<script lang="ts">
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
|
||||
import { api, AssetResponseDto, SharedLinkResponseDto } from '@api';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
|
||||
import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte';
|
||||
import FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { bulkDownload } from '$lib/utils/asset-utils';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
|
||||
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let isOwned: boolean;
|
||||
|
||||
let assets = sharedLink.assets;
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
|
||||
const clearMultiSelectAssetAssetHandler = () => {
|
||||
selectedAssets = new Set();
|
||||
};
|
||||
|
||||
const downloadAssets = async (isAll: boolean) => {
|
||||
await bulkDownload(
|
||||
'immich-shared',
|
||||
isAll ? assets : Array.from(selectedAssets),
|
||||
() => {
|
||||
isMultiSelectionMode = false;
|
||||
clearMultiSelectAssetAssetHandler();
|
||||
},
|
||||
sharedLink?.key
|
||||
);
|
||||
};
|
||||
|
||||
const handleUploadAssets = () => {
|
||||
openFileUploadDialog(undefined, sharedLink?.key, async (assetId) => {
|
||||
await api.assetApi.updateAssetsInSharedLink(
|
||||
{
|
||||
assetIds: [...assets.map((a) => a.id), assetId]
|
||||
},
|
||||
{
|
||||
params: {
|
||||
key: sharedLink?.key
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
notificationController.show({
|
||||
message: 'Add asset to shared link successfully',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveAssetsFromSharedLink = async () => {
|
||||
if (window.confirm('Do you want to remove selected assets from the shared link?')) {
|
||||
await api.assetApi.updateAssetsInSharedLink(
|
||||
{
|
||||
assetIds: assets.filter((a) => !selectedAssets.has(a)).map((a) => a.id)
|
||||
},
|
||||
{
|
||||
params: {
|
||||
key: sharedLink?.key
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assets = assets.filter((a) => !selectedAssets.has(a));
|
||||
clearMultiSelectAssetAssetHandler();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="bg-immich-bg dark:bg-immich-dark-bg">
|
||||
{#if isMultiSelectionMode}
|
||||
<ControlAppBar
|
||||
on:close-button-click={clearMultiSelectAssetAssetHandler}
|
||||
backIcon={Close}
|
||||
tailwindClasses={'bg-white shadow-md'}
|
||||
>
|
||||
<svelte:fragment slot="leading">
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
Selected {selectedAssets.size}
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="trailing">
|
||||
<CircleIconButton
|
||||
title="Download"
|
||||
on:click={() => downloadAssets(false)}
|
||||
logo={CloudDownloadOutline}
|
||||
/>
|
||||
{#if isOwned}
|
||||
<CircleIconButton
|
||||
title="Remove from album"
|
||||
on:click={handleRemoveAssetsFromSharedLink}
|
||||
logo={DeleteOutline}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
{:else}
|
||||
<ControlAppBar
|
||||
on:close-button-click={() => goto('/photos')}
|
||||
backIcon={ArrowLeft}
|
||||
showBackButton={false}
|
||||
>
|
||||
<svelte:fragment slot="leading">
|
||||
<a
|
||||
data-sveltekit-preload-data="hover"
|
||||
class="flex gap-2 place-items-center hover:cursor-pointer ml-6"
|
||||
href="https://immich.app"
|
||||
>
|
||||
<img src="/immich-logo.svg" alt="immich logo" height="30" width="30" />
|
||||
<h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary">
|
||||
IMMICH
|
||||
</h1>
|
||||
</a>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="trailing">
|
||||
{#if sharedLink?.allowUpload}
|
||||
<CircleIconButton
|
||||
title="Add Photos"
|
||||
on:click={handleUploadAssets}
|
||||
logo={FileImagePlusOutline}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<CircleIconButton
|
||||
title="Download"
|
||||
on:click={() => downloadAssets(true)}
|
||||
logo={FolderDownloadOutline}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
<section class="flex flex-col my-[160px] px-6 sm:px-12 md:px-24 lg:px-40">
|
||||
<GalleryViewer {assets} key={sharedLink.key} bind:selectedAssets />
|
||||
</section>
|
||||
</section>
|
||||
@ -0,0 +1,118 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AssetResponseDto, ThumbnailFormat } from '@api';
|
||||
|
||||
import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
|
||||
import ImmichThumbnail from '../../shared-components/immich-thumbnail.svelte';
|
||||
|
||||
export let assets: AssetResponseDto[];
|
||||
export let key: string;
|
||||
export let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
|
||||
let isShowAssetViewer = false;
|
||||
|
||||
let selectedAsset: AssetResponseDto;
|
||||
let currentViewAssetIndex = 0;
|
||||
|
||||
let viewWidth: number;
|
||||
let thumbnailSize = 300;
|
||||
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
|
||||
$: {
|
||||
if (assets.length < 6) {
|
||||
thumbnailSize = Math.floor(viewWidth / assets.length - assets.length);
|
||||
} else {
|
||||
if (viewWidth > 600) thumbnailSize = Math.floor(viewWidth / 6 - 6);
|
||||
else if (viewWidth > 400) thumbnailSize = Math.floor(viewWidth / 4 - 6);
|
||||
else if (viewWidth > 300) thumbnailSize = Math.floor(viewWidth / 2 - 6);
|
||||
else if (viewWidth > 200) thumbnailSize = Math.floor(viewWidth / 2 - 6);
|
||||
else if (viewWidth > 100) thumbnailSize = Math.floor(viewWidth / 1 - 6);
|
||||
}
|
||||
}
|
||||
|
||||
const viewAssetHandler = (event: CustomEvent) => {
|
||||
const { asset }: { asset: AssetResponseDto } = event.detail;
|
||||
|
||||
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
|
||||
selectedAsset = assets[currentViewAssetIndex];
|
||||
isShowAssetViewer = true;
|
||||
pushState(selectedAsset.id);
|
||||
};
|
||||
|
||||
const selectAssetHandler = (event: CustomEvent) => {
|
||||
const { asset }: { asset: AssetResponseDto } = event.detail;
|
||||
let temp = new Set(selectedAssets);
|
||||
|
||||
if (selectedAssets.has(asset)) {
|
||||
temp.delete(asset);
|
||||
} else {
|
||||
temp.add(asset);
|
||||
}
|
||||
|
||||
selectedAssets = temp;
|
||||
};
|
||||
|
||||
const navigateAssetForward = () => {
|
||||
try {
|
||||
if (currentViewAssetIndex < assets.length - 1) {
|
||||
currentViewAssetIndex++;
|
||||
selectedAsset = assets[currentViewAssetIndex];
|
||||
pushState(selectedAsset.id);
|
||||
}
|
||||
} catch (e) {
|
||||
handleError(e, 'Cannot navigate to the next asset');
|
||||
}
|
||||
};
|
||||
|
||||
const navigateAssetBackward = () => {
|
||||
try {
|
||||
if (currentViewAssetIndex > 0) {
|
||||
currentViewAssetIndex--;
|
||||
selectedAsset = assets[currentViewAssetIndex];
|
||||
pushState(selectedAsset.id);
|
||||
}
|
||||
} catch (e) {
|
||||
handleError(e, 'Cannot navigate to previous asset');
|
||||
}
|
||||
};
|
||||
|
||||
const pushState = (assetId: string) => {
|
||||
// add a URL to the browser's history
|
||||
// changes the current URL in the address bar but doesn't perform any SvelteKit navigation
|
||||
history.pushState(null, '', `${$page.url.pathname}/photos/${assetId}`);
|
||||
};
|
||||
|
||||
const closeViewer = () => {
|
||||
isShowAssetViewer = false;
|
||||
history.pushState(null, '', `${$page.url.pathname}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if assets.length > 0}
|
||||
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
|
||||
{#each assets as asset (asset.id)}
|
||||
<ImmichThumbnail
|
||||
{asset}
|
||||
{thumbnailSize}
|
||||
publicSharedKey={key}
|
||||
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
|
||||
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
|
||||
on:select={selectAssetHandler}
|
||||
selected={selectedAssets.has(asset)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Overlay Asset Viewer -->
|
||||
{#if isShowAssetViewer}
|
||||
<AssetViewer
|
||||
asset={selectedAsset}
|
||||
publicSharedKey={key}
|
||||
on:navigate-previous={navigateAssetBackward}
|
||||
on:navigate-next={navigateAssetForward}
|
||||
on:close={closeViewer}
|
||||
/>
|
||||
{/if}
|
||||
Loading…
Reference in New Issue