Skip to content

Commit

Permalink
feat: add color converter extension (oliverschwendener#1146)
Browse files Browse the repository at this point in the history
* feat: added color converter extension

* feat(ColorConverter): added CMYK option

* Removed cmyk again

* Simplified code

* Added tests

* Use correct terms

* Fix stuff
  • Loading branch information
oliverschwendener authored Jul 25, 2024
1 parent 0af5972 commit d319c16
Show file tree
Hide file tree
Showing 16 changed files with 412 additions and 2 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 26 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.11",
"@types/color": "^3.0.6",
"@types/node": "^20.8.6",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
Expand All @@ -55,6 +56,7 @@
"@fluentui/react-components": "^9.54.4",
"@fluentui/react-icons": "^2.0.249",
"better-sqlite3": "^11.1.2",
"color": "^4.2.3",
"fuse.js": "^7.0.0",
"fuzzysort": "^3.0.1",
"i18next": "^23.8.1",
Expand All @@ -66,5 +68,6 @@
"react-router": "^6.21.3",
"react-router-dom": "^6.21.3",
"sharp": "^0.33.3"
}
},
"packageManager": "[email protected]+sha256.dae0f7e822c56b20979bb5965e3b73b8bdabb6b8b8ef121da6d857508599ca35"
}
4 changes: 4 additions & 0 deletions src/main/Extensions/ColorConverter/ColorConversionResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type ColorConversionResult = {
format: string;
value: string;
};
5 changes: 5 additions & 0 deletions src/main/Extensions/ColorConverter/ColorConverter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ColorConversionResult } from "./ColorConversionResult";

export interface ColorConverter {
convertFromString(value: string): ColorConversionResult[];
}
105 changes: 105 additions & 0 deletions src/main/Extensions/ColorConverter/ColorConverterExtension.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { AssetPathResolver } from "@Core/AssetPathResolver";
import { SettingsManager } from "@Core/SettingsManager";
import type { Translator } from "@Core/Translator";
import { describe, expect, it, vi } from "vitest";
import { ColorConversionResult } from "./ColorConversionResult";
import { ColorConverter } from "./ColorConverter";
import { ColorConverterExtension } from "./ColorConverterExtension";

describe(ColorConverterExtension, () => {
describe(ColorConverterExtension.prototype.getSearchResultItems, () => {
it("should return an empty array", async () => {
const actual = await new ColorConverterExtension(null, null, null, null).getSearchResultItems();
expect(actual).toEqual([]);
});
});

describe(ColorConverterExtension.prototype.getImage, () => {
it("should return the correct image", async () => {
const assetPathResolver = <AssetPathResolver>{
getExtensionAssetPath: vi.fn().mockReturnValue("color-converter.png"),
getModuleAssetPath: () => null,
};

const colorConverterExtension = new ColorConverterExtension(assetPathResolver, null, null, null);

expect(colorConverterExtension.getImage()).toEqual({ url: "file://color-converter.png" });

expect(assetPathResolver.getExtensionAssetPath).toHaveBeenCalledWith(
"ColorConverter",
"color-converter.png",
);
});
});

describe(ColorConverterExtension.prototype.getSettingDefaultValue, () => {
const colorConverterExtension = new ColorConverterExtension(null, null, null, null);

it("should return undefined when passing a key that does not exist", () =>
expect(colorConverterExtension.getSettingDefaultValue("key")).toEqual(undefined));

it("should return the default formats when passing 'formats' as key", () =>
expect(colorConverterExtension.getSettingDefaultValue("formats")).toEqual(["HEX", "HLS", "RGB"]));
});

describe(ColorConverterExtension.prototype.isSupported, () =>
it("should return true", () =>
expect(new ColorConverterExtension(null, null, null, null).isSupported()).toBe(true)),
);

describe(ColorConverterExtension.prototype.getInstantSearchResultItems, () => {
it("should return search result items for the enabled formats", () => {
const t = vi.fn().mockReturnValue("translated string");

const assetPathResolver = <AssetPathResolver>{
getExtensionAssetPath: vi.fn().mockReturnValue("color-converter.png"),
getModuleAssetPath: () => null,
};

const translator = <Translator>{
createT: vi.fn().mockReturnValue({ t }),
};

const settingsManager = <SettingsManager>{
getValue: vi.fn().mockReturnValue(["HEX", "RGB"]),
updateValue: null,
};

const colorConverter = <ColorConverter>{
convertFromString: vi.fn().mockReturnValue(<ColorConversionResult[]>[
{ format: "HEX", value: "#FFFFFF" },
{ format: "HLS", value: "hsl(0, 0%, 100%)" },
{ format: "RGB", value: "rgb(255, 255, 255)" },
]),
};

const colorConverterExtension = new ColorConverterExtension(
assetPathResolver,
settingsManager,
translator,
colorConverter,
);

const actual = colorConverterExtension.getInstantSearchResultItems("#fff");

expect(colorConverter.convertFromString).toHaveBeenCalledOnce();
expect(colorConverter.convertFromString).toHaveBeenCalledWith("#fff");

expect(actual.length).toBe(2);
expect(actual.map((a) => a.name)).toEqual(["#FFFFFF", "rgb(255, 255, 255)"]);

expect(actual.map(({ image }) => image)).toEqual([
{ url: "file://color-converter.png" },
{ url: "file://color-converter.png" },
]);
});
});

describe(ColorConverterExtension.prototype.getI18nResources, () =>
it("should support en-US and de-CH", () =>
expect(Object.keys(new ColorConverterExtension(null, null, null, null).getI18nResources())).toEqual([
"en-US",
"de-CH",
])),
);
});
98 changes: 98 additions & 0 deletions src/main/Extensions/ColorConverter/ColorConverterExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { SearchResultItemActionUtility, type SearchResultItem } from "@common/Core";
import type { Image } from "@common/Core/Image";
import type { Resources, Translations } from "@common/Core/Translator";
import type { AssetPathResolver } from "@Core/AssetPathResolver";
import type { Extension } from "@Core/Extension";
import type { SettingsManager } from "@Core/SettingsManager";
import type { Translator } from "@Core/Translator";
import type { ColorConverter } from "./ColorConverter";

