Add quick access to Dashboard, Trending, Explore, Collections, and Stars from GitHub's top navigation.
// ==UserScript==
// @name Better GitHub Navigation
// @name:zh-CN 更好的 GitHub 导航栏
// @namespace https://github.com/ImXiangYu/better-github-nav
// @version 0.1.27
// @description Add quick access to Dashboard, Trending, Explore, Collections, and Stars from GitHub's top navigation.
// @description:zh-CN 在 GitHub 顶部导航中加入 Dashboard、Trending、Explore、Collections、Stars 快捷入口,常用页面一键直达。
// @author Ayubass
// @license MIT
// @match https://github.com/*
// @icon https://github.githubassets.com/pinned-octocat.svg
// @grant GM_registerMenuCommand
// ==/UserScript==
(() => {
// src/constants.js
var SCRIPT_VERSION = "0.1.27";
var CUSTOM_BUTTON_CLASS = "custom-gh-nav-btn";
var CUSTOM_BUTTON_ACTIVE_CLASS = "custom-gh-nav-btn-active";
var CUSTOM_BUTTON_COMPACT_CLASS = "custom-gh-nav-btn-compact";
var QUICK_LINK_MARK_ATTR = "data-better-gh-nav-quick-link";
var CONFIG_STORAGE_KEY = "better-gh-nav-config-v1";
var UI_LANG_STORAGE_KEY = "better-gh-nav-ui-lang-v1";
var SETTINGS_OVERLAY_ID = "custom-gh-nav-settings-overlay";
var SETTINGS_PANEL_ID = "custom-gh-nav-settings-panel";
var SETTINGS_MESSAGE_ID = "custom-gh-nav-settings-message";
var DEFAULT_LINK_KEYS = ["dashboard", "explore", "trending", "collections", "stars"];
var PRESET_LINKS = [
{ key: "dashboard", text: "Dashboard", path: "/dashboard", getHref: () => "/dashboard" },
{ key: "explore", text: "Explore", path: "/explore", getHref: () => "/explore" },
{ key: "trending", text: "Trending", path: "/trending", getHref: () => "/trending" },
{ key: "collections", text: "Collections", path: "/collections", getHref: () => "/collections" },
{ key: "stars", text: "Stars", path: "/stars", getHref: (username) => username ? `/${username}?tab=stars` : "/stars" }
];
var I18N = {
zh: {
menuOpenSettings: "Better GitHub Nav: 打开设置面板",
menuResetSettings: "Better GitHub Nav: 重置快捷链接配置",
menuLangZh: "Better GitHub Nav: 界面语言 -> 中文",
menuLangEn: "Better GitHub Nav: 界面语言 -> English",
menuLangAuto: "Better GitHub Nav: 界面语言 -> 自动(跟随页面)",
resetConfirm: "确认重置快捷链接配置为默认值吗?",
panelTitle: "Better GitHub Nav 设置",
panelDesc: "勾选决定显示项,拖动整行(或右侧手柄)调整显示顺序。",
resetDefault: "恢复默认",
cancel: "取消",
saveAndRefresh: "保存并刷新",
restoredPendingSave: "已恢复默认,点击保存后生效。",
atLeastOneLink: "至少保留 1 个快捷链接。",
dragHandleTitle: "拖动调整顺序",
dragRowTitle: "拖动整行调整顺序"
},
en: {
menuOpenSettings: "Better GitHub Nav: Open Settings Panel",
menuResetSettings: "Better GitHub Nav: Reset Quick Link Config",
menuLangZh: "Better GitHub Nav: UI Language -> 中文",
menuLangEn: "Better GitHub Nav: UI Language -> English",
menuLangAuto: "Better GitHub Nav: UI Language -> Auto (Follow Page)",
resetConfirm: "Reset quick-link config to defaults?",
panelTitle: "Better GitHub Nav Settings",
panelDesc: "Select visible links and drag the row (or handle) to reorder.",
resetDefault: "Reset to Default",
cancel: "Cancel",
saveAndRefresh: "Save and Refresh",
restoredPendingSave: "Defaults restored. Click save to apply.",
atLeastOneLink: "Keep at least 1 quick link.",
dragHandleTitle: "Drag to reorder",
dragRowTitle: "Drag row to reorder"
}
};
// src/config.js
function sanitizeKeys(keys) {
const validSet = new Set(DEFAULT_LINK_KEYS);
const seen = /* @__PURE__ */ new Set();
const result = [];
keys.forEach((key) => {
if (validSet.has(key) && !seen.has(key)) {
seen.add(key);
result.push(key);
}
});
return result;
}
function sanitizeConfig(rawConfig) {
const enabledKeys = sanitizeKeys(Array.isArray(rawConfig?.enabledKeys) ? rawConfig.enabledKeys : DEFAULT_LINK_KEYS);
const orderKeysRaw = sanitizeKeys(Array.isArray(rawConfig?.orderKeys) ? rawConfig.orderKeys : DEFAULT_LINK_KEYS);
const orderSet = new Set(orderKeysRaw);
const orderKeys = [
...orderKeysRaw,
...DEFAULT_LINK_KEYS.filter((key) => !orderSet.has(key))
];
return {
enabledKeys: enabledKeys.length ? enabledKeys : DEFAULT_LINK_KEYS.slice(),
orderKeys: orderKeys.length ? orderKeys : DEFAULT_LINK_KEYS.slice()
};
}
function loadConfig() {
try {
const raw = localStorage.getItem(CONFIG_STORAGE_KEY);
if (!raw) return sanitizeConfig({});
return sanitizeConfig(JSON.parse(raw));
} catch (e) {
return sanitizeConfig({});
}
}
function saveConfig(config) {
localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(sanitizeConfig(config)));
}
function getConfiguredLinks(username) {
const config = loadConfig();
const presetMap = new Map(
PRESET_LINKS.map((link) => [link.key, {
...link,
id: `custom-gh-btn-${link.key}`,
href: link.getHref(username)
}])
);
return config.orderKeys.filter((key) => config.enabledKeys.includes(key)).map((key) => presetMap.get(key)).filter(Boolean);
}
function getDisplayNameByKey(key) {
const link = PRESET_LINKS.find((item) => item.key === key);
return link ? link.text : key;
}
// src/styles.js
function ensureStyles() {
if (document.getElementById("custom-gh-nav-style")) return;
const style = document.createElement("style");
style.id = "custom-gh-nav-style";
style.textContent = `
a.${CUSTOM_BUTTON_CLASS} {
border-radius: 6px;
padding-inline: 8px;
text-decoration: none;
}
a.${CUSTOM_BUTTON_CLASS}.${CUSTOM_BUTTON_COMPACT_CLASS} {
padding-inline: 4px;
}
a.${CUSTOM_BUTTON_CLASS},
a.${CUSTOM_BUTTON_CLASS} span {
font-weight: 600;
}
a.${CUSTOM_BUTTON_CLASS},
a.${CUSTOM_BUTTON_CLASS} * {
cursor: pointer;
}
a.${CUSTOM_BUTTON_CLASS}:hover {
background-color: var(--color-neutral-muted, rgba(177, 186, 196, 0.12));
text-decoration: none;
}
a.${CUSTOM_BUTTON_CLASS}.${CUSTOM_BUTTON_ACTIVE_CLASS} {
background-color: var(--color-neutral-muted, rgba(177, 186, 196, 0.18));
font-weight: 600;
}
#${SETTINGS_OVERLAY_ID} {
position: fixed;
inset: 0;
z-index: 2147483647;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
box-sizing: border-box;
}
#${SETTINGS_PANEL_ID} {
width: min(560px, 100%);
max-height: min(80vh, 720px);
overflow: auto;
background: var(--color-canvas-default, #fff);
color: var(--color-fg-default, #1f2328);
border: 1px solid var(--color-border-default, #d1d9e0);
border-radius: 10px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.25);
padding: 16px;
box-sizing: border-box;
}
.custom-gh-nav-settings-title {
margin: 0 0 8px;
font-size: 16px;
line-height: 1.4;
}
.custom-gh-nav-settings-desc {
margin: 0 0 12px;
color: var(--color-fg-muted, #656d76);
font-size: 13px;
}
.custom-gh-nav-settings-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.custom-gh-nav-settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border: 1px solid var(--color-border-muted, #d8dee4);
border-radius: 8px;
padding: 8px 10px;
background: var(--color-canvas-subtle, #f6f8fa);
cursor: grab;
}
.custom-gh-nav-settings-row:active {
cursor: grabbing;
}
.custom-gh-nav-settings-row-left {
display: inline-flex;
align-items: center;
gap: 8px;
user-select: none;
font-size: 13px;
}
.custom-gh-nav-settings-row-left input {
cursor: pointer;
}
.custom-gh-nav-settings-row-actions {
display: inline-flex;
align-items: center;
gap: 6px;
}
.custom-gh-nav-settings-drag-handle {
border: 1px solid var(--color-border-default, #d1d9e0);
background: var(--color-btn-bg, #f6f8fa);
color: var(--color-fg-muted, #656d76);
border-radius: 6px;
width: 32px;
height: 26px;
line-height: 1;
font-size: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
user-select: none;
pointer-events: none;
}
.custom-gh-nav-settings-row-dragging {
opacity: 0.55;
}
.custom-gh-nav-settings-row-drag-over {
border-color: var(--color-accent-fg, #0969da);
background: var(--color-accent-subtle, #ddf4ff);
}
.custom-gh-nav-settings-btn {
border: 1px solid var(--color-border-default, #d1d9e0);
background: var(--color-btn-bg, #f6f8fa);
color: var(--color-fg-default, #1f2328);
border-radius: 6px;
padding: 4px 10px;
font-size: 12px;
cursor: pointer;
}
.custom-gh-nav-settings-btn:hover {
background: var(--color-btn-hover-bg, #f3f4f6);
}
.custom-gh-nav-settings-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.custom-gh-nav-settings-btn-primary {
background: var(--color-btn-primary-bg, #1f883d);
border-color: var(--color-btn-primary-bg, #1f883d);
color: var(--color-btn-primary-text, #fff);
}
.custom-gh-nav-settings-btn-primary:hover {
background: var(--color-btn-primary-hover-bg, #1a7f37);
}
.custom-gh-nav-settings-footer {
margin-top: 12px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
.custom-gh-nav-settings-message {
min-height: 20px;
margin-top: 8px;
color: var(--color-attention-fg, #9a6700);
font-size: 12px;
}
`;
document.head.appendChild(style);
}
function setActiveStyle(aTag, active, compact = false) {
aTag.classList.add(CUSTOM_BUTTON_CLASS);
if (compact) {
aTag.classList.add(CUSTOM_BUTTON_COMPACT_CLASS);
} else {
aTag.classList.remove(CUSTOM_BUTTON_COMPACT_CLASS);
}
if (active) {
aTag.setAttribute("aria-current", "page");
aTag.classList.add(CUSTOM_BUTTON_ACTIVE_CLASS);
} else {
aTag.removeAttribute("aria-current");
aTag.classList.remove(CUSTOM_BUTTON_ACTIVE_CLASS);
}
}
// src/navigation.js
function normalizePath(href) {
try {
const url = new URL(href, location.origin);
const path = url.pathname.replace(/\/+$/, "");
return path || "/";
} catch (e) {
return "";
}
}
function isCurrentPage(linkPath) {
const currentPath = location.pathname.replace(/\/+$/, "") || "/";
if (linkPath === "/dashboard") return currentPath === "/" || currentPath === "/dashboard";
if (currentPath === linkPath) return true;
if (linkPath !== "/" && currentPath.startsWith(`${linkPath}/`)) return true;
return location.search.includes("tab=stars") && linkPath === normalizePath("/stars");
}
function setLinkText(aTag, text) {
const innerSpan = aTag.querySelector("span");
if (innerSpan) {
innerSpan.textContent = text;
} else {
aTag.textContent = text;
}
}
function ensureAnchor(node, isLiParent) {
let aTag = isLiParent ? node.querySelector("a") : node.tagName.toLowerCase() === "a" ? node : node.querySelector("a");
if (aTag) return aTag;
const fallbackText = (node.textContent || "").trim();
const fallbackHref = !isLiParent && node.getAttribute && node.getAttribute("href") ? node.getAttribute("href") : `${location.pathname}${location.search}`;
const classSource = isLiParent ? node.querySelector('[class*="contextCrumb"], [class*="Breadcrumbs-Item"]') : node;
const spanTemplate = document.querySelector(
'header a[class*="contextCrumb"] span[class*="contextCrumbLast"]'
);
const spanSource = isLiParent ? node.querySelector("span") : node.querySelector("span");
aTag = document.createElement("a");
if (classSource && classSource.className) {
aTag.className = classSource.className.split(/\s+/).filter((cls) => cls && !cls.includes("contextCrumbStatic")).join(" ");
}
if (spanSource && spanSource.className) {
const innerSpan = document.createElement("span");
innerSpan.className = spanTemplate && spanTemplate.className ? spanTemplate.className : spanSource.className;
if (fallbackText) innerSpan.textContent = fallbackText;
aTag.appendChild(innerSpan);
}
if (!aTag.getAttribute("href") && fallbackHref) {
aTag.setAttribute("href", fallbackHref);
}
if (!aTag.textContent.trim() && fallbackText) {
const innerSpan = aTag.querySelector("span");
if (innerSpan) {
innerSpan.textContent = fallbackText;
} else {
aTag.textContent = fallbackText;
}
}
if (isLiParent) {
node.textContent = "";
node.appendChild(aTag);
} else {
node.replaceChildren(aTag);
}
return aTag;
}
function getAnchorHostNode(anchor) {
if (!anchor || !anchor.parentNode) return anchor;
return anchor.parentNode.tagName.toLowerCase() === "li" ? anchor.parentNode : anchor;
}
function cleanupQuickLinksForContainer(renderParent, keepNode) {
const quickAnchors = Array.from(
document.querySelectorAll(
'header a[id^="custom-gh-btn-"], header a[' + QUICK_LINK_MARK_ATTR + '="1"]'
)
);
quickAnchors.forEach((anchor) => {
const host = getAnchorHostNode(anchor);
if (!host || !host.parentNode) return;
if (host === keepNode) return;
if (host.parentNode !== renderParent) {
host.remove();
return;
}
host.remove();
});
}
function addCustomButtons() {
const userLoginMeta = document.querySelector('meta[name="user-login"]');
const username = userLoginMeta ? userLoginMeta.getAttribute("content") : "";
const navPresetLinks = getConfiguredLinks(username);
if (!navPresetLinks.length) return;
const primaryLink = navPresetLinks[0];
const extraLinks = navPresetLinks.slice(1);
const fixedPages = /* @__PURE__ */ new Set(["/dashboard", "/trending", "/explore", "/collections"]);
const shortcutPaths = new Set(PRESET_LINKS.map((link) => link.path));
const compactPages = /* @__PURE__ */ new Set(["/issues", "/pulls", "/repositories"]);
const isOnPresetPage = Array.from(fixedPages).some((path) => isCurrentPage(path));
const shouldUseCompactButtons = Array.from(compactPages).some((path) => isCurrentPage(path));
let targetNode = null;
let targetSource = "";
if (isOnPresetPage) {
targetNode = document.querySelector(
'header nav a[href="/dashboard"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/trending"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/explore"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"])'
);
if (targetNode) targetSource = "preset-nav";
if (!targetNode) {
targetNode = document.querySelector(
'header nav a[id^="custom-gh-btn-"], header nav a[' + QUICK_LINK_MARK_ATTR + '="1"]'
);
if (targetNode) targetSource = "preset-quick";
}
} else {
const breadcrumbNodes = Array.from(document.querySelectorAll(
'header nav[aria-label*="breadcrumb" i] a[href^="/"], header a[class*="contextCrumb"][href^="/"], header a[class*="Breadcrumbs-Item"][href^="/"]'
)).filter((link) => {
if (link.id && link.id.startsWith("custom-gh-btn-")) return false;
if (link.getAttribute(QUICK_LINK_MARK_ATTR) === "1") return false;
const href = normalizePath(link.getAttribute("href") || "");
if (!href || href === "/") return false;
if (shortcutPaths.has(href)) return false;
return true;
});
if (breadcrumbNodes.length) {
targetNode = breadcrumbNodes[breadcrumbNodes.length - 1];
targetSource = "breadcrumb";
}
}
if (!targetNode) {
targetNode = document.querySelector(
'header nav a[aria-current="page"]:not([id^="custom-gh-btn-"]), header nav a[data-active="true"]:not([id^="custom-gh-btn-"]), header nav [aria-current="page"]:not(a), header nav [data-active="true"]:not(a)'
);
if (targetNode) targetSource = "current-nav";
}
if (!targetNode) {
const navLinks = document.querySelectorAll(
'header a:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"])'
);
for (const link of navLinks) {
const text = link.textContent.trim().toLowerCase();
const href = link.getAttribute("href");
if (text === "dashboard" || href === "/dashboard") {
targetNode = link;
targetSource = "legacy-dashboard";
break;
}
}
}
if (!targetNode) {
const currentPath = location.pathname.replace(/\/+$/, "") || "/";
const globalNavCandidates = Array.from(
document.querySelectorAll(
'header nav[aria-label*="global" i] a[href^="/"], header nav[aria-label*="header" i] a[href^="/"], header nav a[href="/pulls"], header nav a[href="/issues"], header nav a[href="/repositories"], header nav a[href="/codespaces"], header nav a[href="/marketplace"], header nav a[href="/explore"]'
)
).filter((link) => {
const href = normalizePath(link.getAttribute("href") || "");
if (!href || href === "/") return false;
if (link.id && link.id.startsWith("custom-gh-btn-")) return false;
if (link.getAttribute(QUICK_LINK_MARK_ATTR) === "1") return false;
return true;
});
if (globalNavCandidates.length) {
targetNode = globalNavCandidates.find((link) => {
const href = normalizePath(link.getAttribute("href") || "");
return href === currentPath;
}) || globalNavCandidates[globalNavCandidates.length - 1];
if (targetNode) targetSource = "global-nav";
}
}
if (!targetNode) {
const currentTextNode = document.querySelector(
'header nav [aria-current="page"]:not(a), header nav [data-active="true"]:not(a)'
);
if (currentTextNode) {
targetNode = currentTextNode;
targetSource = "current-text";
}
}
if (!targetNode) {
const contextCrumbTextNodes = document.querySelectorAll(
'header span[class*="contextCrumbStatic"], header span[class*="contextCrumb"][class*="Breadcrumbs-Item"], header .prc-Breadcrumbs-Item-jcraJ'
);
if (contextCrumbTextNodes.length) {
targetNode = contextCrumbTextNodes[contextCrumbTextNodes.length - 1];
targetSource = "crumb-text";
}
}
let templateNode = targetNode;
if (targetNode) {
const localNav = targetNode.closest("nav, ul, ol");
const localAnchors = localNav ? localNav.querySelectorAll(
'a[href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"])'
) : [];
if (localAnchors.length) {
templateNode = localAnchors[localAnchors.length - 1];
} else {
const nativeNavAnchors = document.querySelectorAll(
'header nav[aria-label*="breadcrumb" i] a[href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header a[class*="contextCrumb"][href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header a[class*="Breadcrumbs-Item"][href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav[aria-label*="global" i] a[href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav[aria-label*="header" i] a[href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/pulls"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/issues"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/repositories"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/codespaces"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/marketplace"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), header nav a[href="/explore"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"])'
);
if (nativeNavAnchors.length) {
templateNode = nativeNavAnchors[nativeNavAnchors.length - 1];
}
}
}
if (targetNode) {
const isTargetLiParent = targetNode.parentNode.tagName.toLowerCase() === "li";
const insertAnchorNode = isTargetLiParent ? targetNode.parentNode : targetNode;
const isTemplateLiParent = templateNode.parentNode.tagName.toLowerCase() === "li";
const cloneTemplateNode = isTemplateLiParent ? templateNode.parentNode : templateNode;
const targetHasAnchor = isTargetLiParent ? Boolean(insertAnchorNode.querySelector("a")) : insertAnchorNode.tagName.toLowerCase() === "a" || Boolean(insertAnchorNode.querySelector("a"));
const shouldForceCreateAnchor = !targetHasAnchor && Boolean(targetNode.closest("header nav"));
const anchorTag = targetHasAnchor || shouldForceCreateAnchor ? ensureAnchor(insertAnchorNode, isTargetLiParent) : null;
cleanupQuickLinksForContainer(insertAnchorNode.parentNode, insertAnchorNode);
const hasShortcutActive = navPresetLinks.some((link) => isCurrentPage(link.path));
if (isOnPresetPage && anchorTag && primaryLink) {
anchorTag.id = primaryLink.id;
anchorTag.setAttribute(QUICK_LINK_MARK_ATTR, "1");
anchorTag.href = primaryLink.href;
setLinkText(anchorTag, primaryLink.text);
setActiveStyle(anchorTag, isCurrentPage(primaryLink.path), shouldUseCompactButtons);
} else {
if (anchorTag && anchorTag.id && anchorTag.id.startsWith("custom-gh-btn-")) {
anchorTag.removeAttribute("id");
}
if (anchorTag) {
anchorTag.removeAttribute(QUICK_LINK_MARK_ATTR);
}
if (anchorTag) {
setActiveStyle(anchorTag, !hasShortcutActive, shouldUseCompactButtons);
}
}
let insertAfterNode = insertAnchorNode;
const linksToRender = isOnPresetPage ? extraLinks : navPresetLinks;
linksToRender.forEach((linkInfo) => {
const newNode = cloneTemplateNode.cloneNode(true);
const aTag = ensureAnchor(newNode, isTemplateLiParent);
aTag.id = linkInfo.id;
aTag.setAttribute(QUICK_LINK_MARK_ATTR, "1");
aTag.href = linkInfo.href;
setLinkText(aTag, linkInfo.text);
setActiveStyle(aTag, isCurrentPage(linkInfo.path), shouldUseCompactButtons);
insertAfterNode.parentNode.insertBefore(newNode, insertAfterNode.nextSibling);
insertAfterNode = newNode;
});
}
}
// src/i18n.js
var uiLang = detectUiLang();
function t(key, vars = {}) {
const dict = I18N[uiLang] || I18N.en;
const fallback = I18N.en;
const template = dict[key] || fallback[key] || key;
return template.replace(/\{(\w+)\}/g, (_, varName) => String(vars[varName] ?? ""));
}
function detectUiLang() {
try {
const preferredLang = (localStorage.getItem(UI_LANG_STORAGE_KEY) || "").toLowerCase();
if (preferredLang === "zh" || preferredLang === "en") return preferredLang;
} catch (e) {
}
const autoLang = (document.documentElement.lang || navigator.language || "").toLowerCase();
return autoLang.startsWith("zh") ? "zh" : "en";
}
function setUiLangPreference(lang) {
try {
if (lang === "zh" || lang === "en") {
localStorage.setItem(UI_LANG_STORAGE_KEY, lang);
} else {
localStorage.removeItem(UI_LANG_STORAGE_KEY);
}
} catch (e) {
}
uiLang = detectUiLang();
}
// src/settings-panel.js
var settingsEscHandler = null;
function closeConfigPanel() {
const overlay = document.getElementById(SETTINGS_OVERLAY_ID);
if (overlay) overlay.remove();
if (settingsEscHandler) {
document.removeEventListener("keydown", settingsEscHandler);
settingsEscHandler = null;
}
}
function createPanelState(config) {
const safeConfig = sanitizeConfig(config);
return {
order: safeConfig.orderKeys.slice(),
enabledSet: new Set(safeConfig.enabledKeys)
};
}
function reorderKeys(state, draggedKey, targetKey, placeAfter = false) {
const fromIndex = state.order.indexOf(draggedKey);
const targetIndex = state.order.indexOf(targetKey);
if (fromIndex < 0 || targetIndex < 0 || fromIndex === targetIndex) return false;
const [movedKey] = state.order.splice(fromIndex, 1);
let insertIndex = targetIndex + (placeAfter ? 1 : 0);
if (fromIndex < targetIndex) {
insertIndex -= 1;
}
state.order.splice(insertIndex, 0, movedKey);
return true;
}
function clearDragClasses(listEl) {
const rows = listEl.querySelectorAll(".custom-gh-nav-settings-row");
rows.forEach((row) => {
row.classList.remove("custom-gh-nav-settings-row-dragging");
row.classList.remove("custom-gh-nav-settings-row-drag-over");
});
}
function renderPanelRows(listEl, state) {
listEl.replaceChildren();
state.order.forEach((key) => {
const row = document.createElement("div");
row.className = "custom-gh-nav-settings-row";
row.draggable = true;
row.title = t("dragRowTitle");
row.dataset.rowKey = key;
const left = document.createElement("label");
left.className = "custom-gh-nav-settings-row-left";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = state.enabledSet.has(key);
checkbox.addEventListener("change", () => {
if (checkbox.checked) {
state.enabledSet.add(key);
} else {
state.enabledSet.delete(key);
}
});
const text = document.createElement("span");
text.textContent = `${getDisplayNameByKey(key)} (${key})`;
left.appendChild(checkbox);
left.appendChild(text);
const actions = document.createElement("div");
actions.className = "custom-gh-nav-settings-row-actions";
const dragHandle = document.createElement("span");
dragHandle.className = "custom-gh-nav-settings-drag-handle";
dragHandle.textContent = "≡";
dragHandle.title = t("dragHandleTitle");
dragHandle.setAttribute("aria-hidden", "true");
row.addEventListener("dragstart", (event) => {
row.classList.add("custom-gh-nav-settings-row-dragging");
listEl.dataset.dragKey = key;
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", key);
}
});
row.addEventListener("dragend", () => {
delete listEl.dataset.dragKey;
clearDragClasses(listEl);
});
row.addEventListener("dragover", (event) => {
event.preventDefault();
row.classList.add("custom-gh-nav-settings-row-drag-over");
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "move";
}
});
row.addEventListener("dragleave", () => {
row.classList.remove("custom-gh-nav-settings-row-drag-over");
});
row.addEventListener("drop", (event) => {
event.preventDefault();
row.classList.remove("custom-gh-nav-settings-row-drag-over");
const draggedKey = event.dataTransfer && event.dataTransfer.getData("text/plain") || listEl.dataset.dragKey || "";
if (!draggedKey || draggedKey === key) return;
const rect = row.getBoundingClientRect();
const placeAfter = event.clientY > rect.top + rect.height / 2;
if (reorderKeys(state, draggedKey, key, placeAfter)) {
renderPanelRows(listEl, state);
}
});
actions.appendChild(dragHandle);
row.appendChild(left);
row.appendChild(actions);
listEl.appendChild(row);
});
}
function openConfigPanel() {
closeConfigPanel();
ensureStyles();
const state = createPanelState(loadConfig());
const overlay = document.createElement("div");
overlay.id = SETTINGS_OVERLAY_ID;
const panel = document.createElement("div");
panel.id = SETTINGS_PANEL_ID;
const title = document.createElement("h3");
title.className = "custom-gh-nav-settings-title";
title.textContent = t("panelTitle");
const desc = document.createElement("p");
desc.className = "custom-gh-nav-settings-desc";
desc.textContent = t("panelDesc");
const list = document.createElement("div");
list.className = "custom-gh-nav-settings-list";
renderPanelRows(list, state);
const message = document.createElement("div");
message.id = SETTINGS_MESSAGE_ID;
message.className = "custom-gh-nav-settings-message";
message.setAttribute("role", "status");
message.setAttribute("aria-live", "polite");
const footer = document.createElement("div");
footer.className = "custom-gh-nav-settings-footer";
const resetBtn = document.createElement("button");
resetBtn.type = "button";
resetBtn.className = "custom-gh-nav-settings-btn";
resetBtn.textContent = t("resetDefault");
resetBtn.addEventListener("click", () => {
state.order = DEFAULT_LINK_KEYS.slice();
state.enabledSet = new Set(DEFAULT_LINK_KEYS);
renderPanelRows(list, state);
message.textContent = t("restoredPendingSave");
});
const cancelBtn = document.createElement("button");
cancelBtn.type = "button";
cancelBtn.className = "custom-gh-nav-settings-btn";
cancelBtn.textContent = t("cancel");
cancelBtn.addEventListener("click", closeConfigPanel);
const saveBtn = document.createElement("button");
saveBtn.type = "button";
saveBtn.className = "custom-gh-nav-settings-btn custom-gh-nav-settings-btn-primary";
saveBtn.textContent = t("saveAndRefresh");
saveBtn.addEventListener("click", () => {
const enabledKeys = state.order.filter((key) => state.enabledSet.has(key));
if (!enabledKeys.length) {
message.textContent = t("atLeastOneLink");
return;
}
saveConfig({
enabledKeys,
orderKeys: state.order.slice()
});
closeConfigPanel();
location.reload();
});
footer.appendChild(resetBtn);
footer.appendChild(cancelBtn);
footer.appendChild(saveBtn);
panel.appendChild(title);
panel.appendChild(desc);
panel.appendChild(list);
panel.appendChild(message);
panel.appendChild(footer);
overlay.appendChild(panel);
overlay.addEventListener("click", (event) => {
if (event.target === overlay) closeConfigPanel();
});
settingsEscHandler = (event) => {
if (event.key === "Escape") closeConfigPanel();
};
document.addEventListener("keydown", settingsEscHandler);
document.body.appendChild(overlay);
}
function registerConfigMenu() {
if (typeof GM_registerMenuCommand !== "function") return;
GM_registerMenuCommand(t("menuOpenSettings"), openConfigPanel);
GM_registerMenuCommand(t("menuResetSettings"), () => {
const shouldReset = confirm(t("resetConfirm"));
if (!shouldReset) return;
localStorage.removeItem(CONFIG_STORAGE_KEY);
closeConfigPanel();
location.reload();
});
GM_registerMenuCommand(t("menuLangZh"), () => {
setUiLangPreference("zh");
closeConfigPanel();
location.reload();
});
GM_registerMenuCommand(t("menuLangEn"), () => {
setUiLangPreference("en");
closeConfigPanel();
location.reload();
});
GM_registerMenuCommand(t("menuLangAuto"), () => {
setUiLangPreference("auto");
closeConfigPanel();
location.reload();
});
}
// src/main.js
console.info(`[Better GitHub Navigation] loaded v${SCRIPT_VERSION}`);
window.__betterGithubNavVersion = SCRIPT_VERSION;
window.__openBetterGithubNavSettings = openConfigPanel;
registerConfigMenu();
ensureStyles();
addCustomButtons();
document.addEventListener("turbo:load", addCustomButtons);
document.addEventListener("pjax:end", addCustomButtons);
var observer = new MutationObserver(() => {
if (!document.querySelector('[id^="custom-gh-btn-"]') && document.querySelector("header")) {
addCustomButtons();
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();