Added support for per-style cover photos
authorAlexander Ebert <ebert@woltlab.com>
Wed, 29 Nov 2017 13:20:01 +0000 (14:20 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 29 Nov 2017 13:20:01 +0000 (14:20 +0100)
See #2484

wcfsetup/install/files/acp/style/layout.scss
wcfsetup/install/files/acp/templates/styleAdd.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/CoverPhoto/Upload.js [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/StyleEditForm.class.php
wcfsetup/install/files/lib/data/style/Style.class.php
wcfsetup/install/files/lib/data/style/StyleAction.class.php
wcfsetup/install/files/lib/data/user/cover/photo/DefaultUserCoverPhoto.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index 7c2e75a8060088601c45470dd7dfca64afd4e8fc..a7efb2992789fa0150d8ba9be05401db927e7580 100644 (file)
@@ -371,3 +371,14 @@ $wcfAcpSubMenuWidth: 300px;
                margin-bottom: 5px;
        }
 }
+
+#coverPhotoPreview {
+       background: no-repeat center center;
+       background-size: cover;
+       height: 200px;
+       margin-bottom: 5px;
+       
+       @include screen-xs {
+               height: 150px;
+       }
+}
index 53a82c6fe4cf5cda065a615c87e9a743ce699c83..9b206c46b6b8c9691688ea9025845f012f3cba7b 100644 (file)
@@ -5,20 +5,35 @@
 {js application='wcf' acp='true' file='WCF.ACP.Style'}
 {js application='wcf' file='WCF.ColorPicker' bundle='WCF.Combined'}
 <script data-relocate="true">
