Profile picture frontend + backend
This commit is contained in:
		
							parent
							
								
									76cd92ee0e
								
							
						
					
					
						commit
						59818eb321
					
				
							
								
								
									
										57
									
								
								Core/API/Parameter/FloatType.class.php
									
									
									
									
									
										Normal file
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										57
									
								
								Core/API/Parameter/FloatType.class.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | |||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace Core\API\Parameter; | ||||||
|  | 
 | ||||||
|  | class FloatType extends Parameter { | ||||||
|  | 
 | ||||||
|  |   public float $minValue; | ||||||
|  |   public float $maxValue; | ||||||
|  |   public function __construct(string $name, float $minValue = PHP_FLOAT_MIN, float $maxValue = PHP_FLOAT_MAX, | ||||||
|  |                               bool $optional = FALSE, ?float $defaultValue = NULL, ?array $choices = NULL) { | ||||||
|  |     $this->minValue = $minValue; | ||||||
|  |     $this->maxValue = $maxValue; | ||||||
|  |     parent::__construct($name, Parameter::TYPE_FLOAT, $optional, $defaultValue, $choices); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public function parseParam($value): bool { | ||||||
|  |     if (!parent::parseParam($value)) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     $this->value = $value; | ||||||
|  |     if ($this->value < $this->minValue || $this->value > $this->maxValue) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public function getTypeName(): string { | ||||||
|  |     $typeName = parent::getTypeName(); | ||||||
|  |     $hasMin = $this->minValue > PHP_FLOAT_MIN; | ||||||
|  |     $hasMax = $this->maxValue < PHP_FLOAT_MAX; | ||||||
|  | 
 | ||||||
|  |     if ($hasMin || $hasMax) { | ||||||
|  |       if ($hasMin && $hasMax) { | ||||||
|  |         $typeName .= " ($this->minValue - $this->maxValue)"; | ||||||
|  |       } else if ($hasMin) { | ||||||
|  |         $typeName .= " (> $this->minValue)"; | ||||||
|  |       } else if ($hasMax) { | ||||||
|  |         $typeName .= " (< $this->maxValue)"; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return $typeName; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   public function toString(): string { | ||||||
|  |     $typeName = $this->getTypeName(); | ||||||
|  |     $str = "$typeName $this->name"; | ||||||
|  |     $defaultValue = (is_null($this->value) ? 'NULL' : $this->value); | ||||||
|  |     if ($this->optional) { | ||||||
|  |       $str = "[$str = $defaultValue]"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return $str; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -121,6 +121,8 @@ namespace Core\API { | |||||||
| namespace Core\API\User { | namespace Core\API\User { | ||||||
| 
 | 
 | ||||||
|   use Core\API\Parameter\ArrayType; |   use Core\API\Parameter\ArrayType; | ||||||
|  |   use Core\API\Parameter\FloatType; | ||||||
|  |   use Core\API\Parameter\IntegerType; | ||||||
|   use Core\API\Parameter\Parameter; |   use Core\API\Parameter\Parameter; | ||||||
|   use Core\API\Parameter\StringType; |   use Core\API\Parameter\StringType; | ||||||
|   use Core\API\Template\Render; |   use Core\API\Template\Render; | ||||||
| @ -1311,10 +1313,15 @@ namespace Core\API\User { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   class UploadPicture extends UserAPI { |   class UploadPicture extends UserAPI { | ||||||
|  | 
 | ||||||
|  |     const MIN_SIZE = 150; | ||||||
|  |     const MAX_SIZE = 800; | ||||||
|  | 
 | ||||||
|     public function __construct(Context $context, bool $externalCall = false) { |     public function __construct(Context $context, bool $externalCall = false) { | ||||||
|       // TODO: we should optimize the process here, we need an offset and size parameter to get a quadratic crop of the uploaded image
 |  | ||||||
|       parent::__construct($context, $externalCall, [ |       parent::__construct($context, $externalCall, [ | ||||||
|         "scale" => new Parameter("scale", Parameter::TYPE_FLOAT, true, NULL), |         "x" => new FloatType("x", 0, PHP_FLOAT_MAX, true, NULL), | ||||||
|  |         "y" => new FloatType("y", 0, PHP_FLOAT_MAX, true, NULL), | ||||||
|  |         "size" => new FloatType("size", self::MIN_SIZE, self::MAX_SIZE, true, NULL), | ||||||
|       ]); |       ]); | ||||||
|       $this->loginRequired = true; |       $this->loginRequired = true; | ||||||
|       $this->forbidMethod("GET"); |       $this->forbidMethod("GET"); | ||||||
| @ -1325,64 +1332,31 @@ namespace Core\API\User { | |||||||
|      */ |      */ | ||||||
|     protected function onTransform(\Imagick $im, $uploadDir): bool|string { |     protected function onTransform(\Imagick $im, $uploadDir): bool|string { | ||||||
| 
 | 
 | ||||||
|       $minSize = 75; |  | ||||||
|       $maxSize = 500; |  | ||||||
| 
 |  | ||||||
|       $width = $im->getImageWidth(); |       $width = $im->getImageWidth(); | ||||||
|       $height = $im->getImageHeight(); |       $height = $im->getImageHeight(); | ||||||
|       $doResize = false; |       $maxPossibleSize = min($width, $height); | ||||||
| 
 | 
 | ||||||
|       if ($width < $minSize || $height < $minSize) { |       $cropX = $this->getParam("x"); | ||||||
|         if ($width < $height) { |       $cropY = $this->getParam("y"); | ||||||
|           $newWidth = $minSize; |       $cropSize = $this->getParam("size") ?? $maxPossibleSize; | ||||||
|           $newHeight = intval(($minSize / $width) * $height); | 
 | ||||||
|         } else { |       if ($maxPossibleSize < self::MIN_SIZE) { | ||||||
|           $newHeight = $minSize; |         return $this->createError("Image must be at least " . self::MIN_SIZE . "x" . self::MIN_SIZE); | ||||||
|           $newWidth = intval(($minSize / $height) * $width); |       } else if ($cropSize > self::MAX_SIZE) { | ||||||
|  |         return $this->createError("Crop must be at most " . self::MAX_SIZE . "x" . self::MAX_SIZE); | ||||||
|  |       } else if ($cropSize > $maxPossibleSize) { | ||||||
|  |         return $this->createError("Invalid crop size"); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|         $doResize = true; |       if ($cropX === null) { | ||||||
|       } else if ($width > $maxSize || $height > $maxSize) { |         $cropX = ($width > $height) ? ($width - $height) / 2 : 0; | ||||||
|         if ($width > $height) { |  | ||||||
|           $newWidth = $maxSize; |  | ||||||
|           $newHeight = intval($height * ($maxSize / $width)); |  | ||||||
|         } else { |  | ||||||
|           $newHeight = $maxSize; |  | ||||||
|           $newWidth = intval($width * ($maxSize / $height)); |  | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|         $doResize = true; |       if ($cropY === null) { | ||||||
|       } else { |         $cropY = ($height > $width) ? ($height - $width) / 2 : 0; | ||||||
|         $newWidth = $width; |  | ||||||
|         $newHeight = $height; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if ($width < $minSize || $height < $minSize) { |  | ||||||
|         return $this->createError("Error processing image. Bad dimensions."); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if ($doResize) { |  | ||||||
|         $width = $newWidth; |  | ||||||
|         $height = $newHeight; |  | ||||||
|         $im->resizeImage($width, $height, \Imagick::FILTER_SINC, 1); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       $size = $this->getParam("size"); |  | ||||||
|       if (is_null($size)) { |  | ||||||
|         $size = min($width, $height); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       $offset = [$this->getParam("offsetX"), $this->getParam("offsetY")]; |  | ||||||
|       if ($size < $minSize or $size > $maxSize) { |  | ||||||
|         return $this->createError("Invalid size. Must be in range of $minSize-$maxSize."); |  | ||||||
|       }/* else if ($offset[0] < 0 || $offset[1] < 0 || $offset[0]+$size > $width ||  $offset[1]+$size > $height) { |  | ||||||
|         return $this->createError("Offsets out of bounds."); |  | ||||||
|       }*/ |  | ||||||
| 
 |  | ||||||
|       if ($offset[0] !== 0 || $offset[1] !== 0 || $size !== $width || $size !== $height) { |  | ||||||
|         $im->cropImage($size, $size, $offset[0], $offset[1]); |  | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       $im->cropImage($cropSize, $cropSize, $cropX, $cropY); | ||||||
|       $fileName = uuidv4() . ".jpg"; |       $fileName = uuidv4() . ".jpg"; | ||||||
|       $im->writeImage("$uploadDir/$fileName"); |       $im->writeImage("$uploadDir/$fileName"); | ||||||
|       $im->destroy(); |       $im->destroy(); | ||||||
|  | |||||||
| @ -58,8 +58,13 @@ return [ | |||||||
|   "user_list_placeholder" => "Keine Benutzer zum Anzeigen", |   "user_list_placeholder" => "Keine Benutzer zum Anzeigen", | ||||||
| 
 | 
 | ||||||
|   # profile picture
 |   # profile picture
 | ||||||
|  |   "remove_picture" => "Profilbild entfernen", | ||||||
|  |   "remove_picture_text" => "Möchten Sie wirklich Ihr aktuelles Profilbild entfernen?", | ||||||
|   "change_picture" => "Profilbild ändern", |   "change_picture" => "Profilbild ändern", | ||||||
|   "profile_picture_of" => "Profilbild von", |   "profile_picture_of" => "Profilbild von", | ||||||
|  |   "change_picture_title" => "Profilbild ändern", | ||||||
|  |   "change_picture_text" => "Wähle ein Profilbild aus und passe die sichtbare Fläche an.", | ||||||
|  |   "profile_picture_invalid_dimensions" => "Das Profilbild muss mindestens 150x150px groß sein.", | ||||||
| 
 | 
 | ||||||
|   # dialogs
 |   # dialogs
 | ||||||
|   "fetch_group_members_error" => "Fehler beim Holen der Gruppenmitglieder", |   "fetch_group_members_error" => "Fehler beim Holen der Gruppenmitglieder", | ||||||
|  | |||||||
| @ -60,8 +60,13 @@ return [ | |||||||
|   "user_list_placeholder" => "No users to display", |   "user_list_placeholder" => "No users to display", | ||||||
| 
 | 
 | ||||||
|   # profile picture
 |   # profile picture
 | ||||||
|  |   "remove_picture" => "Remove profile picture", | ||||||
|  |   "remove_picture_text" => "Do you really want to remove your current profile picture?", | ||||||
|   "change_picture" => "Change profile picture", |   "change_picture" => "Change profile picture", | ||||||
|   "profile_picture_of" => "Profile Picture of", |   "profile_picture_of" => "Profile Picture of", | ||||||
|  |   "change_picture_title" => "Change Profile picture", | ||||||
|  |   "change_picture_text" => "Choose a profile picture and adjust the visible area.", | ||||||
|  |   "profile_picture_invalid_dimensions" => "The profile picture must have at least a size of 150x150.", | ||||||
| 
 | 
 | ||||||
|   # dialogs
 |   # dialogs
 | ||||||
|   "fetch_group_members_error" => "Error fetching group members", |   "fetch_group_members_error" => "Error fetching group members", | ||||||
|  | |||||||
| @ -2,8 +2,9 @@ | |||||||
|   "name": "admin-panel", |   "name": "admin-panel", | ||||||
|   "version": "1.0.0", |   "version": "1.0.0", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "shared": "link:../shared", |     "react": "^18.2.0", | ||||||
|     "react": "^18.2.0" |     "react-image-crop": "^11.0.5", | ||||||
|  |     "shared": "link:../shared" | ||||||
|   }, |   }, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "HTTPS=true react-app-rewired start" |     "dev": "HTTPS=true react-app-rewired start" | ||||||
|  | |||||||
| @ -1,16 +1,32 @@ | |||||||
| import {Box, Button, CircularProgress, Slider, styled} from "@mui/material"; | import { | ||||||
| import {useCallback, useContext, useRef, useState} from "react"; |     Box, | ||||||
|  |     Button, | ||||||
|  |     CircularProgress, | ||||||
|  |     Dialog, DialogActions, | ||||||
|  |     DialogContent, | ||||||
|  |     DialogContentText, | ||||||
|  |     DialogTitle, | ||||||
|  |     styled, TextField | ||||||
|  | } from "@mui/material"; | ||||||
|  | import {useCallback, useContext, useState} from "react"; | ||||||
| import {LocaleContext} from "shared/locale"; | import {LocaleContext} from "shared/locale"; | ||||||
| import PreviewProfilePicture from "./preview-picture"; | import {Delete, Edit, Upload} from "@mui/icons-material"; | ||||||
| import {Delete, Edit} from "@mui/icons-material"; |  | ||||||
| import ProfilePicture from "shared/elements/profile-picture"; | import ProfilePicture from "shared/elements/profile-picture"; | ||||||
|  | import ReactCrop from 'react-image-crop' | ||||||
|  | 
 | ||||||
|  | import 'react-image-crop/dist/ReactCrop.css'; | ||||||
| 
 | 
 | ||||||
| const ProfilePictureBox = styled(Box)((props) => ({ | const ProfilePictureBox = styled(Box)((props) => ({ | ||||||
|     padding: props.theme.spacing(2), |     padding: props.theme.spacing(1), | ||||||
|     display: "grid", |     display: "grid", | ||||||
|     gridTemplateRows: "auto 60px", |     gridTemplateRows: "auto calc(110px - " + props.theme.spacing(1) + ")", | ||||||
|     gridGap: props.theme.spacing(2), |  | ||||||
|     textAlign: "center", |     textAlign: "center", | ||||||
|  |     alignItems: "center", | ||||||
|  |     justifyItems: "center", | ||||||
|  |     "& img": { | ||||||
|  |         maxHeight: 150, | ||||||
|  |         width: "auto", | ||||||
|  |     } | ||||||
| })); | })); | ||||||
| 
 | 
 | ||||||
| const VerticalButtonBar = styled(Box)((props) => ({ | const VerticalButtonBar = styled(Box)((props) => ({ | ||||||
| @ -24,67 +40,46 @@ export default function EditProfilePicture(props) { | |||||||
| 
 | 
 | ||||||
|     // meta
 |     // meta
 | ||||||
|     const {translate: L} = useContext(LocaleContext); |     const {translate: L} = useContext(LocaleContext); | ||||||
|     // const [scale, setScale] = useState(100);
 |  | ||||||
|     const scale = useRef(100); |  | ||||||
|     const {api, showDialog, setProfile, profile, setDialogData, ...other} = props |     const {api, showDialog, setProfile, profile, setDialogData, ...other} = props | ||||||
| 
 | 
 | ||||||
|     const onUploadPicture = useCallback((data) => { |     // data
 | ||||||
|         api.uploadPicture(data, scale.current / 100.0).then((res) => { |     const [crop, setCrop] = useState({ unit: 'px' }); | ||||||
|             if (!res.success) { |     const [image, setImage] = useState({ loading: false, data: null, file: null }); | ||||||
|                 showDialog(res.msg, L("Error uploading profile picture")); | 
 | ||||||
|             } else { |     // ui
 | ||||||
|  |     const [isUploading, setUploading] = useState(false); | ||||||
|  | 
 | ||||||
|  |     const onCloseDialog = useCallback((event = null, reason = null) => { | ||||||
|  |         if (!reason || !["backdropClick", "escapeKeyDown"].includes(reason)) { | ||||||
|  |             setImage({loading: false, data: null, file: null}); | ||||||
|  |         } | ||||||
|  |     }, []); | ||||||
|  | 
 | ||||||
|  |     const onUploadPicture = useCallback(() => { | ||||||
|  |         if (!isUploading) { | ||||||
|  |             setUploading(true); | ||||||
|  |             api.uploadPicture(image.file, crop.width, crop.x, crop.y).then(res => { | ||||||
|  |                 setUploading(false); | ||||||
|  |                 if (res.success) { | ||||||
|  |                     onCloseDialog(); | ||||||
|                     setProfile({...profile, profilePicture: res.profilePicture}); |                     setProfile({...profile, profilePicture: res.profilePicture}); | ||||||
|  |                 } else { | ||||||
|  |                     showDialog(res.msg, L("account.upload_profile_picture_error")); | ||||||
|                 } |                 } | ||||||
|             }) |             }) | ||||||
|     }, [api, scale.current, showDialog, profile]); |         } | ||||||
|  |     }, [api, image, crop, isUploading, showDialog, profile, onCloseDialog]); | ||||||
| 
 | 
 | ||||||
|     const onRemoveImage = useCallback(() => { |     const onRemoveImage = useCallback(() => { | ||||||
|         api.removePicture().then((res) => { |         api.removePicture().then((res) => { | ||||||
|             if (!res.success) { |             if (!res.success) { | ||||||
|                 showDialog(res.msg, L("Error removing profile picture")); |                 showDialog(res.msg, L("account.remove_profile_picture_error")); | ||||||
|             } else { |             } else { | ||||||
|                 setProfile({...profile, profilePicture: null}); |                 setProfile({...profile, profilePicture: null}); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     }, [api, showDialog, profile]); |     }, [api, showDialog, profile]); | ||||||
| 
 | 
 | ||||||
|     const onOpenDialog = useCallback((file = null, data = null) => { |  | ||||||
| 
 |  | ||||||
|         let img = null; |  | ||||||
|         if (data !== null) { |  | ||||||
|             img = new Image(); |  | ||||||
|             img.src = data; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         setDialogData({ |  | ||||||
|             show: true, |  | ||||||
|             title: L("account.change_picture_title"), |  | ||||||
|             text: L("account.change_picture_text"), |  | ||||||
|             options: data === null ? [L("general.cancel")] : [L("general.apply"), L("general.cancel")], |  | ||||||
|             inputs: data === null ? [{ |  | ||||||
|                 key: "pfp-loading", |  | ||||||
|                 type: "custom", |  | ||||||
|                 element: CircularProgress, |  | ||||||
|             }] : [ |  | ||||||
|                 { |  | ||||||
|                     key: "pfp-preview", |  | ||||||
|                     type: "custom", |  | ||||||
|                     element: PreviewProfilePicture, |  | ||||||
|                     img: img, |  | ||||||
|                     scale: scale.current, |  | ||||||
|                     setScale: (v) => scale.current = v, |  | ||||||
|                 }, |  | ||||||
|             ], |  | ||||||
|             onOption: (option) => { |  | ||||||
|                 if (option === 1 && file) { |  | ||||||
|                     onUploadPicture(file) |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 // scale.current = 100;
 |  | ||||||
|             } |  | ||||||
|         }) |  | ||||||
|     }, [setDialogData, onUploadPicture]); |  | ||||||
| 
 |  | ||||||
|     const onSelectImage = useCallback(() => { |     const onSelectImage = useCallback(() => { | ||||||
|         let fileInput = document.createElement("input"); |         let fileInput = document.createElement("input"); | ||||||
|         fileInput.type = "file"; |         fileInput.type = "file"; | ||||||
| @ -94,18 +89,40 @@ export default function EditProfilePicture(props) { | |||||||
|             if (file) { |             if (file) { | ||||||
|                 let reader = new FileReader(); |                 let reader = new FileReader(); | ||||||
|                 reader.onload = function (e) { |                 reader.onload = function (e) { | ||||||
|                     onOpenDialog(file, e.target.result); |                     const imageData = e.target.result; | ||||||
|  |                     const img = new Image(); | ||||||
|  |                     img.src = imageData; | ||||||
|  |                     img.onload = () => { | ||||||
|  |                         let croppedSize; | ||||||
|  |                         if (img.width > img.height) { | ||||||
|  |                             croppedSize = Math.min(800, img.height); | ||||||
|  |                             setCrop({ x: (img.width - img.height) / 2, y: 0, unit: "px", width: croppedSize, height: croppedSize }); | ||||||
|  |                         } else if (img.width < img.height) { | ||||||
|  |                             croppedSize = Math.min(800, img.width); | ||||||
|  |                             setCrop({ x: 0, y: (img.height - img.width) / 2, unit: "px", width: croppedSize, height: croppedSize }); | ||||||
|  |                         } else { | ||||||
|  |                             croppedSize = Math.min(800, img.width); | ||||||
|  |                             setCrop({ x: 0, y: 0, unit: "px", width: croppedSize, height: croppedSize }); | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
|                 onOpenDialog(); |                         if (croppedSize < 150) { | ||||||
|  |                             setImage({ loading: false, file: null, data: null }); | ||||||
|  |                             showDialog(L("account.profile_picture_invalid_dimensions"), L("general.error")); | ||||||
|  |                         } else { | ||||||
|  |                             setImage({ loading: false, file: file, data: imageData }); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 setImage({ file: null, data: null, loading: true }); | ||||||
|                 reader.readAsDataURL(file); |                 reader.readAsDataURL(file); | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|         fileInput.click(); |         fileInput.click(); | ||||||
|     }, [onOpenDialog]); |     }, [showDialog]); | ||||||
| 
 | 
 | ||||||
| 
 |     return <> | ||||||
|     return <ProfilePictureBox {...other}> |         <ProfilePictureBox {...other}> | ||||||
|             <ProfilePicture user={profile} onClick={onSelectImage} /> |             <ProfilePicture user={profile} onClick={onSelectImage} /> | ||||||
|             <VerticalButtonBar> |             <VerticalButtonBar> | ||||||
|                 <Button variant="outlined" size="small" |                 <Button variant="outlined" size="small" | ||||||
| @ -118,9 +135,9 @@ export default function EditProfilePicture(props) { | |||||||
|                         startIcon={<Delete />} color={"error"} |                         startIcon={<Delete />} color={"error"} | ||||||
|                         onClick={() => setDialogData({ |                         onClick={() => setDialogData({ | ||||||
|                             show: true, |                             show: true, | ||||||
|                         title: L("account.picture_remove_title"), |                             title: L("account.remove_picture"), | ||||||
|                         message: L("account.picture_remove_text"), |                             message: L("account.remove_picture_text"), | ||||||
|                         options: [L("general.confirm"), L("general.cancel")], |                             options: [L("general.cancel"), L("general.confirm")], | ||||||
|                             onOption: (option) => option === 1 ? onRemoveImage() : true |                             onOption: (option) => option === 1 ? onRemoveImage() : true | ||||||
|                         })}> |                         })}> | ||||||
|                         {L("account.remove_picture")} |                         {L("account.remove_picture")} | ||||||
| @ -128,4 +145,35 @@ export default function EditProfilePicture(props) { | |||||||
|                 } |                 } | ||||||
|             </VerticalButtonBar> |             </VerticalButtonBar> | ||||||
|         </ProfilePictureBox> |         </ProfilePictureBox> | ||||||
|  |         <Dialog open={image.loading || image.data !== null} maxWidth={"lg"} | ||||||
|  |             onClose={onCloseDialog}> | ||||||
|  |             <DialogTitle> | ||||||
|  |                 {L("account.change_picture_title")} | ||||||
|  |             </DialogTitle> | ||||||
|  |             <DialogContent> | ||||||
|  |                 <DialogContentText> | ||||||
|  |                     {L("account.change_picture_text")} | ||||||
|  |                 </DialogContentText> | ||||||
|  |                 {image.data ? | ||||||
|  |                     <ReactCrop onChange={c => setCrop(c)} crop={crop} keepSelection={true} | ||||||
|  |                                aspect={1} circularCrop={true} disabled={isUploading} | ||||||
|  |                                maxWidth={800} maxHeight={800} minWidth={150} minHeight={150}> | ||||||
|  |                         <img src={image?.data} alt={"preview"} /> | ||||||
|  |                     </ReactCrop> : | ||||||
|  |                     <CircularProgress /> | ||||||
|  |                 } | ||||||
|  |             </DialogContent> | ||||||
|  |             <DialogActions> | ||||||
|  |                 <Button variant={"outlined"} color={"error"} onClick={onCloseDialog} | ||||||
|  |                         disabled={isUploading}> | ||||||
|  |                     {L("general.cancel")} | ||||||
|  |                 </Button> | ||||||
|  |                 <Button variant={"outlined"} type={"submit"} onClick={onUploadPicture} | ||||||
|  |                         disabled={isUploading} | ||||||
|  |                         startIcon={isUploading ? <CircularProgress size={12} /> : <Upload />}> | ||||||
|  |                     {L(isUploading ? "general.uploading" : "general.submit")} | ||||||
|  |                 </Button> | ||||||
|  |             </DialogActions> | ||||||
|  |         </Dialog> | ||||||
|  |     </> | ||||||
| } | } | ||||||
| @ -187,9 +187,11 @@ export default class API { | |||||||
|         return res; |         return res; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async uploadPicture(file, scale=1.0) { |     async uploadPicture(file, size, x = 0, y = 0) { | ||||||
|         const formData = new FormData(); |         const formData = new FormData(); | ||||||
|         formData.append("scale", scale); |         formData.append("size", size); | ||||||
|  |         formData.append("x", x); | ||||||
|  |         formData.append("y", y); | ||||||
|         formData.append("picture", file, file.name); |         formData.append("picture", file, file.name); | ||||||
|         let res = await this.apiCall("user/uploadPicture", formData); |         let res = await this.apiCall("user/uploadPicture", formData); | ||||||
|         if (res.success) { |         if (res.success) { | ||||||
|  | |||||||
| @ -13,6 +13,8 @@ const PicturePlaceholderBox = styled(Box)((props) => ({ | |||||||
|     alignItems: "center", |     alignItems: "center", | ||||||
|     background: "radial-gradient(circle closest-side, gray 98%, transparent 100%);", |     background: "radial-gradient(circle closest-side, gray 98%, transparent 100%);", | ||||||
|     containerType: "inline-size", |     containerType: "inline-size", | ||||||
|  |     width: "100%", | ||||||
|  |     height: "100%", | ||||||
|     "& > span": { |     "& > span": { | ||||||
|         textAlign: "center", |         textAlign: "center", | ||||||
|         fontSize: "30cqw", |         fontSize: "30cqw", | ||||||
|  | |||||||
| @ -9112,6 +9112,11 @@ react-error-overlay@^6.0.11: | |||||||
|   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" |   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" | ||||||
|   integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== |   integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== | ||||||
| 
 | 
 | ||||||
|  | react-image-crop@^11.0.5: | ||||||
|  |   version "11.0.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-11.0.5.tgz#c7abcf9cae28305d253d55d481158a594a937867" | ||||||
|  |   integrity sha512-A/Y/kspOzki1zDL/bSgwWIY1X3CQ9F1QwpdnncWLBVAktnKfAZDIQnWmjXzuzEjZHDMsBlArytIcPBVi6DNklg== | ||||||
|  | 
 | ||||||
| react-is@^16.13.1, react-is@^16.7.0: | react-is@^16.13.1, react-is@^16.7.0: | ||||||
|   version "16.13.1" |   version "16.13.1" | ||||||
|   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" |   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user