forked from mirrors/forgejo
[v14.0/forgejo] fix(ui): don't stretch activity top author image (#10628)
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/10556 This is a followup to !10524. In addition I changed that the tooltip triggers for the whole height, instead for only the bar height, because otherwise it is esp for small bars nearly impossible to get the tooltip to open. Co-authored-by: Beowulf <beowulf@beocode.eu> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10628 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Reviewed-by: Beowulf <beowulf@beocode.eu> Co-authored-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org> Co-committed-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
This commit is contained in:
parent
8514af643d
commit
15f891abd7
2 changed files with 103 additions and 50 deletions
28
web_src/js/components/RepoActivityTopAuthors.test.js
Normal file
28
web_src/js/components/RepoActivityTopAuthors.test.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import {flushPromises, mount} from '@vue/test-utils';
|
||||
import RepoActivityTopAuthors from './RepoActivityTopAuthors.vue';
|
||||
import {expect, test, vi} from 'vitest';
|
||||
|
||||
test('calc image size and shift', async () => {
|
||||
vi.spyOn(RepoActivityTopAuthors.methods, 'init').mockResolvedValue({});
|
||||
|
||||
const repoActivityTopAuthors = mount(RepoActivityTopAuthors, {
|
||||
props: {
|
||||
locale: {
|
||||
commitActivity: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const square = repoActivityTopAuthors.vm.calcImageSizeAndShift({naturalWidth: 50, naturalHeight: 50});
|
||||
expect(square).toEqual([20, 20, 0, 0]);
|
||||
|
||||
const portrait = repoActivityTopAuthors.vm.calcImageSizeAndShift({naturalWidth: 5, naturalHeight: 50});
|
||||
expect(portrait).toEqual([2, 20, 9, 0]);
|
||||
|
||||
const landscape = repoActivityTopAuthors.vm.calcImageSizeAndShift({naturalWidth: 500, naturalHeight: 5});
|
||||
expect(landscape).toEqual([20, 0.2, 0, 9.9]);
|
||||
});
|
||||
|
|
@ -42,56 +42,7 @@ export default {
|
|||
i18nCommitActivity: this,
|
||||
}),
|
||||
mounted() {
|
||||
const refStyle = window.getComputedStyle(this.$refs.style);
|
||||
this.colors.barColor = refStyle.backgroundColor;
|
||||
|
||||
for (const item of this.activityTopAuthors) {
|
||||
const img = new Image();
|
||||
img.src = item.avatar_link;
|
||||
item.avatar_img = img;
|
||||
}
|
||||
|
||||
Chart.register({
|
||||
id: 'image_label',
|
||||
afterDraw: (chart) => {
|
||||
const xAxis = chart.boxes[0];
|
||||
const yAxis = chart.boxes[1];
|
||||
for (const [index] of xAxis.ticks.entries()) {
|
||||
const x = xAxis.getPixelForTick(index);
|
||||
const img = this.activityTopAuthors[index].avatar_img;
|
||||
|
||||
chart.ctx.save();
|
||||
chart.ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, x - 10, yAxis.bottom + 10, 20, 20);
|
||||
chart.ctx.restore();
|
||||
}
|
||||
},
|
||||
beforeEvent: (chart, args) => {
|
||||
const event = args.event;
|
||||
if (event.type !== 'mousemove' && event.type !== 'click') return;
|
||||
|
||||
const yAxis = chart.boxes[1];
|
||||
if (event.y < yAxis.bottom + 10 || event.y > yAxis.bottom + 30) {
|
||||
chart.canvas.style.cursor = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const xAxis = chart.boxes[0];
|
||||
const pointIdx = xAxis.ticks.findIndex((_, index) => {
|
||||
const x = xAxis.getPixelForTick(index);
|
||||
return event.x >= x - 10 && event.x <= x + 10;
|
||||
});
|
||||
|
||||
if (pointIdx === -1) {
|
||||
chart.canvas.style.cursor = '';
|
||||
return;
|
||||
}
|
||||
|
||||
chart.canvas.style.cursor = 'pointer';
|
||||
if (event.type === 'click' && this.activityTopAuthors[pointIdx].home_link) {
|
||||
window.location.href = this.activityTopAuthors[pointIdx].home_link;
|
||||
}
|
||||
},
|
||||
});
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
graphPoints() {
|
||||
|
|
@ -137,8 +88,82 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
init() {
|
||||
const refStyle = window.getComputedStyle(this.$refs.style);
|
||||
this.colors.barColor = refStyle.backgroundColor;
|
||||
|
||||
for (const item of this.activityTopAuthors) {
|
||||
const img = new Image();
|
||||
img.src = item.avatar_link;
|
||||
item.avatar_img = img;
|
||||
}
|
||||
|
||||
Chart.register({
|
||||
id: 'image_label',
|
||||
afterDraw: (chart) => {
|
||||
const xAxis = chart.boxes[0];
|
||||
const yAxis = chart.boxes[1];
|
||||
for (const [index] of xAxis.ticks.entries()) {
|
||||
const x = xAxis.getPixelForTick(index);
|
||||
const img = this.activityTopAuthors[index].avatar_img;
|
||||
|
||||
chart.ctx.save();
|
||||
const [width, height, dx, dy] = this.calcImageSizeAndShift(img);
|
||||
chart.ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, x - 10 + dx, yAxis.bottom + 10 + dy, width, height);
|
||||
chart.ctx.restore();
|
||||
}
|
||||
},
|
||||
beforeEvent: (chart, args) => {
|
||||
const event = args.event;
|
||||
if (event.type !== 'mousemove' && event.type !== 'click') return;
|
||||
|
||||
const yAxis = chart.boxes[1];
|
||||
if (event.y < yAxis.bottom + 10 || event.y > yAxis.bottom + 30) {
|
||||
chart.canvas.style.cursor = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const xAxis = chart.boxes[0];
|
||||
const pointIdx = xAxis.ticks.findIndex((_, index) => {
|
||||
const x = xAxis.getPixelForTick(index);
|
||||
return event.x >= x - 10 && event.x <= x + 10;
|
||||
});
|
||||
|
||||
if (pointIdx === -1) {
|
||||
chart.canvas.style.cursor = '';
|
||||
return;
|
||||
}
|
||||
|
||||
chart.canvas.style.cursor = 'pointer';
|
||||
if (event.type === 'click' && this.activityTopAuthors[pointIdx].home_link) {
|
||||
window.location.href = this.activityTopAuthors[pointIdx].home_link;
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
calcImageSizeAndShift(img) {
|
||||
const targetSize = 20;
|
||||
const [imgWidth, imgHeight] = [img.naturalWidth, img.naturalHeight];
|
||||
|
||||
// The image should be contained in a square,
|
||||
// so the scale depends on the longer dimension.
|
||||
const scale = targetSize / (Math.max(imgWidth, imgHeight));
|
||||
const calcScale = (size) => size * scale;
|
||||
const [width, height] = [calcScale(imgWidth), calcScale(imgHeight)];
|
||||
|
||||
// The image should be centered in the 20x20 square.
|
||||
const calcShift = (size) => (targetSize - size) / 2;
|
||||
const [dx, dy] = [calcShift(width), calcShift(height)];
|
||||
|
||||
return [width, height, dx, dy];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue