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