33 | // No matter the repo is starred or not, the two button are always there
34 | // Select all star buttons and no more filtering
35 |
36 | const $starButtons = $(starButtonSelector);
37 | $starButtons.each(function () {
38 | const placeholderElement = $('
').appendTo('body')[0];
39 | createRoot(placeholderElement).render(
40 |
41 |
42 |
43 | );
44 | });
45 | };
46 |
47 | const restore = async () => {};
48 |
49 | features.add(featureId, {
50 | asLongAs: [isGithub, isPublicRepoWithMeta, hasRepoContainerHeader],
51 | awaitDomReady: false,
52 | init,
53 | restore,
54 | });
55 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-activity-openrank-trends/gitee-index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import $ from 'jquery';
3 | import { createRoot } from 'react-dom/client';
4 | import features from '../../../../feature-manager';
5 | import { getRepoName, isPublicRepoWithMeta, isRepoRoot } from '../../../../helpers/get-gitee-repo-info';
6 | import { getActivity, getOpenrank } from '../../../../api/repo';
7 | import { RepoMeta, metaStore } from '../../../../api/common';
8 | import View from './view';
9 | import { getPlatform } from '../../../../helpers/get-platform';
10 | import isGitee from '../../../../helpers/is-gitee';
11 | const featureId = features.getFeatureID(import.meta.url);
12 | let repoName: string;
13 | let activity: any;
14 | let openrank: any;
15 | let meta: RepoMeta;
16 | let platform: string;
17 | const getData = async () => {
18 | activity = await getActivity(platform, repoName);
19 | openrank = await getOpenrank(platform, repoName);
20 | meta = (await metaStore.get(platform, repoName)) as RepoMeta;
21 | };
22 |
23 | const renderTo = (container: any) => {
24 | createRoot(container).render(
);
25 | };
26 |
27 | const init = async (): Promise
=> {
28 | platform = getPlatform();
29 | repoName = getRepoName();
30 | await getData();
31 |
32 | // create container
33 | const newBorderGridRow = document.createElement('div');
34 | newBorderGridRow.id = featureId;
35 | newBorderGridRow.className = 'side-item trend';
36 | newBorderGridRow.style.marginBottom = '0';
37 | newBorderGridRow.style.fontWeight = '600';
38 | newBorderGridRow.style.fontSize = '16px';
39 | renderTo(newBorderGridRow);
40 | const borderGridRows = $('div.side-item.contrib');
41 | borderGridRows.after(newBorderGridRow);
42 | };
43 |
44 | const restore = async () => {
45 | // Clicking another repo link in one repo will trigger a turbo:visit,
46 | // so in a restoration visit we should be careful of the current repo.
47 | if (repoName !== getRepoName()) {
48 | repoName = getRepoName();
49 | await getData();
50 | }
51 | // rerender the chart or it will be empty
52 | renderTo($(`#${featureId}`).children('.BorderGrid-cell')[0]);
53 | };
54 |
55 | features.add(featureId, {
56 | asLongAs: [isGitee, isPublicRepoWithMeta, isRepoRoot],
57 | awaitDomReady: true,
58 | init,
59 | restore,
60 | });
61 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-activity-openrank-trends/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import $ from 'jquery';
3 | import { createRoot } from 'react-dom/client';
4 | import features from '../../../../feature-manager';
5 | import { getRepoName, isPublicRepoWithMeta, isRepoRoot } from '../../../../helpers/get-github-repo-info';
6 | import { getActivity, getOpenrank } from '../../../../api/repo';
7 | import { RepoMeta, metaStore } from '../../../../api/common';
8 | import View from './view';
9 | import isGithub from '../../../../helpers/is-github';
10 | import { getPlatform } from '../../../../helpers/get-platform';
11 | const featureId = features.getFeatureID(import.meta.url);
12 | let repoName: string;
13 | let activity: any;
14 | let openrank: any;
15 | let meta: RepoMeta;
16 | let platform: string;
17 | const getData = async () => {
18 | activity = await getActivity(platform, repoName);
19 | openrank = await getOpenrank(platform, repoName);
20 | meta = (await metaStore.get(platform, repoName)) as RepoMeta;
21 | };
22 |
23 | const renderTo = (container: any) => {
24 | createRoot(container).render();
25 | };
26 |
27 | const init = async (): Promise => {
28 | platform = getPlatform();
29 | repoName = getRepoName();
30 | await getData();
31 |
32 | // create container
33 | const newBorderGridRow = document.createElement('div');
34 | newBorderGridRow.id = featureId;
35 | newBorderGridRow.className = 'BorderGrid-row';
36 | const newBorderGridCell = document.createElement('div');
37 | newBorderGridCell.className = 'BorderGrid-cell';
38 | newBorderGridRow.appendChild(newBorderGridCell);
39 |
40 | renderTo(newBorderGridCell);
41 |
42 | const borderGridRows = $('div.Layout-sidebar').children('.BorderGrid');
43 | borderGridRows.append(newBorderGridRow);
44 | };
45 |
46 | const restore = async () => {
47 | // Clicking another repo link in one repo will trigger a turbo:visit,
48 | // so in a restoration visit we should be careful of the current repo.
49 | if (repoName !== getRepoName()) {
50 | repoName = getRepoName();
51 | await getData();
52 | }
53 | // rerender the chart or it will be empty
54 | renderTo($(`#${featureId}`).children('.BorderGrid-cell')[0]);
55 | };
56 |
57 | features.add(featureId, {
58 | asLongAs: [isGithub, isPublicRepoWithMeta, isRepoRoot],
59 | awaitDomReady: true,
60 | init,
61 | restore,
62 | });
63 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/developer-activity-openrank-trends/view.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import getGithubTheme from '../../../../helpers/get-github-theme';
3 | import generateDataByMonth from '../../../../helpers/generate-data-by-month';
4 | import optionsStorage, { HypercrxOptions, defaults } from '../../../../options-storage';
5 | import Bars from '../../../../components/Bars';
6 | import { UserMeta } from '../../../../api/common';
7 | import { useTranslation } from 'react-i18next';
8 | import '../../../../helpers/i18n';
9 | import isGithub from '../../../../helpers/is-github';
10 | const theme = isGithub() ? getGithubTheme() : 'light';
11 |
12 | const generateBarsData = (activity: any, openrank: any, updatedAt: number) => {
13 | return {
14 | data1: generateDataByMonth(activity, updatedAt),
15 | data2: generateDataByMonth(openrank, updatedAt),
16 | };
17 | };
18 |
19 | interface Props {
20 | activity: any;
21 | openrank: any;
22 | meta: UserMeta;
23 | }
24 |
25 | const View = ({ activity, openrank, meta }: Props): JSX.Element | null => {
26 | const [options, setOptions] = useState(defaults);
27 | const { t, i18n } = useTranslation();
28 | useEffect(() => {
29 | (async function () {
30 | setOptions(await optionsStorage.getAll());
31 | i18n.changeLanguage(options.locale);
32 | })();
33 | }, [options.locale]);
34 |
35 | if (!activity || !openrank) return null;
36 |
37 | let barsData: any = generateBarsData(activity, openrank, meta.updatedAt);
38 | const BarsComponent = (
39 |
49 | );
50 | return isGithub() ? (
51 |
52 |
{t('component_developerActORTrend_title')}
53 | {BarsComponent}
54 |
55 | ) : (
56 |
57 |
{t('component_developerActORTrend_title')}
58 |
{BarsComponent}
59 |
60 | );
61 | };
62 |
63 | export default View;
64 |
--------------------------------------------------------------------------------
/scripts/deploy.js:
--------------------------------------------------------------------------------
1 | import chromeWebstoreUpload from 'chrome-webstore-upload';
2 | import { EdgeAddonsAPI } from '@plasmohq/edge-addons-api';
3 | import { fileURLToPath } from 'url';
4 | import path, { dirname } from 'path';
5 | import fs from 'fs';
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = dirname(__filename);
9 |
10 | const ZIP_PATH = path.join(__dirname, '..', '/release/hypercrx.zip');
11 |
12 | // getting all the credentials and IDs from environment
13 | const CHROME_EXTENSION_ID = process.env.CHROME_EXTENSION_ID;
14 | const CHROME_CLIENT_ID = process.env.CHROME_CLIENT_ID;
15 | const CHROME_CLIENT_SECRET = process.env.CHROME_CLIENT_SECRET;
16 | const CHROME_REFRESH_TOKEN = process.env.CHROME_REFRESH_TOKEN;
17 | const EDGE_PRODUCT_ID = process.env.EDGE_PRODUCT_ID;
18 | const EDGE_CLIENT_ID = process.env.EDGE_CLIENT_ID;
19 | const EDGE_API_KEY = process.env.EDGE_API_KEY;
20 | const deployToChrome = async () => {
21 | const chromeStore = chromeWebstoreUpload({
22 | extensionId: CHROME_EXTENSION_ID,
23 | clientId: CHROME_CLIENT_ID,
24 | clientSecret: CHROME_CLIENT_SECRET,
25 | refreshToken: CHROME_REFRESH_TOKEN,
26 | });
27 |
28 | const zipFile = fs.createReadStream(ZIP_PATH);
29 |
30 | const token = await chromeStore.fetchToken();
31 | const uploadRes = await chromeStore.uploadExisting(zipFile, token);
32 | console.log('chrome uploadRes: ', JSON.stringify(uploadRes, null, 2));
33 |
34 | if (uploadRes.uploadState !== 'FAILURE') {
35 | const publishRes = await chromeStore.publish('default', token);
36 | console.log('chrome publishRes: ', JSON.stringify(publishRes, null, 2));
37 | } else {
38 | process.exit(-1);
39 | }
40 | };
41 |
42 | const deployToEdge = async () => {
43 | const edgeStore = new EdgeAddonsAPI({
44 | productId: EDGE_PRODUCT_ID,
45 | clientId: EDGE_CLIENT_ID,
46 | apiKey: EDGE_API_KEY,
47 | });
48 |
49 | try {
50 | const publishResp = await edgeStore.submit({
51 | filePath: ZIP_PATH,
52 | notes: 'Updating extension.',
53 | });
54 | console.log('edge publishResp:', publishResp);
55 | const operationId = publishResp.split('/').pop();
56 | const status = await edgeStore.getPublishStatus(operationId);
57 | console.log('Publish status:', status);
58 | } catch (error) {
59 | console.error('edge deployment failed:', error.message);
60 | process.exit(-1);
61 | }
62 | };
63 |
64 | (async () => {
65 | console.log('Start deploying to chrome webstore...');
66 | await deployToChrome();
67 |
68 | console.log('Start deploying to edge add-on store...');
69 | await deployToEdge();
70 | })();
71 |
--------------------------------------------------------------------------------
/utils/crx-webpack-plugin/index.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import { join } from 'path';
3 | import mkdirp from 'mkdirp';
4 | import ChromeExtension from 'crx';
5 |
6 | function CrxWebpackPlugin(options) {
7 | this.options = options || {};
8 | if (!this.options.updateUrl) {
9 | this.options.updateUrl = 'http://localhost:8000/';
10 | }
11 | if (!this.options.updateFilename) {
12 | this.options.updateFilename = 'updates.xml';
13 | }
14 |
15 | // remove trailing slash
16 | this.options.updateUrl = this.options.updateUrl.replace(/\/$/, '');
17 |
18 | // setup paths
19 | this.keyFile = this.options.keyFile;
20 | this.outputPath = this.options.outputPath;
21 | this.contentPath = this.options.contentPath;
22 |
23 | // set output info
24 | this.crxName = this.options.name + '.crx';
25 | this.crxFile = join(this.outputPath, this.crxName);
26 | this.updateFile = join(this.outputPath, this.options.updateFilename);
27 | this.updateUrl = this.options.updateUrl + '/' + this.options.updateFilename;
28 |
29 | // initiate crx
30 | this.crx = new ChromeExtension({
31 | privateKey: fs.readFileSync(this.keyFile),
32 | codebase: this.options.updateUrl + '/' + this.crxName,
33 | });
34 | }
35 |
36 | // hook into webpack
37 | CrxWebpackPlugin.prototype.apply = function (compiler) {
38 | var self = this;
39 | self.logger = compiler.getInfrastructureLogger('crx-webpack-plugin');
40 | return compiler.hooks.done.tap('crx-webpack-plugin', function () {
41 | self.package.call(self);
42 | });
43 | };
44 |
45 | // package the extension
46 | CrxWebpackPlugin.prototype.package = function () {
47 | var self = this;
48 | self.crx.load(self.contentPath).then(function () {
49 | self.crx.pack().then(function (buffer) {
50 | mkdirp(self.outputPath)
51 | .then((made) => {
52 | var updateXML = self.crx.generateUpdateXML();
53 | fs.writeFile(self.updateFile, updateXML, function (err) {
54 | if (err) {
55 | self.logger.error(err);
56 | throw err;
57 | }
58 | self.logger.info('wrote updateFile to ' + self.updateFile);
59 | fs.writeFile(self.crxFile, buffer, function (err) {
60 | if (err) {
61 | self.logger.error(err);
62 | throw err;
63 | }
64 | self.logger.info('wrote crxFile to ' + self.crxFile);
65 | });
66 | });
67 | })
68 | .catch((err) => {
69 | self.logger.error(err);
70 | throw err;
71 | });
72 | });
73 | });
74 | };
75 |
76 | export default CrxWebpackPlugin;
77 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # release.yml
2 | # bump a new version, create a new tag, and create a release in Github Releases
3 | name: release
4 |
5 | on:
6 | workflow_dispatch:
7 | inputs:
8 | version:
9 | description: 'Input a new version number to release. (e.g. 1.2.3)'
10 | required: true
11 |
12 | run-name: Release v${{github.event.inputs.version}}
13 | jobs:
14 | release:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Check out repository code
18 | uses: actions/checkout@v3
19 |
20 | - name: Setup Node.js
21 | uses: actions/setup-node@v3
22 | with:
23 | cache: "yarn"
24 |
25 | - name: Bump version
26 | id: bump-version
27 | run: |
28 | yarn install
29 | yarn run update-version ${{github.event.inputs.version}}
30 |
31 | - name: exit if failed to update new version
32 | if: ${{ github.steps.bump-version != 0}}
33 | run: |
34 | echo ${{ github.steps.bump-version}}
35 | exit 1
36 |
37 | - name: Create release commit and tag
38 | run: |
39 | git config --global user.name 'github-actions[bot]'
40 | git config --global user.email 'github-actions[bot]@users.noreply.github.com'
41 | git add .
42 | git commit -m "chore(release): v${{github.event.inputs.version}}"
43 | git tag -a v${{github.event.inputs.version}} -m "Release v${{github.event.inputs.version}}"
44 | git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/$GITHUB_REPOSITORY
45 | git push origin
46 | env:
47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48 |
49 | - name: Build
50 | run: |
51 | yarn install
52 | yarn run build
53 |
54 | - name: Zip to hypercrx.zip
55 | run: zip -j release/hypercrx.zip build/*
56 |
57 | - name: Append version to release file names
58 | run: |
59 | cp release/hypercrx.crx ${{format('release/hypercrx-{0}.crx', github.event.inputs.version)}}
60 | cp release/hypercrx.zip ${{format('release/hypercrx-{0}.zip', github.event.inputs.version)}}
61 |
62 | - name: Release
63 | uses: softprops/action-gh-release@v1
64 | with:
65 | body: ${{ steps.create_changelog.outputs.changelog }}
66 | generate_release_notes: true
67 | files: |
68 | ${{format('release/hypercrx-{0}.crx', github.event.inputs.version)}}
69 | ${{format('release/hypercrx-{0}.zip', github.event.inputs.version)}}
70 | tag_name: v${{github.event.inputs.version}}
71 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-pr-tooltip/gitee-index.tsx:
--------------------------------------------------------------------------------
1 | import features from '../../../../feature-manager';
2 | import View, { PRDetail } from './view';
3 | import elementReady from 'element-ready';
4 | import { getRepoName, isPublicRepoWithMeta } from '../../../../helpers/get-gitee-repo-info';
5 | import { createRoot } from 'react-dom/client';
6 | import {
7 | getPROpened,
8 | getPRMerged,
9 | getPRReviews,
10 | getMergedCodeAddition,
11 | getMergedCodeDeletion,
12 | } from '../../../../api/repo';
13 | import { RepoMeta, metaStore } from '../../../../api/common';
14 |
15 | import React from 'react';
16 | import $ from 'jquery';
17 | import { getPlatform } from '../../../../helpers/get-platform';
18 | import isGitee from '../../../../helpers/is-gitee';
19 | import { GiteeNativePopover } from '../../components/GiteeNativePopover';
20 |
21 | const featureId = features.getFeatureID(import.meta.url);
22 | let repoName: string;
23 | let PRDetail: PRDetail = {
24 | PROpened: null,
25 | PRMerged: null,
26 | PRReviews: null,
27 | mergedCodeAddition: null,
28 | mergedCodeDeletion: null,
29 | };
30 | let meta: RepoMeta;
31 | let platform: string;
32 | const getData = async () => {
33 | PRDetail.PROpened = await getPROpened(platform, repoName);
34 | PRDetail.PRMerged = await getPRMerged(platform, repoName);
35 | PRDetail.PRReviews = await getPRReviews(platform, repoName);
36 | PRDetail.mergedCodeAddition = await getMergedCodeAddition(platform, repoName);
37 | PRDetail.mergedCodeDeletion = await getMergedCodeDeletion(platform, repoName);
38 | meta = (await metaStore.get(platform, repoName)) as RepoMeta;
39 | };
40 |
41 | const init = async (): Promise => {
42 | platform = getPlatform();
43 | repoName = getRepoName();
44 | await getData();
45 | if (Object.keys(PRDetail.mergedCodeAddition || {}).length === 0) {
46 | PRDetail.mergedCodeAddition = null;
47 | }
48 | if (Object.keys(PRDetail.mergedCodeDeletion || {}).length === 0) {
49 | PRDetail.mergedCodeDeletion = null;
50 | }
51 |
52 | await elementReady('a.item[href*="/pulls"]');
53 | const $prTab = $('a.item[href*="/pulls"]');
54 | const placeholderElement = $('').appendTo('body')[0];
55 | createRoot(placeholderElement).render(
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | const restore = async () => {};
63 |
64 | features.add(featureId, {
65 | asLongAs: [isGitee, isPublicRepoWithMeta],
66 | awaitDomReady: false,
67 | init,
68 | restore,
69 | });
70 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/oss-gpt/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import $ from 'jquery';
4 |
5 | import features from '../../../../feature-manager';
6 | import getGithubTheme from '../../../../helpers/get-github-theme';
7 | import { getRepoName, isPublicRepo } from '../../../../helpers/get-github-repo-info';
8 | import View from './view';
9 | import isGithub from '../../../../helpers/is-github';
10 |
11 | interface DocsMetaItem {
12 | type: 'repo' | 'org';
13 | name: string; // GitHub repo name or org name
14 | key: string; // corresponding docs name
15 | }
16 |
17 | const DOCS_META_DATA_URL = 'https://oss.x-lab.info/hypercrx/docsgpt_active_docs.json';
18 | const featureId = features.getFeatureID(import.meta.url);
19 | let repoName: string;
20 | let docsMetaData: DocsMetaItem[];
21 |
22 | const getData = async () => {
23 | const response = await fetch(DOCS_META_DATA_URL);
24 | if (response.ok) {
25 | docsMetaData = await response.json();
26 | } else {
27 | throw new Error('Failed to fetch docs meta data');
28 | }
29 | };
30 |
31 | const getCurrentDocsName = (repoName: string): string | null => {
32 | const orgName = repoName.split('/')[0];
33 | let result = null;
34 | for (const item of docsMetaData) {
35 | if (item.type === 'repo' && item.name === repoName) {
36 | result = item.key;
37 | break;
38 | } else if (item.type === 'org' && item.name === orgName) {
39 | result = item.key;
40 | break;
41 | }
42 | }
43 | return result;
44 | };
45 |
46 | const renderTo = (container: any) => {
47 | createRoot(container).render(
48 |
53 | );
54 | };
55 |
56 | const init = async (): Promise => {
57 | repoName = getRepoName();
58 | await getData();
59 |
60 | const container = document.createElement('div');
61 | container.id = featureId;
62 | container.dataset.repo = repoName; // mark current repo by data-repo
63 | renderTo(container);
64 | $('body').append(container);
65 |
66 | // TODO need a mechanism to remove extra listeners like this one
67 | document.addEventListener('turbo:load', async () => {
68 | if (await isPublicRepo()) {
69 | if (repoName !== getRepoName()) {
70 | repoName = getRepoName();
71 | renderTo($(`#${featureId}`)[0]);
72 | }
73 | } else {
74 | $(`#${featureId}`).remove();
75 | }
76 | });
77 | };
78 |
79 | features.add(featureId, {
80 | include: [isGithub, isPublicRepo],
81 | awaitDomReady: false,
82 | init,
83 | });
84 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-activity-racing-bar/AvatarColorStore.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore - TS7016: Could not find a declaration file for module 'colorthief'.
2 | import ColorThief from 'colorthief';
3 |
4 | type Color = string;
5 | type RGB = [number, number, number];
6 | interface ColorCache {
7 | [month: string]: {
8 | [loginId: string]: {
9 | colors: Color[];
10 | lastUpdated: number;
11 | };
12 | };
13 | }
14 |
15 | /** The number determines how many colors are extracted from the image */
16 | const COLOR_COUNT = 2;
17 | /** The number determines how many pixels are skipped before the next one is sampled. */
18 | const COLOR_QUALITY = 1;
19 | /** The number determines how long the cache is valid. */
20 | const CACHE_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7;
21 |
22 | /**
23 | * A singleton class that stores the avatar colors of users.
24 | */
25 | class AvatarColorStore {
26 | private static instance: AvatarColorStore;
27 | private colorThief = new ColorThief();
28 |
29 | private loadAvatar(loginId: string): Promise {
30 | return new Promise((resolve, reject) => {
31 | const img = new Image();
32 | img.crossOrigin = 'anonymous';
33 | img.onload = () => resolve(img);
34 | img.onerror = reject;
35 | img.src = `https://avatars.githubusercontent.com/${loginId}?s=8&v=4`;
36 | });
37 | }
38 | private cache: ColorCache = {};
39 |
40 | public async getColors(month: string, loginId: string): Promise {
41 | const now = Date.now();
42 |
43 | if (this.cache[month]?.[loginId] && now - this.cache[month][loginId].lastUpdated < CACHE_EXPIRE_TIME) {
44 | return this.cache[month][loginId].colors;
45 | }
46 |
47 | try {
48 | const img = await this.loadAvatar(loginId);
49 | const rgbs = await this.colorThief.getPalette(img, COLOR_COUNT, COLOR_QUALITY);
50 | const colors = rgbs.map((rgb: RGB) => `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`);
51 |
52 | if (!this.cache[month]) this.cache[month] = {};
53 | this.cache[month][loginId] = { colors, lastUpdated: now };
54 | return colors;
55 | } catch (error) {
56 | return Array(COLOR_COUNT).fill('rgb(255, 255, 255)');
57 | }
58 | }
59 | public async preloadMonth(month: string, contributors: string[]) {
60 | if (!contributors) return;
61 | contributors.forEach((contributor) => {
62 | this.getColors(month, contributor).catch(() => {});
63 | });
64 | }
65 |
66 | public static getInstance(): AvatarColorStore {
67 | if (!AvatarColorStore.instance) {
68 | AvatarColorStore.instance = new AvatarColorStore();
69 | }
70 | return AvatarColorStore.instance;
71 | }
72 | }
73 |
74 | export const avatarColorStore = AvatarColorStore.getInstance();
75 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-activity-openrank-trends/view.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import getGithubTheme from '../../../../helpers/get-github-theme';
3 | import generateDataByMonth from '../../../../helpers/generate-data-by-month';
4 | import optionsStorage, { HypercrxOptions, defaults } from '../../../../options-storage';
5 | import Bars from '../../../../components/Bars';
6 | import { RepoMeta } from '../../../../api/common';
7 | import { useTranslation } from 'react-i18next';
8 | import '../../../../helpers/i18n';
9 | import isGithub from '../../../../helpers/is-github';
10 | const theme = isGithub() ? getGithubTheme() : 'light';
11 |
12 | const generateBarsData = (activity: any, openrank: any, updatedAt: number) => {
13 | return {
14 | data1: generateDataByMonth(activity, updatedAt),
15 | data2: generateDataByMonth(openrank, updatedAt),
16 | };
17 | };
18 |
19 | interface Props {
20 | repoName: string;
21 | activity: any;
22 | openrank: any;
23 | meta: RepoMeta;
24 | }
25 |
26 | const View = ({ repoName, activity, openrank, meta }: Props): JSX.Element | null => {
27 | const [options, setOptions] = useState(defaults);
28 | const { t, i18n } = useTranslation();
29 | useEffect(() => {
30 | (async function () {
31 | setOptions(await optionsStorage.getAll());
32 | i18n.changeLanguage(options.locale);
33 | })();
34 | }, [options.locale]);
35 |
36 | if (!activity || !openrank) return null;
37 |
38 | let barsData: any = generateBarsData(activity, openrank, meta.updatedAt);
39 |
40 | const onClick = (params: any) => {
41 | const { seriesIndex, data } = params;
42 | if (seriesIndex === 0) {
43 | let [year, month] = data.toString().split(',')[0].split('-');
44 | if (month.length < 2) {
45 | month = '0' + month;
46 | }
47 |
48 | window.open(`/${repoName}/issues?q=updated:${year}-${month} sort:updated-asc`);
49 | }
50 | };
51 | const BarsComponent = (
52 |
63 | );
64 | return isGithub() ? (
65 |
66 |
{t('component_repoActORTrend_title')}
67 | {BarsComponent}
68 |
69 | ) : (
70 |
71 |
{t('component_repoActORTrend_title')}
72 |
73 | {BarsComponent}
74 |
75 |
76 | );
77 | };
78 |
79 | export default View;
80 |
--------------------------------------------------------------------------------
/scripts/bump-version.cjs:
--------------------------------------------------------------------------------
1 | // according to https://github.com/TriPSs/conventional-changelog-action#pre-commit-hook
2 | // this script should be a CommonJS module
3 |
4 | const semver =
5 | /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/i;
6 |
7 | function validate({ version }) {
8 | // validate if the given version conforms semver
9 | return String(version).match(semver) === null;
10 | }
11 |
12 | function compare({ oldVersion, newVersion }) {
13 | // compare oldVersion and newVersion number
14 | // return -1 if oldVersion is greater;
15 | // 0 if two versions are equal;
16 | // 1 if newVersion is greater
17 | let [oldMajor, oldMinor, oldPatch] = oldVersion.split('.').map(Number);
18 | let [newMajor, newMinor, newPatch] = newVersion.split('.').map(Number);
19 | if (oldMajor !== newMajor) {
20 | return oldMajor > newMajor ? -1 : 1;
21 | }
22 | if (oldMinor !== newMinor) {
23 | return oldMinor > newMinor ? -1 : 1;
24 | }
25 | if (oldPatch !== newPatch) {
26 | return oldPatch > newPatch ? -1 : 1;
27 | }
28 | return 0;
29 | }
30 |
31 | async function bump({ version, deploy }) {
32 | const { readJson, writeJson, processFile } = require('./utils.cjs');
33 | // update package.json
34 | const pkgPath = 'package.json';
35 | const pkg = await readJson(pkgPath);
36 | if (compare({ oldVersion: pkg.version, newVersion: version }) <= 0) {
37 | throw new Error(
38 | 'Input version number is not greater than the current version number!'
39 | );
40 | }
41 | pkg.version = version;
42 | writeJson(pkgPath, pkg);
43 |
44 | // update update_information.json
45 | const infoPath = 'publish/update_information.json';
46 | const update_info = await readJson(infoPath);
47 | // we only update version number in extension store when deploy
48 | if (deploy) {
49 | update_info.chrome.latest_version = version;
50 | update_info.edge.latest_version = version;
51 | }
52 | if (
53 | compare({
54 | oldVersion: update_info.develop.latest_version,
55 | newVersion: version,
56 | }) <= 0
57 | ) {
58 | throw new Error(
59 | 'Input version number is not greater than the current version number!'
60 | );
61 | }
62 | update_info.develop.latest_version = version;
63 | writeJson(infoPath, update_info);
64 | }
65 |
66 | module.exports = { bump };
67 |
68 | try {
69 | const [nodePath, scriptPath, versionNumber, ...otherArgs] = process.argv;
70 | if (versionNumber !== undefined) {
71 | if (validate({ version: versionNumber })) {
72 | // version number is not valid
73 | throw new Error('Input version number is valid');
74 | }
75 | bump({ version: versionNumber, deploy: true });
76 | }
77 | } catch (error) {
78 | console.error(error);
79 | return -1;
80 | }
81 | return 0;
82 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/perceptor-tab/gitee-index.tsx:
--------------------------------------------------------------------------------
1 | import elementReady from 'element-ready';
2 | import iconSvgPath from './icon-svg-path';
3 | import features from '../../../../feature-manager';
4 | import isPerceptor from '../../../../helpers/is-perceptor';
5 | import { isPublicRepoWithMeta } from '../../../../helpers/get-gitee-repo-info';
6 | import isGitee from '../../../../helpers/is-gitee';
7 |
8 | const featureId = features.getFeatureID(import.meta.url);
9 |
10 | const addPerceptorTab = async (): Promise => {
11 | // Wait for the secondary navigation menu to load
12 | const menuContainer = await elementReady('.ui.secondary.pointing.menu', { waitForChildren: false });
13 | if (!menuContainer) {
14 | return false;
15 | }
16 |
17 | // Create the Perceptor tab based on the pipeline tab
18 | const pipelineTab = await elementReady('a.item[href*="/gitee_go"]', { waitForChildren: false });
19 | if (!pipelineTab) {
20 | return false;
21 | }
22 |
23 | const perceptorTab = pipelineTab.cloneNode(true) as HTMLAnchorElement;
24 | perceptorTab.classList.remove('active');
25 | const perceptorHref = `${location.pathname}?redirect=perceptor`;
26 | perceptorTab.href = perceptorHref;
27 | perceptorTab.id = featureId;
28 |
29 | // Replace the icon and text
30 | const iconElement = perceptorTab.querySelector('i.iconfont') as HTMLElement;
31 | if (iconElement) {
32 | iconElement.className = 'iconfont';
33 | iconElement.innerHTML = ``;
34 | }
35 |
36 | // Clear existing text nodes
37 | perceptorTab.childNodes.forEach((node) => {
38 | if (node.nodeType === Node.TEXT_NODE) {
39 | node.remove();
40 | }
41 | });
42 |
43 | // Add new text node
44 | const textNode = document.createTextNode('\nPerceptor\n');
45 | iconElement?.after(textNode);
46 |
47 | // Add the Perceptor tab before the service dropdown
48 | const serviceDropdown = menuContainer.querySelector('.git-project-service');
49 | if (!serviceDropdown) {
50 | console.error('Failed to find the service dropdown');
51 | return false;
52 | }
53 | serviceDropdown.parentElement?.before(perceptorTab);
54 | };
55 |
56 | const updatePerceptorTabHighlighting = async (): Promise => {
57 | const perceptorTab = document.getElementById(featureId) as HTMLAnchorElement;
58 | if (!perceptorTab) return;
59 |
60 | const allTabs = document.querySelectorAll('.ui.secondary.pointing.menu a.item');
61 | allTabs.forEach((tab) => tab.classList.remove('active'));
62 | perceptorTab.classList.add('active');
63 | };
64 |
65 | const init = async (): Promise => {
66 | await addPerceptorTab();
67 | if (isPerceptor()) {
68 | await updatePerceptorTabHighlighting();
69 | }
70 | };
71 |
72 | features.add(featureId, {
73 | asLongAs: [isGitee, isPublicRepoWithMeta],
74 | awaitDomReady: false,
75 | init,
76 | });
77 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-header-labels/participantView.tsx:
--------------------------------------------------------------------------------
1 | import getGithubTheme from '../../../../helpers/get-github-theme';
2 | import { isNull } from '../../../../helpers/is-null';
3 | import optionsStorage, { HypercrxOptions, defaults } from '../../../../options-storage';
4 | import generateDataByMonth from '../../../../helpers/generate-data-by-month';
5 | import ParticipantChart from './ParticipantChart';
6 | import ContributorChart from './ContributorChart';
7 | import { RepoMeta } from '../../../../api/common';
8 | import React, { useState, useEffect } from 'react';
9 | import TooltipTrigger from '../../../../components/TooltipTrigger';
10 |
11 | import { useTranslation } from 'react-i18next';
12 | import '../../../../helpers/i18n';
13 | import isGithub from '../../../../helpers/is-github';
14 | const theme = isGithub() ? getGithubTheme() : 'light';
15 |
16 | interface Props {
17 | participant: any;
18 | contributor: any;
19 | meta: RepoMeta;
20 | }
21 |
22 | const ParticipantView = ({ participant, contributor, meta }: Props): JSX.Element | null => {
23 | const [options, setOptions] = useState(defaults);
24 | const { t, i18n } = useTranslation();
25 | useEffect(() => {
26 | (async function () {
27 | setOptions(await optionsStorage.getAll());
28 | i18n.changeLanguage(options.locale);
29 | })();
30 | }, [options.locale]);
31 |
32 | if (isNull(participant) || isNull(contributor)) return null;
33 |
34 | const participantData = generateDataByMonth(participant, meta.updatedAt);
35 | const contributorData = generateDataByMonth(contributor, meta.updatedAt);
36 |
37 | return (
38 | <>
39 |
47 |
{t('header_label_contributor')}
48 |
53 |
54 |
55 |
63 |
{t('header_label_participant')}
64 |
69 |
70 |
71 | >
72 | );
73 | };
74 |
75 | export default ParticipantView;
76 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/components/NativePopover.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren, useEffect } from 'react';
2 | import { createRoot, Root } from 'react-dom/client';
3 | import elementReady from 'element-ready';
4 | import $ from 'jquery';
5 | let root: Root | null = null;
6 | interface NativePopoverProps extends PropsWithChildren {
7 | anchor: JQuery;
8 | width: number;
9 | // for now, only support top-middle
10 | arrowPosition: 'top-left' | 'top-middle' | 'top-right' | 'bottom-left' | 'bottom-middle' | 'bottom-right';
11 | }
12 |
13 | export const NativePopover = ({ anchor, width, arrowPosition, children }: NativePopoverProps): JSX.Element => {
14 | useEffect(() => {
15 | (async () => {
16 | await elementReady('div.Popover.js-hovercard-content');
17 | await elementReady('div.Popover-message');
18 | const $popoverContainer = $('div.Popover.js-hovercard-content');
19 | const $popoverContent = $('div.Popover-message');
20 | let popoverTimer: NodeJS.Timeout | null = null;
21 | let leaveTimer: NodeJS.Timeout | null = null;
22 | const showPopover = () => {
23 | popoverTimer = setTimeout(() => {
24 | const anchorOffset = anchor.offset();
25 | const anchorWidth = anchor.outerWidth();
26 | const anchorHeight = anchor.outerHeight();
27 | if (!anchorOffset || !anchorHeight || !anchorWidth) {
28 | return;
29 | }
30 | const { top, left } = anchorOffset;
31 |
32 | $popoverContent.css('padding', '10px 5px');
33 | $popoverContent.css('width', width);
34 | $popoverContainer.css('top', `${top + anchorHeight + 10}px`);
35 | $popoverContainer.css('left', `${left - (width - anchorWidth) / 2}px`);
36 | $popoverContent.attr('class', `Popover-message Box color-shadow-large Popover-message--${arrowPosition}`);
37 | if (root == null) {
38 | root = createRoot($popoverContent[0]);
39 | }
40 |
41 | root.render(children);
42 | $popoverContainer.css('display', 'block');
43 | }, 1000);
44 | };
45 |
46 | const hidePopover = () => {
47 | popoverTimer && clearTimeout(popoverTimer);
48 | $popoverContent.addClass('Popover-message--large');
49 | if (root) {
50 | root.unmount();
51 | root = null;
52 | }
53 | $popoverContainer.css('display', 'none');
54 | };
55 |
56 | anchor[0].addEventListener('mouseenter', () => {
57 | popoverTimer = null;
58 | leaveTimer && clearTimeout(leaveTimer);
59 | showPopover();
60 | });
61 |
62 | anchor[0].addEventListener('mouseleave', () => {
63 | leaveTimer = setTimeout(hidePopover, 200);
64 | });
65 |
66 | anchor[0].addEventListener('click', () => {
67 | hidePopover();
68 | });
69 |
70 | $popoverContainer[0].addEventListener('mouseenter', () => {
71 | leaveTimer && clearTimeout(leaveTimer);
72 | });
73 |
74 | $popoverContainer[0].addEventListener('mouseleave', () => {
75 | leaveTimer = setTimeout(hidePopover, 200);
76 | });
77 | })();
78 | }, []);
79 |
80 | return <>>;
81 | };
82 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-issue-tooltip/view.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | import getGithubTheme from '../../../../helpers/get-github-theme';
4 | import { isNull, isAllNull } from '../../../../helpers/is-null';
5 | import optionsStorage, { HypercrxOptions, defaults } from '../../../../options-storage';
6 | import generateDataByMonth from '../../../../helpers/generate-data-by-month';
7 | import IssueChart from './IssueChart';
8 | import { RepoMeta } from '../../../../api/common';
9 | import TooltipTrigger from '../../../../components/TooltipTrigger';
10 | import { useTranslation } from 'react-i18next';
11 | import '../../../../helpers/i18n';
12 | import isGithub from '../../../../helpers/is-github';
13 | const theme = isGithub() ? getGithubTheme() : 'light';
14 |
15 | export interface IssueDetail {
16 | issuesOpened: any;
17 | issuesClosed: any;
18 | issueComments: any;
19 | }
20 |
21 | interface Props {
22 | currentRepo: string;
23 | issueDetail: IssueDetail;
24 | meta: RepoMeta;
25 | }
26 |
27 | const generateData = (issueDetail: IssueDetail, updatedAt: number): any => {
28 | return {
29 | issuesOpened: generateDataByMonth(issueDetail.issuesOpened, updatedAt),
30 | issuesClosed: generateDataByMonth(issueDetail.issuesClosed, updatedAt),
31 | issueComments: generateDataByMonth(issueDetail.issueComments, updatedAt),
32 | };
33 | };
34 |
35 | const View = ({ currentRepo, issueDetail, meta }: Props): JSX.Element | null => {
36 | const [options, setOptions] = useState(defaults);
37 | const { t, i18n } = useTranslation();
38 | useEffect(() => {
39 | (async function () {
40 | setOptions(await optionsStorage.getAll());
41 | i18n.changeLanguage(options.locale);
42 | })();
43 | }, [options.locale]);
44 |
45 | if (isNull(issueDetail) || isAllNull(issueDetail)) return null;
46 |
47 | const onClick = (curMonth: string, params: any) => {
48 | if (!isGithub()) return;
49 | const seriesIndex = params.seriesIndex;
50 | let type;
51 | if (seriesIndex === 0) {
52 | type = 'created';
53 | } else if (seriesIndex === 1) {
54 | type = 'closed';
55 | } else if (seriesIndex === 2) {
56 | type = 'updated';
57 | }
58 | let [year, month] = curMonth.toString().split(',')[0].split('-');
59 | if (month.length < 2) {
60 | month = '0' + month;
61 | }
62 | window.open(`/${currentRepo}/issues?q=is:issue ${type}:${year}-${month} sort:updated-asc`);
63 | };
64 |
65 | return (
66 | <>
67 |
75 |
{t('issue_popup_title')}
76 |
77 |
78 |
79 |
80 |
87 | >
88 | );
89 | };
90 |
91 | export default View;
92 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/components/GiteeNativePopover.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren, useEffect } from 'react';
2 | import { createRoot, Root } from 'react-dom/client';
3 | import $ from 'jquery';
4 |
5 | let root: Root | null = null;
6 |
7 | interface GiteeNativePopoverProps extends PropsWithChildren {
8 | anchor: JQuery;
9 | width: number;
10 | arrowPosition: string;
11 | }
12 |
13 | export const GiteeNativePopover = ({
14 | anchor,
15 | width,
16 | arrowPosition,
17 | children,
18 | }: GiteeNativePopoverProps): JSX.Element => {
19 | useEffect(() => {
20 | (async () => {
21 | const $popoverContainer = $(`
22 |
29 | `).appendTo('body');
30 |
31 | const $popoverContent = $popoverContainer.find('.popper-profile-card__body');
32 | let popoverTimer: NodeJS.Timeout | null = null;
33 | let leaveTimer: NodeJS.Timeout | null = null;
34 |
35 | const showPopover = () => {
36 | popoverTimer = setTimeout(() => {
37 | const anchorOffset = anchor.offset();
38 | const anchorWidth = anchor.outerWidth();
39 | const anchorHeight = anchor.outerHeight();
40 | if (!anchorOffset || !anchorHeight || !anchorWidth) {
41 | return;
42 | }
43 | const { top, left } = anchorOffset;
44 | $popoverContent.css('padding', '10px 5px');
45 | $popoverContent.css('width', width);
46 | $popoverContainer.css({
47 | top: `${top + anchorHeight}px`,
48 | left: `${left - (width - anchorWidth) / 2}px`,
49 | display: 'block',
50 | transform: `translate3d(0, 0, 0)`,
51 | height: 'auto',
52 | width: width,
53 | });
54 |
55 | if (root == null) {
56 | root = createRoot($popoverContent[0]);
57 | }
58 | root.render(children);
59 | }, 1000);
60 | };
61 |
62 | const hidePopover = () => {
63 | popoverTimer && clearTimeout(popoverTimer);
64 | if (root) {
65 | root.unmount();
66 | root = null;
67 | }
68 | $popoverContainer.css('display', 'none');
69 | };
70 |
71 | anchor[0].addEventListener('mouseenter', () => {
72 | popoverTimer = null;
73 | leaveTimer && clearTimeout(leaveTimer);
74 | showPopover();
75 | });
76 |
77 | anchor[0].addEventListener('mouseleave', () => {
78 | leaveTimer = setTimeout(hidePopover, 200);
79 | });
80 |
81 | $popoverContainer[0].addEventListener('mouseenter', () => {
82 | leaveTimer && clearTimeout(leaveTimer);
83 | });
84 |
85 | $popoverContainer[0].addEventListener('mouseleave', () => {
86 | leaveTimer = setTimeout(hidePopover, 200);
87 | });
88 | })();
89 | }, []);
90 |
91 | return <>>;
92 | };
93 |
--------------------------------------------------------------------------------
/src/api/common.ts:
--------------------------------------------------------------------------------
1 | import { OSS_XLAB_ENDPOINT, ErrorCode } from '../constant';
2 | import request from '../helpers/request';
3 |
4 | export const getMetricByName = async (
5 | platform: string,
6 | owner: string,
7 | metricNameMap: Map,
8 | metric: string
9 | ) => {
10 | try {
11 | return await request(`${OSS_XLAB_ENDPOINT}/${platform}/${owner}/${metricNameMap.get(metric)}.json`);
12 | } catch (error) {
13 | // the catched error being "404" means the metric file is not available so return a null
14 | if (error === ErrorCode.NOT_FOUND) {
15 | return null;
16 | } else {
17 | // other errors should be throwed
18 | throw error;
19 | }
20 | }
21 | };
22 |
23 | export interface Label {
24 | id: string;
25 | name: string;
26 | type: string;
27 | }
28 |
29 | /**
30 | * Common interface for both repo meta and user meta
31 | * e.g. https://oss.open-digger.cn/github/X-lab2017/open-digger/meta.json (repo meta file)
32 | * e.g. https://oss.open-digger.cn/github/tyn1998/meta.json (user meta file)
33 | * @param name repo name or user name
34 | */
35 | export interface CommonMeta {
36 | type: 'user' | 'repo';
37 | updatedAt: number; // time stamp
38 | labels?: Label[];
39 | }
40 |
41 | export interface RepoMeta extends CommonMeta {}
42 |
43 | export interface UserMeta extends CommonMeta {
44 | repos: unknown[];
45 | }
46 |
47 | class MetaStore {
48 | private static instance: MetaStore;
49 | private responseCache: Map>;
50 | private constructor() {
51 | this.responseCache = new Map>();
52 | }
53 |
54 | public static getInstance(): MetaStore {
55 | if (!MetaStore.instance) {
56 | MetaStore.instance = new MetaStore();
57 | }
58 | return MetaStore.instance;
59 | }
60 |
61 | /**
62 | * Fetch the meta file and cache the response
63 | * @param name repo name or user name
64 | */
65 | private fetchMeta(platform: string, name: string) {
66 | const url = `${OSS_XLAB_ENDPOINT}/${platform}/${name}/meta.json`;
67 | const promise = fetch(url);
68 | this.responseCache.set(name, promise);
69 | }
70 |
71 | /**
72 | * Check if the meta file exists
73 | * @param name repo name or user name
74 | * @returns true if the meta file exists, false otherwise
75 | */
76 | public async has(platform: string, name: string) {
77 | if (!this.responseCache.has(name)) {
78 | this.fetchMeta(platform, name);
79 | }
80 | const response = await this.responseCache.get(name)!;
81 | if (!response.ok) {
82 | return false;
83 | } else {
84 | return true;
85 | }
86 | }
87 |
88 | /**
89 | * Get the parsed meta file if it exists
90 | * @param name repo name or user name
91 | * @returns the parsed meta file if it exists, undefined otherwise
92 | */
93 | public async get(platform: string, name: string): Promise {
94 | if (await this.has(platform, name)) {
95 | const meta: CommonMeta = await this.responseCache
96 | .get(name)!
97 | // clone the response to avoid the response being used up
98 | // https://stackoverflow.com/a/54115314/10369621
99 | .then((res) => res.clone().json());
100 | return meta;
101 | }
102 | }
103 | }
104 |
105 | export const metaStore = MetaStore.getInstance();
106 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/perceptor-tab/icon-svg-path.ts:
--------------------------------------------------------------------------------
1 | export default '';
2 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | - Using welcoming and inclusive language
12 | - Being respectful of differing viewpoints and experiences
13 | - Gracefully accepting constructive criticism
14 | - Focusing on what is best for the community
15 | - Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances
20 |
21 | - Trolling, insulting/derogatory comments, and personal or political attacks
22 |
23 | - Public or private harassment
24 |
25 | - Publishing others' private information, such as a physical or electronic address, without explicit permission
26 |
27 | - Other conduct which could reasonably be considered inappropriate in a professional setting
28 |
29 | ## Our Responsibilities
30 |
31 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
32 |
33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
34 |
35 | ## Scope
36 |
37 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
38 |
39 | ## Enforcement
40 |
41 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at syzhao1988@126.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
42 |
43 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
44 |
45 | ## Attribution
46 |
47 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html).
48 |
49 | For answers to common questions about this code of conduct, see [FAQ](https://www.contributor-covenant.org/faq).
50 |
--------------------------------------------------------------------------------
/src/pages/Options/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: -apple-system, 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif, 'Helvetica Neue', Helvetica, Arial,
3 | 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
4 | margin: 0px;
5 | display: flex;
6 | flex-direction: column;
7 | background-color: #1e1e1e;
8 | color: #d4d4d4;
9 | padding: 0px;
10 | transition: all 0.3s ease 0s;
11 | }
12 |
13 | input[type='text'],
14 | textarea {
15 | background-color: #252526;
16 | border: 1px solid #3c3c3c;
17 | color: #dcdcdc;
18 | padding: 8px;
19 | }
20 |
21 | button {
22 | background-color: #007acc;
23 | color: white;
24 | border: none;
25 | padding: 8px 8px;
26 | cursor: pointer;
27 | border-radius: 2px;
28 | }
29 |
30 | button:hover {
31 | background-color: #005a9e;
32 | }
33 |
34 | label {
35 | display: block;
36 | margin-bottom: 5px;
37 | color: #9cdcfe;
38 | }
39 |
40 | .container {
41 | margin: 20px;
42 | padding: 20px;
43 | border: 1px solid #3c3c3c;
44 | border-radius: 4px;
45 | }
46 |
47 | .Box {
48 | width: 75vw;
49 | min-width: 350px;
50 | max-width: 600px;
51 | border-radius: 6px;
52 | }
53 |
54 | .Box-header {
55 | display: flex;
56 | flex-direction: row;
57 | justify-content: flex-start;
58 | align-items: center;
59 | padding: 0 25px;
60 | margin: -1px -1px 0;
61 | background-color: #242a2e;
62 | color: white;
63 | border-top-left-radius: 6px;
64 | border-top-right-radius: 6px;
65 | }
66 |
67 | .Box-header svg.tooltip-icon {
68 | margin-top: 4px;
69 | }
70 |
71 | .Box-title {
72 | font-size: 18px;
73 | font-weight: 600;
74 | margin-right: 8px;
75 | }
76 |
77 | ul {
78 | padding: 0 25px;
79 | }
80 |
81 | li {
82 | padding: 5px 0;
83 | list-style-type: none;
84 | }
85 |
86 | a {
87 | color: #3694de;
88 | }
89 |
90 | .token-options {
91 | width: 75vw;
92 | min-width: 350px;
93 | max-width: 600px;
94 | border-radius: 6px;
95 | margin-top: -10px;
96 | }
97 |
98 | .token-options p {
99 | padding: 0 25px;
100 | }
101 |
102 | .token-options input {
103 | width: calc(100% - 50px);
104 | padding: 8px;
105 | margin: 10px 25px;
106 | border: 1px solid #ccc;
107 | border-radius: 4px;
108 | }
109 |
110 | .ant-radio-wrapper {
111 | color: #d4d4d4;
112 | }
113 |
114 | .ant-radio-inner {
115 | background-color: #252526 !important;
116 | border-color: #3a3d41 !important;
117 | }
118 |
119 | .ant-radio-inner::after {
120 | background-color: #d4d4d4;
121 | opacity: 0;
122 | }
123 |
124 | .ant-radio-checked .ant-radio-inner::after {
125 | opacity: 1;
126 | }
127 |
128 | .ant-radio-checked .ant-radio-inner {
129 | border-color: #3a3d41;
130 | }
131 |
132 | .ant-radio:hover .ant-radio-inner {
133 | border-color: #606060;
134 | }
135 |
136 | .ant-checkbox-wrapper {
137 | color: #d4d4d4;
138 | }
139 |
140 | .ant-checkbox-inner {
141 | background-color: #252526 !important;
142 | border-color: #3a3d41 !important;
143 | }
144 |
145 | .ant-checkbox-checked .ant-checkbox-inner {
146 | background-color: #252526 !important;
147 | border-color: #3a3d41 !important;
148 | }
149 |
150 | .ant-checkbox-checked:hover {
151 | background-color: #252526;
152 | border-color: #3a3d41;
153 | }
154 |
155 | .ant-checkbox-checked .ant-checkbox-inner::after {
156 | border-color: #d4d4d4;
157 | }
158 |
159 | .ant-checkbox:hover .ant-checkbox-inner {
160 | border-color: #606060;
161 | }
162 | .custom-tooltip-option .ant-tooltip-inner {
163 | background-color: #333333 !important;
164 | color: #ffffff !important;
165 | }
166 |
167 | .custom-tooltip-option .ant-tooltip-arrow::before {
168 | background-color: #333333 !important;
169 | }
170 |
--------------------------------------------------------------------------------
/src/api/repo.ts:
--------------------------------------------------------------------------------
1 | import { getMetricByName } from './common';
2 |
3 | // metric names and their implementation names in OpenDigger
4 | const metricNameMap = new Map([
5 | ['activity', 'activity'],
6 | ['openrank', 'openrank'],
7 | ['participant', 'participants'],
8 | ['contributor', 'contributors'],
9 | ['forks', 'technical_fork'],
10 | ['stars', 'stars'],
11 | ['issues_opened', 'issues_new'],
12 | ['issues_closed', 'issues_closed'],
13 | ['issue_comments', 'issue_comments'],
14 | ['PR_opened', 'change_requests'],
15 | ['PR_merged', 'change_requests_accepted'],
16 | ['PR_reviews', 'change_requests_reviews'],
17 | ['merged_code_addition', 'code_change_lines_add'],
18 | ['merged_code_deletion', 'code_change_lines_remove'],
19 | ['merged_code_sum', 'code_change_lines_sum'],
20 | ['developer_network', 'developer_network'],
21 | ['repo_network', 'repo_network'],
22 | ['activity_details', 'activity_details'],
23 | ]);
24 |
25 | export const getActivity = async (platform: string, repo: string) => {
26 | return getMetricByName(platform, repo, metricNameMap, 'activity');
27 | };
28 |
29 | export const getOpenrank = async (platform: string, repo: string) => {
30 | return getMetricByName(platform, repo, metricNameMap, 'openrank');
31 | };
32 |
33 | export const getParticipant = async (platform: string, repo: string) => {
34 | return getMetricByName(platform, repo, metricNameMap, 'participant');
35 | };
36 |
37 | export const getContributor = async (platform: string, repo: string) => {
38 | return getMetricByName(platform, repo, metricNameMap, 'contributor');
39 | };
40 |
41 | export const getForks = async (platform: string, repo: string) => {
42 | return getMetricByName(platform, repo, metricNameMap, 'forks');
43 | };
44 |
45 | export const getStars = async (platform: string, repo: string) => {
46 | return getMetricByName(platform, repo, metricNameMap, 'stars');
47 | };
48 |
49 | export const getIssuesOpened = async (platform: string, repo: string) => {
50 | return getMetricByName(platform, repo, metricNameMap, 'issues_opened');
51 | };
52 |
53 | export const getIssuesClosed = async (platform: string, repo: string) => {
54 | return getMetricByName(platform, repo, metricNameMap, 'issues_closed');
55 | };
56 |
57 | export const getIssueComments = async (platform: string, repo: string) => {
58 | return getMetricByName(platform, repo, metricNameMap, 'issue_comments');
59 | };
60 |
61 | export const getPROpened = async (platform: string, repo: string) => {
62 | return getMetricByName(platform, repo, metricNameMap, 'PR_opened');
63 | };
64 |
65 | export const getPRMerged = async (platform: string, repo: string) => {
66 | return getMetricByName(platform, repo, metricNameMap, 'PR_merged');
67 | };
68 |
69 | export const getPRReviews = async (platform: string, repo: string) => {
70 | return getMetricByName(platform, repo, metricNameMap, 'PR_reviews');
71 | };
72 |
73 | export const getMergedCodeAddition = async (platform: string, repo: string) => {
74 | return getMetricByName(platform, repo, metricNameMap, 'merged_code_addition');
75 | };
76 |
77 | export const getMergedCodeDeletion = async (platform: string, repo: string) => {
78 | return getMetricByName(platform, repo, metricNameMap, 'merged_code_deletion');
79 | };
80 |
81 | export const getMergedCodeSum = async (platform: string, repo: string) => {
82 | return getMetricByName(platform, repo, metricNameMap, 'merged_code_sum');
83 | };
84 |
85 | export const getDeveloperNetwork = async (platform: string, repo: string) => {
86 | return getMetricByName(platform, repo, metricNameMap, 'developer_network');
87 | };
88 |
89 | export const getRepoNetwork = async (platform: string, repo: string) => {
90 | return getMetricByName(platform, repo, metricNameMap, 'repo_network');
91 | };
92 |
93 | export const getActivityDetails = async (platform: string, repo: string) => {
94 | return getMetricByName(platform, repo, metricNameMap, 'activity_details');
95 | };
96 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "HyperCRX",
3 | "version": "1.9.22",
4 | "type": "module",
5 | "private": true,
6 | "description": "Hypertrons Chromium Extension for GitHub and other document websites",
7 | "license": "Apache",
8 | "engines": {
9 | "node": ">=20"
10 | },
11 | "scripts": {
12 | "build": "cross-env NODE_ENV='production' BABEL_ENV='production' node utils/build.js",
13 | "start": "cross-env NODE_ENV='development' BABEL_ENV='development' node utils/server.js",
14 | "deploy": "node scripts/deploy.js",
15 | "prettier": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,scss}\"",
16 | "prettier:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,scss}\"",
17 | "pretty-quick:check": "pretty-quick --staged --check --pattern \"**/*.{js,jsx,ts,tsx,json,css,scss}\"",
18 | "prepare": "husky install",
19 | "update-version": "node scripts/bump-version.cjs"
20 | },
21 | "dependencies": {
22 | "@ant-design/icons": "^5.4.0",
23 | "@ant-design/pro-chat": "^1.9.0",
24 | "@ant-design/pro-editor": "^1.3.0",
25 | "@hot-loader/react-dom": "^17.0.2",
26 | "@octokit/rest": "^21.0.2",
27 | "@types/chrome": "^0.0.203",
28 | "@types/dompurify": "^3.0.5",
29 | "@types/react-copy-to-clipboard": "^5.0.7",
30 | "antd": "^5.20.6",
31 | "antd-style": "^3.6.1",
32 | "buffer": "^6.0.3",
33 | "color": "^4.2.3",
34 | "colorthief": "^2.4.0",
35 | "copy-to-clipboard": "^3.3.3",
36 | "delay": "^5.0.0",
37 | "dom-loaded": "^3.0.0",
38 | "dompurify": "^3.2.4",
39 | "echarts": "^5.3.0",
40 | "element-ready": "^6.2.1",
41 | "github-url-detection": "^8.1.0",
42 | "highlight.js": "^11.10.0",
43 | "i18next": "^23.11.5",
44 | "i18next-browser-languagedetector": "^8.0.0",
45 | "i18next-http-backend": "^2.5.2",
46 | "jquery": "^3.6.0",
47 | "langchain": "^0.2.12",
48 | "lodash-es": "^4.17.21",
49 | "lottie-react": "^2.4.0",
50 | "moment": "^2.30.1",
51 | "openai": "^4.60.1",
52 | "react": "^18.2.0",
53 | "react-chat-widget": "^3.1.4",
54 | "react-dom": "^18.2.0",
55 | "react-hot-loader": "^4.13.0",
56 | "react-i18next": "^14.1.2",
57 | "react-modal": "3.15.1",
58 | "stackedit-js": "^1.0.7",
59 | "stream": "^0.0.3",
60 | "strip-indent": "^4.0.0"
61 | },
62 | "devDependencies": {
63 | "@babel/core": "^7.17.0",
64 | "@babel/plugin-proposal-class-properties": "^7.16.7",
65 | "@babel/preset-env": "^7.16.11",
66 | "@babel/preset-react": "^7.16.7",
67 | "@plasmohq/edge-addons-api": "2.0.0",
68 | "@types/color": "^3.0.6",
69 | "@types/firefox-webext-browser": "^94.0.1",
70 | "@types/jest": "^27.4.0",
71 | "@types/jquery": "^3.5.13",
72 | "@types/lodash-es": "^4.17.8",
73 | "@types/react": "^18.2.8",
74 | "@types/react-dom": "^18.0.5",
75 | "@types/react-modal": "^3.13.1",
76 | "babel-loader": "^8.2.3",
77 | "chrome-webstore-upload": "^1.0.0",
78 | "clean-webpack-plugin": "^4.0.0",
79 | "copy-webpack-plugin": "^7.0.0",
80 | "cross-env": "^7.0.3",
81 | "crx": "^5.0.1",
82 | "css-loader": "^6.6.0",
83 | "file-loader": "^6.2.0",
84 | "fs-extra": "^10.0.0",
85 | "html-loader": "^3.1.0",
86 | "html-webpack-plugin": "^5.5.0",
87 | "husky": "^8.0.1",
88 | "mini-css-extract-plugin": "^2.7.2",
89 | "mkdirp": "^1.0.4",
90 | "prettier": "^3.3.3",
91 | "pretty-quick": "^4.0.0",
92 | "querystring": "^0.2.1",
93 | "sass": "^1.52.1",
94 | "sass-loader": "^12.4.0",
95 | "source-map-loader": "^3.0.1",
96 | "ssestream": "1.0.1",
97 | "terser-webpack-plugin": "^5.3.1",
98 | "ts-loader": "^9.2.6",
99 | "type-fest": "^3.3.0",
100 | "typescript": "^5",
101 | "webpack": "^5.94.0",
102 | "webpack-cli": "^4.9.2",
103 | "webpack-dev-server": "^5.2.1"
104 | },
105 | "resolutions": {
106 | "@types/react": "^18.2.8",
107 | "@types/react-dom": "^18.0.5"
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/fast-pr/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import features from '../../../../feature-manager';
4 | import View from './view';
5 | import i18n from '../../../../helpers/i18n';
6 |
7 | const featureId = features.getFeatureID(import.meta.url);
8 | const t = i18n.t;
9 |
10 | interface MatchedUrl {
11 | filePath: string;
12 | repoName: string;
13 | branch: string;
14 | platform: string;
15 | horizontalRatio: number;
16 | verticalRatio: number;
17 | }
18 |
19 | const CACHE_KEY = 'matchedUrlCache';
20 | const CACHE_EXPIRY = 60 * 60 * 1000;
21 |
22 | const renderTo = (
23 | container: HTMLElement,
24 | filePath: string,
25 | repoName: string,
26 | branch: string,
27 | platform: string,
28 | horizontalRatio: number,
29 | verticalRatio: number
30 | ) => {
31 | createRoot(container).render(
32 |
40 | );
41 | };
42 |
43 | const init = async (matchedUrl: MatchedUrl | null) => {
44 | const existingContainer = document.getElementById(featureId);
45 | if (existingContainer) {
46 | existingContainer.remove();
47 | }
48 | if (matchedUrl) {
49 | const container = document.createElement('div');
50 | container.id = featureId;
51 | renderTo(
52 | container,
53 | matchedUrl.filePath,
54 | matchedUrl.repoName,
55 | matchedUrl.branch,
56 | matchedUrl.platform,
57 | matchedUrl.horizontalRatio,
58 | matchedUrl.verticalRatio
59 | );
60 | document.body.appendChild(container);
61 | }
62 | };
63 | const iframePostMessage = (command: string, matchedFun: string | null, url: string) => {
64 | const iframeElement = document.getElementById('sandboxFrame') as HTMLIFrameElement;
65 | if (iframeElement && iframeElement.contentWindow) {
66 | iframeElement.contentWindow.postMessage({ command: command, matchedFun: matchedFun, url: url }, '*');
67 | }
68 | };
69 | const checkCacheAndInit = (url: string) => {
70 | const cachedData = localStorage.getItem(CACHE_KEY);
71 | const currentTime = Date.now();
72 | if (cachedData) {
73 | const { matchedFun, timestamp } = JSON.parse(cachedData);
74 | if (currentTime - timestamp < CACHE_EXPIRY) {
75 | iframePostMessage('useCachedData', matchedFun, url);
76 | } else {
77 | iframePostMessage('requestMatchedUrl', null, url);
78 | }
79 | return;
80 | }
81 | iframePostMessage('requestMatchedUrl', null, url);
82 | };
83 |
84 | chrome.runtime.onMessage.addListener((message) => {
85 | if (message.type === 'urlChanged') {
86 | handleUrlChange(message.url);
87 | }
88 | });
89 |
90 | function handleUrlChange(url: string) {
91 | const existingContainer = document.getElementById(featureId);
92 | if (existingContainer) {
93 | existingContainer.remove();
94 | }
95 | checkCacheAndInit(url);
96 | }
97 |
98 | window.addEventListener('message', (event: MessageEvent) => {
99 | if (event.data && event.data.matchedFun && event.data.isUpdated) {
100 | const matchedFun = event.data.matchedFun;
101 | const currentTime = Date.now();
102 | localStorage.setItem(CACHE_KEY, JSON.stringify({ matchedFun, timestamp: currentTime }));
103 | }
104 | if (event.data && event.data.matchedUrl) {
105 | init(event.data.matchedUrl);
106 | }
107 | });
108 |
109 | features.add(featureId, {
110 | awaitDomReady: false,
111 | init: async () => {
112 | const iframe = document.createElement('iframe');
113 | iframe.id = 'sandboxFrame';
114 | iframe.src = chrome.runtime.getURL('sandbox.html');
115 | iframe.style.display = 'none';
116 | document.body.appendChild(iframe);
117 | iframe.onload = () => {
118 | const url = window.location.href;
119 | checkCacheAndInit(url);
120 | };
121 | },
122 | });
123 |
--------------------------------------------------------------------------------
/.github/hypertrons.json:
--------------------------------------------------------------------------------
1 | {
2 | "label_setup": {
3 | "version": 1,
4 | "labels": [
5 | {
6 | "__merge__": true
7 | },
8 | {
9 | "name": "difficulty/1",
10 | "description": "Difficulty score for issue or pull, 1 score",
11 | "color": "008672"
12 | },
13 | {
14 | "name": "difficulty/2",
15 | "description": "Difficulty score for issue or pull, 2 score",
16 | "color": "f1ee18"
17 | },
18 | {
19 | "name": "difficulty/3",
20 | "description": "Difficulty score for issue or pull, 3 score",
21 | "color": "67a8f7"
22 | },
23 | {
24 | "name": "difficulty/5",
25 | "description": "Difficulty score for issue or pull, 5 score",
26 | "color": "f7be99"
27 | },
28 | {
29 | "name": "difficulty/8",
30 | "description": "Difficulty score for issue or pull, 8 score, should split the issue",
31 | "color": "e11d21"
32 | },
33 | {
34 | "name": "pull/approved",
35 | "description": "If a pull is approved, it will be automatically merged",
36 | "color": "008672"
37 | },
38 | {
39 | "name": "kind/community",
40 | "description": "Community related issue or pull",
41 | "color": "99ff66",
42 | "keywords": ["community"]
43 | },
44 | {
45 | "name": "kind/CICD",
46 | "description": "CI/CD related issue or pull",
47 | "color": "0099ff",
48 | "keywords": ["continuous integration", "continuous delivery", "[ci]", "[cd]", "[ci/cd]"]
49 | }
50 | ]
51 | },
52 | "weekly_report": {
53 | "version": 1,
54 | "generateTime": "0 0 12 * * 1"
55 | },
56 | "role": {
57 | "version": 1,
58 | "roles": [
59 | {
60 | "name": "committer",
61 | "description": "Committer of the project",
62 | "users": ["frank-zsy", "heming6666", "LiuChangFreeman", "tyn1998", "zhuxiangning", "wxharry", "lhbvvvvv"],
63 | "commands": ["/difficulty", "/rerun", "/complete-checklist", "/start-vote"]
64 | },
65 | {
66 | "name": "replier",
67 | "description": "Replier is responsible for reply issues in time",
68 | "users": ["heming6666", "LiuChangFreeman", "xiaoya-Esther", "tyn1998", "zhuxiangning", "wxharry", "lhbvvvvv"],
69 | "commands": []
70 | },
71 | {
72 | "name": "approver",
73 | "description": "After approvers' approve, pulls should be merged automatically",
74 | "users": ["frank-zsy", "heming6666", "LiuChangFreeman", "tyn1998", "zhuxiangning", "wxharry", "lhbvvvvv"],
75 | "commands": ["/approve"]
76 | },
77 | {
78 | "name": "author",
79 | "description": "Author of the issue or pull",
80 | "users": [],
81 | "commands": ["/rerun"]
82 | },
83 | {
84 | "name": "notauthor",
85 | "description": "Not author of the issue or pull",
86 | "users": [],
87 | "commands": ["/approve"]
88 | },
89 | {
90 | "name": "anyone",
91 | "description": "Anyone",
92 | "users": [],
93 | "commands": ["/self-assign", "/vote"]
94 | }
95 | ]
96 | },
97 | "command": {
98 | "version": 1,
99 | "commands": [
100 | {
101 | "name": "/approve",
102 | "scopes": ["review", "review_comment", "pull_comment"]
103 | }
104 | ]
105 | },
106 | "approve": {
107 | "version": 1
108 | },
109 | "auto_merge": {
110 | "version": 1,
111 | "sched": "0 */5 * * * *"
112 | },
113 | "difficulty": {
114 | "version": 1
115 | },
116 | "issue_reminder": {
117 | "version": 1
118 | },
119 | "rerun": {
120 | "version": 1
121 | },
122 | "auto_label": {
123 | "version": 1
124 | },
125 | "self_assign": {
126 | "version": 1
127 | },
128 | "complete_checklist": {
129 | "version": 1
130 | },
131 | "vote": {
132 | "version": 1
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/components/Graph.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties, useEffect, useRef } from 'react';
2 | import * as echarts from 'echarts';
3 |
4 | import linearMap from '../helpers/linear-map';
5 | import { debounce } from 'lodash-es';
6 |
7 | interface GraphProps {
8 | /**
9 | * data
10 | */
11 | readonly data: any;
12 | /**
13 | * `style` for graph container
14 | */
15 | readonly style?: CSSProperties;
16 | /**
17 | * callback function when click node
18 | */
19 | readonly focusedNodeID?: string;
20 | }
21 |
22 | const NODE_SIZE = [10, 25];
23 |
24 | const generateEchartsData = (data: any, focusedNodeID: string | undefined): any => {
25 | const generateNodes = (nodes: any[]): any => {
26 | const values: number[] = nodes.map((item) => item[1]);
27 | const minMax = [Math.min(...values), Math.max(...values)];
28 | return nodes.map((n: any) => {
29 | const avatarId = n[0].split('/')[0];
30 | return {
31 | id: n[0],
32 | name: n[0],
33 | value: n[1],
34 | symbolSize: linearMap(n[1], minMax, NODE_SIZE),
35 | symbol: `image://https://avatars.githubusercontent.com/${avatarId}`,
36 | label: {
37 | show: n[0] === focusedNodeID ? true : false,
38 | },
39 | };
40 | });
41 | };
42 | const generateEdges = (edges: any[]): any => {
43 | if (edges.length === 0) {
44 | return [];
45 | }
46 | const threshold = edges[0][0].split('/').length === 2 ? 5 : 2.5;
47 | return edges
48 | .map((e: any) => {
49 | return {
50 | source: e[0],
51 | target: e[1],
52 | value: e[2],
53 | };
54 | })
55 | .filter((edge) => edge.value > threshold); // trim edges with small value to avoid a dense but useless graph
56 | };
57 | return {
58 | nodes: generateNodes(data.nodes),
59 | edges: generateEdges(data.edges),
60 | };
61 | };
62 |
63 | const Graph: React.FC = ({ data, style = {}, focusedNodeID }) => {
64 | const divEL = useRef(null);
65 | const graphData = generateEchartsData(data, focusedNodeID);
66 | const option = {
67 | tooltip: {},
68 | animation: true,
69 | animationDuration: 2000,
70 | series: [
71 | {
72 | type: 'graph',
73 | layout: 'force',
74 | nodes: graphData.nodes,
75 | edges: graphData.edges,
76 | // Enable mouse zooming and translating
77 | roam: true,
78 | label: {
79 | position: 'right',
80 | },
81 | force: {
82 | initLayout: 'circular',
83 | gravity: 0.1,
84 | repulsion: 80,
85 | edgeLength: [50, 100],
86 | // Disable the iteration animation of layout
87 | layoutAnimation: false,
88 | },
89 | lineStyle: {
90 | curveness: 0.3,
91 | opacity: 0.2,
92 | },
93 | emphasis: {
94 | focus: 'adjacency',
95 | label: {
96 | position: 'right',
97 | show: true,
98 | },
99 | },
100 | },
101 | ],
102 | };
103 |
104 | useEffect(() => {
105 | let chartDOM = divEL.current;
106 | const instance = echarts.init(chartDOM as any);
107 |
108 | return () => {
109 | instance.dispose();
110 | };
111 | }, []);
112 |
113 | useEffect(() => {
114 | let chartDOM = divEL.current;
115 | const instance = echarts.getInstanceByDom(chartDOM as any);
116 | if (instance) {
117 | instance.setOption(option);
118 | instance.on('click', (params: any) => {
119 | const url = 'https://github.com/' + params.data.id;
120 | window.location.href = url;
121 | });
122 |
123 | const debouncedResize = debounce(() => {
124 | instance.resize();
125 | }, 500);
126 | window.addEventListener('resize', debouncedResize);
127 | }
128 | }, []);
129 |
130 | return (
131 |
134 | );
135 | };
136 |
137 | export default Graph;
138 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-header-labels/index.tsx:
--------------------------------------------------------------------------------
1 | import features from '../../../../feature-manager';
2 | import View from './view';
3 | import ActivityView from './activityView';
4 | import OpenrankView from './openrankView';
5 | import ParticipantView from './participantView';
6 | import { NativePopover } from '../../components/NativePopover';
7 | import elementReady from 'element-ready';
8 | import { getRepoName, hasRepoContainerHeader, isPublicRepoWithMeta } from '../../../../helpers/get-github-repo-info';
9 | import { getActivity, getOpenrank, getParticipant, getContributor } from '../../../../api/repo';
10 | import { RepoMeta, metaStore } from '../../../../api/common';
11 | import React from 'react';
12 | import $ from 'jquery';
13 | import { createRoot } from 'react-dom/client';
14 | import isGithub from '../../../../helpers/is-github';
15 | import { getPlatform } from '../../../../helpers/get-platform';
16 | const featureId = features.getFeatureID(import.meta.url);
17 | let repoName: string;
18 | let activity: any;
19 | let openrank: any;
20 | let participant: any;
21 | let contributor: any;
22 | let meta: RepoMeta;
23 | let platform: string;
24 |
25 | const getData = async () => {
26 | activity = await getActivity(platform, repoName);
27 | openrank = await getOpenrank(platform, repoName);
28 | participant = await getParticipant(platform, repoName);
29 | contributor = await getContributor(platform, repoName);
30 | meta = (await metaStore.get(platform, repoName)) as RepoMeta;
31 | };
32 |
33 | const renderTo = (container: any) => {
34 | createRoot(container).render(
35 |
36 | );
37 | };
38 | const waitForElement = (selector: string) => {
39 | return new Promise((resolve) => {
40 | const observer = new MutationObserver(() => {
41 | const element = document.querySelector(selector);
42 | if (element) {
43 | observer.disconnect();
44 | resolve(element);
45 | }
46 | });
47 | observer.observe(document.body, { childList: true, subtree: true });
48 | });
49 | };
50 | const init = async (): Promise => {
51 | platform = getPlatform();
52 | repoName = getRepoName();
53 | await getData();
54 | const container = document.createElement('div');
55 | container.id = featureId;
56 | renderTo(container);
57 | await elementReady('#repository-container-header');
58 | $('#repository-container-header').find('span.Label').after(container);
59 | await waitForElement('#activity-header-label');
60 | await waitForElement('#OpenRank-header-label');
61 | await waitForElement('#participant-header-label');
62 | const placeholderElement = $('').appendTo('body')[0];
63 | createRoot(placeholderElement).render(
64 | <>
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | >
75 | );
76 | };
77 |
78 | const restore = async () => {
79 | // Clicking another repo link in one repo will trigger a turbo:visit,
80 | // so in a restoration visit we should be careful of the current repo.
81 | if (repoName !== getRepoName()) {
82 | repoName = getRepoName();
83 | await getData();
84 | }
85 | // Ideally, we should do nothing if the container already exists. But after a tubor
86 | // restoration visit, tooltip cannot be triggered though it exists in DOM tree. One
87 | // way to solve this is to rerender the view to the container. At least this way works.
88 | renderTo($(`#${featureId}`)[0]);
89 | };
90 |
91 | features.add(featureId, {
92 | asLongAs: [isGithub, isPublicRepoWithMeta, hasRepoContainerHeader],
93 | awaitDomReady: false,
94 | init,
95 | restore,
96 | });
97 |
--------------------------------------------------------------------------------
/src/pages/Options/components/GitHubToken.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import TooltipTrigger from '../../../components/TooltipTrigger';
3 | import { saveGithubToken, getGithubToken, githubRequest } from '../../../api/githubApi';
4 | import { message } from 'antd';
5 | import { useTranslation } from 'react-i18next';
6 | import { removeGithubToken } from '../../../helpers/github-token';
7 |
8 | const GitHubToken = () => {
9 | const [inputValue, setInputValue] = useState('');
10 | const { t, i18n } = useTranslation();
11 | const inputRef = useRef(null);
12 | const fetchToken = async () => {
13 | const storedToken = await getGithubToken();
14 | if (storedToken) {
15 | updateInputValue();
16 | }
17 | };
18 |
19 | const updateInputValue = async () => {
20 | const userData = await githubRequest('/user');
21 | if (userData && userData.login) {
22 | setInputValue(t('github_account_binded', { username: userData.login }));
23 | }
24 | };
25 |
26 | useEffect(() => {
27 | fetchToken();
28 | }, []);
29 |
30 | useEffect(() => {
31 | fetchToken();
32 | }, [i18n.language]);
33 |
34 | const handleBindAccount = async () => {
35 | const clientId = 'Ov23liyofMsuQYwtfGLb';
36 | const redirectUri = 'https://oauth.hypercrx.cn/github';
37 | const callback = chrome.identity.getRedirectURL();
38 | const scope = encodeURIComponent('read:user, public_repo');
39 | const authUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=token&state=${callback}`;
40 |
41 | chrome.identity.launchWebAuthFlow(
42 | {
43 | url: authUrl,
44 | interactive: true,
45 | },
46 | async (redirectUrl) => {
47 | if (!redirectUrl) {
48 | console.error(chrome.runtime.lastError ? chrome.runtime.lastError.message : 'Authorization failed.');
49 | return;
50 | }
51 | const ret = new URL(redirectUrl).searchParams.get('ret');
52 | if (!ret) {
53 | console.error('Ret not returned in callback URL, check the server config');
54 | return;
55 | }
56 | const retData = JSON.parse(decodeURIComponent(ret));
57 | if (!retData.access_token) {
58 | console.error('Invalid token data returned, check the server config');
59 | showMessage(t('github_account_bind_fail'), 'error');
60 | return;
61 | }
62 | await saveGithubToken(retData.access_token);
63 | updateInputValue();
64 | }
65 | );
66 | };
67 |
68 | const handleUnbindAccount = async () => {
69 | await removeGithubToken();
70 | setInputValue('');
71 | };
72 |
73 | const showMessage = (content: string, type: 'success' | 'error') => {
74 | if (inputRef.current) {
75 | const rect = inputRef.current.getBoundingClientRect();
76 | message.config({
77 | top: rect.top - 50,
78 | duration: 2,
79 | maxCount: 3,
80 | });
81 | message[type](content);
82 | }
83 | };
84 |
85 | return (
86 |
87 |
88 |
{t('github_account_configuration')}
89 |
90 |
91 |
{t('github_account_description')}
92 |
93 |
94 |
102 |
103 |
106 |
107 |
108 | );
109 | };
110 |
111 | export default GitHubToken;
112 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-activity-racing-bar/view.tsx:
--------------------------------------------------------------------------------
1 | import optionsStorage, { HypercrxOptions, defaults } from '../../../../options-storage';
2 | import RacingBar, { MediaControlers } from './RacingBar';
3 | import { RepoActivityDetails, getMonthlyData } from './data';
4 | import { PlayerButton } from './PlayerButton';
5 | import { SpeedController } from './SpeedController';
6 |
7 | import React, { useState, useEffect, useRef } from 'react';
8 | import { Space } from 'antd';
9 | import { PlayCircleFilled, StepBackwardFilled, StepForwardFilled, PauseCircleFilled } from '@ant-design/icons';
10 | import { useTranslation } from 'react-i18next';
11 | import '../../../../helpers/i18n';
12 | interface Props {
13 | currentRepo: string;
14 | width: string;
15 | repoActivityDetails: RepoActivityDetails;
16 | }
17 |
18 | const View = ({ width, repoActivityDetails }: Props): JSX.Element => {
19 | const [options, setOptions] = useState(defaults);
20 | const [speed, setSpeed] = useState(1);
21 | const [playing, setPlaying] = useState(false);
22 | const mediaControlersRef = useRef(null);
23 | const { t, i18n } = useTranslation();
24 | useEffect(() => {
25 | (async function () {
26 | setOptions(await optionsStorage.getAll());
27 | i18n.changeLanguage(options.locale);
28 | })();
29 | }, [options.locale]);
30 |
31 | return (
32 |
33 |
34 |
35 |
{t('component_projectRacingBar_title')}
36 |
37 |
38 | {/* speed control */}
39 | {
42 | setSpeed(speed);
43 | }}
44 | />
45 |
46 | {/* 3 buttons */}
47 |
48 | {/* last month | earliest month */}
49 | }
52 | onClick={mediaControlersRef.current?.previous}
53 | onLongPress={mediaControlersRef.current?.earliest}
54 | />
55 | {/* play | pause */}
56 | : }
58 | onClick={() => {
59 | if (playing) {
60 | mediaControlersRef.current?.pause();
61 | } else {
62 | mediaControlersRef.current?.play();
63 | }
64 | }}
65 | />
66 | {/* next month | latest month */}
67 | }
70 | onClick={mediaControlersRef.current?.next}
71 | onLongPress={mediaControlersRef.current?.latest}
72 | />
73 |
74 |
75 |
76 |
77 |
78 |
88 |
89 |
90 |
{t('component_projectRacingBar_description')}
91 |
92 |
93 |
94 |
95 |
96 | );
97 | };
98 |
99 | export default View;
100 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-fork-tooltip/ForkChart.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import * as echarts from 'echarts';
3 |
4 | import { formatNum, numberWithCommas } from '../../../../helpers/formatter';
5 | import { getInterval, judgeInterval } from '../../../../helpers/judge-interval';
6 | const LIGHT_THEME = {
7 | FG_COLOR: '#24292F',
8 | BG_COLOR: '#ffffff',
9 | SPLIT_LINE_COLOR: '#D0D7DE',
10 | BAR_COLOR: '#9B71FF',
11 | LINE_COLOR: '#8D5BFF',
12 | };
13 |
14 | const DARK_THEME = {
15 | FG_COLOR: '#c9d1d9',
16 | BG_COLOR: '#0d1118',
17 | SPLIT_LINE_COLOR: '#30363D',
18 | BAR_COLOR: '#9B71FF',
19 | LINE_COLOR: '#BFA3FF',
20 | };
21 |
22 | interface ForkChartProps {
23 | theme: 'light' | 'dark';
24 | width: number;
25 | height: number;
26 | data: [string, number][];
27 | }
28 |
29 | const ForkChart = (props: ForkChartProps): JSX.Element => {
30 | const { theme, width, height, data } = props;
31 | const { timeLength, minInterval } = getInterval(data);
32 | const divEL = useRef(null);
33 |
34 | const TH = theme == 'light' ? LIGHT_THEME : DARK_THEME;
35 | const option: echarts.EChartsOption = {
36 | tooltip: {
37 | trigger: 'axis',
38 | textStyle: {
39 | color: TH.FG_COLOR,
40 | },
41 | backgroundColor: TH.BG_COLOR,
42 | formatter: tooltipFormatter,
43 | },
44 | grid: {
45 | top: '5%',
46 | bottom: '5%',
47 | left: '5%',
48 | right: '5%',
49 | containLabel: true,
50 | },
51 | xAxis: {
52 | type: 'time',
53 | // 30 * 3600 * 24 * 1000 milliseconds
54 | splitLine: {
55 | show: false,
56 | },
57 | minInterval: minInterval,
58 | axisLabel: {
59 | color: TH.FG_COLOR,
60 | formatter: {
61 | year: '{yearStyle|{yy}}',
62 | month: '{MMM}',
63 | },
64 | rich: {
65 | yearStyle: {
66 | fontWeight: 'bold',
67 | },
68 | },
69 | },
70 | },
71 | yAxis: [
72 | {
73 | type: 'value',
74 | position: 'left',
75 | axisLabel: {
76 | color: TH.FG_COLOR,
77 | formatter: formatNum,
78 | },
79 | splitLine: {
80 | lineStyle: {
81 | color: TH.SPLIT_LINE_COLOR,
82 | },
83 | },
84 | },
85 | ],
86 | dataZoom: [
87 | {
88 | type: 'inside',
89 | start: 0,
90 | end: 100,
91 | minValueSpan: 3600 * 24 * 1000 * 180,
92 | },
93 | ],
94 | series: [
95 | {
96 | name: 'Fork Event',
97 | type: 'bar',
98 | data: data,
99 | itemStyle: {
100 | color: TH.BAR_COLOR,
101 | },
102 | emphasis: {
103 | focus: 'series',
104 | },
105 | yAxisIndex: 0,
106 | },
107 | {
108 | type: 'line',
109 | symbol: 'none',
110 | lineStyle: {
111 | color: TH.LINE_COLOR,
112 | },
113 | data: data,
114 | emphasis: {
115 | focus: 'series',
116 | },
117 | yAxisIndex: 0,
118 | },
119 | ],
120 | animationEasing: 'elasticOut',
121 | animationDelayUpdate: function (idx: any) {
122 | return idx * 5;
123 | },
124 | };
125 |
126 | useEffect(() => {
127 | let chartDOM = divEL.current;
128 | const instance = echarts.init(chartDOM as any);
129 |
130 | return () => {
131 | instance.dispose();
132 | };
133 | }, []);
134 |
135 | useEffect(() => {
136 | let chartDOM = divEL.current;
137 | const instance = echarts.getInstanceByDom(chartDOM as any);
138 | if (instance) {
139 | judgeInterval(instance, option, timeLength);
140 | instance.setOption(option);
141 | }
142 | }, []);
143 |
144 | return ;
145 | };
146 |
147 | const tooltipFormatter = (params: any) => {
148 | const res = `
149 | ${params[0].data[0]}
150 | ${params[0].marker}
151 |
152 | ${numberWithCommas(params[0].data[1])}
153 |
154 | `;
155 | return res;
156 | };
157 |
158 | export default ForkChart;
159 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-header-labels/ContributorChart.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import * as echarts from 'echarts';
3 |
4 | import { formatNum, numberWithCommas } from '../../../../helpers/formatter';
5 | import { getInterval, judgeInterval } from '../../../../helpers/judge-interval';
6 | const LIGHT_THEME = {
7 | FG_COLOR: '#24292F',
8 | BG_COLOR: '#ffffff',
9 | SPLIT_LINE_COLOR: '#D0D7DE',
10 | BAR_COLOR: '#3E90F1',
11 | LINE_COLOR: '#267FE8',
12 | };
13 |
14 | const DARK_THEME = {
15 | FG_COLOR: '#c9d1d9',
16 | BG_COLOR: '#0d1118',
17 | SPLIT_LINE_COLOR: '#30363D',
18 | BAR_COLOR: '#3E90F1',
19 | LINE_COLOR: '#82BBFF',
20 | };
21 |
22 | interface ContributorChartProps {
23 | theme: 'light' | 'dark';
24 | width: number;
25 | height: number;
26 | data: [string, number][];
27 | }
28 |
29 | const ContributorChart = (props: ContributorChartProps): JSX.Element => {
30 | const { theme, width, height, data } = props;
31 | const { timeLength, minInterval } = getInterval(data);
32 | const divEL = useRef(null);
33 | const TH = theme == 'light' ? LIGHT_THEME : DARK_THEME;
34 |
35 | const option: echarts.EChartsOption = {
36 | tooltip: {
37 | trigger: 'axis',
38 | textStyle: {
39 | color: TH.FG_COLOR,
40 | },
41 | backgroundColor: TH.BG_COLOR,
42 | formatter: tooltipFormatter,
43 | },
44 | grid: {
45 | top: '10%',
46 | bottom: '5%',
47 | left: '8%',
48 | right: '5%',
49 | containLabel: true,
50 | },
51 | xAxis: {
52 | type: 'time',
53 | // 30 * 3600 * 24 * 1000 milliseconds
54 | minInterval: minInterval,
55 | splitLine: {
56 | show: false,
57 | },
58 | axisLabel: {
59 | color: TH.FG_COLOR,
60 | formatter: {
61 | year: '{yearStyle|{yy}}',
62 | month: '{MMM}',
63 | },
64 | rich: {
65 | yearStyle: {
66 | fontWeight: 'bold',
67 | },
68 | },
69 | },
70 | },
71 | yAxis: [
72 | {
73 | type: 'value',
74 | position: 'left',
75 | axisLabel: {
76 | color: TH.FG_COLOR,
77 | formatter: formatNum,
78 | },
79 | splitLine: {
80 | lineStyle: {
81 | color: TH.SPLIT_LINE_COLOR,
82 | },
83 | },
84 | },
85 | ],
86 | dataZoom: [
87 | {
88 | type: 'inside',
89 | start: 0,
90 | end: 100,
91 | minValueSpan: 3600 * 24 * 1000 * 180,
92 | },
93 | ],
94 | series: [
95 | {
96 | type: 'bar',
97 | data: data,
98 | itemStyle: {
99 | color: '#ff8061',
100 | },
101 | emphasis: {
102 | focus: 'series',
103 | },
104 | yAxisIndex: 0,
105 | },
106 | {
107 | type: 'line',
108 | symbol: 'none',
109 | lineStyle: {
110 | color: '#ff8061',
111 | },
112 | data: data,
113 | emphasis: {
114 | focus: 'series',
115 | },
116 | yAxisIndex: 0,
117 | },
118 | ],
119 | animationEasing: 'elasticOut',
120 | animationDelayUpdate: function (idx: any) {
121 | return idx * 5;
122 | },
123 | };
124 |
125 | useEffect(() => {
126 | let chartDOM = divEL.current;
127 | const instance = echarts.init(chartDOM as any);
128 |
129 | return () => {
130 | instance.dispose();
131 | };
132 | }, []);
133 |
134 | useEffect(() => {
135 | let chartDOM = divEL.current;
136 | const instance = echarts.getInstanceByDom(chartDOM as any);
137 | if (instance) {
138 | judgeInterval(instance, option, timeLength);
139 | instance.setOption(option);
140 | }
141 | }, []);
142 |
143 | return ;
144 | };
145 |
146 | const tooltipFormatter = (params: any) => {
147 | const res = `
148 | ${params[0].data[0]}
149 | ${params[0].marker}
150 |
151 | ${numberWithCommas(params[0].data[1])}
152 |
153 | `;
154 | return res;
155 | };
156 |
157 | export default ContributorChart;
158 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-header-labels/ActivityChart.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import * as echarts from 'echarts';
3 |
4 | import { formatNum, numberWithCommas } from '../../../../helpers/formatter';
5 | import { getInterval, judgeInterval } from '../../../../helpers/judge-interval';
6 | const LIGHT_THEME = {
7 | FG_COLOR: '#24292F',
8 | BG_COLOR: '#ffffff',
9 | SPLIT_LINE_COLOR: '#D0D7DE',
10 | BAR_COLOR: '#FF8161',
11 | LINE_COLOR: '#FF6B47',
12 | };
13 |
14 | const DARK_THEME = {
15 | FG_COLOR: '#c9d1d9',
16 | BG_COLOR: '#0d1118',
17 | SPLIT_LINE_COLOR: '#30363D',
18 | BAR_COLOR: '#FF8161',
19 | LINE_COLOR: '#FFA994',
20 | };
21 |
22 | interface ActivityChartProps {
23 | theme: 'light' | 'dark';
24 | width: number;
25 | height: number;
26 | data: [string, number][];
27 | }
28 |
29 | const ActivityChart = (props: ActivityChartProps): JSX.Element => {
30 | const { theme, width, height, data } = props;
31 | const { timeLength, minInterval } = getInterval(data);
32 | const divEL = useRef(null);
33 |
34 | const TH = theme == 'light' ? LIGHT_THEME : DARK_THEME;
35 |
36 | const option: echarts.EChartsOption = {
37 | tooltip: {
38 | trigger: 'axis',
39 | textStyle: {
40 | color: TH.FG_COLOR,
41 | },
42 | backgroundColor: TH.BG_COLOR,
43 | formatter: tooltipFormatter,
44 | },
45 | grid: {
46 | top: '5%',
47 | bottom: '5%',
48 | left: '5%',
49 | right: '5%',
50 | containLabel: true,
51 | },
52 | xAxis: {
53 | type: 'time',
54 | // 30 * 3600 * 24 * 1000 milliseconds
55 | minInterval: minInterval,
56 | splitLine: {
57 | show: false,
58 | },
59 | axisLabel: {
60 | color: TH.FG_COLOR,
61 | formatter: {
62 | year: '{yearStyle|{yy}}',
63 | month: '{MMM}',
64 | },
65 | rich: {
66 | yearStyle: {
67 | fontWeight: 'bold',
68 | },
69 | },
70 | },
71 | },
72 | yAxis: [
73 | {
74 | type: 'value',
75 | position: 'left',
76 | axisLabel: {
77 | color: TH.FG_COLOR,
78 | formatter: formatNum,
79 | },
80 | splitLine: {
81 | lineStyle: {
82 | color: TH.SPLIT_LINE_COLOR,
83 | },
84 | },
85 | },
86 | ],
87 | dataZoom: [
88 | {
89 | type: 'inside',
90 | start: 0,
91 | end: 100,
92 | minValueSpan: 3600 * 24 * 1000 * 180,
93 | },
94 | ],
95 | series: [
96 | {
97 | type: 'bar',
98 | data: data,
99 | itemStyle: {
100 | color: TH.BAR_COLOR,
101 | },
102 | emphasis: {
103 | focus: 'series',
104 | },
105 | yAxisIndex: 0,
106 | },
107 | {
108 | type: 'line',
109 | symbol: 'none',
110 | lineStyle: {
111 | color: TH.LINE_COLOR,
112 | },
113 | data: data,
114 | emphasis: {
115 | focus: 'series',
116 | },
117 | yAxisIndex: 0,
118 | },
119 | ],
120 | animationEasing: 'elasticOut',
121 | animationDelayUpdate: function (idx: any) {
122 | return idx * 5;
123 | },
124 | };
125 |
126 | useEffect(() => {
127 | let chartDOM = divEL.current;
128 | const instance = echarts.init(chartDOM as any);
129 |
130 | return () => {
131 | instance.dispose();
132 | };
133 | }, []);
134 |
135 | useEffect(() => {
136 | let chartDOM = divEL.current;
137 | const instance = echarts.getInstanceByDom(chartDOM as any);
138 | if (instance) {
139 | judgeInterval(instance, option, timeLength);
140 | instance.setOption(option);
141 | }
142 | }, []);
143 |
144 | return ;
145 | };
146 |
147 | const tooltipFormatter = (params: any) => {
148 | const res = `
149 | ${params[0].data[0]}
150 | ${params[0].marker}
151 |
152 | ${numberWithCommas(params[0].data[1].toFixed(2))}
153 |
154 | `;
155 | return res;
156 | };
157 |
158 | export default ActivityChart;
159 |
--------------------------------------------------------------------------------
/src/pages/ContentScripts/features/repo-header-labels/OpenRankChart.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import * as echarts from 'echarts';
3 |
4 | import { formatNum, numberWithCommas } from '../../../../helpers/formatter';
5 | import { getInterval, judgeInterval } from '../../../../helpers/judge-interval';
6 | const LIGHT_THEME = {
7 | FG_COLOR: '#24292F',
8 | BG_COLOR: '#ffffff',
9 | SPLIT_LINE_COLOR: '#D0D7DE',
10 | BAR_COLOR: '#ED3E4A',
11 | LINE_COLOR: '#F02331',
12 | };
13 |
14 | const DARK_THEME = {
15 | FG_COLOR: '#c9d1d9',
16 | BG_COLOR: '#0d1118',
17 | SPLIT_LINE_COLOR: '#30363D',
18 | BAR_COLOR: '#ED3E4A',
19 | LINE_COLOR: '#FF8088',
20 | };
21 |
22 | interface OpenRankChartProps {
23 | theme: 'light' | 'dark';
24 | width: number;
25 | height: number;
26 | data: [string, number][];
27 | }
28 |
29 | const OpenRankChart = (props: OpenRankChartProps): JSX.Element => {
30 | const { theme, width, height, data } = props;
31 | const { timeLength, minInterval } = getInterval(data);
32 | const divEL = useRef(null);
33 |
34 | const TH = theme == 'light' ? LIGHT_THEME : DARK_THEME;
35 |
36 | const option: echarts.EChartsOption = {
37 | tooltip: {
38 | trigger: 'axis',
39 | textStyle: {
40 | color: TH.FG_COLOR,
41 | },
42 | backgroundColor: TH.BG_COLOR,
43 | formatter: tooltipFormatter,
44 | },
45 | grid: {
46 | top: '5%',
47 | bottom: '5%',
48 | left: '5%',
49 | right: '5%',
50 | containLabel: true,
51 | },
52 | xAxis: {
53 | type: 'time',
54 | // 30 * 3600 * 24 * 1000 milliseconds
55 | minInterval: minInterval,
56 | splitLine: {
57 | show: false,
58 | },
59 | axisLabel: {
60 | color: TH.FG_COLOR,
61 | formatter: {
62 | year: '{yearStyle|{yy}}',
63 | month: '{MMM}',
64 | },
65 | rich: {
66 | yearStyle: {
67 | fontWeight: 'bold',
68 | },
69 | },
70 | },
71 | },
72 | yAxis: [
73 | {
74 | type: 'value',
75 | position: 'left',
76 | axisLabel: {
77 | color: TH.FG_COLOR,
78 | formatter: formatNum,
79 | },
80 | splitLine: {
81 | lineStyle: {
82 | color: TH.SPLIT_LINE_COLOR,
83 | },
84 | },
85 | },
86 | ],
87 | dataZoom: [
88 | {
89 | type: 'inside',
90 | start: 0,
91 | end: 100,
92 | minValueSpan: 3600 * 24 * 1000 * 180,
93 | },
94 | ],
95 | series: [
96 | {
97 | type: 'bar',
98 | data: data,
99 | itemStyle: {
100 | color: TH.BAR_COLOR,
101 | },
102 | emphasis: {
103 | focus: 'series',
104 | },
105 | yAxisIndex: 0,
106 | },
107 | {
108 | type: 'line',
109 | symbol: 'none',
110 | lineStyle: {
111 | color: TH.LINE_COLOR,
112 | },
113 | data: data,
114 | emphasis: {
115 | focus: 'series',
116 | },
117 | yAxisIndex: 0,
118 | },
119 | ],
120 | animationEasing: 'elasticOut',
121 | animationDelayUpdate: function (idx: any) {
122 | return idx * 5;
123 | },
124 | };
125 |
126 | useEffect(() => {
127 | let chartDOM = divEL.current;
128 | const instance = echarts.init(chartDOM as any);
129 |
130 | return () => {
131 | instance.dispose();
132 | };
133 | }, []);
134 |
135 | useEffect(() => {
136 | let chartDOM = divEL.current;
137 | const instance = echarts.getInstanceByDom(chartDOM as any);
138 | if (instance) {
139 | judgeInterval(instance, option, timeLength);
140 | instance.setOption(option);
141 | }
142 | }, []);
143 |
144 | return ;
145 | };
146 |
147 | const tooltipFormatter = (params: any) => {
148 | const res = `
149 | ${params[0].data[0]}
150 | ${params[0].marker}
151 |
152 | ${numberWithCommas(params[0].data[1].toFixed(2))}
153 |
154 | `;
155 | return res;
156 | };
157 |
158 | export default OpenRankChart;
159 |
--------------------------------------------------------------------------------