Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,6 @@
"error_get_user_info": "Failed to get user info: {{status}}",
"error_get_file": "The path is a directory, not a file.",
"github_token_not_found": "GitHub token not found.\nPlease bind your account in the Settings page of the extension.",
"gitee_token_not_found": "Gitee token not found.\nPlease bind your account in the Settings page of the extension."
"gitee_token_not_found": "Gitee token not found.\nPlease bind your account in the Settings page of the extension.",
"tips_loading_avatars": "Loading {{currentMonth}} avatars {{loadedAvatars}}/{{totalAvatars}} "
}
3 changes: 2 additions & 1 deletion src/locales/zh_CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,6 @@
"error_get_user_info": "获取用户信息失败:{{status}}",
"error_get_file": "该路径是一个目录,而不是文件",
"github_token_not_found": "GitHub 的 token 没有找到。\n请记得在扩展程序的设置页面中绑定您的账号。",
"gitee_token_not_found": "Gitee 的 token 没有找到。\n请记得在扩展程序的设置页面中绑定您的账号。"
"gitee_token_not_found": "Gitee 的 token 没有找到。\n请记得在扩展程序的设置页面中绑定您的账号。",
"tips_loading_avatars": "加载 {{currentMonth}} 的头像 {{loadedAvatars}}/{{totalAvatars}} "
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// @ts-ignore - TS7016: Could not find a declaration file for module 'colorthief'.
import ColorThief from 'colorthief';

type LoginId = string;
type Color = string;
type RGB = [number, number, number];
interface ColorCache {
colors: Color[];
lastUpdated: number; // timestamp
[month: string]: {
[loginId: string]: {
colors: Color[];
lastUpdated: number;
};
};
}

/** The number determines how many colors are extracted from the image */
Expand All @@ -32,41 +35,32 @@ class AvatarColorStore {
img.src = `https://avatars.githubusercontent.com/${loginId}?s=8&v=4`;
});
}
private cache: ColorCache = {};

