diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 122b70924a9ca..38af2fa43ace3 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -763,6 +763,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 = Note: The saved image format after cropping is unified as PNG. change_password = Update Password old_password = Current Password diff --git a/package-lock.json b/package-lock.json index 005c2e5fb50c1..e8c9d89e43b6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,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", @@ -6787,6 +6788,12 @@ } } }, + "node_modules/cropperjs": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz", + "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", diff --git a/package.json b/package.json index c65f0617d87a9..a962c72c804e1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index 9c7e2de218324..7aef9f11c8ebc 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -127,6 +127,24 @@ +
+
+

{{ctx.Locale.Tr "preview"}}

+
+ +
+
+
+
+

{{ctx.Locale.Tr "edit"}}

+ {{ctx.Locale.Tr "settings.cropper_prompt"}} +
+
+ +
+
+
+
diff --git a/web_src/css/features/cropper.css b/web_src/css/features/cropper.css new file mode 100644 index 0000000000000..cbd98f6618683 --- /dev/null +++ b/web_src/css/features/cropper.css @@ -0,0 +1,28 @@ +@import "cropperjs/dist/cropper.css"; + +.cropper-panel { + display: flex; + column-gap: 10px; + + .cropper-editor { + flex: 1; + max-width: 100%; + overflow: hidden; + .cropper-wrapper { + height: 600px; + max-height: 600px; + } + >div { + display: flex; + column-gap: 10px; + } + } + + #cropper-result { + overflow: hidden; + width: 256px; + height: 256px; + max-width: 256px; + max-height: 256px; + } +} diff --git a/web_src/css/index.css b/web_src/css/index.css index 817f6997da2a2..174a4a9cbc392 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -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"; diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts new file mode 100644 index 0000000000000..00d92f9b1a792 --- /dev/null +++ b/web_src/js/features/comp/Cropper.ts @@ -0,0 +1,48 @@ +import {showElem} from '../../utils/dom.ts'; + +export async function initCompCropper() { + const cropperContainer = document.querySelector('#cropper-panel'); + if (!cropperContainer) { + return; + } + + const {default: Cropper} = await import(/* webpackChunkName: "cropperjs" */'cropperjs'); + + const source = document.querySelector('#cropper-source'); + const result = document.querySelector('#cropper-result'); + const input = document.querySelector('#new-avatar'); + + const done = function (url: string, filename: string): void { + source.src = url; + result.src = url; + + if (input._cropper) { + input._cropper.replace(url); + } else { + input._cropper = new Cropper(source, { + aspectRatio: 1, + viewMode: 1, + autoCrop: false, + crop() { + const canvas = input._cropper.getCroppedCanvas(); + result.src = canvas.toDataURL(); + canvas.toBlob((blob) => { + const file = new File([blob], filename, {type: 'image/png', lastModified: Date.now()}); + const container = new DataTransfer(); + container.items.add(file); + input.files = container.files; + }); + }, + }); + } + showElem(cropperContainer); + }; + + input.addEventListener('change', (e: Event & {target: HTMLInputElement}) => { + const files = e.target.files; + + if (files?.length > 0) { + done(URL.createObjectURL(files[0]), files[0].name); + } + }); +} diff --git a/web_src/js/index.ts b/web_src/js/index.ts index eeead37333bd8..dcd2549554025 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -49,6 +49,7 @@ import {initRepoRelease, initRepoReleaseNew} from './features/repo-release.ts'; import {initRepoEditor} from './features/repo-editor.ts'; import {initCompSearchUserBox} from './features/comp/SearchUserBox.ts'; import {initInstall} from './features/install.ts'; +import {initCompCropper} from './features/comp/Cropper.ts'; import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts'; import {initRepoBranchButton} from './features/repo-branch.ts'; import {initCommonOrganization} from './features/common-organization.ts'; @@ -137,6 +138,7 @@ onDomReady(() => { initCompSearchUserBox, initCompWebHookEditor, + initCompCropper, initInstall,