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 { 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 });
|
||||
|
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 { 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"),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user