]> git.agnieray.net Git - galette.git/commitdiff
Resize and crop members picture to a fixed ratio
authorGuillaume AGNIERAY <dev@agnieray.net>
Mon, 9 Oct 2023 17:35:06 +0000 (19:35 +0200)
committerJohan Cwiklinski <johan@x-tnd.be>
Thu, 12 Oct 2023 15:33:19 +0000 (17:33 +0200)
closes #1717

Add cropping options in settings parameters tab
Add cropping focus selection on member form
Add cropping on resizeImage()
Minimum image dimensions required from cropping

Restore and extend drag and drop picture feature
Clean CSS and template

14 files changed:
galette/lib/Galette/Controllers/AjaxController.php
galette/lib/Galette/Controllers/Crud/MembersController.php
galette/lib/Galette/Core/Picture.php
galette/lib/Galette/Core/Preferences.php
galette/lib/Galette/Entity/Adherent.php
galette/lib/Galette/IO/FileInterface.php
galette/lib/Galette/IO/FileTrait.php
galette/templates/default/components/forms/picture.html.twig
galette/templates/default/elements/js/photo_dnd.js.twig
galette/templates/default/elements/member_card.html.twig
galette/templates/default/pages/member_form.html.twig
galette/templates/default/pages/members_public_gallery.html.twig
galette/templates/default/pages/preferences.html.twig
ui/semantic/galette/views/item.overrides

index ab71736560c7a65a3b05e771737c85934233dca5..504e710a665013ca7745dda1c2990f1c4b856689 100644 (file)
@@ -108,6 +108,10 @@ class AjaxController extends AbstractController
         $mid = $post['member_id'];
         $fsize = $post['filesize'];
         $fname = $post['filename'];
+        $cropping = null;
+        if ($post['cropping'] != false) {
+            $cropping = $post['cropping'];
+        }
         $tmpname = GALETTE_TEMPIMAGES_PATH . 'ajax_upload_' . $fname;
 
         $temp = explode('base64,', $post['file']);
@@ -126,7 +130,8 @@ class AjaxController extends AbstractController
                 'tmp_name'  => $tmpname,
                 'size'      => $fsize
             ),
-            true
+            true,
+            $cropping
         );
 
         if ($res < 0) {
index f987beca73b298d323731fcefc45928717de0702..23e3f9b124a5d5df52e3976afde555b8e84aef5d 100644 (file)
@@ -1596,7 +1596,13 @@ class MembersController extends CrudController
             }
 
             if (count($error_detected) === 0) {
-                $files_res = $member->handleFiles($_FILES);
+                $cropping = null;
+                if ($this->preferences->pref_force_picture_ratio == 1) {
+                    $cropping = [];
+                    $cropping['ratio'] = isset($this->preferences->pref_member_picture_ratio) ? $this->preferences->pref_member_picture_ratio : 'square_ratio';
+                    $cropping['focus'] = isset($post['crop_focus']) ? $post['crop_focus'] : 'center';
+                }
+                $files_res = $member->handleFiles($_FILES, $cropping);
                 if (is_array($files_res)) {
                     $error_detected = array_merge($error_detected, $files_res);
                 }
index cf5339db0afd705f5ef7a34a57cad507b526ced1..27508f089deef87a559bbda2ac86b0ba97505b27 100644 (file)
@@ -425,12 +425,13 @@ class Picture implements FileInterface
     /**
      * Stores an image on the disk and in the database
      *
-     * @param object  $file the uploaded file
-     * @param boolean $ajax If the image cames from an ajax call (dnd)
+     * @param object  $file     The uploaded file
+     * @param boolean $ajax     If the image cames from an ajax call (dnd)
+     * @param array   $cropping Cropping properties
      *
      * @return bool|int
      */
-    public function store($file, $ajax = false)
+    public function store($file, $ajax = false, $cropping = null)
     {
         /** TODO: fix max size (by preferences ?) */
         global $zdb;
@@ -508,6 +509,22 @@ class Picture implements FileInterface
             );
         }
 
