From 4ccc4983e076a0804e8905fd6e13ad00943dc704 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 9 Mar 2025 05:17:29 +0100 Subject: [PATCH 1/8] Upgrade cropperjs to v2 --- package-lock.json | 134 ++++++++++++++++++++++++++-- package.json | 2 +- web_src/css/features/cropper.css | 8 +- web_src/js/features/comp/Cropper.ts | 56 ++++++++---- 4 files changed, 174 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 33b03aafceee9..c784e4d3a642f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", "clippie": "4.1.5", - "cropperjs": "1.6.2", + "cropperjs": "2.0.0", "css-loader": "7.1.2", "dayjs": "1.11.13", "dropzone": "6.0.0-beta.2", @@ -432,6 +432,126 @@ "node": ">=14.0.0" } }, + "node_modules/@cropper/element": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element/-/element-2.0.0.tgz", + "integrity": "sha512-lsthn0nQq73GExUE7Mg/ss6Q3RXADGDv055hxoLFwvl/wGHgy6ZkYlfLZ/VmgBHC6jDK5IgPBFnqrPqlXWSGBA==", + "license": "MIT", + "dependencies": { + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-canvas": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-canvas/-/element-canvas-2.0.0.tgz", + "integrity": "sha512-GPtGJgSm92crJhhhwUsaMw3rz2KfJWWSz7kRAlufFEV/EHTP5+6r6/Z1BCGRna830i+Avqbm435XLOtA7PVJwA==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-crosshair": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-crosshair/-/element-crosshair-2.0.0.tgz", + "integrity": "sha512-KfPfyrdeFvUC31Ws7ATtcalWWSaMtrC6bMoCipZhqbUOE7wZoL4ecDSL6BUOZxPa74awZUqfzirCDjHvheBfyw==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-grid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-grid/-/element-grid-2.0.0.tgz", + "integrity": "sha512-i78SQ0IJTLFveKX6P7svkfMYVdgHrQ8ZmmEw8keFy9n1ZVbK+SK0UHK5FNMRNI/gtVhKJOGEnK/zeyjUdj4Iyw==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-handle": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-handle/-/element-handle-2.0.0.tgz", + "integrity": "sha512-ZJvW+0MkK9E8xYymGdoruaQn2kwjSHFpNSWinjyq6csuVQiCPxlX5ovAEDldmZ9MWePPtWEi3vLKQOo2Yb0T8g==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-image": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-image/-/element-image-2.0.0.tgz", + "integrity": "sha512-9BxiTS/aHRmrjopaFQb9mQQXmx4ruhYHGkDZMVz24AXpMFjUY6OpqrWse/WjzD9tfhMFvEdu17b3VAekcAgpeg==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/element-canvas": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-selection": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-selection/-/element-selection-2.0.0.tgz", + "integrity": "sha512-ensNnbIfJsJ8bhbJTH/RXtk2URFvTOO4TvfRk461n2FPEC588D7rwBmUJxQg74IiTi4y1JbCI+6j+4LyzYBLCQ==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/element-canvas": "^2.0.0", + "@cropper/element-image": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-shade": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-shade/-/element-shade-2.0.0.tgz", + "integrity": "sha512-jv/2bbNZnhU4W+T4G0c8ADocLIZvQFTXgCf2RFDNhI5UVxurzWBnDdb8Mx8LnVplnkTqO+xUmHZYve0CwgWo+Q==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/element-canvas": "^2.0.0", + "@cropper/element-selection": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/element-viewer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/element-viewer/-/element-viewer-2.0.0.tgz", + "integrity": "sha512-zY+3VRN5TvpM8twlphYtXw0tzJL2VgzeK7ufhL1BixVqOdRxwP13TprYIhqwGt9EW/SyJZUiaIu396T89kRX8A==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/element-canvas": "^2.0.0", + "@cropper/element-image": "^2.0.0", + "@cropper/element-selection": "^2.0.0", + "@cropper/utils": "^2.0.0" + } + }, + "node_modules/@cropper/elements": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/elements/-/elements-2.0.0.tgz", + "integrity": "sha512-PQkPo1nUjxLFUQuHYu+6atfHxpX9B41Xribao6wpvmvmNIFML6LQdNqqWYb6LyM7ujsu71CZdBiMT5oetjJVoQ==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.0", + "@cropper/element-canvas": "^2.0.0", + "@cropper/element-crosshair": "^2.0.0", + "@cropper/element-grid": "^2.0.0", + "@cropper/element-handle": "^2.0.0", + "@cropper/element-image": "^2.0.0", + "@cropper/element-selection": "^2.0.0", + "@cropper/element-shade": "^2.0.0", + "@cropper/element-viewer": "^2.0.0" + } + }, + "node_modules/@cropper/utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@cropper/utils/-/utils-2.0.0.tgz", + "integrity": "sha512-cprLYr+7kK3faGgoOsTW9gIn5sefDr2KwOmgyjzIXk+8PLpW8FgFKEg5FoWfRD5zMAmkCBuX6rGKDK3VdUEGrg==", + "license": "MIT" + }, "node_modules/@csstools/css-parser-algorithms": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", @@ -4925,10 +5045,14 @@ } }, "node_modules/cropperjs": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz", - "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==", - "license": "MIT" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-2.0.0.tgz", + "integrity": "sha512-TO2j0Qre01kPHbow4FuTrbdEB4jTmGRySxW49jyEIqlJZuEBfrvCTT0vC3eRB2WBXudDfKi1Onako6DKWKxeAQ==", + "license": "MIT", + "dependencies": { + "@cropper/elements": "^2.0.0", + "@cropper/utils": "^2.0.0" + } }, "node_modules/cross-spawn": { "version": "7.0.6", diff --git a/package.json b/package.json index 37be19eca70cc..e7a880d37b55f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", "clippie": "4.1.5", - "cropperjs": "1.6.2", + "cropperjs": "2.0.0", "css-loader": "7.1.2", "dayjs": "1.11.13", "dropzone": "6.0.0-beta.2", diff --git a/web_src/css/features/cropper.css b/web_src/css/features/cropper.css index f7f8168006056..c9281af7d25bd 100644 --- a/web_src/css/features/cropper.css +++ b/web_src/css/features/cropper.css @@ -1,6 +1,8 @@ -@import "cropperjs/dist/cropper.css"; - .avatar-file-with-cropper + .cropper-panel .cropper-wrapper { - max-width: 400px; + width: 400px; max-height: 400px; } + +cropper-canvas { + height: 400px; +} diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts index aaa169115238d..a833056104e25 100644 --- a/web_src/js/features/comp/Cropper.ts +++ b/web_src/js/features/comp/Cropper.ts @@ -1,4 +1,5 @@ -import {showElem, type DOMEvent} from '../../utils/dom.ts'; +import {CropperCanvas, CropperImage} from 'cropperjs'; +import {createElementFromHTML, showElem, type DOMEvent} from '../../utils/dom.ts'; type CropperOpts = { container: HTMLElement, @@ -7,25 +8,46 @@ type CropperOpts = { } async function initCompCropper({container, fileInput, imageSource}: CropperOpts) { - const {default: Cropper} = await import(/* webpackChunkName: "cropperjs" */'cropperjs'); + 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; - }); - }, + + const canvasEl = createElementFromHTML(` + + + + + + + + + + + + + + + + + + + `); + + const imgEl = canvasEl.querySelector('cropper-image'); + + canvasEl.addEventListener('action', async (e) => { + const canvas = await (e.target as CropperCanvas).$toCanvas(); + 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; + }); }); + imageSource.replaceWith(canvasEl); + fileInput.addEventListener('input', (e: DOMEvent) => { const files = e.target.files; if (files?.length > 0) { @@ -33,7 +55,7 @@ async function initCompCropper({container, fileInput, imageSource}: CropperOpts) currentFileLastModified = files[0].lastModified; const fileURL = URL.createObjectURL(files[0]); imageSource.src = fileURL; - cropper.replace(fileURL); + imgEl.src = fileURL; showElem(container); } }); From cced48119bfeddd2f82d4bb9b25c0b543daec755 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 9 Mar 2025 05:19:42 +0100 Subject: [PATCH 2/8] wip --- web_src/js/features/comp/Cropper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts index a833056104e25..4e7a4169cf8ed 100644 --- a/web_src/js/features/comp/Cropper.ts +++ b/web_src/js/features/comp/Cropper.ts @@ -1,5 +1,5 @@ -import {CropperCanvas, CropperImage} from 'cropperjs'; import {createElementFromHTML, showElem, type DOMEvent} from '../../utils/dom.ts'; +import type {CropperCanvas, CropperImage} from 'cropperjs'; type CropperOpts = { container: HTMLElement, @@ -14,7 +14,7 @@ async function initCompCropper({container, fileInput, imageSource}: CropperOpts) const canvasEl = createElementFromHTML(` - + From 356bf32e353a5624a83cdae5bb7bdf0bc57ae6e7 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 14 Mar 2025 07:14:58 +0100 Subject: [PATCH 3/8] ignore ts error --- web_src/js/features/comp/Cropper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts index 4e7a4169cf8ed..48ff93994c1ec 100644 --- a/web_src/js/features/comp/Cropper.ts +++ b/web_src/js/features/comp/Cropper.ts @@ -55,6 +55,7 @@ async function initCompCropper({container, fileInput, imageSource}: CropperOpts) currentFileLastModified = files[0].lastModified; const fileURL = URL.createObjectURL(files[0]); imageSource.src = fileURL; + // @ts-expect-error - https://github.com/go-gitea/gitea/pull/33827 imgEl.src = fileURL; showElem(container); } From af941ab3b4bddffa3c7809ab4729f345306ed312 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 14 Mar 2025 07:26:29 +0100 Subject: [PATCH 4/8] add theme color, link to avatar settings section from user profile --- templates/shared/user/profile_big_avatar.tmpl | 2 +- templates/user/settings/profile.tmpl | 2 +- web_src/js/features/comp/Cropper.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index 91f04e0b53dc6..c6c3b6d8de2cd 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -1,7 +1,7 @@
{{if eq .SignedUserID .ContextUser.ID}} - + {{/* the size doesn't take affect (and no need to take affect), image size(width) should be controlled by the parent container since this is not a flex layout*/}} {{ctx.AvatarUtils.Avatar .ContextUser 256}} diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index 03c3c18f28c1c..49a79789dbdc1 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -100,7 +100,7 @@

{{ctx.Locale.Tr "settings.avatar"}}

-
+
{{.CsrfTokenHtml}} {{if not .DisableGravatar}} diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts index 48ff93994c1ec..24455406d9491 100644 --- a/web_src/js/features/comp/Cropper.ts +++ b/web_src/js/features/comp/Cropper.ts @@ -13,11 +13,11 @@ async function initCompCropper({container, fileInput, imageSource}: CropperOpts) let currentFileLastModified = 0; const canvasEl = createElementFromHTML(` - + - + From f5d870c1365d930f0ce22da32e103df022cb1027 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 14 Mar 2025 07:29:44 +0100 Subject: [PATCH 5/8] move id to header --- templates/user/settings/profile.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index 49a79789dbdc1..724b09660af4c 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -97,10 +97,10 @@
-

+

{{ctx.Locale.Tr "settings.avatar"}}

-
+
{{.CsrfTokenHtml}} {{if not .DisableGravatar}} From 11feb734bb10720c32157e3718d6dda547bbd003 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 14 Mar 2025 10:22:43 +0100 Subject: [PATCH 6/8] finish --- templates/shared/avatar_upload_crop.tmpl | 2 +- web_src/js/features/comp/Cropper.ts | 101 +++++++++++------------ 2 files changed, 50 insertions(+), 53 deletions(-) diff --git a/templates/shared/avatar_upload_crop.tmpl b/templates/shared/avatar_upload_crop.tmpl index 2c4166fa9c93c..7f2138f60b9d2 100644 --- a/templates/shared/avatar_upload_crop.tmpl +++ b/templates/shared/avatar_upload_crop.tmpl @@ -4,5 +4,5 @@ {{- /* the cropper-panel must be next sibling of the input "avatar" */ -}}
{{ctx.Locale.Tr "settings.cropper_prompt"}}
-
+
diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts index 24455406d9491..c593ca855ed0f 100644 --- a/web_src/js/features/comp/Cropper.ts +++ b/web_src/js/features/comp/Cropper.ts @@ -1,70 +1,67 @@ -import {createElementFromHTML, showElem, type DOMEvent} from '../../utils/dom.ts'; -import type {CropperCanvas, CropperImage} from 'cropperjs'; +import {createElementFromHTML, hideElem, showElem, type DOMEvent} from '../../utils/dom.ts'; +import {debounce} from 'perfect-debounce'; +import type {CropperCanvas, CropperSelection} from 'cropperjs'; type CropperOpts = { container: HTMLElement, - imageSource: HTMLImageElement, + wrapper: HTMLDivElement, fileInput: HTMLInputElement, } -async function initCompCropper({container, fileInput, imageSource}: CropperOpts) { +async function initCompCropper({container, fileInput, wrapper}: CropperOpts) { await import(/* webpackChunkName: "cropperjs" */'cropperjs'); - let currentFileName = ''; - let currentFileLastModified = 0; - const canvasEl = createElementFromHTML(` - - - - - - - - - - - - - - - - - - - `); - - const imgEl = canvasEl.querySelector('cropper-image'); + fileInput.addEventListener('input', (e: DOMEvent) => { + if (!e.target.files?.length) { + wrapper.replaceChildren(); + hideElem(container); + return; + } - canvasEl.addEventListener('action', async (e) => { - const canvas = await (e.target as CropperCanvas).$toCanvas(); - 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; - }); - }); + const [file] = e.target.files; + const objectUrl = URL.createObjectURL(file); + const canvasEl = createElementFromHTML(` + + + + + + + + + + + + + + + + + `); + canvasEl.querySelector('cropper-selection').addEventListener('change', debounce(async (e) => { + const selection = e.target as CropperSelection; + if (!selection.width || !selection.height) return; + const canvas = await selection.$toCanvas(); - imageSource.replaceWith(canvasEl); + canvas.toBlob((blob) => { + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(new File( + [blob], + file.name.replace(/\.[^.]{3,4}$/, '.png'), + {type: 'image/png', lastModified: file.lastModified}, + )); + fileInput.files = dataTransfer.files; + }); + }, 200)); - fileInput.addEventListener('input', (e: DOMEvent) => { - 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; - // @ts-expect-error - https://github.com/go-gitea/gitea/pull/33827 - imgEl.src = fileURL; - showElem(container); - } + wrapper.replaceChildren(canvasEl); + showElem(container); }); } export async function initAvatarUploaderWithCropper(fileInput: HTMLInputElement) { const panel = fileInput.nextElementSibling as HTMLElement; if (!panel?.matches('.cropper-panel')) throw new Error('Missing cropper panel for avatar uploader'); - const imageSource = panel.querySelector('.cropper-source'); - await initCompCropper({container: panel, fileInput, imageSource}); + const wrapper = panel.querySelector('.cropper-wrapper'); + await initCompCropper({container: panel, fileInput, wrapper}); } From b8b54d8f17b6d54f88cacfc2823f54d018892e0c Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 14 Mar 2025 10:40:19 +0100 Subject: [PATCH 7/8] rename variable --- web_src/js/features/comp/Cropper.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts index c593ca855ed0f..2b4afe21d6165 100644 --- a/web_src/js/features/comp/Cropper.ts +++ b/web_src/js/features/comp/Cropper.ts @@ -20,7 +20,7 @@ async function initCompCropper({container, fileInput, wrapper}: CropperOpts) { const [file] = e.target.files; const objectUrl = URL.createObjectURL(file); - const canvasEl = createElementFromHTML(` + const cropperCanvas = createElementFromHTML(` @@ -38,7 +38,7 @@ async function initCompCropper({container, fileInput, wrapper}: CropperOpts) { `); - canvasEl.querySelector('cropper-selection').addEventListener('change', debounce(async (e) => { + cropperCanvas.querySelector('cropper-selection').addEventListener('change', debounce(async (e) => { const selection = e.target as CropperSelection; if (!selection.width || !selection.height) return; const canvas = await selection.$toCanvas(); @@ -54,7 +54,7 @@ async function initCompCropper({container, fileInput, wrapper}: CropperOpts) { }); }, 200)); - wrapper.replaceChildren(canvasEl); + wrapper.replaceChildren(cropperCanvas); showElem(container); }); } From 8f17dbc4f0cc4609cc04f472133152580920626e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Mon, 17 Mar 2025 10:04:30 +0800 Subject: [PATCH 8/8] use two words for id to avoid conflicts --- templates/shared/user/profile_big_avatar.tmpl | 2 +- templates/user/settings/profile.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index c6c3b6d8de2cd..d94ba0499e0b4 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -1,7 +1,7 @@
{{if eq .SignedUserID .ContextUser.ID}} - + {{/* the size doesn't take affect (and no need to take affect), image size(width) should be controlled by the parent container since this is not a flex layout*/}} {{ctx.AvatarUtils.Avatar .ContextUser 256}} diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index 724b09660af4c..abf9a3f16add9 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -97,7 +97,7 @@
-

+

{{ctx.Locale.Tr "settings.avatar"}}