Skip to content

Commit

Permalink
Extracted favicon url image generation
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverschwendener committed Feb 19, 2024
1 parent 189cc7b commit 1b1241d
Show file tree
Hide file tree
Showing 16 changed files with 185 additions and 85 deletions.
1 change: 1 addition & 0 deletions src/main/Core/Dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ export type Dependencies = {
SystemPreferences?: Electron.SystemPreferences;
Translator?: Core.Translator;
UeliCommandInvoker?: Core.UeliCommandInvoker;
UrlImageGenerator?: Core.UrlImageGenerator;
};
5 changes: 5 additions & 0 deletions src/main/Core/ImageGenerator/Contract/UrlImageGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Image } from "@common/Core/Image";

export interface UrlImageGenerator {
getImage(url: string): Image;
}
1 change: 1 addition & 0 deletions src/main/Core/ImageGenerator/Contract/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./UrlImageGenerator";
12 changes: 12 additions & 0 deletions src/main/Core/ImageGenerator/ImageGeneratorModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Dependencies } from "@Core/Dependencies";
import type { DependencyRegistry } from "@Core/DependencyRegistry";
import { UrlImageGenerator } from "./UrlImageGenerator";

export class ImageGeneratorModule {
public static bootstrap(dependencyRegistry: DependencyRegistry<Dependencies>) {
dependencyRegistry.register(
"UrlImageGenerator",
new UrlImageGenerator(dependencyRegistry.get("SettingsManager")),
);
}
}
45 changes: 45 additions & 0 deletions src/main/Core/ImageGenerator/UrlImageGenerator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { SettingsManager } from "@Core/SettingsManager";
import type { Image } from "@common/Core/Image";
import { describe, expect, it, vi } from "vitest";
import { UrlImageGenerator } from "./UrlImageGenerator";

describe(UrlImageGenerator, () => {
it("should generate correct favicon urls using the Google favicon provider", () => {
const getValueMock = vi.fn().mockReturnValue("Google");
const settingsManager = <SettingsManager>{ getValue: (k, d) => getValueMock(k, d) };

const urlImageGenerator = new UrlImageGenerator(settingsManager);

expect(urlImageGenerator.getImage("https://github.com")).toEqual(<Image>{
url: "https://www.google.com/s2/favicons?domain=github.com&sz=48",
});

expect(getValueMock).toHaveBeenCalledWith("imageGenerator.faviconApiProvider", "Google");
});

it("should generate correct favicon urls using the Favicone favicon provider", () => {
const getValueMock = vi.fn().mockReturnValue("Favicone");
const settingsManager = <SettingsManager>{ getValue: (k, d) => getValueMock(k, d) };

const urlImageGenerator = new UrlImageGenerator(settingsManager);

expect(urlImageGenerator.getImage("https://github.com")).toEqual(<Image>{
url: "https://favicone.com/github.com?s=48",
});

expect(getValueMock).toHaveBeenCalledWith("imageGenerator.faviconApiProvider", "Google");
});

it("should use the Google API when the configured provider is invalid", () => {
const getValueMock = vi.fn().mockReturnValue("Invalid");
const settingsManager = <SettingsManager>{ getValue: (k, d) => getValueMock(k, d) };

const urlImageGenerator = new UrlImageGenerator(settingsManager);

expect(urlImageGenerator.getImage("https://github.com")).toEqual(<Image>{
url: "https://www.google.com/s2/favicons?domain=github.com&sz=48",
});

expect(getValueMock).toHaveBeenCalledWith("imageGenerator.faviconApiProvider", "Google");
});
});
27 changes: 27 additions & 0 deletions src/main/Core/ImageGenerator/UrlImageGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { SettingsManager } from "@Core/SettingsManager";
import type { Image } from "@common/Core/Image";
import type { UrlImageGenerator as UrlImageGeneratorInterface } from "./Contract";

export class UrlImageGenerator implements UrlImageGeneratorInterface {
public constructor(private readonly settingsManager: SettingsManager) {}

public getImage(url: string): Image {
const { host } = new URL(url);
const size = 48;

const imageUrls: Record<string, () => string> = {
Google: () => `https://www.google.com/s2/favicons?domain=${host}&sz=${size}`,
Favicone: () => `https://favicone.com/${host}?s=${size}`,
};

return {
url: Object.keys(imageUrls).includes(this.getFaviconApiProvider())
? imageUrls[this.getFaviconApiProvider()]()
: imageUrls["Google"](),
};
}

private getFaviconApiProvider(): string {
return this.settingsManager.getValue("imageGenerator.faviconApiProvider", "Google");
}
}
2 changes: 2 additions & 0 deletions src/main/Core/ImageGenerator/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./Contract";
export * from "./ImageGeneratorModule";
1 change: 1 addition & 0 deletions src/main/Core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from "./ExtensionRegistry";
export * from "./FavoriteManager";
export * from "./FileSystemUtility";
export * from "./GlobalShortcut";
export * from "./ImageGenerator";
export * from "./Logger";
export * from "./NativeTheme";
export * from "./OperatingSystem";
Expand Down
6 changes: 3 additions & 3 deletions src/main/Extensions/BrowserBookmarks/BrowserBookmark.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SearchResultItem } from "@common/Core";