-       require(['WoltLabSuite/Core/Acp/Ui/Style/Favicon/Upload', 'WoltLabSuite/Core/Acp/Ui/Style/Image/Upload', 'WoltLabSuite/Core/Acp/Ui/Style/Editor', 'WoltLabSuite/Core/Ui/Toggle/Input'], function(AcpUiStyleFaviconUpload, AcpUiStyleImageUpload, AcpUiStyleEditor, UiToggleInput) {
+       require([
+               'WoltLabSuite/Core/Acp/Ui/Style/CoverPhoto/Upload', 'WoltLabSuite/Core/Acp/Ui/Style/Favicon/Upload', 'WoltLabSuite/Core/Acp/Ui/Style/Image/Upload', 'WoltLabSuite/Core/Acp/Ui/Style/Editor', 'WoltLabSuite/Core/Ui/Toggle/Input', 'Language'
+       ], function(
+               AcpUiStyleCoverPhotoUpload, AcpUiStyleFaviconUpload, AcpUiStyleImageUpload, AcpUiStyleEditor, UiToggleInput, Language
+       ) {
                AcpUiStyleEditor.setup({
                        isTainted: {if $isTainted}true{else}false{/if},
                        styleId: {if $action === 'edit'}{@$style->styleID}{else}0{/if},
                        styleRuleMap: styleRuleMap
                });
                
-               {if $action == 'edit'}new AcpUiStyleFaviconUpload({@$style->styleID});{/if}
                new AcpUiStyleImageUpload({if $action == 'add'}0{else}{@$style->styleID}{/if}, '{$tmpHash}', false);
                new AcpUiStyleImageUpload({if $action == 'add'}0{else}{@$style->styleID}{/if}, '{$tmpHash}', true);
                
                new UiToggleInput('input[name="useGoogleFont"]', {
                        show: ['#wcfFontFamilyGoogleContainer']
                });
+               
+               {if $action === 'edit'}
+                       Language.addObject({
+                               'wcf.user.coverPhoto.upload.error.invalidExtension': '{lang}wcf.user.coverPhoto.upload.error.invalidExtension{/lang}',
+                               'wcf.user.coverPhoto.upload.error.minHeight': '{lang}wcf.user.coverPhoto.upload.error.minHeight{/lang}',
+                               'wcf.user.coverPhoto.upload.error.minWidth': '{lang}wcf.user.coverPhoto.upload.error.minWidth{/lang}',
+                               'wcf.user.coverPhoto.upload.error.uploadFailed': '{lang}wcf.user.coverPhoto.upload.error.uploadFailed{/lang}'
+                       });
+                       
+                       new AcpUiStyleFaviconUpload({@$style->styleID});
+                       new AcpUiStyleCoverPhotoUpload({@$style->styleID});
+               {/if}
        });
        
        $(function() {
@@ -46,7 +61,7 @@
                
                $('.jsUnitSelect').change(function(event) {
                        var $target = $(event.currentTarget);
-                       $target.prev().attr('step', (($target.val() == 'em' || $target.val() == 'rem') ? '0.01' : '1'));
+                       $target.prev().attr('step', (($target.val() === 'em' || $target.val() === 'rem') ? '0.01' : '1'));
                }).trigger('change');
        });
 </script>
                                {event name='fileFields'}
                        </section>
                        
-                       {if $action == 'edit'}
+                       {if $action === 'edit'}
                                <section class="section">
                                        <h2 class="sectionTitle">{lang}wcf.acp.style.general.favicon{/lang}</h2>
                                        
                                                <dt><label for="favicon">{lang}wcf.acp.style.favicon{/lang}</label></dt>
                                                <dd>
                                                        <div class="selectedFaviconPreview">
-                                                               <img src="{if $action == 'add'}{@$__wcf->getPath()}images/favicon/default.apple-touch-icon.png{else}{@$style->getFaviconAppleTouchIcon()}{/if}" alt="" id="faviconImage" style="height: 32px; width: 32px;">
+                                                               <img src="{@$style->getFaviconAppleTouchIcon()}" alt="" id="faviconImage" style="height: 32px; width: 32px;">
                                                        </div>
                                                        <div id="uploadFavicon"></div>
                                                        <small>{lang}wcf.acp.style.favicon.description{/lang}</small>
                                        
                                        {event name='faviconFields'}
                                </section>
+                               
+                               <section class="section">
+                                       <h2 class="sectionTitle">{lang}wcf.acp.style.general.coverPhoto{/lang}</h2>
+                                       
+                                       <dl>
+                                               <dt><label for="coverPhoto">{lang}wcf.acp.style.coverPhoto{/lang}</label></dt>
+                                               <dd>
+                                                       <div id="coverPhotoPreview" style="background-image: url({@$__wcf->getPath()}images/coverPhotos/{@$style->getCoverPhoto()})"></div>
+                                                       <div id="uploadCoverPhoto"></div>
+                                                       <small>{lang}wcf.acp.style.coverPhoto.description{/lang}</small>
+                                               </dd>
+                                       </dl>
+                                       
+                                       {event name='coverPhotoFields'}
+                               </section>
                        {/if}
                        
                        {event name='generalFieldsets'}
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/CoverPhoto/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/CoverPhoto/Upload.js
new file mode 100644 (file)
index 0000000..cf4db82
--- /dev/null
@@ -0,0 +1,59 @@
+/**
+ * Handles uploading the style's cover photo.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Style/CoverPhoto/Upload
+ */
+define(['Core', 'Dom/Traverse', 'Language', 'Ui/Notification', 'Upload'], function(Core, DomTraverse, Language, UiNotification, Upload) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function AcpUiStyleCoverPhotoUpload(styleId) {
+               this._styleId = ~~styleId;
+               
+               Upload.call(this, 'uploadCoverPhoto', 'coverPhotoPreview', {
+                       action: 'uploadCoverPhoto',
+                       className: 'wcf\\data\\style\\StyleAction'
+               });
+       }
+       Core.inherit(AcpUiStyleCoverPhotoUpload, Upload, {
+               /**
+                * @see WoltLabSuite/Core/Upload#_createFileElement
+                */
+               _createFileElement: function(file) {
+                       return this._target;
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Upload#_getParameters
+                */
+               _getParameters: function() {
+                       return {
+                               styleID: this._styleId
+                       };
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Upload#_success
+                */
+               _success: function(uploadId, data) {
+                       var errorMessage = '';
+                       if (data.returnValues.url) {
+                               this._target.style.setProperty('background-image', 'url(' + data.returnValues.url + '?timestamp=' + Date.now() + ')', '');
+                               
+                               UiNotification.show();
+                       }
+                       else if (data.returnValues.errorType) {
+                               errorMessage = Language.get('wcf.user.coverPhoto.upload.error.' + data.returnValues.errorType);
+                       }
+                       
+                       elInnerError(this._button, errorMessage);
+               }
+       });
+       
+       return AcpUiStyleCoverPhotoUpload;
+});
index f479c2b5a4de42cf4da827743fdd9ebb59b7bd4d..2ddc531f2edd00319b5d22eea3453f994ed7ef0c 100644 (file)
@@ -2,6 +2,7 @@
 namespace wcf\acp\form;
 use wcf\data\style\Style;
 use wcf\data\style\StyleAction;
+use wcf\data\user\cover\photo\UserCoverPhoto;
 use wcf\form\AbstractForm;
 use wcf\system\exception\IllegalLinkException;
 use wcf\system\language\I18nHandler;
@@ -207,7 +208,9 @@ class StyleEditForm extends StyleAddForm {
                        'action' => 'edit',
                        'isTainted' => $this->style->isTainted,
                        'style' => $this->style,
-                       'styleID' => $this->styleID
+                       'styleID' => $this->styleID,
+                       'coverPhotoMinHeight' => UserCoverPhoto::MIN_HEIGHT,
+                       'coverPhotoMinWidth' => UserCoverPhoto::MIN_WIDTH
                ]);
        }
 }
