Skip to content

Commit

Permalink
Allow cropping an avatar before setting it (go-gitea#32565)
Browse files Browse the repository at this point in the history
Provide a cropping tool on the avatar editing page, allowing users to
select the cropping area themselves. This way, users can decide the
displayed area of the image, rather than us deciding for them.

---------

Co-authored-by: silverwind <[email protected]>
Co-authored-by: wxiaoguang <[email protected]>
Co-authored-by: delvh <[email protected]>
Co-authored-by: Giteabot <[email protected]>
  • Loading branch information
5 people authored Nov 28, 2024
1 parent f1bea3c commit 68d9f36
Show file tree
Hide file tree
Showing 12 changed files with 80 additions and 9 deletions.
1 change: 1 addition & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,7 @@ uploaded_avatar_not_a_image = The uploaded file is not an image.
uploaded_avatar_is_too_big = The uploaded file size (%d KiB) exceeds the maximum size (%d KiB).
update_avatar_success = Your avatar has been updated.
update_user_avatar_success = The user's avatar has been updated.
cropper_prompt = You can edit the image before saving. The edited image will be saved as PNG.

change_password = Update Password
old_password = Current Password
Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.0.1",
"clippie": "4.1.3",
"cropperjs": "1.6.2",
"css-loader": "7.1.2",
"dayjs": "1.11.13",
"dropzone": "6.0.0-beta.2",
Expand Down
5 changes: 5 additions & 0 deletions templates/user/settings/profile.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@
<input id="new-avatar" name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
</div>

<div class="field tw-pl-4 cropper-panel tw-hidden">
<div>{{ctx.Locale.Tr "settings.cropper_prompt"}}</div>
<div class="cropper-wrapper"><img class="cropper-source" src alt></div>
</div>

<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "settings.update_avatar"}}</button>
<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{ctx.Locale.Tr "settings.delete_current_avatar"}}</button>
Expand Down
6 changes: 6 additions & 0 deletions web_src/css/features/cropper.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@import "cropperjs/dist/cropper.css";

.page-content.user.profile .cropper-panel .cropper-wrapper {
max-width: 400px;
max-height: 400px;
}
1 change: 1 addition & 0 deletions web_src/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
@import "./features/codeeditor.css";
@import "./features/projects.css";
@import "./features/tribute.css";
@import "./features/cropper.css";
@import "./features/console.css";

@import "./markup/content.css";
Expand Down
40 changes: 40 additions & 0 deletions web_src/js/features/comp/Cropper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {showElem} from '../../utils/dom.ts';

type CropperOpts = {
container: HTMLElement,
imageSource: HTMLImageElement,
fileInput: HTMLInputElement,
}

export async function initCompCropper({container, fileInput, imageSource}: CropperOpts) {
const {default: Cropper} = await import(/* webpackChunkName: "cropperjs" */'cropperjs');
let currentFileName = '';
let currentFileLastModified = 0;
const cropper = new Cropper(imageSource, {
aspectRatio: 1,
viewMode: 2,
autoCrop: false,
crop() {
const canvas = cropper.getCroppedCanvas();
canvas.toBlob((blob) => {
const croppedFileName = currentFileName.replace(/\.[^.]{3,4}$/, '.png');
const croppedFile = new File([blob], croppedFileName, {type: 'image/png', lastModified: currentFileLastModified});
const dataTransfer = new DataTransfer();
dataTransfer.items.add(croppedFile);
fileInput.files = dataTransfer.files;
});
},
});

fileInput.addEventListener('input', (e: Event & {target: HTMLInputElement}) => {
const files = e.target.files;
if (files?.length > 0) {
currentFileName = files[0].name;
currentFileLastModified = files[0].lastModified;
const fileURL = URL.createObjectURL(files[0]);
imageSource.src = fileURL;
cropper.replace(fileURL);
showElem(container);
}
});
}
8 changes: 4 additions & 4 deletions web_src/js/features/repo-settings-branches.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {beforeEach, describe, expect, test, vi} from 'vitest';
import {initRepoBranchesSettings} from './repo-settings-branches.ts';
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
import {POST} from '../modules/fetch.ts';
import {createSortable} from '../modules/sortable.ts';

Expand Down Expand Up @@ -31,7 +31,7 @@ describe('Repository Branch Settings', () => {
});

test('should initialize sortable for protected branches list', () => {
initRepoBranchesSettings();
initRepoSettingsBranchesDrag();

expect(createSortable).toHaveBeenCalledWith(
document.querySelector('#protected-branches-list'),
Expand All @@ -45,7 +45,7 @@ describe('Repository Branch Settings', () => {
test('should not initialize if protected branches list is not present', () => {
document.body.innerHTML = '';

initRepoBranchesSettings();
initRepoSettingsBranchesDrag();

expect(createSortable).not.toHaveBeenCalled();
});
Expand All @@ -59,7 +59,7 @@ describe('Repository Branch Settings', () => {
return {destroy: vi.fn()};
});

initRepoBranchesSettings();
initRepoSettingsBranchesDrag();

expect(POST).toHaveBeenCalledWith(
'some/repo/branches/priority',
Expand Down
2 changes: 1 addition & 1 deletion web_src/js/features/repo-settings-branches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {queryElemChildren} from '../utils/dom.ts';

export function initRepoBranchesSettings() {
export function initRepoSettingsBranchesDrag() {
const protectedBranchesList = document.querySelector('#protected-branches-list');
if (!protectedBranchesList) return;

Expand Down
4 changes: 2 additions & 2 deletions web_src/js/features/repo-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {minimatch} from 'minimatch';
import {createMonaco} from './codeeditor.ts';
import {onInputDebounce, queryElems, toggleElem} from '../utils/dom.ts';
import {POST} from '../modules/fetch.ts';
import {initRepoBranchesSettings} from './repo-settings-branches.ts';
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';

const {appSubUrl, csrfToken} = window.config;

Expand Down Expand Up @@ -155,5 +155,5 @@ export function initRepoSettings() {
initRepoSettingsCollaboration();
initRepoSettingsSearchTeamBox();
initRepoSettingsGitHook();
initRepoBranchesSettings();
initRepoSettingsBranchesDrag();
}
12 changes: 11 additions & 1 deletion web_src/js/features/user-settings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import {hideElem, showElem} from '../utils/dom.ts';
import {initCompCropper} from './comp/Cropper.ts';

function initUserSettingsAvatarCropper() {
const fileInput = document.querySelector<HTMLInputElement>('#new-avatar');
const container = document.querySelector<HTMLElement>('.user.settings.profile .cropper-panel');
const imageSource = container.querySelector<HTMLImageElement>('.cropper-source');
initCompCropper({container, fileInput, imageSource});
}

export function initUserSettings() {
if (!document.querySelectorAll('.user.settings.profile').length) return;
if (!document.querySelector('.user.settings.profile')) return;

initUserSettingsAvatarCropper();

const usernameInput = document.querySelector('#username');
if (!usernameInput) return;
Expand Down
2 changes: 1 addition & 1 deletion web_src/js/modules/sortable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {SortableOptions, SortableEvent} from 'sortablejs';

export async function createSortable(el: HTMLElement, opts: {handle?: string} & SortableOptions = {}) {
export async function createSortable(el: Element, opts: {handle?: string} & SortableOptions = {}) {
// @ts-expect-error: wrong type derived by typescript
const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs');

Expand Down

0 comments on commit 68d9f36

Please sign in to comment.