Added GithubRepos (https://github.com/Vendicated/Vencord/pull/3292)
Also added Equicord Contrib to needed people
This commit is contained in:
Crxa 2025-03-16 21:36:32 +00:00
parent dc114666a8
commit 4269a8dd5b
12 changed files with 827 additions and 1 deletions

View file

@ -276,7 +276,7 @@ export default function PluginSettings() {
if (!search.length) return true;
return (
plugin.name.toLowerCase().includes(search) ||
plugin.name.toLowerCase().includes(search.replace(/\s+/g, "")) ||
plugin.description.toLowerCase().includes(search) ||
plugin.tags?.some(t => t.toLowerCase().includes(search))
);

View file

@ -0,0 +1,115 @@
import { Flex } from "@components/Flex";
import { openModal } from "@utils/modal";
import { UserProfileStore, React, useEffect, useState } from "@webpack/common";
import { fetchReposByUserId, fetchReposByUsername, fetchUserInfo, GitHubUserInfo } from "../services/githubApi";
import { GitHubRepo } from "../types";
import { settings } from "../utils/settings";
import { RepoCard } from "./RepoCard";
import { ReposModal } from "./ReposModal";
export function GitHubReposComponent({ id, theme }: { id: string, theme: string; }) {
const [repos, setRepos] = useState<GitHubRepo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [userInfo, setUserInfo] = useState<GitHubUserInfo | null>(null);
const openReposModal = () => {
if (!userInfo) return;
const sortedRepos = [...repos].sort((a, b) => b.stargazers_count - a.stargazers_count);
openModal(props => (
<ReposModal
repos={sortedRepos}
username={userInfo.username}
rootProps={props}
/>
));
};
useEffect(() => {
const fetchData = async () => {
try {
const profile = UserProfileStore.getUserProfile(id);
if (!profile) {
setLoading(false);
return;
}
const connections = profile.connectedAccounts;
if (!connections?.length) {
setLoading(false);
return;
}
const githubConnection = connections.find(conn => conn.type === "github");
if (!githubConnection) {
setLoading(false);
return;
}
const username = githubConnection.name;
const userInfoData = await fetchUserInfo(username);
if (userInfoData) {
setUserInfo(userInfoData);
}
const githubId = githubConnection.id;
// Try to fetch by ID first, fall back to username
const reposById = await fetchReposByUserId(githubId);
if (reposById) {
setRepos(reposById);
setLoading(false);
return;
}
const reposByUsername = await fetchReposByUsername(username);
setRepos(reposByUsername);
setLoading(false);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Failed to fetch repositories";
setError(errorMessage);
setLoading(false);
}
};
fetchData();
}, [id]);
if (loading) return <div className="vc-github-repos-loading">Loading repositories...</div>;
if (error) return <div className="vc-github-repos-error">Error: {error}</div>;
if (!repos.length) return null;
const topRepos = repos.slice(0, 3);
return (
<div className="vc-github-repos-container">
<div className="vc-github-repos-header">
GitHub Repositories
{userInfo && (
<span className="vc-github-repos-count">
{` (${topRepos.length}/${userInfo.totalRepos})`}
</span>
)}
</div>
<Flex className="vc-github-repos-list" flexDirection="column">
{topRepos.map(repo => (
<RepoCard
repo={repo}
theme={theme}
showStars={settings.store.showStars}
showLanguage={settings.store.showLanguage}
/>
))}
</Flex>
<div className="vc-github-repos-footer">
<button
className="vc-github-repos-show-more"
onClick={openReposModal}
>
Show More
</button>
</div>
</div>
);
}

View file

@ -0,0 +1,51 @@
import { Flex } from "@components/Flex";
import { React } from "@webpack/common";
import { RepoCardProps } from "../types";
import { getLanguageColor } from "../utils/colors";
import { Star } from "./icons/Star";
export function RepoCard({ repo, theme, showStars, showLanguage }: RepoCardProps) {
const handleClick = () => window.open(repo.html_url, "_blank");
const renderStars = () => {
if (!showStars) return null;
return (
<div className="vc-github-repo-stars">
<Star className="vc-github-repo-star-icon" />
{repo.stargazers_count.toLocaleString()}
</div>
);
};
const renderLanguage = () => {
if (!showLanguage || !repo.language) return null;
return (
<div className="vc-github-repo-language">
<span
className="vc-github-repo-language-color"
style={{ backgroundColor: getLanguageColor(repo.language) }}
/>
{repo.language}
</div>
);
};
return (
<div className="vc-github-repo-card" onClick={handleClick}>
<Flex className="vc-github-repo-header">
<div className="vc-github-repo-name">{repo.name}</div>
{renderStars()}
</Flex>
{repo.description && (
<div className="vc-github-repo-description">
{repo.description}
</div>
)}
{renderLanguage()}
</div>
);
}

View file

@ -0,0 +1,94 @@
import { ModalContent, ModalFooter, ModalHeader, ModalRoot } from "@utils/modal";
import { Button, Forms, React } from "@webpack/common";
import { GitHubRepo } from "../types";
import { getLanguageColor } from "../utils/colors";
import { Star } from "./icons/Star";
interface ReposModalProps {
repos: GitHubRepo[];
username: string;
rootProps: any;
}
export function ReposModal({ repos, username, rootProps }: ReposModalProps) {
const renderTableHeader = () => (
<thead>
<tr>
<th>Repository</th>
<th>Description</th>
<th>Language</th>
<th>Stars</th>
</tr>
</thead>
);
const renderTableRow = (repo: GitHubRepo) => (
<tr key={repo.id} onClick={() => window.open(repo.html_url, "_blank")}>
<td>
<div className="vc-github-repos-table-name">{repo.name}</div>
</td>
<td>
<div className="vc-github-repos-table-description">
{repo.description || ""}
</div>
</td>
<td>
{repo.language && (
<div className="vc-github-repos-table-language">
<span
className="vc-github-repos-table-language-color"
style={{ backgroundColor: getLanguageColor(repo.language) }}
/>
<span>{repo.language}</span>
</div>
)}
</td>
<td>
<div className="vc-github-repos-table-stars">
<Star className="vc-github-repos-table-star-icon" />
<span>{repo.stargazers_count.toLocaleString()}</span>
</div>
</td>
</tr>
);
return (
<ModalRoot className="vc-github-repos-modal" size="large" {...rootProps}>
<ModalHeader>
<Forms.FormTitle tag="h2" className="vc-github-repos-modal-title">
{username}'s GitHub Repositories
</Forms.FormTitle>
</ModalHeader>
<ModalContent className="vc-github-repos-modal-content">
<div className="vc-github-repos-table-container">
<table className="vc-github-repos-table">
<colgroup>
<col style={{ width: "20%" }} />
<col style={{ width: "45%" }} />
<col style={{ width: "15%" }} />
<col style={{ width: "10%" }} />
</colgroup>
{renderTableHeader()}
<tbody>
{repos.map(renderTableRow)}
</tbody>
</table>
</div>
</ModalContent>
<ModalFooter>
<Button
onClick={() => window.open(`https://github.com/${username}?tab=repositories`, "_blank")}
>
View on GitHub
</Button>
<Button
color={Button.Colors.TRANSPARENT}
look={Button.Looks.LINK}
onClick={rootProps.onClose}
>
Close
</Button>
</ModalFooter>
</ModalRoot>
);
}

View file

@ -0,0 +1,17 @@
import { React } from "@webpack/common";
import { IconProps } from "../../types";
export function Star({ className, width = 16, height = 16 }: IconProps) {
return (
<svg
className={className}
width={width}
height={height}
viewBox="0 0 16 16"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z" />
</svg>
);
}

View file

@ -0,0 +1,102 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { findByCodeLazy } from "@webpack";
import { React } from "@webpack/common";
import definePlugin from "@utils/types";
import { Logger } from "@utils/Logger";
import { settings } from "./utils/settings";
import { GitHubReposComponent } from "./components/GitHubReposComponent";
import { Devs } from "@utils/constants";
const getProfileThemeProps = findByCodeLazy(".getPreviewThemeColors", "primaryColor:");
const logger = new Logger("GitHubRepos");
logger.info("Plugin loaded");
const profilePopoutComponent = ErrorBoundary.wrap(
(props: { user: any; displayProfile?: any; }) => {
return (
<GitHubReposComponent
{...props}
id={props.user.id}
theme={getProfileThemeProps(props).theme}
/>
);
},
{
noop: true,
onError: (err) => {
logger.error("Error in profile popout component", err);
return null;
}
}
);
export default definePlugin({
name: "GitHubRepos",
description: "Displays a user's public GitHub repositories in their profile",
authors: [Devs.talhakf],
settings,
patches: [
{
find: ".hasAvatarForGuild(null==",
replacement: {
match: /currentUser:\i,guild:\i}\)(?<=user:(\i),bio:null==(\i)\?.+?)/,
replace: (m, user, profile) => {
return `${m},$self.profilePopoutComponent({ user: ${user}, displayProfile: ${profile} })`;
}
}
},
{
find: "renderBio",
replacement: {
match: /renderBio\(\){.+?return (.*?)}/s,
replace: (m, returnStatement) => {
return `renderBio(){
const originalReturn = ${returnStatement};
const user = this.props.user;
if (!user) return originalReturn;
try {
const component = $self.profilePopoutComponent({
user: user,
displayProfile: this.props.displayProfile
});
if (!originalReturn) return component;
return React.createElement(
React.Fragment,
null,
originalReturn,
component
);
} catch (err) {
console.error("[GitHubRepos] Error in bio patch:", err);
return originalReturn;
}
}`;
}
}
}
],
start() {
logger.info("Plugin started");
},
stop() {
logger.info("Plugin stopped");
},
profilePopoutComponent
});

View file

@ -0,0 +1,58 @@
import { GitHubRepo } from "../types";
import { Logger } from "@utils/Logger";
const logger = new Logger("GitHubRepos");
export interface GitHubUserInfo {
username: string;
totalRepos: number;
}
export async function fetchUserInfo(username: string): Promise<GitHubUserInfo | null> {
try {
const userInfoUrl = `https://api.github.com/users/${username}`;
const userInfoResponse = await fetch(userInfoUrl);
if (!userInfoResponse.ok) return null;
const userData = await userInfoResponse.json();
return {
username: userData.login,
totalRepos: userData.public_repos
};
} catch (error) {
logger.error("Error fetching user info", error);
return null;
}
}
export async function fetchReposByUserId(githubId: string, perPage: number = 30): Promise<GitHubRepo[] | null> {
try {
const apiUrl = `https://api.github.com/user/${githubId}/repos?sort=stars&direction=desc&per_page=${perPage}`;
const response = await fetch(apiUrl);
if (!response.ok) return null;
const data = await response.json();
return sortReposByStars(data);
} catch (error) {
logger.error("Error fetching repos by ID", error);
return null;
}
}
export async function fetchReposByUsername(username: string, perPage: number = 30): Promise<GitHubRepo[]> {
const apiUrl = `https://api.github.com/users/${username}/repos?sort=stars&direction=desc&per_page=${perPage}`;
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`Error fetching repos by username: ${response.status}`);
}
const data = await response.json();
return sortReposByStars(data);
}
function sortReposByStars(repos: GitHubRepo[]): GitHubRepo[] {
return repos.sort((a, b) => b.stargazers_count - a.stargazers_count);
}

View file

@ -0,0 +1,287 @@
.vc-github-repos-container {
margin-top: 16px;
padding: 16px;
border-radius: 8px;
border: 1px solid var(--background-modifier-accent);
background-color: transparent;
max-height: 500px;
display: flex;
flex-direction: column;
}
.vc-github-repos-header {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
color: var(--header-primary);
display: flex;
align-items: center;
}
.vc-github-repos-count {
font-size: 14px;
font-weight: 400;
color: var(--text-muted);
margin-left: 4px;
}
.vc-github-repos-list {
gap: 8px;
overflow-y: auto;
max-height: 400px;
padding-right: 8px;
margin-right: -8px;
scrollbar-width: thin;
flex: 1;
}
.vc-github-repos-list::-webkit-scrollbar {
width: 8px;
}
.vc-github-repos-list::-webkit-scrollbar-track {
background: var(--scrollbar-thin-track);
border-radius: 10px;
}
.vc-github-repos-list::-webkit-scrollbar-thumb {
background: var(--scrollbar-thin-thumb);
border-radius: 10px;
}
.vc-github-repos-list::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thin-thumb-hover);
}
.vc-github-repos-footer {
margin-top: 12px;
text-align: center;
padding-top: 8px;
border-top: 1px solid var(--background-modifier-accent);
}
.vc-github-repos-view-all {
color: var(--text-link);
font-size: 14px;
text-decoration: none;
}
.vc-github-repos-view-all:hover {
text-decoration: underline;
}
.vc-github-repos-show-more {
background-color: transparent;
color: var(--text-normal);
border: 1px solid var(--background-modifier-accent);
border-radius: 3px;
padding: 6px 12px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.vc-github-repos-show-more:hover {
background-color: var(--background-modifier-hover);
border-color: var(--background-modifier-selected);
}
.vc-github-repos-loading,
.vc-github-repos-error {
color: var(--text-normal);
font-size: 14px;
margin: 8px 0;
}
.vc-github-repos-error {
color: var(--text-danger);
}
.vc-github-repo-card {
padding: 12px;
border-radius: 6px;
border: 1px solid var(--background-modifier-accent);
background-color: transparent;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.vc-github-repo-card:hover {
background-color: var(--background-modifier-hover);
border-color: var(--background-modifier-selected);
}
.vc-github-repo-header {
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.vc-github-repo-name {
font-weight: 600;
color: var(--header-primary);
display: flex;
align-items: center;
gap: 4px;
}
.vc-github-repo-fork-icon {
color: var(--text-muted);
}
.vc-github-repo-stars {
display: flex;
align-items: center;
gap: 4px;
color: var(--text-muted);
font-size: 14px;
}
.vc-github-repo-star-icon {
color: var(--text-warning);
}
.vc-github-repo-description {
color: var(--text-normal);
font-size: 14px;
margin-bottom: 8px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
}
.vc-github-repo-language {
display: flex;
align-items: center;
gap: 6px;
color: var(--text-muted);
font-size: 12px;
}
.vc-github-repo-language-color {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
}
.vc-github-repos-modal {
width: 90%;
max-width: 900px;
}
.vc-github-repos-modal-title {
color: var(--header-primary);
font-size: 20px;
font-weight: 600;
margin: 0;
}
.vc-github-repos-modal-content {
padding: 16px 0;
overflow: hidden;
}
.vc-github-repos-table-container {
width: 100%;
overflow-x: auto;
}
.vc-github-repos-table {
width: 100%;
border-collapse: collapse;
color: var(--text-normal);
font-size: 14px;
table-layout: fixed;
}
.vc-github-repos-table colgroup {
display: table-column-group;
}
.vc-github-repos-table col {
display: table-column;
}
.vc-github-repos-table thead {
border-bottom: 2px solid var(--background-modifier-accent);
}
.vc-github-repos-table th {
text-align: left;
padding: 10px 16px;
font-weight: 600;
color: var(--header-primary);
border-bottom: 2px solid var(--background-modifier-accent);
}
.vc-github-repos-table th:last-child {
text-align: center;
}
.vc-github-repos-table tbody tr {
border-bottom: 1px solid var(--background-modifier-accent);
cursor: pointer;
transition: background-color 0.2s ease;
}
.vc-github-repos-table tbody tr:hover {
background-color: var(--background-modifier-hover);
}
.vc-github-repos-table td {
padding: 12px 16px;
vertical-align: middle;
border-bottom: 1px solid var(--background-modifier-accent);
overflow: hidden;
text-overflow: ellipsis;
}
.vc-github-repos-table-name {
font-weight: 500;
color: var(--header-primary);
display: flex;
align-items: center;
gap: 6px;
}
.vc-github-repos-table-fork-icon {
color: var(--text-muted);
}
.vc-github-repos-table-description {
color: var(--text-normal);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vc-github-repos-table-language {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.vc-github-repos-table-language-color {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.vc-github-repos-table-stars {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
white-space: nowrap;
text-align: center;
}
.vc-github-repos-table-star-icon {
color: var(--text-warning);
}

View file

@ -0,0 +1,22 @@
export interface GitHubRepo {
id: number;
name: string;
html_url: string;
description: string;
stargazers_count: number;
language: string;
fork: boolean;
}
export interface IconProps {
className?: string;
width?: number;
height?: number;
}
export interface RepoCardProps {
repo: GitHubRepo;
theme: string;
showStars: boolean;
showLanguage: boolean;
}

View file

@ -0,0 +1,61 @@
export function getLanguageColor(language: string): string {
const colors: Record<string, string> = {
"JavaScript": "#f1e05a",
"TypeScript": "#3178c6",
"Python": "#3572A5",
"Java": "#b07219",
"C#": "#178600",
"C++": "#f34b7d",
"C": "#555555",
"HTML": "#e34c26",
"CSS": "#563d7c",
"PHP": "#4F5D95",
"Ruby": "#701516",
"Go": "#00ADD8",
"Rust": "#dea584",
"Swift": "#ffac45",
"Kotlin": "#A97BFF",
"Dart": "#00B4AB",
"Shell": "#89e051",
"PowerShell": "#012456",
"Lua": "#000080",
"Perl": "#0298c3",
"R": "#198CE7",
"Scala": "#c22d40",
"Haskell": "#5e5086",
"Elixir": "#6e4a7e",
"Clojure": "#db5855",
"Vue": "#41b883",
"Svelte": "#ff3e00",
"Jupyter Notebook": "#DA5B0B",
"Assembly": "#6E4C13",
"COBOL": "#004B85",
"CoffeeScript": "#244776",
"Crystal": "#000100",
"D": "#BA595E",
"F#": "#B845FC",
"Fortran": "#4d41b1",
"GLSL": "#5686A5",
"Groovy": "#e69f56",
"Julia": "#a270ba",
"Markdown": "#083fa1",
"MATLAB": "#bb92ac",
"Objective-C": "#438eff",
"OCaml": "#3be133",
"Pascal": "#E3F171",
"Prolog": "#74283c",
"PureScript": "#1D222D",
"Racket": "#3c5caa",
"Raku": "#0000fb",
"Reason": "#ff5847",
"SCSS": "#c6538c",
"Solidity": "#AA6746",
"Tcl": "#e4cc98",
"Verilog": "#b2b7f8",
"VHDL": "#adb2cb",
"WebAssembly": "#04133b",
"Zig": "#ec915c"
};
return colors[language] || "#858585";
}

View file

@ -0,0 +1,15 @@
import { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
export const settings = definePluginSettings({
showStars: {
type: OptionType.BOOLEAN,
description: "Show repository stars",
default: true
},
showLanguage: {
type: OptionType.BOOLEAN,
description: "Show repository language",
default: true
}
});

View file

@ -1020,6 +1020,10 @@ export const EquicordDevs = Object.freeze({
name: "S€th",
id: 1273447359417942128n
},
talhakf: {
name: "talhakf",
id: 1140716160560676976n
},
} satisfies Record<string, Dev>);
// iife so #__PURE__ works correctly