+        // Source image must have minimum dimensions to match the cropping process requirements
+        // and ensure the final picture will fit the maximum allowed resizing dimensions.
+        if (isset($cropping['ratio']) && isset($cropping['focus'])) {
+            if ($current[0] < $this->mincropsize || $current[1] < $this->mincropsize) {
+                $min_current = min($current[0], $current[1]);
+                Analog::log(
+                    '[' . $class . '] Image is too small. The minimum image side size allowed is ' .
+                    $this->mincropsize . 'px, but current is ' . $min_current . 'px.',
+                    Analog::ERROR
+                );
+                return self::IMAGE_TOO_SMALL;
+            } else {
+                Analog::log('[' . $class . '] Image dimensions are OK, proceed', Analog::DEBUG);
+            }
+        }
+
         $this->delete();
 
         $new_file = $this->store_path .
@@ -522,7 +539,7 @@ class Picture implements FileInterface
         if ($current[0] > $this->max_width || $current[1] > $this->max_height) {
             /** FIXME: what if image cannot be resized?
                 Should'nt we want to stop the process here? */
-            $this->resizeImage($new_file, $extension);
+            $this->resizeImage($new_file, $extension, null, $cropping);
         }
 
         return $this->storeInDb($zdb, $this->db_id, $new_file, $extension);
@@ -678,16 +695,17 @@ class Picture implements FileInterface
     }
 
     /**
-     * Resize the image if it exceeds max allowed sizes
+     * Resize and eventually crop the image if it exceeds max allowed sizes
      *
-     * @param string $source the source image
-     * @param string $ext    file's extension
-     * @param string $dest   the destination image.
-     *                       If null, we'll use the source image. Defaults to null
+     * @param string $source   The source image
+     * @param string $ext      File's extension
+     * @param string $dest     The destination image.
+     *                         If null, we'll use the source image. Defaults to null
+     * @param array  $cropping Cropping properties
      *
      * @return void|false
      */
