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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 40 additions & 0 deletions
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

Lines changed: 4 additions & 4 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 11 additions & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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)