mirror of
https://github.com/immich-app/immich.git
synced 2026-05-29 11:02:38 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f1b8e1d9b | |||
| 6e78d6e131 | |||
| aecf8ec88b | |||
| bcff1d42b0 |
@@ -1,46 +1,46 @@
|
||||
dev:
|
||||
@printf "This command has been removed. Please use:\n\n mise dev # or mise //:dev from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise dev # or mise //:dev from another directory\n\n" >&2 && exit 1
|
||||
|
||||
dev-down:
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-down # or mise //:dev-down from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-down # or mise //:dev-down from another directory\n\n" >&2 && exit 1
|
||||
|
||||
dev-update:
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-update # or mise //:dev-update from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-update # or mise //:dev-update from another directory\n\n" >&2 && exit 1
|
||||
|
||||
dev-scale:
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-scale # or mise //:dev-scale from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-scale # or mise //:dev-scale from another directory\n\n" >&2 && exit 1
|
||||
|
||||
dev-docs:
|
||||
npm --prefix docs run start
|
||||
|
||||
.PHONY: e2e
|
||||
e2e:
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e # or mise //:e2e from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e # or mise //:e2e from another directory\n\n" >&2 && exit 1
|
||||
|
||||
e2e-dev:
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-dev # or mise //:e2e-dev from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-dev # or mise //:e2e-dev from another directory\n\n" >&2 && exit 1
|
||||
|
||||
e2e-update:
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-update # or mise //:e2e-update from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-update # or mise //:e2e-update from another directory\n\n" >&2 && exit 1
|
||||
|
||||
e2e-down:
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-down # or mise //:e2e-down from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-down # or mise //:e2e-down from another directory\n\n" >&2 && exit 1
|
||||
|
||||
prod:
|
||||
@printf "This command has been removed. Please use:\n\n mise prod # or mise //:prod from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise prod # or mise //:prod from another directory\n\n" >&2 && exit 1
|
||||
|
||||
prod-down:
|
||||
@printf "This command has been removed. Please use:\n\n mise prod-down # or mise //:prod-down from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise prod-down # or mise //:prod-down from another directory\n\n" >&2 && exit 1
|
||||
|
||||
prod-scale:
|
||||
@printf "This command has been removed. Please use:\n\n mise prod-scale # or mise //:prod-scale from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise prod-scale # or mise //:prod-scale from another directory\n\n" >&2 && exit 1
|
||||
|
||||
.PHONY: open-api
|
||||
open-api:
|
||||
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n" >&2 && exit 1
|
||||
|
||||
sql:
|
||||
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n" >&2 && exit 1
|
||||
|
||||
|
||||
renovate:
|
||||
@@ -52,16 +52,7 @@ renovate:
|
||||
MODULES = e2e server web cli sdk docs .github
|
||||
|
||||
test-e2e:
|
||||
docker compose -f ./e2e/docker-compose.yml build
|
||||
pnpm --filter immich-e2e run test
|
||||
pnpm --filter immich-e2e run test:web
|
||||
@printf "This command has been removed. Please use:\n\n mise //e2e:test # or mise //e2e:test-web for web tests, respectively\n\n" >&2 && exit 1
|
||||
|
||||
clean:
|
||||
find . -name "node_modules" -type d -prune -exec rm -rf {} +
|
||||
find . -name "dist" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name "build" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name ".svelte-kit" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name "coverage" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' +
|
||||
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml down -v --remove-orphans || true
|
||||
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true
|
||||
@printf "This command has been removed. Please use:\n\n mise clean # or mise //:clean from another directory\n\n" >&2 && exit 1
|
||||
|
||||
+16
-1
@@ -1,11 +1,21 @@
|
||||
[tasks.install]
|
||||
run = "pnpm install --filter immich-e2e --frozen-lockfile"
|
||||
|
||||
[tasks.build]
|
||||
dir = "{{ config_root }}"
|
||||
run = "docker compose build"
|
||||
|
||||
[tasks.test]
|
||||
depends = ["//e2e:build", "//e2e:ci-setup"]
|
||||
env._.path = "./node_modules/.bin"
|
||||
run = "vitest --run"
|
||||
|
||||
[tasks.playwright-install]
|
||||
env._.path = "./node_modules/.bin"
|
||||
run = "playwright install"
|
||||
|
||||
[tasks."test-web"]
|
||||
depends = ["//e2e:build", "//e2e:ci-setup", "//e2e:playwright-install"]
|
||||
env._.path = "./node_modules/.bin"
|
||||
run = "playwright test"
|
||||
|
||||
@@ -30,7 +40,12 @@ run = "tsc --noEmit"
|
||||
|
||||
|
||||
[tasks.ci-setup]
|
||||
depends = ["//:sdk:install", "//:sdk:build", "//cli:install", "//cli:build"]
|
||||
depends = [
|
||||
"//:sdk:install",
|
||||
"//:sdk:build",
|
||||
"//packages/cli:install",
|
||||
"//packages/cli:build",
|
||||
]
|
||||
run = { task = ":install" }
|
||||
|
||||
|
||||
|
||||
@@ -165,3 +165,14 @@ run = "pnpm format"
|
||||
|
||||
[tasks."i18n:format-fix"]
|
||||
run = "pnpm format:fix"
|
||||
|
||||
[tasks.clean]
|
||||
run = [
|
||||
"find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name 'dist' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name 'build' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name '.svelte-kit' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name 'coverage' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name '.pnpm-store' -type d -prune -exec rm -rf '{}' +",
|
||||
{ task = "//:*-down" },
|
||||
]
|
||||
|
||||
Generated
+1
@@ -450,6 +450,7 @@ Class | Method | HTTP request | Description
|
||||
- [MemoryCreateDto](doc//MemoryCreateDto.md)
|
||||
- [MemoryResponseDto](doc//MemoryResponseDto.md)
|
||||
- [MemorySearchOrder](doc//MemorySearchOrder.md)
|
||||
- [MemorySearchResponseDto](doc//MemorySearchResponseDto.md)
|
||||
- [MemoryStatisticsResponseDto](doc//MemoryStatisticsResponseDto.md)
|
||||
- [MemoryType](doc//MemoryType.md)
|
||||
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
|
||||
|
||||
Generated
+1
@@ -195,6 +195,7 @@ part 'model/memories_update.dart';
|
||||
part 'model/memory_create_dto.dart';
|
||||
part 'model/memory_response_dto.dart';
|
||||
part 'model/memory_search_order.dart';
|
||||
part 'model/memory_search_response_dto.dart';
|
||||
part 'model/memory_statistics_response_dto.dart';
|
||||
part 'model/memory_type.dart';
|
||||
part 'model/memory_update_dto.dart';
|
||||
|
||||
Generated
+26
-11
@@ -261,11 +261,14 @@ class MemoriesApi {
|
||||
///
|
||||
/// * [MemorySearchOrder] order:
|
||||
///
|
||||
/// * [int] page:
|
||||
/// Page number
|
||||
///
|
||||
/// * [int] size:
|
||||
/// Number of memories to return
|
||||
///
|
||||
/// * [MemoryType] type:
|
||||
Future<Response> memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async {
|
||||
Future<Response> memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? page, int? size, MemoryType? type, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/memories/statistics';
|
||||
|
||||
@@ -288,6 +291,9 @@ class MemoriesApi {
|
||||
if (order != null) {
|
||||
queryParams.addAll(_queryParams('', 'order', order));
|
||||
}
|
||||
if (page != null) {
|
||||
queryParams.addAll(_queryParams('', 'page', page));
|
||||
}
|
||||
if (size != null) {
|
||||
queryParams.addAll(_queryParams('', 'size', size));
|
||||
}
|
||||
@@ -326,12 +332,15 @@ class MemoriesApi {
|
||||
///
|
||||
/// * [MemorySearchOrder] order:
|
||||
///
|
||||
/// * [int] page:
|
||||
/// Page number
|
||||
///
|
||||
/// * [int] size:
|
||||
/// Number of memories to return
|
||||
///
|
||||
/// * [MemoryType] type:
|
||||
Future<MemoryStatisticsResponseDto?> memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async {
|
||||
final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, );
|
||||
Future<MemoryStatisticsResponseDto?> memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? page, int? size, MemoryType? type, }) async {
|
||||
final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, page: page, size: size, type: type, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
@@ -428,11 +437,14 @@ class MemoriesApi {
|
||||
///
|
||||
/// * [MemorySearchOrder] order:
|
||||
///
|
||||
/// * [int] page:
|
||||
/// Page number
|
||||
///
|
||||
/// * [int] size:
|
||||
/// Number of memories to return
|
||||
///
|
||||
/// * [MemoryType] type:
|
||||
Future<Response> searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async {
|
||||
Future<Response> searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? page, int? size, MemoryType? type, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/memories';
|
||||
|
||||
@@ -455,6 +467,9 @@ class MemoriesApi {
|
||||
if (order != null) {
|
||||
queryParams.addAll(_queryParams('', 'order', order));
|
||||
}
|
||||
if (page != null) {
|
||||
queryParams.addAll(_queryParams('', 'page', page));
|
||||
}
|
||||
if (size != null) {
|
||||
queryParams.addAll(_queryParams('', 'size', size));
|
||||
}
|
||||
@@ -493,12 +508,15 @@ class MemoriesApi {
|
||||
///
|
||||
/// * [MemorySearchOrder] order:
|
||||
///
|
||||
/// * [int] page:
|
||||
/// Page number
|
||||
///
|
||||
/// * [int] size:
|
||||
/// Number of memories to return
|
||||
///
|
||||
/// * [MemoryType] type:
|
||||
Future<List<MemoryResponseDto>?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async {
|
||||
final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, );
|
||||
Future<MemorySearchResponseDto?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? page, int? size, MemoryType? type, }) async {
|
||||
final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, page: page, size: size, type: type, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
@@ -506,11 +524,8 @@ class MemoriesApi {
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<MemoryResponseDto>') as List)
|
||||
.cast<MemoryResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MemorySearchResponseDto',) as MemorySearchResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Generated
+2
@@ -436,6 +436,8 @@ class ApiClient {
|
||||
return MemoryResponseDto.fromJson(value);
|
||||
case 'MemorySearchOrder':
|
||||
return MemorySearchOrderTypeTransformer().decode(value);
|
||||
case 'MemorySearchResponseDto':
|
||||
return MemorySearchResponseDto.fromJson(value);
|
||||
case 'MemoryStatisticsResponseDto':
|
||||
return MemoryStatisticsResponseDto.fromJson(value);
|
||||
case 'MemoryType':
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// 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 MemorySearchResponseDto {
|
||||
/// Returns a new [MemorySearchResponseDto] instance.
|
||||
MemorySearchResponseDto({
|
||||
required this.hasNextPage,
|
||||
this.items = const [],
|
||||
required this.total,
|
||||
});
|
||||
|
||||
/// Whether there are more pages
|
||||
bool hasNextPage;
|
||||
|
||||
List<MemoryResponseDto> items;
|
||||
|
||||
/// Total number of matching memories
|
||||
///
|
||||
/// Minimum value: 0
|
||||
/// Maximum value: 9007199254740991
|
||||
int total;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is MemorySearchResponseDto &&
|
||||
other.hasNextPage == hasNextPage &&
|
||||
_deepEquality.equals(other.items, items) &&
|
||||
other.total == total;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(hasNextPage.hashCode) +
|
||||
(items.hashCode) +
|
||||
(total.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'MemorySearchResponseDto[hasNextPage=$hasNextPage, items=$items, total=$total]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'hasNextPage'] = this.hasNextPage;
|
||||
json[r'items'] = this.items;
|
||||
json[r'total'] = this.total;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [MemorySearchResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static MemorySearchResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "MemorySearchResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return MemorySearchResponseDto(
|
||||
hasNextPage: mapValueOfType<bool>(json, r'hasNextPage')!,
|
||||
items: MemoryResponseDto.listFromJson(json[r'items']),
|
||||
total: mapValueOfType<int>(json, r'total')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<MemorySearchResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <MemorySearchResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = MemorySearchResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, MemorySearchResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, MemorySearchResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = MemorySearchResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of MemorySearchResponseDto-objects as value to a dart map
|
||||
static Map<String, List<MemorySearchResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<MemorySearchResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = MemorySearchResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'hasNextPage',
|
||||
'items',
|
||||
'total',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6363,6 +6363,17 @@
|
||||
"$ref": "#/components/schemas/MemorySearchOrder"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Page number",
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"maximum": 9007199254740991,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"required": false,
|
||||
@@ -6388,10 +6399,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MemoryResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
"$ref": "#/components/schemas/MemorySearchResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6532,6 +6540,17 @@
|
||||
"$ref": "#/components/schemas/MemorySearchOrder"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Page number",
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"maximum": 9007199254740991,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"required": false,
|
||||
@@ -18807,6 +18826,32 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MemorySearchResponseDto": {
|
||||
"properties": {
|
||||
"hasNextPage": {
|
||||
"description": "Whether there are more pages",
|
||||
"type": "boolean"
|
||||
},
|
||||
"items": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/MemoryResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"total": {
|
||||
"description": "Total number of matching memories",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hasNextPage",
|
||||
"items",
|
||||
"total"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"MemoryStatisticsResponseDto": {
|
||||
"properties": {
|
||||
"total": {
|
||||
|
||||
@@ -1316,6 +1316,13 @@ export type MemoryResponseDto = {
|
||||
/** Last update date */
|
||||
updatedAt: string;
|
||||
};
|
||||
export type MemorySearchResponseDto = {
|
||||
/** Whether there are more pages */
|
||||
hasNextPage: boolean;
|
||||
items: MemoryResponseDto[];
|
||||
/** Total number of matching memories */
|
||||
total: number;
|
||||
};
|
||||
export type MemoryCreateDto = {
|
||||
/** Asset IDs to associate with memory */
|
||||
assetIds?: string[];
|
||||
@@ -4678,22 +4685,24 @@ export function reverseGeocode({ lat, lon }: {
|
||||
/**
|
||||
* Retrieve memories
|
||||
*/
|
||||
export function searchMemories({ $for, isSaved, isTrashed, order, size, $type }: {
|
||||
export function searchMemories({ $for, isSaved, isTrashed, order, page, size, $type }: {
|
||||
$for?: string;
|
||||
isSaved?: boolean;
|
||||
isTrashed?: boolean;
|
||||
order?: MemorySearchOrder;
|
||||
page?: number;
|
||||
size?: number;
|
||||
$type?: MemoryType;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: MemoryResponseDto[];
|
||||
data: MemorySearchResponseDto;
|
||||
}>(`/memories${QS.query(QS.explode({
|
||||
"for": $for,
|
||||
isSaved,
|
||||
isTrashed,
|
||||
order,
|
||||
page,
|
||||
size,
|
||||
"type": $type
|
||||
}))}`, {
|
||||
@@ -4718,11 +4727,12 @@ export function createMemory({ memoryCreateDto }: {
|
||||
/**
|
||||
* Retrieve memories statistics
|
||||
*/
|
||||
export function memoriesStatistics({ $for, isSaved, isTrashed, order, size, $type }: {
|
||||
export function memoriesStatistics({ $for, isSaved, isTrashed, order, page, size, $type }: {
|
||||
$for?: string;
|
||||
isSaved?: boolean;
|
||||
isTrashed?: boolean;
|
||||
order?: MemorySearchOrder;
|
||||
page?: number;
|
||||
size?: number;
|
||||
$type?: MemoryType;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
@@ -4734,6 +4744,7 @@ export function memoriesStatistics({ $for, isSaved, isTrashed, order, size, $typ
|
||||
isSaved,
|
||||
isTrashed,
|
||||
order,
|
||||
page,
|
||||
size,
|
||||
"type": $type
|
||||
}))}`, {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
MemoryCreateDto,
|
||||
MemoryResponseDto,
|
||||
MemorySearchDto,
|
||||
MemorySearchResponseDto,
|
||||
MemoryStatisticsResponseDto,
|
||||
MemoryUpdateDto,
|
||||
} from 'src/dtos/memory.dto';
|
||||
@@ -28,7 +29,7 @@ export class MemoryController {
|
||||
'Retrieve a list of memories. Memories are sorted descending by creation date by default, although they can also be sorted in ascending order, or randomly.',
|
||||
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
|
||||
})
|
||||
searchMemories(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemoryResponseDto[]> {
|
||||
searchMemories(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemorySearchResponseDto> {
|
||||
return this.service.search(auth, dto);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const MemorySearchSchema = z
|
||||
isTrashed: stringToBool.optional().describe('Include trashed memories'),
|
||||
isSaved: stringToBool.optional().describe('Filter by saved status'),
|
||||
size: z.coerce.number().int().min(1).optional().describe('Number of memories to return'),
|
||||
page: z.coerce.number().int().min(1).optional().describe('Page number'),
|
||||
order: AssetOrderWithRandomSchema.optional(),
|
||||
})
|
||||
.meta({ id: 'MemorySearchDto' });
|
||||
@@ -75,11 +76,20 @@ const MemoryResponseSchema = z
|
||||
})
|
||||
.meta({ id: 'MemoryResponseDto' });
|
||||
|
||||
const MemorySearchResponseSchema = z
|
||||
.object({
|
||||
total: z.int().min(0).describe('Total number of matching memories'),
|
||||
items: z.array(MemoryResponseSchema),
|
||||
hasNextPage: z.boolean().describe('Whether there are more pages'),
|
||||
})
|
||||
.meta({ id: 'MemorySearchResponseDto' });
|
||||
|
||||
export class MemorySearchDto extends createZodDto(MemorySearchSchema) {}
|
||||
export class MemoryUpdateDto extends createZodDto(MemoryUpdateSchema) {}
|
||||
export class MemoryCreateDto extends createZodDto(MemoryCreateSchema) {}
|
||||
export class MemoryStatisticsResponseDto extends createZodDto(MemoryStatisticsResponseSchema) {}
|
||||
export class MemoryResponseDto extends createZodDto(MemoryResponseSchema) {}
|
||||
export class MemorySearchResponseDto extends createZodDto(MemorySearchResponseSchema) {}
|
||||
|
||||
export const mapMemory = (entity: Memory, auth: AuthDto): MemoryResponseDto => {
|
||||
return {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AssetOrderWithRandom, AssetVisibility } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { MemoryTable } from 'src/schema/tables/memory.table';
|
||||
import { IBulkAsset } from 'src/types';
|
||||
import { paginationHelper } from 'src/utils/pagination';
|
||||
|
||||
@Injectable()
|
||||
export class MemoryRepository implements IBulkAsset {
|
||||
@@ -57,8 +58,8 @@ export class MemoryRepository implements IBulkAsset {
|
||||
{ params: [DummyValue.UUID, {}] },
|
||||
{ name: 'date filter', params: [DummyValue.UUID, { for: DummyValue.DATE }] },
|
||||
)
|
||||
search(ownerId: string, dto: MemorySearchDto) {
|
||||
return this.searchBuilder(ownerId, dto)
|
||||
async search(ownerId: string, dto: MemorySearchDto) {
|
||||
const items = await this.searchBuilder(ownerId, dto)
|
||||
.select((eb) =>
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
@@ -89,8 +90,11 @@ export class MemoryRepository implements IBulkAsset {
|
||||
? qb.orderBy(sql`RANDOM()`)
|
||||
: qb.orderBy('memoryAt', (dto.order?.toLowerCase() || 'desc') as OrderByDirection),
|
||||
)
|
||||
.$if(dto.size !== undefined, (qb) => qb.limit(dto.size!))
|
||||
.$if(dto.size !== undefined, (qb) => qb.limit(dto.size! + 1))
|
||||
.$if(dto.page !== undefined && dto.size !== undefined, (qb) => qb.offset((dto.page! - 1) * dto.size!))
|
||||
.execute();
|
||||
|
||||
return paginationHelper(items, dto.size ?? items.length);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
|
||||
@@ -34,21 +34,28 @@ describe(MemoryService.name, () => {
|
||||
const asset = AssetFactory.create();
|
||||
const memory1 = MemoryFactory.from({ ownerId: userId }).asset(asset).build();
|
||||
const memory2 = MemoryFactory.create({ ownerId: userId });
|
||||
mocks.memory.search.mockResolvedValue([getForMemory(memory1), getForMemory(memory2)]);
|
||||
mocks.memory.search.mockResolvedValue({
|
||||
items: [getForMemory(memory1), getForMemory(memory2)],
|
||||
hasNextPage: false,
|
||||
});
|
||||
mocks.memory.statistics.mockResolvedValue({ total: 2 });
|
||||
|
||||
await expect(sut.search(factory.auth({ user: { id: userId } }), {})).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
await expect(sut.search(factory.auth({ user: { id: userId } }), {})).resolves.toMatchObject({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: memory1.id,
|
||||
assets: expect.arrayContaining([expect.objectContaining({ id: asset.id })]),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
hasNextPage: false,
|
||||
total: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should map empty result', async () => {
|
||||
mocks.memory.search.mockResolvedValue([]);
|
||||
await expect(sut.search(factory.auth(), {})).resolves.toEqual([]);
|
||||
mocks.memory.search.mockResolvedValue({ items: [], hasNextPage: false });
|
||||
mocks.memory.statistics.mockResolvedValue({ total: 0 });
|
||||
await expect(sut.search(factory.auth(), {})).resolves.toMatchObject({ items: [], hasNextPage: false, total: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -71,10 +71,14 @@ export class MemoryService extends BaseService {
|
||||
}
|
||||
|
||||
async search(auth: AuthDto, dto: MemorySearchDto) {
|
||||
const memories = await this.memoryRepository.search(auth.user.id, dto);
|
||||
return memories
|
||||
.filter((memory: Memory) => memory.assets && memory.assets.length > 0)
|
||||
.map((memory: Memory) => mapMemory(memory, auth));
|
||||
const { items, hasNextPage } = await this.memoryRepository.search(auth.user.id, dto);
|
||||
const { total } = await this.memoryRepository.statistics(auth.user.id, dto);
|
||||
|
||||
return {
|
||||
total,
|
||||
items: items.map((memory: Memory) => mapMemory(memory, auth)),
|
||||
hasNextPage,
|
||||
};
|
||||
}
|
||||
|
||||
statistics(auth: AuthDto, dto: MemorySearchDto) {
|
||||
|
||||
@@ -133,8 +133,8 @@ describe(MemoryService.name, () => {
|
||||
await sut.onMemoriesCreate();
|
||||
|
||||
const memories = await memoryRepo.search(user.id, {});
|
||||
expect(memories.length).toBe(1);
|
||||
expect(memories[0]).toEqual(
|
||||
expect(memories.items.length).toBe(1);
|
||||
expect(memories.items[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(Date),
|
||||
@@ -173,8 +173,8 @@ describe(MemoryService.name, () => {
|
||||
await sut.onMemoriesCreate();
|
||||
|
||||
const memories = await memoryRepo.search(user.id, {});
|
||||
expect(memories.length).toBe(1);
|
||||
expect(memories[0]).toEqual(
|
||||
expect(memories.items.length).toBe(1);
|
||||
expect(memories.items[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(Date),
|
||||
@@ -228,12 +228,12 @@ describe(MemoryService.name, () => {
|
||||
await sut.onMemoriesCreate();
|
||||
|
||||
const memories = await memoryRepo.search(user.id, {});
|
||||
expect(memories.length).toBe(1);
|
||||
expect(memories.items.length).toBe(1);
|
||||
|
||||
await sut.onMemoriesCreate();
|
||||
|
||||
const memoriesAfter = await memoryRepo.search(user.id, {});
|
||||
expect(memoriesAfter.length).toBe(1);
|
||||
expect(memoriesAfter.items.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -205,11 +205,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={[
|
||||
'group flex overflow-hidden transition-[background-color,border-radius] focus-visible:outline-none',
|
||||
backgroundColorClass,
|
||||
{ 'rounded-xl': selected },
|
||||
]}
|
||||
class={['group flex overflow-hidden focus-visible:outline-none', backgroundColorClass, { 'rounded-xl': selected }]}
|
||||
style:width="{width}px"
|
||||
style:height="{height}px"
|
||||
onmouseenter={onMouseEnter}
|
||||
@@ -249,16 +245,8 @@
|
||||
]}
|
||||
>
|
||||
<ImageThumbnail
|
||||
class={[
|
||||
'absolute transition-[border-radius] group-focus-visible:rounded-lg',
|
||||
{ 'rounded-xl': selected },
|
||||
imageClass,
|
||||
]}
|
||||
brokenAssetClass={[
|
||||
'z-1 absolute group-focus-visible:rounded-lg transition-[border-radius]',
|
||||
{ 'rounded-xl': selected },
|
||||
brokenAssetClass,
|
||||
]}
|
||||
class={['absolute group-focus-visible:rounded-lg', { 'rounded-xl': selected }, imageClass]}
|
||||
brokenAssetClass={['z-1 absolute group-focus-visible:rounded-lg', { 'rounded-xl': selected }, brokenAssetClass]}
|
||||
url={getAssetMediaUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })}
|
||||
altText={$getAltText(asset)}
|
||||
widthStyle="{width}px"
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { deleteMemory, type MemoryResponseDto, removeMemoryAssets, searchMemories, updateMemory } from '@immich/sdk';
|
||||
import {
|
||||
deleteMemory,
|
||||
type MemoryResponseDto,
|
||||
removeMemoryAssets,
|
||||
searchMemories,
|
||||
updateMemory,
|
||||
MemorySearchOrder,
|
||||
MemoryType,
|
||||
} from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { asLocalTimeISO } from '$lib/utils/date-time';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
|
||||
type MemoryIndex = {
|
||||
@@ -20,10 +27,31 @@ export type MemoryAsset = MemoryIndex & {
|
||||
nextMemory?: MemoryResponseDto;
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 250;
|
||||
|
||||
class MemoryManager {
|
||||
#loading: Promise<void> | undefined;
|
||||
#filters:
|
||||
| {
|
||||
$for?: string;
|
||||
isSaved?: boolean;
|
||||
isTrashed?: boolean;
|
||||
order?: MemorySearchOrder;
|
||||
page?: number;
|
||||
size?: number;
|
||||
$type?: MemoryType;
|
||||
}
|
||||
| undefined;
|
||||
#hasNextPage: boolean;
|
||||
#page: number;
|
||||
#total: number;
|
||||
|
||||
constructor() {
|
||||
this.#filters = undefined;
|
||||
this.#hasNextPage = true;
|
||||
this.#page = 1;
|
||||
this.#total = 0;
|
||||
|
||||
eventManager.on({
|
||||
AuthLogout: () => this.clearCache(),
|
||||
AuthUserLoaded: () => this.initialize(),
|
||||
@@ -37,6 +65,16 @@ class MemoryManager {
|
||||
this.scheduleHourlyRefresh();
|
||||
}
|
||||
|
||||
get filters() {
|
||||
return this.#filters;
|
||||
}
|
||||
|
||||
set filters(filters) {
|
||||
this.#filters = filters;
|
||||
this.clearCache();
|
||||
void this.loadNextPage();
|
||||
}
|
||||
|
||||
ready() {
|
||||
return this.initialize();
|
||||
}
|
||||
@@ -117,22 +155,46 @@ class MemoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
loadNextPage() {
|
||||
if (this.#hasNextPage) {
|
||||
if (this.#loading === undefined) {
|
||||
this.#loading = this.load(this.#page++);
|
||||
} else {
|
||||
void this.#loading.then(() => (this.#loading = this.load(this.#page++)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get hasNextPage() {
|
||||
return this.#hasNextPage;
|
||||
}
|
||||
|
||||
get total() {
|
||||
return this.#total;
|
||||
}
|
||||
|
||||
private clearCache() {
|
||||
this.#loading = undefined;
|
||||
this.#hasNextPage = true;
|
||||
this.#page = 1;
|
||||
this.memories = [];
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
if (!this.#loading) {
|
||||
this.#loading = this.load();
|
||||
this.#loading = this.load(this.#page++);
|
||||
}
|
||||
|
||||
return this.#loading;
|
||||
}
|
||||
|
||||
private async load() {
|
||||
const memories = await searchMemories({ $for: asLocalTimeISO(DateTime.now()) });
|
||||
this.memories = memories.filter((memory) => memory.assets.length > 0);
|
||||
private async load(page: number) {
|
||||
if (this.#filters !== undefined) {
|
||||
const { items, hasNextPage, total } = await searchMemories({ ...this.#filters, page, size: PAGE_SIZE });
|
||||
this.memories.push(...items);
|
||||
this.#hasNextPage = hasNextPage;
|
||||
this.#total = total;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleHourlyRefresh() {
|
||||
@@ -146,12 +208,14 @@ class MemoryManager {
|
||||
const initialDelay = nextEvent.diff(now).as('milliseconds');
|
||||
|
||||
setTimeout(() => {
|
||||
this.#loading = this.load();
|
||||
this.clearCache();
|
||||
this.#loading = this.load(0);
|
||||
|
||||
// Schedule subsequent events hourly
|
||||
setInterval(
|
||||
() => {
|
||||
this.#loading = this.load();
|
||||
this.clearCache();
|
||||
this.#loading = this.load(0);
|
||||
},
|
||||
60 * 60 * 1000,
|
||||
);
|
||||
|
||||
@@ -20,12 +20,13 @@ import {
|
||||
type UserResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { toastManager, type ActionItem, type IfLike } from '@immich/ui';
|
||||
import { DateTime } from 'luxon';
|
||||
import { init, register, t } from 'svelte-i18n';
|
||||
import { derived, get } from 'svelte/store';
|
||||
import { defaultLang, locales } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
||||
import { alwaysLoadOriginalFile, lang } from '$lib/stores/preferences.store';
|
||||
import { alwaysLoadOriginalFile, lang, locale } from '$lib/stores/preferences.store';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { langs } from '$lib/utils/i18n';
|
||||
@@ -358,9 +359,13 @@ export const handlePromiseError = <T>(promise: Promise<T>): void => {
|
||||
|
||||
export const memoryLaneTitle = derived(t, ($t) => {
|
||||
return (memory: MemoryResponseDto) => {
|
||||
const now = new Date();
|
||||
if (memory.type === MemoryType.OnThisDay) {
|
||||
return $t('years_ago', { values: { years: now.getFullYear() - memory.data.year } });
|
||||
const now = new Date();
|
||||
const memoryDate = new Date(memory.memoryAt);
|
||||
|
||||
return memoryDate.getUTCDate() === now.getDate() && memoryDate.getUTCMonth() === now.getMonth()
|
||||
? $t('years_ago', { values: { years: now.getFullYear() - memory.data.year } })
|
||||
: DateTime.fromJSDate(memoryDate).toLocaleString(DateTime.DATE_MED, { locale: get(locale) });
|
||||
}
|
||||
|
||||
return $t('unknown');
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
import SingleGridRow from '$lib/components/shared-components/SingleGridRow.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAssetMediaUrl, getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { getAssetMediaUrl, getPeopleThumbnailUrl, memoryLaneTitle } from '$lib/utils';
|
||||
import { getAssetInfo, AssetMediaSize, type SearchExploreResponseDto } from '@immich/sdk';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { Icon, ImageCarousel } from '@immich/ui';
|
||||
import { mdiHeart } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
@@ -28,13 +28,22 @@
|
||||
return targetField?.items || [];
|
||||
};
|
||||
|
||||
let places = $derived(getFieldItems(data.items, 'exifInfo.city'));
|
||||
let places = $derived(getFieldItems(data.explore, 'exifInfo.city'));
|
||||
let recents = $derived(
|
||||
getFieldItems(data.items, 'createdAt').sort((a, b) => new Date(b.value).getTime() - new Date(a.value).getTime()),
|
||||
getFieldItems(data.explore, 'createdAt').sort((a, b) => new Date(b.value).getTime() - new Date(a.value).getTime()),
|
||||
);
|
||||
let people = $state(data.people.people);
|
||||
let memories = $derived(
|
||||
data.memories.map((memory) => ({
|
||||
id: memory.id,
|
||||
title: $memoryLaneTitle(memory),
|
||||
href: Route.memories({ id: memory.assets[0].id }),
|
||||
alt: $t('memory_lane_title', { values: { title: $getAltText(toTimelineAsset(memory.assets[0])) } }),
|
||||
src: getAssetMediaUrl({ id: memory.assets[0].id }),
|
||||
})),
|
||||
);
|
||||
let people = $state(data.response.people);
|
||||
|
||||
let hasPeople = $derived(data.response.total > 0);
|
||||
let hasPeople = $derived(data.people.total > 0);
|
||||
|
||||
const onPersonThumbnailReady = ({ id }: { id: string }) => {
|
||||
for (const person of people) {
|
||||
@@ -124,6 +133,20 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if memories.length > 0}
|
||||
<div class="mt-2 mb-6">
|
||||
<div class="flex justify-between">
|
||||
<p class="mb-4 font-medium dark:text-immich-dark-fg">{$t('memories')}</p>
|
||||
<a
|
||||
href={Route.memories()}
|
||||
class="pe-4 text-sm font-medium hover:text-immich-primary dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
|
||||
draggable="false">{$t('view_all')}</a
|
||||
>
|
||||
</div>
|
||||
<ImageCarousel items={memories} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if recents.length > 0}
|
||||
<div class="mt-2 mb-6">
|
||||
<div class="flex justify-between">
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { getAllPeople, getExploreData } from '@immich/sdk';
|
||||
import { getAllPeople, getExploreData, MemorySearchOrder } from '@immich/sdk';
|
||||
import { memoryManager } from '$lib/managers/memory-manager.svelte';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
await authenticate(url);
|
||||
const [items, response] = await Promise.all([getExploreData(), getAllPeople({ withHidden: false })]);
|
||||
memoryManager.filters = { size: 12, order: MemorySearchOrder.Desc };
|
||||
|
||||
const [explore, people] = await Promise.all([
|
||||
getExploreData(),
|
||||
getAllPeople({ withHidden: false }),
|
||||
memoryManager.ready(),
|
||||
]);
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
items,
|
||||
response,
|
||||
explore,
|
||||
people,
|
||||
memories: memoryManager.memories,
|
||||
meta: {
|
||||
title: $t('explore'),
|
||||
},
|
||||
|
||||
@@ -1,5 +1,104 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAssetMediaUrl, memoryLaneTitle } from '$lib/utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { mdiHeartOutline, mdiHeart } from '@mdi/js';
|
||||
import { Button, Icon } from '@immich/ui';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { page } from '$app/state';
|
||||
import MemoryViewer from './MemoryViewer.svelte';
|
||||
import { QueryParameter } from '$lib/constants';
|
||||
import { memoryManager } from '$lib/managers/memory-manager.svelte';
|
||||
import { clearQueryParam, setQueryValue } from '$lib/utils/navigation';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
let onlyFavorites = $state(page.url.searchParams.get('favorites') === 'true');
|
||||
let lastElement: HTMLElement | undefined = $state();
|
||||
|
||||
const toggleFavorites = async () => {
|
||||
onlyFavorites = !onlyFavorites;
|
||||
memoryManager.filters = onlyFavorites ? { isSaved: true } : {};
|
||||
await memoryManager.ready();
|
||||
|
||||
if (onlyFavorites) {
|
||||
void setQueryValue('favorites', 'true');
|
||||
} else {
|
||||
void clearQueryParam('favorites', page.url);
|
||||
}
|
||||
};
|
||||
|
||||
const intersectionObserver = new IntersectionObserver((entries) => {
|
||||
const entry = entries.find((entry) => entry.target === lastElement);
|
||||
if (entry?.isIntersecting && memoryManager.hasNextPage) {
|
||||
void memoryManager.loadNextPage();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (lastElement) {
|
||||
intersectionObserver.disconnect();
|
||||
intersectionObserver.observe(lastElement);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<MemoryViewer />
|
||||
{#if page.url.searchParams.has(QueryParameter.ID)}
|
||||
<MemoryViewer />
|
||||
{:else}
|
||||
<UserPageLayout title={data.meta.title} description={`(${memoryManager.total.toLocaleString($locale)})`}>
|
||||
{#snippet buttons()}
|
||||
<div class="flex place-items-center gap-2">
|
||||
<Button
|
||||
leadingIcon={mdiHeartOutline}
|
||||
size="small"
|
||||
variant={onlyFavorites ? 'filled' : 'ghost'}
|
||||
color="secondary"
|
||||
onclick={() => toggleFavorites()}>{$t('only_favorites')}</Button
|
||||
>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#if memoryManager.memories.length > 0}
|
||||
<div class="grid w-full grid-cols-2 gap-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 2xl:grid-cols-7">
|
||||
{#each memoryManager.memories as memory, index (memory.id)}
|
||||
<a
|
||||
href={Route.memories({ id: memory.assets[0].id })}
|
||||
class="item-card relative inline-block aspect-video"
|
||||
bind:this={
|
||||
() => (index === memoryManager.memories.length - 1 ? lastElement : null),
|
||||
(e) => {
|
||||
if (index === memoryManager.memories.length - 1) {
|
||||
lastElement = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
{#if memory.isSaved}
|
||||
<div class="absolute inset-s-2 top-2 z-2">
|
||||
<Icon data-icon-favorite icon={mdiHeart} size="24" class="text-white" />
|
||||
</div>
|
||||
{/if}
|
||||
<img
|
||||
src={getAssetMediaUrl({ id: memory.assets[0].id })}
|
||||
alt={$getAltText(toTimelineAsset(memory.assets[0]))}
|
||||
class="size-full object-cover brightness-75"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span
|
||||
class="absolute bottom-2 w-full px-1 text-center text-sm font-medium text-ellipsis text-white capitalize backdrop-blur-[1px] hover:cursor-pointer"
|
||||
>
|
||||
{$memoryLaneTitle(memory)}
|
||||
</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}{/if}
|
||||
</UserPageLayout>
|
||||
{/if}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { QueryParameter } from '$lib/constants';
|
||||
import { memoryManager } from '$lib/managers/memory-manager.svelte';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import type { PageLoad } from './$types';
|
||||
@@ -6,10 +9,19 @@ export const load = (async ({ url }) => {
|
||||
const user = await authenticate(url);
|
||||
const $t = await getFormatter();
|
||||
|
||||
const filters = url.searchParams.get('favorites') === 'true' ? { isSaved: true } : {};
|
||||
if (
|
||||
!(url.searchParams.has(QueryParameter.ID) && memoryManager.memories.length > 0) &&
|
||||
!isEqual(memoryManager.filters, filters)
|
||||
) {
|
||||
memoryManager.filters = filters;
|
||||
await memoryManager.ready();
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
meta: {
|
||||
title: $t('memory'),
|
||||
title: $t('memories'),
|
||||
},
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
let progressBarController: Tween<number> | undefined = $state(undefined);
|
||||
let videoPlayer: HTMLVideoElement | undefined = $state();
|
||||
const asHref = (asset: { id: string }) => `?${QueryParameter.ID}=${asset.id}`;
|
||||
let previousPage = $state(Route.memories());
|
||||
|
||||
const handleNavigate = async (asset?: { id: string }) => {
|
||||
if (assetViewerManager.isViewing) {
|
||||
@@ -106,7 +107,7 @@
|
||||
const handlePreviousAsset = () => handleNavigate(current?.previous?.asset);
|
||||
const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]);
|
||||
const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]);
|
||||
const handleEscape = async () => goto(Route.photos());
|
||||
const handleEscape = async () => goto(previousPage);
|
||||
const handleSelectAll = () =>
|
||||
assetMultiSelectManager.selectAssets(current?.memory.assets.map((a) => toTimelineAsset(a)) || []);
|
||||
|
||||
@@ -249,7 +250,7 @@
|
||||
|
||||
const init = (target: Page | NavigationTarget | null) => {
|
||||
if (memoryManager.memories.length === 0) {
|
||||
return handlePromiseError(goto(Route.photos()));
|
||||
return handlePromiseError(goto(previousPage));
|
||||
}
|
||||
|
||||
current = loadFromParams(target);
|
||||
@@ -281,6 +282,10 @@
|
||||
};
|
||||
|
||||
afterNavigate(({ from, to }) => {
|
||||
if (from?.url !== null && !from?.url.searchParams.has(QueryParameter.ID)) {
|
||||
previousPage = from!.url.toString();
|
||||
}
|
||||
|
||||
memoryManager.ready().then(
|
||||
() => {
|
||||
let target;
|
||||
@@ -381,7 +386,7 @@
|
||||
icon={mdiClose}
|
||||
aria-label={$t('close')}
|
||||
size="large"
|
||||
onclick={() => goto(Route.photos())}
|
||||
onclick={() => goto(previousPage)}
|
||||
/>
|
||||
<p class="text-lg">
|
||||
{$memoryLaneTitle(current.memory)}
|
||||
|
||||
@@ -33,12 +33,14 @@
|
||||
type OnLink,
|
||||
type OnUnlink,
|
||||
} from '$lib/utils/actions';
|
||||
import { asLocalTimeISO } from '$lib/utils/date-time';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import { ActionButton, CommandPaletteDefaultProvider, ImageCarousel } from '@immich/ui';
|
||||
import { mdiDotsVertical } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
@@ -81,6 +83,10 @@
|
||||
assetMultiSelectManager.clear();
|
||||
};
|
||||
|
||||
if (memoryManager.filters === undefined || memoryManager.filters.$for !== asLocalTimeISO(DateTime.now())) {
|
||||
memoryManager.filters = { $for: asLocalTimeISO(DateTime.now()) };
|
||||
}
|
||||
|
||||
const items = $derived(
|
||||
memoryManager.memories.map((memory) => ({
|
||||
id: memory.id,
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
import WorkflowStepCard from '$lib/components/workflows/WorkflowStepCard.svelte';
|
||||
import WorkflowJsonEditor from '$lib/components/workflows/WorkflowJsonEditor.svelte';
|
||||
import WorkflowSummary from '$lib/components/workflows/WorkflowSummary.svelte';
|
||||
import WorkflowJsonEditor from './WorkflowJsonEditor.svelte';
|
||||
import WorkflowStepCard from './WorkflowStepCard.svelte';
|
||||
import WorkflowSummary from './WorkflowSummary.svelte';
|
||||
|
||||
type WorkflowJsonContent = Required<
|
||||
Pick<WorkflowUpdateDto, 'description' | 'enabled' | 'name' | 'steps' | 'trigger'>
|
||||
|
||||
Reference in New Issue
Block a user