-    private function resizeImage($source, $ext, $dest = null)
+    private function resizeImage($source, $ext, $dest = null, $cropping = null)
     {
         $class = get_class($this);
 
@@ -750,39 +768,144 @@ class Picture implements FileInterface
 
             $ratio = $cur_width / $cur_height;
 
-            // calculate image size according to ratio
-            if ($cur_width > $cur_height) {
-                $h = round($w / $ratio);
+            // Define cropping variables if necessary.
+            $thumb_cropped = false;
+            // Cropping is based on the smallest side of the source in order to
+            // provide as less focusing options as possible if the source doesn't
+            // fit the final ratio (center, top, bottom, left, right).
+            $min_size = min($cur_width, $cur_height);
+            // Cropping dimensions.
+            $crop_width = $min_size;
+            $crop_height = $min_size;
+            // Cropping focus.
+            $crop_x = 0;
+            $crop_y = 0;
+            if (isset($cropping['ratio']) && isset($cropping['focus'])) {
+                // Calculate cropping dimensions
+                switch ($cropping['ratio']) {
+                    case 'portrait_ratio':
+                        // Calculate cropping dimensions
+                        if ($ratio < 1) {
+                            $crop_height = ceil($crop_width * 4 / 3);
+                        } else {
+                            $crop_width = ceil($crop_height * 3 / 4);
+                        }
+                        // Calculate resizing dimensions
+                        $w = ceil($h * 3 / 4);
+                        break;
+                    case 'landscape_ratio':
+                        // Calculate cropping dimensions
+                        if ($ratio > 1) {
+                            $crop_width = ceil($crop_height * 4 / 3);
+                        } else {
+                            $crop_height = ceil($crop_width * 3 / 4);
+                        }
+                        // Calculate resizing dimensions
+                        $h = ceil($w * 3 / 4);
+                        break;
+                }
+                // Calculate focus coordinates
+                switch ($cropping['focus']) {
+                    case 'center':
+                        if ($ratio > 1) {
+                            $crop_x = ceil(($cur_width - $crop_width) / 2);
+                        } elseif ($ratio == 1) {
+                            $crop_x = ceil(($cur_width - $crop_width) / 2);
+                            $crop_y = ceil(($cur_height - $crop_height) / 2);
+                        } else {
+                            $crop_y = ceil(($cur_height - $crop_height) / 2);
+                        }
+                        break;
+                    case 'top':
+                        $crop_x = ceil(($cur_width - $crop_width) / 2);
+                        break;
+                    case 'bottom':
+                        $crop_y = $cur_height - $crop_height;
+                        break;
+                    case 'right':
+                        $crop_x = $cur_width - $crop_width;
+                        break;
+                }
+                // Cropped image.
+                $thumb_cropped = imagecreatetruecolor($crop_width, $crop_height);
+                // Cropped ratio.
+                $ratio = $crop_width / $crop_height;
+            // Otherwise, calculate image size according to the source's ratio.
             } else {
-                $w = round($h * $ratio);
+                if ($cur_width > $cur_height) {
+                    $h = round($w / $ratio);
+                } else {
+                    $w = round($h * $ratio);
+                }
             }
 
+            // Resized image.
             $thumb = imagecreatetruecolor($w, $h);
+
+            $image = false;
             switch ($ext) {
                 case 'jpg':
                     $image = imagecreatefromjpeg($source);
-                    imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
+                    // Crop
+                    if ($thumb_cropped !== false) {
+                        // First, crop.
+                        imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
+                        // Then, resize.
+                        imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
+                    // Resize
+                    } else {
+                        imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
+                    }
                     imagejpeg($thumb, $dest);
                     break;
                 case 'png':
                     $image = imagecreatefrompng($source);
                     // Turn off alpha blending and set alpha flag. That prevent alpha
                     // transparency to be saved as an arbitrary color (black in my tests)
-                    imagealphablending($thumb, false);
                     imagealphablending($image, false);
-                    imagesavealpha($thumb, true);
                     imagesavealpha($image, true);
-                    imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
+                    imagealphablending($thumb, false);
+                    imagesavealpha($thumb, true);
+                    // Crop
+                    if ($thumb_cropped !== false) {
+                        imagealphablending($thumb_cropped, false);
+                        imagesavealpha($thumb_cropped, true);
+                        // First, crop.
+                        imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
+                        // Then, resize.
+                        imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
+                    // Resize
+                    } else {
+                        imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
+                    }
                     imagepng($thumb, $dest);
                     break;
                 case 'gif':
                     $image = imagecreatefromgif($source);
-                    imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
+                    // Crop
+                    if ($thumb_cropped !== false) {
+                        // First, crop.
+                        imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
+                        // Then, resize.
+                        imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
+                    // Resize
+                    } else {
+                        imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
+                    }
                     imagegif($thumb, $dest);
                     break;
                 case 'webp':
                     $image = imagecreatefromwebp($source);
-                    imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
+                    // Crop
+                    if ($thumb_cropped !== false) {
+                        // First, crop.
+                        imagecopyresampled($thumb_cropped, $image, 0, 0, $crop_x, $crop_y, $cur_width, $cur_height, $cur_width, $cur_height);
+                        // Then, resize.
+                        imagecopyresampled($thumb, $thumb_cropped, 0, 0, 0, 0, $w, $h, $crop_width, $crop_height);
+                    // Resize
+                    } else {
+                        imagecopyresampled($thumb, $image, 0, 0, 0, 0, $w, $h, $cur_width, $cur_height);
+                    }
                     imagewebp($thumb, $dest);
                     break;
             }
index 60d18d9aa2b1f7db97d5461e15fde99893c09517..e3bedf85b93f8053f40dc121069b5d4d41e4ea07 100644 (file)
@@ -102,6 +102,8 @@ use Galette\Repository\Members;
  * @property string $pref_etiq_rows
  * @property string $pref_etiq_corps
  * @property boolean $pref_etiq_border
+ * @property boolean $pref_force_picture_ratio
+ * @property string $pref_member_picture_ratio
  * @property string $pref_card_abrev
  * @property string $pref_card_strip
  * @property string $pref_card_tcol
@@ -240,6 +242,8 @@ class Preferences
         'pref_etiq_corps'    =>    12,
         'pref_etiq_border'    =>    true,
         /* Preferences for members cards */
+        'pref_force_picture_ratio'    =>    false,
+        'pref_member_picture_ratio'    =>    'square_ratio',
         'pref_card_abrev'    =>    'GALETTE',
         'pref_card_strip'    =>    'Gestion d\'Adherents en Ligne Extrêmement Tarabiscotée',
         'pref_card_tcol'    =>    '#FFFFFF',
index 851518538c9221b6be2877fbda00dbe5fb1fccd3..769c517e6c9d6ce2ef67a077b9e24e4825bf2e86 100644 (file)
@@ -2040,11 +2040,12 @@ class Adherent
     /**
      * Handle files (photo and dynamics files)
      *
-     * @param array $files Files sent
+     * @param array $files    Files sent
+     * @param array $cropping Cropping properties
      *
      * @return array|true
      */
-    public function handleFiles(array $files)
+    public function handleFiles(array $files, array $cropping = null)
     {
         $this->errors = [];
         // picture upload
@@ -2052,7 +2053,11 @@ class Adherent
             if ($files['photo']['error'] === UPLOAD_ERR_OK) {
                 if ($files['photo']['tmp_name'] != '') {
                     if (is_uploaded_file($files['photo']['tmp_name'])) {
-                        $res = $this->picture->store($files['photo']);
+                        if ($this->preferences->pref_force_picture_ratio == 1 && isset($cropping)) {
+                            $res = $this->picture->store($files['photo'], false, $cropping);
+                        } else {
+                            $res = $this->picture->store($files['photo']);
+                        }
                         if ($res < 0) {
                             $this->errors[]
                                 = $this->picture->getErrorMessage($res);
index d3827ae540d5f93c88283ec8c4ea33ecd6790ba7..940f97592c6af97a1e00ac36aa5b9602577635f7 100644 (file)
@@ -54,9 +54,11 @@ interface FileInterface
     public const INVALID_FILENAME = -1;
     public const INVALID_EXTENSION = -2;
     public const FILE_TOO_BIG = -3;
-    public const MIME_NOT_ALLOWED = -4;
-    public const NEW_FILE_EXISTS = -5;
-    public const INVALID_FILE = -6;
-    public const CANT_WRITE = -7;
+    public const IMAGE_TOO_SMALL = -4;
+    public const MIME_NOT_ALLOWED = -5;
+    public const NEW_FILE_EXISTS = -6;
+    public const INVALID_FILE = -7;
+    public const CANT_WRITE = -8;
     public const MAX_FILE_SIZE = 2048;
+    public const MIN_CROP_SIZE = 267;
 }
index 47fa635049ec5f44ee4c781fded14defd1c065b5..6b79e2a44150d32b12b3ff04dbc55f5edb70b50c 100644 (file)
@@ -76,6 +76,7 @@ trait FileTrait
     protected $allowed_extensions = array();
     protected $allowed_mimes = array();
     protected $maxlenght;
+    protected $mincropsize;
 
     public static $mime_types = array(
         'txt'       => 'text/plain',
@@ -181,10 +182,11 @@ trait FileTrait
     /**
      * Initialization
      *
-     * @param string $dest       File destination directory
-     * @param array  $extensions Array of permitted extensions
-     * @param array  $mimes      Array of permitted mime types
-     * @param int    $maxlenght  Maximum lenght for each file
+     * @param string $dest        File destination directory
+     * @param array  $extensions  Array of permitted extensions
+     * @param array  $mimes       Array of permitted mime types
+     * @param int    $maxlenght   Maximum lenght for each file
+     * @param int    $mincropsize Minimum image side size required for cropping
      *
      * @return void
      */
@@ -192,7 +194,8 @@ trait FileTrait
         $dest,
         $extensions = null,
         $mimes = null,
-        $maxlenght = null
+        $maxlenght = null,
+        $mincropsize = null
     ) {
         if ($dest !== null && substr($dest, -1) !== '/') {
             //normalize path
@@ -210,6 +213,11 @@ trait FileTrait
         } else {
             $this->maxlenght = self::MAX_FILE_SIZE;
         }
+        if ($mincropsize !== null) {
+            $this->mincropsize = $mincropsize;
+        } else {
+            $this->mincropsize = self::MIN_CROP_SIZE;
+        }
     }
 
     /**
@@ -492,6 +500,12 @@ trait FileTrait
                     _T("File is too big. Maximum allowed size is %dKo")
                 );
                 break;
+            case self::IMAGE_TOO_SMALL:
+                $error = sprintf(
+                    _T("Image is too small. The minimum image side size allowed is %spx"),
+                    $this->mincropsize
+                );
+                break;
             case self::MIME_NOT_ALLOWED:
                 /** FIXME: should be more descriptive */
                 $error = _T("Mime-Type not allowed");
index 2f861974012ad41181704d7fdb073c03c3b4d8f5..537bebf1a2b134783aabfcc655b4fa3f79949140 100644 (file)
@@ -1,30 +1,40 @@
 <div class="field ui items">
     <label>{{ _T("Picture:") }}</label>
     <div class="item">
-    {% if member.id %}
-        <div class="image">
-        {% set photo_id = member.id %}
-            <img id="photo_adh" src="{{ url_for("photo", {"id": photo_id, "rand": time}) }}" class="picture" width="{{ member.picture.getOptimalWidth() }}" height="{{ member.picture.getOptimalHeight() }}" alt="{{ _T("Picture") }}"/>
-        </div>
-    {% endif %}
         <div class="content">
+    {% if member.hasPicture() == 1 %}
+            <div class="extra ui basic fitted segment">
+                <div class="ui toggle checkbox">
+                    <input type="checkbox" name="del_photo" id="del_photo" value="1"/>
+                    <label for="del_photo" class="labelalign">{{ _T("Delete image") }}</label>
+                </div>
+            </div>
+    {% endif %}
             <div class="description">
                 <div class="ui file action input">
-                    <input type="file" name="photo"/>
-                    <label for="photo" class="ui button{% if constant('GALETTE_MODE') == constant('\\Galette\\Core\\Galette::MODE_DEMO') %} disabled{% endif %}">
+                    <input id="photo_new" type="file" name="photo"/>
+                    <label for="photo_new" class="ui button{% if constant('GALETTE_MODE') == constant('\\Galette\\Core\\Galette::MODE_DEMO') %} disabled{% endif %}">
                         <i class="blue upload icon"></i>
                         {% if member.hasPicture() == 1 %}{{ _T("Choose another file") }}{% else %}{{ _T("Choose a file") }}{% endif %}
                     </label>
                 </div>
             </div>
-            <div class="extra ui basic fitted segment">
-    {% if member.hasPicture() == 1 %}
-                <div class="ui toggle checkbox">
-                    <input type="checkbox" name="del_photo" id="del_photo" value="1"/>
-                    <label for="del_photo" class="labelalign">{{ _T("Delete image") }}</label>
+    {% if preferences.pref_force_picture_ratio == 1 %}
+            {% set system_ratio = (preferences.pref_member_picture_ratio == 'square_ratio') ? _T("Square (1:1)") : (preferences.pref_member_picture_ratio == 'portrait_ratio') ? _T("Portrait (3:4)") : (preferences.pref_member_picture_ratio == 'landscape_ratio') ? _T("Landscape (4:3)") %}
+            <div id="crop_focus_field" class="extra ui basic fitted segment displaynone">
+                <div class="inline field">
+                    <label for="crop_focus">{{ _T("Cropping focus") }}</label>
+                    <select name="crop_focus" id="crop_focus" class="ui dropdown nochosen">
+                        <option value="center">{{ _T("Center") }}</option>
+                        <option value="top">{{ _T("Top") }}</option>
+                        <option value="bottom">{{ _T("Bottom") }}</option>
+                        <option value="left">{{ _T("Left") }}</option>
+                        <option value="right">{{ _T("Right") }}</option>
+                    </select>
+                    <i class="tooltip circular inverted primary small icon info" data-html="{{ _T("Choose the area of the original image to preserve after cropping to the final ratio defined in the settings : %ratio")|replace({"%ratio": system_ratio}) }}"></i>
                 </div>
-    {% endif %}
             </div>
+    {% endif %}
         </div>
     </div>
 </div>
index 104dd5a1b6033a22ab9f26411aeb424e3564d34e..796d4917ded0c26b4a5ec1a0a837e26229518e82 100644 (file)
-    {% if member.id %}
-                //Photo dnd
-                // Check if window.FileReader exists to make
-                // sure the browser supports file uploads
-                if ( typeof(window.FileReader) ) {
-                    var _dz = $('#photo_adh');
+{% if member.id %}
+    //Photo dnd
+    // Check if window.FileReader exists to make
+    // sure the browser supports file uploads
+    if ( typeof(window.FileReader) ) {
+        var _dz = $('#photo_adh');
 
-                    // Add a nice drag effect
-                    _dz[0].ondragover = function() {
-                        _dz.addClass('dndhover');
-                        return false;
-                    };
+        if (_dz[0]) {
+            // Add a nice drag effect
+            _dz[0].ondragover = function() {
+                _dz.css({ opacity: 0.4 });
+                _dz.transition('pulsating');
+                return false;
+            };
 
-                    // Remove the drag effect when stopping our drag
-                    _dz[0].ondragend = function() {
-                        _dz.removeClass('dndhover');
-                        return false;
-                    };
+            // Remove the drag effect when leaving the dropping zone
+            _dz[0].ondragleave = function() {
+                _dz.css({ opacity: 1 });
+                _dz.transition('stop all');
+                return false;
+            };
 
-                    // The drop event handles the file sending
-                    _dz[0].ondrop = function(event) {
-                        // Stop the browser from opening the file in the window
-                        event.preventDefault();
-                        _dz.removeClass('dndhover');
+            // The drop event handles the file sending
+            _dz[0].ondrop = function(event) {
+                // Stop the browser from opening the file in the window
+                event.preventDefault();
+                _dz.css({ opacity: 1 });
+                _dz.transition('stop all');
+                $('.message').remove();
 
-                        var file = event.dataTransfer.files[0];
-                        var reader = new FileReader();
-                        reader.readAsDataURL(file);
+                var file = event.dataTransfer.files[0];
 
-                        reader.onload = function(evt) {
-                            $.ajax({
-                                    type: 'POST',
-                                    dataType: 'json',
-                                    url : '{{ url_for("photoDnd") }}',
-                                    data: {
-                                        member_id: {{ member.id }},
-                                        filename: file.name,
-                                        filesize: file.size,
-                                        file: evt.target.result
-                                    },
-                                    {% include "elements/js/loader.js.twig" %},
-                                    success: function(res){
-                                        if ( res.result == true ) {
-                                            d = new Date();
-                                            var _photo = $('#photo_adh');
-                                            _photo.removeAttr('width').removeAttr('height');
-                                            _photo.attr('src', $('#photo_adh')[0].src + '?' + d.getTime());
-                                        }
+    {% if preferences.pref_force_picture_ratio == 1 %}
+                {% set system_ratio = (preferences.pref_member_picture_ratio == 'square_ratio') ? _T("Square (1:1)") : (preferences.pref_member_picture_ratio == 'portrait_ratio') ? _T("Portrait (3:4)") : (preferences.pref_member_picture_ratio == 'landscape_ratio') ? _T("Landscape (4:3)") %}
 
-                                        //display message
-                                        $.ajax({
-                                            url: '{{ url_for("ajaxMessages") }}',
-                                            method: "GET",
-                                            success: function (message) {
-                                                $('#asso_name').after(message);
-                                            }
-                                        });
-                                    },
-                                error: function() {
-                                    {% include "elements/js/modal.js.twig" with {
-                                        modal_title_twig: _T("An error occurred sending photo :(")|e("js"),
-                                        modal_without_content: true,
-                                        modal_class: "mini",
-                                        modal_deny_only: true,
-                                        modal_cancel_text: _T("Close")|e("js"),
-                                        modal_classname: "redalert",
-                                    } %}
-                                }
-                            });
+                var cropping = { ratio: '{{ preferences.pref_member_picture_ratio }}' };
+                var focus_select = '<div class="ui basic horizontally fitted segment form"><div class="field">';
+                    focus_select += '<select name="crop_focus_ajax" id="crop_focus_ajax" class="ui dropdown nochosen">';
+                    focus_select += '<option value="center">{{ _T("Center") }}</option>';
+                    focus_select += '<option value="top">{{ _T("Top") }}</option>';
+                    focus_select += '<option value="bottom">{{ _T("Bottom") }}</option>';
+                    focus_select += '<option value="left">{{ _T("Left") }}</option>';
+                    focus_select += '<option value="right">{{ _T("Right") }}</option>';
+                    focus_select += '</select>';
+                    focus_select += '</div></div>';
+
+                {% include "elements/js/modal.js.twig" with {
+                    modal_title_twig: _T("Cropping focus")|e("js"),
+                    modal_content_twig: _T("Choose the area of the original image to preserve after cropping to the final ratio defined in the settings : %ratio")|replace({"%ratio": system_ratio})|e('js'),
+                    modal_class: "tiny",
+                    modal_other_options: {
+                        closable: false
+                    },
+                    modal_onshow: "$(this).find('.content').append(focus_select);$('#crop_focus_ajax').dropdown();",
+                    modal_onapprove: "cropping.focus=$(this).find('#crop_focus_ajax').val();_fileLoad(file, cropping);"
+                } %}
+    {% else %}
+                _fileLoad(file);
+    {% endif %}
+            }
+
+            var _fileLoad = function(file, cropping_settings) {
+                var reader = new FileReader();
+                reader.readAsDataURL(file);
+
+                var cropping = false;
+                if (cropping_settings) {
+                    var cropping = cropping_settings;
+                }
+
+                reader.onload = function(evt) {
+                    $.ajax({
+                            type: 'POST',
+                            dataType: 'json',
+                            url : '{{ url_for("photoDnd") }}',
+                            data: {
+                                member_id: {{ member.id }},
+                                filename: file.name,
+                                filesize: file.size,
+                                file: evt.target.result,
+                                cropping: cropping
+                            },
+                            {% include "elements/js/loader.js.twig" with {
+                                selector: '#member_card'
+                            } %},
+                            success: function(res){
+                                window.location.reload(true);
+                            },
+                        error: function() {
+                            {% include "elements/js/modal.js.twig" with {
+                                modal_title_twig: _T("An error occurred sending photo :(")|e("js"),
+                                modal_without_content: true,
+                                modal_class: "tiny",
+                                modal_deny_only: true,
+                                modal_cancel_text: _T("Close")|e("js"),
+                                modal_classname: "redalert",
+                            } %}
                         }
-                    }
+                    });
                 }
-    {% endif %}
+            }
+        }
+    }
+{% endif %}
index 98d4508ebd5d437eff257c021eb0c2ffc0427b19..47a6444bdf0298752d8b49972ca35621b0f57038 100644 (file)
@@ -1,11 +1,22 @@
-<div class="ui horizontal fluid card">
+<div id="member_card" class="ui horizontal fluid card">
     {% if not (member.picture.path ends with '/default/images/default.png') %}
     <div class="image">
         <img
+                id="photo_adh"
                 src="{{ url_for("photo", {"id": member.id, "rand": time}) }}"
                 width="{{ member.picture.getOptimalWidth() }}"
                 height="{{ member.picture.getOptimalHeight() }}"
-                alt="{{ _T("Picture") }}">
+                alt="{{ _T("Picture") }}"
+                class="green no-touch tooltip"
+        {# Drag'n drop is disabled on member form #}
+        {% if (hidden_elements is not defined) %}
+                data-html="{{ _T("Drag and drop an image file to change the picture") }}">
+        <span class="ui bottom left corner tiny label">
+            <i class="download icon"></i>
+        </span>
+        {% else %}
+        >
+        {% endif %}
     </div>
     {% endif %}
     <div class="content">
index 018e90eb14eeb28382c4b5d6ed0d43ccd0ce0e42..71b2f9066df64da7a6579ae9c4da8637b70acaf7 100644 (file)
                 });
         {% endif %}
     {% endif %}
-                {% include "elements/js/photo_dnd.js.twig" %}
 
                 $('#ddn_adh').on('blur', function() {
                     var _bdate = $(this).val();
                         $('#member_age').html('');
                     }
                 });
+
+    {% if preferences.pref_force_picture_ratio == 1 %}
+                // Show photo cropping preferences on file selection
+                let _photo_new = document.getElementById('photo_new');
+                _photo_new.addEventListener('change', function () {
+                    if (_photo_new.files.length > 0) {
+                        let _crop_focus = document.getElementById('crop_focus_field');
+                        _crop_focus.classList.remove('displaynone');
+                        $('#crop_focus_field').transition('glow');
+                        return;
+                    }
+                });
+    {% endif %}
 {% endif %}
             });
         </script>
index 1c9bd44afc0b1ebfdf0a26745be120c87622d6d7..0723aa3f6df9cedd40325abf8fcadeefb6b84234 100644 (file)
@@ -39,8 +39,8 @@
                     />
             </div>
             <div class="content">
-                <div class="header">{{ member.sfullname }}</div>
-                {% if member.nickname != '' %}<div class="meta">{{ member.nickname|escape }}</div>{% endif %}
+                <div class="center aligned header">{{ member.sfullname }}</div>
+                {% if member.nickname != '' %}<div class="center aligned meta">{{ member.nickname|escape }}</div>{% endif %}
             </div>
         </div>
     {% else %}
index eb1aca64f2efcff418d8ff41ff902395a1841665..c8c419947c4ab0c61b37d6d2885b57ff9335f69d 100644 (file)
                             <label for="pref_bool_publicpages">{{ _T("Public pages enabled?") }}</label>
                         </div>
                     </div>
-                    <div class="field" id="publicpages_visibility"{% if not pref.pref_bool_publicpages %} class="displaynone"{% endif %}>
+                    <div id="publicpages_visibility" class="field{% if not pref.pref_bool_publicpages %} displaynone{% endif %}">
                         <label for="pref_publicpages_visibility">{{ _T("Show public pages for") }}</label>
                         <select name="pref_publicpages_visibility" id="pref_publicpages_visibility" class="ui search dropdown nochosen">
                             <option value="{{ constant('Galette\\Core\\Preferences::PUBLIC_PAGES_VISIBILITY_PUBLIC') }}"{% if pref.pref_publicpages_visibility == constant('Galette\\Core\\Preferences::PUBLIC_PAGES_VISIBILITY_PUBLIC') %} selected="selected"{% endif %}>{{ _T("Everyone") }}</option>
                             <option value="{{ constant('Galette\\Core\\Preferences::PUBLIC_PAGES_VISIBILITY_PRIVATE') }}"{% if pref.pref_publicpages_visibility == constant('Galette\\Core\\Preferences::PUBLIC_PAGES_VISIBILITY_PRIVATE') %} selected="selected"{% endif %}>{{ _T("Admin and staff only") }}</option>
                         </select>
                     </div>
+                    <div class="field inline">
+                        <div class="ui right aligned toggle checkbox">
+                            <input type="checkbox" name="pref_force_picture_ratio" id="pref_force_picture_ratio" value="1" {% if pref.pref_force_picture_ratio == 1 %}checked="checked"{% endif %}/>
+                            <label for="pref_force_picture_ratio">{{ _T("Force member picture ratio") }}</label>
+                        </div>
+                        <i class="tooltip circular inverted primary small icon info" data-html="{{ _T("If checked, the members's picture will be resized and cropped to the ratio selected below.") }}" data-variation="inverted wide"></i>
+                    </div>
+                    <div id="pref_member_picture_ratio_field" class="inline field{% if pref.pref_force_picture_ratio != 1 %} displaynone{% endif %}">
+                        <label for="pref_member_picture_ratio" title="{{ _T("Choose the") }}">{{ _T("Select a ratio") }}</label>
+                        <select name="pref_member_picture_ratio" id="pref_member_picture_ratio" class="ui dropdown nochosen">
+                            <option value="square_ratio"{% if pref.pref_member_picture_ratio == 'square_ratio' %} selected="selected"{% endif %}>{{ _T("Square (1:1)") }}</option>
+                            <option value="portrait_ratio"{% if pref.pref_member_picture_ratio == 'portrait_ratio' %} selected="selected"{% endif %}>{{ _T("Portrait (3:4)") }}</option>
+                            <option value="landscape_ratio"{% if pref.pref_member_picture_ratio == 'landscape_ratio' %} selected="selected"{% endif %}>{{ _T("Landscape (4:3)") }}</option>
+                        </select>
+                    </div>
                     <div class="field inline">
                         <div class="ui right aligned toggle checkbox">
                             <input type="checkbox" name="pref_bool_selfsubscribe" id="pref_bool_selfsubscribe" value="1"{% if pref.pref_bool_selfsubscribe %} checked="checked"{% endif %}/>
                         }
                     }
                 });
