diff --git a/.gitignore b/.gitignore index aac90f073c..1a47c83859 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ icons/folder.svg icons/folder-open.svg icons/folder-root.svg icons/folder-root-open.svg +icons/*.clone.svg icons/clones src/scripts/preview/*.html diff --git a/src/icons/generator/clones/clonesGenerator.ts b/src/icons/generator/clones/clonesGenerator.ts index 6e81b8e27f..7f309642c1 100644 --- a/src/icons/generator/clones/clonesGenerator.ts +++ b/src/icons/generator/clones/clonesGenerator.ts @@ -1,6 +1,9 @@ import { + CustomClone, FileIconClone, + FileIcons, FolderIconClone, + FolderTheme, IconConfiguration, IconJsonOptions, } from '../../../models'; @@ -14,10 +17,11 @@ import merge from 'lodash.merge'; import { getFileConfigHash } from '../../../helpers/fileConfig'; import { cloneIcon, createCloneConfig } from './utils/cloning'; import { writeFileSync } from 'fs'; +import { cloneIconExtension, clonesFolder } from '../constants'; /** * Creates custom icons by cloning already existing icons and changing - * their colors, allowing users create their own variations. + * their colors, based on the user's provided configurations. */ export function customClonesIcons( config: IconConfiguration, @@ -43,6 +47,62 @@ export function customClonesIcons( return clonedIconsConfig; } +/** + * Creates custom icons by cloning already existing icons and changing + * their colors, based on the configurations provided by the extension. + * (this is meant to be called at build time) + */ +export function generateConfiguredClones( + iconsList: FolderTheme[] | FileIcons, + config: IconConfiguration +) { + let iconsToClone: CustomClone[] = []; + + if (Array.isArray(iconsList)) { + iconsToClone = iconsList.reduce((acc, theme) => { + const icons = theme.icons?.filter((icon) => icon.clone) ?? []; + return acc.concat( + icons.map((icon) => ({ + folderNames: icon.folderNames, + name: icon.name, + ...icon.clone!, + })) + ); + }, [] as FolderIconClone[]); + } else { + const icons = iconsList.icons?.filter((icon) => icon.clone) ?? []; + iconsToClone = icons.map( + (icon) => + ({ + fileExtensions: icon.fileExtensions, + fileNames: icon.fileNames, + name: icon.name, + ...icon.clone!, + } as FileIconClone) + ); + } + + iconsToClone?.forEach((clone) => { + const clones = getCloneData(clone, config, '', '', cloneIconExtension); + if (!clones) { + return; + } + + clones.forEach((clone) => { + try { + // generates the new icon content (svg) + const content = cloneIcon(clone.base.path, clone.color); + + // write the new .svg file to the disk + writeFileSync(clone.path, content); + } catch (error) { + console.error(error); + return; + } + }); + }); +} + /** Checks if there are any custom clones to be created */ export function hasCustomClones(options: IconJsonOptions): boolean { return ( @@ -58,13 +118,13 @@ export function hasCustomClones(options: IconJsonOptions): boolean { * @param hash current hash being applied to the icons * @returns a partial icon configuration for the new icon */ -export function createIconClone( +function createIconClone( cloneOpts: FolderIconClone | FileIconClone, config: IconConfiguration, hash: string ): IconConfiguration { // get clones to be created - const clones = getCloneData(cloneOpts, config, hash); + const clones = getCloneData(cloneOpts, config, clonesFolder, hash); if (!clones) { return {}; } @@ -74,7 +134,7 @@ export function createIconClone( clones.forEach((clone) => { try { // generates the new icon content (svg) - const content = cloneIcon(clone.base.path, hash, clone.color); + const content = cloneIcon(clone.base.path, clone.color, hash); try { // write the new .svg file to the disk diff --git a/src/icons/generator/clones/utils/clone-data.ts b/src/icons/generator/clones/utils/clone-data.ts index c6fdce390c..9f8dad2606 100644 --- a/src/icons/generator/clones/utils/clone-data.ts +++ b/src/icons/generator/clones/utils/clone-data.ts @@ -43,7 +43,7 @@ function resolvePath(path: string): string { return join(__dirname, String(path)); } else { // executed via script - return join(__dirname, '..', '..', '..', String(path)); + return join(__dirname, '..', '..', '..', '..', String(path)); } } @@ -65,7 +65,9 @@ const isDark = (daa: IconData) => export function getCloneData( cloneOpts: CustomClone, config: IconConfiguration, - hash: string + subFolder: string, + hash: string, + ext?: string ): CloneData[] | undefined { const baseIcon = isFolder(cloneOpts) ? getFolderIconBaseData(cloneOpts, config) @@ -74,15 +76,17 @@ export function getCloneData( if (baseIcon) { return baseIcon.map((base) => { const cloneIcon = isFolder(cloneOpts) - ? getFolderIconCloneData(base, cloneOpts, hash) - : getFileIconCloneData(base, cloneOpts, hash); + ? getFolderIconCloneData(base, cloneOpts, hash, subFolder, ext) + : getFileIconCloneData(base, cloneOpts, hash, subFolder, ext); return { name: getIconName(cloneOpts.name, base), color: isDark(base) ? cloneOpts.color : cloneOpts.lightColor ?? cloneOpts.color, - inConfigPath: `${iconFolderPath}clones/${basename(cloneIcon.path)}`, + inConfigPath: `${iconFolderPath}${subFolder}${basename( + cloneIcon.path + )}`, base, ...cloneIcon, }; @@ -114,7 +118,7 @@ function getFileIconBaseData( }); light && icons.push({ - type: Type.Folder, + type: Type.File, variant: Variant.Light, path: resolvePath(light), }); @@ -126,10 +130,12 @@ function getFileIconBaseData( function getFileIconCloneData( base: IconData, cloneOpts: FileIconClone, - hash: string + hash: string, + subFolder: string, + ext = '.svg' ): IconData { const name = getIconName(cloneOpts.name, base); - const clonePath = join(dirname(base.path), 'clones', `${name}${hash}.svg`); + const clonePath = join(dirname(base.path), subFolder, `${name}${hash}${ext}`); return { variant: base.variant, @@ -140,12 +146,16 @@ function getFileIconCloneData( /** returns path, type and variant for the base folder icons to be cloned */ function getFolderIconBaseData( - cloneOpts: FolderIconClone, + clone: FolderIconClone, config: IconConfiguration ): IconData[] | undefined { const icons = []; const folderBase = - cloneOpts.base === 'folder' ? 'folder' : `folder-${cloneOpts.base}`; + clone.base === 'folder' + ? 'folder' + : clone.base.startsWith('folder-') + ? clone.base + : `folder-${clone.base}`; const base = config.iconDefinitions?.[`${folderBase}`]?.iconPath; const open = @@ -158,20 +168,19 @@ function getFolderIconBaseData( ]?.iconPath; if (base && open) { - base && - icons.push({ - type: Type.Folder, - variant: Variant.Base, - path: resolvePath(base), - }); - open && - icons.push({ - type: Type.Folder, - variant: Variant.Open, - path: resolvePath(open), - }); + icons.push({ + type: Type.Folder, + variant: Variant.Base, + path: resolvePath(base), + }); - if (cloneOpts.lightColor && (!light || !lightOpen)) { + icons.push({ + type: Type.Folder, + variant: Variant.Open, + path: resolvePath(open), + }); + + if (clone.lightColor && (!light || !lightOpen)) { // the original icon does not have a light version, so we re-use the base icons light = base; lightOpen = open; @@ -201,10 +210,12 @@ function getFolderIconBaseData( function getFolderIconCloneData( base: IconData, cloneOpts: FolderIconClone, - hash: string + hash: string, + subFolder: string, + ext = '.svg' ): IconData { const name = getIconName(cloneOpts.name, base); - const path = join(dirname(base.path), 'clones', `${name}${hash}.svg`); + const path = join(dirname(base.path), subFolder, `${name}${hash}${ext}`); return { type: base.type, variant: base.variant, path }; } @@ -226,26 +237,32 @@ export function clearCloneFolder(keep: boolean = true): void { function getIconName(baseName: string, data: IconData): string { let prefix = ''; - let sufix = ''; + let suffix = ''; if (data.type === Type.Folder) { - prefix = baseName === 'folder' ? '' : `folder-`; + prefix = + baseName === 'folder' + ? '' + : baseName.startsWith('folder-') + ? '' + : 'folder-'; + switch (data.variant) { case Variant.Base: break; case Variant.Open: - sufix = openedFolder; + suffix = openedFolder; break; case Variant.Light: - sufix = lightColorFileEnding; + suffix = lightColorFileEnding; break; case Variant.LightOpen: - sufix = `${openedFolder}${lightColorFileEnding}`; + suffix = `${openedFolder}${lightColorFileEnding}`; break; } } else { - sufix = data.variant === Variant.Light ? lightColorFileEnding : ''; + suffix = data.variant === Variant.Light ? lightColorFileEnding : ''; } - return `${prefix}${baseName}${sufix}`; + return `${prefix}${baseName}${suffix}`; } diff --git a/src/icons/generator/clones/utils/cloning.ts b/src/icons/generator/clones/utils/cloning.ts index 6ca355fc32..94589905fe 100644 --- a/src/icons/generator/clones/utils/cloning.ts +++ b/src/icons/generator/clones/utils/cloning.ts @@ -28,7 +28,7 @@ export function readIcon(path: string, hash: string): string { } /** Clones an icon and changes its colors according to the clone options. */ -export function cloneIcon(path: string, hash: string, color: string): string { +export function cloneIcon(path: string, color: string, hash = ''): string { const baseContent = readIcon(path, hash); const svg = parseSync(baseContent); const replacements = replacementMap(color, getColorList(svg)); diff --git a/src/icons/generator/constants.ts b/src/icons/generator/constants.ts index 01b9568697..fbf1cf3f3e 100644 --- a/src/icons/generator/constants.ts +++ b/src/icons/generator/constants.ts @@ -23,6 +23,16 @@ export const lightColorFileEnding: string = '_light'; */ export const highContrastColorFileEnding: string = '_highContrast'; +/** + * Pattern to match the file icon definition. + */ +export const cloneIconExtension: string = '.clone.svg'; + +/** + * User Defined Clones subfolder + */ +export const clonesFolder: string = 'clones/'; + /** * Pattern to match wildcards for custom file icon mappings. */ diff --git a/src/icons/generator/fileGenerator.ts b/src/icons/generator/fileGenerator.ts index 9311170d6e..2d52688424 100644 --- a/src/icons/generator/fileGenerator.ts +++ b/src/icons/generator/fileGenerator.ts @@ -8,6 +8,7 @@ import { IconJsonOptions, } from '../../models/index'; import { + cloneIconExtension, highContrastColorFileEnding, iconFolderPath, lightColorFileEnding, @@ -38,20 +39,26 @@ export const loadFileIconDefinitions = ( allFileIcons.forEach((icon) => { if (icon.disabled) return; - config = merge({}, config, setIconDefinition(config, icon.name)); + const isClone = icon.clone !== undefined; + config = merge({}, config, setIconDefinition(config, icon.name, isClone)); if (icon.light) { config = merge( {}, config, - setIconDefinition(config, icon.name, lightColorFileEnding) + setIconDefinition(config, icon.name, isClone, lightColorFileEnding) ); } if (icon.highContrast) { config = merge( {}, config, - setIconDefinition(config, icon.name, highContrastColorFileEnding) + setIconDefinition( + config, + icon.name, + isClone, + highContrastColorFileEnding + ) ); } @@ -79,7 +86,7 @@ export const loadFileIconDefinitions = ( config = merge( {}, config, - setIconDefinition(config, fileIcons.defaultIcon.name) + setIconDefinition(config, fileIcons.defaultIcon.name, false) ); config.file = fileIcons.defaultIcon.name; @@ -90,6 +97,7 @@ export const loadFileIconDefinitions = ( setIconDefinition( config, fileIcons.defaultIcon.name, + false, lightColorFileEnding ) ); @@ -105,6 +113,7 @@ export const loadFileIconDefinitions = ( setIconDefinition( config, fileIcons.defaultIcon.name, + false, highContrastColorFileEnding ) ); @@ -187,13 +196,15 @@ const disableIconsByPack = ( const setIconDefinition = ( config: IconConfiguration, iconName: string, + isClone: boolean, appendix: string = '' ) => { const obj: Partial = { iconDefinitions: {} }; + const ext = isClone ? cloneIconExtension : '.svg'; if (config.options) { const fileConfigHash = getFileConfigHash(config.options); obj.iconDefinitions![`${iconName}${appendix}`] = { - iconPath: `${iconFolderPath}${iconName}${appendix}${fileConfigHash}.svg`, + iconPath: `${iconFolderPath}${iconName}${appendix}${fileConfigHash}${ext}`, }; } return obj; diff --git a/src/icons/generator/folderGenerator.ts b/src/icons/generator/folderGenerator.ts index c661a31b32..f5efe65efb 100644 --- a/src/icons/generator/folderGenerator.ts +++ b/src/icons/generator/folderGenerator.ts @@ -11,6 +11,7 @@ import { IconJsonOptions, } from '../../models/index'; import { + cloneIconExtension, highContrastColorFileEnding, iconFolderPath, lightColorFileEnding, @@ -173,20 +174,27 @@ const setIconDefinitions = ( config: IconConfiguration, icon: FolderIcon | DefaultIcon ) => { + const isClone = (icon as FolderIcon).clone !== undefined; config = merge({}, config); - config = createIconDefinitions(config, icon.name); + + config = createIconDefinitions(config, icon.name, '', isClone); if (icon.light) { config = merge( {}, config, - createIconDefinitions(config, icon.name, lightColorFileEnding) + createIconDefinitions(config, icon.name, lightColorFileEnding, isClone) ); } if (icon.highContrast) { config = merge( {}, config, - createIconDefinitions(config, icon.name, highContrastColorFileEnding) + createIconDefinitions( + config, + icon.name, + highContrastColorFileEnding, + isClone + ) ); } return config; @@ -195,17 +203,20 @@ const setIconDefinitions = ( const createIconDefinitions = ( config: IconConfiguration, iconName: string, - appendix: string = '' + appendix: string = '', + isClone = false ) => { config = merge({}, config); const fileConfigHash = getFileConfigHash(config.options ?? {}); const configIconDefinitions = config.iconDefinitions; + const ext = isClone ? cloneIconExtension : '.svg'; + if (configIconDefinitions) { configIconDefinitions[iconName + appendix] = { - iconPath: `${iconFolderPath}${iconName}${appendix}${fileConfigHash}.svg`, + iconPath: `${iconFolderPath}${iconName}${appendix}${fileConfigHash}${ext}`, }; configIconDefinitions[`${iconName}${openedFolder}${appendix}`] = { - iconPath: `${iconFolderPath}${iconName}${openedFolder}${appendix}${fileConfigHash}.svg`, + iconPath: `${iconFolderPath}${iconName}${openedFolder}${appendix}${fileConfigHash}${ext}`, }; } return config; diff --git a/src/icons/generator/jsonGenerator.ts b/src/icons/generator/jsonGenerator.ts index 7e7c5aceae..1e8645c476 100644 --- a/src/icons/generator/jsonGenerator.ts +++ b/src/icons/generator/jsonGenerator.ts @@ -26,7 +26,10 @@ import { validateOpacityValue, validateSaturationValue, } from './index'; -import { customClonesIcons } from './clones/clonesGenerator'; +import { + customClonesIcons, + generateConfiguredClones, +} from './clones/clonesGenerator'; /** * Generate the complete icon configuration object that can be written as JSON file. @@ -133,8 +136,16 @@ export const createIconFile = ( } renameIconFiles(iconJsonPath, options); - // generate custom cloned icons after opacity and saturation have - // been set so that those changes are also applied to the clones + // create configured icon clones at build time + if (!updatedConfigs) { + console.log('Generating icon clones...'); + generateConfiguredClones(folderIcons, json); + generateConfiguredClones(fileIcons, json); + } + + // generate custom cloned icons set by the user via vscode options + // after opacity and saturation have been set so that those changes + // are also applied to the user defined clones json = merge({}, json, customClonesIcons(json, options)); } catch (error) { throw new Error('Failed to update icons: ' + error); @@ -198,7 +209,10 @@ const renameIconFiles = (iconJsonPath: string, options: IconJsonOptions) => { // append file config to file name const newFilePath = join( iconPath, - f.replace(/(^[^\.~]+)(.*)\.svg/, `$1${fileConfigHash}.svg`) + f.replace( + /(^[^\.~]+).*?(\.clone\.svg|\.svg)/, + `$1${fileConfigHash}$2` + ) ); // if generated files are already in place, do not overwrite them diff --git a/src/models/icons/cloneOptions.ts b/src/models/icons/cloneOptions.ts new file mode 100644 index 0000000000..7d1d1e737c --- /dev/null +++ b/src/models/icons/cloneOptions.ts @@ -0,0 +1,5 @@ +export interface CloneOptions { + base: string; + color: string; + lightColor?: string; +} diff --git a/src/models/icons/files/fileIcon.ts b/src/models/icons/files/fileIcon.ts index 617fe48ac0..55b6278157 100644 --- a/src/models/icons/files/fileIcon.ts +++ b/src/models/icons/files/fileIcon.ts @@ -1,4 +1,5 @@ import { RequireAtLeastOne } from '../../../helpers/types'; +import { CloneOptions } from '../cloneOptions'; import { IconPack } from '../index'; interface BasicFileIcon { @@ -38,6 +39,11 @@ interface BasicFileIcon { * Defines a pack to which this icon belongs. A pack can be toggled and all icons inside this pack can be enabled or disabled together. */ enabledFor?: IconPack[]; + + /** + * Options for generating an icon based on another icon. + */ + clone?: CloneOptions; } /** diff --git a/src/models/icons/folders/folderIcon.ts b/src/models/icons/folders/folderIcon.ts index 386d3a7f64..bc75a7852d 100644 --- a/src/models/icons/folders/folderIcon.ts +++ b/src/models/icons/folders/folderIcon.ts @@ -1,3 +1,4 @@ +import { CloneOptions } from '../cloneOptions'; import { IconPack } from '../index'; export interface FolderIcon { @@ -31,4 +32,9 @@ export interface FolderIcon { * Defines a pack to which this icon belongs. A pack can be toggled and all icons inside this pack can be enabled or disabled together. */ enabledFor?: IconPack[]; + + /** + * Options for generating an icon based on another icon. + */ + clone?: CloneOptions; }