export class ColorConverterExtension implements Extension {
public readonly id = "ColorConverter";

public readonly name = "Color Converter";

public readonly nameTranslation = {
key: "extensionName",
namespace: "extension[ColorConverter]",
};

public readonly author = {
name: "Oliver Schwendener",
githubUserName: "oliverschwendener",
};

private readonly defaultSettings = {
formats: ["HEX", "HLS", "RGB"],
};

public constructor(
private readonly assetPathResolver: AssetPathResolver,
private readonly settingsManager: SettingsManager,
private readonly translator: Translator,
private readonly colorConverter: ColorConverter,
) {}

async getSearchResultItems(): Promise<SearchResultItem[]> {
return [];
}

public isSupported(): boolean {
return true;
}

public getSettingDefaultValue<T>(key: string): T {
return this.defaultSettings[key] as T;
}

public getImage(): Image {
return {
url: `file://${this.assetPathResolver.getExtensionAssetPath(this.id, "color-converter.png")}`,
};
}

public getI18nResources(): Resources<Translations> {
return {
"en-US": {
formats: "Color Formats",
selectAColorFormat: "Select a color format",
color: "{{ format }} Color",
copyColorToClipboard: "Copy color to clipboard",
extensionName: "Color Converter",
},
"de-CH": {
formats: "Farbformate",
selectAColorFormat: "Wähle ein Farbformat",
color: "{{ format }} Farbe",
copyColorToClipboard: "Farbe in die Zwischenablage kopieren",
extensionName: "Farbkonverter",
},
};
}

public getInstantSearchResultItems(searchTerm: string): SearchResultItem[] {
const { t } = this.translator.createT(this.getI18nResources());

return this.colorConverter
.convertFromString(searchTerm)
.filter(({ format }) => this.getEnabledColorFormats().includes(format))
.map(({ format, value }) => ({
defaultAction: SearchResultItemActionUtility.createCopyToClipboardAction({
textToCopy: value,
description: "Copy color to clipboard",
descriptionTranslation: {
key: "copyColorToClipboard",
namespace: "extension[ColorConverter]",
},
}),
description: t("color", { format }),
id: `color-${value}-${format}`,
image: this.getImage(),
name: value,
}));
}

private getEnabledColorFormats(): string[] {
return this.settingsManager.getValue(`extension[${this.id}].formats`, this.defaultSettings.formats);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Dependencies } from "@Core/Dependencies";
import type { DependencyRegistry } from "@Core/DependencyRegistry";
import { describe, expect, it, vi } from "vitest";
import { ColorConverterExtension } from "./ColorConverterExtension";
import { ColorConverterExtensionModule } from "./ColorConverterExtensionModule";
import { QixColorConverter } from "./QixColorConverter";

describe(ColorConverterExtensionModule, () => {
describe(ColorConverterExtensionModule.prototype.bootstrap, () => {
it("should bootstrap the extension", () => {
const dependencyRegistry = <DependencyRegistry<Dependencies>>{
get: vi.fn().mockReturnValue(null),
register: null,
};

expect(new ColorConverterExtensionModule().bootstrap(dependencyRegistry)).toEqual({
extension: new ColorConverterExtension(null, null, null, new QixColorConverter()),
});

expect(dependencyRegistry.get).toHaveBeenCalledWith("AssetPathResolver");
expect(dependencyRegistry.get).toHaveBeenCalledWith("SettingsManager");
expect(dependencyRegistry.get).toHaveBeenCalledWith("Translator");
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Dependencies } from "@Core/Dependencies";
import type { DependencyRegistry } from "@Core/DependencyRegistry";
import type { ExtensionBootstrapResult } from "../ExtensionBootstrapResult";
import type { ExtensionModule } from "../ExtensionModule";
import { ColorConverterExtension } from "./ColorConverterExtension";
import { QixColorConverter } from "./QixColorConverter";

export class ColorConverterExtensionModule implements ExtensionModule {
public bootstrap(dependencyRegistry: DependencyRegistry<Dependencies>): ExtensionBootstrapResult {
return {
extension: new ColorConverterExtension(
dependencyRegistry.get("AssetPathResolver"),
dependencyRegistry.get("SettingsManager"),
dependencyRegistry.get("Translator"),
new QixColorConverter(),
),
};
}
}
50 changes: 50 additions & 0 deletions src/main/Extensions/ColorConverter/QixColorConverter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { ColorConversionResult } from "./ColorConversionResult";
import { QixColorConverter } from "./QixColorConverter";

describe(QixColorConverter, () => {
describe(QixColorConverter.prototype.convertFromString, () => {
it("should return an empty array when the input can't be parsed as a color", () => {
expect(new QixColorConverter().convertFromString("")).toEqual([]);
expect(new QixColorConverter().convertFromString(" ")).toEqual([]);
expect(new QixColorConverter().convertFromString("1234")).toEqual([]);
expect(new QixColorConverter().convertFromString("invalid input")).toEqual([]);
expect(new QixColorConverter().convertFromString("#")).toEqual([]);
expect(new QixColorConverter().convertFromString("#ff")).toEqual([]);
expect(new QixColorConverter().convertFromString("#ffg")).toEqual([]);
expect(new QixColorConverter().convertFromString("rgb")).toEqual([]);
expect(new QixColorConverter().convertFromString("rgb()")).toEqual([]);
expect(new QixColorConverter().convertFromString("rgb(1)")).toEqual([]);
expect(new QixColorConverter().convertFromString("rgb(1,2)")).toEqual([]);
expect(new QixColorConverter().convertFromString("rgb(1,2,4,5,6)")).toEqual([]);
});

it("should be able to parse hex colors", () => {
expect(new QixColorConverter().convertFromString("#fff")).toEqual(<ColorConversionResult[]>[
{ format: "HEX", value: "#FFFFFF" },
{ format: "HLS", value: "hsl(0, 0%, 100%)" },
{ format: "RGB", value: "rgb(255, 255, 255)" },
]);

expect(new QixColorConverter().convertFromString("#ffffff")).toEqual(<ColorConversionResult[]>[
{ format: "HEX", value: "#FFFFFF" },
{ format: "HLS", value: "hsl(0, 0%, 100%)" },
{ format: "RGB", value: "rgb(255, 255, 255)" },
]);
});

it("should be able to parse rgb colors", () => {
expect(new QixColorConverter().convertFromString("rgb(255,255,255)")).toEqual(<ColorConversionResult[]>[
{ format: "HEX", value: "#FFFFFF" },
{ format: "HLS", value: "hsl(0, 0%, 100%)" },
{ format: "RGB", value: "rgb(255, 255, 255)" },
]);

expect(new QixColorConverter().convertFromString("rgba(255,255,255,1)")).toEqual(<ColorConversionResult[]>[
{ format: "HEX", value: "#FFFFFF" },
{ format: "HLS", value: "hsl(0, 0%, 100%)" },
{ format: "RGB", value: "rgb(255, 255, 255)" },
]);
});
});
});
Loading

0 comments on commit d319c16

Please sign in to comment.