-                /*$('#smtp, #gmail').checkbox({
-                    onChecked: function() {
-                        console.log($(this).closest('#smtp_parameters'));
-                        //$('#smtp_parameters, #smtp_auth').toggleClass('displaynone');
-                    }
-                });*/
 
                 $('.pointing.menu .item').tab({
                     onVisible: function(tabPath) {
                     }
                 });
 
-                $('#pref_bool_publicpages').change(function(){
-                    $('#publicpages_visibility').toggleClass('displaynone');
+                let _publicpages_status = document.getElementById('pref_bool_publicpages');
+                let _publicpages_visibility = document.getElementById('publicpages_visibility');
+                _publicpages_status.addEventListener('change', function () {
+                    _publicpages_visibility.classList.toggle('displaynone');
+                    return;
+                });
+
+                let _crop_status = document.getElementById('pref_force_picture_ratio');
+                let _crop_ratio = document.getElementById('pref_member_picture_ratio_field');
+                _crop_status.addEventListener('change', function () {
+                    _crop_ratio.classList.toggle('displaynone');
+                    return;
                 });
 
                 $('#btnmail').on('click', function(e) {
index 0f7a150546349b3e01c7b4178be8ae8c7b2a12c9..7197c9c4445a00c72c2ef83a1e55dbc27dfa40ff 100644 (file)
@@ -4,9 +4,6 @@
 
 .edit-member {
   .fieldset-1 .ui.items > .item {
-    & > .image {
-      display: none;
-    }
     & > .content {
       padding: 0;
     }