index 1844e91389d03f49796123dd0abde1195e555578..71abce2e0a87c07a21f113bcaa03f2966a255e67 100644 (file)
@@ -31,6 +31,7 @@ use wcf\system\WCF;
  * @property-read      string          $packageName            package identifier used to export the style as a package or empty (thus style cannot be exported as package)
  * @property-read      integer         $isTainted              is `0` if the original declarations of an imported or installed style are not and cannot be altered, otherwise `1`
  * @property-read      integer         $hasFavicon             is `0` if the default favicon data should be used
+ * @property-read      integer         $coverPhotoExtension    extension of the style's cover photo file
  * @property-read       string          $apiVersion             the style's compatibility version, possible values: '3.0' or '3.1'
  */
 class Style extends DatabaseObject {
@@ -186,6 +187,19 @@ class Style extends DatabaseObject {
                return $this->getFaviconPath('favicon.ico', false);
        }
        
+       /**
+        * Returns the cover photo filename.
+        * 
+        * @return      string
+        */
+       public function getCoverPhoto() {
+               if ($this->coverPhotoExtension) {
+                       return $this->styleID . '.' . $this->coverPhotoExtension;
+               }
+               
+               return 'default.png';
+       }
+       
        /**
         * Returns the path to a favicon-related file.
         * 
index 513917105f8a996bbd2258678ed0988bd9125ef7..b67d70ebebfeb9bb741b32b3d2c0497a11145398 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\data\style;
+use wcf\data\user\cover\photo\UserCoverPhoto;
 use wcf\data\user\UserAction;
 use wcf\data\AbstractDatabaseObjectAction;
 use wcf\data\IToggleAction;
@@ -101,6 +102,9 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction,
                        // create favicon data
                        $this->updateFavicons($style->getDecoratedObject());
                        
+                       // handle the cover photo
+                       $this->updateCoverPhoto($style->getDecoratedObject());
+                       
                        // reset stylesheet
                        StyleHandler::getInstance()->resetStylesheet($style->getDecoratedObject());
                }
@@ -270,6 +274,7 @@ class StyleAction extends AbstractDatabaseObjectAction implements IToggleAction,
         * Updates style favicon files.
         * 
         * @param       Style           $style
+        * @since       3.1
         */
        protected function updateFavicons(Style $style) {
                $styleID = $style->styleID;
@@ -348,6 +353,31 @@ BROWSERCONFIG;
                }
        }
        
+       /**
+        * Updates the style cover photo.
+        * 
+        * @param       Style           $style
+        * @since       3.1
+        */
+       protected function updateCoverPhoto(Style $style) {
+               $styleID = $style->styleID;
+               $fileExtension = WCF::getSession()->getVar('styleCoverPhoto-'.$styleID);
+               if ($fileExtension) {
+                       // remove old image
+                       if ($style->coverPhotoExtension) {
+                               @unlink(WCF_DIR . 'images/coverPhotos/' . $style->getCoverPhoto());
+                       }
+                       
+                       rename(
+                               WCF_DIR . 'images/coverPhotos/' . $styleID . '.tmp.' . $fileExtension,
+                               WCF_DIR . 'images/coverPhotos/' . $styleID . '.' . $fileExtension
+                       );
+                       
+                       (new StyleEditor($style))->update(['coverPhotoExtension' => $fileExtension]);
+                       WCF::getSession()->unregister('styleCoverPhoto-'.$style->styleID);
+               }
+       }
+       
        /**
         * @inheritDoc
         */
@@ -558,6 +588,8 @@ BROWSERCONFIG;
        
        /**
         * Validates parameters to upload a favicon.
+        * 
+        * @since       3.1
         */
        public function validateUploadFavicon() {
                // ignore tmp hash, uploading is supported for existing styles only
@@ -571,6 +603,7 @@ BROWSERCONFIG;
         * Handles favicon upload.
         *
         * @return      string[]
+        * @since       3.1
         */
        public function uploadFavicon() {
                // save files
@@ -628,6 +661,84 @@ BROWSERCONFIG;
                return ['errorType' => $file->getValidationErrorType()];
        }
        
+       /**
+        * Validates parameters to upload a cover photo.
+        *
+        * @since       3.1
+        */
+       public function validateUploadCoverPhoto() {
+               // ignore tmp hash, uploading is supported for existing styles only
+               // and files will be finally processed on form submit
+               $this->parameters['tmpHash'] = '@@@WCF_INVALID_TMP_HASH@@@';
+               
+               $this->validateUpload();
+       }
+       
+       /**
+        * Handles the cover photo upload.
+        *
+        * @return      string[]
+        * @since       3.1
+        */
+       public function uploadCoverPhoto() {
+               // save files
+               /** @noinspection PhpUndefinedMethodInspection */
+               /** @var UploadFile[] $files */
+               $files = $this->parameters['__files']->getFiles();
+               $file = $files[0];
+               
+               try {
+                       if (!$file->getValidationErrorType()) {
+                               $fileLocation = $file->getLocation();
+                               try {
+                                       if (($imageData = getimagesize($fileLocation)) === false) {
+                                               throw new UserInputException('coverPhoto');
+                                       }
+                                       switch ($imageData[2]) {
+                                               case IMAGETYPE_PNG:
+                                               case IMAGETYPE_JPEG:
+                                               case IMAGETYPE_GIF:
+                                                       // fine
+                                                       break;
+                                               default:
+                                                       throw new UserInputException('coverPhoto');
+                                       }
+                                       
+                                       if ($imageData[0] < UserCoverPhoto::MIN_WIDTH) {
+                                               throw new UserInputException('coverPhoto', 'minWidth');
+                                       }
+                                       else if ($imageData[1] < UserCoverPhoto::MIN_HEIGHT) {
+                                               throw new UserInputException('coverPhoto', 'minHeight');
+                                       }
+                               }
+                               catch (SystemException $e) {
+                                       throw new UserInputException('coverPhoto');
+                               }
+                               
+                               // move uploaded file
+                               if (@copy($fileLocation, WCF_DIR.'images/coverPhotos/'.$this->style->styleID.'.tmp.'.$file->getFileExtension())) {
+                                       @unlink($fileLocation);
+                                       
+                                       // store extension within session variables
+                                       WCF::getSession()->register('styleCoverPhoto-'.$this->style->styleID, $file->getFileExtension());
+                                       
+                                       // return result
+                                       return [
+                                               'url' => WCF::getPath().'images/coverPhotos/'.$this->style->styleID.'.tmp.'.$file->getFileExtension()
+                                       ];
+                               }
+                               else {
+                                       throw new UserInputException('coverPhoto', 'uploadFailed');
+                               }
+                       }
+               }
+               catch (UserInputException $e) {
+                       $file->setValidationErrorType($e->getType());
+               }
+               
+               return ['errorType' => $file->getValidationErrorType()];
+       }
+       
        /**
         * Validates parameters to assign a new default style.
         */
index a5e8ea7154a920dc78a20e29a49aa48399e80f8e..09f6e90db116ce4eee83dbe0d664779be5a2e27f 100644 (file)
@@ -37,10 +37,6 @@ class DefaultUserCoverPhoto implements IUserCoverPhoto {
         * @inheritDoc
         */
        public function getFilename() {
-               /*if ($coverPhoto = StyleHandler::getInstance()->getStyle()->getDefaultCoverPhoto()) {
-                       return $coverPhoto;
-               }*/
-               
-               return 'default.png';
+               return StyleHandler::getInstance()->getStyle()->getCoverPhoto();
        }
 }
index b18c6bc8ca2236941a80a25cd68c399aa341d742..c5616dcd465a7c2ac8be3b4a95902df0e62eed37 100644 (file)
@@ -1930,6 +1930,8 @@ Als Benachrichtigungs-URL in der Konfiguration der sofortigen Zahlungsbestätigu
                <item name="wcf.acp.style.copyright"><![CDATA[Copyright]]></item>
                <item name="wcf.acp.style.copyStyle"><![CDATA[Stil kopieren]]></item>
                <item name="wcf.acp.style.copyStyle.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den Stil <span class="confirmationObject">{$style->styleName}</span> wirklich duplizieren?]]></item>
+               <item name="wcf.acp.style.coverPhoto"><![CDATA[Standard-Profilbild]]></item>
+               <item name="wcf.acp.style.coverPhoto.description"><![CDATA[Das Bild muss mindestens {$coverPhotoMinWidth}×{$coverPhotoMinHeight} Pixel groß sein, als Dateiendung sind GIF, JPG, JPEG und PNG zulässig.]]></item>
                <item name="wcf.acp.style.delete.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den Stil <span class="confirmationObject">{$style->styleName}</span> wirklich löschen?]]></item>
                <item name="wcf.acp.style.edit"><![CDATA[Stil bearbeiten]]></item>
                <item name="wcf.acp.style.exportAsPackage"><![CDATA[Als Paket exportieren]]></item>
@@ -1941,6 +1943,7 @@ Als Benachrichtigungs-URL in der Konfiguration der sofortigen Zahlungsbestätigu
                <item name="wcf.acp.style.exportStyle.components"><![CDATA[Optionen]]></item>
                <item name="wcf.acp.style.exportStyle.components.description"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Wähle{else}Wählen Sie{/if} hier aus, welche Bestandteile des Stils „{$style->styleName}“ mit exportiert werden sollen.]]></item>
                <item name="wcf.acp.style.general"><![CDATA[Daten]]></item>
