- // ==UserScript==
- // @name GitHub Plus
- // @name:zh-CN GitHub 增强
- // @namespace http://tampermonkey.net/
- // @version 0.3.5
- // @description Enhance GitHub with additional features.
- // @description:zh-CN 为 GitHub 增加额外的功能。
- // @author PRO-2684
- // @match https://github.com/*
- // @match https://*.github.com/*
- // @run-at document-start
- // @icon http://github.com/favicon.ico
- // @license gpl-3.0
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_deleteValue
- // @grant GM_registerMenuCommand
- // @grant GM_unregisterMenuCommand
- // @grant GM_addValueChangeListener
- // @grant GM_addElement
- // @require https://github.com/PRO-2684/GM_config/releases/download/v1.2.1/config.min.js#md5=525526b8f0b6b8606cedf08c651163c2
- // ==/UserScript==
-
- (function() {
- 'use strict';
- const { name, version } = GM_info.script;
- const idPrefix = "ghp-"; // Prefix for the IDs of the elements
- /**
- * The top domain of the current page.
- * @type {string}
- */
- const topDomain = location.hostname.split(".").slice(-2).join(".");
- /**
- * The official domain of GitHub.
- * @type {string}
- */
- const officialDomain = "github.com";
- /**
- * The color used for logging. Matches the color of the GitHub.
- * @type {string}
- */
- const themeColor = "#f78166";
- /**
- * Regular expression to match the expanded assets URL. (https://<host>/<username>/<repo>/releases/expanded_assets/<version>)
- */
- const expandedAssetsRegex = new RegExp(`https://${topDomain.replaceAll(".", "\\.")}/([^/]+)/([^/]+)/releases/expanded_assets/([^/]+)`);
- /**
- * Data about the release. Maps `owner`, `repo` and `version` to the details of a release. Details are `Promise` objects if exist.
- */
- let releaseData = {};
- /**
- * Rate limit data for the GitHub API.
- * @type {Object}
- * @property {number} limit The maximum number of requests that the consumer is permitted to make per hour.
- * @property {number} remaining The number of requests remaining in the current rate limit window.
- * @property {number} reset The time at which the current rate limit window resets in UTC epoch seconds.
- */
- let rateLimit = {
- limit: -1,
- remaining: -1,
- reset: -1
- };
-
- // Configuration
- const configDesc = {
- $default: {
- autoClose: false
- },
- code: {
- name: "🔢 Code Features",
- type: "folder",
- items: {
- cloneFullCommand: {
- name: "📥 Clone Full Command",
- title: "Append `git clone ` before `https` and `git@` URLs under the code tab",
- type: "bool",
- value: false,
- },
- tabSize: {
- name: "➡️ Tab Size",
- title: "Set Tab indentation size",
- type: "int",
- min: 0,
- value: 4,
- },
- cursorBlink: {
- name: "😉 Cursor Blink",
- title: "Enable cursor blinking",
- type: "bool",
- value: false,
- },
- cursorAnimation: {
- name: "🌊 Cursor Animation",
- title: "Make cursor move smoothly",
- type: "bool",
- value: false,
- },
- fullWidth: {
- name: "🔲 Full Width",
- title: "Make the code block full width (copilot button may cover the end of the line)",
- type: "bool",
- value: false,
- },
- },
- },
- appearance: {
- name: "🎨 Appearance",
- type: "folder",
- items: {
- dashboard: {
- name: "📰 Dashboard",
- title: "Configures the dashboard",
- type: "enum",
- options: ["Default", "Hide Copilot", "Hide Feed", "Mobile-Like"],
- },
- leftSidebar: {
- name: "↖️ Left Sidebar",
- title: "Configures the left sidebar",
- type: "enum",
- options: ["Default", "Hidden"],
- },
- rightSidebar: {
- name: "↗️ Right Sidebar",
- title: "Configures the right sidebar",
- type: "enum",
- options: ["Default", "Hide 'Latest changes'", "Hide 'Explore repositories'", "Hide Completely"],
- },
- stickyAvatar: {
- name: "📌 Sticky Avatar",
- title: "Make the avatar sticky",
- type: "bool",
- value: false,
- },
- },
- },
- release: {
- name: "📦 Release Features",
- type: "folder",
- items: {
- uploader: {
- name: "⬆️ Release Uploader",
- title: "Show uploader of release assets",
- type: "bool",
- value: true,
- },
- downloads: {
- name: "📥 Release Downloads",
- title: "Show download counts of release assets",
- type: "bool",
- value: true,
- },
- histogram: {
- name: "📊 Release Histogram",
- title: "Show a histogram of download counts for each release asset",
- type: "bool",
- },
- hideArchives: {
- name: "🫥 Hide Archives",
- title: "Hide source code archives (zip, tar.gz) in the release assets",
- type: "bool",
- },
- },
- },
- additional: {
- name: "🪄 Additional Features",
- type: "folder",
- items: {
- trackingPrevention: {
- name: "🎭 Tracking Prevention",
- title: () => { return `Prevent some tracking by GitHub (${name} has prevented tracking ${GM_getValue("trackingPrevented", 0)} time(s))`; },
- type: "bool",
- value: true,
- },
- },
- },
- advanced: {
- name: "⚙️ Advanced Settings",
- type: "folder",
- items: {
- token: {
- name: "🔑 Personal Access Token",
- title: "Your personal access token for GitHub API, starting with `github_pat_` (used for increasing rate limit)",
- type: "str",
- },
- rateLimit: {
- name: "📈 Rate Limit",
- title: "View the current rate limit status",
- type: "action",
- },
- debug: {
- name: "🐞 Debug",
- title: "Enable debug mode",
- type: "bool",
- },
- },
- },
- };
- const config = new GM_config(configDesc);
-
- // Helper function for css
- function injectCSS(id, css) {
- const style = document.head.appendChild(document.createElement("style"));
- style.id = idPrefix + id;
- style.textContent = css;
- return style;
- }
- function cssHelper(id, enable) {
- const current = document.getElementById(idPrefix + id);
- if (current) {
- current.disabled = !enable;
- } else if (enable) {
- injectCSS(id, dynamicStyles[id]);
- }
- }
- // General functions
- const $ = document.querySelector.bind(document);
- const $$ = document.querySelectorAll.bind(document);
- /**
- * Log the given arguments if debug mode is enabled.
- * @param {...any} args The arguments to log.
- */
- function log(...args) {
- if (config.get("advanced.debug")) console.log(`%c[${name}]%c`, `color:${themeColor};`, "color: unset;", ...args);
- }
- /**
- * Warn the given arguments.
- * @param {...any} args The arguments to warn.
- */
- function warn(...args) {
- console.warn(`%c[${name}]%c`, `color:${themeColor};`, "color: unset;", ...args);
- }
- /**
- * Replace the domain of the given URL with the top domain if needed.
- * @param {string} url The URL to fix.
- * @returns {string} The fixed URL.
- */
- function fixDomain(url) {
- return (topDomain === officialDomain) ? url : url.replace(`https://${officialDomain}/`, `https://${topDomain}/`); // Replace top domain
- }
- /**
- * Fetch the given URL with the personal access token, if given. Also updates rate limit.
- * @param {string} url The URL to fetch.
- * @param {RequestInit} options The options to pass to `fetch`.
- * @returns {Promise<Response>} The response from the fetch.
- */
- async function fetchWithToken(url, options) {
- const token = config.get("advanced.token");
- if (token) {
- if (!options) options = {};
- if (!options.headers) options.headers = {};
- options.headers.accept = "application/vnd.github+json";
- options.headers["X-GitHub-Api-Version"] = "2022-11-28";
- options.headers.Authorization = `Bearer ${token}`;
- }
- const r = await fetch(url, options);
- function parseRateLimit(suffix, defaultValue = -1) {
- const parsed = parseInt(r.headers.get(`X-RateLimit-${suffix}`));
- return isNaN(parsed) ? defaultValue : parsed;
- }
- // Update rate limit
- for (const key of Object.keys(rateLimit)) {
- rateLimit[key] = parseRateLimit(key); // Case-insensitive
- }
- const resetDate = new Date(rateLimit.reset * 1000).toLocaleString();
- log(`Rate limit: remaining ${rateLimit.remaining}/${rateLimit.limit}, resets at ${resetDate}`);
- if (r.status === 403 || r.status === 429) { // If we get 403 or 429, we've hit the rate limit.
- throw new Error(`Rate limit exceeded! Will reset at ${resetDate}`);
- } else if (rateLimit.remaining === 0) {
- warn(`Rate limit has been exhausted! Will reset at ${resetDate}`);
- }
- return r;
- }
-
- // CSS-related features
- const dynamicStyles = {
- "code.cursorBlink": "[data-testid='navigation-cursor'] { animation: blink 1s step-end infinite; }",
- "code.cursorAnimation": "[data-testid='navigation-cursor'] { transition: top 0.1s ease-in-out, left 0.1s ease-in-out; }",
- "code.fullWidth": "#copilot-button-positioner { padding-right: 0; }",
- "appearance.stickyAvatar": `
- div.TimelineItem-avatar { /* .js-timeline-item > .TimelineItem > .TimelineItem-avatar */
- position: relative;
- margin-left: -40px;
- left: -32px;
- & > a[data-hovercard-type='user'] {
- position: sticky;
- top: 5em;
- }
- }
- /* .page-responsive .timeline-comment--caret {
- &::before, &::after {
- position: sticky;
- top: 4em;
- margin-top: -1em;
- transform: translate(-0.5em, 2em);
- }
- } */
- `,
- };
- for (const prop in dynamicStyles) {
- cssHelper(prop, config.get(prop));
- }
-
- // Code features
- /**
- * Show the full command to clone a repository.
- * @param {HTMLElement} [target] The target element to search for the embedded data.
- */
- function cloneFullCommand(target = document.body) {
- document.currentScript?.remove(); // Self-remove
- const embeddedData = target.querySelector('react-partial[partial-name="repos-overview"] > script[data-target="react-partial.embeddedData"]'); // The element containing the repository information
- if (!embeddedData) {
- log("Full clone command not enabled - no embedded data found");
- return false;
- }
- const data = JSON.parse(embeddedData?.textContent);
- const protocolInfo = data.props?.initialPayload?.overview?.codeButton?.local?.protocolInfo;
- if (!protocolInfo) {
- log("Full clone command not enabled - no protocol information found");
- return false;
- }
- function prefix(uri) {
- return !uri || uri.startsWith("git clone ") ? uri : "git clone " + uri;
- }
- protocolInfo.httpUrl = prefix(protocolInfo.httpUrl);
- protocolInfo.sshUrl = prefix(protocolInfo.sshUrl);
- embeddedData.textContent = JSON.stringify(data);
- log("Full clone command enabled");
- return true;
- }
- if (config.get("code.cloneFullCommand")) {
- // document.addEventListener("DOMContentLoaded", cloneFullCommand, { once: true }); // Doesn't work, since our script is running too late, after `embeddedData` is accessed by GitHub. Need to add the script in the head so as to defer DOM parsing.
- const dataPresent = $('react-partial[partial-name="repos-overview"] > script[data-target="react-partial.embeddedData"]');
- if (dataPresent) {
- cloneFullCommand();
- } else {
- // https://a.opnxng.com/exchange/stackoverflow.com/questions/41394983/how-to-defer-inline-javascript
- const logDef = config.get("advanced.debug") ? `const log = (...args) => console.log("%c[${name}]%c", "color:${themeColor};", "color: unset;", ...args);\n` : "const log = () => {};\n"; // Define the `log` function, respecting the debug mode
- const scriptText = logDef + "const target = document.body;\n" + cloneFullCommand.toString().replace(/^.*?{|}$/g, ""); // Get the function body
- const wrapped = `(function() {${scriptText}})();`; // Wrap the function in an IIFE so as to prevent polluting the global scope
- GM_addElement(document.head, "script", { textContent: wrapped, type: "module" }); // Use `GM_addElement` instead of native `appendChild` to bypass CSP
- // Utilize data URI and set `defer` attribute to defer the script execution (can't bypass CSP)
- // GM_addElement(document.head, "script", { src: `data:text/javascript,${encodeURIComponent(wrapped)}`, defer: true });
- }
- // Adapt to dynamic loading
- document.addEventListener("turbo:before-render", e => {
- cloneFullCommand(e.detail.newBody.querySelector("[data-turbo-body]") ?? e.detail.newBody);
- });
- }
- /**
- * Set the tab size for the code blocks.
- * @param {number} size The tab size to set.
- */
- function tabSize(size) {
- const id = idPrefix + "tabSize";
- const style = document.getElementById(id) ?? injectCSS(id, "");
- style.textContent = `pre, code { tab-size: ${size}; }`;
- }
-
- // Appearance features
- /**
- * Dynamic styles for the enum settings.
- * @type {Object<string, Array<string>>}
- */
- const enumStyles = {
- "appearance.dashboard": [
- "/* Default */",
- "/* Hide Copilot */ #dashboard > .news > .copilotPreview__container { display: none; }",
- "/* Hide Feed */ #dashboard > .news > feed-container { display: none; }",
- `/* Mobile-Like */
- .application-main > div > aside[aria-label="Account context"] {
- display: block !important;
- }
- #dashboard > .news {
- > .copilotPreview__container { display: none; }
- > feed-container { display: none; }
- > .d-block.d-md-none { display: block !important; }
- }`,
- ],
- "appearance.leftSidebar": [
- "/* Default */",
- "/* Hidden */ .application-main .feed-background > aside.feed-left-sidebar { display: none; }",
- ],
- "appearance.rightSidebar": [
- "/* Default */",
- "/* Hide 'Latest changes' */ aside.feed-right-sidebar > .dashboard-changelog { display: none; }",
- "/* Hide 'Explore repositories' */ aside.feed-right-sidebar > [aria-label='Explore repositories'] { display: none; }",
- "/* Hide Completely */ aside.feed-right-sidebar { display: none; }",
- ],
- };
- /**
- * Helper function to configure enum styles.
- * @param {string} id The ID of the style.
- * @param {string} mode The mode to set.
- */
- function enumStyleHelper(id, mode) {
- const style = document.getElementById(idPrefix + id) ?? injectCSS(id, "");
- style.textContent = enumStyles[id][mode];
- }
- for (const prop in enumStyles) {
- enumStyleHelper(prop, config.get(prop));
- }
-
- // Release features
- /**
- * Get the release data for the given owner, repo and version.
- * @param {string} owner The owner of the repository.
- * @param {string} repo The repository name.
- * @param {string} version The version tag of the release.
- * @returns {Promise<Object>} The release data, which resolves to an object mapping download link to details.
- */
- async function getReleaseData(owner, repo, version) {
- if (!releaseData[owner]) releaseData[owner] = {};
- if (!releaseData[owner][repo]) releaseData[owner][repo] = {};
- if (!releaseData[owner][repo][version]) {
- const url = `https://api.${topDomain}/repos/${owner}/${repo}/releases/tags/${version}`;
- const promise = fetchWithToken(url).then(
- response => response.json()
- ).then(data => {
- log(`Fetched release data for ${owner}/${repo}@${version}:`, data);
- const assets = {};
- for (const asset of data.assets) {
- assets[fixDomain(asset.browser_download_url)] = {
- downloads: asset.download_count,
- uploader: {
- name: asset.uploader.login,
- url: fixDomain(asset.uploader.html_url)
- }
- };
- }
- log(`Processed release data for ${owner}/${repo}@${version}:`, assets);
- return assets;
- });
- releaseData[owner][repo][version] = promise;
- }
- return releaseData[owner][repo][version];
- }
- /**
- * Create a link to the uploader's profile.
- * @param {Object} uploader The uploader information.
- * @param {string} uploader.name The name of the uploader.
- * @param {string} uploader.url The URL to the uploader's profile.
- */
- function createUploaderLink(uploader) {
- const link = document.createElement("a");
- link.href = uploader.url;
- link.setAttribute("class", "text-sm-left flex-auto ml-md-3 nowrap");
- if (uploader.url.startsWith(`https://${topDomain}/apps/`)) {
- link.classList.add("color-fg-success");
- // Remove suffix `[bot]` from the name if exists
- const name = uploader.name.endsWith("[bot]") ? uploader.name.slice(0, -5) : uploader.name;
- link.title = `Uploaded by GitHub App @${name}`;
- link.textContent = `@${name}`;
- } else {
- link.classList.add("color-fg-muted");
- link.setAttribute("data-hovercard-url", `/users/${uploader.name}/hovercard`);
- link.title = `Uploaded by @${uploader.name}`;
- link.textContent = `@${uploader.name}`;
- }
- return link;
- }
- /**
- * Create a span element with the given download count.
- * @param {number} downloads The download count.
- */
- function createDownloadCount(downloads) {
- const downloadCount = document.createElement("span");
- downloadCount.textContent = `${downloads} DL`;
- downloadCount.title = `${downloads} downloads`;
- downloadCount.setAttribute("class", "color-fg-muted text-sm-left flex-shrink-0 flex-grow-0 ml-md-3 nowrap");
- return downloadCount;
- }
- /**
- * Show a histogram of the download counts for the given release entry.
- * @param {HTMLElement} asset One of the release assets.
- * @param {number} value The download count of the asset.
- * @param {number} max The maximum download count of all assets.
- */
- function showHistogram(asset, value, max) {
- asset.style.setProperty("--percent", `${value / max * 100}%`);
- }
- /**
- * Adding additional info (download count) to the release entries under the given element.
- * @param {HTMLElement} el The element to search for release entries.
- * @param {Object} info Additional information about the release (owner, repo, version).
- * @param {string} info.owner The owner of the repository.
- * @param {string} info.repo The repository name.
- * @param {string} info.version The version of the release.
- */
- async function addAdditionalInfoToRelease(el, info) {
- const entries = el.querySelectorAll("ul > li");
- const assets = [];
- const hideArchives = config.get("release.hideArchives");
- entries.forEach((asset) => {
- if (asset.querySelector("svg.octicon-package")) {
- // Release asset
- assets.push(asset);
- } else if (hideArchives) {
- // Source code archive
- asset.remove();
- }
- });
- const releaseData = await getReleaseData(info.owner, info.repo, info.version);
- if (!releaseData) return;
- const maxDownloads = Math.max(0, ...Object.values(releaseData).map(asset => asset.downloads));
- assets.forEach(asset => {
- const downloadLink = asset.children[0].querySelector("a")?.href;
- const statistics = asset.children[1];
- const assetInfo = releaseData[downloadLink];
- if (!assetInfo) return;
- asset.classList.add("ghp-release-asset");
- const size = statistics.querySelector("span.flex-auto");
- size.classList.remove("flex-auto");
- size.classList.add("flex-shrink-0", "flex-grow-0");
- if (config.get("release.downloads")) {
- const downloadCount = createDownloadCount(assetInfo.downloads);
- statistics.prepend(downloadCount);
- }
- if (config.get("release.uploader")) {
- const uploaderLink = createUploaderLink(assetInfo.uploader);
- statistics.prepend(uploaderLink);
- }
- if (config.get("release.histogram") && maxDownloads > 0 && assets.length > 1) {
- showHistogram(asset, assetInfo.downloads, maxDownloads);
- }
- });
- }
- /**
- * Handle the `include-fragment-replace` event.
- * @param {CustomEvent} event The event object.
- */
- function onFragmentReplace(event) {
- const self = event.target;
- const src = self.src;
- const match = expandedAssetsRegex.exec(src);
- if (!match) return;
- const [_, owner, repo, version] = match;
- const info = { owner, repo, version };
- const fragment = event.detail.fragment;
- log("Found expanded assets:", fragment);
- for (const child of fragment.children) {
- addAdditionalInfoToRelease(child, info);
- }
- }
- /**
- * Find all release entries and setup listeners to show the download count.
- */
- function setupListeners() {
- log("Calling setupListeners");
- if (!config.get("release.downloads") && !config.get("release.uploader") && !config.get("release.histogram")) return; // No need to run
- // IncludeFragmentElement: https://github.com/github/include-fragment-element/blob/main/src/include-fragment-element.ts
- const fragments = document.querySelectorAll('[data-hpc] details[data-view-component="true"] include-fragment');
- fragments.forEach(fragment => {
- if (!fragment.hasAttribute("data-ghp-listening")) {
- fragment.toggleAttribute("data-ghp-listening", true);
- fragment.addEventListener("include-fragment-replace", onFragmentReplace, { once: true });
- if (config.get("release.hideArchives")) {
- // Fix assets count
- const summary = fragment.parentElement.previousElementSibling;
- if (summary.tagName === "SUMMARY" && summary.firstElementChild.textContent === "Assets") {
- const counter = summary.querySelector("span.Counter");
- if (counter) {
- const count = parseInt(counter.textContent) - 2; // Exclude the source code archives
- log(counter, count + 2, count);
- counter.textContent = count.toString();
- counter.title = count.toString();
- }
- }
- }
- }
- });
- }
- if (location.hostname === topDomain) { // Only run on GitHub main site
- document.addEventListener("DOMContentLoaded", setupListeners, { once: true });
- // Examine event listeners on `document`, and you can see the event listeners for the `turbo:*` events. (Remember to check `Framework Listeners`)
- document.addEventListener("turbo:load", setupListeners);
- // Other possible approaches and reasons against them:
- // - Use `MutationObserver` - Not efficient
- // - Hook `CustomEvent` to make `include-fragment-replace` events bubble - Monkey-patching
- // - Patch `IncludeFragmentElement.prototype.fetch`, just like GitHub itself did at `https://github.githubassets.com/assets/app/assets/modules/github/include-fragment-element-hacks.ts`
- // - Monkey-patching
- // - If using regex to modify the response, it would be tedious to maintain
- // - If using `DOMParser`, the same HTML would be parsed twice
- injectCSS("release", `
- @media (min-width: 1012px) { /* Making more room for the additional info */
- .ghp-release-asset .col-lg-9 {
- width: 60%; /* Originally ~75% */
- }
- }
- .nowrap { /* Preventing text wrapping */
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .ghp-release-asset { /* Styling the histogram */
- background: linear-gradient(to right, var(--bgColor-accent-muted) var(--percent, 0%), transparent 0);
- }
- `);
- }
-
- // Tracking prevention
- function preventTracking() {
- log("Calling preventTracking");
- const elements = [
- // Prevents tracking data from being sent to https://collector.github.com/github/collect
- // https://github.githubassets.com/assets/node_modules/@github/hydro-analytics-client/dist/meta-helpers.js
- // Breakpoint on function `getOptionsFromMeta` to see the argument `prefix`, which is `octolytics`
- // Or investigate `hydro-analytics.ts` mentioned above, you may find: `const options = getOptionsFromMeta('octolytics')`
- // Later, this script gathers information from `meta[name^="${prefix}-"]` elements, so we can remove them.
- // If `collectorUrl` is not set, the script will throw an error, thus preventing tracking.
- ...$$("meta[name^=octolytics-]"),
- // Prevents tracking data from being sent to `https://api.github.com/_private/browser/stats`
- // From "Network" tab, we can find that this request is sent by `https://github.githubassets.com/assets/ui/packages/stats/stats.ts` at function `safeSend`, who accepts two arguments: `url` and `data`
- // Search for this function in the current script, and you will find that it is only called once by function `flushStats`
- // `url` parameter is set in this function, by: `const url = ssrSafeDocument?.head?.querySelector<HTMLMetaElement>('meta[name="browser-stats-url"]')?.content`
- // After removing the meta tag, the script will return, so we can remove this meta tag to prevent tracking.
- $("meta[name=browser-stats-url]")
- ];
- elements.forEach(el => el?.remove());
- if (elements.some(el => el)) {
- log("Prevented tracking", elements);
- GM_setValue("trackingPrevented", GM_getValue("trackingPrevented", 0) + 1);
- }
- }
- if (config.get("additional.trackingPrevention")) {
- // document.addEventListener("DOMContentLoaded", preventTracking);
- // All we need to remove is in the `head` element, so we can run it immediately.
- preventTracking();
- document.addEventListener("turbo:before-render", preventTracking);
- }
-
- // Debugging
- if (config.get("advanced.debug")) {
- const events = ["turbo:before-render", "turbo:before-morph-element", "turbo:before-frame-render", "turbo:load", "turbo:render", "turbo:morph", "turbo:morph-element", "turbo:frame-render"];
- events.forEach(event => {
- document.addEventListener(event, e => log(`Event: ${event}`, e));
- });
- }
-
- // Callbacks
- const callbacks = {
- "code.tabSize": tabSize,
- };
- for (const [prop, callback] of Object.entries(callbacks)) {
- callback(config.get(prop));
- }
-
- // Show rate limit
- config.addEventListener("get", (e) => {
- if (e.detail.prop === "advanced.rateLimit") {
- const resetDate = new Date(rateLimit.reset * 1000).toLocaleString();
- alert(`Rate limit: remaining ${rateLimit.remaining}/${rateLimit.limit}, resets at ${resetDate}.\nIf you see -1, it means the rate limit has not been fetched yet, or GitHub has not provided the rate limit information.`);
- }
- });
- config.addEventListener("set", (e) => {
- if (e.detail.prop in dynamicStyles) {
- cssHelper(e.detail.prop, e.detail.after);
- }
- if (e.detail.prop in enumStyles) {
- enumStyleHelper(e.detail.prop, e.detail.after);
- }
- if (e.detail.prop in callbacks) {
- callbacks[e.detail.prop](e.detail.after);
- }
- });
-
- log(`${name} v${version} has been loaded 🎉`);
- })();