Feat/bookmarks support multiple browsers (#1108)

* fix(Focus): restore focus correctly on Windows

* Extracted browser window toggling into separate class

* Added description

* feat(BrowserBookmarks): support multiple browsers at the same time

* Added generic extension icon
This commit is contained in:
Oliver Schwendener 2024-05-29 17:43:02 +02:00 committed by GitHub
parent 6ef5a29c7f
commit 306e350ba8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 409 additions and 276 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -4,8 +4,8 @@ import type { EnvironmentVariableProvider } from "@Core/EnvironmentVariableProvi
import type { EventSubscriber } from "@Core/EventSubscriber";
import type { SettingsManager } from "@Core/SettingsManager";
import type { UeliCommand, UeliCommandInvokedEvent } from "@Core/UeliCommand";
import { OperatingSystem } from "@common/Core";
import type { App, BrowserWindow } from "electron";
import type { OperatingSystem } from "@common/Core";
import type { BrowserWindow } from "electron";
import { join } from "path";
import { AppIconFilePathResolver } from "./AppIconFilePathResolver";
import {
@ -19,10 +19,10 @@ import {
defaultWindowSize,
} from "./BrowserWindowConstructorOptionsProvider";
import { BrowserWindowCreator } from "./BrowserWindowCreator";
import { BrowserWindowToggler } from "./BrowserWindowToggler";
import { WindowBoundsMemory } from "./WindowBoundsMemory";
import { openAndFocusBrowserWindow } from "./openAndFocusBrowserWindow";
import { sendToBrowserWindow } from "./sendToBrowserWindow";
import { toggleBrowserWindow } from "./toggleBrowserWindow";
export class BrowserWindowModule {
public static async bootstrap(dependencyRegistry: DependencyRegistry<Dependencies>) {
@ -62,6 +62,14 @@ export class BrowserWindowModule {
browserWindowConstructorOptionsProviders[operatingSystem],
).create();
const browserWindowToggler = new BrowserWindowToggler(
operatingSystem,
app,
browserWindow,
defaultWindowSize,
settingsManager,
);
eventEmitter.emitEvent("browserWindowCreated", { browserWindow });
nativeTheme.addListener("updated", () => browserWindow.setIcon(appIconFilePathResolver.getAppIconFilePath()));
@ -74,12 +82,11 @@ export class BrowserWindowModule {
BrowserWindowModule.registerEvents(
browserWindow,
app,
dependencyRegistry.get("EventSubscriber"),
windowBoundsMemory,
settingsManager,
vibrancyProvider,
backgroundMaterialProvider,
browserWindowToggler,
);
await BrowserWindowModule.loadFileOrUrl(browserWindow, dependencyRegistry.get("EnvironmentVariableProvider"));
@ -99,22 +106,15 @@ export class BrowserWindowModule {
private static registerEvents(
browserWindow: BrowserWindow,
app: App,
eventSubscriber: EventSubscriber,
windowBoundsMemory: WindowBoundsMemory,
settingsManager: SettingsManager,
vibrancyProvider: VibrancyProvider,
backgroundMaterialProvider: BackgroundMaterialProvider,
browserWindowToggler: BrowserWindowToggler,
) {
eventSubscriber.subscribe("hotkeyPressed", () => {
toggleBrowserWindow({
app,
browserWindow,
defaultSize: defaultWindowSize,
alwaysCenter: settingsManager.getValue("window.alwaysCenter", false),
bounds: windowBoundsMemory.getBoundsNearestToCursor(),
});
});
eventSubscriber.subscribe("hotkeyPressed", () =>
browserWindowToggler.toggle(windowBoundsMemory.getBoundsNearestToCursor()),
);
eventSubscriber.subscribe("settingUpdated", ({ key, value }: { key: string; value: unknown }) => {
sendToBrowserWindow(browserWindow, `settingUpdated[${key}]`, { value });

View File

@ -0,0 +1,222 @@
import type { SettingsManager } from "@Core/SettingsManager";
import type { OperatingSystem } from "@common/Core";
import type { App, BrowserWindow, Rectangle, Size } from "electron";
import { describe, expect, it, vi } from "vitest";
import { BrowserWindowToggler } from "./BrowserWindowToggler";
describe(BrowserWindowToggler, () => {
const createBrowserWindow = ({ isFocused, isVisible }: { isVisible: boolean; isFocused: boolean }) => {
const centerMock = vi.fn();
const focusMock = vi.fn();
const hideMock = vi.fn();
const isFocusedMock = vi.fn().mockReturnValue(isFocused);
const isVisibleMock = vi.fn().mockReturnValue(isVisible);
const minimizeMock = vi.fn();
const restoreMock = vi.fn();
const setBoundsMock = vi.fn();
const showMock = vi.fn();
const webContentsSendMock = vi.fn();
const browserWindow = <BrowserWindow>{
center: () => centerMock(),
focus: () => focusMock(),
hide: () => hideMock(),
isFocused: () => isFocusedMock(),
isVisible: () => isVisibleMock(),
minimize: () => minimizeMock(),
restore: () => restoreMock(),
setBounds: (b) => setBoundsMock(b),
show: () => showMock(),
webContents: { send: (c) => webContentsSendMock(c) },
};
return {
browserWindow,
centerMock,
focusMock,
hideMock,
isFocusedMock,
isVisibleMock,
minimizeMock,
restoreMock,
setBoundsMock,
showMock,
webContentsSendMock,
};
};
const createApp = () => {
const showMock = vi.fn();
const hideMock = vi.fn();
const app = <App>{ show: () => showMock(), hide: () => hideMock() };
return { app, showMock, hideMock };
};
describe(BrowserWindowToggler.prototype.toggle, () => {
it("should show and focus the window if its visible but not focused, recentering it and resizing it to the default size", () => {
const testShowAndFocus = ({ operatingSystem }: { operatingSystem: OperatingSystem }) => {
const { app, showMock: appShowMock } = createApp();
const {
browserWindow,
centerMock,
focusMock,
isFocusedMock,
isVisibleMock,
restoreMock,
setBoundsMock,
showMock,
webContentsSendMock,
} = createBrowserWindow({ isFocused: false, isVisible: true });
const defaultSize = <Size>{ width: 100, height: 200 };
const settingsManager = <SettingsManager>{};
new BrowserWindowToggler(operatingSystem, app, browserWindow, defaultSize, settingsManager).toggle();
expect(isVisibleMock).toHaveBeenCalledOnce();
expect(isFocusedMock).toHaveBeenCalledOnce();
expect(appShowMock).toHaveBeenCalledOnce();
operatingSystem === "Windows"
? expect(restoreMock).toHaveBeenCalledOnce()
: expect(restoreMock).not.toHaveBeenCalled();
expect(showMock).toHaveBeenCalledOnce();
expect(focusMock).toHaveBeenCalledOnce();
expect(webContentsSendMock).toHaveBeenCalledWith("windowFocused");
expect(setBoundsMock).toHaveBeenCalledWith(defaultSize);
expect(centerMock).toHaveBeenCalledOnce();
};
testShowAndFocus({ operatingSystem: "Windows" });
testShowAndFocus({ operatingSystem: "macOS" });
testShowAndFocus({ operatingSystem: "Linux" });
});
it("should show and focus the window if its hidden, recentering it and resizing it to the default size", () => {
const { app, showMock: appShowMock } = createApp();
const {
browserWindow,
centerMock,
focusMock,
isVisibleMock,
restoreMock,
setBoundsMock,
showMock,
webContentsSendMock,
} = createBrowserWindow({ isFocused: false, isVisible: false });
const defaultSize = <Size>{ width: 100, height: 200 };
const settingsManager = <SettingsManager>{};
new BrowserWindowToggler("Windows", app, browserWindow, defaultSize, settingsManager).toggle();
expect(isVisibleMock).toHaveBeenCalledOnce();
expect(appShowMock).toHaveBeenCalledOnce();
expect(restoreMock).toHaveBeenCalledOnce();
expect(showMock).toHaveBeenCalledOnce();
expect(focusMock).toHaveBeenCalledOnce();
expect(webContentsSendMock).toHaveBeenCalledWith("windowFocused");
expect(setBoundsMock).toHaveBeenCalledWith(defaultSize);
expect(centerMock).toHaveBeenCalledOnce();
});
it("should show and focus the window if its hidden, repositioning it with the given bounds", () => {
const { app, showMock: appShowMock } = createApp();
const {
browserWindow,
focusMock,
isVisibleMock,
restoreMock,
setBoundsMock,
showMock,
webContentsSendMock,
centerMock,
} = createBrowserWindow({ isFocused: false, isVisible: false });
const defaultSize = <Size>{ width: 100, height: 200 };
const bounds = <Rectangle>{ x: 10, y: 20, width: 30, height: 40 };
const getValueMock = vi.fn().mockReturnValue(false);
const settingsManager = <SettingsManager>{ getValue: (k, d) => getValueMock(k, d) };
new BrowserWindowToggler("Windows", app, browserWindow, defaultSize, settingsManager).toggle(bounds);
expect(isVisibleMock).toHaveBeenCalledOnce();
expect(appShowMock).toHaveBeenCalledOnce();
expect(restoreMock).toHaveBeenCalledOnce();
expect(showMock).toHaveBeenCalledOnce();
expect(focusMock).toHaveBeenCalledOnce();
expect(webContentsSendMock).toHaveBeenCalledWith("windowFocused");
expect(setBoundsMock).toHaveBeenCalledWith(bounds);
expect(centerMock).not.toHaveBeenCalled();
});
it("should show and focus the window if its hidden, repositioning it with the given bounds and recentering it if alwaysCenter is set to true", () => {
const { app, showMock: appShowMock } = createApp();
const {
browserWindow,
centerMock,
focusMock,
isVisibleMock,
restoreMock,
setBoundsMock,
showMock,
webContentsSendMock,
} = createBrowserWindow({ isFocused: false, isVisible: false });
const defaultSize = <Size>{ width: 100, height: 200 };
const bounds = <Rectangle>{ x: 10, y: 20, width: 30, height: 40 };
const getValueMock = vi.fn().mockReturnValue(true);
const settingsManager = <SettingsManager>{ getValue: (k, d) => getValueMock(k, d) };
new BrowserWindowToggler("Windows", app, browserWindow, defaultSize, settingsManager).toggle(bounds);
expect(isVisibleMock).toHaveBeenCalledOnce();
expect(appShowMock).toHaveBeenCalledOnce();
expect(restoreMock).toHaveBeenCalledOnce();
expect(showMock).toHaveBeenCalledOnce();
expect(focusMock).toHaveBeenCalledOnce();
expect(webContentsSendMock).toHaveBeenCalledWith("windowFocused");
expect(setBoundsMock).toHaveBeenCalledWith(bounds);
expect(centerMock).toHaveBeenCalledOnce();
});
it("should hide the window if it is visible and focussed", () => {
const testHide = ({ operatingSystem }: { operatingSystem: OperatingSystem }) => {
const { app, hideMock: appHideMock } = createApp();
const { browserWindow, isVisibleMock, isFocusedMock, minimizeMock, hideMock } = createBrowserWindow({
isFocused: true,
isVisible: true,
});
const defaultSize = <Size>{ width: 100, height: 200 };
const settingsManager = <SettingsManager>{};
new BrowserWindowToggler(operatingSystem, app, browserWindow, defaultSize, settingsManager).toggle();
expect(isVisibleMock).toHaveBeenCalledOnce();
expect(isFocusedMock).toHaveBeenCalledOnce();
expect(appHideMock).toHaveBeenCalledOnce();
operatingSystem === "Windows"
? expect(minimizeMock).toHaveBeenCalledOnce()
: expect(minimizeMock).not.toHaveBeenCalled();
expect(hideMock).toHaveBeenCalledOnce();
};
testHide({ operatingSystem: "Windows" });
testHide({ operatingSystem: "macOS" });
testHide({ operatingSystem: "Linux" });
});
});
});

View File

@ -0,0 +1,64 @@
import type { SettingsManager } from "@Core/SettingsManager";
import type { OperatingSystem } from "@common/Core";
import type { App, BrowserWindow, Rectangle, Size } from "electron";
export class BrowserWindowToggler {
public constructor(
private readonly operatingSystem: OperatingSystem,
private readonly app: App,
private readonly browserWindow: BrowserWindow,
private readonly defaultSize: Size,
private readonly settingsManager: SettingsManager,
) {}
public toggle(bounds?: Rectangle): void {
if (this.isVisibleAndFocused()) {
this.hide();
} else {
this.showAndFocus(bounds);
}
}
private isVisibleAndFocused(): boolean {
return this.browserWindow.isVisible() && this.browserWindow.isFocused();
}
private hide(): void {
this.app.hide && this.app.hide();
// In order to restore focus correctly to the previously focused window, we need to minimize the window on
// Windows.
if (this.operatingSystem === "Windows") {
this.browserWindow.minimize();
}
this.browserWindow.hide();
}
private showAndFocus(bounds?: Rectangle): void {
this.app.show && this.app.show();
// Because the window is minimized on Windows when hidden, we need to restore it before focusing it.
if (this.operatingSystem === "Windows") {
this.browserWindow.restore();
}
this.repositionWindow(bounds);
this.browserWindow.show();
this.browserWindow.focus();
this.browserWindow.webContents.send("windowFocused");
}
private repositionWindow(bounds: Rectangle): void {
this.browserWindow.setBounds(bounds ?? this.defaultSize);
if (!bounds || this.alwaysCenter()) {
this.browserWindow.center();
}
}
private alwaysCenter(): boolean {
return this.settingsManager.getValue("window.alwaysCenter", false);
}
}

View File

@ -1,174 +0,0 @@
import type { App, BrowserWindow, Rectangle, Size } from "electron";
import { describe, expect, it, vi } from "vitest";
import { toggleBrowserWindow } from "./toggleBrowserWindow";
describe(toggleBrowserWindow, () => {
describe("toggleBrowserWindow", () => {
const testToggleBrowserWindow = ({
app,
browserWindow,
expectations,
}: {
app: App;
browserWindow: BrowserWindow;
expectations: (() => void)[];
}) => {
toggleBrowserWindow({
app,
browserWindow,
alwaysCenter: false,
defaultSize: { width: 0, height: 0 },
});
for (const expectation of expectations) {
expectation();
}
};
it("should show and focus the window if its visible but not focused", () => {
const appShowMock = vi.fn();
const setBoundsMock = vi.fn();
const centerMock = vi.fn();
const showMock = vi.fn();
const focusMock = vi.fn();
const webContentsSendMock = vi.fn();
testToggleBrowserWindow({
app: <App>{ show: () => appShowMock() },
browserWindow: <BrowserWindow>{
isVisible: () => true,
isFocused: () => false,
setBounds: (b) => setBoundsMock(b),
center: () => centerMock(),
show: () => showMock(),
focus: () => focusMock(),
webContents: { send: (c) => webContentsSendMock(c) },
},
expectations: [
() => expect(appShowMock).toHaveBeenCalledOnce(),
() => expect(showMock).toHaveBeenCalledOnce(),
() => expect(focusMock).toHaveBeenCalledOnce(),
() => expect(webContentsSendMock).toHaveBeenCalledWith("windowFocused"),
],
});
});
it("should show and focus the window if its hidden", () => {
const setBoundsMock = vi.fn();
const centerMock = vi.fn();
const showMock = vi.fn();
const focusMock = vi.fn();
const webContentsSendMock = vi.fn();
testToggleBrowserWindow({
app: <App>{},
browserWindow: <BrowserWindow>{
isVisible: () => false,
setBounds: (b) => setBoundsMock(b),
center: () => centerMock(),
show: () => showMock(),
focus: () => focusMock(),
webContents: { send: (c) => webContentsSendMock(c) },
},
expectations: [
() => expect(showMock).toHaveBeenCalledOnce(),
() => expect(focusMock).toHaveBeenCalledOnce(),
() => expect(webContentsSendMock).toHaveBeenCalledWith("windowFocused"),
],
});
});
it("should hide the window if it is visible and focussed", () => {
const hideMock = vi.fn();
testToggleBrowserWindow({
app: <App>{},
browserWindow: <BrowserWindow>{
isVisible: () => true,
isFocused: () => true,
hide: () => hideMock(),
},
expectations: [() => expect(hideMock).toHaveBeenCalledOnce()],
});
});
});
describe("repositionWindow", () => {
const testRepositionWindow = ({
alwaysCenter,
defaultSize,
resizeTo,
bounds,
expectRecenter,
}: {
alwaysCenter: boolean;
defaultSize: Size;
resizeTo: Size | Rectangle;
bounds: Rectangle;
expectRecenter: boolean;
}) => {
const setBounds = vi.fn();
const center = vi.fn();
const show = vi.fn();
const focus = vi.fn();
const send = vi.fn();
toggleBrowserWindow({
app: <App>{},
browserWindow: <BrowserWindow>{
isVisible: () => false,
setBounds: (b) => setBounds(b),
center: () => center(),
show: () => show(),
focus: () => focus(),
webContents: { send: (c) => send(c) },
},
alwaysCenter,
defaultSize,
bounds,
});
if (expectRecenter) {
expect(center).toHaveBeenCalledOnce();
}
expect(setBounds).toHaveBeenCalledWith(resizeTo);
};
it("should reposition the window with the given bounds", () =>
testRepositionWindow({
alwaysCenter: false,
defaultSize: { width: 0, height: 0 },
resizeTo: { x: 0, y: 0, width: 1, height: 1 },
bounds: { x: 0, y: 0, width: 1, height: 1 },
expectRecenter: false,
}));
it("should reposition the window with the default size when no bounds given", () =>
testRepositionWindow({
alwaysCenter: false,
defaultSize: { width: 0, height: 0 },
resizeTo: { height: 0, width: 0 },
bounds: undefined,
expectRecenter: false,
}));
it("should re-center the window when no bounds given", () =>
testRepositionWindow({
alwaysCenter: false,
defaultSize: { width: 0, height: 0 },
resizeTo: { height: 0, width: 0 },
bounds: undefined,
expectRecenter: true,
}));
it("should re-center the window when alwaysCenter is set to true", () =>
testRepositionWindow({
alwaysCenter: true,
defaultSize: { width: 0, height: 0 },
resizeTo: { x: 0, y: 0, width: 1, height: 1 },
bounds: { x: 0, y: 0, width: 1, height: 1 },
expectRecenter: true,
}));
});
});

View File

@ -1,46 +0,0 @@
import type { App, BrowserWindow, Rectangle, Size } from "electron";
const repositionWindow = ({
browserWindow,
defaultSize,
alwaysCenter,
bounds,
}: {
browserWindow: BrowserWindow;
defaultSize: Size;
alwaysCenter: boolean;
bounds?: Rectangle;
}) => {
browserWindow.setBounds(bounds ?? defaultSize);
if (!bounds || alwaysCenter) {
browserWindow.center();
}
};
export const toggleBrowserWindow = ({
app,
browserWindow,
defaultSize,
alwaysCenter,
bounds,
}: {
app: App;
browserWindow: BrowserWindow;
defaultSize: Size;
alwaysCenter: boolean;
bounds?: Rectangle;
}) => {
if (browserWindow.isVisible() && browserWindow.isFocused()) {
app.hide && app.hide();
browserWindow.hide();
} else {
app.show && app.show();
repositionWindow({ browserWindow, defaultSize, alwaysCenter, bounds });
browserWindow.show();
browserWindow.focus();
browserWindow.webContents.send("windowFocused");
}
};

View File

@ -2,6 +2,7 @@ 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 type { Translator } from "@Core/Translator";
import { SearchResultItemActionUtility, type OperatingSystem, type SearchResultItem } from "@common/Core";
import { getExtensionSettingKey } from "@common/Core/Extension";
import type { Image } from "@common/Core/Image";
@ -10,8 +11,9 @@ import type { BrowserBookmark } from "./BrowserBookmark";
import type { BrowserBookmarkRepository } from "./BrowserBookmarkRepository";
type Settings = {
browser: Browser;
browsers: Browser[];
searchResultStyle: string;
iconType: string;
};
export class BrowserBookmarks implements Extension {
@ -29,8 +31,9 @@ export class BrowserBookmarks implements Extension {
};
private readonly defaultSettings: Settings = {
browser: "Google Chrome",
browsers: [],
searchResultStyle: "nameOnly",
iconType: "favicon",
};
public constructor(
@ -39,11 +42,57 @@ export class BrowserBookmarks implements Extension {
private readonly assetPathResolver: AssetPathResolver,
private readonly urlImageGenerator: UrlImageGenerator,
private readonly operatingSystem: OperatingSystem,
private readonly translator: Translator,
) {}
public async getSearchResultItems(): Promise<SearchResultItem[]> {
const browserBookmarks = await this.browserBookmarkRepositories[this.getCurrentlyConfiguredBrowser()].getAll();
return browserBookmarks.map((browserBookmark) => this.toSearchResultItem(browserBookmark));
const { t } = this.translator.createT(this.getI18nResources());
const browsers = this.getCurrentlyConfiguredBrowsers();
const toSearchResultItem = (browserBookmark: BrowserBookmark, browserName: Browser) => {
return {
description: t("searchResultItemDescription", { browserName }),
defaultAction: SearchResultItemActionUtility.createOpenUrlSearchResultAction({
url: browserBookmark.getUrl(),
}),
id: browserBookmark.getId(),
name: this.getName(browserBookmark),
image: this.getBrowserBookmarkImage(browserBookmark, browserName),
additionalActions: [
SearchResultItemActionUtility.createCopyToClipboardAction({
textToCopy: browserBookmark.getUrl(),
description: "Copy URL to clipboard",
descriptionTranslation: {
key: "copyUrlToClipboard",
namespace: "extension[BrowserBookmarks]",
},
}),
],
};
};
let result: SearchResultItem[] = [];
for (const browser of browsers) {
const repository: BrowserBookmarkRepository | undefined = this.browserBookmarkRepositories[browser];
if (repository) {
result = [...result, ...(await repository.getAll()).map((b) => toSearchResultItem(b, browser))];
}
}
return result;
}
private getBrowserBookmarkImage(browserBookmark: BrowserBookmark, browser: Browser): Image {
const iconType = this.settingsManager.getValue<string>(
`extension[${this.id}].iconType`,
this.defaultSettings["iconType"],
);
return iconType === "browserIcon"
? this.getBrowserImage(browser)
: this.urlImageGenerator.getImage(browserBookmark.getUrl());
}
public isSupported(): boolean {
@ -57,14 +106,15 @@ export class BrowserBookmarks implements Extension {
public getSettingKeysTriggeringRescan(): string[] {
return [
"imageGenerator.faviconApiProvider",
getExtensionSettingKey(this.id, "browser"),
getExtensionSettingKey(this.id, "browsers"),
getExtensionSettingKey(this.id, "searchResultStyle"),
getExtensionSettingKey(this.id, "iconType"),
];
}
public getImage(): Image {
return {
url: `file://${this.getAssetFilePath(this.getCurrentlyConfiguredBrowser())}`,
url: `file://${this.assetPathResolver.getExtensionAssetPath(this.id, "browser-bookmarks.png")}`,
};
}
@ -72,6 +122,12 @@ export class BrowserBookmarks implements Extension {
return this.getBrowserImageFilePath(key as Browser);
}
private getBrowserImage(browser: Browser): Image {
return {
url: `file://${this.getBrowserImageFilePath(browser)}`,
};
}
public getI18nResources() {
return {
"en-US": {
@ -79,40 +135,28 @@ export class BrowserBookmarks implements Extension {
"searchResultStyle.nameOnly": "Name only",
"searchResultStyle.urlOnly": "URL only",
"searchResultStyle.nameAndUrl": "Name & URL",
selectBrowsers: "Select browsers",
iconType: "Icon Type",
"iconType.favicon": "Favicon",
"iconType.browserIcon": "Browser icon",
copyUrlToClipboard: "Copy URL to clipboard",
searchResultItemDescription: "{{browserName}} Bookmark",
},
"de-CH": {
extensionName: "Browserlesezeichen",
"searchResultStyle.nameOnly": "Nur Name",
"searchResultStyle.urlOnly": "Nur URL",
"searchResultStyle.nameAndUrl": "Name & URL",
selectBrowsers: "Browser auswählen",
iconType: "Symboltyp",
"iconType.favicon": "Favicon",
"iconType.browserIcon": "Browsersymbol",
copyUrlToClipboard: "URL in Zwischenablage kopieren",
searchResultItemDescription: "{{browserName}} Lesezeichen",
},
};
}
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"),
@ -128,10 +172,10 @@ export class BrowserBookmarks implements Extension {
return Object.keys(names).includes(searchResultStyle) ? names[searchResultStyle]() : names["nameOnly"]();
}
private getCurrentlyConfiguredBrowser(): Browser {
return this.settingsManager.getValue<Browser>(
getExtensionSettingKey("BrowserBookmarks", "browser"),
this.getSettingDefaultValue("browser"),
private getCurrentlyConfiguredBrowsers(): Browser[] {
return this.settingsManager.getValue<Browser[]>(
getExtensionSettingKey("BrowserBookmarks", "browsers"),
this.getSettingDefaultValue("browsers"),
);
}

View File

@ -16,6 +16,7 @@ export class BrowserBookmarksModule implements ExtensionModule {
const settingsManager = dependencyRegistry.get("SettingsManager");
const assetPathResolver = dependencyRegistry.get("AssetPathResolver");
const urlImageGenerator = dependencyRegistry.get("UrlImageGenerator");
const translator = dependencyRegistry.get("Translator");
return {
extension: new BrowserBookmarks(
@ -48,6 +49,7 @@ export class BrowserBookmarksModule implements ExtensionModule {
assetPathResolver,
urlImageGenerator,
operatingSystem,
translator,
),
};
}

View File

@ -12,7 +12,7 @@ export const BrowserBookmarksSettings = () => {
const extensionId = "BrowserBookmarks";
const browsers: Browser[] = [
const browserOptions: Browser[] = [
"Arc",
"Brave Browser",
"Firefox",
@ -23,23 +23,33 @@ export const BrowserBookmarksSettings = () => {
const searchResultStyles = ["nameOnly", "urlOnly", "nameAndUrl"];
const { value: browser, updateValue: setBrowser } = useExtensionSetting<Browser>({ extensionId, key: "browser" });
const { value: browsers, updateValue: setBrowsers } = useExtensionSetting<Browser[]>({
extensionId,
key: "browsers",
});
const { value: searchResultStyle, updateValue: setSearchResultStyle } = useExtensionSetting<string>({
extensionId,
key: "searchResultStyle",
});
const { value: iconType, updateValue: setIconType } = useExtensionSetting<string>({
extensionId,
key: "iconType",
});
return (
<SectionList>
<Section>
<Field label="Browser">
<Field label="Browsers">
<Dropdown
value={`${browser}`}
selectedOptions={[browser]}
onOptionSelect={(_, { optionValue }) => optionValue && setBrowser(optionValue as Browser)}
value={browsers.join(", ")}
selectedOptions={browsers}
onOptionSelect={(_, { selectedOptions }) => setBrowsers(selectedOptions as Browser[])}
multiselect
placeholder={t("selectBrowsers", { ns })}
>
{browsers.map((browserName) => (
{browserOptions.map((browserName) => (
<Option key={browserName} value={browserName} text={browserName}>
<img
style={{ width: 20, height: 20 }}
@ -67,7 +77,18 @@ export const BrowserBookmarksSettings = () => {
</Dropdown>
</Field>
</Section>
<Section></Section>
<Section>
<Field label="Icon Type">
<Dropdown
value={t(`iconType.${iconType}`, { ns })}
selectedOptions={[iconType]}
onOptionSelect={(_, { optionValue }) => optionValue && setIconType(optionValue)}
>
<Option value="favicon">{t("iconType.favicon", { ns })}</Option>
<Option value="browserIcon">{t("iconType.browserIcon", { ns })}</Option>
</Dropdown>
</Field>
</Section>
</SectionList>
);
};