mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
feat: user upload heatmap (#28593)
* Feat - Heatmap * Implemented Comments to prettify and code cleanup * fixing code to pass cases. * fixing errors for OpenAPI Clients * Improving the code. * Fix code * Rerun generated client check * Rerun generated client * feat: command for user pages (#28554) * fix(web): timeline stuttering with many assets in 1 day (#28509) * fix(web): timeline stuttering with many assets in 1 day * cache isInOrNearViewport per day * skip inOrNearViewport check on first run * chore(ml): allow insightface 1.x (#28595) * chore(ml): allow insightface 1.x The new insightface 1.0 release appears to have no breaking code changes nor relevant license changes ([before](2a78baec42/README.md), [after](70f3269ea6/README.md), c.f.fd7ddfef54/machine-learning/README.md), and it works on my machine. * Update uv.lock * please excuse my incompetence * Triggering the actions. * bad merge * Fix code * Code clear * Resolve conflict * Resolve conflict * Resolve conflict * Resolve errors * Resolve errors * Resolve errors more * chore: clean up --------- Co-authored-by: Alex <alex.tran1502@gmail.com> Co-authored-by: Ben Beckford <ben@benjaminbeckford.com> Co-authored-by: Aaron Liu <aaronliu0130@gmail.com> Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
parent
58528cad08
commit
b3d49045de
31 changed files with 1159 additions and 8 deletions
|
|
@ -570,6 +570,7 @@
|
||||||
"asset_added_to_album": "Added to album",
|
"asset_added_to_album": "Added to album",
|
||||||
"asset_adding_to_album": "Adding to album…",
|
"asset_adding_to_album": "Adding to album…",
|
||||||
"asset_created": "Asset created",
|
"asset_created": "Asset created",
|
||||||
|
"asset_day_count": "{date}: {count, plural, one {# asset} other {# assets}}",
|
||||||
"asset_description_updated": "Asset description has been updated",
|
"asset_description_updated": "Asset description has been updated",
|
||||||
"asset_filename_is_offline": "Asset {filename} is offline",
|
"asset_filename_is_offline": "Asset {filename} is offline",
|
||||||
"asset_has_unassigned_faces": "Asset has unassigned faces",
|
"asset_has_unassigned_faces": "Asset has unassigned faces",
|
||||||
|
|
@ -1400,6 +1401,7 @@
|
||||||
"leave": "Leave",
|
"leave": "Leave",
|
||||||
"leave_album": "Leave album",
|
"leave_album": "Leave album",
|
||||||
"lens_model": "Lens model",
|
"lens_model": "Lens model",
|
||||||
|
"less": "Less",
|
||||||
"let_others_respond": "Let others respond",
|
"let_others_respond": "Let others respond",
|
||||||
"level": "Level",
|
"level": "Level",
|
||||||
"library": "Library",
|
"library": "Library",
|
||||||
|
|
@ -2412,6 +2414,7 @@
|
||||||
"updated_password": "Updated password",
|
"updated_password": "Updated password",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"upload_concurrency": "Upload concurrency",
|
"upload_concurrency": "Upload concurrency",
|
||||||
|
"upload_day_count": "{date}: {count, plural, one {# upload} other {# uploads}}",
|
||||||
"upload_details": "Upload Details",
|
"upload_details": "Upload Details",
|
||||||
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
|
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
|
||||||
"upload_dialog_title": "Upload Asset",
|
"upload_dialog_title": "Upload Asset",
|
||||||
|
|
@ -2427,6 +2430,8 @@
|
||||||
"upload_to_immich": "Upload to Immich ({count})",
|
"upload_to_immich": "Upload to Immich ({count})",
|
||||||
"uploading": "Uploading",
|
"uploading": "Uploading",
|
||||||
"uploading_media": "Uploading media",
|
"uploading_media": "Uploading media",
|
||||||
|
"uploads": "Uploads",
|
||||||
|
"uploads_count": "{count, plural, one {# upload} other {# uploads}}",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"usage": "Usage",
|
"usage": "Usage",
|
||||||
"use_biometric": "Use biometric",
|
"use_biometric": "Use biometric",
|
||||||
|
|
|
||||||
5
mobile/openapi/README.md
generated
5
mobile/openapi/README.md
generated
|
|
@ -293,6 +293,7 @@ Class | Method | HTTP request | Description
|
||||||
*UsersApi* | [**deleteProfileImage**](doc//UsersApi.md#deleteprofileimage) | **DELETE** /users/profile-image | Delete user profile image
|
*UsersApi* | [**deleteProfileImage**](doc//UsersApi.md#deleteprofileimage) | **DELETE** /users/profile-image | Delete user profile image
|
||||||
*UsersApi* | [**deleteUserLicense**](doc//UsersApi.md#deleteuserlicense) | **DELETE** /users/me/license | Delete user product key
|
*UsersApi* | [**deleteUserLicense**](doc//UsersApi.md#deleteuserlicense) | **DELETE** /users/me/license | Delete user product key
|
||||||
*UsersApi* | [**deleteUserOnboarding**](doc//UsersApi.md#deleteuseronboarding) | **DELETE** /users/me/onboarding | Delete user onboarding
|
*UsersApi* | [**deleteUserOnboarding**](doc//UsersApi.md#deleteuseronboarding) | **DELETE** /users/me/onboarding | Delete user onboarding
|
||||||
|
*UsersApi* | [**getMyCalendarHeatmap**](doc//UsersApi.md#getmycalendarheatmap) | **GET** /users/me/calendar-heatmap | Retrieve calendar heatmap activity
|
||||||
*UsersApi* | [**getMyPreferences**](doc//UsersApi.md#getmypreferences) | **GET** /users/me/preferences | Get my preferences
|
*UsersApi* | [**getMyPreferences**](doc//UsersApi.md#getmypreferences) | **GET** /users/me/preferences | Get my preferences
|
||||||
*UsersApi* | [**getMyUser**](doc//UsersApi.md#getmyuser) | **GET** /users/me | Get current user
|
*UsersApi* | [**getMyUser**](doc//UsersApi.md#getmyuser) | **GET** /users/me | Get current user
|
||||||
*UsersApi* | [**getProfileImage**](doc//UsersApi.md#getprofileimage) | **GET** /users/{id}/profile-image | Retrieve user profile image
|
*UsersApi* | [**getProfileImage**](doc//UsersApi.md#getprofileimage) | **GET** /users/{id}/profile-image | Retrieve user profile image
|
||||||
|
|
@ -307,6 +308,7 @@ Class | Method | HTTP request | Description
|
||||||
*UsersAdminApi* | [**createUserAdmin**](doc//UsersAdminApi.md#createuseradmin) | **POST** /admin/users | Create a user
|
*UsersAdminApi* | [**createUserAdmin**](doc//UsersAdminApi.md#createuseradmin) | **POST** /admin/users | Create a user
|
||||||
*UsersAdminApi* | [**deleteUserAdmin**](doc//UsersAdminApi.md#deleteuseradmin) | **DELETE** /admin/users/{id} | Delete a user
|
*UsersAdminApi* | [**deleteUserAdmin**](doc//UsersAdminApi.md#deleteuseradmin) | **DELETE** /admin/users/{id} | Delete a user
|
||||||
*UsersAdminApi* | [**getUserAdmin**](doc//UsersAdminApi.md#getuseradmin) | **GET** /admin/users/{id} | Retrieve a user
|
*UsersAdminApi* | [**getUserAdmin**](doc//UsersAdminApi.md#getuseradmin) | **GET** /admin/users/{id} | Retrieve a user
|
||||||
|
*UsersAdminApi* | [**getUserCalendarHeatmapAdmin**](doc//UsersAdminApi.md#getusercalendarheatmapadmin) | **GET** /admin/users/{id}/calendar-heatmap | Retrieve calendar heatmap activity
|
||||||
*UsersAdminApi* | [**getUserPreferencesAdmin**](doc//UsersAdminApi.md#getuserpreferencesadmin) | **GET** /admin/users/{id}/preferences | Retrieve user preferences
|
*UsersAdminApi* | [**getUserPreferencesAdmin**](doc//UsersAdminApi.md#getuserpreferencesadmin) | **GET** /admin/users/{id}/preferences | Retrieve user preferences
|
||||||
*UsersAdminApi* | [**getUserSessionsAdmin**](doc//UsersAdminApi.md#getusersessionsadmin) | **GET** /admin/users/{id}/sessions | Retrieve user sessions
|
*UsersAdminApi* | [**getUserSessionsAdmin**](doc//UsersAdminApi.md#getusersessionsadmin) | **GET** /admin/users/{id}/sessions | Retrieve user sessions
|
||||||
*UsersAdminApi* | [**getUserStatisticsAdmin**](doc//UsersAdminApi.md#getuserstatisticsadmin) | **GET** /admin/users/{id}/statistics | Retrieve user statistics
|
*UsersAdminApi* | [**getUserStatisticsAdmin**](doc//UsersAdminApi.md#getuserstatisticsadmin) | **GET** /admin/users/{id}/statistics | Retrieve user statistics
|
||||||
|
|
@ -398,6 +400,9 @@ Class | Method | HTTP request | Description
|
||||||
- [BulkIdsDto](doc//BulkIdsDto.md)
|
- [BulkIdsDto](doc//BulkIdsDto.md)
|
||||||
- [CLIPConfig](doc//CLIPConfig.md)
|
- [CLIPConfig](doc//CLIPConfig.md)
|
||||||
- [CQMode](doc//CQMode.md)
|
- [CQMode](doc//CQMode.md)
|
||||||
|
- [CalendarHeatmapResponseDto](doc//CalendarHeatmapResponseDto.md)
|
||||||
|
- [CalendarHeatmapResponseDtoSeriesInner](doc//CalendarHeatmapResponseDtoSeriesInner.md)
|
||||||
|
- [CalendarHeatmapType](doc//CalendarHeatmapType.md)
|
||||||
- [CastResponse](doc//CastResponse.md)
|
- [CastResponse](doc//CastResponse.md)
|
||||||
- [CastUpdate](doc//CastUpdate.md)
|
- [CastUpdate](doc//CastUpdate.md)
|
||||||
- [ChangePasswordDto](doc//ChangePasswordDto.md)
|
- [ChangePasswordDto](doc//ChangePasswordDto.md)
|
||||||
|
|
|
||||||
3
mobile/openapi/lib/api.dart
generated
3
mobile/openapi/lib/api.dart
generated
|
|
@ -140,6 +140,9 @@ part 'model/bulk_id_response_dto.dart';
|
||||||
part 'model/bulk_ids_dto.dart';
|
part 'model/bulk_ids_dto.dart';
|
||||||
part 'model/clip_config.dart';
|
part 'model/clip_config.dart';
|
||||||
part 'model/cq_mode.dart';
|
part 'model/cq_mode.dart';
|
||||||
|
part 'model/calendar_heatmap_response_dto.dart';
|
||||||
|
part 'model/calendar_heatmap_response_dto_series_inner.dart';
|
||||||
|
part 'model/calendar_heatmap_type.dart';
|
||||||
part 'model/cast_response.dart';
|
part 'model/cast_response.dart';
|
||||||
part 'model/cast_update.dart';
|
part 'model/cast_update.dart';
|
||||||
part 'model/change_password_dto.dart';
|
part 'model/change_password_dto.dart';
|
||||||
|
|
|
||||||
84
mobile/openapi/lib/api/users_admin_api.dart
generated
84
mobile/openapi/lib/api/users_admin_api.dart
generated
|
|
@ -193,6 +193,90 @@ class UsersAdminApi {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve calendar heatmap activity
|
||||||
|
///
|
||||||
|
/// Retrieve activity counts for a specified period, in a calendar heatmap format.
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
///
|
||||||
|
/// * [DateTime] from:
|
||||||
|
/// Start date in UTC
|
||||||
|
///
|
||||||
|
/// * [DateTime] to:
|
||||||
|
/// End date in UTC
|
||||||
|
///
|
||||||
|
/// * [CalendarHeatmapType] type:
|
||||||
|
Future<Response> getUserCalendarHeatmapAdminWithHttpInfo(String id, { DateTime? from, DateTime? to, CalendarHeatmapType? type, Future<void>? abortTrigger, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/users/{id}/calendar-heatmap'
|
||||||
|
.replaceAll('{id}', id);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (from != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'from', from));
|
||||||
|
}
|
||||||
|
if (to != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'to', to));
|
||||||
|
}
|
||||||
|
if (type != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'type', type));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
abortTrigger: abortTrigger,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve calendar heatmap activity
|
||||||
|
///
|
||||||
|
/// Retrieve activity counts for a specified period, in a calendar heatmap format.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
///
|
||||||
|
/// * [DateTime] from:
|
||||||
|
/// Start date in UTC
|
||||||
|
///
|
||||||
|
/// * [DateTime] to:
|
||||||
|
/// End date in UTC
|
||||||
|
///
|
||||||
|
/// * [CalendarHeatmapType] type:
|
||||||
|
Future<CalendarHeatmapResponseDto?> getUserCalendarHeatmapAdmin(String id, { DateTime? from, DateTime? to, CalendarHeatmapType? type, Future<void>? abortTrigger, }) async {
|
||||||
|
final response = await getUserCalendarHeatmapAdminWithHttpInfo(id, from: from, to: to, type: type, abortTrigger: abortTrigger,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// 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) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'CalendarHeatmapResponseDto',) as CalendarHeatmapResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieve user preferences
|
/// Retrieve user preferences
|
||||||
///
|
///
|
||||||
/// Retrieve the preferences of a specific user.
|
/// Retrieve the preferences of a specific user.
|
||||||
|
|
|
||||||
79
mobile/openapi/lib/api/users_api.dart
generated
79
mobile/openapi/lib/api/users_api.dart
generated
|
|
@ -208,6 +208,85 @@ class UsersApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve calendar heatmap activity
|
||||||
|
///
|
||||||
|
/// Retrieve activity counts for a specified period, in a calendar heatmap format.
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [DateTime] from:
|
||||||
|
/// Start date in UTC
|
||||||
|
///
|
||||||
|
/// * [DateTime] to:
|
||||||
|
/// End date in UTC
|
||||||
|
///
|
||||||
|
/// * [CalendarHeatmapType] type:
|
||||||
|
Future<Response> getMyCalendarHeatmapWithHttpInfo({ DateTime? from, DateTime? to, CalendarHeatmapType? type, Future<void>? abortTrigger, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/users/me/calendar-heatmap';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (from != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'from', from));
|
||||||
|
}
|
||||||
|
if (to != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'to', to));
|
||||||
|
}
|
||||||
|
if (type != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'type', type));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
abortTrigger: abortTrigger,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve calendar heatmap activity
|
||||||
|
///
|
||||||
|
/// Retrieve activity counts for a specified period, in a calendar heatmap format.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [DateTime] from:
|
||||||
|
/// Start date in UTC
|
||||||
|
///
|
||||||
|
/// * [DateTime] to:
|
||||||
|
/// End date in UTC
|
||||||
|
///
|
||||||
|
/// * [CalendarHeatmapType] type:
|
||||||
|
Future<CalendarHeatmapResponseDto?> getMyCalendarHeatmap({ DateTime? from, DateTime? to, CalendarHeatmapType? type, Future<void>? abortTrigger, }) async {
|
||||||
|
final response = await getMyCalendarHeatmapWithHttpInfo(from: from, to: to, type: type, abortTrigger: abortTrigger,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// 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) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'CalendarHeatmapResponseDto',) as CalendarHeatmapResponseDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Get my preferences
|
/// Get my preferences
|
||||||
///
|
///
|
||||||
/// Retrieve the preferences for the current user.
|
/// Retrieve the preferences for the current user.
|
||||||
|
|
|
||||||
6
mobile/openapi/lib/api_client.dart
generated
6
mobile/openapi/lib/api_client.dart
generated
|
|
@ -325,6 +325,12 @@ class ApiClient {
|
||||||
return CLIPConfig.fromJson(value);
|
return CLIPConfig.fromJson(value);
|
||||||
case 'CQMode':
|
case 'CQMode':
|
||||||
return CQModeTypeTransformer().decode(value);
|
return CQModeTypeTransformer().decode(value);
|
||||||
|
case 'CalendarHeatmapResponseDto':
|
||||||
|
return CalendarHeatmapResponseDto.fromJson(value);
|
||||||
|
case 'CalendarHeatmapResponseDtoSeriesInner':
|
||||||
|
return CalendarHeatmapResponseDtoSeriesInner.fromJson(value);
|
||||||
|
case 'CalendarHeatmapType':
|
||||||
|
return CalendarHeatmapTypeTypeTransformer().decode(value);
|
||||||
case 'CastResponse':
|
case 'CastResponse':
|
||||||
return CastResponse.fromJson(value);
|
return CastResponse.fromJson(value);
|
||||||
case 'CastUpdate':
|
case 'CastUpdate':
|
||||||
|
|
|
||||||
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
|
|
@ -100,6 +100,9 @@ String parameterToString(dynamic value) {
|
||||||
if (value is CQMode) {
|
if (value is CQMode) {
|
||||||
return CQModeTypeTransformer().encode(value).toString();
|
return CQModeTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
if (value is CalendarHeatmapType) {
|
||||||
|
return CalendarHeatmapTypeTypeTransformer().encode(value).toString();
|
||||||
|
}
|
||||||
if (value is Colorspace) {
|
if (value is Colorspace) {
|
||||||
return ColorspaceTypeTransformer().encode(value).toString();
|
return ColorspaceTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
129
mobile/openapi/lib/model/calendar_heatmap_response_dto.dart
generated
Normal file
129
mobile/openapi/lib/model/calendar_heatmap_response_dto.dart
generated
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
//
|
||||||
|
// 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 CalendarHeatmapResponseDto {
|
||||||
|
/// Returns a new [CalendarHeatmapResponseDto] instance.
|
||||||
|
CalendarHeatmapResponseDto({
|
||||||
|
required this.from,
|
||||||
|
this.series = const [],
|
||||||
|
required this.to,
|
||||||
|
required this.totalCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Start date in UTC
|
||||||
|
String from;
|
||||||
|
|
||||||
|
List<CalendarHeatmapResponseDtoSeriesInner> series;
|
||||||
|
|
||||||
|
/// End date in UTC
|
||||||
|
String to;
|
||||||
|
|
||||||
|
/// Total activity count over the period
|
||||||
|
///
|
||||||
|
/// Minimum value: 0
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
int totalCount;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is CalendarHeatmapResponseDto &&
|
||||||
|
other.from == from &&
|
||||||
|
_deepEquality.equals(other.series, series) &&
|
||||||
|
other.to == to &&
|
||||||
|
other.totalCount == totalCount;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(from.hashCode) +
|
||||||
|
(series.hashCode) +
|
||||||
|
(to.hashCode) +
|
||||||
|
(totalCount.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'CalendarHeatmapResponseDto[from=$from, series=$series, to=$to, totalCount=$totalCount]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'from'] = this.from;
|
||||||
|
json[r'series'] = this.series;
|
||||||
|
json[r'to'] = this.to;
|
||||||
|
json[r'totalCount'] = this.totalCount;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [CalendarHeatmapResponseDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static CalendarHeatmapResponseDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "CalendarHeatmapResponseDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return CalendarHeatmapResponseDto(
|
||||||
|
from: mapValueOfType<String>(json, r'from')!,
|
||||||
|
series: CalendarHeatmapResponseDtoSeriesInner.listFromJson(json[r'series']),
|
||||||
|
to: mapValueOfType<String>(json, r'to')!,
|
||||||
|
totalCount: mapValueOfType<int>(json, r'totalCount')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<CalendarHeatmapResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <CalendarHeatmapResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = CalendarHeatmapResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, CalendarHeatmapResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, CalendarHeatmapResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = CalendarHeatmapResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of CalendarHeatmapResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<CalendarHeatmapResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<CalendarHeatmapResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = CalendarHeatmapResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'from',
|
||||||
|
'series',
|
||||||
|
'to',
|
||||||
|
'totalCount',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
112
mobile/openapi/lib/model/calendar_heatmap_response_dto_series_inner.dart
generated
Normal file
112
mobile/openapi/lib/model/calendar_heatmap_response_dto_series_inner.dart
generated
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
//
|
||||||
|
// 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 CalendarHeatmapResponseDtoSeriesInner {
|
||||||
|
/// Returns a new [CalendarHeatmapResponseDtoSeriesInner] instance.
|
||||||
|
CalendarHeatmapResponseDtoSeriesInner({
|
||||||
|
required this.count,
|
||||||
|
required this.date,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Activity count
|
||||||
|
///
|
||||||
|
/// Minimum value: 0
|
||||||
|
/// Maximum value: 9007199254740991
|
||||||
|
int count;
|
||||||
|
|
||||||
|
/// Date in UTC
|
||||||
|
String date;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is CalendarHeatmapResponseDtoSeriesInner &&
|
||||||
|
other.count == count &&
|
||||||
|
other.date == date;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(count.hashCode) +
|
||||||
|
(date.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'CalendarHeatmapResponseDtoSeriesInner[count=$count, date=$date]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'count'] = this.count;
|
||||||
|
json[r'date'] = this.date;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [CalendarHeatmapResponseDtoSeriesInner] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static CalendarHeatmapResponseDtoSeriesInner? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "CalendarHeatmapResponseDtoSeriesInner");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return CalendarHeatmapResponseDtoSeriesInner(
|
||||||
|
count: mapValueOfType<int>(json, r'count')!,
|
||||||
|
date: mapValueOfType<String>(json, r'date')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<CalendarHeatmapResponseDtoSeriesInner> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <CalendarHeatmapResponseDtoSeriesInner>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = CalendarHeatmapResponseDtoSeriesInner.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, CalendarHeatmapResponseDtoSeriesInner> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, CalendarHeatmapResponseDtoSeriesInner>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = CalendarHeatmapResponseDtoSeriesInner.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of CalendarHeatmapResponseDtoSeriesInner-objects as value to a dart map
|
||||||
|
static Map<String, List<CalendarHeatmapResponseDtoSeriesInner>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<CalendarHeatmapResponseDtoSeriesInner>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = CalendarHeatmapResponseDtoSeriesInner.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'count',
|
||||||
|
'date',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
85
mobile/openapi/lib/model/calendar_heatmap_type.dart
generated
Normal file
85
mobile/openapi/lib/model/calendar_heatmap_type.dart
generated
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
//
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
/// Type of calendar heatmap
|
||||||
|
class CalendarHeatmapType {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const CalendarHeatmapType._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
static const upload = CalendarHeatmapType._(r'Upload');
|
||||||
|
static const taken = CalendarHeatmapType._(r'Taken');
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][CalendarHeatmapType].
|
||||||
|
static const values = <CalendarHeatmapType>[
|
||||||
|
upload,
|
||||||
|
taken,
|
||||||
|
];
|
||||||
|
|
||||||
|
static CalendarHeatmapType? fromJson(dynamic value) => CalendarHeatmapTypeTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<CalendarHeatmapType> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <CalendarHeatmapType>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = CalendarHeatmapType.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [CalendarHeatmapType] to String,
|
||||||
|
/// and [decode] dynamic data back to [CalendarHeatmapType].
|
||||||
|
class CalendarHeatmapTypeTypeTransformer {
|
||||||
|
factory CalendarHeatmapTypeTypeTransformer() => _instance ??= const CalendarHeatmapTypeTypeTransformer._();
|
||||||
|
|
||||||
|
const CalendarHeatmapTypeTypeTransformer._();
|
||||||
|
|
||||||
|
String encode(CalendarHeatmapType data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a CalendarHeatmapType.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
CalendarHeatmapType? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case r'Upload': return CalendarHeatmapType.upload;
|
||||||
|
case r'Taken': return CalendarHeatmapType.taken;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [CalendarHeatmapTypeTypeTransformer] instance.
|
||||||
|
static CalendarHeatmapTypeTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -31,6 +31,12 @@ void main() {
|
||||||
if (!deserialized.contains(entry.key)) {
|
if (!deserialized.contains(entry.key)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip new DTOs
|
||||||
|
if (!baseRequired.containsKey(entry.key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
final have = patched[entry.key] ?? const <String>{};
|
final have = patched[entry.key] ?? const <String>{};
|
||||||
final newlyRequired = entry.value.difference(
|
final newlyRequired = entry.value.difference(
|
||||||
baseRequired[entry.key] ?? const <String>{},
|
baseRequired[entry.key] ?? const <String>{},
|
||||||
|
|
|
||||||
|
|
@ -1264,6 +1264,96 @@
|
||||||
"x-immich-state": "Stable"
|
"x-immich-state": "Stable"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/users/{id}/calendar-heatmap": {
|
||||||
|
"get": {
|
||||||
|
"description": "Retrieve activity counts for a specified period, in a calendar heatmap format.",
|
||||||
|
"operationId": "getUserCalendarHeatmapAdmin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "from",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"description": "Start date in UTC",
|
||||||
|
"schema": {
|
||||||
|
"format": "date",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
|
||||||
|
"example": "2024-01-01",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "to",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"description": "End date in UTC",
|
||||||
|
"schema": {
|
||||||
|
"format": "date",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
|
||||||
|
"example": "2024-01-01",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"default": "Upload",
|
||||||
|
"$ref": "#/components/schemas/CalendarHeatmapType"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CalendarHeatmapResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "Retrieve calendar heatmap activity",
|
||||||
|
"tags": [
|
||||||
|
"Users (admin)"
|
||||||
|
],
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v3",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v3",
|
||||||
|
"state": "Stable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-permission": "user.read",
|
||||||
|
"x-immich-state": "Stable"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/users/{id}/preferences": {
|
"/admin/users/{id}/preferences": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Retrieve the preferences of a specific user.",
|
"description": "Retrieve the preferences of a specific user.",
|
||||||
|
|
@ -14485,6 +14575,86 @@
|
||||||
"x-immich-state": "Stable"
|
"x-immich-state": "Stable"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/users/me/calendar-heatmap": {
|
||||||
|
"get": {
|
||||||
|
"description": "Retrieve activity counts for a specified period, in a calendar heatmap format.",
|
||||||
|
"operationId": "getMyCalendarHeatmap",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "from",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"description": "Start date in UTC",
|
||||||
|
"schema": {
|
||||||
|
"format": "date",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
|
||||||
|
"example": "2024-01-01",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "to",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"description": "End date in UTC",
|
||||||
|
"schema": {
|
||||||
|
"format": "date",
|
||||||
|
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
|
||||||
|
"example": "2024-01-01",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"default": "Upload",
|
||||||
|
"$ref": "#/components/schemas/CalendarHeatmapType"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CalendarHeatmapResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "Retrieve calendar heatmap activity",
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v3",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v3",
|
||||||
|
"state": "Stable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-permission": "user.read",
|
||||||
|
"x-immich-state": "Stable"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/users/me/license": {
|
"/users/me/license": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"description": "Delete the registered product key for the current user.",
|
"description": "Delete the registered product key for the current user.",
|
||||||
|
|
@ -17645,6 +17815,64 @@
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"CalendarHeatmapResponseDto": {
|
||||||
|
"properties": {
|
||||||
|
"from": {
|
||||||
|
"description": "Start date in UTC",
|
||||||
|
"example": "2024-01-01",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"series": {
|
||||||
|
"items": {
|
||||||
|
"properties": {
|
||||||
|
"count": {
|
||||||
|
"description": "Activity count",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"description": "Date in UTC",
|
||||||
|
"example": "2024-01-01",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"date",
|
||||||
|
"count"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"description": "End date in UTC",
|
||||||
|
"example": "2024-12-31",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"totalCount": {
|
||||||
|
"description": "Total activity count over the period",
|
||||||
|
"maximum": 9007199254740991,
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"from",
|
||||||
|
"series",
|
||||||
|
"to",
|
||||||
|
"totalCount"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"CalendarHeatmapType": {
|
||||||
|
"description": "Type of calendar heatmap",
|
||||||
|
"enum": [
|
||||||
|
"Upload",
|
||||||
|
"Taken"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"CastResponse": {
|
"CastResponse": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"gCastEnabled": {
|
"gCastEnabled": {
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,20 @@ export type UserAdminUpdateDto = {
|
||||||
/** Storage label */
|
/** Storage label */
|
||||||
storageLabel?: string | null;
|
storageLabel?: string | null;
|
||||||
};
|
};
|
||||||
|
export type CalendarHeatmapResponseDto = {
|
||||||
|
/** Start date in UTC */
|
||||||
|
"from": string;
|
||||||
|
series: {
|
||||||
|
/** Activity count */
|
||||||
|
count: number;
|
||||||
|
/** Date in UTC */
|
||||||
|
date: string;
|
||||||
|
}[];
|
||||||
|
/** End date in UTC */
|
||||||
|
to: string;
|
||||||
|
/** Total activity count over the period */
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
export type AlbumsResponse = {
|
export type AlbumsResponse = {
|
||||||
defaultAssetOrder: AssetOrder;
|
defaultAssetOrder: AssetOrder;
|
||||||
};
|
};
|
||||||
|
|
@ -3544,6 +3558,26 @@ export function updateUserAdmin({ id, userAdminUpdateDto }: {
|
||||||
body: userAdminUpdateDto
|
body: userAdminUpdateDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Retrieve calendar heatmap activity
|
||||||
|
*/
|
||||||
|
export function getUserCalendarHeatmapAdmin({ $from, id, to, $type }: {
|
||||||
|
$from?: string;
|
||||||
|
id: string;
|
||||||
|
to?: string;
|
||||||
|
$type?: CalendarHeatmapType;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: CalendarHeatmapResponseDto;
|
||||||
|
}>(`/admin/users/${encodeURIComponent(id)}/calendar-heatmap${QS.query(QS.explode({
|
||||||
|
"from": $from,
|
||||||
|
to,
|
||||||
|
"type": $type
|
||||||
|
}))}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Retrieve user preferences
|
* Retrieve user preferences
|
||||||
*/
|
*/
|
||||||
|
|
@ -6578,6 +6612,25 @@ export function updateMyUser({ userUpdateMeDto }: {
|
||||||
body: userUpdateMeDto
|
body: userUpdateMeDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Retrieve calendar heatmap activity
|
||||||
|
*/
|
||||||
|
export function getMyCalendarHeatmap({ $from, to, $type }: {
|
||||||
|
$from?: string;
|
||||||
|
to?: string;
|
||||||
|
$type?: CalendarHeatmapType;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: CalendarHeatmapResponseDto;
|
||||||
|
}>(`/users/me/calendar-heatmap${QS.query(QS.explode({
|
||||||
|
"from": $from,
|
||||||
|
to,
|
||||||
|
"type": $type
|
||||||
|
}))}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Delete user product key
|
* Delete user product key
|
||||||
*/
|
*/
|
||||||
|
|
@ -6905,6 +6958,10 @@ export enum UserStatus {
|
||||||
Removing = "removing",
|
Removing = "removing",
|
||||||
Deleted = "deleted"
|
Deleted = "deleted"
|
||||||
}
|
}
|
||||||
|
export enum CalendarHeatmapType {
|
||||||
|
Upload = "Upload",
|
||||||
|
Taken = "Taken"
|
||||||
|
}
|
||||||
export enum AssetOrder {
|
export enum AssetOrder {
|
||||||
Asc = "asc",
|
Asc = "asc",
|
||||||
Desc = "desc"
|
Desc = "desc"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger';
|
||||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||||
import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto';
|
import { AssetStatsDto, AssetStatsResponseDto } from 'src/dtos/asset.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { CalendarHeatmapDto, CalendarHeatmapResponseDto } from 'src/dtos/calendar-heatmap.dto';
|
||||||
import { SessionResponseDto } from 'src/dtos/session.dto';
|
import { SessionResponseDto } from 'src/dtos/session.dto';
|
||||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||||
import {
|
import {
|
||||||
|
|
@ -85,6 +86,21 @@ export class UserAdminController {
|
||||||
return this.service.delete(auth, id, dto);
|
return this.service.delete(auth, id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':id/calendar-heatmap')
|
||||||
|
@Authenticated({ permission: Permission.UserRead })
|
||||||
|
@Endpoint({
|
||||||
|
summary: 'Retrieve calendar heatmap activity',
|
||||||
|
description: 'Retrieve activity counts for a specified period, in a calendar heatmap format.',
|
||||||
|
history: new HistoryBuilder().added('v3').stable('v3'),
|
||||||
|
})
|
||||||
|
getUserCalendarHeatmapAdmin(
|
||||||
|
@Auth() auth: AuthDto,
|
||||||
|
@Param() { id }: UUIDParamDto,
|
||||||
|
@Query() dto: CalendarHeatmapDto,
|
||||||
|
): Promise<CalendarHeatmapResponseDto> {
|
||||||
|
return this.service.getCalendarHeatmap(auth, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id/sessions')
|
@Get(':id/sessions')
|
||||||
@Authenticated({ permission: Permission.AdminSessionRead, admin: true })
|
@Authenticated({ permission: Permission.AdminSessionRead, admin: true })
|
||||||
@Endpoint({
|
@Endpoint({
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
Param,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
|
Query,
|
||||||
Res,
|
Res,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
|
|
@ -17,6 +18,7 @@ import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||||
import { NextFunction, Response } from 'express';
|
import { NextFunction, Response } from 'express';
|
||||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { CalendarHeatmapDto, CalendarHeatmapResponseDto } from 'src/dtos/calendar-heatmap.dto';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
|
import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
|
||||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||||
|
|
@ -60,6 +62,17 @@ export class UserController {
|
||||||
return this.service.getMe(auth);
|
return this.service.getMe(auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('me/calendar-heatmap')
|
||||||
|
@Authenticated({ permission: Permission.UserRead })
|
||||||
|
@Endpoint({
|
||||||
|
summary: 'Retrieve calendar heatmap activity',
|
||||||
|
description: 'Retrieve activity counts for a specified period, in a calendar heatmap format.',
|
||||||
|
history: new HistoryBuilder().added('v3').stable('v3'),
|
||||||
|
})
|
||||||
|
getMyCalendarHeatmap(@Auth() auth: AuthDto, @Query() dto: CalendarHeatmapDto): Promise<CalendarHeatmapResponseDto> {
|
||||||
|
return this.service.getCalendarHeatmap(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Put('me')
|
@Put('me')
|
||||||
@Authenticated({ permission: Permission.UserUpdate })
|
@Authenticated({ permission: Permission.UserUpdate })
|
||||||
@Endpoint({
|
@Endpoint({
|
||||||
|
|
|
||||||
36
server/src/dtos/calendar-heatmap.dto.ts
Normal file
36
server/src/dtos/calendar-heatmap.dto.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { createZodDto } from 'nestjs-zod';
|
||||||
|
import { CalendarHeatmapType } from 'src/enum';
|
||||||
|
import { isoDateToDate } from 'src/validation';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
const CalendarHeatmapTypeSchema = z
|
||||||
|
.enum(CalendarHeatmapType)
|
||||||
|
.describe('Type of calendar heatmap')
|
||||||
|
.meta({ id: 'CalendarHeatmapType' });
|
||||||
|
|
||||||
|
const CalendarHeatmapSchema = z
|
||||||
|
.object({
|
||||||
|
from: isoDateToDate.optional().describe('Start date in UTC'),
|
||||||
|
to: isoDateToDate.optional().describe('End date in UTC'),
|
||||||
|
type: CalendarHeatmapTypeSchema.optional().default(CalendarHeatmapType.Upload),
|
||||||
|
})
|
||||||
|
.refine((dto) => !dto.from || !dto.to || dto.from <= dto.to, { message: 'from must be before to', path: ['from'] })
|
||||||
|
.meta({ id: 'CalendarHeatmapDto' });
|
||||||
|
|
||||||
|
export class CalendarHeatmapDto extends createZodDto(CalendarHeatmapSchema) {}
|
||||||
|
|
||||||
|
const CalendarHeatmapResponseSchema = z
|
||||||
|
.object({
|
||||||
|
from: z.string().describe('Start date in UTC').meta({ example: '2024-01-01' }),
|
||||||
|
to: z.string().describe('End date in UTC').meta({ example: '2024-12-31' }),
|
||||||
|
series: z.array(
|
||||||
|
z.object({
|
||||||
|
date: z.string().describe('Date in UTC').meta({ example: '2024-01-01' }),
|
||||||
|
count: z.int().nonnegative().describe('Activity count'),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
totalCount: z.int().nonnegative().describe('Total activity count over the period'),
|
||||||
|
})
|
||||||
|
.meta({ id: 'CalendarHeatmapResponseDto' });
|
||||||
|
|
||||||
|
export class CalendarHeatmapResponseDto extends createZodDto(CalendarHeatmapResponseSchema) {}
|
||||||
|
|
@ -1175,3 +1175,8 @@ export enum WorkflowType {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkflowTypeSchema = z.enum(WorkflowType).describe('Workflow type').meta({ id: 'WorkflowType' });
|
export const WorkflowTypeSchema = z.enum(WorkflowType).describe('Workflow type').meta({ id: 'WorkflowType' });
|
||||||
|
|
||||||
|
export enum CalendarHeatmapType {
|
||||||
|
Upload = 'Upload',
|
||||||
|
Taken = 'Taken',
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -339,6 +339,22 @@ where
|
||||||
limit
|
limit
|
||||||
$3
|
$3
|
||||||
|
|
||||||
|
-- AssetRepository.getCalendarHeatmap
|
||||||
|
select
|
||||||
|
date_trunc('DAY', "asset"."createdAt" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC' as "date",
|
||||||
|
count(*) as "count"
|
||||||
|
from
|
||||||
|
"asset"
|
||||||
|
where
|
||||||
|
"ownerId" = $1::uuid
|
||||||
|
and "createdAt" >= $2
|
||||||
|
and "createdAt" < $3
|
||||||
|
and "deletedAt" is null
|
||||||
|
group by
|
||||||
|
date_trunc('DAY', "asset"."createdAt" AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
|
||||||
|
order by
|
||||||
|
"date" asc
|
||||||
|
|
||||||
-- AssetRepository.getTimeBuckets
|
-- AssetRepository.getTimeBuckets
|
||||||
with
|
with
|
||||||
"asset" as (
|
"asset" as (
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,15 @@ import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { LockableProperty, Stack } from 'src/database';
|
import { LockableProperty, Stack } from 'src/database';
|
||||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AssetFileType, AssetOrder, AssetOrderBy, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
import {
|
||||||
|
AssetFileType,
|
||||||
|
AssetOrder,
|
||||||
|
AssetOrderBy,
|
||||||
|
AssetStatus,
|
||||||
|
AssetType,
|
||||||
|
AssetVisibility,
|
||||||
|
CalendarHeatmapType,
|
||||||
|
} from 'src/enum';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AssetAudioTable, AssetKeyframeTable, AssetVideoTable } from 'src/schema/tables/asset-av.table';
|
import { AssetAudioTable, AssetKeyframeTable, AssetVideoTable } from 'src/schema/tables/asset-av.table';
|
||||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||||
|
|
@ -706,6 +714,32 @@ export class AssetRepository {
|
||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({
|
||||||
|
params: [DummyValue.UUID, { from: DummyValue.DATE, to: DummyValue.DATE, type: CalendarHeatmapType.Upload }],
|
||||||
|
})
|
||||||
|
getCalendarHeatmap(ownerId: string, dto: { from: Date; to: Date; type: CalendarHeatmapType }) {
|
||||||
|
const dateColumns: Record<CalendarHeatmapType, { order: AssetOrderBy; column: 'createdAt' | 'localDateTime' }> = {
|
||||||
|
[CalendarHeatmapType.Upload]: { order: AssetOrderBy.CreatedAt, column: 'createdAt' },
|
||||||
|
[CalendarHeatmapType.Taken]: { order: AssetOrderBy.TakenAt, column: 'localDateTime' },
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const { order, column } = dateColumns[dto.type];
|
||||||
|
|
||||||
|
const date = truncatedDate<Date>(order, 'DAY');
|
||||||
|
|
||||||
|
return this.db
|
||||||
|
.selectFrom('asset')
|
||||||
|
.select(date.as('date'))
|
||||||
|
.select((eb) => eb.fn.countAll<number>().as('count'))
|
||||||
|
.where('ownerId', '=', asUuid(ownerId))
|
||||||
|
.where(column, '>=', dto.from)
|
||||||
|
.where(column, '<', dto.to)
|
||||||
|
.where('deletedAt', 'is', null)
|
||||||
|
.groupBy(date)
|
||||||
|
.orderBy('date', 'asc')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{}] })
|
@GenerateSql({ params: [{}] })
|
||||||
async getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]> {
|
async getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]> {
|
||||||
return this.db
|
return this.db
|
||||||
|
|
|
||||||
36
server/src/services/shared/user-methods.ts
Normal file
36
server/src/services/shared/user-methods.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { CalendarHeatmapDto } from 'src/dtos/calendar-heatmap.dto';
|
||||||
|
import { CalendarHeatmapType } from 'src/enum';
|
||||||
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
|
import { asDateString } from 'src/utils/date';
|
||||||
|
|
||||||
|
export const getCalendarHeatmap = async (
|
||||||
|
userId: string,
|
||||||
|
dto: CalendarHeatmapDto,
|
||||||
|
repos: { asset: AssetRepository },
|
||||||
|
) => {
|
||||||
|
const toDate = DateTime.fromJSDate(dto.to ?? new Date(), { zone: 'utc' }).startOf('day');
|
||||||
|
const fromDate = (
|
||||||
|
dto.from ? DateTime.fromJSDate(dto.from, { zone: 'utc' }) : toDate.minus({ weeks: 52 }).plus({ days: 1 })
|
||||||
|
).startOf('day');
|
||||||
|
|
||||||
|
const counts = await repos.asset.getCalendarHeatmap(userId, {
|
||||||
|
from: fromDate.toJSDate(),
|
||||||
|
to: toDate.plus({ days: 1 }).toJSDate(),
|
||||||
|
type: dto.type ?? CalendarHeatmapType.Upload,
|
||||||
|
});
|
||||||
|
const countsMap = new Map(counts.map((item) => [asDateString(item.date)!, item.count]));
|
||||||
|
|
||||||
|
const series: Array<{ date: string; count: number }> = [];
|
||||||
|
for (let date = fromDate; date <= toDate; date = date.plus({ days: 1 })) {
|
||||||
|
const key = date.toISODate()!;
|
||||||
|
series.push({ date: key, count: countsMap.get(key) ?? 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: fromDate.toISODate()!,
|
||||||
|
to: toDate.toISODate()!,
|
||||||
|
series,
|
||||||
|
totalCount: series.reduce((totalCount, item) => totalCount + item.count, 0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -2,6 +2,7 @@ import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/com
|
||||||
import { SALT_ROUNDS } from 'src/constants';
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { AssetStatsDto, AssetStatsResponseDto, mapStats } from 'src/dtos/asset.dto';
|
import { AssetStatsDto, AssetStatsResponseDto, mapStats } from 'src/dtos/asset.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { CalendarHeatmapDto, CalendarHeatmapResponseDto } from 'src/dtos/calendar-heatmap.dto';
|
||||||
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
|
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
|
||||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||||
import {
|
import {
|
||||||
|
|
@ -15,6 +16,7 @@ import {
|
||||||
import { JobName, UserMetadataKey, UserStatus } from 'src/enum';
|
import { JobName, UserMetadataKey, UserStatus } from 'src/enum';
|
||||||
import { UserFindOptions } from 'src/repositories/user.repository';
|
import { UserFindOptions } from 'src/repositories/user.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
import { getCalendarHeatmap } from 'src/services/shared/user-methods';
|
||||||
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
|
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -122,6 +124,11 @@ export class UserAdminService extends BaseService {
|
||||||
return mapUserAdmin(user);
|
return mapUserAdmin(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCalendarHeatmap(auth: AuthDto, id: string, dto: CalendarHeatmapDto): Promise<CalendarHeatmapResponseDto> {
|
||||||
|
await this.findOrFail(id, { withDeleted: false });
|
||||||
|
return getCalendarHeatmap(id, dto, { asset: this.assetRepository });
|
||||||
|
}
|
||||||
|
|
||||||
async getSessions(auth: AuthDto, id: string): Promise<SessionResponseDto[]> {
|
async getSessions(auth: AuthDto, id: string): Promise<SessionResponseDto[]> {
|
||||||
const sessions = await this.sessionRepository.getByUserId(id);
|
const sessions = await this.sessionRepository.getByUserId(id);
|
||||||
return sessions.map((session) => mapSession(session));
|
return sessions.map((session) => mapSession(session));
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnEvent, OnJob } from 'src/decorators';
|
import { OnEvent, OnJob } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { CalendarHeatmapDto, CalendarHeatmapResponseDto } from 'src/dtos/calendar-heatmap.dto';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
|
import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
|
||||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||||
|
|
@ -15,6 +16,7 @@ import { ArgOf } from 'src/repositories/event.repository';
|
||||||
import { UserFindOptions } from 'src/repositories/user.repository';
|
import { UserFindOptions } from 'src/repositories/user.repository';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
import { getCalendarHeatmap } from 'src/services/shared/user-methods';
|
||||||
import { JobOf, UserMetadataItem } from 'src/types';
|
import { JobOf, UserMetadataItem } from 'src/types';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
|
|
@ -46,6 +48,10 @@ export class UserService extends BaseService {
|
||||||
return mapUserAdmin(user);
|
return mapUserAdmin(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCalendarHeatmap(auth: AuthDto, dto: CalendarHeatmapDto): Promise<CalendarHeatmapResponseDto> {
|
||||||
|
return getCalendarHeatmap(auth.user.id, dto, { asset: this.assetRepository });
|
||||||
|
}
|
||||||
|
|
||||||
async updateMe({ user }: AuthDto, dto: UserUpdateMeDto): Promise<UserAdminResponseDto> {
|
async updateMe({ user }: AuthDto, dto: UserUpdateMeDto): Promise<UserAdminResponseDto> {
|
||||||
if (dto.email) {
|
if (dto.email) {
|
||||||
const duplicate = await this.userRepository.getByEmail(dto.email);
|
const duplicate = await this.userRepository.getByEmail(dto.email);
|
||||||
|
|
|
||||||
|
|
@ -301,8 +301,8 @@ export function withTags(eb: ExpressionBuilder<DB, 'asset'>) {
|
||||||
).as('tags');
|
).as('tags');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function truncatedDate<O>(order: AssetOrderBy = AssetOrderBy.TakenAt) {
|
export function truncatedDate<O>(order: AssetOrderBy = AssetOrderBy.TakenAt, size?: 'DAY' | 'MONTH') {
|
||||||
return sql<O>`date_trunc(${sql.lit('MONTH')}, ${sql.ref(order === AssetOrderBy.CreatedAt ? 'asset.createdAt' : 'localDateTime')} AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'`;
|
return sql<O>`date_trunc(${sql.lit(size ?? 'MONTH')}, ${sql.ref(order === AssetOrderBy.CreatedAt ? 'asset.createdAt' : 'localDateTime')} AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withTagId<O>(qb: SelectQueryBuilder<DB, 'asset', O>, tagId: string) {
|
export function withTagId<O>(qb: SelectQueryBuilder<DB, 'asset', O>, tagId: string) {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export const newAssetRepositoryMock = (): Mocked<RepositoryInterface<AssetReposi
|
||||||
remove: vitest.fn(),
|
remove: vitest.fn(),
|
||||||
findLivePhotoMatch: vitest.fn(),
|
findLivePhotoMatch: vitest.fn(),
|
||||||
getStatistics: vitest.fn(),
|
getStatistics: vitest.fn(),
|
||||||
|
getCalendarHeatmap: vitest.fn(),
|
||||||
getTimeBucket: vitest.fn(),
|
getTimeBucket: vitest.fn(),
|
||||||
getTimeBuckets: vitest.fn(),
|
getTimeBuckets: vitest.fn(),
|
||||||
getAssetIdByCity: vitest.fn(),
|
getAssetIdByCity: vitest.fn(),
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,13 @@
|
||||||
title: string;
|
title: string;
|
||||||
headerAction?: ActionItem;
|
headerAction?: ActionItem;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
|
class?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { icon, title, headerAction, children }: Props = $props();
|
const { icon, title, headerAction, class: className, children }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card color="secondary">
|
<Card color="secondary" class={className}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex w-full items-center justify-between px-4 py-2">
|
<div class="flex w-full items-center justify-between px-4 py-2">
|
||||||
<div class="flex gap-2 text-primary">
|
<div class="flex gap-2 text-primary">
|
||||||
|
|
|
||||||
105
web/src/lib/components/CalendarHeatmap.svelte
Normal file
105
web/src/lib/components/CalendarHeatmap.svelte
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
|
import type { CalendarHeatmapResponseDto } from '@immich/sdk';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: CalendarHeatmapResponseDto;
|
||||||
|
itemLabel: (item: { date: string; count: number }) => string;
|
||||||
|
totalLabel: (count: number) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, itemLabel, totalLabel }: Props = $props();
|
||||||
|
|
||||||
|
const { rows } = $derived.by(() => {
|
||||||
|
const weeks = Array.from({ length: Math.ceil(data.series.length / 7) }, (_, index) =>
|
||||||
|
data.series.slice(index * 7, index * 7 + 7),
|
||||||
|
);
|
||||||
|
|
||||||
|
const rows = Array.from({ length: 7 }, (_, dayIndex) => weeks.map((week) => week[dayIndex]).filter(Boolean));
|
||||||
|
const endDate = DateTime.fromISO(data.to, { zone: 'utc' });
|
||||||
|
|
||||||
|
const months = Array.from({ length: 4 }, (_, index) =>
|
||||||
|
endDate.minus({ months: 11 - index * 4 }).toLocaleString({ month: 'short' }, { locale: $locale }),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { rows, months };
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxCount = $derived(Math.max(...data.series.map((item) => item.count), 0));
|
||||||
|
|
||||||
|
const itemColors = (count: number) => {
|
||||||
|
if (count === 0 || maxCount === 0) {
|
||||||
|
return 'bg-gray-200 dark:bg-gray-700';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count <= Math.ceil(maxCount * 0.25)) {
|
||||||
|
return 'bg-immich-primary/30';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count <= Math.ceil(maxCount * 0.5)) {
|
||||||
|
return 'bg-immich-primary/50';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count <= Math.ceil(maxCount * 0.75)) {
|
||||||
|
return 'bg-immich-primary/70';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'bg-immich-primary';
|
||||||
|
};
|
||||||
|
|
||||||
|
// const dayLabels = $derived([
|
||||||
|
// '',
|
||||||
|
// dayOfWeek('monday', { locale: $locale, style: 'short' }),
|
||||||
|
// '',
|
||||||
|
// dayOfWeek('wednesday', { locale: $locale, style: 'short' }),
|
||||||
|
// '',
|
||||||
|
// dayOfWeek('friday', { locale: $locale, style: 'short' }),
|
||||||
|
// '',
|
||||||
|
// ]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mt-4 w-full">
|
||||||
|
<div class="relative w-full">
|
||||||
|
<!-- TODO -->
|
||||||
|
<!-- <div class="absolute top-4 left-0 flex flex-col gap-0.5">
|
||||||
|
{#each dayLabels as dayLabel, i (i)}
|
||||||
|
<div class="relative flex h-3 w-6 items-center text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{dayLabel}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- <div class="mb-1 flex justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{#each getUploadActivityMonths() as month (month)}
|
||||||
|
<div>{month}</div>
|
||||||
|
{/each}
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="grid grid-rows-7 gap-0.5">
|
||||||
|
{#each rows as row, dayIndex (dayIndex)}
|
||||||
|
<div class="grid grid-cols-52 gap-0.5">
|
||||||
|
{#each row as day (day.date)}
|
||||||
|
<div
|
||||||
|
class="aspect-square w-full min-w-0 rounded-sm {itemColors(day.count)}"
|
||||||
|
title={itemLabel({ date: day.date, count: day.count })}
|
||||||
|
aria-label={itemLabel({ date: day.date, count: day.count })}
|
||||||
|
></div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<span>{$t('less')}</span>
|
||||||
|
<span class="size-3 rounded-sm bg-gray-200 dark:bg-gray-700"></span>
|
||||||
|
<span class="size-3 rounded-sm bg-immich-primary/30"></span>
|
||||||
|
<span class="size-3 rounded-sm bg-immich-primary/50"></span>
|
||||||
|
<span class="size-3 rounded-sm bg-immich-primary/70"></span>
|
||||||
|
<span class="size-3 rounded-sm bg-immich-primary"></span>
|
||||||
|
<span>{$t('more')}</span>
|
||||||
|
<span class="ml-4">{totalLabel(data.totalCount)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { cleanClass } from '$lib';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
height: number;
|
height: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
invisible?: boolean;
|
invisible?: boolean;
|
||||||
|
class?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { height = 0, title, invisible = false }: Props = $props();
|
let { height = 0, title, invisible = false, class: className }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={['overflow-clip', { invisible }]} style:height={height + 'px'}>
|
<div class={cleanClass('overflow-clip', invisible && 'invisible', className)} style:height={height + 'px'}>
|
||||||
{#if title}
|
{#if title}
|
||||||
<div
|
<div
|
||||||
class="flex h-6 place-items-center pt-7 pb-5 text-xs font-medium text-immich-fg max-md:pt-5 max-md:pb-3 md:text-sm dark:text-immich-dark-fg"
|
class="flex h-6 place-items-center pt-7 pb-5 text-xs font-medium text-immich-fg max-md:pt-5 max-md:pb-3 md:text-sm dark:text-immich-dark-fg"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export const cleanClass = (...classNames: unknown[]) => {
|
export const cleanClass = (...classNames: unknown[]) => {
|
||||||
|
|
@ -16,3 +17,10 @@ export const cleanClass = (...classNames: unknown[]) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isDefined = <T>(value: T): value is NonNullable<T> => value !== null && value !== undefined;
|
export const isDefined = <T>(value: T): value is NonNullable<T> => value !== null && value !== undefined;
|
||||||
|
|
||||||
|
export const getHeatmapRange = () => {
|
||||||
|
const to = DateTime.utc().startOf('day');
|
||||||
|
const from = to.minus({ weeks: 51 }).plus({ days: 1 });
|
||||||
|
const fromSunday = from.minus({ days: from.weekday % 7 });
|
||||||
|
return { $from: fromSunday.toISODate()!, to: to.toISODate()! };
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -78,3 +78,12 @@ export const getAlbumDateRange = (album: { startDate?: string; endDate?: string
|
||||||
*/
|
*/
|
||||||
export const asLocalTimeISO = (date: DateTime<true>) =>
|
export const asLocalTimeISO = (date: DateTime<true>) =>
|
||||||
(date.setZone('utc', { keepLocalTime: true }) as DateTime<true>).toISO();
|
(date.setZone('utc', { keepLocalTime: true }) as DateTime<true>).toISO();
|
||||||
|
|
||||||
|
const days = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
||||||
|
|
||||||
|
type DayOfWeek = (typeof days)[number];
|
||||||
|
export const dayOfWeek = (day: DayOfWeek, options?: { locale?: string; style?: 'long' | 'short' | 'narrow' }) => {
|
||||||
|
const fmt = new Intl.DateTimeFormat(options?.locale, { weekday: options?.style ?? 'long', timeZone: 'UTC' });
|
||||||
|
// 2021-08-01 is a Sunday
|
||||||
|
return fmt.format(new Date(Date.UTC(2021, 7, 1 + days.indexOf(day))));
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getHeatmapRange } from '$lib';
|
||||||
|
import CalendarHeatmap from '$lib/components/CalendarHeatmap.svelte';
|
||||||
|
import Skeleton from '$lib/elements/Skeleton.svelte';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import {
|
import {
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
|
CalendarHeatmapType,
|
||||||
getAlbumStatistics,
|
getAlbumStatistics,
|
||||||
getAssetStatistics,
|
getAssetStatistics,
|
||||||
|
getMyCalendarHeatmap,
|
||||||
type AlbumStatisticsResponseDto,
|
type AlbumStatisticsResponseDto,
|
||||||
type AssetStatsResponseDto,
|
type AssetStatsResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
|
|
@ -11,6 +16,8 @@
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
// always start on Sunday
|
||||||
|
|
||||||
let timelineStats: AssetStatsResponseDto = $state({
|
let timelineStats: AssetStatsResponseDto = $state({
|
||||||
videos: 0,
|
videos: 0,
|
||||||
images: 0,
|
images: 0,
|
||||||
|
|
@ -95,4 +102,28 @@
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
<div class="hidden lg:block">
|
||||||
|
<Heading size="tiny" class="mt-8">{$t('uploads')}</Heading>
|
||||||
|
{#await getMyCalendarHeatmap({ ...getHeatmapRange(), $type: CalendarHeatmapType.Upload })}
|
||||||
|
<Skeleton height={80} class="mt-2 rounded-lg" />
|
||||||
|
{:then data}
|
||||||
|
<CalendarHeatmap
|
||||||
|
{data}
|
||||||
|
itemLabel={(item) => $t('upload_day_count', { values: item })}
|
||||||
|
totalLabel={(count) => $t('uploads_count', { values: { count } })}
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
|
|
||||||
|
<Heading size="tiny" class="mt-8">{$t('assets')}</Heading>
|
||||||
|
{#await getMyCalendarHeatmap({ ...getHeatmapRange(), $type: CalendarHeatmapType.Taken })}
|
||||||
|
<Skeleton height={80} class="mt-2 rounded-lg" />
|
||||||
|
{:then data}
|
||||||
|
<CalendarHeatmap
|
||||||
|
{data}
|
||||||
|
itemLabel={(item) => $t('asset_day_count', { values: item })}
|
||||||
|
totalLabel={(count) => $t('assets_count', { values: { count } })}
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,11 @@
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { createDateFormatter, findLocale } from '$lib/utils';
|
import { createDateFormatter, findLocale } from '$lib/utils';
|
||||||
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
||||||
import { type UserAdminResponseDto } from '@immich/sdk';
|
import { CalendarHeatmapType, getUserCalendarHeatmapAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Badge,
|
Badge,
|
||||||
|
CardTitle,
|
||||||
Code,
|
Code,
|
||||||
CommandPaletteDefaultProvider,
|
CommandPaletteDefaultProvider,
|
||||||
Container,
|
Container,
|
||||||
|
|
@ -33,6 +34,7 @@
|
||||||
mdiChartPie,
|
mdiChartPie,
|
||||||
mdiChartPieOutline,
|
mdiChartPieOutline,
|
||||||
mdiCheckCircle,
|
mdiCheckCircle,
|
||||||
|
mdiCloudUploadOutline,
|
||||||
mdiDevices,
|
mdiDevices,
|
||||||
mdiFeatureSearchOutline,
|
mdiFeatureSearchOutline,
|
||||||
mdiPlayCircle,
|
mdiPlayCircle,
|
||||||
|
|
@ -41,6 +43,9 @@
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { LayoutData } from './$types';
|
import type { LayoutData } from './$types';
|
||||||
|
import { getHeatmapRange } from '$lib';
|
||||||
|
import Skeleton from '$lib/elements/Skeleton.svelte';
|
||||||
|
import CalendarHeatmap from '$lib/components/CalendarHeatmap.svelte';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
|
|
@ -205,6 +210,23 @@
|
||||||
{/each}
|
{/each}
|
||||||
</Stack>
|
</Stack>
|
||||||
</AdminCard>
|
</AdminCard>
|
||||||
|
|
||||||
|
<div class="col-span-2 px-4 py-2">
|
||||||
|
<div class="flex gap-2 text-primary">
|
||||||
|
<Icon icon={mdiCloudUploadOutline} size="1.5rem" />
|
||||||
|
<CardTitle>{$t('uploads')}</CardTitle>
|
||||||
|
</div>
|
||||||
|
{#await getUserCalendarHeatmapAdmin({ ...getHeatmapRange(), id: user.id, $type: CalendarHeatmapType.Upload })}
|
||||||
|
<Skeleton height={80} class="mt-2 rounded-lg" />
|
||||||
|
{:then data}
|
||||||
|
<CalendarHeatmap
|
||||||
|
{data}
|
||||||
|
itemLabel={(item) => $t('upload_day_count', { values: item })}
|
||||||
|
totalLabel={(count) => $t('uploads_count', { values: { count } })}
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
<!-- </AdminCard> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue