Skip to content

Commit

Permalink
Merge pull request #6127 from WoltLab/6.2-user-coverphoto
Browse files Browse the repository at this point in the history
Use file processor for user cover photos
  • Loading branch information
Cyperghost authored Dec 23, 2024
2 parents 7efd39d + 4360ace commit 37efb38
Show file tree
Hide file tree
Showing 36 changed files with 860 additions and 960 deletions.
4 changes: 4 additions & 0 deletions com.woltlab.wcf/fileDelete.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,8 @@
<file>js/WoltLabSuite/Core/Ui/Redactor/RuntimeStyle.js</file>
<file>js/WoltLabSuite/Core/Ui/Redactor/Spoiler.js</file>
<file>js/WoltLabSuite/Core/Ui/Redactor/Table.js</file>
<file>js/WoltLabSuite/Core/Ui/User/CoverPhoto/Delete.js</file>
<file>js/WoltLabSuite/Core/Ui/User/CoverPhoto/Upload.js</file>
<file>js/WoltLabSuite/Core/Ui/User/Menu/DropDown.js</file>
<file>js/WoltLabSuite/WebComponent/fa-brand.js</file>
<file>js/WoltLabSuite/WebComponent/fa-icon.js</file>
Expand Down Expand Up @@ -3002,6 +3004,8 @@
<file>lib/system/template/plugin/WordwrapModifierTemplatePlugin.class.php</file>
<file>lib/system/upload/AvatarUploadFileSaveStrategy.class.php</file>
<file>lib/system/upload/AvatarUploadFileValidationStrategy.class.php</file>
<file>lib/system/upload/UserCoverPhotoUploadFileSaveStrategy.class.php</file>
<file>lib/system/upload/UserCoverPhotoUploadFileValidationStrategy.class.php</file>
<file>lib/system/user/UserCollapsibleContentHandler.class.php</file>
<file>lib/system/user/activity/point/AbstractUserActivityPointObjectProcessor.class.php</file>
<file>lib/system/user/activity/point/DefaultUserActivityPointObjectProcessor.class.php</file>
Expand Down
5 changes: 5 additions & 0 deletions com.woltlab.wcf/objectType.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1749,6 +1749,11 @@
<definitionname>com.woltlab.wcf.file</definitionname>
<classname>wcf\system\file\processor\UserAvatarFileProcessor</classname>
</type>
<type>
<name>com.woltlab.wcf.user.coverPhoto</name>
<definitionname>com.woltlab.wcf.file</definitionname>
<classname>wcf\system\file\processor\UserCoverPhotoFileProcessor</classname>
</type>
<!-- deprecated -->
<type>
<name>com.woltlab.wcf.page.controller</name>
Expand Down
40 changes: 0 additions & 40 deletions com.woltlab.wcf/templates/user.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -115,44 +115,4 @@
<woltlab-core-notice type="info">{lang}wcf.user.profile.protected{/lang}</woltlab-core-notice>
{/if}

{if $user->userID == $__wcf->user->userID || $user->canEdit()}
{if $__wcf->getSession()->getPermission('user.profile.coverPhoto.canUploadCoverPhoto')}
<div id="userProfileCoverPhotoUpload" class="jsStaticDialogContent" data-title="{lang}wcf.user.coverPhoto.upload{/lang}">
{if $__wcf->user->disableCoverPhoto}
<woltlab-core-notice type="error">{lang}wcf.user.coverPhoto.error.disabled{/lang}</woltlab-core-notice>
{else}
<div id="coverPhotoUploadPreview"></div>

{* placeholder for the upload button *}
<div id="coverPhotoUploadButtonContainer"></div>
<small>{lang}wcf.user.coverPhoto.upload.description{/lang}</small>
{/if}
</div>
<script data-relocate="true">
require(['Language', 'WoltLabSuite/Core/Ui/User/CoverPhoto/Upload'], function (Language, UiUserCoverPhotoUpload) {
Language.addObject({
'wcf.user.coverPhoto.delete.confirmMessage': '{jslang}wcf.user.coverPhoto.delete.confirmMessage{/jslang}',
'wcf.user.coverPhoto.upload.error.fileExtension': '{jslang}wcf.user.coverPhoto.upload.error.fileExtension{/jslang}',
'wcf.user.coverPhoto.upload.error.tooLarge': '{jslang}wcf.user.coverPhoto.upload.error.tooLarge{/jslang}',
'wcf.user.coverPhoto.upload.error.uploadFailed': '{jslang}wcf.user.coverPhoto.upload.error.uploadFailed{/jslang}',
'wcf.user.coverPhoto.upload.error.badImage': '{jslang}wcf.user.coverPhoto.upload.error.badImage{/jslang}'
});
{if !$__wcf->user->disableCoverPhoto}
new UiUserCoverPhotoUpload({@$user->userID});
{/if}
});
</script>
{/if}
<script data-relocate="true">
require(['Language', 'WoltLabSuite/Core/Ui/User/CoverPhoto/Delete'], function (Language, UiUserCoverPhotoDelete) {
Language.addObject({
'wcf.user.coverPhoto.delete.confirmMessage': '{jslang}wcf.user.coverPhoto.delete.confirmMessage{/jslang}'
});
UiUserCoverPhotoDelete.init({@$user->userID});
});
</script>
{/if}

