mirror of
https://github.com/catppuccin/web-file-explorer-icons.git
synced 2026-01-09 05:50:49 +08:00
feat: custom associations
This commit is contained in:
parent
2a0ae82a19
commit
c99c5187ab
@ -15,6 +15,21 @@
|
||||
<option value="mocha">Mocha</option>
|
||||
</select>
|
||||
</section>
|
||||
<section class="associations">
|
||||
<label>Associations</label>
|
||||
<div>
|
||||
<span>File extensions</span>
|
||||
<ul id="associations-file-extensions"></ul>
|
||||
</div>
|
||||
<div>
|
||||
<span>File names</span>
|
||||
<ul id="associations-file-names"></ul>
|
||||
</div>
|
||||
<div>
|
||||
<span>Folder names</span>
|
||||
<ul id="associations-folder-names"></ul>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</html>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import './styles.css';
|
||||
|
||||
import type { Flavor } from '@/lib/types';
|
||||
import type { Associations, Flavor, IconName } from '@/lib/types';
|
||||
|
||||
import { flavor } from '@/lib/storage';
|
||||
import { customAssociations, flavor } from '@/lib/storage';
|
||||
import { icons } from '@/lib/constants';
|
||||
|
||||
async function init() {
|
||||
const flavorEl = document.querySelector('#flavor') as HTMLSelectElement;
|
||||
@ -15,6 +16,121 @@ async function init() {
|
||||
await flavor.setValue(value);
|
||||
document.documentElement.setAttribute('theme', value);
|
||||
});
|
||||
|
||||
const associations = await customAssociations.getValue();
|
||||
|
||||
for (const [key, el] of Object.entries({
|
||||
fileExtensions: document.querySelector(
|
||||
'ul#associations-file-extensions',
|
||||
),
|
||||
fileNames: document.querySelector('ul#associations-file-names'),
|
||||
folderNames: document.querySelector('ul#associations-folder-names'),
|
||||
} as Record<keyof Associations, HTMLUListElement>) as [
|
||||
keyof Associations,
|
||||
HTMLUListElement,
|
||||
][]) {
|
||||
for (const [association, icon] of Object.entries(associations[key])) {
|
||||
const li = document.createElement('li');
|
||||
const inputA = document.createElement('input');
|
||||
const inputB = document.createElement('input');
|
||||
const del = document.createElement('button');
|
||||
|
||||
del.className = 'delete';
|
||||
del.innerHTML = icons['x'];
|
||||
|
||||
inputA.value = association;
|
||||
inputA.setAttribute('required', 'true');
|
||||
|
||||
inputB.value = icon;
|
||||
inputB.setAttribute('required', 'true');
|
||||
|
||||
li.appendChild(inputA);
|
||||
li.appendChild(inputB);
|
||||
li.appendChild(del);
|
||||
el.appendChild(li);
|
||||
|
||||
del.addEventListener('click', async () => {
|
||||
if (inputA.checkValidity()) {
|
||||
delete associations[key][association];
|
||||
await customAssociations.setValue(associations);
|
||||
li.remove();
|
||||
}
|
||||
});
|
||||
inputA.addEventListener('change', async () => {
|
||||
if (inputA.checkValidity()) {
|
||||
delete associations[key][association];
|
||||
associations[key][inputA.value] = icon;
|
||||
await customAssociations.setValue(associations);
|
||||
}
|
||||
});
|
||||
inputB.addEventListener('change', async () => {
|
||||
if (inputB.checkValidity()) {
|
||||
associations[key][association] = inputB.value as IconName;
|
||||
await customAssociations.setValue(associations);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addEmpty() {
|
||||
const li = document.createElement('li');
|
||||
const inputA = document.createElement('input');
|
||||
const inputB = document.createElement('input');
|
||||
const del = document.createElement('button');
|
||||
|
||||
del.className = 'delete';
|
||||
del.innerHTML = icons['x'];
|
||||
|
||||
inputA.setAttribute('required', 'true');
|
||||
inputB.setAttribute('required', 'true');
|
||||
|
||||
li.appendChild(inputA);
|
||||
li.appendChild(inputB);
|
||||
li.appendChild(del);
|
||||
el.appendChild(li);
|
||||
|
||||
del.addEventListener('click', async () => {
|
||||
if (inputA.checkValidity()) {
|
||||
delete associations[key][inputA.value];
|
||||
await customAssociations.setValue(associations);
|
||||
li.remove();
|
||||
}
|
||||
});
|
||||
|
||||
let addedEmpty = false;
|
||||
|
||||
inputA.addEventListener('change', async () => {
|
||||
if (inputA.checkValidity()) {
|
||||
delete associations[key][inputA.value];
|
||||
associations[key][inputA.value] = inputB.value as IconName;
|
||||
await customAssociations.setValue(associations);
|
||||
}
|
||||
if (
|
||||
!addedEmpty &&
|
||||
inputA.checkValidity() &&
|
||||
inputB.checkValidity()
|
||||
) {
|
||||
addEmpty();
|
||||
addedEmpty = true;
|
||||
}
|
||||
});
|
||||
inputB.addEventListener('change', async () => {
|
||||
if (inputB.checkValidity()) {
|
||||
associations[key][inputA.value] = inputB.value as IconName;
|
||||
await customAssociations.setValue(associations);
|
||||
}
|
||||
if (
|
||||
!addedEmpty &&
|
||||
inputA.checkValidity() &&
|
||||
inputB.checkValidity()
|
||||
) {
|
||||
addEmpty();
|
||||
addedEmpty = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
@ -119,7 +119,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
width: 400px;
|
||||
width: 600px;
|
||||
height: max-content;
|
||||
padding: 1rem;
|
||||
|
||||
@ -131,7 +131,8 @@ body {
|
||||
color: var(--ctp-text) !important;
|
||||
}
|
||||
|
||||
select {
|
||||
select,
|
||||
input {
|
||||
background-color: var(--ctp-base);
|
||||
color: var(--ctp-text) !important;
|
||||
border: none;
|
||||
@ -139,10 +140,15 @@ select {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
select:focus-visible {
|
||||
select:focus-visible,
|
||||
input:focus-visible {
|
||||
outline: 3px solid var(--ctp-mauve);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--ctp-subtext0);
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
@ -153,6 +159,52 @@ section {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
section.associations {
|
||||
flex-direction: column;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
section.associations div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
section.associations div span {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
section.associations ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
section.associations ul li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
color: var(--ctp-text);
|
||||
background-color: transparent;
|
||||
appearance: none;
|
||||
border: none;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.delete {
|
||||
color: var(--ctp-red);
|
||||
}
|
||||
|
||||
button.delete svg {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
|
||||
24
src/lib/associations.ts
Normal file
24
src/lib/associations.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { Associations } from './types';
|
||||
|
||||
import { associations as json } from '@/vscode-icons.json';
|
||||
import { customAssociations } from './storage';
|
||||
|
||||
export async function getAssociations(): Promise<Associations> {
|
||||
const custom = await customAssociations.getValue();
|
||||
|
||||
return {
|
||||
languageIds: json.languageIds as Associations['languageIds'],
|
||||
fileExtensions: {
|
||||
...json.fileExtensions,
|
||||
...custom.fileExtensions,
|
||||
} as Associations['languageIds'],
|
||||
fileNames: {
|
||||
...json.fileNames,
|
||||
...custom.fileNames,
|
||||
} as Associations['fileNames'],
|
||||
folderNames: {
|
||||
...json.folderNames,
|
||||
...custom.folderNames,
|
||||
} as Associations['folderNames'],
|
||||
};
|
||||
}
|
||||
@ -18,3 +18,7 @@ export const selectors = {
|
||||
.react-directory-filename-column > svg,
|
||||
[aria-label="Parent directory"] svg`,
|
||||
};
|
||||
|
||||
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>',
|
||||
};
|
||||
|
||||
@ -2,8 +2,8 @@ import type { PublicPath } from 'wxt/browser';
|
||||
import type { IconName } from '@/lib/types';
|
||||
|
||||
import { selectors } from '@/lib/constants';
|
||||
import { associations } from '@/vscode-icons.json';
|
||||
import { flavor } from '@/lib/storage';
|
||||
import { getAssociations } from './associations';
|
||||
|
||||
export async function replaceIconInRow(row: HTMLElement) {
|
||||
const icon = row.querySelector(selectors.icon) as HTMLElement;
|
||||
@ -39,7 +39,7 @@ export async function replaceIcon(icon: HTMLElement, row: HTMLElement) {
|
||||
}
|
||||
}
|
||||
|
||||
const iconName = findIconMatch(
|
||||
const iconName = await findIconMatch(
|
||||
fileName,
|
||||
fileExtensions,
|
||||
isDir,
|
||||
@ -114,17 +114,19 @@ export async function replaceElementWithIcon(
|
||||
}
|
||||
}
|
||||
|
||||
function findIconMatch(
|
||||
async function findIconMatch(
|
||||
fileName: string,
|
||||
fileExtensions: string[],
|
||||
isDir: boolean,
|
||||
isSubmodule: boolean,
|
||||
): IconName {
|
||||
): Promise<IconName> {
|
||||
// Special parent directory folder icon:
|
||||
if (fileName === '..') return '_folder';
|
||||
|
||||
if (isSubmodule) return 'folder_git';
|
||||
|
||||
const associations = await getAssociations();
|
||||
|
||||
if (isDir) {
|
||||
if (fileName in associations.folderNames)
|
||||
return associations.folderNames[fileName];
|
||||
|
||||
@ -1,5 +1,17 @@
|
||||
import type { Flavor } from './types';
|
||||
import type { Associations, Flavor } from './types';
|
||||
|
||||
export const flavor = storage.defineItem<Flavor>('local:flavor', {
|
||||
defaultValue: 'mocha',
|
||||
});
|
||||
|
||||
export const customAssociations = storage.defineItem<Associations>(
|
||||
'local:associations',
|
||||
{
|
||||
defaultValue: {
|
||||
languageIds: {},
|
||||
fileExtensions: {},
|
||||
fileNames: {},
|
||||
folderNames: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@ -5,3 +5,10 @@ export type Flavor = 'latte' | 'frappe' | 'macchiato' | 'mocha';
|
||||
type RemoveIconPrefixAndSuffix<T extends string> =
|
||||
T extends `/${Flavor}/${infer Rest}.svg` ? Rest : never;
|
||||
export type IconName = RemoveIconPrefixAndSuffix<PublicPath>;
|
||||
|
||||
export type Associations = {
|
||||
languageIds: Record<string, IconName>;
|
||||
fileExtensions: Record<string, IconName>;
|
||||
fileNames: Record<string, IconName>;
|
||||
folderNames: Record<string, IconName>;
|
||||
};
|
||||
|
||||
@ -36,7 +36,7 @@ export default defineConfig({
|
||||
);
|
||||
await hfs.deleteAll(join(PUBLIC_DIR, 'css-variables'));
|
||||
|
||||
// Write assocations/config file:
|
||||
// Write associations/config file:
|
||||
await hfs.write(
|
||||
join(__dirname, './src/vscode-icons.json'),
|
||||
JSON.stringify(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user