From 306e350ba8d2b7bd896a6fd0ab22bda444a0700b Mon Sep 17 00:00:00 2001 From: Oliver Schwendener Date: Wed, 29 May 2024 17:43:02 +0200 Subject: [PATCH] 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 --- .../BrowserBookmarks/browser-bookmarks.png | Bin 0 -> 8245 bytes .../Core/BrowserWindow/BrowserWindowModule.ts | 32 +-- .../BrowserWindowToggler.test.ts | 222 ++++++++++++++++++ .../BrowserWindow/BrowserWindowToggler.ts | 64 +++++ .../BrowserWindow/toggleBrowserWindow.test.ts | 174 -------------- .../Core/BrowserWindow/toggleBrowserWindow.ts | 46 ---- .../BrowserBookmarks/BrowserBookmarks.ts | 108 ++++++--- .../BrowserBookmarksModule.ts | 2 + .../BrowserBookmarksSettings.tsx | 37 ++- 9 files changed, 409 insertions(+), 276 deletions(-) create mode 100644 assets/Extensions/BrowserBookmarks/browser-bookmarks.png create mode 100644 src/main/Core/BrowserWindow/BrowserWindowToggler.test.ts create mode 100644 src/main/Core/BrowserWindow/BrowserWindowToggler.ts delete mode 100644 src/main/Core/BrowserWindow/toggleBrowserWindow.test.ts delete mode 100644 src/main/Core/BrowserWindow/toggleBrowserWindow.ts diff --git a/assets/Extensions/BrowserBookmarks/browser-bookmarks.png b/assets/Extensions/BrowserBookmarks/browser-bookmarks.png new file mode 100644 index 0000000000000000000000000000000000000000..3db525958ddd20258ed91d1a039bafe7e0b64d0b GIT binary patch literal 8245 zcmeHMe^8U>9e?sB#vrC7G8a*20=0FzTG(*~s*ywur>vD|Z#$d_!E-$vipfw!gd{IQ zOOzq9-WF>if!S3n9C6wjpji@RRg_8aa7zUwQJ@(@AP|K7c;CEx-r(+b`(wS{&DGuI zy~{n%J>Spw$Mbo<@B2K@H>Y-{ZDTH4z6b!oOx-Se2LJ?i5x}6sX6E>PA8f*8+kbup zfbdB2M*-(+mjS?aq>48G^mwiFp{>z*F!Rf)_huq5e-v{;LiaZCbMDF+(QQ9&KdlgM zewu!NN6!v5eb+B`-+wnTY;g6(c*nhyjNhMJ^xBh{4qnqo!Wb5IdN-^1^_-?M(f97` zvLtKw4ZV>q+*wnFy9_q>gM)YFg==EI(`R~Jej9(_ynnD-@sqrr#dPDGF0hsCe62q7 zWC@DCwL4D;X7|<0SG*0%!%z-bB18;;1~z|@a1Jmc0U`tpKw*HB$MWJRtXBaI1uTHD z0L|H#jqu(kg*-sx0Y=~F?KI&zQV2DKfc4qIvM@A`6haLY@D5n%kzgMw1QZ^i2z#2D zuYwz-5FkQ;p!J!-2mmFh@q7a42ZPPZmzEvx+%^^Ul@a|boo45O3?1m_T zaal0HXdVqVlxSBv7}5a)U?E%Vy5K-O7|H?IQWBw{qbwF8pd~37J(eJ(0)#=G3^1ez z7>h?j!bymUo}^&*NGYThYY?&t3>5=xISDZ?Bq0*oa3xw20iu7%L3ljs-JER1SY=71 z_}1AASO*;R`2bfM!+H`a6CFZ-0>}o3lc|xewFBWO2mMz}7L0fNTS5A@W__{VJG!x9 z-pzQKaAV@L#iyyD`4?mHo_qErppb?zTR9TpJ&;U&MmY`rmTdla*5pO963UPH&s!)z zp1l+Q_K6AQM<_r3(-suUk5GPu^5dJ2&!PP#w7*pI)z?`w9JpzhH(K2r&?1 zAjCk3fe-^B2L2}uytWAKtM@KGBoXFPbL_kO?T7uyJ1df?GyrRt@iG>OSTd4@2SGR)ppt^)g z3#zXo)%R0vR6s9>v9!=id!mwZ6J04nikhq}k4xUqEnC|UOUx0JwPb1WY@azQiD8~i zxs-5#Pkmhij?vHbF=5?(?~Pn4azz<+nfb{fDgu&eAA9~eH6AQeH}us{Ep92Hd=)g0 zuw-G4Ooln|?xlotSD6gpVV!*(e??CDlv!I^1`bKebIWeV+ou;~=h!bjG=~e#uQVm_ zfM4Cvmwg9z>|2GWE#}z;A1BD@FCkat?90sM7PO0l)|Fm3MvVob;OCPn#QY1BHZ74g z7L6lWR_q?l$1Qm~#~iKgUf*p_mO);5ZGB9PL^ffGm_+Qc>%&@2dzFJV9Cl6YI@{c1 zXQS9LCdk{mL-GbD!2V`4%3JrxLi|xpt=2en)Tmfq-kUP)lb!I+CTzjZq*C!eFu02Kj!BDEoY#>?a4?It zx~^L z`@JKNEpHaT`pMw%$-8j7fWC|MOy(VE%`@hW9pFF7ykCfn0Mzg5$1HN8U7 z>A_y&>sF3bGSpeA>fAHv^XsK80trz=Go-m->kUx#i5sBns~S$0QGN>_@v)bDctG6t&Q_hNRjK|wGBx=JJlh`cFE#1<1Ma14 zwzsKh;>-SYe&ED;HEMk_iE+E7CoIm!m2a}yw=El`(7V9Ul}K0Y5{%D{rW=QEO?qKu z>OE<(oACBtuTPpqO}4Bd+)wwTm5n!QBH_5# zfLjhrj%^TNk4N+6JVZNmo0xWG1X3k16s6kV<#f)e$0}0Ig!eu%cTp{s!Z6uvcF}bm zMLlMz>$ZxoSDK;W$?Djbt4(h{>O7^|o?>+1dTDazs=+Uqk3QFM19r%u&4@dAkf-1D zc8QhtKnd!qraKng)!$-0Lb;C&!aP z6?co3!vV~t@w=Zg0-q665K^cy%*<49iFkj<_o4UoL_>qSm%E{ktH6&M104x70(TvK zQi0vsZ@L5jZl2ySLe64lc;uo$t}OCYL5FG>)f!FoxM0fj+nd#dUSs0ppZX&P2LB$h zM) { @@ -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 }); diff --git a/src/main/Core/BrowserWindow/BrowserWindowToggler.test.ts b/src/main/Core/BrowserWindow/BrowserWindowToggler.test.ts new file mode 100644 index 00000000..b7cccb1a --- /dev/null +++ b/src/main/Core/BrowserWindow/BrowserWindowToggler.test.ts @@ -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 = { + 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 = { 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 = { width: 100, height: 200 }; + const 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 = { width: 100, height: 200 }; + const 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 = { width: 100, height: 200 }; + const bounds = { x: 10, y: 20, width: 30, height: 40 }; + + const getValueMock = vi.fn().mockReturnValue(false); + const 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 = { width: 100, height: 200 }; + const bounds = { x: 10, y: 20, width: 30, height: 40 }; + + const getValueMock = vi.fn().mockReturnValue(true); + const 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 = { width: 100, height: 200 }; + const 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" }); + }); + }); +}); diff --git a/src/main/Core/BrowserWindow/BrowserWindowToggler.ts b/src/main/Core/BrowserWindow/BrowserWindowToggler.ts new file mode 100644 index 00000000..8b0e8952 --- /dev/null +++ b/src/main/Core/BrowserWindow/BrowserWindowToggler.ts @@ -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); + } +} diff --git a/src/main/Core/BrowserWindow/toggleBrowserWindow.test.ts b/src/main/Core/BrowserWindow/toggleBrowserWindow.test.ts deleted file mode 100644 index c97f169f..00000000 --- a/src/main/Core/BrowserWindow/toggleBrowserWindow.test.ts +++ /dev/null @@ -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: { show: () => appShowMock() }, - 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: {}, - 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: {}, - 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: {}, - 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, - })); - }); -}); diff --git a/src/main/Core/BrowserWindow/toggleBrowserWindow.ts b/src/main/Core/BrowserWindow/toggleBrowserWindow.ts deleted file mode 100644 index 405c4a26..00000000 --- a/src/main/Core/BrowserWindow/toggleBrowserWindow.ts +++ /dev/null @@ -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"); - } -}; diff --git a/src/main/Extensions/BrowserBookmarks/BrowserBookmarks.ts b/src/main/Extensions/BrowserBookmarks/BrowserBookmarks.ts index 0d656342..d891f5c9 100644 --- a/src/main/Extensions/BrowserBookmarks/BrowserBookmarks.ts +++ b/src/main/Extensions/BrowserBookmarks/BrowserBookmarks.ts @@ -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 { - 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( + `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( 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( - getExtensionSettingKey("BrowserBookmarks", "browser"), - this.getSettingDefaultValue("browser"), + private getCurrentlyConfiguredBrowsers(): Browser[] { + return this.settingsManager.getValue( + getExtensionSettingKey("BrowserBookmarks", "browsers"), + this.getSettingDefaultValue("browsers"), ); } diff --git a/src/main/Extensions/BrowserBookmarks/BrowserBookmarksModule.ts b/src/main/Extensions/BrowserBookmarks/BrowserBookmarksModule.ts index 49713eb2..57c78f60 100644 --- a/src/main/Extensions/BrowserBookmarks/BrowserBookmarksModule.ts +++ b/src/main/Extensions/BrowserBookmarks/BrowserBookmarksModule.ts @@ -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, ), }; } diff --git a/src/renderer/Extensions/BrowserBookmarks/BrowserBookmarksSettings.tsx b/src/renderer/Extensions/BrowserBookmarks/BrowserBookmarksSettings.tsx index 3e94152b..2f78c501 100644 --- a/src/renderer/Extensions/BrowserBookmarks/BrowserBookmarksSettings.tsx +++ b/src/renderer/Extensions/BrowserBookmarks/BrowserBookmarksSettings.tsx @@ -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({ extensionId, key: "browser" }); + const { value: browsers, updateValue: setBrowsers } = useExtensionSetting({ + extensionId, + key: "browsers", + }); const { value: searchResultStyle, updateValue: setSearchResultStyle } = useExtensionSetting({ extensionId, key: "searchResultStyle", }); + const { value: iconType, updateValue: setIconType } = useExtensionSetting({ + extensionId, + key: "iconType", + }); + return (
- + 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) => (
-
+
+ + optionValue && setIconType(optionValue)} + > + + + + +
); };