export interface BrowserBookmark {
toSearchResultItem(...args: unknown[]): SearchResultItem;
getName(): string;
getUrl(): string;
getId(): string;
}
69 changes: 47 additions & 22 deletions src/main/Extensions/BrowserBookmarks/BrowserBookmarks.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import type { SearchResultItem } from "@common/Core";
import { getExtensionSettingKey, type Translations } from "@common/Core/Extension";
import type { Image } from "@common/Core/Image";
import type { Browser } from "@common/Extensions/BrowserBookmarks";
import type { AssetPathResolver } from "@Core/AssetPathResolver";
import type { Extension } from "@Core/Extension";
import type { UrlImageGenerator } from "@Core/ImageGenerator";
import type { SettingsManager } from "@Core/SettingsManager";
import { SearchResultItemActionUtility, type SearchResultItem } from "@common/Core";
import { getExtensionSettingKey, type Translations } from "@common/Core/Extension";
import type { Image } from "@common/Core/Image";
import type { Browser } from "@common/Extensions/BrowserBookmarks";
import type { BrowserBookmark } from "./BrowserBookmark";
import type { BrowserBookmarkRepository } from "./BrowserBookmarkRepository";

type Settings = {
browser: Browser;
searchResultStyle: string;
faviconApi: string;
};

