Skip to content

Commit 68d9f36

Browse files
kerwin612silverwindwxiaoguangdelvhGiteaBot
authored
Allow cropping an avatar before setting it (#32565)
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]>
1 parent f1bea3c commit 68d9f36

File tree

12 files changed

+80
-9
lines changed

12 files changed

+80
-9
lines changed

options/locale/locale_en-US.ini

+1
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,7 @@ uploaded_avatar_not_a_image = The uploaded file is not an image.
765765
uploaded_avatar_is_too_big = The uploaded file size (%d KiB) exceeds the maximum size (%d KiB).
766766
update_avatar_success = Your avatar has been updated.
767767
update_user_avatar_success = The user's avatar has been updated.
768+
cropper_prompt = You can edit the image before saving. The edited image will be saved as PNG.
768769

769770
change_password = Update Password
770771
old_password = Current Password

package-lock.json

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"chartjs-adapter-dayjs-4": "1.0.4",
2222
"chartjs-plugin-zoom": "2.0.1",
2323
"clippie": "4.1.3",
24+
"cropperjs": "1.6.2",
2425
"css-loader": "7.1.2",
2526
"dayjs": "1.11.13",
2627
"dropzone": "6.0.0-beta.2",

templates/user/settings/profile.tmpl

+5
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@
127127
<input id="new-avatar" name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
128128
</div>
129129

130+
<div class="field tw-pl-4 cropper-panel tw-hidden">
131+
<div>{{ctx.Locale.Tr "settings.cropper_prompt"}}</div>
132+
<div class="cropper-wrapper"><img class="cropper-source" src alt></div>
133+
</div>
134+
130135
<div class="field">
131136
<button class="ui primary button">{{ctx.Locale.Tr "settings.update_avatar"}}</button>
132137
<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{ctx.Locale.Tr "settings.delete_current_avatar"}}</button>

web_src/css/features/cropper.css

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@import "cropperjs/dist/cropper.css";
2+
3+
.page-content.user.profile .cropper-panel .cropper-wrapper {
4+
max-width: 400px;
5+
max-height: 400px;
6+
}

web_src/css/index.css

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
@import "./features/codeeditor.css";
4141
@import "./features/projects.css";
4242
@import "./features/tribute.css";
43+
@import "./features/cropper.css";
4344
@import "./features/console.css";
4445

4546
@import "./markup/content.css";

web_src/js/features/comp/Cropper.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {showElem} from '../../utils/dom.ts';
2+
3+
type CropperOpts = {
4+
container: HTMLElement,
5+
imageSource: HTMLImageElement,
6+
fileInput: HTMLInputElement,
7+
}
8+
9+
export async function initCompCropper({container, fileInput, imageSource}: CropperOpts) {
10+
const {default: Cropper} = await import(/* webpackChunkName: "cropperjs" */'cropperjs');
11+
let currentFileName = '';
12+
let currentFileLastModified = 0;
13+
const cropper = new Cropper(imageSource, {
14+
aspectRatio: 1,
15+
viewMode: 2,
16+
autoCrop: false,
17+
crop() {
18+
const canvas = cropper.getCroppedCanvas();
19+
canvas.toBlob((blob) => {
20+
const croppedFileName = currentFileName.replace(/\.[^.]{3,4}$/, '.png');
21+
const croppedFile = new File([blob], croppedFileName, {type: 'image/png', lastModified: currentFileLastModified});
22+
const dataTransfer = new DataTransfer();
23+
dataTransfer.items.add(croppedFile);
24+
fileInput.files = dataTransfer.files;
25+
});
26+
},
27+
});
28+
29+
fileInput.addEventListener('input', (e: Event & {target: HTMLInputElement}) => {
30+
const files = e.target.files;
31+
if (files?.length > 0) {
32+
currentFileName = files[0].name;
33+
currentFileLastModified = files[0].lastModified;
34+
const fileURL = URL.createObjectURL(files[0]);
35+
imageSource.src = fileURL;
36+
cropper.replace(fileURL);
37+
showElem(container);
38+
}
39+
});
40+
}

web_src/js/features/repo-settings-branches.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {beforeEach, describe, expect, test, vi} from 'vitest';
2-
import {initRepoBranchesSettings} from './repo-settings-branches.ts';
2+
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
33
import {POST} from '../modules/fetch.ts';
44
import {createSortable} from '../modules/sortable.ts';
55

@@ -31,7 +31,7 @@ describe('Repository Branch Settings', () => {
3131
});
3232

3333
test('should initialize sortable for protected branches list', () => {
34-
initRepoBranchesSettings();
34+
initRepoSettingsBranchesDrag();
3535

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

48-
initRepoBranchesSettings();
48+
initRepoSettingsBranchesDrag();
4949

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

62-
initRepoBranchesSettings();
62+
initRepoSettingsBranchesDrag();
6363

6464
expect(POST).toHaveBeenCalledWith(
6565
'some/repo/branches/priority',

web_src/js/features/repo-settings-branches.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {POST} from '../modules/fetch.ts';
33
import {showErrorToast} from '../modules/toast.ts';
44
import {queryElemChildren} from '../utils/dom.ts';
55

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

web_src/js/features/repo-settings.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {minimatch} from 'minimatch';
33
import {createMonaco} from './codeeditor.ts';
44
import {onInputDebounce, queryElems, toggleElem} from '../utils/dom.ts';
55
import {POST} from '../modules/fetch.ts';
6-
import {initRepoBranchesSettings} from './repo-settings-branches.ts';
6+
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
77

88
const {appSubUrl, csrfToken} = window.config;
99

@@ -155,5 +155,5 @@ export function initRepoSettings() {
155155
initRepoSettingsCollaboration();
156156
initRepoSettingsSearchTeamBox();
157157
initRepoSettingsGitHook();
158-
initRepoBranchesSettings();
158+
initRepoSettingsBranchesDrag();
159159
}

web_src/js/features/user-settings.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
import {hideElem, showElem} from '../utils/dom.ts';
2+
import {initCompCropper} from './comp/Cropper.ts';
3+
4+
function initUserSettingsAvatarCropper() {
5+
const fileInput = document.querySelector<HTMLInputElement>('#new-avatar');
6+
const container = document.querySelector<HTMLElement>('.user.settings.profile .cropper-panel');
7+
const imageSource = container.querySelector<HTMLImageElement>('.cropper-source');
8+
initCompCropper({container, fileInput, imageSource});
9+
}
210

311
export function initUserSettings() {
4-
if (!document.querySelectorAll('.user.settings.profile').length) return;
12+
if (!document.querySelector('.user.settings.profile')) return;
13+
14+
initUserSettingsAvatarCropper();
515

616
const usernameInput = document.querySelector('#username');
717
if (!usernameInput) return;

web_src/js/modules/sortable.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type {SortableOptions, SortableEvent} from 'sortablejs';
22

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

0 commit comments

Comments
 (0)