{include file='footer'}
2 changes: 1 addition & 1 deletion com.woltlab.wcf/templates/userCard.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div class="userCard__header__background">
<img
class="userCard__header__background__image"
src="{$user->getCoverPhoto()->getURL()}"
src="{$user->getCoverPhoto()->getThumbnailURL()}"
loading="lazy">
</div>
<div class="userCard__header__avatar">
Expand Down
18 changes: 8 additions & 10 deletions com.woltlab.wcf/templates/userProfileHeader.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,20 @@
>
<div class="userProfileHeader__coverPhotoContainer">
<div class="userProfileHeader__coverPhoto">
<img src="{$view->user->getCoverPhoto()->getURL()}" class="userProfileHeader__coverPhotoImage">
<img src="{$view->user->getCoverPhoto()->getURL()}" data-object-id="{$view->user->getCoverPhoto()->getObjectID()}" class="userProfileHeader__coverPhotoImage">
</div>

<div class="userProfileHeader__manageButtons">
{event name='beforeManageButtons'}

{if $view->canEditCoverPhoto()}
<div class="dropdown">
<button type="button" class="button small dropdownToggle">{icon name='camera'} {lang}wcf.user.coverPhoto.edit{/lang}</button>
<ul class="dropdownMenu">
{if $view->canAddCoverPhoto()}
<li><button type="button" class="jsButtonUploadCoverPhoto jsStaticDialog" data-dialog-id="userProfileCoverPhotoUpload">{lang}wcf.user.coverPhoto.upload{/lang}</button></li>
{/if}
<li{if !$view->user->coverPhotoHash} style="display:none;"{/if}><button type="button" class="jsButtonDeleteCoverPhoto">{lang}wcf.user.coverPhoto.delete{/lang}</button></li>
</ul>
</div>
<ul class="userProfileManageCoverPhoto buttonGroup buttonList smallButtons">
<li>
<button type="button" data-edit-cover-photo="{link controller="UserCoverPhoto" id=$user->userID}{/link}" data-default-cover-photo="{$__wcf->styleHandler->getStyle()->getCoverPhotoUrl()}" class="button small">
{icon name='camera'} {lang}wcf.user.coverPhoto.management{/lang}
</button>
</li>
</ul>
{/if}

{if $view->user->canEditAvatar()}
Expand Down
3 changes: 3 additions & 0 deletions ts/WoltLabSuite/Core/Bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ export function setup(options: BoostrapOptions): void {
whenFirstSeen(".jsEnablesOptions", () => {
void import("./Component/Option/Enable").then(({ setup }) => setup());
});
whenFirstSeen("[data-edit-cover-photo]", () => {
void import("./Component/User/CoverPhoto").then(({ setup }) => setup());
});

// Move the reCAPTCHA widget overlay to the `pageOverlayContainer`
// when widget form elements are placed in a dialog.
Expand Down
118 changes: 72 additions & 46 deletions ts/WoltLabSuite/Core/Component/Image/Cropper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ function inSelection(selection: Selection, maxSelection: Selection): boolean {
);
}

function clampValue(position: number, length: number, availableLength: number): number {
if (position < 0) {
return 0;
}

if (position + length > availableLength) {
return Math.floor(availableLength - length);
}

return Math.floor(position);
}