public async getColors(loginId: LoginId): Promise<Color[]> {
// Create a unique key for this user's cache entry.
const cacheKey = `color-cache:${loginId}`;
public async getColors(month: string, loginId: string): Promise<Color[]> {
const now = Date.now();

const now = new Date().getTime();
const colorCache: ColorCache = (await chrome.storage.local.get(cacheKey))[cacheKey];
if (this.cache[month]?.[loginId] && now - this.cache[month][loginId].lastUpdated < CACHE_EXPIRE_TIME) {
return this.cache[month][loginId].colors;
}

// Check if the cache is stale or doesn't exist.
if (!colorCache || now - colorCache.lastUpdated > CACHE_EXPIRE_TIME) {
let colors: Color[];
// a single white color causes error: https://github.com/lokesh/color-thief/issues/40#issuecomment-802424484
try {
colors = await this.loadAvatar(loginId)
.then((img) => this.colorThief.getPalette(img, COLOR_COUNT, COLOR_QUALITY))
.then((rgbs) => {
return rgbs.map((rgb: RGB) => `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`);
});
// Store the updated cache entry with the unique key.
await chrome.storage.local.set({
[cacheKey]: {
colors,
lastUpdated: now,
},
});
} catch (error) {
console.error(`Cannot extract colors of the avatar of ${loginId}, error info: `, error);
colors = Array(COLOR_COUNT).fill('rgb(255, 255, 255)');
}
try {
const img = await this.loadAvatar(loginId);
const rgbs = await this.colorThief.getPalette(img, COLOR_COUNT, COLOR_QUALITY);
const colors = rgbs.map((rgb: RGB) => `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`);

if (!this.cache[month]) this.cache[month] = {};
this.cache[month][loginId] = { colors, lastUpdated: now };
return colors;
} catch (error) {
return Array(COLOR_COUNT).fill('rgb(255, 255, 255)');
}

// Return the cached colors.
return colorCache.colors;
}
public async preloadMonth(month: string, contributors: string[]) {
if (!contributors) return;
contributors.forEach((contributor) => {
this.getColors(month, contributor).catch(() => {});
});
}

public static getInstance(): AvatarColorStore {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import React, { useEffect, useState, useRef, forwardRef, useImperativeHandle, Fo
import { Spin } from 'antd';
import * as echarts from 'echarts';
import type { EChartsType } from 'echarts';
import { avatarColorStore } from './AvatarColorStore';
import i18n from '../../../../helpers/i18n';
const t = i18n.t;

export interface MediaControlers {
play: () => void;
Expand All @@ -31,11 +34,38 @@ const RacingBar = forwardRef(

const months = Object.keys(data);
const monthIndexRef = useRef<number>(months.length - 1);
const [longTermContributorsCount] = countLongTermContributors(data);

const [longTermContributorsCount, contributors] = countLongTermContributors(data);
const maxBars = longTermContributorsCount >= 20 ? 20 : 10;
const height = longTermContributorsCount >= 20 ? 600 : 300;
const [loadedAvatars, loadAvatars] = useLoadedAvatars(contributors);
const maxBars = longTermContributorsCount >= 10 ? 15 : 10;
const height = longTermContributorsCount >= 10 ? 450 : 300;

const [currentMonth, setCurrentMonth] = useState(months[months.length - 1]);
const currentContributors = data[currentMonth]?.map((item) => item[0]) || [];

const [isLoading, setIsLoading] = useState(false);
const [loadedAvatars, totalAvatars, loadAvatars] = useLoadedAvatars(currentContributors, currentMonth);

useEffect(() => {
const preloadAdjacentMonths = async () => {
const [year, month] = currentMonth.split('-').map(Number);
const prevMonth = new Date(year, month - 2, 1);
const nextMonth = new Date(year, month, 1);
const prevMonthKey = formatDateToMonthKey(prevMonth);
const nextMonthKey = formatDateToMonthKey(nextMonth);

Promise.allSettled([
avatarColorStore.preloadMonth(
prevMonthKey,
data[prevMonthKey]?.map((item) => item[0])
),
avatarColorStore.preloadMonth(
nextMonthKey,
data[nextMonthKey]?.map((item) => item[0])
),
]);
};
preloadAdjacentMonths();
}, [currentMonth]);

const updateMonth = async (instance: EChartsType, month: string, enableAnimation: boolean) => {
const option = await getOption(data, month, speedRef.current, maxBars, enableAnimation);
Expand Down Expand Up @@ -73,21 +103,29 @@ const RacingBar = forwardRef(
}
};

const next = () => {
const next = async () => {
pause();
if (monthIndexRef.current < months.length - 1) {
setIsLoading(true);
const instance = echarts.getInstanceByDom(divEL.current!)!;
monthIndexRef.current++;
setCurrentMonth(months[monthIndexRef.current]);
await loadAvatars();
updateMonth(instance, months[monthIndexRef.current], false);
setIsLoading(false);
}
};

const previous = () => {
const previous = async () => {
pause();
if (monthIndexRef.current > 0) {
setIsLoading(true);
const instance = echarts.getInstanceByDom(divEL.current!)!;
setCurrentMonth(months[monthIndexRef.current]);
await loadAvatars();
monthIndexRef.current--;
updateMonth(instance, months[monthIndexRef.current], false);
setIsLoading(false);
}
};

Expand Down Expand Up @@ -135,9 +173,10 @@ const RacingBar = forwardRef(
return (
<div className="hypertrons-crx-border">
<Spin
spinning={loadedAvatars < contributors.length}
tip={`Loading avatars ${loadedAvatars}/${contributors.length}`}
style={{ maxHeight: 'none' }} // disable maxHeight to make the loading tip be placed in the center
spinning={isLoading || loadedAvatars < totalAvatars}
tip={t('tips_loading_avatars', { currentMonth, loadedAvatars, totalAvatars })}
// tip={`Loading ${currentMonth} avatars (${loadedAvatars}/${totalAvatars})`}
style={{ maxHeight: 'none' }}
>
<div ref={divEL} style={{ width: '100%', height }} />
</Spin>
Expand All @@ -146,4 +185,10 @@ const RacingBar = forwardRef(
}
);

function formatDateToMonthKey(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
return `${year}-${month}`;
}

export default RacingBar;
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function getMonthlyData(data: RepoActivityDetails) {
for (const key in data) {
// Check if the key matches the yyyy-mm format (e.g., "2020-05")
if (/^\d{4}-\d{2}$/.test(key)) {
monthlyData[key] = data[key];
monthlyData[key] = data[key].sort((a, b) => b[1] - a[1]).slice(0, 15);
}
}
return monthlyData;
Expand Down Expand Up @@ -73,14 +73,13 @@ export const getOption = async (
const topData = take(sortedData, maxBars);
const barData: BarSeriesOption['data'] = await Promise.all(
topData.map(async (item) => {
// rich name cannot contain special characters such as '-'
rich[`avatar${item[0].replaceAll('-', '')}`] = {
backgroundColor: {
image: `https://avatars.githubusercontent.com/${item[0]}?s=48&v=4`,
},
height: 20,
};
const avatarColors = await avatarColorStore.getColors(item[0]);
const avatarColors = await avatarColorStore.getColors(month, item[0]);
return {
value: item,
itemStyle: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
import { avatarColorStore } from './AvatarColorStore';

import { useState } from 'react';
import { useEffect, useState } from 'react';

export const useLoadedAvatars = (contributors: string[]): [number, () => void] => {
const [loadedAvatars, setLoadedAvatars] = useState(0);
export const useLoadedAvatars = (contributors: string[], month: string): [number, number, () => void] => {
const [loadedCount, setLoadedCount] = useState(0);
const totalAvatarCount = contributors.length;

const load = async () => {
const promises = contributors.map(async (contributor) => {
await avatarColorStore.getColors(contributor);
setLoadedAvatars((loadedAvatars) => loadedAvatars + 1);
});
await Promise.all(promises);
setLoadedCount(0);

await Promise.all(
contributors.map(async (contributor) => {
try {
await avatarColorStore.getColors(month, contributor);
setLoadedCount((prev) => prev + 1);
} catch (error) {
setLoadedCount((prev) => prev + 1);
}
})
);
};

return [loadedAvatars, load];
useEffect(() => {
load();
}, [month]);

return [loadedCount, totalAvatarCount, load];
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface Props {
repoActivityDetails: RepoActivityDetails;
}

const View = ({ currentRepo, width, repoActivityDetails }: Props): JSX.Element => {
const View = ({ width, repoActivityDetails }: Props): JSX.Element => {
const [options, setOptions] = useState<HypercrxOptions>(defaults);
const [speed, setSpeed] = useState<number>(1);
const [playing, setPlaying] = useState<boolean>(false);
Expand Down
Loading