Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement custom web search #1249

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions assets/Extensions/CustomWebSearch/customwebsearch.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions docs/Extensions/CustomWebSearch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Custom Web Search Extension

This extension allows define custom search engines, to directly search on the preferred website.

![Example](example.png)

## Settings

- **Name**: The name for your custom search engine (only visual).
- **Prefix**: The prefix, which should trigger the custom search engine
- **Url**: The URL, to which you want to be redirected. Use `{{query}}` placeholder, where the search term should be inserted
- **Encode search term**: Specifies, if the search term should be URL encoded (e.g. `%20` for a space)

![Settings](settings.png)

## About this extension

Author: [NiewView](https://github.com/NiewView)
Extension Icon: Designed by [OpenMoji](https://openmoji.org/) – the open-source emoji and icon project. License: [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/#)

Supported operating systems:

- Windows
- macOS
- Linux
Binary file added docs/Extensions/CustomWebSearch/example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Extensions/CustomWebSearch/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type CustomSearchEngineSetting = {
id: string;
name: string;
prefix: string;
url: string;
encodeSearchTerm: boolean;
};
5 changes: 5 additions & 0 deletions src/common/Extensions/CustomWebSearch/Settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { CustomSearchEngineSetting } from "./CustomSearchEngineSetting";

export type Settings = {
customSearchEngines: CustomSearchEngineSetting[];
};
2 changes: 2 additions & 0 deletions src/common/Extensions/CustomWebSearch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./CustomSearchEngineSetting";
export * from "./Settings";
135 changes: 135 additions & 0 deletions src/main/Extensions/CustomWebSearch/CustomWebSearchExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import type { AssetPathResolver } from "@Core/AssetPathResolver";
import type { Extension } from "@Core/Extension";
import type { SettingsManager } from "@Core/SettingsManager";
import type { OperatingSystem, SearchResultItem } from "@common/Core";
import type { Image } from "@common/Core/Image";
import type { CustomSearchEngineSetting, Settings } from "@common/Extensions/CustomWebSearch";

export class CustomWebSearchExtension implements Extension {
public readonly id = "CustomWebSearch";
public readonly name = "Browser Search";

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

public readonly author = {
name: "NiewView",
githubUserName: "NiewView",
};

public constructor(
private readonly operatingSystem: OperatingSystem,
private readonly assetPathResolver: AssetPathResolver,
private readonly settingsManager: SettingsManager,
) {}

async getSearchResultItems(): Promise<SearchResultItem[]> {
// Custom search engines do not have static search results
return [];
}

public getInstantSearchResultItems(searchTerm: string): SearchResultItem[] {
const customSearchEngines = this.settingsManager.getValue<CustomSearchEngineSetting[]>(
`extension[${this.id}].customSearchEngines`,
this.getSettingDefaultValue("customSearchEngines"),
);

const selectedSearchEngine = customSearchEngines.find((engine) => searchTerm.startsWith(engine.prefix));

if (!selectedSearchEngine) {
return [];
}

const searchInput = searchTerm.replace(selectedSearchEngine.prefix, "").trim();
const encodeSearchInput = selectedSearchEngine.encodeSearchTerm ? encodeURIComponent(searchInput) : searchInput;

return [
{
name: selectedSearchEngine.name,
description: `Search in ${selectedSearchEngine.name}`,
id: `${selectedSearchEngine.name}:instantResult`,
image: this.getImage(),
defaultAction: {
handlerId: "Url",
description: `Search in default Browser`,
argument: selectedSearchEngine.url.replace("{{query}}", encodeSearchInput),
},
},
];
}

public isSupported(): boolean {
return ["macOS", "Linux", "Windows"].includes(this.operatingSystem);
}

public getSettingDefaultValue(key: keyof Settings) {
const defaultSettings: Settings = {
customSearchEngines: [
{
id: crypto.randomUUID(),
name: "Wikipedia",
prefix: "wiki",
url: "https://en.wikipedia.org/wiki/{{query}}",
encodeSearchTerm: true,
},
],
};

return defaultSettings[key];
}

public getImage(): Image {
const path = this.assetPathResolver.getExtensionAssetPath("CustomWebSearch", "customwebsearch.svg");

return {
url: `file://${path}`,
};
}

public getDefaultFileImage(): Image {
const path = this.assetPathResolver.getExtensionAssetPath("CustomWebSearch", "customwebsearch.svg");

return {
url: `file://${path}`,
};
}

public getI18nResources() {
return {
"en-US": {
extensionName: "Custom Seb Search",
addSearchEngine: "Add web search",
prefix: "Prefix",
prefixTooltip: "The prefix to trigger this custom search engine.",
prefixError: "Prefix is required",
name: "Name",
nameError: "Name is required",
searchEngineUrl: "URL template",
searchEngineUrlTooltip: "Use `{{query}}` where the search term should be inserted.",
searchEngineUrlError: "The URL template does not contain `{{query}}` placeholder.",
encodeSearchTerm: "Encode search term",
encodeSearchTermTooltip: "Encode the search term before passing it to the search engine.",
add: "Add",
cancel: "Cancel",
},
"de-CH": {
extensionName: "Personalisierte Websuche",
addSearchEngine: "Websuche hinzufügen",
prefix: "Prefix",
prefixDescription: "Der Präfix, um diese benutzerdefinierte Websuche auszulösen.",
name: "Name",
nameDescription: "Der Name der benutzerdefinierten Websuche.",
searchEngineUrl: "URL-Template",
searchEngineUrlTooltip: "Verwenden Sie `{{query}}`, wo der Suchbegriff eingefügt werden soll.",
searchEngineUrlWarning: "Das URL-Template enthält keinen `{{query}}` Platzhalter.",
encodeSearchTerm: "Suchbegriff URL-kodieren",
encodeSearchTermTooltip:
"Gibt an, ob der Suchbegriff vor der Übergabe an die Suchmaschine kodiert werden soll.",
add: "Hinzufügen",
cancel: "Abbrechen",
},
};
}
}
17 changes: 17 additions & 0 deletions src/main/Extensions/CustomWebSearch/CustomWebSearchModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Dependencies } from "@Core/Dependencies";
import type { DependencyRegistry } from "@Core/DependencyRegistry";
import type { ExtensionBootstrapResult } from "../ExtensionBootstrapResult";
import type { ExtensionModule } from "../ExtensionModule";
import { CustomWebSearchExtension } from "./CustomWebSearchExtension";

export class CustomWebSearchModule implements ExtensionModule {
public bootstrap(dependencyRegistry: DependencyRegistry<Dependencies>): ExtensionBootstrapResult {
return {
extension: new CustomWebSearchExtension(
dependencyRegistry.get("OperatingSystem"),
dependencyRegistry.get("AssetPathResolver"),
dependencyRegistry.get("SettingsManager"),
),
};
}
}
1 change: 1 addition & 0 deletions src/main/Extensions/CustomWebSearch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./CustomWebSearchModule";
2 changes: 2 additions & 0 deletions src/main/Extensions/ExtensionLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { BrowserBookmarksModule } from "./BrowserBookmarks";
import { CalculatorModule } from "./Calculator";
import { ColorConverterExtensionModule } from "./ColorConverter";
import { CurrencyConversionModule } from "./CurrencyConversion/CurrencyConversionModule";
import { CustomWebSearchModule } from "./CustomWebSearch";
import { DeeplTranslatorModule } from "./DeeplTranslator";
import type { ExtensionModule } from "./ExtensionModule";
import { FileSearchModule } from "./FileSearch/FileSearchModule";
Expand Down Expand Up @@ -35,6 +36,7 @@ export class ExtensionLoader {
new CalculatorModule(),
new ColorConverterExtensionModule(),
new CurrencyConversionModule(),
new CustomWebSearchModule(),
new DeeplTranslatorModule(),
new FileSearchModule(),
new JetBrainsToolboxModule(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { CustomSearchEngineSetting, Settings } from "@common/Extensions/CustomWebSearch";
import { useExtensionSetting } from "@Core/Hooks";
import { SettingGroup } from "@Core/Settings/SettingGroup";
import { SettingGroupList } from "@Core/Settings/SettingGroupList";
import {
Button,
Table,
TableBody,
TableCell,
TableCellActions,
TableCellLayout,
TableHeader,
TableHeaderCell,
TableRow,
Tooltip,
} from "@fluentui/react-components";
import { CheckmarkRegular, DismissRegular } from "@fluentui/react-icons";
import { useTranslation } from "react-i18next";
import { EditCustomSearchEngine } from "./EditCustomSearchEngine";

const createCustomSearchEngineSetting = (): CustomSearchEngineSetting => ({
id: crypto.randomUUID(),
name: "",
prefix: "",
url: "",
encodeSearchTerm: true,
});

export const CustomWebSearchSettings = () => {
const extensionId = "CustomWebSearch";

const { t } = useTranslation("extension[CustomWebSearch]");

const { value: customSearchEngineSettings, updateValue: setCustomSearchEngineSettings } = useExtensionSetting<
Settings["customSearchEngines"]
>({
extensionId,
key: "customSearchEngines",
});

const addCustomSearchEngineSetting = (engineSetting: CustomSearchEngineSetting) =>
setCustomSearchEngineSettings([...customSearchEngineSettings, engineSetting]);

const removeCustomSearchEngineSetting = (id: string) =>
setCustomSearchEngineSettings(customSearchEngineSettings.filter((setting) => setting.id !== id));

return (
<SettingGroupList>
<SettingGroup title={t("folders")}>
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell style={{ width: 120 }}>{t("name")}</TableHeaderCell>
<TableHeaderCell style={{ width: 80 }}>{t("prefix")}</TableHeaderCell>
<TableHeaderCell>{t("url")}</TableHeaderCell>
<TableHeaderCell style={{ width: 80 }}>{t("encodeSearchTerm")}</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{customSearchEngineSettings.map(({ id, name, prefix, url, encodeSearchTerm }) => (
<TableRow key={name}>
<TableCell>{name}</TableCell>
<TableCell>{prefix}</TableCell>
<TableCell style={{ overflow: "hidden" }}>{url}</TableCell>
<TableCell>
<TableCellLayout
style={{ display: "flex", flexDirection: "row", justifyContent: "center" }}
>
{encodeSearchTerm ? <CheckmarkRegular /> : ""}
</TableCellLayout>
<TableCellActions>
<Tooltip relationship="label" content={t("remove")}>
<Button
size="small"
icon={<DismissRegular />}
onClick={() => removeCustomSearchEngineSetting(id)}
/>
</Tooltip>
</TableCellActions>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div>
<EditCustomSearchEngine
onSave={addCustomSearchEngineSetting}
initialEngineSetting={{
...createCustomSearchEngineSetting(),
}}
/>
</div>
</SettingGroup>
</SettingGroupList>
);
};
Loading