abstract class ImageCropper {
readonly configuration: CropperConfiguration;
readonly file: File;
Expand Down Expand Up @@ -166,8 +178,8 @@ abstract class ImageCropper {
const height = width / this.configuration.aspectRatio;

return this.cropperSelection!.$toCanvas({
width: Math.max(Math.min(Math.floor(width), this.maxSize.width), this.minSize.width),
height: Math.max(Math.min(Math.ceil(height), this.maxSize.height), this.minSize.height),
width: Math.max(Math.min(Math.round(width), this.maxSize.width), this.minSize.width),
height: Math.max(Math.min(Math.round(height), this.maxSize.height), this.minSize.height),
});
}

Expand All @@ -188,53 +200,62 @@ abstract class ImageCropper {

this.centerSelection();

// Limit the selection to the canvas boundaries
this.cropperSelection!.addEventListener("change", (event: CustomEvent) => {
// see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries
const cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect();
const selection = event.detail as Selection;

const maxSelection: Selection = {
x: 0,
y: 0,
width: cropperCanvasRect.width,
height: cropperCanvasRect.height,
};

if (!inSelection(selection, maxSelection)) {
event.preventDefault();
}
});

// Limit the selection to the min/max size
this.cropperSelection!.addEventListener("change", (event: CustomEvent) => {
const selection = event.detail as Selection;
this.cropperCanvasRect = this.cropperCanvas!.getBoundingClientRect();

// Limit the selection to the min/max size.
const selectionRatio = Math.min(
this.cropperCanvasRect.width / this.width,
this.cropperCanvasRect.height / this.height,
);

const minWidth = this.minSize.width * selectionRatio;
const maxWidth = this.cropperCanvasRect.width;
const minHeight = minWidth / this.configuration.aspectRatio;
const maxHeight = maxWidth / this.configuration.aspectRatio;

if (
selection.width < minWidth ||
selection.height < minHeight ||
selection.width > maxWidth ||
selection.height > maxHeight
) {
// Round all values to integers to avoid dealing with the wonderful world
// of IEEE 754 numbers.
const minWidth = Math.ceil(this.minSize.width * selectionRatio);
const maxWidth = Math.round(this.cropperCanvasRect.width);
const minHeight = Math.ceil(minWidth / this.configuration.aspectRatio);
const maxHeight = Math.round(maxWidth / this.configuration.aspectRatio);

const width = Math.round(selection.width);
const height = Math.round(selection.height);
const x = Math.round(selection.x);
const y = Math.round(selection.y);

if (width < minWidth || height < minHeight || width > maxWidth || height > maxHeight) {
event.preventDefault();

// Stop the event handling here otherwise the following code would try
// to adjust the position on an invalid size that could potentially
// violate the boundaries.
return;
}

// Limit the selection to the canvas boundaries.
// see https://fengyuanchen.github.io/cropperjs/v2/api/cropper-selection.html#limit-boundaries
const maxSelection: Selection = {
x: 0,
y: 0,
width: maxWidth,
height: Math.round(this.cropperCanvasRect.height),
};

if (!inSelection(selection, maxSelection)) {
event.preventDefault();
// Clamp the position to the boundaries of the canvas.
void this.cropperSelection!.$nextTick().then(() => {
this.cropperSelection!.$change(
clampValue(x, width, maxSelection.width),
clampValue(y, height, maxSelection.height),
width,
height,
);
});
}
});
}

protected setCropperStyle() {
this.cropperCanvas!.style.aspectRatio = `${this.width}/${this.height}`;

this.cropperSelection!.aspectRatio = this.configuration.aspectRatio;
}

Expand All @@ -245,26 +266,31 @@ abstract class ImageCropper {

const dimension = DomUtil.innerDimensions(this.cropperCanvas!.parentElement!);
const ratio = Math.min(dimension.width / this.width, dimension.height / this.height);
const imageRatio = this.width / this.height;

const canvasHeight = Math.round(this.height * ratio);
const canvasWidth = Math.round(canvasHeight * imageRatio);

this.cropperCanvas!.style.height = `${this.height * ratio}px`;
this.cropperCanvas!.style.width = `${this.width * ratio}px`;
this.cropperCanvas!.style.height = `${canvasHeight}px`;
this.cropperCanvas!.style.width = `${canvasWidth}px`;

this.cropperImage!.$center("contain");
this.cropperImage!.$center("cover");
this.cropperCanvasRect = this.cropperImage!.getBoundingClientRect();

const selectionRatio = Math.min(
this.cropperCanvasRect.width / this.maxSize.width,
this.cropperCanvasRect.height / this.maxSize.height,
);

this.cropperSelection!.$change(
0,
0,
Math.min(this.cropperCanvasRect.width, this.maxSize.width * selectionRatio),
Math.min(this.cropperCanvasRect.height, this.maxSize.height * selectionRatio),
this.configuration.aspectRatio,
true,
);
let selectionHeight = Math.min(this.cropperCanvasRect.height, Math.floor(this.maxSize.height * selectionRatio));
let selectionWidth = Math.floor(selectionHeight * this.configuration.aspectRatio);

if (selectionWidth > this.cropperCanvasRect.width) {
selectionWidth = Math.floor(this.cropperCanvasRect.width);
selectionHeight = Math.floor(selectionWidth / this.configuration.aspectRatio);
}

this.cropperSelection!.$change(0, 0, selectionWidth, selectionHeight, this.configuration.aspectRatio, true);

this.cropperSelection!.$center();
this.cropperSelection!.scrollIntoView({ block: "center", inline: "center" });
Expand All @@ -275,7 +301,7 @@ abstract class ImageCropper {
<cropper-image skewable scalable translatable rotatable></cropper-image>
<cropper-shade hidden></cropper-shade>
<cropper-handle action="scale" hidden disabled></cropper-handle>
<cropper-selection precise movable resizable outlined>
<cropper-selection movable resizable outlined>
<cropper-grid role="grid" bordered covered></cropper-grid>
<cropper-crosshair centered></cropper-crosshair>
<cropper-handle action="move" theme-color="rgba(255, 255, 255, 0.35)"></cropper-handle>
Expand Down
Loading

0 comments on commit 37efb38

Please sign in to comment.