mirror of
https://github.com/oliverschwendener/ueli.git
synced 2024-10-04 10:07:19 +03:00
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:
parent
6ef5a29c7f
commit
306e350ba8
BIN
assets/Extensions/BrowserBookmarks/browser-bookmarks.png
Normal file
BIN
assets/Extensions/BrowserBookmarks/browser-bookmarks.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
@ -4,8 +4,8 @@ import type { EnvironmentVariableProvider } from "@Core/EnvironmentVariableProvi
|
|||||||
import type { EventSubscriber } from "@Core/EventSubscriber";
|
import type { EventSubscriber } from "@Core/EventSubscriber";
|
||||||
import type { SettingsManager } from "@Core/SettingsManager";
|
import type { SettingsManager } from "@Core/SettingsManager";
|
||||||
import type { UeliCommand, UeliCommandInvokedEvent } from "@Core/UeliCommand";
|
import type { UeliCommand, UeliCommandInvokedEvent } from "@Core/UeliCommand";
|
||||||
import { OperatingSystem } from "@common/Core";
|
import type { OperatingSystem } from "@common/Core";
|
||||||
import type { App, BrowserWindow } from "electron";
|
import type { BrowserWindow } from "electron";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { AppIconFilePathResolver } from "./AppIconFilePathResolver";
|
import { AppIconFilePathResolver } from "./AppIconFilePathResolver";
|
||||||
import {
|
import {
|
||||||
@ -19,10 +19,10 @@ import {
|
|||||||
defaultWindowSize,
|
defaultWindowSize,
|
||||||
} from "./BrowserWindowConstructorOptionsProvider";
|
} from "./BrowserWindowConstructorOptionsProvider";
|
||||||
import { BrowserWindowCreator } from "./BrowserWindowCreator";
|
import { BrowserWindowCreator } from "./BrowserWindowCreator";
|
||||||
|
import { BrowserWindowToggler } from "./BrowserWindowToggler";
|
||||||
import { WindowBoundsMemory } from "./WindowBoundsMemory";
|
import { WindowBoundsMemory } from "./WindowBoundsMemory";
|
||||||
import { openAndFocusBrowserWindow } from "./openAndFocusBrowserWindow";
|
import { openAndFocusBrowserWindow } from "./openAndFocusBrowserWindow";
|
||||||
import { sendToBrowserWindow } from "./sendToBrowserWindow";
|
import { sendToBrowserWindow } from "./sendToBrowserWindow";
|
||||||
import { toggleBrowserWindow } from "./toggleBrowserWindow";
|
|
||||||
|
|
||||||
export class BrowserWindowModule {
|
export class BrowserWindowModule {
|
||||||
public static async bootstrap(dependencyRegistry: DependencyRegistry<Dependencies>) {
|
public static async bootstrap(dependencyRegistry: DependencyRegistry<Dependencies>) {
|
||||||
@ -62,6 +62,14 @@ export class BrowserWindowModule {
|
|||||||
browserWindowConstructorOptionsProviders[operatingSystem],
|
browserWindowConstructorOptionsProviders[operatingSystem],
|
||||||
).create();
|
).create();
|
||||||
|
|
||||||
|
const browserWindowToggler = new BrowserWindowToggler(
|
||||||
|
operatingSystem,
|
||||||
|
app,
|
||||||
|
browserWindow,
|
||||||
|
defaultWindowSize,
|
||||||
|
settingsManager,
|
||||||
|
);
|
||||||
|
|
||||||
eventEmitter.emitEvent("browserWindowCreated", { browserWindow });
|
eventEmitter.emitEvent("browserWindowCreated", { browserWindow });
|
||||||
|
|
||||||
nativeTheme.addListener("updated", () => browserWindow.setIcon(appIconFilePathResolver.getAppIconFilePath()));
|
nativeTheme.addListener("updated", () => browserWindow.setIcon(appIconFilePathResolver.getAppIconFilePath()));
|
||||||
@ -74,12 +82,11 @@ export class BrowserWindowModule {
|
|||||||
|
|
||||||
BrowserWindowModule.registerEvents(
|
BrowserWindowModule.registerEvents(
|
||||||
browserWindow,
|
browserWindow,
|
||||||
app,
|
|
||||||
dependencyRegistry.get("EventSubscriber"),
|
dependencyRegistry.get("EventSubscriber"),
|
||||||
windowBoundsMemory,
|
windowBoundsMemory,
|
||||||
settingsManager,
|
|
||||||
vibrancyProvider,
|
vibrancyProvider,
|
||||||
backgroundMaterialProvider,
|
backgroundMaterialProvider,
|
||||||
|
browserWindowToggler,
|
||||||
);
|
);
|
||||||
|
|
||||||
await BrowserWindowModule.loadFileOrUrl(browserWindow, dependencyRegistry.get("EnvironmentVariableProvider"));
|
await BrowserWindowModule.loadFileOrUrl(browserWindow, dependencyRegistry.get("EnvironmentVariableProvider"));
|
||||||
@ -99,22 +106,15 @@ export class BrowserWindowModule {
|
|||||||
|
|
||||||
private static registerEvents(
|
private static registerEvents(
|
||||||
browserWindow: BrowserWindow,
|
browserWindow: BrowserWindow,
|
||||||
app: App,
|
|
||||||
eventSubscriber: EventSubscriber,
|
eventSubscriber: EventSubscriber,
|
||||||
windowBoundsMemory: WindowBoundsMemory,
|
windowBoundsMemory: WindowBoundsMemory,
|
||||||
settingsManager: SettingsManager,
|
|
||||||
vibrancyProvider: VibrancyProvider,
|
vibrancyProvider: VibrancyProvider,
|
||||||
backgroundMaterialProvider: BackgroundMaterialProvider,
|
backgroundMaterialProvider: BackgroundMaterialProvider,
|
||||||
|
browserWindowToggler: BrowserWindowToggler,
|
||||||
) {
|
) {
|
||||||
eventSubscriber.subscribe("hotkeyPressed", () => {
|
eventSubscriber.subscribe("hotkeyPressed", () =>
|
||||||
toggleBrowserWindow({
|
browserWindowToggler.toggle(windowBoundsMemory.getBoundsNearestToCursor()),
|
||||||
app,
|
);
|
||||||
browserWindow,
|
|
||||||
defaultSize: defaultWindowSize,
|
|
||||||
alwaysCenter: settingsManager.getValue("window.alwaysCenter", false),
|
|
||||||
bounds: windowBoundsMemory.getBoundsNearestToCursor(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSubscriber.subscribe("settingUpdated", ({ key, value }: { key: string; value: unknown }) => {
|
eventSubscriber.subscribe("settingUpdated", ({ key, value }: { key: string; value: unknown }) => {
|
||||||
sendToBrowserWindow(browserWindow, `settingUpdated[${key}]`, { value });
|
sendToBrowserWindow(browserWindow, `settingUpdated[${key}]`, { value });
|
||||||
|
222
src/main/Core/BrowserWindow/BrowserWindowToggler.test.ts
Normal file
222
src/main/Core/BrowserWindow/BrowserWindowToggler.test.ts
Normal 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" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
64
src/main/Core/BrowserWindow/BrowserWindowToggler.ts
Normal file
64
src/main/Core/BrowserWindow/BrowserWindowToggler.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
@ -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");
|
|
||||||
}
|
|
||||||
};
|
|
@ -2,6 +2,7 @@ import type { AssetPathResolver } from "@Core/AssetPathResolver";
|
|||||||
import type { Extension } from "@Core/Extension";
|
import type { Extension } from "@Core/Extension";
|
||||||
import type { UrlImageGenerator } from "@Core/ImageGenerator";
|
import type { UrlImageGenerator } from "@Core/ImageGenerator";
|
||||||
import type { SettingsManager } from "@Core/SettingsManager";
|
import type { SettingsManager } from "@Core/SettingsManager";
|
||||||
|
import type { Translator } from "@Core/Translator";
|
||||||
import { SearchResultItemActionUtility, type OperatingSystem, type SearchResultItem } from "@common/Core";
|
import { SearchResultItemActionUtility, type OperatingSystem, type SearchResultItem } from "@common/Core";
|
||||||
import { getExtensionSettingKey } from "@common/Core/Extension";
|
import { getExtensionSettingKey } from "@common/Core/Extension";
|
||||||
import type { Image } from "@common/Core/Image";
|
import type { Image } from "@common/Core/Image";
|
||||||
@ -10,8 +11,9 @@ import type { BrowserBookmark } from "./BrowserBookmark";
|
|||||||
import type { BrowserBookmarkRepository } from "./BrowserBookmarkRepository";
|
import type { BrowserBookmarkRepository } from "./BrowserBookmarkRepository";
|
||||||
|
|
||||||
type Settings = {
|
type Settings = {
|
||||||
browser: Browser;
|
browsers: Browser[];
|
||||||
searchResultStyle: string;
|
searchResultStyle: string;
|
||||||
|
iconType: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class BrowserBookmarks implements Extension {
|
export class BrowserBookmarks implements Extension {
|
||||||
@ -29,8 +31,9 @@ export class BrowserBookmarks implements Extension {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly defaultSettings: Settings = {
|
private readonly defaultSettings: Settings = {
|
||||||
browser: "Google Chrome",
|
browsers: [],
|
||||||
searchResultStyle: "nameOnly",
|
searchResultStyle: "nameOnly",
|
||||||
|
iconType: "favicon",
|
||||||
};
|
};
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -39,11 +42,57 @@ export class BrowserBookmarks implements Extension {
|
|||||||
private readonly assetPathResolver: AssetPathResolver,
|
private readonly assetPathResolver: AssetPathResolver,
|
||||||
private readonly urlImageGenerator: UrlImageGenerator,
|
private readonly urlImageGenerator: UrlImageGenerator,
|
||||||
private readonly operatingSystem: OperatingSystem,
|
private readonly operatingSystem: OperatingSystem,
|
||||||
|
private readonly translator: Translator,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getSearchResultItems(): Promise<SearchResultItem[]> {
|
public async getSearchResultItems(): Promise<SearchResultItem[]> {
|
||||||
const browserBookmarks = await this.browserBookmarkRepositories[this.getCurrentlyConfiguredBrowser()].getAll();
|
const { t } = this.translator.createT(this.getI18nResources());
|
||||||
return browserBookmarks.map((browserBookmark) => this.toSearchResultItem(browserBookmark));
|
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 {
|
public isSupported(): boolean {
|
||||||
@ -57,14 +106,15 @@ export class BrowserBookmarks implements Extension {
|
|||||||
public getSettingKeysTriggeringRescan(): string[] {
|
public getSettingKeysTriggeringRescan(): string[] {
|
||||||
return [
|
return [
|
||||||
"imageGenerator.faviconApiProvider",
|
"imageGenerator.faviconApiProvider",
|
||||||
getExtensionSettingKey(this.id, "browser"),
|
getExtensionSettingKey(this.id, "browsers"),
|
||||||
getExtensionSettingKey(this.id, "searchResultStyle"),
|
getExtensionSettingKey(this.id, "searchResultStyle"),
|
||||||
|
getExtensionSettingKey(this.id, "iconType"),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public getImage(): Image {
|
public getImage(): Image {
|
||||||
return {
|
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);
|
return this.getBrowserImageFilePath(key as Browser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getBrowserImage(browser: Browser): Image {
|
||||||
|
return {
|
||||||
|
url: `file://${this.getBrowserImageFilePath(browser)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public getI18nResources() {
|
public getI18nResources() {
|
||||||
return {
|
return {
|
||||||
"en-US": {
|
"en-US": {
|
||||||
@ -79,40 +135,28 @@ export class BrowserBookmarks implements Extension {
|
|||||||
"searchResultStyle.nameOnly": "Name only",
|
"searchResultStyle.nameOnly": "Name only",
|
||||||
"searchResultStyle.urlOnly": "URL only",
|
"searchResultStyle.urlOnly": "URL only",
|
||||||
"searchResultStyle.nameAndUrl": "Name & URL",
|
"searchResultStyle.nameAndUrl": "Name & URL",
|
||||||
|
selectBrowsers: "Select browsers",
|
||||||
|
iconType: "Icon Type",
|
||||||
|
"iconType.favicon": "Favicon",
|
||||||
|
"iconType.browserIcon": "Browser icon",
|
||||||
copyUrlToClipboard: "Copy URL to clipboard",
|
copyUrlToClipboard: "Copy URL to clipboard",
|
||||||
|
searchResultItemDescription: "{{browserName}} Bookmark",
|
||||||
},
|
},
|
||||||
"de-CH": {
|
"de-CH": {
|
||||||
extensionName: "Browserlesezeichen",
|
extensionName: "Browserlesezeichen",
|
||||||
"searchResultStyle.nameOnly": "Nur Name",
|
"searchResultStyle.nameOnly": "Nur Name",
|
||||||
"searchResultStyle.urlOnly": "Nur URL",
|
"searchResultStyle.urlOnly": "Nur URL",
|
||||||
"searchResultStyle.nameAndUrl": "Name & URL",
|
"searchResultStyle.nameAndUrl": "Name & URL",
|
||||||
|
selectBrowsers: "Browser auswählen",
|
||||||
|
iconType: "Symboltyp",
|
||||||
|
"iconType.favicon": "Favicon",
|
||||||
|
"iconType.browserIcon": "Browsersymbol",
|
||||||
copyUrlToClipboard: "URL in Zwischenablage kopieren",
|
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 {
|
private getName(browserBookmark: BrowserBookmark): string {
|
||||||
const searchResultStyle = this.settingsManager.getValue<string>(
|
const searchResultStyle = this.settingsManager.getValue<string>(
|
||||||
getExtensionSettingKey(this.id, "searchResultStyle"),
|
getExtensionSettingKey(this.id, "searchResultStyle"),
|
||||||
@ -128,10 +172,10 @@ export class BrowserBookmarks implements Extension {
|
|||||||
return Object.keys(names).includes(searchResultStyle) ? names[searchResultStyle]() : names["nameOnly"]();
|
return Object.keys(names).includes(searchResultStyle) ? names[searchResultStyle]() : names["nameOnly"]();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCurrentlyConfiguredBrowser(): Browser {
|
private getCurrentlyConfiguredBrowsers(): Browser[] {
|
||||||
return this.settingsManager.getValue<Browser>(
|
return this.settingsManager.getValue<Browser[]>(
|
||||||
getExtensionSettingKey("BrowserBookmarks", "browser"),
|
getExtensionSettingKey("BrowserBookmarks", "browsers"),
|
||||||
this.getSettingDefaultValue("browser"),
|
this.getSettingDefaultValue("browsers"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ export class BrowserBookmarksModule implements ExtensionModule {
|
|||||||
const settingsManager = dependencyRegistry.get("SettingsManager");
|
const settingsManager = dependencyRegistry.get("SettingsManager");
|
||||||
const assetPathResolver = dependencyRegistry.get("AssetPathResolver");
|
const assetPathResolver = dependencyRegistry.get("AssetPathResolver");
|
||||||
const urlImageGenerator = dependencyRegistry.get("UrlImageGenerator");
|
const urlImageGenerator = dependencyRegistry.get("UrlImageGenerator");
|
||||||
|
const translator = dependencyRegistry.get("Translator");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
extension: new BrowserBookmarks(
|
extension: new BrowserBookmarks(
|
||||||
@ -48,6 +49,7 @@ export class BrowserBookmarksModule implements ExtensionModule {
|
|||||||
assetPathResolver,
|
assetPathResolver,
|
||||||
urlImageGenerator,
|
urlImageGenerator,
|
||||||
operatingSystem,
|
operatingSystem,
|
||||||
|
translator,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ export const BrowserBookmarksSettings = () => {
|
|||||||
|
|
||||||
const extensionId = "BrowserBookmarks";
|
const extensionId = "BrowserBookmarks";
|
||||||
|
|
||||||
const browsers: Browser[] = [
|
const browserOptions: Browser[] = [
|
||||||
"Arc",
|
"Arc",
|
||||||
"Brave Browser",
|
"Brave Browser",
|
||||||
"Firefox",
|
"Firefox",
|
||||||
@ -23,23 +23,33 @@ export const BrowserBookmarksSettings = () => {
|
|||||||
|
|
||||||
const searchResultStyles = ["nameOnly", "urlOnly", "nameAndUrl"];
|
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>({
|
const { value: searchResultStyle, updateValue: setSearchResultStyle } = useExtensionSetting<string>({
|
||||||
extensionId,
|
extensionId,
|
||||||
key: "searchResultStyle",
|
key: "searchResultStyle",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { value: iconType, updateValue: setIconType } = useExtensionSetting<string>({
|
||||||
|
extensionId,
|
||||||
|
key: "iconType",
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionList>
|
<SectionList>
|
||||||
<Section>
|
<Section>
|
||||||
<Field label="Browser">
|
<Field label="Browsers">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
value={`${browser}`}
|
value={browsers.join(", ")}
|
||||||
selectedOptions={[browser]}
|
selectedOptions={browsers}
|
||||||
onOptionSelect={(_, { optionValue }) => optionValue && setBrowser(optionValue as Browser)}
|
onOptionSelect={(_, { selectedOptions }) => setBrowsers(selectedOptions as Browser[])}
|
||||||
|
multiselect
|
||||||
|
placeholder={t("selectBrowsers", { ns })}
|
||||||
>
|
>
|
||||||
{browsers.map((browserName) => (
|
{browserOptions.map((browserName) => (
|
||||||
<Option key={browserName} value={browserName} text={browserName}>
|
<Option key={browserName} value={browserName} text={browserName}>
|
||||||
<img
|
<img
|
||||||
style={{ width: 20, height: 20 }}
|
style={{ width: 20, height: 20 }}
|
||||||
@ -67,7 +77,18 @@ export const BrowserBookmarksSettings = () => {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Field>
|
</Field>
|
||||||
</Section>
|
</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>
|
</SectionList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user