Merge branch 'main' of https://github.com/immich-app/immich into feat/cli-albums
This commit is contained in:
commit
927718fa32
79 changed files with 1544 additions and 176 deletions
121
cli/src/api/open-api/api.ts
generated
121
cli/src/api/open-api/api.ts
generated
|
@ -4,7 +4,7 @@
|
|||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.85.0
|
||||
* The version of the OpenAPI document: 1.86.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
@ -2355,6 +2355,12 @@ export interface OAuthConfigResponseDto {
|
|||
* @interface PartnerResponseDto
|
||||
*/
|
||||
export interface PartnerResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {UserAvatarColor}
|
||||
* @memberof PartnerResponseDto
|
||||
*/
|
||||
'avatarColor': UserAvatarColor;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
@ -2440,6 +2446,8 @@ export interface PartnerResponseDto {
|
|||
*/
|
||||
'updatedAt': string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -4344,6 +4352,12 @@ export interface UpdateTagDto {
|
|||
* @interface UpdateUserDto
|
||||
*/
|
||||
export interface UpdateUserDto {
|
||||
/**
|
||||
*
|
||||
* @type {UserAvatarColor}
|
||||
* @memberof UpdateUserDto
|
||||
*/
|
||||
'avatarColor'?: UserAvatarColor;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
@ -4399,6 +4413,8 @@ export interface UpdateUserDto {
|
|||
*/
|
||||
'storageLabel'?: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -4436,12 +4452,40 @@ export interface UsageByUserDto {
|
|||
*/
|
||||
'videos': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
|
||||
export const UserAvatarColor = {
|
||||
Primary: 'primary',
|
||||
Pink: 'pink',
|
||||
Red: 'red',
|
||||
Yellow: 'yellow',
|
||||
Blue: 'blue',
|
||||
Green: 'green',
|
||||
Purple: 'purple',
|
||||
Orange: 'orange',
|
||||
Gray: 'gray',
|
||||
Amber: 'amber'
|
||||
} as const;
|
||||
|
||||
export type UserAvatarColor = typeof UserAvatarColor[keyof typeof UserAvatarColor];
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface UserDto
|
||||
*/
|
||||
export interface UserDto {
|
||||
/**
|
||||
*
|
||||
* @type {UserAvatarColor}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'avatarColor': UserAvatarColor;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
@ -4467,12 +4511,20 @@ export interface UserDto {
|
|||
*/
|
||||
'profileImagePath': string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface UserResponseDto
|
||||
*/
|
||||
export interface UserResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {UserAvatarColor}
|
||||
* @memberof UserResponseDto
|
||||
*/
|
||||
'avatarColor': UserAvatarColor;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
@ -4552,6 +4604,8 @@ export interface UserResponseDto {
|
|||
*/
|
||||
'updatedAt': string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -16477,6 +16531,44 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteProfileImage: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/user/profile-image`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
|
@ -16802,6 +16894,15 @@ export const UserApiFp = function(configuration?: Configuration) {
|
|||
const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deleteProfileImage(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteProfileImage(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
|
@ -16899,6 +17000,14 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
|
|||
createUser(requestParameters: UserApiCreateUserRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
|
||||
return localVarFp.createUser(requestParameters.createUserDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteProfileImage(options?: AxiosRequestConfig): AxiosPromise<void> {
|
||||
return localVarFp.deleteProfileImage(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {UserApiDeleteUserRequest} requestParameters Request parameters.
|
||||
|
@ -17105,6 +17214,16 @@ export class UserApi extends BaseAPI {
|
|||
return UserApiFp(this.configuration).createUser(requestParameters.createUserDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof UserApi
|
||||
*/
|
||||
public deleteProfileImage(options?: AxiosRequestConfig) {
|
||||
return UserApiFp(this.configuration).deleteProfileImage(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {UserApiDeleteUserRequest} requestParameters Request parameters.
|
||||
|
|
2
cli/src/api/open-api/base.ts
generated
2
cli/src/api/open-api/base.ts
generated
|
@ -4,7 +4,7 @@
|
|||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.85.0
|
||||
* The version of the OpenAPI document: 1.86.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
|
2
cli/src/api/open-api/common.ts
generated
2
cli/src/api/open-api/common.ts
generated
|
@ -4,7 +4,7 @@
|
|||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.85.0
|
||||
* The version of the OpenAPI document: 1.86.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
|
2
cli/src/api/open-api/configuration.ts
generated
2
cli/src/api/open-api/configuration.ts
generated
|
@ -4,7 +4,7 @@
|
|||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.85.0
|
||||
* The version of the OpenAPI document: 1.86.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
|
2
cli/src/api/open-api/index.ts
generated
2
cli/src/api/open-api/index.ts
generated
|
@ -4,7 +4,7 @@
|
|||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.85.0
|
||||
* The version of the OpenAPI document: 1.86.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.85.0"
|
||||
version = "1.86.0"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
|
|
@ -35,8 +35,8 @@ platform :android do
|
|||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 109,
|
||||
"android.injected.version.name" => "1.85.0",
|
||||
"android.injected.version.code" => 110,
|
||||
"android.injected.version.name" => "1.86.0",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
|
|
@ -5,17 +5,17 @@
|
|||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000244">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000218">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="67.0562">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="65.551055">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="33.087498">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="31.674724">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
|
384
mobile/assets/i18n/fr-CA.json
Normal file
384
mobile/assets/i18n/fr-CA.json
Normal file
|
@ -0,0 +1,384 @@
|
|||
{
|
||||
"add_to_album_bottom_sheet_added": "Ajouté à {album}",
|
||||
"add_to_album_bottom_sheet_already_exists": "Déjà dans {album}",
|
||||
"advanced_settings_prefer_remote_subtitle": "Certains appareils sont très lents à charger des vignettes à partir de ressources présentes sur l'appareil. Activez ce paramètre pour charger des images externes à la place.",
|
||||
"advanced_settings_prefer_remote_title": "Préférer les images externes",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Permet d'ignorer la vérification du certificat SSL pour le point d'accès du serveur. Requis pour les certificats auto-signés.",
|
||||
"advanced_settings_self_signed_ssl_title": "Autoriser les certificats SSL auto-signés",
|
||||
"advanced_settings_tile_subtitle": "Paramètres d'utilisateur avancés",
|
||||
"advanced_settings_tile_title": "Avancé",
|
||||
"advanced_settings_troubleshooting_subtitle": "Activer des fonctions supplémentaires pour le dépannage",
|
||||
"advanced_settings_troubleshooting_title": "Dépannage",
|
||||
"album_info_card_backup_album_excluded": "EXCLUS",
|
||||
"album_info_card_backup_album_included": "INCLUS",
|
||||
"album_thumbnail_card_item": "1 élément",
|
||||
"album_thumbnail_card_items": "{} éléments",
|
||||
"album_thumbnail_card_shared": " · Partagé",
|
||||
"album_thumbnail_owned": "Possédé",
|
||||
"album_thumbnail_shared_by": "Partagé par {}",
|
||||
"album_viewer_appbar_share_delete": "Supprimer l'album",
|
||||
"album_viewer_appbar_share_err_delete": "Échec de la suppression de l'album",
|
||||
"album_viewer_appbar_share_err_leave": "Impossible de quitter l'album",
|
||||
"album_viewer_appbar_share_err_remove": "Il y a des problèmes lors de la suppression des éléments de l'album",
|
||||
"album_viewer_appbar_share_err_title": "Échec de la modification du titre de l'album",
|
||||
"album_viewer_appbar_share_leave": "Quitter l'album",
|
||||
"album_viewer_appbar_share_remove": "Retirer de l'album",
|
||||
"album_viewer_appbar_share_to": "Partager à",
|
||||
"album_viewer_page_share_add_users": "Ajouter des utilisateurs",
|
||||
"all_people_page_title": "Personnes",
|
||||
"all_videos_page_title": "Vidéos",
|
||||
"app_bar_signout_dialog_content": "Êtes-vous sûr de vouloir vous déconnecter?",
|
||||
"app_bar_signout_dialog_ok": "Oui",
|
||||
"app_bar_signout_dialog_title": "Se déconnecter",
|
||||
"archive_page_no_archived_assets": "Aucun élément archivé n'a été trouvé",
|
||||
"archive_page_title": "Archive ({})",
|
||||
"asset_list_layout_settings_dynamic_layout_title": "Affichage dynamique",
|
||||
"asset_list_layout_settings_group_automatically": "Automatique",
|
||||
"asset_list_layout_settings_group_by": "Grouper les éléments par",
|
||||
"asset_list_layout_settings_group_by_month": "Mois",
|
||||
"asset_list_layout_settings_group_by_month_day": "Mois + jour",
|
||||
"asset_list_settings_subtitle": "Paramètres de disposition de la grille de photos",
|
||||
"asset_list_settings_title": "Grille de photos",
|
||||
"backup_album_selection_page_albums_device": "Albums sur l'appareil ({})",
|
||||
"backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure",
|
||||
"backup_album_selection_page_assets_scatter": "Les éléments peuvent être répartis sur plusieurs albums. De ce fait, les albums peuvent être inclus ou exclus pendant le processus de sauvegarde.",
|
||||
"backup_album_selection_page_select_albums": "Sélectionner les albums",
|
||||
"backup_album_selection_page_selection_info": "Informations sur la sélection",
|
||||
"backup_album_selection_page_total_assets": "Total des éléments uniques",
|
||||
"backup_all": "Tout",
|
||||
"backup_background_service_backup_failed_message": "Échec de la sauvegarde des éléments. Nouvelle tentative...",
|
||||
"backup_background_service_connection_failed_message": "Impossible de se connecter au serveur. Nouvelle tentative...",
|
||||
"backup_background_service_current_upload_notification": "Transfert {}",
|
||||
"backup_background_service_default_notification": "Recherche de nouveaux éléments...",
|
||||
"backup_background_service_error_title": "Erreur de sauvegarde",
|
||||
"backup_background_service_in_progress_notification": "Sauvegarde de vos éléments...",
|
||||
"backup_background_service_upload_failure_notification": "Impossible de transférer {}",
|
||||
"backup_controller_page_albums": "Sauvegarder les albums",
|
||||
"backup_controller_page_background_app_refresh_disabled_content": "Activez le rafraîchissement de l'application en arrière-plan dans Paramètres > Général > Rafraîchissement de l'application en arrière-plan afin d'utiliser la sauvegarde en arrière-plan.",
|
||||
"backup_controller_page_background_app_refresh_disabled_title": "Rafraîchissement de l'application en arrière-plan désactivé",
|
||||
"backup_controller_page_background_app_refresh_enable_button_text": "Aller aux paramètres",
|
||||
"backup_controller_page_background_battery_info_link": "Montrez-moi comment",
|
||||
"backup_controller_page_background_battery_info_message": "Pour une expérience optimale de la sauvegarde en arrière-plan, veuillez désactiver toute optimisation de la batterie limitant l'activité en arrière-plan pour Immich.\n\nÉtant donné que cela est spécifique à chaque appareil, veuillez consulter les informations requises pour le fabricant de votre appareil.",
|
||||
"backup_controller_page_background_battery_info_ok": "OK",
|
||||
"backup_controller_page_background_battery_info_title": "Optimisation de la batterie",
|
||||
"backup_controller_page_background_charging": "Seulement pendant la charge",
|
||||
"backup_controller_page_background_configure_error": "Échec de la configuration du service d'arrière-plan",
|
||||
"backup_controller_page_background_delay": "Retarder la sauvegarde des nouveaux éléments d'actif: {}",
|
||||
"backup_controller_page_background_description": "Activez le service d'arrière-plan pour sauvegarder automatiquement tous les nouveaux éléments sans avoir à ouvrir l'application.",
|
||||
"backup_controller_page_background_is_off": "La sauvegarde automatique en arrière-plan est désactivée",
|
||||
"backup_controller_page_background_is_on": "La sauvegarde automatique en arrière-plan est activée",
|
||||
"backup_controller_page_background_turn_off": "Désactiver le service d'arrière-plan",
|
||||
"backup_controller_page_background_turn_on": "Activer le service d'arrière-plan",
|
||||
"backup_controller_page_background_wifi": "Uniquement sur WiFi",
|
||||
"backup_controller_page_backup": "Sauvegardé",
|
||||
"backup_controller_page_backup_selected": "Sélectionné: ",
|
||||
"backup_controller_page_backup_sub": "Photos et vidéos sauvegardées",
|
||||
"backup_controller_page_cancel": "Annuler",
|
||||
"backup_controller_page_created": "Créé le: {}",
|
||||
"backup_controller_page_desc_backup": "Activez la sauvegarde pour envoyer automatiquement les nouveaux éléments sur le serveur.",
|
||||
"backup_controller_page_excluded": "Exclus: ",
|
||||
"backup_controller_page_failed": "Échec de l'opération ({})",
|
||||
"backup_controller_page_filename": "Nom du fichier: {} [{}]",
|
||||
"backup_controller_page_id": "ID: {}",
|
||||
"backup_controller_page_info": "Informations de sauvegarde",
|
||||
"backup_controller_page_none_selected": "Aucune sélection",
|
||||
"backup_controller_page_remainder": "Restant",
|
||||
"backup_controller_page_remainder_sub": "Photos et albums restants à sauvegarder à partir de la sélection",
|
||||
"backup_controller_page_select": "Sélectionner",
|
||||
"backup_controller_page_server_storage": "Stockage du serveur",
|
||||
"backup_controller_page_start_backup": "Démarrer la sauvegarde",
|
||||
"backup_controller_page_status_off": "La sauvegarde est désactivée",
|
||||
"backup_controller_page_status_on": "La sauvegarde est activée",
|
||||
"backup_controller_page_storage_format": "{} de {} utilisé",
|
||||
"backup_controller_page_to_backup": "Albums à sauvegarder",
|
||||
"backup_controller_page_total": "Total",
|
||||
"backup_controller_page_total_sub": "Toutes les photos et vidéos uniques des albums sélectionnés",
|
||||
"backup_controller_page_turn_off": "Désactiver la sauvegarde",
|
||||
"backup_controller_page_turn_on": "Activer la sauvegarde",
|
||||
"backup_controller_page_uploading_file_info": "Transfert des informations du fichier",
|
||||
"backup_err_only_album": "Impossible de retirer le seul album",
|
||||
"backup_info_card_assets": "éléments",
|
||||
"backup_manual_cancelled": "Annulé",
|
||||
"backup_manual_failed": "Echec",
|
||||
"backup_manual_in_progress": "Téléchargement déjà en cours. Essayez après un instant",
|
||||
"backup_manual_success": "Succès ",
|
||||
"backup_manual_title": "Statut du téléchargement ",
|
||||
"cache_settings_album_thumbnails": "vignettes de la page bibliothèque ({} éléments)",
|
||||
"cache_settings_clear_cache_button": "Effacer le cache",
|
||||
"cache_settings_clear_cache_button_title": "Efface le cache de l'application. Cela aura un impact significatif sur les performances de l'application jusqu'à ce que le cache soit reconstruit.",
|
||||
"cache_settings_image_cache_size": "Taille du cache des images ({} éléments)",
|
||||
"cache_settings_statistics_album": "vignettes de la bibliothèque",
|
||||
"cache_settings_statistics_assets": "{} éléments ({})",
|
||||
"cache_settings_statistics_full": "Images complètes",
|
||||
"cache_settings_statistics_shared": "vignettes d'albums partagés",
|
||||
"cache_settings_statistics_thumbnail": "vignettes",
|
||||
"cache_settings_statistics_title": "Utilisation du cache",
|
||||
"cache_settings_subtitle": "Contrôler le comportement de mise en cache de l'application mobile Immich",
|
||||
"cache_settings_thumbnail_size": "Taille du cache des vignettes ({} éléments)",
|
||||
"cache_settings_tile_subtitle": "Contrôler le comportement du stockage local",
|
||||
"cache_settings_tile_title": "Stockage local",
|
||||
"cache_settings_title": "Paramètres de mise en cache",
|
||||
"change_password_form_confirm_password": "Confirmez le mot de passe",
|
||||
"change_password_form_description": "Bonjour {firstName} {lastName},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé de changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.",
|
||||
"change_password_form_new_password": "Nouveau mot de passe",
|
||||
"change_password_form_password_mismatch": "Les mots de passe ne correspondent pas",
|
||||
"change_password_form_reenter_new_password": "Saisissez à nouveau le nouveau mot de passe",
|
||||
"common_add_to_album": "Ajouter à l'album",
|
||||
"common_change_password": "Modifier le mot de passe",
|
||||
"common_create_new_album": "Créer un nouvel album",
|
||||
"common_server_error": "Veuillez vérifier votre connexion réseau, vous assurer que le serveur est accessible et que les versions de l'application et du serveur sont compatibles.",
|
||||
"common_shared": "Partagé",
|
||||
"control_bottom_app_bar_add_to_album": "Ajouter à l'album",
|
||||
"control_bottom_app_bar_album_info": "{} éléments",
|
||||
"control_bottom_app_bar_album_info_shared": "{} éléments - Partagés",
|
||||
"control_bottom_app_bar_archive": "Archive",
|
||||
"control_bottom_app_bar_create_new_album": "Créer un nouvel album",
|
||||
"control_bottom_app_bar_delete": "Supprimer",
|
||||
"control_bottom_app_bar_favorite": "Favoris",
|
||||
"control_bottom_app_bar_share": "Partager",
|
||||
"control_bottom_app_bar_share_to": "Partager à",
|
||||
"control_bottom_app_bar_stack": "Empiler",
|
||||
"control_bottom_app_bar_unarchive": "Désarchiver",
|
||||
"control_bottom_app_bar_upload": "Téléverser",
|
||||
"create_album_page_untitled": "Sans titre",
|
||||
"create_shared_album_page_create": "Créer",
|
||||
"create_shared_album_page_share": "Partager",
|
||||
"create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
|
||||
"create_shared_album_page_share_select_photos": "Sélectionner les photos",
|
||||
"curated_location_page_title": "Places",
|
||||
"curated_object_page_title": "Objets",
|
||||
"daily_title_text_date": "E, dd MMM",
|
||||
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||
"date_format": "E, LLL d, y • h:mm a",
|
||||
"delete_dialog_alert": "Ces éléments seront définitivement supprimés de Immich et de votre appareil.",
|
||||
"delete_dialog_cancel": "Annuler",
|
||||
"delete_dialog_ok": "Supprimer",
|
||||
"delete_dialog_title": "Supprimer définitivement",
|
||||
"delete_shared_link_dialog_content": "Êtes-vous sûr de vouloir supprimer ce lien partagé?",
|
||||
"delete_shared_link_dialog_title": "Supprimer le lien partagé",
|
||||
"description_input_hint_text": "Ajouter une description...",
|
||||
"description_input_submit_error": "Erreur de mise à jour de la description, vérifier le journal pour plus de détails",
|
||||
"exif_bottom_sheet_description": "Ajouter une description...",
|
||||
"exif_bottom_sheet_details": "DÉTAILS",
|
||||
"exif_bottom_sheet_location": "LOCALISATION",
|
||||
"experimental_settings_new_asset_list_subtitle": "En cours de développement",
|
||||
"experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale",
|
||||
"experimental_settings_subtitle": "Utilisez à vos dépends!",
|
||||
"experimental_settings_title": "Expérimental",
|
||||
"favorites_page_no_favorites": "Aucun élément favori n'a été trouvé",
|
||||
"favorites_page_title": "Favoris",
|
||||
"home_page_add_to_album_conflicts": "{added} éléments ajoutés à l'album {album}. Les éléments {failed} sont déjà dans l'album.",
|
||||
"home_page_add_to_album_err_local": "Impossible d'ajouter des éléments locaux aux albums pour le moment, étape ignorée",
|
||||
"home_page_add_to_album_success": "{added} éléments ajoutés à l'album {album}.",
|
||||
"home_page_archive_err_local": "Impossible d'archiver les ressources locales pour l'instant, étape ignorée",
|
||||
"home_page_building_timeline": "Construction de la chronologie",
|
||||
"home_page_favorite_err_local": "Impossible d'ajouter des éléments locaux aux favoris pour le moment, étape ignorée",
|
||||
"home_page_first_time_notice": "Si c'est la première fois que vous utilisez l'application, veillez à choisir un ou plusieurs albums de sauvegarde afin que la chronologie puisse alimenter les photos et les vidéos de cet ou ces albums.",
|
||||
"home_page_upload_err_limit": "Limite de téléchargement de 30 éléments en même temps, demande ignorée",
|
||||
"image_viewer_page_state_provider_download_error": "Erreur de téléchargement",
|
||||
"image_viewer_page_state_provider_download_success": "Téléchargement réussi",
|
||||
"image_viewer_page_state_provider_share_error": "Erreur de partage",
|
||||
"library_page_albums": "Albums",
|
||||
"library_page_archive": "Archive",
|
||||
"library_page_device_albums": "Albums sur l'appareil",
|
||||
"library_page_favorites": "Favoris",
|
||||
"library_page_new_album": "Nouvel album",
|
||||
"library_page_sharing": "Partage",
|
||||
"library_page_sort_created": "Créations les plus récentes",
|
||||
"library_page_sort_last_modified": "Dernière modification",
|
||||
"library_page_sort_most_recent_photo": "Photo la plus récente",
|
||||
"library_page_sort_title": "Titre de l'album",
|
||||
"login_disabled": "La connexion a été désactivée ",
|
||||
"login_form_api_exception": "Erreur de l'API. Veuillez vérifier l'URL du serveur et et réessayer.",
|
||||
"login_form_button_text": "Connexion",
|
||||
"login_form_email_hint": "votreemail@email.com",
|
||||
"login_form_endpoint_hint": "http://adresse-ip-serveur:port/api",
|
||||
"login_form_endpoint_url": "URL du point d'accès au serveur",
|
||||
"login_form_err_http": "Veuillez préciser http:// ou https://",
|
||||
"login_form_err_invalid_email": "E-mail invalide",
|
||||
"login_form_err_invalid_url": "URL invalide",
|
||||
"login_form_err_leading_whitespace": "Espace en début de ligne",
|
||||
"login_form_err_trailing_whitespace": "Espace de fin de ligne",
|
||||
"login_form_failed_get_oauth_server_config": "Erreur de connexion par OAuth, vérifiez l\"URL du serveur",
|
||||
"login_form_failed_get_oauth_server_disable": "La fonctionnalité OAuth n'est pas disponible sur ce serveur",
|
||||
"login_form_failed_login": "Erreur de connexion, vérifiez l'url du serveur, l'email et le mot de passe",
|
||||
"login_form_handshake_exception": "Il y a eu une exception de liaison avec le serveur. Activez la prise en charge des certificats auto-signés dans les paramètres si vous utilisez un certificat auto-signé.",
|
||||
"login_form_label_email": "E-mail",
|
||||
"login_form_label_password": "Mot de passe",
|
||||
"login_form_next_button": "Suivant",
|
||||
"login_form_password_hint": "mot de passe",
|
||||
"login_form_save_login": "Rester connecté",
|
||||
"login_form_server_empty": "Saisissez l'URL du serveur.",
|
||||
"login_form_server_error": "Impossible de se connecter au serveur.",
|
||||
"login_password_changed_error": "Une erreur s'est produite lors de la mise à jour de votre mot de passe",
|
||||
"login_password_changed_success": "Mot de passe mis à jour avec succès",
|
||||
"map_cannot_get_user_location": "Impossible d'obtenir la localisation de l'utilisateur",
|
||||
"map_location_dialog_cancel": "Annuler",
|
||||
"map_location_dialog_yes": "Oui",
|
||||
"map_location_service_disabled_content": "Le service de localisation doit être activé pour afficher les éléments de votre emplacement actuel. Souhaitez-vous l'activer maintenant?",
|
||||
"map_location_service_disabled_title": "Service de localisation désactivé",
|
||||
"map_no_assets_in_bounds": "Pas de photos dans cette zone",
|
||||
"map_no_location_permission_content": "L'autorisation de localisation est nécessaire pour afficher les éléments de votre emplacement actuel. Souhaitez-vous l'autoriser maintenant?",
|
||||
"map_no_location_permission_title": "Permission de localisation refusée",
|
||||
"map_settings_dark_mode": "Mode sombre",
|
||||
"map_settings_dialog_cancel": "Annuler",
|
||||
"map_settings_dialog_save": "Sauvegarder",
|
||||
"map_settings_dialog_title": "Paramètres de la carte",
|
||||
"map_settings_include_show_archived": "Inclure les archives",
|
||||
"map_settings_only_relative_range": "Plage de dates",
|
||||
"map_settings_only_show_favorites": "Afficher uniquement les favoris",
|
||||
"map_zoom_to_see_photos": "Dézoomer pour voir les photos",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Photos avec mouvement",
|
||||
"notification_permission_dialog_cancel": "Annuler",
|
||||
"notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.",
|
||||
"notification_permission_dialog_settings": "Paramètres",
|
||||
"notification_permission_list_tile_content": "Accordez la permission d'activer les notifications.",
|
||||
"notification_permission_list_tile_enable_button": "Activer les notifications",
|
||||
"notification_permission_list_tile_title": "Permission de notification",
|
||||
"partner_page_add_partner": "Ajouter un partenaire",
|
||||
"partner_page_empty_message": "Vos photos ne sont pas encore partagées avec un partenaire.",
|
||||
"partner_page_no_more_users": "Plus d'utilisateurs à ajouter",
|
||||
"partner_page_partner_add_failed": "Échec de l'ajout d'un partenaire",
|
||||
"partner_page_select_partner": "Sélectionner un partenaire",
|
||||
"partner_page_shared_to_title": "Partagé avec",
|
||||
"partner_page_stop_sharing_content": "{} ne pourra plus accéder à vos photos.",
|
||||
"partner_page_stop_sharing_title": "Arrêter de partager vos photos?",
|
||||
"partner_page_title": "Partenaire",
|
||||
"permission_onboarding_continue_anyway": "Continuer quand même",
|
||||
"permission_onboarding_get_started": "Commencer",
|
||||
"permission_onboarding_go_to_settings": "Accéder aux paramètres",
|
||||
"permission_onboarding_grant_permission": "Accorder l'autorisation",
|
||||
"permission_onboarding_log_out": "Se déconnecter",
|
||||
"permission_onboarding_permission_denied": "Permission refusée. Pour utiliser Immich, accordez lautorisation pour les photos et vidéos dans les Paramètres.",
|
||||
"permission_onboarding_permission_granted": "Permission accordée! Vous êtes prêts.",
|
||||
"permission_onboarding_permission_limited": "Permission limitée. Pour permettre à Immich de sauvegarder et de gérer l'ensemble de votre bibliothèque, accordez l'autorisation pour les photos et vidéos dans les Paramètres.",
|
||||
"permission_onboarding_request": "Immich demande l'autorisation de visionner vos photos et vidéo",
|
||||
"profile_drawer_app_logs": "Journaux",
|
||||
"profile_drawer_client_server_up_to_date": "Le client et le serveur sont à jour",
|
||||
"profile_drawer_documentation": "Documentation",
|
||||
"profile_drawer_github": "GitHub",
|
||||
"profile_drawer_settings": "Paramètres",
|
||||
"profile_drawer_sign_out": "Se déconnecter",
|
||||
"profile_drawer_trash": "Corbeille",
|
||||
"recently_added_page_title": "Récemment ajouté",
|
||||
"search_bar_hint": "Rechercher vos photos",
|
||||
"search_page_categories": "Catégories",
|
||||
"search_page_favorites": "Favoris",
|
||||
"search_page_motion_photos": "Photos avec mouvement",
|
||||
"search_page_no_objects": "Aucune information disponible sur les objets",
|
||||
"search_page_no_places": "Aucune information disponible sur la localisation",
|
||||
"search_page_people": "Personnes",
|
||||
"search_page_places": "Lieux",
|
||||
"search_page_recently_added": "Récemment ajouté",
|
||||
"search_page_screenshots": "Captures d'écran",
|
||||
"search_page_selfies": "Selfies",
|
||||
"search_page_things": "Objets",
|
||||
"search_page_videos": "Vidéos",
|
||||
"search_page_view_all_button": "Voir tout",
|
||||
"search_page_your_activity": "Votre activité",
|
||||
"search_result_page_new_search_hint": "Nouvelle recherche",
|
||||
"search_suggestion_list_smart_search_hint_1": "La recherche intelligente est activée par défaut. Pour rechercher des métadonnées, utilisez la syntaxe suivante",
|
||||
"search_suggestion_list_smart_search_hint_2": "m:votre-terme-de-recherche",
|
||||
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
||||
"select_user_for_sharing_page_err_album": "Échec de la création de l'album",
|
||||
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
||||
"server_info_box_app_version": "Version de l'application",
|
||||
"server_info_box_server_url": "URL du serveur",
|
||||
"server_info_box_server_version": "Version du serveur",
|
||||
"setting_image_viewer_help": "Le visualiseur de détails charge d'abord la petite vignette, puis l'aperçu de taille moyenne (s'il est activé), enfin l'original (s'il est activé).",
|
||||
"setting_image_viewer_original_subtitle": "Activez cette option pour charger l'image en résolution originale (volumineux!). Désactiver pour réduire l'utilisation des données (réseau et cache de l'appareil).",
|
||||
"setting_image_viewer_original_title": "Charger l'image originale",
|
||||
"setting_image_viewer_preview_subtitle": "Activer pour charger une image de résolution moyenne. Désactiver pour charger directement l'original ou utiliser uniquement la vignette.",
|
||||
"setting_image_viewer_preview_title": "Charger l'image d'aperçu",
|
||||
"setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan: {}",
|
||||
"setting_notifications_notify_hours": "{} heures",
|
||||
"setting_notifications_notify_immediately": "immédiatement",
|
||||
"setting_notifications_notify_minutes": "{} minutes",
|
||||
"setting_notifications_notify_never": "jamais",
|
||||
"setting_notifications_notify_seconds": "{} secondes",
|
||||
"setting_notifications_single_progress_subtitle": "Informations détaillées sur la progression du transfert par élément",
|
||||
"setting_notifications_single_progress_title": "Afficher la progression du détail de la sauvegarde en arrière-plan",
|
||||
"setting_notifications_subtitle": "Ajustez vos préférences de notification",
|
||||
"setting_notifications_title": "Notifications",
|
||||
"setting_notifications_total_progress_subtitle": "Progression globale du transfert (effectué/total des éléments)",
|
||||
"setting_notifications_total_progress_title": "Afficher la progression totale de la sauvegarde en arrière-plan",
|
||||
"setting_pages_app_bar_settings": "Paramètres",
|
||||
"settings_require_restart": "Veuillez redémarrer Immich pour appliquer ce paramètre",
|
||||
"share_add": "Ajouter",
|
||||
"share_add_photos": "Ajouter des photos",
|
||||
"share_add_title": "Ajouter un titre",
|
||||
"share_create_album": "Créer un album",
|
||||
"shared_album_activities_input_disable": "Les commentaires sont désactivés",
|
||||
"shared_album_activities_input_hint": "Dire quelque chose",
|
||||
"shared_album_activity_remove_content": "Souhaitez-vous supprimer cette activité?",
|
||||
"shared_album_activity_remove_title": "Supprimer l'activité",
|
||||
"shared_album_activity_setting_subtitle": "Laisser les autres réagir",
|
||||
"shared_album_activity_setting_title": "Commentaires et likes",
|
||||
"share_dialog_preparing": "Préparation...",
|
||||
"shared_link_app_bar_title": "Liens partagés",
|
||||
"shared_link_create_app_bar_title": "Créer un lien pour partager",
|
||||
"shared_link_create_info": "Permettre à toute personne ayant le lien de voir la ou les photos sélectionnées",
|
||||
"shared_link_create_submit_button": "Créer le lien",
|
||||
"shared_link_edit_allow_download": "Autoriser les utilisateurs publics à télécharger",
|
||||
"shared_link_edit_allow_upload": "Autoriser les utilisateurs publics à téléverser",
|
||||
"shared_link_edit_app_bar_title": "Modifier le lien",
|
||||
"shared_link_edit_change_expiry": "Modifier le délai d'expiration",
|
||||
"shared_link_edit_description": "Description",
|
||||
"shared_link_edit_description_hint": "Saisir la description du partage",
|
||||
"shared_link_edit_expire_after": "Expire après",
|
||||
"shared_link_edit_password": "Mot de passe",
|
||||
"shared_link_edit_password_hint": "Saisir le mot de passe de partage",
|
||||
"shared_link_edit_show_meta": "Afficher les métadonnées",
|
||||
"shared_link_edit_submit_button": "Mettre à jour le lien",
|
||||
"shared_link_empty": "Vous n'avez pas de liens partagés",
|
||||
"shared_link_manage_links": "Gérer les liens partagés",
|
||||
"share_done": "Fait",
|
||||
"share_invite": "Inviter à l'album",
|
||||
"sharing_page_album": "Albums partagés",
|
||||
"sharing_page_description": "Créez des albums partagés pour partager des photos et des vidéos avec les personnes de votre réseau.",
|
||||
"sharing_page_empty_list": "LISTE VIDE",
|
||||
"sharing_silver_appbar_create_shared_album": "Créer un album partagé",
|
||||
"sharing_silver_appbar_shared_links": "Liens partagés",
|
||||
"sharing_silver_appbar_share_partner": "Partager avec un partenaire",
|
||||
"tab_controller_nav_library": "Bibliothèque",
|
||||
"tab_controller_nav_photos": "Photos",
|
||||
"tab_controller_nav_search": "Recherche",
|
||||
"tab_controller_nav_sharing": "Partage",
|
||||
"theme_setting_asset_list_storage_indicator_title": "Afficher l'indicateur de stockage sur les tuiles des éléments",
|
||||
"theme_setting_asset_list_tiles_per_row_title": "Nombre d'éléments par ligne ({})",
|
||||
"theme_setting_dark_mode_switch": "Mode sombre",
|
||||
"theme_setting_image_viewer_quality_subtitle": "Ajustez la qualité de la visionneuse d'images détaillées",
|
||||
"theme_setting_image_viewer_quality_title": "Qualité de la visualisation des images",
|
||||
"theme_setting_system_theme_switch": "Automatique (suivre les paramètres du système)",
|
||||
"theme_setting_theme_subtitle": "Choisissez le thème de l'application",
|
||||
"theme_setting_theme_title": "Thème",
|
||||
"theme_setting_three_stage_loading_subtitle": "Le chargement en trois étapes peut améliorer les performances de chargement, mais entraîne une augmentation significative de la charge du réseau.",
|
||||
"theme_setting_three_stage_loading_title": "Activer le chargement en trois étapes",
|
||||
"translated_text_options": "Options",
|
||||
"trash_page_delete": "Supprimer",
|
||||
"trash_page_delete_all": "Tout supprimer",
|
||||
"trash_page_empty_trash_btn": "Vider la corbeille",
|
||||
"trash_page_empty_trash_dialog_content": "Voulez-vous vider les éléments de la corbeille? Ces objets seront définitivement retirés d'Immich",
|
||||
"trash_page_empty_trash_dialog_ok": "Ok",
|
||||
"trash_page_info": "Les éléments mis à la corbeille seront définitivement supprimés au bout de {} jours.",
|
||||
"trash_page_no_assets": "Pas d'éléments dans la corbeille",
|
||||
"trash_page_restore": "Restaurer",
|
||||
"trash_page_restore_all": "Tout restaurer",
|
||||
"trash_page_select_assets_btn": "Sélectionner les éléments",
|
||||
"trash_page_select_btn": "Sélectionner",
|
||||
"trash_page_title": "Corbeille ({})",
|
||||
"upload_dialog_cancel": "Annuler",
|
||||
"upload_dialog_info": "Voulez-vous sauvegarder la sélection vers le serveur?",
|
||||
"upload_dialog_ok": "Télécharger ",
|
||||
"upload_dialog_title": "Télécharger cet élément ",
|
||||
"version_announcement_overlay_ack": "Confirmer",
|
||||
"version_announcement_overlay_release_notes": "notes de mise à jour",
|
||||
"version_announcement_overlay_text_1": "Bonjour, une nouvelle version de",
|
||||
"version_announcement_overlay_text_2": "veuillez prendre le temps de visiter le ",
|
||||
"version_announcement_overlay_text_3": " et assurez-vous que votre configuration docker-compose et .env est à jour pour éviter toute erreur de configuration, en particulier si vous utilisez WatchTower ou tout autre mécanisme qui gère la mise à jour automatique de votre application serveur.",
|
||||
"version_announcement_overlay_title": "Nouvelle version serveur disponible \uD83C\uDF89",
|
||||
"viewer_remove_from_stack": "Retirer de la pile",
|
||||
"viewer_stack_use_as_main_asset": "Utiliser comme élément principal",
|
||||
"viewer_unstack": "Désempiler"
|
||||
}
|
|
@ -169,4 +169,4 @@ SPEC CHECKSUMS:
|
|||
|
||||
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
COCOAPODS: 1.12.1
|
||||
|
|
|
@ -379,7 +379,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 125;
|
||||
CURRENT_PROJECT_VERSION = 126;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
@ -515,7 +515,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 125;
|
||||
CURRENT_PROJECT_VERSION = 126;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
@ -543,7 +543,7 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 125;
|
||||
CURRENT_PROJECT_VERSION = 126;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
<string>es</string>
|
||||
<string>vi</string>
|
||||
<string>fr</string>
|
||||
<string>fr</string>
|
||||
<string>ja</string>
|
||||
<string>pl</string>
|
||||
<string>fi</string>
|
||||
|
@ -59,11 +60,11 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.85.0</string>
|
||||
<string>1.86.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>125</string>
|
||||
<string>126</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true />
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
|
|
@ -19,7 +19,7 @@ platform :ios do
|
|||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.85.0"
|
||||
version_number: "1.86.0"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
|
|
@ -5,32 +5,32 @@
|
|||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000291">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000247">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.199372">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.168149">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="6.104477">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="8.849255">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.164465">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.190436">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="108.828838">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="130.972743">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="60.89387">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="56.94492">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ const List<Locale> locales = [
|
|||
Locale('it', 'IT'),
|
||||
Locale('es', 'ES'),
|
||||
Locale('vi', 'VN'),
|
||||
Locale('fr', 'CA'),
|
||||
Locale('fr', 'FR'),
|
||||
Locale('ja', 'JP'),
|
||||
Locale('pl', 'PL'),
|
||||
|
|
|
@ -117,12 +117,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||
|
||||
buildOwnerInfo() {
|
||||
return ListTile(
|
||||
leading: owner != null
|
||||
? UserCircleAvatar(
|
||||
user: owner,
|
||||
useRandomBackgroundColor: true,
|
||||
)
|
||||
: const SizedBox(),
|
||||
leading:
|
||||
owner != null ? UserCircleAvatar(user: owner) : const SizedBox(),
|
||||
title: Text(
|
||||
album.owner.value?.name ?? "",
|
||||
style: const TextStyle(
|
||||
|
@ -151,7 +147,6 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
|||
return ListTile(
|
||||
leading: UserCircleAvatar(
|
||||
user: user,
|
||||
useRandomBackgroundColor: true,
|
||||
radius: 22,
|
||||
),
|
||||
title: Text(
|
||||
|
|
|
@ -217,7 +217,6 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||
user: album.sharedUsers.toList()[index],
|
||||
radius: 18,
|
||||
size: 36,
|
||||
useRandomBackgroundColor: true,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/utils/hash.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
@ -16,6 +18,7 @@ class User {
|
|||
this.isPartnerSharedBy = false,
|
||||
this.isPartnerSharedWith = false,
|
||||
this.profileImagePath = '',
|
||||
this.avatarColor = AvatarColorEnum.primary,
|
||||
this.memoryEnabled = true,
|
||||
this.inTimeline = false,
|
||||
});
|
||||
|
@ -32,6 +35,7 @@ class User {
|
|||
profileImagePath = dto.profileImagePath,
|
||||
isAdmin = dto.isAdmin,
|
||||
memoryEnabled = dto.memoriesEnabled ?? false,
|
||||
avatarColor = dto.avatarColor.toAvatarColor(),
|
||||
inTimeline = false;
|
||||
|
||||
User.fromPartnerDto(PartnerResponseDto dto)
|
||||
|
@ -44,6 +48,7 @@ class User {
|
|||
profileImagePath = dto.profileImagePath,
|
||||
isAdmin = dto.isAdmin,
|
||||
memoryEnabled = dto.memoriesEnabled ?? false,
|
||||
avatarColor = dto.avatarColor.toAvatarColor(),
|
||||
inTimeline = dto.inTimeline ?? false;
|
||||
|
||||
@Index(unique: true, replace: false, type: IndexType.hash)
|
||||
|
@ -55,6 +60,8 @@ class User {
|
|||
bool isPartnerSharedWith;
|
||||
bool isAdmin;
|
||||
String profileImagePath;
|
||||
@Enumerated(EnumType.ordinal)
|
||||
AvatarColorEnum avatarColor;
|
||||
bool memoryEnabled;
|
||||
bool inTimeline;
|
||||
|
||||
|
@ -68,6 +75,7 @@ class User {
|
|||
if (other is! User) return false;
|
||||
return id == other.id &&
|
||||
updatedAt.isAtSameMomentAs(other.updatedAt) &&
|
||||
avatarColor == other.avatarColor &&
|
||||
email == other.email &&
|
||||
name == other.name &&
|
||||
isPartnerSharedBy == other.isPartnerSharedBy &&
|
||||
|
@ -88,7 +96,77 @@ class User {
|
|||
isPartnerSharedBy.hashCode ^
|
||||
isPartnerSharedWith.hashCode ^
|
||||
profileImagePath.hashCode ^
|
||||
avatarColor.hashCode ^
|
||||
isAdmin.hashCode ^
|
||||
memoryEnabled.hashCode ^
|
||||
inTimeline.hashCode;
|
||||
}
|
||||
|
||||
enum AvatarColorEnum {
|
||||
// do not change this order or reuse indices for other purposes, adding is OK
|
||||
primary,
|
||||
pink,
|
||||
red,
|
||||
yellow,
|
||||
blue,
|
||||
green,
|
||||
purple,
|
||||
orange,
|
||||
gray,
|
||||
amber,
|
||||
}
|
||||
|
||||
extension AvatarColorEnumHelper on UserAvatarColor {
|
||||
AvatarColorEnum toAvatarColor() {
|
||||
switch (this) {
|
||||
case UserAvatarColor.primary:
|
||||
return AvatarColorEnum.primary;
|
||||
case UserAvatarColor.pink:
|
||||
return AvatarColorEnum.pink;
|
||||
case UserAvatarColor.red:
|
||||
return AvatarColorEnum.red;
|
||||
case UserAvatarColor.yellow:
|
||||
return AvatarColorEnum.yellow;
|
||||
case UserAvatarColor.blue:
|
||||
return AvatarColorEnum.blue;
|
||||
case UserAvatarColor.green:
|
||||
return AvatarColorEnum.green;
|
||||
case UserAvatarColor.purple:
|
||||
return AvatarColorEnum.purple;
|
||||
case UserAvatarColor.orange:
|
||||
return AvatarColorEnum.orange;
|
||||
case UserAvatarColor.gray:
|
||||
return AvatarColorEnum.gray;
|
||||
case UserAvatarColor.amber:
|
||||
return AvatarColorEnum.amber;
|
||||
}
|
||||
return AvatarColorEnum.primary;
|
||||
}
|
||||
}
|
||||
|
||||
extension AvatarColorToColorHelper on AvatarColorEnum {
|
||||
Color toColor([bool isDarkTheme = false]) {
|
||||
switch (this) {
|
||||
case AvatarColorEnum.primary:
|
||||
return isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF);
|
||||
case AvatarColorEnum.pink:
|
||||
return const Color.fromARGB(255, 244, 114, 182);
|
||||
case AvatarColorEnum.red:
|
||||
return const Color.fromARGB(255, 239, 68, 68);
|
||||
case AvatarColorEnum.yellow:
|
||||
return const Color.fromARGB(255, 234, 179, 8);
|
||||
case AvatarColorEnum.blue:
|
||||
return const Color.fromARGB(255, 59, 130, 246);
|
||||
case AvatarColorEnum.green:
|
||||
return const Color.fromARGB(255, 22, 163, 74);
|
||||
case AvatarColorEnum.purple:
|
||||
return const Color.fromARGB(255, 147, 51, 234);
|
||||
case AvatarColorEnum.orange:
|
||||
return const Color.fromARGB(255, 234, 88, 12);
|
||||
case AvatarColorEnum.gray:
|
||||
return const Color.fromARGB(255, 75, 85, 99);
|
||||
case AvatarColorEnum.amber:
|
||||
return const Color.fromARGB(255, 217, 119, 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,53 +17,59 @@ const UserSchema = CollectionSchema(
|
|||
name: r'User',
|
||||
id: -7838171048429979076,
|
||||
properties: {
|
||||
r'email': PropertySchema(
|
||||
r'avatarColor': PropertySchema(
|
||||
id: 0,
|
||||
name: r'avatarColor',
|
||||
type: IsarType.byte,
|
||||
enumMap: _UseravatarColorEnumValueMap,
|
||||
),
|
||||
r'email': PropertySchema(
|
||||
id: 1,
|
||||
name: r'email',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'id': PropertySchema(
|
||||
id: 1,
|
||||
id: 2,
|
||||
name: r'id',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'inTimeline': PropertySchema(
|
||||
id: 2,
|
||||
id: 3,
|
||||
name: r'inTimeline',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isAdmin': PropertySchema(
|
||||
id: 3,
|
||||
id: 4,
|
||||
name: r'isAdmin',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isPartnerSharedBy': PropertySchema(
|
||||
id: 4,
|
||||
id: 5,
|
||||
name: r'isPartnerSharedBy',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'isPartnerSharedWith': PropertySchema(
|
||||
id: 5,
|
||||
id: 6,
|
||||
name: r'isPartnerSharedWith',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'memoryEnabled': PropertySchema(
|
||||
id: 6,
|
||||
id: 7,
|
||||
name: r'memoryEnabled',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'name': PropertySchema(
|
||||
id: 7,
|
||||
id: 8,
|
||||
name: r'name',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'profileImagePath': PropertySchema(
|
||||
id: 8,
|
||||
id: 9,
|
||||
name: r'profileImagePath',
|
||||
type: IsarType.string,
|
||||
),
|
||||
r'updatedAt': PropertySchema(
|
||||
id: 9,
|
||||
id: 10,
|
||||
name: r'updatedAt',
|
||||
type: IsarType.dateTime,
|
||||
)
|
||||
|
@ -130,16 +136,17 @@ void _userSerialize(
|
|||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeString(offsets[0], object.email);
|
||||
writer.writeString(offsets[1], object.id);
|
||||
writer.writeBool(offsets[2], object.inTimeline);
|
||||
writer.writeBool(offsets[3], object.isAdmin);
|
||||
writer.writeBool(offsets[4], object.isPartnerSharedBy);
|
||||
writer.writeBool(offsets[5], object.isPartnerSharedWith);
|
||||
writer.writeBool(offsets[6], object.memoryEnabled);
|
||||
writer.writeString(offsets[7], object.name);
|
||||
writer.writeString(offsets[8], object.profileImagePath);
|
||||
writer.writeDateTime(offsets[9], object.updatedAt);
|
||||
writer.writeByte(offsets[0], object.avatarColor.index);
|
||||
writer.writeString(offsets[1], object.email);
|
||||
writer.writeString(offsets[2], object.id);
|
||||
writer.writeBool(offsets[3], object.inTimeline);
|
||||
writer.writeBool(offsets[4], object.isAdmin);
|
||||
writer.writeBool(offsets[5], object.isPartnerSharedBy);
|
||||
writer.writeBool(offsets[6], object.isPartnerSharedWith);
|
||||
writer.writeBool(offsets[7], object.memoryEnabled);
|
||||
writer.writeString(offsets[8], object.name);
|
||||
writer.writeString(offsets[9], object.profileImagePath);
|
||||
writer.writeDateTime(offsets[10], object.updatedAt);
|
||||
}
|
||||
|
||||
User _userDeserialize(
|
||||
|
@ -149,16 +156,19 @@ User _userDeserialize(
|
|||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = User(
|
||||
email: reader.readString(offsets[0]),
|
||||
id: reader.readString(offsets[1]),
|
||||
inTimeline: reader.readBoolOrNull(offsets[2]) ?? false,
|
||||
isAdmin: reader.readBool(offsets[3]),
|
||||
isPartnerSharedBy: reader.readBoolOrNull(offsets[4]) ?? false,
|
||||
isPartnerSharedWith: reader.readBoolOrNull(offsets[5]) ?? false,
|
||||
memoryEnabled: reader.readBoolOrNull(offsets[6]) ?? true,
|
||||
name: reader.readString(offsets[7]),
|
||||
profileImagePath: reader.readStringOrNull(offsets[8]) ?? '',
|
||||
updatedAt: reader.readDateTime(offsets[9]),
|
||||
avatarColor:
|
||||
_UseravatarColorValueEnumMap[reader.readByteOrNull(offsets[0])] ??
|
||||
AvatarColorEnum.primary,
|
||||
email: reader.readString(offsets[1]),
|
||||
id: reader.readString(offsets[2]),
|
||||
inTimeline: reader.readBoolOrNull(offsets[3]) ?? false,
|
||||
isAdmin: reader.readBool(offsets[4]),
|
||||
isPartnerSharedBy: reader.readBoolOrNull(offsets[5]) ?? false,
|
||||
isPartnerSharedWith: reader.readBoolOrNull(offsets[6]) ?? false,
|
||||
memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true,
|
||||
name: reader.readString(offsets[8]),
|
||||
profileImagePath: reader.readStringOrNull(offsets[9]) ?? '',
|
||||
updatedAt: reader.readDateTime(offsets[10]),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
|
@ -171,30 +181,58 @@ P _userDeserializeProp<P>(
|
|||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readString(offset)) as P;
|
||||
return (_UseravatarColorValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||
AvatarColorEnum.primary) as P;
|
||||
case 1:
|
||||
return (reader.readString(offset)) as P;
|
||||
case 2:
|
||||
return (reader.readBoolOrNull(offset) ?? false) as P;
|
||||
return (reader.readString(offset)) as P;
|
||||
case 3:
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 4:
|
||||
return (reader.readBoolOrNull(offset) ?? false) as P;
|
||||
case 4:
|
||||
return (reader.readBool(offset)) as P;
|
||||
case 5:
|
||||
return (reader.readBoolOrNull(offset) ?? false) as P;
|
||||
case 6:
|
||||
return (reader.readBoolOrNull(offset) ?? true) as P;
|
||||
return (reader.readBoolOrNull(offset) ?? false) as P;
|
||||
case 7:
|
||||
return (reader.readString(offset)) as P;
|
||||
return (reader.readBoolOrNull(offset) ?? true) as P;
|
||||
case 8:
|
||||
return (reader.readStringOrNull(offset) ?? '') as P;
|
||||
return (reader.readString(offset)) as P;
|
||||
case 9:
|
||||
return (reader.readStringOrNull(offset) ?? '') as P;
|
||||
case 10:
|
||||
return (reader.readDateTime(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
const _UseravatarColorEnumValueMap = {
|
||||
'primary': 0,
|
||||
'pink': 1,
|
||||
'red': 2,
|
||||
'yellow': 3,
|
||||
'blue': 4,
|
||||
'green': 5,
|
||||
'purple': 6,
|
||||
'orange': 7,
|
||||
'gray': 8,
|
||||
'amber': 9,
|
||||
};
|
||||
const _UseravatarColorValueEnumMap = {
|
||||
0: AvatarColorEnum.primary,
|
||||
1: AvatarColorEnum.pink,
|
||||
2: AvatarColorEnum.red,
|
||||
3: AvatarColorEnum.yellow,
|
||||
4: AvatarColorEnum.blue,
|
||||
5: AvatarColorEnum.green,
|
||||
6: AvatarColorEnum.purple,
|
||||
7: AvatarColorEnum.orange,
|
||||
8: AvatarColorEnum.gray,
|
||||
9: AvatarColorEnum.amber,
|
||||
};
|
||||
|
||||
Id _userGetId(User object) {
|
||||
return object.isarId;
|
||||
}
|
||||
|
@ -382,6 +420,59 @@ extension UserQueryWhere on QueryBuilder<User, User, QWhereClause> {
|
|||
}
|
||||
|
||||
extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
|
||||
QueryBuilder<User, User, QAfterFilterCondition> avatarColorEqualTo(
|
||||
AvatarColorEnum value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'avatarColor',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> avatarColorGreaterThan(
|
||||
AvatarColorEnum value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'avatarColor',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> avatarColorLessThan(
|
||||
AvatarColorEnum value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'avatarColor',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> avatarColorBetween(
|
||||
AvatarColorEnum lower,
|
||||
AvatarColorEnum upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'avatarColor',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterFilterCondition> emailEqualTo(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
|
@ -1167,6 +1258,18 @@ extension UserQueryLinks on QueryBuilder<User, User, QFilterCondition> {
|
|||
}
|
||||
|
||||
extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> {
|
||||
QueryBuilder<User, User, QAfterSortBy> sortByAvatarColor() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'avatarColor', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterSortBy> sortByAvatarColorDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'avatarColor', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterSortBy> sortByEmail() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'email', Sort.asc);
|
||||
|
@ -1289,6 +1392,18 @@ extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> {
|
|||
}
|
||||
|
||||
extension UserQuerySortThenBy on QueryBuilder<User, User, QSortThenBy> {
|
||||
QueryBuilder<User, User, QAfterSortBy> thenByAvatarColor() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'avatarColor', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterSortBy> thenByAvatarColorDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'avatarColor', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QAfterSortBy> thenByEmail() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'email', Sort.asc);
|
||||
|
@ -1423,6 +1538,12 @@ extension UserQuerySortThenBy on QueryBuilder<User, User, QSortThenBy> {
|
|||
}
|
||||
|
||||
extension UserQueryWhereDistinct on QueryBuilder<User, User, QDistinct> {
|
||||
QueryBuilder<User, User, QDistinct> distinctByAvatarColor() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'avatarColor');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, User, QDistinct> distinctByEmail(
|
||||
{bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
|
@ -1496,6 +1617,12 @@ extension UserQueryProperty on QueryBuilder<User, User, QQueryProperty> {
|
|||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, AvatarColorEnum, QQueryOperations> avatarColorProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'avatarColor');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<User, String, QQueryOperations> emailProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'email');
|
||||
|
|
|
@ -22,14 +22,12 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
|||
final user = Store.tryGet(StoreKey.currentUser);
|
||||
|
||||
buildUserProfileImage() {
|
||||
const immichImage = CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
|
||||
if (authState.profileImagePath.isEmpty || user == null) {
|
||||
return immichImage;
|
||||
if (user == null) {
|
||||
return const CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||
backgroundColor: Colors.transparent,
|
||||
);
|
||||
}
|
||||
|
||||
final userImage = UserCircleAvatar(
|
||||
|
@ -38,18 +36,6 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
|||
user: user,
|
||||
);
|
||||
|
||||
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
|
||||
return authState.profileImagePath.isNotEmpty ? userImage : immichImage;
|
||||
}
|
||||
|
||||
if (uploadProfileImageStatus == UploadProfileStatus.success) {
|
||||
return userImage;
|
||||
}
|
||||
|
||||
if (uploadProfileImageStatus == UploadProfileStatus.failure) {
|
||||
return immichImage;
|
||||
}
|
||||
|
||||
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
|
||||
return const SizedBox(
|
||||
height: 40,
|
||||
|
@ -58,7 +44,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
return immichImage;
|
||||
return userImage;
|
||||
}
|
||||
|
||||
pickUserProfileImage() async {
|
||||
|
|
|
@ -4,8 +4,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_dialog.dart';
|
||||
import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
|
||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||
|
@ -26,7 +24,6 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||
final bool isEnableAutoBackup =
|
||||
backupState.backgroundBackup || backupState.autoBackup;
|
||||
final ServerInfo serverInfoState = ref.watch(serverInfoProvider);
|
||||
AuthenticationState authState = ref.watch(authenticationProvider);
|
||||
final user = Store.tryGet(StoreKey.currentUser);
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
const widgetSize = 30.0;
|
||||
|
@ -55,7 +52,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||
alignment: Alignment.bottomRight,
|
||||
isLabelVisible: serverInfoState.isVersionMismatch,
|
||||
offset: const Offset(2, 2),
|
||||
child: authState.profileImagePath.isEmpty || user == null
|
||||
child: user == null
|
||||
? const Icon(
|
||||
Icons.face_outlined,
|
||||
size: widgetSize,
|
||||
|
|
|
@ -3,7 +3,6 @@ import 'dart:math';
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/ui/transparent_image.dart';
|
||||
|
@ -13,32 +12,17 @@ class UserCircleAvatar extends ConsumerWidget {
|
|||
final User user;
|
||||
double radius;
|
||||
double size;
|
||||
bool useRandomBackgroundColor;
|
||||
|
||||
UserCircleAvatar({
|
||||
super.key,
|
||||
this.radius = 22,
|
||||
this.size = 44,
|
||||
this.useRandomBackgroundColor = false,
|
||||
required this.user,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final randomColors = [
|
||||
Colors.red[200],
|
||||
Colors.blue[200],
|
||||
Colors.green[200],
|
||||
Colors.yellow[200],
|
||||
Colors.purple[200],
|
||||
Colors.orange[200],
|
||||
Colors.pink[200],
|
||||
Colors.teal[200],
|
||||
Colors.indigo[200],
|
||||
Colors.cyan[200],
|
||||
Colors.brown[200],
|
||||
];
|
||||
|
||||
bool isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||
final profileImageUrl =
|
||||
'${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${user.id}?d=${Random().nextInt(1024)}';
|
||||
|
||||
|
@ -46,15 +30,16 @@ class UserCircleAvatar extends ConsumerWidget {
|
|||
user.name[0].toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.isDarkTheme ? Colors.black : Colors.white,
|
||||
fontSize: 12,
|
||||
color: isDarkTheme && user.avatarColor == AvatarColorEnum.primary
|
||||
? Colors.black
|
||||
: Colors.white,
|
||||
),
|
||||
);
|
||||
return CircleAvatar(
|
||||
backgroundColor: useRandomBackgroundColor
|
||||
? randomColors[Random().nextInt(randomColors.length)]
|
||||
: context.primaryColor,
|
||||
backgroundColor: user.avatarColor.toColor(),
|
||||
radius: radius,
|
||||
child: user.profileImagePath == ""
|
||||
child: user.profileImagePath.isEmpty
|
||||
? textIcon
|
||||
: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
|
|
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
|
@ -166,6 +166,7 @@ doc/UpdateTagDto.md
|
|||
doc/UpdateUserDto.md
|
||||
doc/UsageByUserDto.md
|
||||
doc/UserApi.md
|
||||
doc/UserAvatarColor.md
|
||||
doc/UserDto.md
|
||||
doc/UserResponseDto.md
|
||||
doc/ValidateAccessTokenResponseDto.md
|
||||
|
@ -343,6 +344,7 @@ lib/model/update_stack_parent_dto.dart
|
|||
lib/model/update_tag_dto.dart
|
||||
lib/model/update_user_dto.dart
|
||||
lib/model/usage_by_user_dto.dart
|
||||
lib/model/user_avatar_color.dart
|
||||
lib/model/user_dto.dart
|
||||
lib/model/user_response_dto.dart
|
||||
lib/model/validate_access_token_response_dto.dart
|
||||
|
@ -511,6 +513,7 @@ test/update_tag_dto_test.dart
|
|||
test/update_user_dto_test.dart
|
||||
test/usage_by_user_dto_test.dart
|
||||
test/user_api_test.dart
|
||||
test/user_avatar_color_test.dart
|
||||
test/user_dto_test.dart
|
||||
test/user_response_dto_test.dart
|
||||
test/validate_access_token_response_dto_test.dart
|
||||
|
|
4
mobile/openapi/README.md
generated
4
mobile/openapi/README.md
generated
|
@ -3,7 +3,7 @@ Immich API
|
|||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.85.0
|
||||
- API version: 1.86.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
|
@ -195,6 +195,7 @@ Class | Method | HTTP request | Description
|
|||
*TagApi* | [**updateTag**](doc//TagApi.md#updatetag) | **PATCH** /tag/{id} |
|
||||
*UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image |
|
||||
*UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user |
|
||||
*UserApi* | [**deleteProfileImage**](doc//UserApi.md#deleteprofileimage) | **DELETE** /user/profile-image |
|
||||
*UserApi* | [**deleteUser**](doc//UserApi.md#deleteuser) | **DELETE** /user/{id} |
|
||||
*UserApi* | [**getAllUsers**](doc//UserApi.md#getallusers) | **GET** /user |
|
||||
*UserApi* | [**getMyUserInfo**](doc//UserApi.md#getmyuserinfo) | **GET** /user/me |
|
||||
|
@ -352,6 +353,7 @@ Class | Method | HTTP request | Description
|
|||
- [UpdateTagDto](doc//UpdateTagDto.md)
|
||||
- [UpdateUserDto](doc//UpdateUserDto.md)
|
||||
- [UsageByUserDto](doc//UsageByUserDto.md)
|
||||
- [UserAvatarColor](doc//UserAvatarColor.md)
|
||||
- [UserDto](doc//UserDto.md)
|
||||
- [UserResponseDto](doc//UserResponseDto.md)
|
||||
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)
|
||||
|
|
1
mobile/openapi/doc/PartnerResponseDto.md
generated
1
mobile/openapi/doc/PartnerResponseDto.md
generated
|
@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
|
|||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) | |
|
||||
**createdAt** | [**DateTime**](DateTime.md) | |
|
||||
**deletedAt** | [**DateTime**](DateTime.md) | |
|
||||
**email** | **String** | |
|
||||
|
|
1
mobile/openapi/doc/UpdateUserDto.md
generated
1
mobile/openapi/doc/UpdateUserDto.md
generated
|
@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
|
|||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) | | [optional]
|
||||
**email** | **String** | | [optional]
|
||||
**externalPath** | **String** | | [optional]
|
||||
**id** | **String** | |
|
||||
|
|
51
mobile/openapi/doc/UserApi.md
generated
51
mobile/openapi/doc/UserApi.md
generated
|
@ -11,6 +11,7 @@ Method | HTTP request | Description
|
|||
------------- | ------------- | -------------
|
||||
[**createProfileImage**](UserApi.md#createprofileimage) | **POST** /user/profile-image |
|
||||
[**createUser**](UserApi.md#createuser) | **POST** /user |
|
||||
[**deleteProfileImage**](UserApi.md#deleteprofileimage) | **DELETE** /user/profile-image |
|
||||
[**deleteUser**](UserApi.md#deleteuser) | **DELETE** /user/{id} |
|
||||
[**getAllUsers**](UserApi.md#getallusers) | **GET** /user |
|
||||
[**getMyUserInfo**](UserApi.md#getmyuserinfo) | **GET** /user/me |
|
||||
|
@ -130,6 +131,56 @@ Name | Type | Description | Notes
|
|||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **deleteProfileImage**
|
||||
> deleteProfileImage()
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = UserApi();
|
||||
|
||||
try {
|
||||
api_instance.deleteProfileImage();
|
||||
} catch (e) {
|
||||
print('Exception when calling UserApi->deleteProfileImage: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
void (empty response body)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: Not defined
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **deleteUser**
|
||||
> UserResponseDto deleteUser(id)
|
||||
|
||||
|
|
14
mobile/openapi/doc/UserAvatarColor.md
generated
Normal file
14
mobile/openapi/doc/UserAvatarColor.md
generated
Normal file
|
@ -0,0 +1,14 @@
|
|||
# openapi.model.UserAvatarColor
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
1
mobile/openapi/doc/UserDto.md
generated
1
mobile/openapi/doc/UserDto.md
generated
|
@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
|
|||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) | |
|
||||
**email** | **String** | |
|
||||
**id** | **String** | |
|
||||
**name** | **String** | |
|
||||
|
|
1
mobile/openapi/doc/UserResponseDto.md
generated
1
mobile/openapi/doc/UserResponseDto.md
generated
|
@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
|
|||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**avatarColor** | [**UserAvatarColor**](UserAvatarColor.md) | |
|
||||
**createdAt** | [**DateTime**](DateTime.md) | |
|
||||
**deletedAt** | [**DateTime**](DateTime.md) | |
|
||||
**email** | **String** | |
|
||||
|
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
|
@ -192,6 +192,7 @@ part 'model/update_stack_parent_dto.dart';
|
|||
part 'model/update_tag_dto.dart';
|
||||
part 'model/update_user_dto.dart';
|
||||
part 'model/usage_by_user_dto.dart';
|
||||
part 'model/user_avatar_color.dart';
|
||||
part 'model/user_dto.dart';
|
||||
part 'model/user_response_dto.dart';
|
||||
part 'model/validate_access_token_response_dto.dart';
|
||||
|
|
33
mobile/openapi/lib/api/user_api.dart
generated
33
mobile/openapi/lib/api/user_api.dart
generated
|
@ -120,6 +120,39 @@ class UserApi {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'DELETE /user/profile-image' operation and returns the [Response].
|
||||
Future<Response> deleteProfileImageWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/user/profile-image';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteProfileImage() async {
|
||||
final response = await deleteProfileImageWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'DELETE /user/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
|
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
|
@ -473,6 +473,8 @@ class ApiClient {
|
|||
return UpdateUserDto.fromJson(value);
|
||||
case 'UsageByUserDto':
|
||||
return UsageByUserDto.fromJson(value);
|
||||
case 'UserAvatarColor':
|
||||
return UserAvatarColorTypeTransformer().decode(value);
|
||||
case 'UserDto':
|
||||
return UserDto.fromJson(value);
|
||||
case 'UserResponseDto':
|
||||
|
|
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
|
@ -127,6 +127,9 @@ String parameterToString(dynamic value) {
|
|||
if (value is TranscodePolicy) {
|
||||
return TranscodePolicyTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is UserAvatarColor) {
|
||||
return UserAvatarColorTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is VideoCodec) {
|
||||
return VideoCodecTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
|
10
mobile/openapi/lib/model/partner_response_dto.dart
generated
10
mobile/openapi/lib/model/partner_response_dto.dart
generated
|
@ -13,6 +13,7 @@ part of openapi.api;
|
|||
class PartnerResponseDto {
|
||||
/// Returns a new [PartnerResponseDto] instance.
|
||||
PartnerResponseDto({
|
||||
required this.avatarColor,
|
||||
required this.createdAt,
|
||||
required this.deletedAt,
|
||||
required this.email,
|
||||
|
@ -29,6 +30,8 @@ class PartnerResponseDto {
|
|||
required this.updatedAt,
|
||||
});
|
||||
|
||||
UserAvatarColor avatarColor;
|
||||
|
||||
DateTime createdAt;
|
||||
|
||||
DateTime? deletedAt;
|
||||
|
@ -71,6 +74,7 @@ class PartnerResponseDto {
|
|||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PartnerResponseDto &&
|
||||
other.avatarColor == avatarColor &&
|
||||
other.createdAt == createdAt &&
|
||||
other.deletedAt == deletedAt &&
|
||||
other.email == email &&
|
||||
|
@ -89,6 +93,7 @@ class PartnerResponseDto {
|
|||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(avatarColor.hashCode) +
|
||||
(createdAt.hashCode) +
|
||||
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
||||
(email.hashCode) +
|
||||
|
@ -105,10 +110,11 @@ class PartnerResponseDto {
|
|||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PartnerResponseDto[createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, inTimeline=$inTimeline, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'avatarColor'] = this.avatarColor;
|
||||
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||
if (this.deletedAt != null) {
|
||||
json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String();
|
||||
|
@ -154,6 +160,7 @@ class PartnerResponseDto {
|
|||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PartnerResponseDto(
|
||||
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!,
|
||||
createdAt: mapDateTime(json, r'createdAt', '')!,
|
||||
deletedAt: mapDateTime(json, r'deletedAt', ''),
|
||||
email: mapValueOfType<String>(json, r'email')!,
|
||||
|
@ -215,6 +222,7 @@ class PartnerResponseDto {
|
|||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'avatarColor',
|
||||
'createdAt',
|
||||
'deletedAt',
|
||||
'email',
|
||||
|
|
19
mobile/openapi/lib/model/update_user_dto.dart
generated
19
mobile/openapi/lib/model/update_user_dto.dart
generated
|
@ -13,6 +13,7 @@ part of openapi.api;
|
|||
class UpdateUserDto {
|
||||
/// Returns a new [UpdateUserDto] instance.
|
||||
UpdateUserDto({
|
||||
this.avatarColor,
|
||||
this.email,
|
||||
this.externalPath,
|
||||
required this.id,
|
||||
|
@ -24,6 +25,14 @@ class UpdateUserDto {
|
|||
this.storageLabel,
|
||||
});
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
UserAvatarColor? avatarColor;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
|
@ -92,6 +101,7 @@ class UpdateUserDto {
|
|||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UpdateUserDto &&
|
||||
other.avatarColor == avatarColor &&
|
||||
other.email == email &&
|
||||
other.externalPath == externalPath &&
|
||||
other.id == id &&
|
||||
|
@ -105,6 +115,7 @@ class UpdateUserDto {
|
|||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(avatarColor == null ? 0 : avatarColor!.hashCode) +
|
||||
(email == null ? 0 : email!.hashCode) +
|
||||
(externalPath == null ? 0 : externalPath!.hashCode) +
|
||||
(id.hashCode) +
|
||||
|
@ -116,10 +127,15 @@ class UpdateUserDto {
|
|||
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UpdateUserDto[email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
||||
String toString() => 'UpdateUserDto[avatarColor=$avatarColor, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.avatarColor != null) {
|
||||
json[r'avatarColor'] = this.avatarColor;
|
||||
} else {
|
||||
// json[r'avatarColor'] = null;
|
||||
}
|
||||
if (this.email != null) {
|
||||
json[r'email'] = this.email;
|
||||
} else {
|
||||
|
@ -172,6 +188,7 @@ class UpdateUserDto {
|
|||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return UpdateUserDto(
|
||||
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
|
||||
email: mapValueOfType<String>(json, r'email'),
|
||||
externalPath: mapValueOfType<String>(json, r'externalPath'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
|
|
109
mobile/openapi/lib/model/user_avatar_color.dart
generated
Normal file
109
mobile/openapi/lib/model/user_avatar_color.dart
generated
Normal file
|
@ -0,0 +1,109 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class UserAvatarColor {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const UserAvatarColor._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const primary = UserAvatarColor._(r'primary');
|
||||
static const pink = UserAvatarColor._(r'pink');
|
||||
static const red = UserAvatarColor._(r'red');
|
||||
static const yellow = UserAvatarColor._(r'yellow');
|
||||
static const blue = UserAvatarColor._(r'blue');
|
||||
static const green = UserAvatarColor._(r'green');
|
||||
static const purple = UserAvatarColor._(r'purple');
|
||||
static const orange = UserAvatarColor._(r'orange');
|
||||
static const gray = UserAvatarColor._(r'gray');
|
||||
static const amber = UserAvatarColor._(r'amber');
|
||||
|
||||
/// List of all possible values in this [enum][UserAvatarColor].
|
||||
static const values = <UserAvatarColor>[
|
||||
primary,
|
||||
pink,
|
||||
red,
|
||||
yellow,
|
||||
blue,
|
||||
green,
|
||||
purple,
|
||||
orange,
|
||||
gray,
|
||||
amber,
|
||||
];
|
||||
|
||||
static UserAvatarColor? fromJson(dynamic value) => UserAvatarColorTypeTransformer().decode(value);
|
||||
|
||||
static List<UserAvatarColor>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UserAvatarColor>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UserAvatarColor.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [UserAvatarColor] to String,
|
||||
/// and [decode] dynamic data back to [UserAvatarColor].
|
||||
class UserAvatarColorTypeTransformer {
|
||||
factory UserAvatarColorTypeTransformer() => _instance ??= const UserAvatarColorTypeTransformer._();
|
||||
|
||||
const UserAvatarColorTypeTransformer._();
|
||||
|
||||
String encode(UserAvatarColor data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a UserAvatarColor.
|
||||
///
|
||||
/// 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.
|
||||
UserAvatarColor? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'primary': return UserAvatarColor.primary;
|
||||
case r'pink': return UserAvatarColor.pink;
|
||||
case r'red': return UserAvatarColor.red;
|
||||
case r'yellow': return UserAvatarColor.yellow;
|
||||
case r'blue': return UserAvatarColor.blue;
|
||||
case r'green': return UserAvatarColor.green;
|
||||
case r'purple': return UserAvatarColor.purple;
|
||||
case r'orange': return UserAvatarColor.orange;
|
||||
case r'gray': return UserAvatarColor.gray;
|
||||
case r'amber': return UserAvatarColor.amber;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [UserAvatarColorTypeTransformer] instance.
|
||||
static UserAvatarColorTypeTransformer? _instance;
|
||||
}
|
||||
|
10
mobile/openapi/lib/model/user_dto.dart
generated
10
mobile/openapi/lib/model/user_dto.dart
generated
|
@ -13,12 +13,15 @@ part of openapi.api;
|
|||
class UserDto {
|
||||
/// Returns a new [UserDto] instance.
|
||||
UserDto({
|
||||
required this.avatarColor,
|
||||
required this.email,
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.profileImagePath,
|
||||
});
|
||||
|
||||
UserAvatarColor avatarColor;
|
||||
|
||||
String email;
|
||||
|
||||
String id;
|
||||
|
@ -29,6 +32,7 @@ class UserDto {
|
|||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UserDto &&
|
||||
other.avatarColor == avatarColor &&
|
||||
other.email == email &&
|
||||
other.id == id &&
|
||||
other.name == name &&
|
||||
|
@ -37,16 +41,18 @@ class UserDto {
|
|||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(avatarColor.hashCode) +
|
||||
(email.hashCode) +
|
||||
(id.hashCode) +
|
||||
(name.hashCode) +
|
||||
(profileImagePath.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UserDto[email=$email, id=$id, name=$name, profileImagePath=$profileImagePath]';
|
||||
String toString() => 'UserDto[avatarColor=$avatarColor, email=$email, id=$id, name=$name, profileImagePath=$profileImagePath]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'avatarColor'] = this.avatarColor;
|
||||
json[r'email'] = this.email;
|
||||
json[r'id'] = this.id;
|
||||
json[r'name'] = this.name;
|
||||
|
@ -62,6 +68,7 @@ class UserDto {
|
|||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return UserDto(
|
||||
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!,
|
||||
email: mapValueOfType<String>(json, r'email')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
|
@ -113,6 +120,7 @@ class UserDto {
|
|||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'avatarColor',
|
||||
'email',
|
||||
'id',
|
||||
'name',
|
||||
|
|
10
mobile/openapi/lib/model/user_response_dto.dart
generated
10
mobile/openapi/lib/model/user_response_dto.dart
generated
|
@ -13,6 +13,7 @@ part of openapi.api;
|
|||
class UserResponseDto {
|
||||
/// Returns a new [UserResponseDto] instance.
|
||||
UserResponseDto({
|
||||
required this.avatarColor,
|
||||
required this.createdAt,
|
||||
required this.deletedAt,
|
||||
required this.email,
|
||||
|
@ -28,6 +29,8 @@ class UserResponseDto {
|
|||
required this.updatedAt,
|
||||
});
|
||||
|
||||
UserAvatarColor avatarColor;
|
||||
|
||||
DateTime createdAt;
|
||||
|
||||
DateTime? deletedAt;
|
||||
|
@ -62,6 +65,7 @@ class UserResponseDto {
|
|||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UserResponseDto &&
|
||||
other.avatarColor == avatarColor &&
|
||||
other.createdAt == createdAt &&
|
||||
other.deletedAt == deletedAt &&
|
||||
other.email == email &&
|
||||
|
@ -79,6 +83,7 @@ class UserResponseDto {
|
|||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(avatarColor.hashCode) +
|
||||
(createdAt.hashCode) +
|
||||
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
||||
(email.hashCode) +
|
||||
|
@ -94,10 +99,11 @@ class UserResponseDto {
|
|||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UserResponseDto[createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
String toString() => 'UserResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, externalPath=$externalPath, id=$id, isAdmin=$isAdmin, memoriesEnabled=$memoriesEnabled, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'avatarColor'] = this.avatarColor;
|
||||
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||
if (this.deletedAt != null) {
|
||||
json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String();
|
||||
|
@ -138,6 +144,7 @@ class UserResponseDto {
|
|||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return UserResponseDto(
|
||||
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!,
|
||||
createdAt: mapDateTime(json, r'createdAt', '')!,
|
||||
deletedAt: mapDateTime(json, r'deletedAt', ''),
|
||||
email: mapValueOfType<String>(json, r'email')!,
|
||||
|
@ -198,6 +205,7 @@ class UserResponseDto {
|
|||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'avatarColor',
|
||||
'createdAt',
|
||||
'deletedAt',
|
||||
'email',
|
||||
|
|
|
@ -16,6 +16,11 @@ void main() {
|
|||
// final instance = PartnerResponseDto();
|
||||
|
||||
group('test PartnerResponseDto', () {
|
||||
// UserAvatarColor avatarColor
|
||||
test('to test the property `avatarColor`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// DateTime createdAt
|
||||
test('to test the property `createdAt`', () async {
|
||||
// TODO
|
||||
|
|
5
mobile/openapi/test/update_user_dto_test.dart
generated
5
mobile/openapi/test/update_user_dto_test.dart
generated
|
@ -16,6 +16,11 @@ void main() {
|
|||
// final instance = UpdateUserDto();
|
||||
|
||||
group('test UpdateUserDto', () {
|
||||
// UserAvatarColor avatarColor
|
||||
test('to test the property `avatarColor`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String email
|
||||
test('to test the property `email`', () async {
|
||||
// TODO
|
||||
|
|
5
mobile/openapi/test/user_api_test.dart
generated
5
mobile/openapi/test/user_api_test.dart
generated
|
@ -27,6 +27,11 @@ void main() {
|
|||
// TODO
|
||||
});
|
||||
|
||||
//Future deleteProfileImage() async
|
||||
test('test deleteProfileImage', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<UserResponseDto> deleteUser(String id) async
|
||||
test('test deleteUser', () async {
|
||||
// TODO
|
||||
|
|
21
mobile/openapi/test/user_avatar_color_test.dart
generated
Normal file
21
mobile/openapi/test/user_avatar_color_test.dart
generated
Normal file
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for UserAvatarColor
|
||||
void main() {
|
||||
|
||||
group('test UserAvatarColor', () {
|
||||
|
||||
});
|
||||
|
||||
}
|
5
mobile/openapi/test/user_dto_test.dart
generated
5
mobile/openapi/test/user_dto_test.dart
generated
|
@ -16,6 +16,11 @@ void main() {
|
|||
// final instance = UserDto();
|
||||
|
||||
group('test UserDto', () {
|
||||
// UserAvatarColor avatarColor
|
||||
test('to test the property `avatarColor`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String email
|
||||
test('to test the property `email`', () async {
|
||||
// TODO
|
||||
|
|
5
mobile/openapi/test/user_response_dto_test.dart
generated
5
mobile/openapi/test/user_response_dto_test.dart
generated
|
@ -16,6 +16,11 @@ void main() {
|
|||
// final instance = UserResponseDto();
|
||||
|
||||
group('test UserResponseDto', () {
|
||||
// UserAvatarColor avatarColor
|
||||
test('to test the property `avatarColor`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// DateTime createdAt
|
||||
test('to test the property `createdAt`', () async {
|
||||
// TODO
|
||||
|
|
|
@ -2,7 +2,7 @@ name: immich_mobile
|
|||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.85.0+109
|
||||
version: 1.86.0+110
|
||||
isar_version: &isar_version 3.1.0+1
|
||||
|
||||
environment:
|
||||
|
|
|
@ -5578,6 +5578,29 @@
|
|||
}
|
||||
},
|
||||
"/user/profile-image": {
|
||||
"delete": {
|
||||
"operationId": "deleteProfileImage",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"User"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "createProfileImage",
|
||||
"parameters": [],
|
||||
|
@ -5750,7 +5773,7 @@
|
|||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.85.0",
|
||||
"version": "1.86.0",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
|
@ -7632,6 +7655,9 @@
|
|||
},
|
||||
"PartnerResponseDto": {
|
||||
"properties": {
|
||||
"avatarColor": {
|
||||
"$ref": "#/components/schemas/UserAvatarColor"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
|
@ -7682,6 +7708,7 @@
|
|||
}
|
||||
},
|
||||
"required": [
|
||||
"avatarColor",
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
|
@ -9140,6 +9167,9 @@
|
|||
},
|
||||
"UpdateUserDto": {
|
||||
"properties": {
|
||||
"avatarColor": {
|
||||
"$ref": "#/components/schemas/UserAvatarColor"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -9202,8 +9232,26 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserAvatarColor": {
|
||||
"enum": [
|
||||
"primary",
|
||||
"pink",
|
||||
"red",
|
||||
"yellow",
|
||||
"blue",
|
||||
"green",
|
||||
"purple",
|
||||
"orange",
|
||||
"gray",
|
||||
"amber"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"UserDto": {
|
||||
"properties": {
|
||||
"avatarColor": {
|
||||
"$ref": "#/components/schemas/UserAvatarColor"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -9218,6 +9266,7 @@
|
|||
}
|
||||
},
|
||||
"required": [
|
||||
"avatarColor",
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
|
@ -9227,6 +9276,9 @@
|
|||
},
|
||||
"UserResponseDto": {
|
||||
"properties": {
|
||||
"avatarColor": {
|
||||
"$ref": "#/components/schemas/UserAvatarColor"
|
||||
},
|
||||
"createdAt": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
|
@ -9274,6 +9326,7 @@
|
|||
}
|
||||
},
|
||||
"required": [
|
||||
"avatarColor",
|
||||
"id",
|
||||
"name",
|
||||
"email",
|
||||
|
|
4
server/package-lock.json
generated
4
server/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "immich",
|
||||
"version": "1.85.0",
|
||||
"version": "1.86.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich",
|
||||
"version": "1.85.0",
|
||||
"version": "1.86.0",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.22.11",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "immich",
|
||||
"version": "1.85.0",
|
||||
"version": "1.86.0",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
|
|
@ -248,6 +248,7 @@ describe('AuthService', () => {
|
|||
userMock.getAdmin.mockResolvedValue(null);
|
||||
userMock.create.mockResolvedValue({ ...dto, id: 'admin', createdAt: new Date('2021-01-01') } as UserEntity);
|
||||
await expect(sut.adminSignUp(dto)).resolves.toEqual({
|
||||
avatarColor: expect.any(String),
|
||||
id: 'admin',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
email: 'test@immich.com',
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { UserAvatarColor } from '@app/infra/entities';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { authStub, newPartnerRepositoryMock, partnerStub } from '@test';
|
||||
import { IAccessRepository, IPartnerRepository, PartnerDirection } from '../repositories';
|
||||
|
@ -19,6 +20,7 @@ const responseDto = {
|
|||
updatedAt: new Date('2021-01-01'),
|
||||
externalPath: null,
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.PRIMARY,
|
||||
inTimeline: true,
|
||||
},
|
||||
user1: <PartnerResponseDto>{
|
||||
|
@ -35,6 +37,7 @@ const responseDto = {
|
|||
updatedAt: new Date('2021-01-01'),
|
||||
externalPath: null,
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.PRIMARY,
|
||||
inTimeline: true,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { UserAvatarColor } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsEmail, IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||
import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||
import { Optional, toEmail, toSanitized } from '../../domain.util';
|
||||
|
||||
export class UpdateUserDto {
|
||||
|
@ -44,4 +45,9 @@ export class UpdateUserDto {
|
|||
@Optional()
|
||||
@IsBoolean()
|
||||
memoriesEnabled?: boolean;
|
||||
|
||||
@Optional()
|
||||
@IsEnum(UserAvatarColor)
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
avatarColor?: UserAvatarColor;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,26 @@
|
|||
import { UserEntity } from '@app/infra/entities';
|
||||
import { UserAvatarColor, UserEntity } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum } from 'class-validator';
|
||||
|
||||
export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => {
|
||||
const values = Object.values(UserAvatarColor);
|
||||
const randomIndex = Math.floor(
|
||||
user.email
|
||||
.split('')
|
||||
.map((letter) => letter.charCodeAt(0))
|
||||
.reduce((a, b) => a + b, 0) % values.length,
|
||||
);
|
||||
return values[randomIndex] as UserAvatarColor;
|
||||
};
|
||||
|
||||
export class UserDto {
|
||||
id!: string;
|
||||
name!: string;
|
||||
email!: string;
|
||||
profileImagePath!: string;
|
||||
@IsEnum(UserAvatarColor)
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
avatarColor!: UserAvatarColor;
|
||||
}
|
||||
|
||||
export class UserResponseDto extends UserDto {
|
||||
|
@ -25,6 +41,7 @@ export const mapSimpleUser = (entity: UserEntity): UserDto => {
|
|||
email: entity.email,
|
||||
name: entity.name,
|
||||
profileImagePath: entity.profileImagePath,
|
||||
avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -98,7 +98,6 @@ export class UserCore {
|
|||
if (payload.storageLabel) {
|
||||
payload.storageLabel = sanitize(payload.storageLabel);
|
||||
}
|
||||
|
||||
const userEntity = await this.userRepository.create(payload);
|
||||
await this.libraryRepository.create({
|
||||
owner: { id: userEntity.id } as UserEntity,
|
||||
|
|
|
@ -323,17 +323,52 @@ describe(UserService.name, () => {
|
|||
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
|
||||
|
||||
await sut.createProfileImage(userStub.admin, file);
|
||||
|
||||
expect(userMock.update).toHaveBeenCalledWith(userStub.admin.id, { profileImagePath: file.path });
|
||||
await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw an error if the user profile could not be updated with the new image', async () => {
|
||||
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||
userMock.get.mockResolvedValue(userStub.profilePath);
|
||||
userMock.update.mockRejectedValue(new InternalServerErrorException('mocked error'));
|
||||
|
||||
await expect(sut.createProfileImage(userStub.admin, file)).rejects.toThrowError(InternalServerErrorException);
|
||||
});
|
||||
|
||||
it('should delete the previous profile image', async () => {
|
||||
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||
userMock.get.mockResolvedValue(userStub.profilePath);
|
||||
const files = [userStub.profilePath.profileImagePath];
|
||||
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
|
||||
|
||||
await sut.createProfileImage(userStub.admin, file);
|
||||
await expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]);
|
||||
});
|
||||
|
||||
it('should not delete the profile image if it has not been set', async () => {
|
||||
const file = { path: '/profile/path' } as Express.Multer.File;
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
userMock.update.mockResolvedValue({ ...userStub.admin, profileImagePath: file.path });
|
||||
|
||||
await sut.createProfileImage(userStub.admin, file);
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteProfileImage', () => {
|
||||
it('should send an http error has no profile image', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.admin);
|
||||
|
||||
await expect(sut.deleteProfileImage(userStub.admin)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should delete the profile image if user has one', async () => {
|
||||
userMock.get.mockResolvedValue(userStub.profilePath);
|
||||
const files = [userStub.profilePath.profileImagePath];
|
||||
|
||||
await sut.deleteProfileImage(userStub.admin);
|
||||
await expect(jobMock.queue.mock.calls).toEqual([[{ name: JobName.DELETE_FILES, data: { files } }]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserProfileImage', () => {
|
||||
|
|
|
@ -93,10 +93,23 @@ export class UserService {
|
|||
authUser: AuthUserDto,
|
||||
fileInfo: Express.Multer.File,
|
||||
): Promise<CreateProfileImageResponseDto> {
|
||||
const { profileImagePath: oldpath } = await this.findOrFail(authUser.id, { withDeleted: false });
|
||||
const updatedUser = await this.userRepository.update(authUser.id, { profileImagePath: fileInfo.path });
|
||||
if (oldpath !== '') {
|
||||
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } });
|
||||
}
|
||||
return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath);
|
||||
}
|
||||
|
||||
async deleteProfileImage(authUser: AuthUserDto): Promise<void> {
|
||||
const user = await this.findOrFail(authUser.id, { withDeleted: false });
|
||||
if (user.profileImagePath === '') {
|
||||
throw new BadRequestException("Can't delete a missing profile Image");
|
||||
}
|
||||
await this.userRepository.update(authUser.id, { profileImagePath: '' });
|
||||
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } });
|
||||
}
|
||||
|
||||
async getProfileImage(id: string): Promise<ImmichReadStream> {
|
||||
const user = await this.findOrFail(id, {});
|
||||
if (!user.profileImagePath) {
|
||||
|
@ -111,7 +124,7 @@ export class UserService {
|
|||
throw new BadRequestException('Admin account does not exist');
|
||||
}
|
||||
|
||||
const providedPassword = await ask(admin);
|
||||
const providedPassword = await ask(mapUser(admin));
|
||||
const password = providedPassword || randomBytes(24).toString('base64').replace(/\W/g, '');
|
||||
|
||||
await this.userCore.updateUser(admin, admin.id, { password });
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
SignUpDto,
|
||||
UserResponseDto,
|
||||
ValidateAccessTokenResponseDto,
|
||||
mapUser,
|
||||
} from '@app/domain';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
@ -71,7 +72,7 @@ export class AuthController {
|
|||
@Post('change-password')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
changePassword(@AuthUser() authUser: AuthUserDto, @Body() dto: ChangePasswordDto): Promise<UserResponseDto> {
|
||||
return this.service.changePassword(authUser, dto);
|
||||
return this.service.changePassword(authUser, dto).then(mapUser);
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
Delete,
|
||||
Get,
|
||||
Header,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
|
@ -54,6 +56,12 @@ export class UserController {
|
|||
return this.service.create(createUserDto);
|
||||
}
|
||||
|
||||
@Delete('profile-image')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
deleteProfileImage(@AuthUser() authUser: AuthUserDto): Promise<void> {
|
||||
return this.service.deleteProfileImage(authUser);
|
||||
}
|
||||
|
||||
@AdminRoute()
|
||||
@Delete(':id')
|
||||
deleteUser(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
|
||||
|
|
|
@ -10,6 +10,19 @@ import {
|
|||
import { AssetEntity } from './asset.entity';
|
||||
import { TagEntity } from './tag.entity';
|
||||
|
||||
export enum UserAvatarColor {
|
||||
PRIMARY = 'primary',
|
||||
PINK = 'pink',
|
||||
RED = 'red',
|
||||
YELLOW = 'yellow',
|
||||
BLUE = 'blue',
|
||||
GREEN = 'green',
|
||||
PURPLE = 'purple',
|
||||
ORANGE = 'orange',
|
||||
GRAY = 'gray',
|
||||
AMBER = 'amber',
|
||||
}
|
||||
|
||||
@Entity('users')
|
||||
export class UserEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
|
@ -18,6 +31,9 @@ export class UserEntity {
|
|||
@Column({ default: '' })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
avatarColor!: UserAvatarColor | null;
|
||||
|
||||
@Column({ default: false })
|
||||
isAdmin!: boolean;
|
||||
|
||||
|
|
14
server/src/infra/migrations/1699889987493-AddAvatarColor.ts
Normal file
14
server/src/infra/migrations/1699889987493-AddAvatarColor.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddAvatarColor1699889987493 implements MigrationInterface {
|
||||
name = 'AddAvatarColor1699889987493'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "avatarColor" character varying`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "avatarColor"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -18,6 +18,7 @@ const password = 'Password123';
|
|||
const email = 'admin@immich.app';
|
||||
|
||||
const adminSignupResponse = {
|
||||
avatarColor: expect.any(String),
|
||||
id: expect.any(String),
|
||||
name: 'Immich Admin',
|
||||
email: 'admin@immich.app',
|
||||
|
|
9
server/test/fixtures/user.stub.ts
vendored
9
server/test/fixtures/user.stub.ts
vendored
|
@ -1,4 +1,4 @@
|
|||
import { UserEntity } from '@app/infra/entities';
|
||||
import { UserAvatarColor, UserEntity } from '@app/infra/entities';
|
||||
import { authStub } from './auth.stub';
|
||||
|
||||
export const userStub = {
|
||||
|
@ -17,6 +17,7 @@ export const userStub = {
|
|||
tags: [],
|
||||
assets: [],
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.PRIMARY,
|
||||
}),
|
||||
user1: Object.freeze<UserEntity>({
|
||||
...authStub.user1,
|
||||
|
@ -33,6 +34,7 @@ export const userStub = {
|
|||
tags: [],
|
||||
assets: [],
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.PRIMARY,
|
||||
}),
|
||||
user2: Object.freeze<UserEntity>({
|
||||
...authStub.user2,
|
||||
|
@ -49,6 +51,7 @@ export const userStub = {
|
|||
tags: [],
|
||||
assets: [],
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.PRIMARY,
|
||||
}),
|
||||
storageLabel: Object.freeze<UserEntity>({
|
||||
...authStub.user1,
|
||||
|
@ -65,6 +68,7 @@ export const userStub = {
|
|||
tags: [],
|
||||
assets: [],
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.PRIMARY,
|
||||
}),
|
||||
externalPath1: Object.freeze<UserEntity>({
|
||||
...authStub.user1,
|
||||
|
@ -81,6 +85,7 @@ export const userStub = {
|
|||
tags: [],
|
||||
assets: [],
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.PRIMARY,
|
||||
}),
|
||||
externalPath2: Object.freeze<UserEntity>({
|
||||
...authStub.user1,
|
||||
|
@ -97,6 +102,7 @@ export const userStub = {
|
|||
tags: [],
|
||||
assets: [],
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.PRIMARY,
|
||||
}),
|
||||
profilePath: Object.freeze<UserEntity>({
|
||||
...authStub.user1,
|
||||
|
@ -113,5 +119,6 @@ export const userStub = {
|
|||
tags: [],
|
||||
assets: [],
|
||||
memoriesEnabled: true,
|
||||
avatarColor: UserAvatarColor.PRIMARY,
|
||||
}),
|
||||
};
|
||||
|
|
121
web/src/api/open-api/api.ts
generated
121
web/src/api/open-api/api.ts
generated
|
@ -4,7 +4,7 @@
|
|||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.85.0
|
||||
* The version of the OpenAPI document: 1.86.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
@ -2355,6 +2355,12 @@ export interface OAuthConfigResponseDto {
|
|||
* @interface PartnerResponseDto
|
||||
*/
|
||||
export interface PartnerResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {UserAvatarColor}
|
||||
* @memberof PartnerResponseDto
|
||||
*/
|
||||
'avatarColor': UserAvatarColor;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
@ -2440,6 +2446,8 @@ export interface PartnerResponseDto {
|
|||
*/
|
||||
'updatedAt': string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -4344,6 +4352,12 @@ export interface UpdateTagDto {
|
|||
* @interface UpdateUserDto
|
||||
*/
|
||||
export interface UpdateUserDto {
|
||||
/**
|
||||
*
|
||||
* @type {UserAvatarColor}
|
||||
* @memberof UpdateUserDto
|
||||
*/
|
||||
'avatarColor'?: UserAvatarColor;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
@ -4399,6 +4413,8 @@ export interface UpdateUserDto {
|
|||
*/
|
||||
'storageLabel'?: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -4436,12 +4452,40 @@ export interface UsageByUserDto {
|
|||
*/
|
||||
'videos': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
|
||||
export const UserAvatarColor = {
|
||||
Primary: 'primary',
|
||||
Pink: 'pink',
|
||||
Red: 'red',
|
||||
Yellow: 'yellow',
|
||||
Blue: 'blue',
|
||||
Green: 'green',
|
||||
Purple: 'purple',
|
||||
Orange: 'orange',
|
||||
Gray: 'gray',
|
||||
Amber: 'amber'
|
||||
} as const;
|
||||
|
||||
export type UserAvatarColor = typeof UserAvatarColor[keyof typeof UserAvatarColor];
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface UserDto
|
||||
*/
|
||||
export interface UserDto {
|
||||
/**
|
||||
*
|
||||
* @type {UserAvatarColor}
|
||||
* @memberof UserDto
|
||||
*/
|
||||
'avatarColor': UserAvatarColor;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
@ -4467,12 +4511,20 @@ export interface UserDto {
|
|||
*/
|
||||
'profileImagePath': string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface UserResponseDto
|
||||
*/
|
||||
export interface UserResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {UserAvatarColor}
|
||||
* @memberof UserResponseDto
|
||||
*/
|
||||
'avatarColor': UserAvatarColor;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
@ -4552,6 +4604,8 @@ export interface UserResponseDto {
|
|||
*/
|
||||
'updatedAt': string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -16477,6 +16531,44 @@ export const UserApiAxiosParamCreator = function (configuration?: Configuration)
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteProfileImage: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/user/profile-image`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
|
@ -16802,6 +16894,15 @@ export const UserApiFp = function(configuration?: Configuration) {
|
|||
const localVarAxiosArgs = await localVarAxiosParamCreator.createUser(createUserDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async deleteProfileImage(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.deleteProfileImage(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
|
@ -16899,6 +17000,14 @@ export const UserApiFactory = function (configuration?: Configuration, basePath?
|
|||
createUser(requestParameters: UserApiCreateUserRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
|
||||
return localVarFp.createUser(requestParameters.createUserDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
deleteProfileImage(options?: AxiosRequestConfig): AxiosPromise<void> {
|
||||
return localVarFp.deleteProfileImage(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {UserApiDeleteUserRequest} requestParameters Request parameters.
|
||||
|
@ -17105,6 +17214,16 @@ export class UserApi extends BaseAPI {
|
|||
return UserApiFp(this.configuration).createUser(requestParameters.createUserDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof UserApi
|
||||
*/
|
||||
public deleteProfileImage(options?: AxiosRequestConfig) {
|
||||
return UserApiFp(this.configuration).deleteProfileImage(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {UserApiDeleteUserRequest} requestParameters Request parameters.
|
||||
|
|
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
|
@ -4,7 +4,7 @@
|
|||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.85.0
|
||||
* The version of the OpenAPI document: 1.86.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
|
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
|
@ -4,7 +4,7 @@
|
|||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.85.0
|
||||
* The version of the OpenAPI document: 1.86.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
|
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
|
@ -4,7 +4,7 @@
|
|||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.85.0
|
||||
* The version of the OpenAPI document: 1.86.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
|
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
|
@ -4,7 +4,7 @@
|
|||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.85.0
|
||||
* The version of the OpenAPI document: 1.86.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
<section class="immich-scrollbar max-h-[400px] overflow-y-auto pb-4">
|
||||
<div class="flex w-full place-items-center justify-between gap-4 p-5">
|
||||
<div class="flex place-items-center gap-4">
|
||||
<UserAvatar user={album.owner} size="md" autoColor />
|
||||
<UserAvatar user={album.owner} size="md" />
|
||||
<p class="text-sm font-medium">{album.owner.name}</p>
|
||||
</div>
|
||||
|
||||
|
@ -90,7 +90,7 @@
|
|||
class="flex w-full place-items-center justify-between gap-4 p-5 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
<div class="flex place-items-center gap-4">
|
||||
<UserAvatar {user} size="md" autoColor />
|
||||
<UserAvatar {user} size="md" />
|
||||
<p class="text-sm font-medium">{user.name}</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
on:click={() => handleUnselect(user)}
|
||||
class="flex place-items-center gap-1 rounded-full border border-gray-400 p-1 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
<UserAvatar {user} size="sm" autoColor />
|
||||
<UserAvatar {user} size="sm" />
|
||||
<p class="text-xs font-medium">{user.name}</p>
|
||||
</button>
|
||||
{/key}
|
||||
|
@ -94,7 +94,7 @@
|
|||
>✓</span
|
||||
>
|
||||
{:else}
|
||||
<UserAvatar {user} size="md" autoColor />
|
||||
<UserAvatar {user} size="md" />
|
||||
{/if}
|
||||
|
||||
<div class="text-left">
|
||||
|
|
|
@ -333,7 +333,7 @@
|
|||
<p class="text-sm">SHARED BY</p>
|
||||
<div class="flex gap-4 pt-4">
|
||||
<div>
|
||||
<UserAvatar user={asset.owner} size="md" autoColor />
|
||||
<UserAvatar user={asset.owner} size="md" />
|
||||
</div>
|
||||
|
||||
<div class="mb-auto mt-auto">
|
||||
|
|
|
@ -1,16 +1,48 @@
|
|||
<script lang="ts">
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import type { UserResponseDto } from '@api';
|
||||
import { api, UserAvatarColor, type UserResponseDto } from '@api';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import UserAvatar from '../user-avatar.svelte';
|
||||
import { mdiCog, mdiLogout } from '@mdi/js';
|
||||
import { mdiCog, mdiLogout, mdiPencil } from '@mdi/js';
|
||||
import { notificationController, NotificationType } from '../notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import AvatarSelector from './avatar-selector.svelte';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
|
||||
let isShowSelectAvatar = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const handleSaveProfile = async (color: UserAvatarColor) => {
|
||||
try {
|
||||
if (user.profileImagePath !== '') {
|
||||
await api.userApi.deleteProfileImage();
|
||||
}
|
||||
|
||||
const { data } = await api.userApi.updateUser({
|
||||
updateUserDto: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
avatarColor: color,
|
||||
},
|
||||
});
|
||||
|
||||
user = data;
|
||||
isShowSelectAvatar = false;
|
||||
|
||||
notificationController.show({
|
||||
message: 'Saved profile',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save profile');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -22,8 +54,22 @@
|
|||
<div
|
||||
class="mx-4 mt-4 flex flex-col items-center justify-center gap-4 rounded-3xl bg-white p-4 dark:bg-immich-dark-primary/10"
|
||||
>
|
||||
<UserAvatar size="xl" {user} />
|
||||
<div class="relative">
|
||||
{#key user}
|
||||
<UserAvatar {user} size="xl" />
|
||||
|
||||
<div
|
||||
class="absolute z-10 bottom-0 right-0 rounded-full w-6 h-6 border dark:border-immich-dark-primary bg-immich-primary"
|
||||
>
|
||||
<button
|
||||
class="flex items-center justify-center w-full h-full text-white"
|
||||
on:click={() => (isShowSelectAvatar = true)}
|
||||
>
|
||||
<Icon path={mdiPencil} />
|
||||
</button>
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-center text-lg font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
{user.name}
|
||||
|
@ -51,3 +97,10 @@
|
|||
>
|
||||
</div>
|
||||
</div>
|
||||
{#if isShowSelectAvatar}
|
||||
<AvatarSelector
|
||||
{user}
|
||||
on:close={() => (isShowSelectAvatar = false)}
|
||||
on:choose={({ detail: color }) => handleSaveProfile(color)}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<script lang="ts">
|
||||
import { mdiClose } from '@mdi/js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { UserAvatarColor, UserResponseDto } from '@api';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import FullScreenModal from '../full-screen-modal.svelte';
|
||||
import UserAvatar from '../user-avatar.svelte';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const colors: UserAvatarColor[] = Object.values(UserAvatarColor);
|
||||
</script>
|
||||
|
||||
<FullScreenModal on:clickOutside={() => dispatch('close')} on:escape={() => dispatch('close')}>
|
||||
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
|
||||
<div
|
||||
class=" rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg p-4"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary text-sm">
|
||||
SELECT AVATAR COLOR
|
||||
</h1>
|
||||
<div>
|
||||
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center p-4 mt-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
{#each colors as color}
|
||||
<button on:click={() => dispatch('choose', color)}>
|
||||
<UserAvatar {user} {color} size="xl" showProfileImage={false} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FullScreenModal>
|
|
@ -124,7 +124,9 @@
|
|||
on:mouseleave={() => (shouldShowAccountInfo = false)}
|
||||
on:click={() => (shouldShowAccountInfoPanel = !shouldShowAccountInfoPanel)}
|
||||
>
|
||||
<UserAvatar {user} size="lg" showTitle={false} interactive />
|
||||
{#key user}
|
||||
<UserAvatar {user} size="lg" showTitle={false} interactive />
|
||||
{/key}
|
||||
</button>
|
||||
|
||||
{#if shouldShowAccountInfo && !shouldShowAccountInfoPanel}
|
||||
|
@ -139,7 +141,7 @@
|
|||
{/if}
|
||||
|
||||
{#if shouldShowAccountInfoPanel}
|
||||
<AccountInfoPanel {user} on:logout={logOut} />
|
||||
<AccountInfoPanel bind:user on:logout={logOut} />
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1,35 +1,40 @@
|
|||
<script lang="ts" context="module">
|
||||
export type Color = 'primary' | 'pink' | 'red' | 'yellow' | 'blue' | 'green';
|
||||
export type Size = 'full' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
export type Size = 'full' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'xxxl';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { imageLoad } from '$lib/utils/image-load';
|
||||
import { api } from '@api';
|
||||
import { UserAvatarColor, api } from '@api';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
profileImagePath: string;
|
||||
avatarColor: UserAvatarColor;
|
||||
}
|
||||
|
||||
export let user: User;
|
||||
export let color: Color = 'primary';
|
||||
export let color: UserAvatarColor = user.avatarColor;
|
||||
export let size: Size = 'full';
|
||||
export let rounded = true;
|
||||
export let interactive = false;
|
||||
export let showTitle = true;
|
||||
export let autoColor = false;
|
||||
export let showProfileImage = true;
|
||||
|
||||
let showFallback = true;
|
||||
|
||||
const colorClasses: Record<Color, string> = {
|
||||
const colorClasses: Record<UserAvatarColor, string> = {
|
||||
primary: 'bg-immich-primary dark:bg-immich-dark-primary text-immich-dark-fg dark:text-immich-fg',
|
||||
pink: 'bg-pink-400 text-immich-bg',
|
||||
red: 'bg-red-500 text-immich-bg',
|
||||
yellow: 'bg-yellow-500 text-immich-bg',
|
||||
blue: 'bg-blue-500 text-immich-bg',
|
||||
green: 'bg-green-600 text-immich-bg',
|
||||
purple: 'bg-purple-600 text-immich-bg',
|
||||
orange: 'bg-orange-600 text-immich-bg',
|
||||
gray: 'bg-gray-600 text-immich-bg',
|
||||
amber: 'bg-amber-600 text-immich-bg',
|
||||
};
|
||||
|
||||
const sizeClasses: Record<Size, string> = {
|
||||
|
@ -37,18 +42,12 @@
|
|||
sm: 'w-7 h-7',
|
||||
md: 'w-10 h-10',
|
||||
lg: 'w-12 h-12',
|
||||
xl: 'w-20 h-20',
|
||||
xl: 'w-16 h-16',
|
||||
xxl: 'w-24 h-24',
|
||||
xxxl: 'w-28 h-28',
|
||||
};
|
||||
|
||||
// Get color based on the user UUID.
|
||||
function getUserColor() {
|
||||
const seed = parseInt(user.id.split('-')[0], 16);
|
||||
const colors = Object.keys(colorClasses).filter((color) => color !== 'primary') as Color[];
|
||||
const randomIndex = seed % colors.length;
|
||||
return colors[randomIndex];
|
||||
}
|
||||
|
||||
$: colorClass = colorClasses[autoColor ? getUserColor() : color];
|
||||
$: colorClass = colorClasses[color];
|
||||
$: sizeClass = sizeClasses[size];
|
||||
$: title = `${user.name} (${user.email})`;
|
||||
$: interactiveClass = interactive
|
||||
|
@ -61,7 +60,7 @@
|
|||
class:rounded-full={rounded}
|
||||
title={showTitle ? title : undefined}
|
||||
>
|
||||
{#if user.profileImagePath}
|
||||
{#if showProfileImage && user.profileImagePath}
|
||||
<img
|
||||
src={api.getProfileImageUrl(user.id)}
|
||||
alt="Profile image of {title}"
|
||||
|
@ -74,12 +73,12 @@
|
|||
{/if}
|
||||
{#if showFallback}
|
||||
<span
|
||||
class="flex h-full w-full select-none items-center justify-center"
|
||||
class="flex h-full w-full select-none items-center justify-center font-medium"
|
||||
class:text-xs={size === 'sm'}
|
||||
class:text-lg={size === 'lg'}
|
||||
class:text-xl={size === 'xl'}
|
||||
class:font-medium={!autoColor}
|
||||
class:font-semibold={autoColor}
|
||||
class:text-2xl={size === 'xxl'}
|
||||
class:text-3xl={size === 'xxxl'}
|
||||
>
|
||||
{(user.name[0] || '').toUpperCase()}
|
||||
</span>
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
>✓</span
|
||||
>
|
||||
{:else}
|
||||
<UserAvatar {user} size="lg" autoColor />
|
||||
<UserAvatar {user} size="lg" />
|
||||
{/if}
|
||||
|
||||
<div class="text-left">
|
||||
|
|
|
@ -113,7 +113,7 @@
|
|||
<div class="rounded-2xl border border-gray-200 dark:border-gray-800 mt-6 bg-slate-50 dark:bg-gray-900 p-5">
|
||||
<div class="flex gap-4 rounded-lg pb-4 transition-all justify-between">
|
||||
<div class="flex gap-4">
|
||||
<UserAvatar user={partner.user} size="md" autoColor />
|
||||
<UserAvatar user={partner.user} size="md" />
|
||||
<div class="text-left">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{partner.user.name}
|
||||
|
|
|
@ -603,13 +603,13 @@
|
|||
|
||||
<!-- owner -->
|
||||
<button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
|
||||
<UserAvatar user={album.owner} size="md" autoColor />
|
||||
<UserAvatar user={album.owner} size="md" />
|
||||
</button>
|
||||
|
||||
<!-- users -->
|
||||
{#each album.sharedUsers as user (user.id)}
|
||||
<button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
|
||||
<UserAvatar {user} size="md" autoColor />
|
||||
<UserAvatar {user} size="md" />
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
href="/partners/{partner.id}"
|
||||
class="flex gap-4 rounded-lg px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
<UserAvatar user={partner} size="lg" autoColor />
|
||||
<UserAvatar user={partner} size="lg" />
|
||||
<div class="text-left">
|
||||
<p class="text-immich-fg dark:text-immich-dark-fg">
|
||||
{partner.name}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { UserResponseDto } from '@api';
|
||||
import { UserAvatarColor, type UserResponseDto } from '@api';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { Sync } from 'factory.ts';
|
||||
|
||||
|
@ -16,4 +16,5 @@ export const userFactory = Sync.makeFactory<UserResponseDto>({
|
|||
updatedAt: Sync.each(() => faker.date.past().toISOString()),
|
||||
memoriesEnabled: true,
|
||||
oauthId: '',
|
||||
avatarColor: UserAvatarColor.Primary,
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue