From 14db952e94b3bc68146a1166ea0e19316fdadbdf Mon Sep 17 00:00:00 2001 From: Crxaw <48805031+sitescript@users.noreply.github.com> Date: Mon, 17 Mar 2025 05:40:53 +0000 Subject: [PATCH] Feat(plugin + fix ) GithubRepos & Pluginsettings (#185) * Updated Pluginsettings.tsx (https://github.com/Vendicated/Vencord/pull/3293) Added GithubRepos (https://github.com/Vendicated/Vencord/pull/3292) Also added Equicord Contrib to needed people * Fixed lint error * Hopefully fixed it? * Resolved conflicts --------- Co-authored-by: Crxa Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com> --- src/components/PluginSettings/index.tsx | 2 +- .../components/GitHubReposComponent.tsx | 123 ++++++++ .../githubRepos/components/RepoCard.tsx | 51 ++++ .../githubRepos/components/ReposModal.tsx | 94 ++++++ .../githubRepos/components/icons/Star.tsx | 17 ++ src/equicordplugins/githubRepos/index.tsx | 102 +++++++ .../githubRepos/services/githubApi.ts | 58 ++++ src/equicordplugins/githubRepos/styles.css | 287 ++++++++++++++++++ .../githubRepos/types/index.ts | 22 ++ .../githubRepos/utils/colors.ts | 61 ++++ .../githubRepos/utils/settings.ts | 15 + src/utils/constants.ts | 4 + 12 files changed, 835 insertions(+), 1 deletion(-) create mode 100644 src/equicordplugins/githubRepos/components/GitHubReposComponent.tsx create mode 100644 src/equicordplugins/githubRepos/components/RepoCard.tsx create mode 100644 src/equicordplugins/githubRepos/components/ReposModal.tsx create mode 100644 src/equicordplugins/githubRepos/components/icons/Star.tsx create mode 100644 src/equicordplugins/githubRepos/index.tsx create mode 100644 src/equicordplugins/githubRepos/services/githubApi.ts create mode 100644 src/equicordplugins/githubRepos/styles.css create mode 100644 src/equicordplugins/githubRepos/types/index.ts create mode 100644 src/equicordplugins/githubRepos/utils/colors.ts create mode 100644 src/equicordplugins/githubRepos/utils/settings.ts diff --git a/src/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx index 6b949e6a..14683853 100644 --- a/src/components/PluginSettings/index.tsx +++ b/src/components/PluginSettings/index.tsx @@ -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)) ); diff --git a/src/equicordplugins/githubRepos/components/GitHubReposComponent.tsx b/src/equicordplugins/githubRepos/components/GitHubReposComponent.tsx new file mode 100644 index 00000000..6199ec22 --- /dev/null +++ b/src/equicordplugins/githubRepos/components/GitHubReposComponent.tsx @@ -0,0 +1,123 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Flex } from "@components/Flex"; +import { openModal } from "@utils/modal"; +import { React, useEffect, UserProfileStore, 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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [userInfo, setUserInfo] = useState(null); + + const openReposModal = () => { + if (!userInfo) return; + + const sortedRepos = [...repos].sort((a, b) => b.stargazers_count - a.stargazers_count); + openModal(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
Loading repositories...
; + if (error) return
Error: {error}
; + if (!repos.length) return null; + + const topRepos = repos.slice(0, 3); + + return ( +
+
+ GitHub Repositories + {userInfo && ( + + {` (${topRepos.length}/${userInfo.totalRepos})`} + + )} +
+ + {topRepos.map(repo => ( + + ))} + +
+ +
+
+ ); +} diff --git a/src/equicordplugins/githubRepos/components/RepoCard.tsx b/src/equicordplugins/githubRepos/components/RepoCard.tsx new file mode 100644 index 00000000..23338cfa --- /dev/null +++ b/src/equicordplugins/githubRepos/components/RepoCard.tsx @@ -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 ( +
+ + {repo.stargazers_count.toLocaleString()} +
+ ); + }; + + const renderLanguage = () => { + if (!showLanguage || !repo.language) return null; + + return ( +
+ + {repo.language} +
+ ); + }; + + return ( +
+ +
{repo.name}
+ {renderStars()} +
+ + {repo.description && ( +
+ {repo.description} +
+ )} + + {renderLanguage()} +
+ ); +} \ No newline at end of file diff --git a/src/equicordplugins/githubRepos/components/ReposModal.tsx b/src/equicordplugins/githubRepos/components/ReposModal.tsx new file mode 100644 index 00000000..c3a591b1 --- /dev/null +++ b/src/equicordplugins/githubRepos/components/ReposModal.tsx @@ -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 = () => ( + + + Repository + Description + Language + Stars + + + ); + + const renderTableRow = (repo: GitHubRepo) => ( + window.open(repo.html_url, "_blank")}> + +
{repo.name}
+ + +
+ {repo.description || ""} +
+ + + {repo.language && ( +
+ + {repo.language} +
+ )} + + +
+ + {repo.stargazers_count.toLocaleString()} +
+ + + ); + + return ( + + + + {username}'s GitHub Repositories + + + +
+ + + + + + + + {renderTableHeader()} + + {repos.map(renderTableRow)} + +
+
+
+ + + + +
+ ); +} \ No newline at end of file diff --git a/src/equicordplugins/githubRepos/components/icons/Star.tsx b/src/equicordplugins/githubRepos/components/icons/Star.tsx new file mode 100644 index 00000000..6b5e87c4 --- /dev/null +++ b/src/equicordplugins/githubRepos/components/icons/Star.tsx @@ -0,0 +1,17 @@ +import { React } from "@webpack/common"; +import { IconProps } from "../../types"; + +export function Star({ className, width = 16, height = 16 }: IconProps) { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/equicordplugins/githubRepos/index.tsx b/src/equicordplugins/githubRepos/index.tsx new file mode 100644 index 00000000..10e0c537 --- /dev/null +++ b/src/equicordplugins/githubRepos/index.tsx @@ -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 { EquicordDevs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import definePlugin from "@utils/types"; +import { findByCodeLazy } from "@webpack"; +import { React } from "@webpack/common"; + +import { GitHubReposComponent } from "./components/GitHubReposComponent"; +import { settings } from "./utils/settings"; + +const getProfileThemeProps = findByCodeLazy(".getPreviewThemeColors", "primaryColor:"); + +const logger = new Logger("GitHubRepos"); +logger.info("Plugin loaded"); + +const profilePopoutComponent = ErrorBoundary.wrap( + (props: { user: any; displayProfile?: any; }) => { + return ( + + ); + }, + { + 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: [EquicordDevs.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 +}); diff --git a/src/equicordplugins/githubRepos/services/githubApi.ts b/src/equicordplugins/githubRepos/services/githubApi.ts new file mode 100644 index 00000000..92f3b1fc --- /dev/null +++ b/src/equicordplugins/githubRepos/services/githubApi.ts @@ -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 { + 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 { + 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 { + 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); +} \ No newline at end of file diff --git a/src/equicordplugins/githubRepos/styles.css b/src/equicordplugins/githubRepos/styles.css new file mode 100644 index 00000000..ab6ce26f --- /dev/null +++ b/src/equicordplugins/githubRepos/styles.css @@ -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); +} \ No newline at end of file diff --git a/src/equicordplugins/githubRepos/types/index.ts b/src/equicordplugins/githubRepos/types/index.ts new file mode 100644 index 00000000..5ccca82a --- /dev/null +++ b/src/equicordplugins/githubRepos/types/index.ts @@ -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; +} \ No newline at end of file diff --git a/src/equicordplugins/githubRepos/utils/colors.ts b/src/equicordplugins/githubRepos/utils/colors.ts new file mode 100644 index 00000000..64da1284 --- /dev/null +++ b/src/equicordplugins/githubRepos/utils/colors.ts @@ -0,0 +1,61 @@ +export function getLanguageColor(language: string): string { + const colors: Record = { + "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"; +} \ No newline at end of file diff --git a/src/equicordplugins/githubRepos/utils/settings.ts b/src/equicordplugins/githubRepos/utils/settings.ts new file mode 100644 index 00000000..c5572871 --- /dev/null +++ b/src/equicordplugins/githubRepos/utils/settings.ts @@ -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 + } +}); \ No newline at end of file diff --git a/src/utils/constants.ts b/src/utils/constants.ts index ac407800..081f127f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1020,6 +1020,10 @@ export const EquicordDevs = Object.freeze({ name: "SteelTech", id: 1344190786476183643n }, + talhakf: { + name: "talhakf", + id: 1140716160560676976n + }, } satisfies Record); // iife so #__PURE__ works correctly