fix: improve profile image handling and review translations

This commit is contained in:
Chubby Granny Chaser
2026-05-26 14:12:29 +01:00
parent 5049cc3ca2
commit ee669dfc6b
42 changed files with 653 additions and 66 deletions
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+2
View File
@@ -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.",
+2
View File
@@ -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.",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -957,6 +957,19 @@
"view_wrapped_button": "Voir le Bilan 2025 de {{displayName}}",
"no_friends_yet": "Vous navez 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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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": "バナーを変更",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+2
View File
@@ -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.",
+13
View File
@@ -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",
+13
View File
@@ -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",
+2
View File
@@ -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": "Перетащите изображение, чтобы изменить его положение, затем используйте масштаб для изменения размера.",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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",
+13
View File
@@ -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;
}
@@ -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),
};
}
};
@@ -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);
+3
View File
@@ -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