+               <item name="wcf.acp.style.general.coverPhoto"><![CDATA[Profilbild]]></item>
                <item name="wcf.acp.style.general.files"><![CDATA[Dateien]]></item>
                <item name="wcf.acp.style.globals"><![CDATA[Globale Einstellungen]]></item>
                <item name="wcf.acp.style.globals.fixedLayoutWidth"><![CDATA[Breite]]></item>
index a8ddd0bdf41f6f023e3f0d35e2159504543c4815..5d9b2b6c7671e3b8d77539ef0868e595df596415 100644 (file)
@@ -1871,6 +1871,8 @@ When prompted for the notification URL for the instant payment notifications, pl
                <item name="wcf.acp.style.copyright"><![CDATA[Copyright]]></item>
                <item name="wcf.acp.style.copyStyle"><![CDATA[Duplicate Style]]></item>
                <item name="wcf.acp.style.copyStyle.confirmMessage"><![CDATA[Do you really want to duplicate the style <span class="confirmationObject">{$style->styleName}</span>?]]></item>
+               <item name="wcf.acp.style.coverPhoto"><![CDATA[Default Cover Photo]]></item>
+               <item name="wcf.acp.style.coverPhoto.description"><![CDATA[The image must be at least {$coverPhotoMinWidth}×{$coverPhotoMinHeight} pixels large, acceptable image types are GIF, JPG, JPEG and PNG.]]></item>
                <item name="wcf.acp.style.delete.confirmMessage"><![CDATA[Do you really want to delete the style <span class="confirmationObject">{$style->styleName}</span>?]]></item>
                <item name="wcf.acp.style.edit"><![CDATA[Edit Style]]></item>
                <item name="wcf.acp.style.exportAsPackage"><![CDATA[Export as package]]></item>
@@ -1882,6 +1884,7 @@ When prompted for the notification URL for the instant payment notifications, pl
                <item name="wcf.acp.style.exportStyle.components"><![CDATA[Options]]></item>
                <item name="wcf.acp.style.exportStyle.components.description"><![CDATA[Select components included in the export for the style “{$style->styleName}”.]]></item>
                <item name="wcf.acp.style.general"><![CDATA[Data]]></item>
+               <item name="wcf.acp.style.general.coverPhoto"><![CDATA[Cover Photo]]></item>
                <item name="wcf.acp.style.general.favicon"><![CDATA[Favicon]]></item>
                <item name="wcf.acp.style.general.files"><![CDATA[Files]]></item>
                <item name="wcf.acp.style.globals"><![CDATA[Global Settings]]></item>
index a6079a06c5254be76e6d4e72cc1a40a6b88c0ae2..30634e9126cdcc7335442cd6245a7529447a94d3 100644 (file)
@@ -1271,6 +1271,7 @@ CREATE TABLE wcf1_style (
        packageName VARCHAR(255) NOT NULL DEFAULT '',
        isTainted TINYINT(1) NOT NULL DEFAULT 0,
        hasFavicon TINYINT(1) NOT NULL DEFAULT 0,
+       coverPhotoExtension VARCHAR(4) NOT NULL DEFAULT '',
        apiVersion ENUM('3.0', '3.1') NOT NULL DEFAULT '3.0' 
 );