mirror of
https://github.com/catppuccin/web-file-explorer-icons.git
synced 2026-01-09 05:50:49 +08:00
feat!: rewrite site selectors (#176)
This commit is contained in:
parent
39e75715a2
commit
55434c068d
@ -1,95 +1,5 @@
|
||||
const GITHUB_SELECTORS = {
|
||||
row: [
|
||||
'.js-navigation-container[role=grid] > .js-navigation-item',
|
||||
// Old commit details and pull request file tree.
|
||||
'file-tree .ActionList-content',
|
||||
'a.tree-browser-result',
|
||||
// For the inner repository file sidepanel. Extra specificity to avoid matching icons on the new commit details page, which uses the same component.
|
||||
'ul[aria-label="Files"] .PRIVATE_TreeView-item-content',
|
||||
'.react-directory-filename-column',
|
||||
'[aria-label="Parent directory"]',
|
||||
],
|
||||
filename: [
|
||||
'div[role="rowheader"] > span',
|
||||
'.ActionList-item-label',
|
||||
'a.tree-browser-result > marked-text',
|
||||
'.PRIVATE_TreeView-item-content > .PRIVATE_TreeView-item-content-text',
|
||||
'.react-directory-filename-column .react-directory-filename-cell a',
|
||||
'[aria-label="Parent directory"] div',
|
||||
],
|
||||
icon: [
|
||||
'.octicon-file',
|
||||
'.octicon-file-directory-fill',
|
||||
'.octicon-file-directory-open-fill',
|
||||
'.octicon-file-submodule',
|
||||
'.react-directory-filename-column > svg',
|
||||
'[aria-label="Parent directory"] svg',
|
||||
'.PRIVATE_TreeView-item-visual > svg',
|
||||
],
|
||||
};
|
||||
|
||||
const GITLAB_SELECTORS = {
|
||||
row: [
|
||||
'.tree-table .tree-item',
|
||||
'.file-header-content',
|
||||
'.diff-tree-list .file-row',
|
||||
],
|
||||
filename: [
|
||||
'.tree-item-file-name .tree-item-link span:last-of-type',
|
||||
'.file-title-name',
|
||||
'span.gl-truncate-component',
|
||||
],
|
||||
icon: [
|
||||
'.folder-icon',
|
||||
'.file-icon',
|
||||
'span svg:has(use[href^="/assets/file_icons/"])',
|
||||
],
|
||||
};
|
||||
|
||||
const FORGEJO_SELECTORS = {
|
||||
row: [
|
||||
'#repo-files-table .entry',
|
||||
'#diff-file-tree .item-file',
|
||||
'#diff-file-tree .item-directory',
|
||||
],
|
||||
filename: ['.name a.muted', 'span.gt-ellipsis'],
|
||||
icon: ['.octicon-file-directory-fill', '.octicon-file'],
|
||||
};
|
||||
|
||||
const GITEA_SELECTORS = {
|
||||
row: [
|
||||
'#repo-files-table .repo-file-item',
|
||||
'#diff-file-tree .item-file',
|
||||
'#diff-file-tree .item-directory',
|
||||
],
|
||||
filename: ['.name a.muted', 'span.gt-ellipsis'],
|
||||
icon: ['.octicon-file-directory-fill', '.octicon-file'],
|
||||
};
|
||||
|
||||
function mergeSelectors(key: keyof typeof GITHUB_SELECTORS): string {
|
||||
return [
|
||||
...GITHUB_SELECTORS[key],
|
||||
...GITLAB_SELECTORS[key],
|
||||
...FORGEJO_SELECTORS[key],
|
||||
...GITEA_SELECTORS[key],
|
||||
].join(',');
|
||||
}
|
||||
|
||||
export const SELECTORS = {
|
||||
row: mergeSelectors('row'),
|
||||
filename: mergeSelectors('filename'),
|
||||
icon: mergeSelectors('icon'),
|
||||
};
|
||||
|
||||
export const icons = {
|
||||
x: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path></svg>',
|
||||
};
|
||||
|
||||
export const ATTRIBUTE_PREFIX = 'data-catppuccin-file-explorer-icons';
|
||||
|
||||
export const MATCHES = [
|
||||
'*://github.com/*',
|
||||
'*://gitlab.com/*',
|
||||
'*://codeberg.org/*',
|
||||
'*://gitea.com/*',
|
||||
];
|
||||
|
||||
@ -2,24 +2,47 @@ import { defineContentScript } from 'wxt/sandbox';
|
||||
|
||||
import { observe } from 'selector-observer';
|
||||
|
||||
import { MATCHES, SELECTORS } from '@/constants';
|
||||
import { type ReplacementSelectorSet, matches, sites } from '@/sites';
|
||||
import { flavor } from '@/storage';
|
||||
import { createStylesElement } from '@/utils';
|
||||
import { injectStyles, replaceIconInRow } from './lib';
|
||||
|
||||
export default defineContentScript({
|
||||
// Make sure `matches` URLs are updated in wxt.config.ts as well.
|
||||
matches: MATCHES,
|
||||
matches: matches,
|
||||
runAt: 'document_start',
|
||||
|
||||
main() {
|
||||
// Monitor DOM elements that match a CSS selector.
|
||||
observe(SELECTORS.row, {
|
||||
async add(row) {
|
||||
await replaceIconInRow(row as HTMLElement);
|
||||
},
|
||||
});
|
||||
const stylesEl = createStylesElement();
|
||||
|
||||
flavor.watch(injectStyles);
|
||||
injectStyles();
|
||||
for (const site of sites) {
|
||||
if (site.domains.includes(window.location.hostname)) {
|
||||
runReplacements(site.replacements, stylesEl);
|
||||
// Assume URLs only have one matching site implementation. Can change this in the future.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* No matching domain. */
|
||||
const replacements = sites.flatMap((site) => site.replacements);
|
||||
runReplacements(replacements, stylesEl);
|
||||
},
|
||||
});
|
||||
|
||||
function runReplacements(
|
||||
replacements: Array<ReplacementSelectorSet>,
|
||||
stylesEl: Element,
|
||||
) {
|
||||
// Monitor DOM elements that match a CSS selector.
|
||||
for (const replacement of replacements) {
|
||||
observe(replacement.row, {
|
||||
async add(rowEl: HTMLElement) {
|
||||
await replaceIconInRow(rowEl, replacement);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const rawStyles = replacements.map(({ styles }) => styles || '').join('\n');
|
||||
flavor.watch(() => injectStyles(stylesEl, rawStyles));
|
||||
injectStyles(stylesEl, rawStyles);
|
||||
}
|
||||
|
||||
@ -1,40 +1,29 @@
|
||||
import type { IconName } from '@/types';
|
||||
|
||||
import { getAssociations } from '@/associations';
|
||||
import { ATTRIBUTE_PREFIX, SELECTORS } from '@/constants';
|
||||
import { flavor, monochrome, specificFolders } from '@/storage';
|
||||
import { createStylesElement } from '@/utils';
|
||||
|
||||
import { flavors } from '@catppuccin/palette';
|
||||
|
||||
import { ATTRIBUTE_PREFIX } from '@/constants';
|
||||
import icons from '@/icons.json';
|
||||
import type { ReplacementSelectorSet } from '@/sites';
|
||||
|
||||
export async function injectStyles() {
|
||||
const styles = createStylesElement();
|
||||
|
||||
styles.textContent = /* css */ `
|
||||
:root {
|
||||
${flavors[await flavor.getValue()].colorEntries
|
||||
export async function injectStyles(stylesEl: Element, siteStyles: string) {
|
||||
stylesEl.textContent =
|
||||
/* css */ `
|
||||
:root {
|
||||
${flavors[await flavor.getValue()].colorEntries
|
||||
.map(([name, { hex }]) => `--ctp-${name}: ${hex};`)
|
||||
.join('\n ')}
|
||||
}
|
||||
|
||||
ul[aria-label="Files"] .PRIVATE_TreeView-directory-icon svg {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
svg[${ATTRIBUTE_PREFIX}-iconname$='_open']:has(~ svg.octicon-file-directory-open-fill:not([data-catppuccin-file-explorer-icons])),
|
||||
svg:not([${ATTRIBUTE_PREFIX}-iconname$='_open']):has(~ svg.octicon-file-directory-fill:not([data-catppuccin-file-explorer-icons])),
|
||||
svg[${ATTRIBUTE_PREFIX}]:has(+ .octicon-file) {
|
||||
display: inline-block !important;
|
||||
}
|
||||
`.trim();
|
||||
}
|
||||
`.trim() + siteStyles;
|
||||
}
|
||||
|
||||
async function createIconElement(
|
||||
iconName: IconName,
|
||||
fileName: string,
|
||||
originalIcon: HTMLElement,
|
||||
originalIconEl: HTMLElement,
|
||||
): Promise<SVGSVGElement> {
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
if (await monochrome.getValue()) {
|
||||
@ -49,11 +38,11 @@ async function createIconElement(
|
||||
svg.setAttribute(`${ATTRIBUTE_PREFIX}-iconname`, iconName);
|
||||
svg.setAttribute(`${ATTRIBUTE_PREFIX}-filename`, fileName);
|
||||
|
||||
for (const attribute of originalIcon.getAttributeNames()) {
|
||||
for (const attribute of originalIconEl.getAttributeNames()) {
|
||||
if (!attribute.startsWith(ATTRIBUTE_PREFIX)) {
|
||||
svg.setAttribute(
|
||||
attribute,
|
||||
originalIcon.getAttribute(attribute) as string,
|
||||
originalIconEl.getAttribute(attribute) as string,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -61,102 +50,11 @@ async function createIconElement(
|
||||
return svg;
|
||||
}
|
||||
|
||||
export async function replaceIconInRow(row: HTMLElement) {
|
||||
const icon = row.querySelector(SELECTORS.icon) as HTMLElement;
|
||||
if (icon && !icon?.hasAttribute(ATTRIBUTE_PREFIX))
|
||||
await replaceIcon(icon, row);
|
||||
}
|
||||
|
||||
export async function replaceIcon(icon: HTMLElement, row: HTMLElement) {
|
||||
const fileNameEl = row.querySelector(SELECTORS.filename) as HTMLElement;
|
||||
if (!fileNameEl) return;
|
||||
const fileName = fileNameEl.textContent
|
||||
?.split('/')
|
||||
.at(0)
|
||||
.trim()
|
||||
/* Remove [Unicode LEFT-TO-RIGHT MARK](https://en.wikipedia.org/wiki/Left-to-right_mark) used on GitLab's merge request diff file tree. */
|
||||
.replace(/\u200E/g, '');
|
||||
|
||||
const isDir =
|
||||
(icon.getAttribute('aria-label') === 'Directory' ||
|
||||
icon.getAttribute('class')?.includes('octicon-file-directory-') ||
|
||||
icon.classList.contains('icon-directory') ||
|
||||
icon.classList.contains('folder-icon')) &&
|
||||
!fileNameEl.getAttribute('aria-label')?.includes('(Symlink to file)');
|
||||
const isSubmodule =
|
||||
icon.classList.contains('octicon-file-submodule') ||
|
||||
fileNameEl.getAttribute('aria-label')?.includes('(Submodule)');
|
||||
const isOpen =
|
||||
isDir && icon.classList.contains('octicon-file-directory-open-fill');
|
||||
|
||||
const fileExtensions: Array<string> = [];
|
||||
// Avoid doing an explosive combination of extensions for very long filenames
|
||||
// (most file systems do not allow files > 255 length) with lots of `.` characters
|
||||
// https://github.com/microsoft/vscode/issues/116199
|
||||
if (fileName.length <= 255) {
|
||||
for (let i = 0; i < fileName.length; i += 1) {
|
||||
if (fileName[i] === '.')
|
||||
fileExtensions.push(fileName.toLowerCase().slice(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
const iconName = await findIconMatch(
|
||||
fileName,
|
||||
fileExtensions,
|
||||
isDir,
|
||||
isSubmodule,
|
||||
);
|
||||
|
||||
await replaceElementWithIcon(
|
||||
icon,
|
||||
isOpen ? (`${iconName}_open` as IconName) : iconName,
|
||||
fileName,
|
||||
);
|
||||
}
|
||||
|
||||
export async function replaceElementWithIcon(
|
||||
icon: HTMLElement,
|
||||
iconName: IconName,
|
||||
fileName: string,
|
||||
) {
|
||||
const replacement = await createIconElement(iconName, fileName, icon);
|
||||
|
||||
const prevEl = icon.previousElementSibling;
|
||||
if (prevEl?.hasAttribute(ATTRIBUTE_PREFIX)) {
|
||||
replacement.replaceWith(prevEl);
|
||||
}
|
||||
// If the icon to replace is an icon from this extension, replace it with the new icon.
|
||||
else if (icon.hasAttribute(ATTRIBUTE_PREFIX)) {
|
||||
icon.replaceWith(replacement);
|
||||
}
|
||||
// If neither of the above, prepend the new icon in front of the original icon.
|
||||
// If we remove the icon, GitHub code view crashes when you navigate through the
|
||||
// tree view. Instead, we just hide it via `style` attribute (not CSS class).
|
||||
else {
|
||||
icon.style.display = 'none';
|
||||
icon.before(replacement);
|
||||
}
|
||||
|
||||
if (
|
||||
icon.parentElement.classList.contains('PRIVATE_TreeView-directory-icon')
|
||||
) {
|
||||
const companion = await createIconElement(
|
||||
(iconName.includes('_open')
|
||||
? iconName.replace('_open', '')
|
||||
: `${iconName}_open`) as IconName,
|
||||
fileName,
|
||||
icon,
|
||||
);
|
||||
|
||||
replacement.after(companion);
|
||||
}
|
||||
}
|
||||
|
||||
async function findIconMatch(
|
||||
fileName: string,
|
||||
fileExtensions: Array<string>,
|
||||
isDir: boolean,
|
||||
isSubmodule: boolean,
|
||||
isDirectory: boolean,
|
||||
): Promise<IconName> {
|
||||
// Special parent directory folder icon.
|
||||
if (fileName === '..') return '_folder';
|
||||
@ -166,7 +64,7 @@ async function findIconMatch(
|
||||
|
||||
if (useSpecificFolders && isSubmodule) return 'folder_git';
|
||||
|
||||
if (isDir) {
|
||||
if (isDirectory) {
|
||||
if (useSpecificFolders) {
|
||||
if (fileName in associations.folderNames)
|
||||
return associations.folderNames[fileName];
|
||||
@ -191,3 +89,74 @@ async function findIconMatch(
|
||||
|
||||
return '_file';
|
||||
}
|
||||
|
||||
export async function replaceIconInRow(
|
||||
rowEl: HTMLElement,
|
||||
selectors: ReplacementSelectorSet,
|
||||
) {
|
||||
const iconEl = rowEl.querySelector(selectors.icon) as HTMLElement;
|
||||
console.log({ iconEl });
|
||||
// Icon already has extension prefix, not necessary to replace again.
|
||||
if (!iconEl || iconEl?.hasAttribute(ATTRIBUTE_PREFIX)) return;
|
||||
|
||||
const fileNameEl = rowEl.querySelector(selectors.filename) as HTMLElement;
|
||||
if (!fileNameEl) return;
|
||||
const fileName = fileNameEl.textContent
|
||||
?.split('/')
|
||||
.at(0)
|
||||
.trim()
|
||||
/* Remove [Unicode LEFT-TO-RIGHT MARK](https://en.wikipedia.org/wiki/Left-to-right_mark) used on GitLab's merge request diff file tree. */
|
||||
.replace(/\u200E/g, '');
|
||||
|
||||
const fileExtensions: Array<string> = [];
|
||||
// Avoid doing an explosive combination of extensions for very long filenames
|
||||
// (most file systems do not allow files > 255 length) with lots of `.` characters
|
||||
// https://github.com/microsoft/vscode/issues/116199
|
||||
if (fileName.length <= 255) {
|
||||
for (let i = 0; i < fileName.length; i += 1) {
|
||||
if (fileName[i] === '.')
|
||||
fileExtensions.push(fileName.toLowerCase().slice(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
const isDirectory = selectors.isDirectory(rowEl, fileNameEl, iconEl);
|
||||
const isSubmodule = selectors.isSubmodule(rowEl, fileNameEl, iconEl);
|
||||
const isCollapsable = selectors.isCollapsable(rowEl, fileNameEl, iconEl);
|
||||
console.log({ isCollapsable });
|
||||
|
||||
const iconName = await findIconMatch(
|
||||
fileName,
|
||||
fileExtensions,
|
||||
isSubmodule,
|
||||
isDirectory,
|
||||
);
|
||||
|
||||
const replacementEl = await createIconElement(iconName, fileName, iconEl);
|
||||
|
||||
// Check if element sibling before current element was inserted by extension, replace the old replacement with the new one instead.
|
||||
const prevEl = iconEl.previousElementSibling;
|
||||
if (prevEl?.hasAttribute(ATTRIBUTE_PREFIX)) {
|
||||
replacementEl.replaceWith(prevEl);
|
||||
}
|
||||
// If the current icon element is an icon from this extension, replace it with the new icon.
|
||||
else if (iconEl.hasAttribute(ATTRIBUTE_PREFIX)) {
|
||||
iconEl.replaceWith(replacementEl);
|
||||
}
|
||||
// If neither of the above, prepend the new icon to the original icon element.
|
||||
// If we remove the icon, GitHub code view crashes when you navigate through the
|
||||
// tree view. Instead, we just hide it via `style` attribute (not CSS class).
|
||||
else {
|
||||
iconEl.style.display = 'none';
|
||||
iconEl.before(replacementEl);
|
||||
}
|
||||
|
||||
if (isCollapsable) {
|
||||
const companionEl = await createIconElement(
|
||||
`${iconName}_open` as IconName,
|
||||
fileName,
|
||||
iconEl,
|
||||
);
|
||||
|
||||
replacementEl.after(companionEl);
|
||||
}
|
||||
}
|
||||
|
||||
43
src/sites/forgejo.ts
Normal file
43
src/sites/forgejo.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { ReplacementSelectorSet, Site } from '.';
|
||||
import { ATTRIBUTE_PREFIX } from '../constants';
|
||||
|
||||
const mainRepositoryImplementation: ReplacementSelectorSet = {
|
||||
row: '#repo-files-table .entry',
|
||||
filename: '.name a',
|
||||
icon: '.svg',
|
||||
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('octicon-file-directory-fill'),
|
||||
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('octicon-file-submodule'),
|
||||
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
|
||||
};
|
||||
|
||||
/* Both commits and pull requests. */
|
||||
const diffTreeImplementation: ReplacementSelectorSet = {
|
||||
row: '.diff-file-tree-items .item-directory, .diff-file-tree-items .item-file',
|
||||
filename: '.gt-ellipsis',
|
||||
icon: '.octicon-file-directory-fill, .octicon-file',
|
||||
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('octicon-file-directory-fill'),
|
||||
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('octicon-file-submodule'),
|
||||
isCollapsable: (rowEl, fileNameEl, iconEl) =>
|
||||
diffTreeImplementation.isDirectory(rowEl, fileNameEl, iconEl),
|
||||
};
|
||||
diffTreeImplementation.styles = /* css */ `
|
||||
${diffTreeImplementation.row} {
|
||||
svg.octicon-file-directory-fill {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
svg.octicon-chevron-down ~ svg[${ATTRIBUTE_PREFIX}-iconname$='_open'],
|
||||
svg.octicon-chevron-right ~ svg[${ATTRIBUTE_PREFIX}]:not([${ATTRIBUTE_PREFIX}-iconname$='_open']) {
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
`.trim();
|
||||
|
||||
export const forgejo: Site = {
|
||||
domains: ['codeberg.org'],
|
||||
replacements: [mainRepositoryImplementation, diffTreeImplementation],
|
||||
};
|
||||
43
src/sites/gitea.ts
Normal file
43
src/sites/gitea.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { ReplacementSelectorSet, Site } from '.';
|
||||
import { ATTRIBUTE_PREFIX } from '../constants';
|
||||
|
||||
const mainRepositoryImplementation: ReplacementSelectorSet = {
|
||||
row: '#repo-files-table .repo-file-item',
|
||||
filename: '.name a',
|
||||
icon: '.svg',
|
||||
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('octicon-file-directory-fill'),
|
||||
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('octicon-file-submodule'),
|
||||
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
|
||||
};
|
||||
|
||||
const diffTreeImplementation: ReplacementSelectorSet = {
|
||||
row: '.diff-file-tree-items .item-directory, .diff-file-tree-items .item-file',
|
||||
filename: '.gt-ellipsis',
|
||||
icon: '.octicon-file-directory-fill, .octicon-file-directory-open-fill, .octicon-file',
|
||||
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('octicon-file-directory-fill') ||
|
||||
iconEl.classList.contains('octicon-file-directory-open-fill'),
|
||||
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('octicon-file-submodule'),
|
||||
isCollapsable: (rowEl, fileNameEl, iconEl) =>
|
||||
diffTreeImplementation.isDirectory(rowEl, fileNameEl, iconEl),
|
||||
};
|
||||
diffTreeImplementation.styles = /* css */ `
|
||||
${diffTreeImplementation.row} {
|
||||
svg.octicon-file-directory-fill, svg.octicon-file-directory-open-fill {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
svg.octicon-chevron-down ~ svg[${ATTRIBUTE_PREFIX}-iconname$='_open'],
|
||||
svg.octicon-chevron-right ~ svg[${ATTRIBUTE_PREFIX}]:not([${ATTRIBUTE_PREFIX}-iconname$='_open']) {
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
`.trim();
|
||||
|
||||
export const gitea: Site = {
|
||||
domains: ['gitea.com'],
|
||||
replacements: [mainRepositoryImplementation, diffTreeImplementation],
|
||||
};
|
||||
87
src/sites/github.ts
Normal file
87
src/sites/github.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import type { ReplacementSelectorSet, Site } from '.';
|
||||
import { ATTRIBUTE_PREFIX } from '../constants';
|
||||
|
||||
// For the inner repository file sidepanel. Extra specificity to avoid matching icons on the new commit details page, which uses the same component.
|
||||
const repositorySideTreeImplementation: ReplacementSelectorSet = {
|
||||
row: 'ul[aria-label="Files"] .PRIVATE_TreeView-item-content',
|
||||
filename:
|
||||
'.PRIVATE_TreeView-item-content > .PRIVATE_TreeView-item-content-text',
|
||||
icon: '.PRIVATE_TreeView-item-visual svg',
|
||||
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.getAttribute('class')?.includes('octicon-file-directory-'),
|
||||
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('octicon-file-submodule'),
|
||||
isCollapsable: (rowEl, fileNameEl, iconEl) =>
|
||||
repositorySideTreeImplementation.isDirectory(rowEl, fileNameEl, iconEl),
|
||||
};
|
||||
repositorySideTreeImplementation.styles = /* css */ `
|
||||
${repositorySideTreeImplementation.row} {
|
||||
/* Hide directory icons by default. */
|
||||
.PRIVATE_TreeView-directory-icon svg {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show relevant extension directory icon depending on open/closed state. */
|
||||
svg[${ATTRIBUTE_PREFIX}-iconname$='_open']:has(~ svg.octicon-file-directory-open-fill:not([data-catppuccin-file-explorer-icons])),
|
||||
svg:not([${ATTRIBUTE_PREFIX}-iconname$='_open']):has(~ svg.octicon-file-directory-fill:not([data-catppuccin-file-explorer-icons])),
|
||||
svg[${ATTRIBUTE_PREFIX}]:has(+ .octicon-file) {
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
`.trim();
|
||||
|
||||
const repositoryMainImplementation: ReplacementSelectorSet = {
|
||||
row: '.react-directory-filename-column',
|
||||
filename: '.react-directory-filename-cell a',
|
||||
icon: 'svg',
|
||||
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('icon-directory'),
|
||||
isSubmodule: (_rowEl, fileNameEl, _iconEl) =>
|
||||
fileNameEl.getAttribute('aria-label')?.includes('(Submodule)'),
|
||||
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
|
||||
};
|
||||
|
||||
const repositoryMainParentDirectoryImplementation: ReplacementSelectorSet = {
|
||||
row: '#folder-row-0 [aria-label="Parent directory"]',
|
||||
filename: 'div',
|
||||
icon: 'svg',
|
||||
isDirectory: (_rowEl, _fileNameEl, _iconEl) => true,
|
||||
isSubmodule: (_rowEl, _fileNameEl, _iconEl) => false,
|
||||
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
|
||||
};
|
||||
|
||||
const pullRequestTreeImplementation: ReplacementSelectorSet = {
|
||||
row: 'file-tree .ActionList-content',
|
||||
filename: '.ActionList-item-label',
|
||||
icon: '.ActionList-item-visual svg',
|
||||
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.getAttribute('aria-label') === 'Directory',
|
||||
isSubmodule: (_rowEl, fileNameEl, _iconEl) =>
|
||||
fileNameEl.getAttribute('aria-label')?.includes('(Submodule)'),
|
||||
isCollapsable: (rowEl, fileNameEl, iconEl) =>
|
||||
pullRequestTreeImplementation.isDirectory(rowEl, fileNameEl, iconEl),
|
||||
};
|
||||
pullRequestTreeImplementation.styles = /* css */ `
|
||||
${pullRequestTreeImplementation.row} {
|
||||
/* Hide directory icons by default. */
|
||||
${pullRequestTreeImplementation.icon}[aria-label="Directory"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show relevant extension directory icon depending on open/closed state. */
|
||||
&[aria-expanded="true"] svg[${ATTRIBUTE_PREFIX}-iconname$='_open'],
|
||||
&[aria-expanded="false"] svg[${ATTRIBUTE_PREFIX}]:not([${ATTRIBUTE_PREFIX}-iconname$='_open']) {
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
`.trim();
|
||||
|
||||
export const github: Site = {
|
||||
domains: ['github.com'],
|
||||
replacements: [
|
||||
repositorySideTreeImplementation,
|
||||
repositoryMainImplementation,
|
||||
pullRequestTreeImplementation,
|
||||
repositoryMainParentDirectoryImplementation,
|
||||
],
|
||||
};
|
||||
67
src/sites/gitlab.ts
Normal file
67
src/sites/gitlab.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import type { ReplacementSelectorSet, Site } from '.';
|
||||
import { ATTRIBUTE_PREFIX } from '../constants';
|
||||
|
||||
const repositoryMainImplementation: ReplacementSelectorSet = {
|
||||
row: '.tree-table .tree-item',
|
||||
filename: '.tree-item-file-name .tree-item-link',
|
||||
icon: 'span svg:has(use)',
|
||||
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('folder-icon'),
|
||||
isSubmodule: (_rowEl, fileNameEl, _iconEl) =>
|
||||
fileNameEl.classList.contains('is-submodule'),
|
||||
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
|
||||
};
|
||||
|
||||
const fileContentsHeaderImplementation: ReplacementSelectorSet = {
|
||||
row: '.project-show-files .file-header-content',
|
||||
filename: '.file-title-name',
|
||||
icon: 'span svg:has(use)',
|
||||
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('folder-icon'),
|
||||
isSubmodule: (_rowEl, _fileNameEl, _iconEl) => false,
|
||||
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
|
||||
};
|
||||
|
||||
const commitDiffFileHeaderImplementation: ReplacementSelectorSet = {
|
||||
row: '.js-diffs-batch .file-header-content',
|
||||
filename: '.file-title-name',
|
||||
icon: 'svg:has(+ .file-title-name)',
|
||||
isDirectory: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.classList.contains('folder-icon'),
|
||||
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.getAttribute('data-testid') === 'folder-git-icon',
|
||||
isCollapsable: (_rowEl, _fileNameEl, _iconEl) => false,
|
||||
};
|
||||
|
||||
const mergeRequestsTreeImplementation: ReplacementSelectorSet = {
|
||||
row: '.diff-tree-list .file-row',
|
||||
filename: '.file-row-name',
|
||||
icon: '.file-row-icon svg',
|
||||
isDirectory: (rowEl, _fileNameEl, _iconEl) =>
|
||||
rowEl.classList.contains('folder'),
|
||||
isSubmodule: (_rowEl, _fileNameEl, iconEl) =>
|
||||
iconEl.firstElementChild.getAttribute('href').endsWith('#folder-git'),
|
||||
isCollapsable: (rowEl, fileNameEl, iconEl) =>
|
||||
mergeRequestsTreeImplementation.isDirectory(rowEl, fileNameEl, iconEl),
|
||||
};
|
||||
mergeRequestsTreeImplementation.styles = /* css */ `
|
||||
/* Hide directory icons by default. */
|
||||
${mergeRequestsTreeImplementation.row}.folder ${mergeRequestsTreeImplementation.icon} {
|
||||
display: none !important;
|
||||
}
|
||||
/* Show relevant extension directory icon depending on open/closed state. */
|
||||
${mergeRequestsTreeImplementation.row}.is-open svg[${ATTRIBUTE_PREFIX}-iconname$='_open'],
|
||||
${mergeRequestsTreeImplementation.row}:not(.is-open) svg[${ATTRIBUTE_PREFIX}]:not([${ATTRIBUTE_PREFIX}-iconname$='_open']) {
|
||||
display: inline-block !important;
|
||||
}
|
||||
`.trim();
|
||||
|
||||
export const gitlab: Site = {
|
||||
domains: ['gitlab.com'],
|
||||
replacements: [
|
||||
repositoryMainImplementation,
|
||||
fileContentsHeaderImplementation,
|
||||
commitDiffFileHeaderImplementation,
|
||||
mergeRequestsTreeImplementation,
|
||||
],
|
||||
};
|
||||
40
src/sites/index.ts
Normal file
40
src/sites/index.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { forgejo } from './forgejo';
|
||||
import { gitea } from './gitea';
|
||||
import { github } from './github';
|
||||
import { gitlab } from './gitlab';
|
||||
|
||||
export type FnWithContext<T> = (
|
||||
rowEl: HTMLElement,
|
||||
fileNameEl: HTMLElement,
|
||||
iconEl: HTMLElement,
|
||||
) => T;
|
||||
|
||||
export type ReplacementSelectorSet = {
|
||||
row: string;
|
||||
filename: string;
|
||||
icon: string;
|
||||
isDirectory: FnWithContext<boolean>;
|
||||
isSubmodule: FnWithContext<boolean>;
|
||||
isCollapsable: FnWithContext<boolean>;
|
||||
styles?: string;
|
||||
};
|
||||
|
||||
export type Site = {
|
||||
domains: Array<string>;
|
||||
replacements: Array<ReplacementSelectorSet>;
|
||||
};
|
||||
|
||||
// import type { Site } from '.';
|
||||
//
|
||||
// export const mySite: SupportedSite = {
|
||||
// urls: ['*://mysite.com/*'],
|
||||
// replacements: [someImplementationFoo, someImplementationBar],
|
||||
// styles: /* css */ `
|
||||
// /* ... */
|
||||
// `.trim(),
|
||||
// };
|
||||
|
||||
export const sites: Array<Site> = [github, gitlab, gitea, forgejo];
|
||||
export const matches: Array<string> = sites
|
||||
.flatMap((site) => site.domains)
|
||||
.map((domain) => `*://${domain}/*`);
|
||||
@ -6,7 +6,7 @@ import { optimize } from 'svgo';
|
||||
|
||||
import jiti from 'jiti';
|
||||
|
||||
import { MATCHES } from './src/constants';
|
||||
import { matches } from './src/sites';
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
@ -77,7 +77,7 @@ export default defineConfig({
|
||||
manifest.content_scripts ??= [];
|
||||
manifest.content_scripts.push({
|
||||
// Make sure `matches` URLs are updated in src/entries/content/index.ts as well.
|
||||
matches: MATCHES,
|
||||
matches: matches,
|
||||
run_at: 'document_start',
|
||||
js: ['content-scripts/content.js'],
|
||||
});
|
||||
@ -90,6 +90,7 @@ export default defineConfig({
|
||||
'https://github.com/catppuccin/catppuccin',
|
||||
'https://gitlab.com/gitlab-org/gitlab',
|
||||
'https://codeberg.org/forgejo/forgejo',
|
||||
'https://gitea.com/gitea/gitea-mirror',
|
||||
'https://gitea.catppuccin.com/catppuccin/catppuccin',
|
||||
],
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user