export class BrowserBookmarks implements Extension {
Expand All @@ -30,31 +31,18 @@ export class BrowserBookmarks implements Extension {
private readonly defaultSettings: Settings = {
browser: "Google Chrome",
searchResultStyle: "nameOnly",
faviconApi: "Google",
};

public constructor(
private readonly browserBookmarkRepository: BrowserBookmarkRepository,
private readonly settingsManager: SettingsManager,
private readonly assetPathResolver: AssetPathResolver,
private readonly urlImageGenerator: UrlImageGenerator,
) {}

public async getSearchResultItems(): Promise<SearchResultItem[]> {
const browser = this.getCurrentlyConfiguredBrowser();

const searchResultStyle = this.settingsManager.getValue<string>(
getExtensionSettingKey(this.id, "searchResultStyle"),
this.getSettingDefaultValue("searchResultStyle"),
);

const faviconApi = this.settingsManager.getValue<string>(
getExtensionSettingKey(this.id, "faviconApi"),
this.getSettingDefaultValue("faviconApi"),
);

return (await this.browserBookmarkRepository.getAll(browser)).map((browserBookmark) =>
browserBookmark.toSearchResultItem(searchResultStyle, faviconApi),
);
const browserBookmarks = await this.browserBookmarkRepository.getAll(this.getCurrentlyConfiguredBrowser());
return browserBookmarks.map((browserBookmark) => this.toSearchResultItem(browserBookmark));
}

public isSupported(): boolean {
Expand All @@ -67,9 +55,9 @@ export class BrowserBookmarks implements Extension {

public getSettingKeysTriggeringRescan(): string[] {
return [
"imageGenerator.faviconApiProvider",
getExtensionSettingKey(this.id, "browser"),
getExtensionSettingKey(this.id, "searchResultStyle"),
getExtensionSettingKey(this.id, "faviconApi"),
];
}

Expand Down Expand Up @@ -102,6 +90,43 @@ export class BrowserBookmarks implements Extension {
};
}

private toSearchResultItem(browserBookmark: BrowserBookmark): SearchResultItem {
return {
description: "Browser Bookmark",
defaultAction: SearchResultItemActionUtility.createOpenUrlSearchResultAction({
url: browserBookmark.getUrl(),
}),
id: browserBookmark.getId(),
name: this.getName(browserBookmark),
image: this.urlImageGenerator.getImage(browserBookmark.getUrl()),
additionalActions: [
SearchResultItemActionUtility.createCopyToClipboardAction({
textToCopy: browserBookmark.getUrl(),
description: "Copy URL to clipboard",
descriptionTranslation: {
key: "copyUrlToClipboard",
namespace: "extension[BrowserBookmarks]",
},
}),
],
};
}

private getName(browserBookmark: BrowserBookmark): string {
const searchResultStyle = this.settingsManager.getValue<string>(
getExtensionSettingKey(this.id, "searchResultStyle"),
this.getSettingDefaultValue("searchResultStyle"),
);

const names: Record<string, () => string> = {
nameOnly: () => browserBookmark.getName(),
urlOnly: () => browserBookmark.getUrl(),
nameAndUrl: () => `${browserBookmark.getName()} - ${browserBookmark.getUrl()}`,
};

return Object.keys(names).includes(searchResultStyle) ? names[searchResultStyle]() : names["nameOnly"]();
}

private getCurrentlyConfiguredBrowser(): Browser {
return this.settingsManager.getValue<Browser>(
getExtensionSettingKey("BrowserBookmarks", "browser"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ export class BrowserBookmarksModule implements ExtensionModule {
const operatingSystem = dependencyRegistry.get("OperatingSystem");
const settingsManager = dependencyRegistry.get("SettingsManager");
const assetPathResolver = dependencyRegistry.get("AssetPathResolver");
const urlImageGenerator = dependencyRegistry.get("UrlImageGenerator");

return {
extension: new BrowserBookmarks(
new ChromiumBrowserBookmarkRepository(app, fileSystemUtility, operatingSystem),
settingsManager,
assetPathResolver,
urlImageGenerator,
),
};
}
Expand Down
45 changes: 6 additions & 39 deletions src/main/Extensions/BrowserBookmarks/ChromiumBrowserBookmark.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { SearchResultItem } from "@common/Core";
import { SearchResultItemActionUtility } from "@common/Core";
import type { BrowserBookmark } from "./BrowserBookmark";

export class ChromiumBrowserBookmark implements BrowserBookmark {
Expand All @@ -10,46 +8,15 @@ export class ChromiumBrowserBookmark implements BrowserBookmark {
private readonly id: string,
) {}

public toSearchResultItem(searchResultStyle: string, faviconApi: string): SearchResultItem {
return {
description: "Browser Bookmark",
defaultAction: SearchResultItemActionUtility.createOpenUrlSearchResultAction({ url: this.url }),
id: this.getId(),
name: this.getName(searchResultStyle),
image: { url: this.getImageUrl(faviconApi) },
additionalActions: [
SearchResultItemActionUtility.createCopyToClipboardAction({
textToCopy: this.url,
description: "Copy URL to clipboard",
descriptionTranslation: {
key: "copyUrlToClipboard",
namespace: "extension[BrowserBookmarks]",
},
}),
],
};
public getId(): string {
return `BrowserBookmark[Chromium]-${this.guid}-${this.id}`;
}

private getId(): string {
return `BrowserBookmark-${this.guid}-${this.id}`;
public getName(): string {
return this.name;
}

private getName(searchResultStyle: string): string {
const nameMap: Record<string, string> = {
nameOnly: this.name,
urlOnly: this.url,
nameAndUrl: `${this.name} - ${this.url}`,
};

return nameMap[searchResultStyle];
}

private getImageUrl(faviconApi: string): string {
const { host } = new URL(this.url);
const size = 48;

return faviconApi === "Google"
? `https://www.google.com/s2/favicons?domain=${host}&sz=${size}`
: `https://favicone.com/${host}?s=${size}`;
public getUrl(): string {
return this.url;
}
}
1 change: 1 addition & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import * as Extensions from "./Extensions";
Core.SettingsReaderModule.bootstrap(dependencyRegistry);
Core.SettingsWriterModule.bootstrap(dependencyRegistry);
Core.SettingsManagerModule.bootstrap(dependencyRegistry);
Core.ImageGeneratorModule.bootstrap(dependencyRegistry);
Core.TranslatorModule.bootstrap(dependencyRegistry);
Core.SearchIndexModule.bootstrap(dependencyRegistry);
Core.NativeThemeModule.bootstrap(dependencyRegistry);
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/Core/Settings/Pages/General/General.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SectionList } from "../../SectionList";
import { Autostart } from "./Autostart";
import { HotKey } from "./HotKey";
import { Language } from "./Language";
import { UrlImageGenerator } from "./UrlImageGenerator";

export const General = () => {
return (
Expand All @@ -16,6 +17,9 @@ export const General = () => {
<Section>
<Autostart />
</Section>
<Section>
<UrlImageGenerator />
</Section>
</SectionList>
);
};
27 changes: 27 additions & 0 deletions src/renderer/Core/Settings/Pages/General/UrlImageGenerator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useSetting } from "@Core/Hooks";
import { Dropdown, Field, Option } from "@fluentui/react-components";

export const UrlImageGenerator = () => {
const faviconApiProviders = ["Google", "Favicone"];

const { value: faviconApiProvider, updateValue: setFaviconApiProvider } = useSetting<string>({
key: "imageGenerator.faviconApiProvider",
defaultValue: "Google",
});

return (
<Field label="Favicon API" hint="This API is used to generate icons for URLs">
<Dropdown
value={faviconApiProvider}
selectedOptions={[faviconApiProvider]}
onOptionSelect={(_, { optionValue }) => optionValue && setFaviconApiProvider(optionValue)}
>
{faviconApiProviders.map((f) => (
<Option key={`faviconApi-${f}`} value={f} text={f}>
{f}
</Option>
))}
</Dropdown>
</Field>
);
};
Loading

0 comments on commit 1b1241d

Please sign in to comment.