mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-06-02 06:14:48 +02:00
fix: improve profile image handling and review translations
This commit is contained in:
@@ -904,6 +904,19 @@
|
||||
"loading": "Loading",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Change banner",
|
||||
"replace_banner": "Replace banner",
|
||||
"remove_banner": "Remove banner",
|
||||
|
||||
@@ -891,6 +891,19 @@
|
||||
"your_friend_code": "Your friend code:",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Upload banner",
|
||||
"uploading_banner": "Uploading banner…",
|
||||
"change_banner": "Change banner",
|
||||
|
||||
@@ -904,6 +904,19 @@
|
||||
"loading": "Loading",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Change banner",
|
||||
"replace_banner": "Replace banner",
|
||||
"remove_banner": "Remove banner",
|
||||
|
||||
@@ -891,6 +891,19 @@
|
||||
"your_friend_code": "Your friend code:",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Upload banner",
|
||||
"uploading_banner": "Uploading banner…",
|
||||
"change_banner": "Change banner",
|
||||
|
||||
@@ -904,6 +904,19 @@
|
||||
"loading": "Loading",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Change banner",
|
||||
"replace_banner": "Replace banner",
|
||||
"remove_banner": "Remove banner",
|
||||
|
||||
@@ -891,6 +891,19 @@
|
||||
"your_friend_code": "Your friend code:",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Upload banner",
|
||||
"uploading_banner": "Uploading banner…",
|
||||
"change_banner": "Change banner",
|
||||
|
||||
@@ -904,6 +904,19 @@
|
||||
"loading": "Loading",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Change banner",
|
||||
"replace_banner": "Replace banner",
|
||||
"remove_banner": "Remove banner",
|
||||
|
||||
@@ -981,6 +981,8 @@
|
||||
"your_friend_code": "Your friend code:",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
|
||||
@@ -967,6 +967,8 @@
|
||||
"your_friend_code": "Tu código de amistad:",
|
||||
"copy_friend_code": "Copiar código de amistad",
|
||||
"copied": "¡Copiado!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"crop_profile_picture": "Ajustar foto de perfil",
|
||||
"crop_profile_banner": "Ajustar banner",
|
||||
"crop_profile_image_description": "Arrastrá la imagen para reposicionarla y usá el zoom para redimensionarla.",
|
||||
|
||||
@@ -894,6 +894,19 @@
|
||||
"loading": "Loading",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Change banner",
|
||||
"replace_banner": "Replace banner",
|
||||
"remove_banner": "Remove banner",
|
||||
|
||||
@@ -891,6 +891,19 @@
|
||||
"your_friend_code": "Your friend code:",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Upload banner",
|
||||
"uploading_banner": "Uploading banner…",
|
||||
"change_banner": "Change banner",
|
||||
|
||||
@@ -910,6 +910,19 @@
|
||||
"loading": "Loading",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Change banner",
|
||||
"replace_banner": "Replace banner",
|
||||
"remove_banner": "Remove banner",
|
||||
|
||||
@@ -957,6 +957,19 @@
|
||||
"view_wrapped_button": "Voir le Bilan 2025 de {{displayName}}",
|
||||
"no_friends_yet": "Vous n’avez encore ajouté aucun ami",
|
||||
"copied": "Copié !",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Changer la bannière",
|
||||
"replace_banner": "Remplacer la bannière",
|
||||
"remove_banner": "Supprimer la bannière",
|
||||
|
||||
@@ -914,6 +914,19 @@
|
||||
"your_friend_code": "A barát kódod:",
|
||||
"copy_friend_code": "Barátkód kimásolása",
|
||||
"copied": "Kimásolva!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Borítókép feltöltése",
|
||||
"uploading_banner": "Borítókép feltöltése…",
|
||||
"change_banner": "Borítókép változtatása",
|
||||
|
||||
@@ -914,6 +914,19 @@
|
||||
"your_friend_code": "Kode teman Anda:",
|
||||
"copy_friend_code": "Salin kode teman",
|
||||
"copied": "Disalin!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Unggah banner",
|
||||
"uploading_banner": "Mengunggah banner…",
|
||||
"change_banner": "Ganti banner",
|
||||
|
||||
@@ -891,6 +891,19 @@
|
||||
"your_friend_code": "Il tuo codice amico:",
|
||||
"copy_friend_code": "Copia codice amico",
|
||||
"copied": "Copiato!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Carica banner",
|
||||
"uploading_banner": "Caricamento banner…",
|
||||
"change_banner": "Cambia banner",
|
||||
|
||||
@@ -916,6 +916,19 @@
|
||||
"your_friend_code": "あなたのフレンドコード:",
|
||||
"copy_friend_code": "フレンドコードをコピー",
|
||||
"copied": "コピーしました!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "バナーをアップロード",
|
||||
"uploading_banner": "バナーをアップロード中…",
|
||||
"change_banner": "バナーを変更",
|
||||
|
||||
@@ -891,6 +891,19 @@
|
||||
"your_friend_code": "Your friend code:",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Upload banner",
|
||||
"uploading_banner": "Uploading banner…",
|
||||
"change_banner": "Change banner",
|
||||
|
||||
@@ -891,6 +891,19 @@
|
||||
"your_friend_code": "Your friend code:",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Upload banner",
|
||||
"uploading_banner": "Uploading banner…",
|
||||
"change_banner": "Change banner",
|
||||
|
||||
@@ -910,6 +910,19 @@
|
||||
"loading": "Loading",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Change banner",
|
||||
"replace_banner": "Replace banner",
|
||||
"remove_banner": "Remove banner",
|
||||
|
||||
@@ -891,6 +891,19 @@
|
||||
"your_friend_code": "Your friend code:",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Upload banner",
|
||||
"uploading_banner": "Uploading banner…",
|
||||
"change_banner": "Change banner",
|
||||
|
||||
@@ -891,6 +891,19 @@
|
||||
"your_friend_code": "Your friend code:",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Upload banner",
|
||||
"uploading_banner": "Uploading banner…",
|
||||
"change_banner": "Change banner",
|
||||
|
||||
@@ -974,6 +974,19 @@
|
||||
"your_friend_code": "Twój kod znajomego:",
|
||||
"copy_friend_code": "Kopiuj kod znajomego",
|
||||
"copied": "Skopiowano!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Prześlij baner",
|
||||
"uploading_banner": "Przesyłanie baneru…",
|
||||
"change_banner": "Zmień baner",
|
||||
|
||||
@@ -952,6 +952,8 @@
|
||||
"your_friend_code": "Seu código de amigo:",
|
||||
"copy_friend_code": "Copiar código de amigo",
|
||||
"copied": "Copiado!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"crop_profile_picture": "Ajustar foto de perfil",
|
||||
"crop_profile_banner": "Ajustar banner",
|
||||
"crop_profile_image_description": "Arraste a imagem para reposicioná-la e use o zoom para redimensioná-la.",
|
||||
|
||||
@@ -856,6 +856,19 @@
|
||||
"your_friend_code": "O teu código de amigo:",
|
||||
"copy_friend_code": "Copiar código de amigo",
|
||||
"copied": "Copiado!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Fazer upload do banner",
|
||||
"uploading_banner": "A fazer upload do banner…",
|
||||
"change_banner": "Alterar banner",
|
||||
|
||||
@@ -891,6 +891,19 @@
|
||||
"your_friend_code": "Your friend code:",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Upload banner",
|
||||
"uploading_banner": "Uploading banner…",
|
||||
"change_banner": "Change banner",
|
||||
|
||||
@@ -977,6 +977,8 @@
|
||||
"your_friend_code": "Ваш код друга:",
|
||||
"copy_friend_code": "Копировать код друга",
|
||||
"copied": "Скопировано!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"crop_profile_picture": "Настроить фото профиля",
|
||||
"crop_profile_banner": "Настроить баннер",
|
||||
"crop_profile_image_description": "Перетащите изображение, чтобы изменить его положение, затем используйте масштаб для изменения размера.",
|
||||
|
||||
@@ -1127,6 +1127,19 @@
|
||||
"your_friend_code": "Your friend code:",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"upload_banner": "Upload banner",
|
||||
"uploading_banner": "Uploading banner…",
|
||||
"change_banner": "Change banner",
|
||||
|
||||
@@ -904,6 +904,19 @@
|
||||
"loading": "Loading",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Change banner",
|
||||
"replace_banner": "Replace banner",
|
||||
"remove_banner": "Remove banner",
|
||||
|
||||
@@ -913,6 +913,19 @@
|
||||
"loading": "Loading",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Change banner",
|
||||
"replace_banner": "Replace banner",
|
||||
"remove_banner": "Remove banner",
|
||||
|
||||
@@ -910,6 +910,19 @@
|
||||
"loading": "Loading",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Change banner",
|
||||
"replace_banner": "Replace banner",
|
||||
"remove_banner": "Remove banner",
|
||||
|
||||
@@ -904,6 +904,19 @@
|
||||
"loading": "Loading",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Change banner",
|
||||
"replace_banner": "Replace banner",
|
||||
"remove_banner": "Remove banner",
|
||||
|
||||
@@ -913,6 +913,19 @@
|
||||
"loading": "Loading",
|
||||
"copy_friend_code": "Copy friend code",
|
||||
"copied": "Copied!",
|
||||
"change_profile_picture": "Change profile picture",
|
||||
"crop_profile_picture": "Adjust profile picture",
|
||||
"crop_profile_banner": "Adjust banner",
|
||||
"crop_profile_image_description": "Drag the image to reposition it, then use zoom to resize it.",
|
||||
"crop_profile_image_stage": "Profile image crop area. Use the arrow keys to move the image.",
|
||||
"zoom": "Zoom",
|
||||
"reset": "Reset",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"toggle_grid": "Toggle grid",
|
||||
"rotate": "Rotate",
|
||||
"applying_crop": "Applying?",
|
||||
"apply_crop": "Apply",
|
||||
"change_banner": "Change banner",
|
||||
"replace_banner": "Replace banner",
|
||||
"remove_banner": "Remove banner",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileTypeFromFile } from "file-type";
|
||||
import sharp from "sharp";
|
||||
import { registerEvent } from "../register-event";
|
||||
|
||||
export interface ProfileImageMetadata {
|
||||
@@ -7,8 +9,61 @@ export interface ProfileImageMetadata {
|
||||
isAnimated: boolean;
|
||||
}
|
||||
|
||||
const isGifAnimated = (buffer: Buffer) => {
|
||||
if (buffer.length < 13) return false;
|
||||
const HEADER_BYTES = 256 * 1024;
|
||||
const PNG_SIGNATURE = Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
||||
]);
|
||||
const WEBP_ANIMATION_FLAG = 0x02;
|
||||
|
||||
const MIME_BY_EXTENSION: Record<string, string> = {
|
||||
".apng": "image/apng",
|
||||
".gif": "image/gif",
|
||||
".png": "image/png",
|
||||
".webp": "image/webp",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
};
|
||||
|
||||
const getMimeType = async (imagePath: string) => {
|
||||
const fileType = await fileTypeFromFile(imagePath).catch(() => null);
|
||||
|
||||
return (
|
||||
fileType?.mime ??
|
||||
MIME_BY_EXTENSION[path.extname(imagePath).toLowerCase()] ??
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
const readHeader = async (imagePath: string) => {
|
||||
const handle = await fs.promises.open(imagePath, "r");
|
||||
|
||||
try {
|
||||
const buffer = Buffer.alloc(HEADER_BYTES);
|
||||
const { bytesRead } = await handle.read(buffer, 0, HEADER_BYTES, 0);
|
||||
|
||||
return buffer.subarray(0, bytesRead);
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
};
|
||||
|
||||
const skipGifSubBlocks = (buffer: Buffer, offset: number) => {
|
||||
let nextOffset = offset;
|
||||
|
||||
while (nextOffset < buffer.length) {
|
||||
const blockSize = buffer[nextOffset];
|
||||
nextOffset += 1;
|
||||
|
||||
if (blockSize === 0) return nextOffset;
|
||||
|
||||
nextOffset += blockSize;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const isGifAnimated = (buffer: Buffer): boolean | null => {
|
||||
if (buffer.length < 13) return null;
|
||||
|
||||
let frameCount = 0;
|
||||
let offset = 13;
|
||||
@@ -18,19 +73,6 @@ const isGifAnimated = (buffer: Buffer) => {
|
||||
offset += 3 * 2 ** ((packedField & 0x07) + 1);
|
||||
}
|
||||
|
||||
const skipSubBlocks = () => {
|
||||
while (offset < buffer.length) {
|
||||
const blockSize = buffer[offset];
|
||||
offset += 1;
|
||||
|
||||
if (blockSize === 0) return true;
|
||||
|
||||
offset += blockSize;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
while (offset < buffer.length) {
|
||||
const marker = buffer[offset];
|
||||
offset += 1;
|
||||
@@ -38,8 +80,7 @@ const isGifAnimated = (buffer: Buffer) => {
|
||||
if (marker === 0x2c) {
|
||||
frameCount += 1;
|
||||
if (frameCount > 1) return true;
|
||||
|
||||
if (offset + 9 > buffer.length) return false;
|
||||
if (offset + 9 > buffer.length) return null;
|
||||
|
||||
const imagePackedField = buffer[offset + 8];
|
||||
offset += 9;
|
||||
@@ -48,50 +89,119 @@ const isGifAnimated = (buffer: Buffer) => {
|
||||
offset += 3 * 2 ** ((imagePackedField & 0x07) + 1);
|
||||
}
|
||||
|
||||
offset += 1;
|
||||
const nextOffset = skipGifSubBlocks(buffer, offset + 1);
|
||||
if (nextOffset === null) return null;
|
||||
|
||||
if (!skipSubBlocks()) return false;
|
||||
continue;
|
||||
offset = nextOffset;
|
||||
} else if (marker === 0x21) {
|
||||
const nextOffset = skipGifSubBlocks(buffer, offset + 1);
|
||||
if (nextOffset === null) return null;
|
||||
|
||||
offset = nextOffset;
|
||||
} else if (marker === 0x3b) {
|
||||
return false;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const isWebpAnimated = (buffer: Buffer): boolean | null => {
|
||||
if (buffer.length < 21) return null;
|
||||
if (buffer.toString("ascii", 0, 4) !== "RIFF") return false;
|
||||
if (buffer.toString("ascii", 8, 12) !== "WEBP") return false;
|
||||
|
||||
let offset = 12;
|
||||
|
||||
while (offset + 8 <= buffer.length) {
|
||||
const chunkType = buffer.toString("ascii", offset, offset + 4);
|
||||
const chunkSize = buffer.readUInt32LE(offset + 4);
|
||||
const chunkDataOffset = offset + 8;
|
||||
|
||||
if (chunkType === "VP8X") {
|
||||
return (
|
||||
chunkDataOffset < buffer.length &&
|
||||
(buffer[chunkDataOffset] & WEBP_ANIMATION_FLAG) !== 0
|
||||
);
|
||||
}
|
||||
|
||||
if (marker === 0x21) {
|
||||
offset += 1;
|
||||
if (!skipSubBlocks()) return false;
|
||||
continue;
|
||||
}
|
||||
if (chunkType === "ANIM") return true;
|
||||
|
||||
if (marker === 0x3b) break;
|
||||
offset = chunkDataOffset + chunkSize + (chunkSize % 2);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const isPngAnimated = (buffer: Buffer): boolean | null => {
|
||||
if (buffer.length < PNG_SIGNATURE.length + 12) return null;
|
||||
if (!buffer.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE))
|
||||
return false;
|
||||
|
||||
let offset = PNG_SIGNATURE.length;
|
||||
|
||||
while (offset + 12 <= buffer.length) {
|
||||
const chunkSize = buffer.readUInt32BE(offset);
|
||||
const chunkType = buffer.toString("ascii", offset + 4, offset + 8);
|
||||
|
||||
if (chunkType === "acTL") return true;
|
||||
if (chunkType === "IDAT" || chunkType === "IEND") return false;
|
||||
|
||||
offset += 12 + chunkSize;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const canBeAnimated = (mimeType: string | null) => {
|
||||
return (
|
||||
mimeType === "image/gif" ||
|
||||
mimeType === "image/apng" ||
|
||||
mimeType === "image/webp" ||
|
||||
mimeType === "image/png"
|
||||
);
|
||||
};
|
||||
|
||||
const detectAnimationFromHeader = (mimeType: string | null, buffer: Buffer) => {
|
||||
if (mimeType === "image/gif") return isGifAnimated(buffer);
|
||||
if (mimeType === "image/webp") return isWebpAnimated(buffer);
|
||||
if (mimeType === "image/apng" || mimeType === "image/png") {
|
||||
return isPngAnimated(buffer);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isWebpAnimated = (buffer: Buffer) => {
|
||||
return buffer.includes(Buffer.from("ANIM"));
|
||||
};
|
||||
const detectAnimation = async (mimeType: string | null, imagePath: string) => {
|
||||
const metadata = await sharp(imagePath, { animated: true }).metadata();
|
||||
|
||||
const isPngAnimated = (buffer: Buffer) => {
|
||||
return buffer.includes(Buffer.from("acTL"));
|
||||
if (metadata.pages) return metadata.pages > 1;
|
||||
|
||||
return (
|
||||
detectAnimationFromHeader(mimeType, await readHeader(imagePath)) ?? true
|
||||
);
|
||||
};
|
||||
|
||||
const getProfileImageMetadata = async (
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
imagePath: string
|
||||
): Promise<ProfileImageMetadata> => {
|
||||
const [fileType, buffer] = await Promise.all([
|
||||
fileTypeFromFile(imagePath).catch(() => null),
|
||||
fs.promises.readFile(imagePath),
|
||||
]);
|
||||
const mimeType = await getMimeType(imagePath);
|
||||
|
||||
const mimeType = fileType?.mime ?? null;
|
||||
if (!canBeAnimated(mimeType)) {
|
||||
return { mimeType, isAnimated: false };
|
||||
}
|
||||
|
||||
return {
|
||||
mimeType,
|
||||
isAnimated:
|
||||
(mimeType === "image/gif" && isGifAnimated(buffer)) ||
|
||||
(mimeType === "image/webp" && isWebpAnimated(buffer)) ||
|
||||
(mimeType === "image/png" && isPngAnimated(buffer)),
|
||||
};
|
||||
try {
|
||||
return {
|
||||
mimeType,
|
||||
isAnimated: await detectAnimation(mimeType, imagePath),
|
||||
};
|
||||
} catch {
|
||||
return { mimeType, isAnimated: true };
|
||||
}
|
||||
};
|
||||
|
||||
registerEvent("getProfileImageMetadata", getProfileImageMetadata);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import type { GameReview } from "@types";
|
||||
|
||||
import { sanitizeHtml } from "@shared";
|
||||
import { getReviewTranslationLanguage, sanitizeHtml } from "@shared";
|
||||
import { useDate, useFormat } from "@renderer/hooks";
|
||||
import { formatNumber } from "@renderer/helpers";
|
||||
import { Avatar } from "@renderer/components";
|
||||
@@ -72,9 +72,12 @@ export function ReviewItem({
|
||||
|
||||
const isDifferentLanguage =
|
||||
getBaseLanguage(review.detectedLanguage) !== getBaseLanguage(i18n.language);
|
||||
const reviewTranslationLanguage = getReviewTranslationLanguage(i18n.language);
|
||||
|
||||
const needsTranslation =
|
||||
!isOwnReview && isDifferentLanguage && review.translations[i18n.language];
|
||||
!isOwnReview &&
|
||||
isDifferentLanguage &&
|
||||
review.translations[reviewTranslationLanguage];
|
||||
|
||||
const getLanguageName = (languageCode: string | null) => {
|
||||
if (!languageCode) return "";
|
||||
@@ -104,7 +107,7 @@ export function ReviewItem({
|
||||
|
||||
// Determine which content to show - always show original for own reviews
|
||||
const displayContent = needsTranslation
|
||||
? review.translations[i18n.language]
|
||||
? review.translations[reviewTranslationLanguage]
|
||||
: review.reviewHtml;
|
||||
|
||||
if (isBlocked && !isVisible) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import { getProfileImageMetadata } from "../profile-image-metadata";
|
||||
import { ProfileImageCropModal } from "../profile-image-crop-modal/profile-image-crop-modal";
|
||||
import "./edit-profile-modal.scss";
|
||||
|
||||
@@ -92,18 +93,16 @@ export function EditProfileModal(
|
||||
name="profileImageUrl"
|
||||
render={({ field: { value, onChange } }) => {
|
||||
const handleProfileImagePath = async (path: string) => {
|
||||
const metadata = await window.electron
|
||||
.getProfileImageMetadata(path)
|
||||
.catch(() => null);
|
||||
const metadata = await getProfileImageMetadata(path);
|
||||
|
||||
if (metadata?.isAnimated && hasActiveSubscription) {
|
||||
if (metadata.isAnimated && hasActiveSubscription) {
|
||||
// Crop while preserving animation (handled in main/sharp).
|
||||
setCropIsAnimated(true);
|
||||
setProfileImageToCrop(path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata?.isAnimated && !hasActiveSubscription) {
|
||||
if (metadata.isAnimated && !hasActiveSubscription) {
|
||||
const { imagePath } = await window.electron
|
||||
.processProfileImage(path)
|
||||
.catch(() => {
|
||||
@@ -129,7 +128,7 @@ export function EditProfileModal(
|
||||
filters: [
|
||||
{
|
||||
name: "Image",
|
||||
extensions: ["jpg", "jpeg", "png", "gif", "webp"],
|
||||
extensions: ["jpg", "jpeg", "png", "apng", "gif", "webp"],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -155,6 +154,7 @@ export function EditProfileModal(
|
||||
type="button"
|
||||
className="edit-profile-modal__avatar-container"
|
||||
onClick={handleChangeProfileAvatar}
|
||||
aria-label={t("change_profile_picture")}
|
||||
>
|
||||
<Avatar
|
||||
size={128}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Star, ThumbsUp, ThumbsDown, TrashIcon, Languages } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import type { GameShop } from "@types";
|
||||
import { sanitizeHtml } from "@shared";
|
||||
import { getReviewTranslationLanguage, sanitizeHtml } from "@shared";
|
||||
import { useDate } from "@renderer/hooks";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
import "./profile-content.scss";
|
||||
@@ -65,9 +65,12 @@ export function ProfileReviewItem({
|
||||
|
||||
const isDifferentLanguage =
|
||||
getBaseLanguage(review.detectedLanguage) !== getBaseLanguage(i18n.language);
|
||||
const reviewTranslationLanguage = getReviewTranslationLanguage(i18n.language);
|
||||
|
||||
const needsTranslation =
|
||||
!isOwnReview && isDifferentLanguage && review.translations[i18n.language];
|
||||
!isOwnReview &&
|
||||
isDifferentLanguage &&
|
||||
review.translations[reviewTranslationLanguage];
|
||||
|
||||
const getLanguageName = (languageCode: string | null) => {
|
||||
if (!languageCode) return "";
|
||||
@@ -82,7 +85,7 @@ export function ProfileReviewItem({
|
||||
};
|
||||
|
||||
const displayContent = needsTranslation
|
||||
? review.translations[i18n.language]
|
||||
? review.translations[reviewTranslationLanguage]
|
||||
: review.reviewHtml;
|
||||
|
||||
return (
|
||||
|
||||
@@ -53,6 +53,15 @@
|
||||
border-color: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
border-color: globals.$brand-teal;
|
||||
box-shadow:
|
||||
0 12px 32px rgba(0, 0, 0, 0.45),
|
||||
0 0 0 3px rgba(globals.$brand-teal, 0.35),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
+54
-6
@@ -9,6 +9,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Button, Modal } from "@renderer/components";
|
||||
import { useToast } from "@renderer/hooks";
|
||||
import { logger } from "@renderer/logger";
|
||||
|
||||
import "./profile-image-crop-modal.scss";
|
||||
|
||||
@@ -34,6 +35,7 @@ const CROP_OUTPUT_SIZE: Record<CropVariant, { width: number; height: number }> =
|
||||
const MAX_ZOOM = 4;
|
||||
const MIN_ZOOM = 1;
|
||||
const ZOOM_STEP = 0.25;
|
||||
const KEYBOARD_PAN_STEP = 10;
|
||||
|
||||
const clamp = (value: number, min: number, max: number) =>
|
||||
Math.min(Math.max(value, min), max);
|
||||
@@ -51,7 +53,9 @@ export function ProfileImageCropModal({
|
||||
|
||||
const frameRef = useRef<HTMLDivElement | null>(null);
|
||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||
const wheelTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const interactionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null
|
||||
);
|
||||
const dragStateRef = useRef<{
|
||||
pointerId: number;
|
||||
startX: number;
|
||||
@@ -171,11 +175,26 @@ export function ProfileImageCropModal({
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (wheelTimeoutRef.current) clearTimeout(wheelTimeoutRef.current);
|
||||
if (interactionTimeoutRef.current) {
|
||||
clearTimeout(interactionTimeoutRef.current);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const scheduleInteractionEnd = () => {
|
||||
setIsInteracting(true);
|
||||
|
||||
if (interactionTimeoutRef.current) {
|
||||
clearTimeout(interactionTimeoutRef.current);
|
||||
}
|
||||
|
||||
interactionTimeoutRef.current = setTimeout(
|
||||
() => setIsInteracting(false),
|
||||
400
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || !imagePath) {
|
||||
setPreviewUrl(null);
|
||||
@@ -309,9 +328,34 @@ export function ProfileImageCropModal({
|
||||
y: event.clientY - rect.top,
|
||||
});
|
||||
|
||||
setIsInteracting(true);
|
||||
if (wheelTimeoutRef.current) clearTimeout(wheelTimeoutRef.current);
|
||||
wheelTimeoutRef.current = setTimeout(() => setIsInteracting(false), 400);
|
||||
scheduleInteractionEnd();
|
||||
};
|
||||
|
||||
const handleFrameKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (isApplying || !imageSize.width) return;
|
||||
|
||||
const panStep = KEYBOARD_PAN_STEP * (event.shiftKey ? 4 : 1);
|
||||
const movement = {
|
||||
ArrowDown: { x: 0, y: panStep },
|
||||
ArrowLeft: { x: -panStep, y: 0 },
|
||||
ArrowRight: { x: panStep, y: 0 },
|
||||
ArrowUp: { x: 0, y: -panStep },
|
||||
}[event.key];
|
||||
|
||||
if (!movement) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
setPosition((currentPosition) =>
|
||||
clampPosition(
|
||||
{
|
||||
x: currentPosition.x + movement.x,
|
||||
y: currentPosition.y + movement.y,
|
||||
},
|
||||
scale
|
||||
)
|
||||
);
|
||||
scheduleInteractionEnd();
|
||||
};
|
||||
|
||||
const handleZoomChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -423,7 +467,7 @@ export function ProfileImageCropModal({
|
||||
|
||||
onApply(tempImagePath);
|
||||
} catch (error) {
|
||||
console.error("Failed to crop profile image", error);
|
||||
logger.error("Failed to crop profile image", error);
|
||||
showErrorToast(t("image_process_failure"));
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
@@ -471,6 +515,10 @@ export function ProfileImageCropModal({
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerCancel={handlePointerUp}
|
||||
onWheel={handleWheel}
|
||||
onKeyDown={handleFrameKeyDown}
|
||||
role="application"
|
||||
tabIndex={0}
|
||||
aria-label={t("crop_profile_image_stage")}
|
||||
>
|
||||
{previewUrl && (
|
||||
<img
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { logger } from "@renderer/logger";
|
||||
|
||||
export interface ProfileImageMetadata {
|
||||
mimeType: string | null;
|
||||
isAnimated: boolean;
|
||||
}
|
||||
|
||||
const ANIMATION_CAPABLE_IMAGE_EXTENSION = /\.(apng|gif|png|webp)$/i;
|
||||
|
||||
const canPathBeAnimated = (imagePath: string) => {
|
||||
return ANIMATION_CAPABLE_IMAGE_EXTENSION.test(imagePath.split(/[?#]/)[0]);
|
||||
};
|
||||
|
||||
export const getProfileImageMetadata = async (
|
||||
imagePath: string
|
||||
): Promise<ProfileImageMetadata> => {
|
||||
try {
|
||||
return await window.electron.getProfileImageMetadata(imagePath);
|
||||
} catch (error) {
|
||||
logger.warn("Failed to get profile image metadata", error);
|
||||
|
||||
return {
|
||||
mimeType: null,
|
||||
isAnimated: canPathBeAnimated(imagePath),
|
||||
};
|
||||
}
|
||||
};
|
||||
+4
-5
@@ -6,6 +6,7 @@ import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getProfileImageMetadata } from "../profile-image-metadata";
|
||||
import { ProfileImageCropModal } from "../profile-image-crop-modal/profile-image-crop-modal";
|
||||
import "./upload-background-image-button.scss";
|
||||
|
||||
@@ -66,18 +67,16 @@ export function UploadBackgroundImageButton() {
|
||||
filters: [
|
||||
{
|
||||
name: "Image",
|
||||
extensions: ["jpg", "jpeg", "png", "gif", "webp"],
|
||||
extensions: ["jpg", "jpeg", "png", "apng", "gif", "webp"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (filePaths && filePaths.length > 0) {
|
||||
const path = filePaths[0];
|
||||
const metadata = await window.electron
|
||||
.getProfileImageMetadata(path)
|
||||
.catch(() => null);
|
||||
const metadata = await getProfileImageMetadata(path);
|
||||
|
||||
if (metadata?.isAnimated) {
|
||||
if (metadata.isAnimated) {
|
||||
// Crop while preserving animation (handled in main/sharp).
|
||||
setCropIsAnimated(true);
|
||||
setBannerImageToCrop(path);
|
||||
|
||||
@@ -199,6 +199,9 @@ export const getDateLocale = (language: string) => {
|
||||
return enUS;
|
||||
};
|
||||
|
||||
export const getReviewTranslationLanguage = (language: string) =>
|
||||
language.split("-")[0].toLowerCase();
|
||||
|
||||
export const formatDate = (
|
||||
date: number | Date | string,
|
||||
language: string
|
||||
|
||||
Reference in New Issue
Block a user