Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"dev": "electron-vite dev",
"dev:watch": "electron-vite dev --watch",
"dev:devtools": "cross-env BROWSER_WINDOW_DEVTOOLS=true bun run dev",
"dev:omnibox-devtools": "cross-env OMNIBOX_DEVTOOLS=true bun run dev",
"dev:all-devtools": "cross-env BROWSER_WINDOW_DEVTOOLS=true OMNIBOX_DEVTOOLS=true bun run dev",
"build": "bun run typecheck && cross-env PRODUCTION_BUILD=true electron-vite build && bun run script:prune-frontend-routes",
"build:unpack": "bun run build && electron-builder --dir",
"build:win": "bun run build && electron-builder --win",
Expand Down
15 changes: 9 additions & 6 deletions src/main/controllers/tabs-controller/context-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import contextMenu from "electron-context-menu";
import { Tab } from "./tab";
import { TabsController } from "./index";
import { saveImageAs } from "./save-image-as";
import { getSearchSettingsSnapshot } from "@/saving/settings";
import { buildSearchUrlFromSearchSettings, getSearchEngineDisplayName } from "~/search/search-settings";

// Define types for navigation history
interface NavigationHistory {
Expand Down Expand Up @@ -48,7 +50,8 @@ export function createTabContextMenu(
const canGoBack = navigationHistory.canGoBack();
const canGoForward = navigationHistory.canGoForward();
const lookUpSelection = defaultActions.lookUpSelection({});
const searchEngine = "Google";
const searchSettings = getSearchSettingsSnapshot();
const searchEngine = getSearchEngineDisplayName(searchSettings);

// Helper function to create a new tab
const createNewTab = async (url: string, overrideWindow?: BrowserWindow) => {
Expand All @@ -72,7 +75,8 @@ export function createTabContextMenu(
defaultActions as MenuActions,
parameters,
createNewTab,
searchEngine
searchEngine,
searchSettings
);
const imageItems = createImageItems(parameters, webContents, window, createNewTab, defaultActions as MenuActions);

Expand Down Expand Up @@ -266,7 +270,8 @@ function createSelectionItems(
defaultActions: MenuActions,
parameters: Electron.ContextMenuParams,
createNewTab: (url: string) => Promise<void>,
searchEngine: string
searchEngine: string,
searchSettings: ReturnType<typeof getSearchSettingsSnapshot>
): Electron.MenuItemConstructorOptions[] {
const selectionText = parameters.selectionText;

Expand All @@ -281,9 +286,7 @@ function createSelectionItems(
{
label: `Search ${searchEngine} for "${displaySelectionText}"`,
click: () => {
const searchURL = new URL("https://www.google.com/search");
searchURL.searchParams.set("q", selectionText);
createNewTab(searchURL.toString());
createNewTab(buildSearchUrlFromSearchSettings(searchSettings, selectionText));
}
}
];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BrowserWindow as ElectronBrowserWindow, Rectangle, WebContents, WebContentsView } from "electron";
import { app, BrowserWindow as ElectronBrowserWindow, Rectangle, WebContents, WebContentsView } from "electron";
import { debugPrint } from "@/modules/output";
import { clamp } from "@/modules/utils";
import { browserWindowsController } from "@/controllers/windows-controller/interfaces/browser";
Expand Down Expand Up @@ -34,7 +34,7 @@ type PaddedBounds = {
shadowPadding: OmniboxShadowPadding;
};

const OMNIBOX_OPEN_DEVTOOLS = false;
const OMNIBOX_OPEN_DEVTOOLS = !app.isPackaged && !!process.env.OMNIBOX_DEVTOOLS;

function normalizeBounds(bounds: Electron.Rectangle, windowBounds: Rectangle): Rectangle {
const width = clamp(Math.round(bounds.width), 0, windowBounds.width);
Expand Down
15 changes: 12 additions & 3 deletions src/main/ipc/window/settings.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { sendMessageToListeners } from "@/ipc/listeners-manager";
import { BasicSettings, BasicSettingCards } from "@/modules/basic-settings";
import { getSettingValueById, setSettingValueById } from "@/saving/settings";
import { getSearchSettingsSnapshot, getSettingValueById, setSettingValueById } from "@/saving/settings";
import { settings } from "@/controllers/windows-controller/interfaces/settings";
import { ipcMain } from "electron";
import type { SettingsChangedEvent } from "~/flow/interfaces/settings/settings";

ipcMain.on("settings:open", () => {
settings.show();
Expand All @@ -27,6 +28,14 @@ ipcMain.handle("settings:get-basic-settings", () => {
};
});

export function fireOnSettingsChanged() {
sendMessageToListeners("settings:on-changed");
ipcMain.handle("settings:get-search-settings-snapshot", () => {
return getSearchSettingsSnapshot();
});

ipcMain.on("settings:get-search-settings-snapshot-sync", (event) => {
event.returnValue = getSearchSettingsSnapshot();
});

export function fireOnSettingsChanged(payload: SettingsChangedEvent) {
sendMessageToListeners("settings:on-changed", payload);
}
53 changes: 53 additions & 0 deletions src/main/modules/basic-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
// This will make it easier to add new settings and cards.

import type { BasicSetting, BasicSettingCard } from "~/types/settings";
import {
CUSTOM_SEARCH_SUGGESTION_PROVIDER_OPTIONS,
DEFAULT_SEARCH_SETTINGS_SNAPSHOT,
SEARCH_ENGINE_SETTING_OPTIONS
} from "~/search/search-settings";
import { CUSTOM_SEARCH_TEMPLATE_EXAMPLE } from "~/search/custom-search";

/**
* Maps archive tab duration settings to their equivalent values in seconds.
Expand Down Expand Up @@ -78,6 +84,46 @@ export const BasicSettings: BasicSetting[] = [
]
},

// [GENERAL] Search Engine
{
id: "searchEngine",
name: "Search Engine",
showName: true,
description: "Pick a built-in engine or switch to a custom URL template.",
type: "enum",
defaultValue: DEFAULT_SEARCH_SETTINGS_SNAPSHOT.searchEngine,
options: SEARCH_ENGINE_SETTING_OPTIONS.map((option) => ({ ...option }))
},

{
id: "customSearchUrlTemplate",
name: "Search URL Template",
showName: true,
description: "Use {{query}} where Flow should insert the search text.",
type: "string",
defaultValue: "",
placeholder: CUSTOM_SEARCH_TEMPLATE_EXAMPLE
},

{
id: "customSearchSuggestionsProvider",
name: "Suggestions Source",
showName: true,
description: "Autocomplete can be disabled or powered by a built-in engine.",
type: "enum",
defaultValue: DEFAULT_SEARCH_SETTINGS_SNAPSHOT.customSearchSuggestionsProvider,
options: CUSTOM_SEARCH_SUGGESTION_PROVIDER_OPTIONS.map((option) => ({ ...option }))
},

{
id: "duckduckgoAiEnabled",
name: "DuckDuckGo AI Features",
showName: true,
description: "Allow DuckDuckGo to open AI-assisted search results instead of forcing classic web results.",
type: "boolean",
defaultValue: DEFAULT_SEARCH_SETTINGS_SNAPSHOT.duckduckgoAiEnabled
},

// New Tab Mode
{
id: "newTabMode",
Expand Down Expand Up @@ -269,6 +315,13 @@ export const BasicSettingCards: BasicSettingCard[] = [
settings: ["commandPaletteOpacity"]
},

// Search Engine Card
{
title: "Search Engine",
subtitle: "Choose your default search engine",
settings: ["searchEngine", "duckduckgoAiEnabled", "customSearchUrlTemplate", "customSearchSuggestionsProvider"]
},

// Sidebar Settings Card
{
title: "Sidebar Settings",
Expand Down
90 changes: 86 additions & 4 deletions src/main/saving/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@ import { fireOnSettingsChanged } from "@/ipc/window/settings";
import { BasicSettings } from "@/modules/basic-settings";
import { TypedEventEmitter } from "@/modules/typed-event-emitter";
import { BasicSetting, SettingType } from "~/types/settings";
import {
type SearchSettingsSnapshot,
getDefaultSearchSettingsSnapshot,
isSearchEngineSettingId,
isCustomSearchSuggestionsProviderId,
isSearchSettingsSnapshotKey,
validateActiveSearchSettings,
DEFAULT_SEARCH_SETTINGS_SNAPSHOT
} from "~/search/search-settings";
import type { SettingsChangedEvent } from "~/flow/interfaces/settings/settings";

export const SettingsDataStore = getDatastore("settings");

type SettingsEvents = {
"settings-changed": [];
"settings-changed": [SettingsChangedEvent];
};
export const settingsEmitter = new TypedEventEmitter<SettingsEvents>();

Expand All @@ -17,13 +27,81 @@ export const settingsEmitter = new TypedEventEmitter<SettingsEvents>();
// Settings: Settings Config //
const basicSettingsCurrentValues: Record<string, SettingType["defaultValue"]> = {};

function getSearchSettingsSnapshotFromValues(
values: Partial<Record<string, SettingType["defaultValue"]>>
): SearchSettingsSnapshot {
const defaults = getDefaultSearchSettingsSnapshot();

return {
searchEngine: isSearchEngineSettingId(values.searchEngine) ? values.searchEngine : defaults.searchEngine,
customSearchUrlTemplate:
typeof values.customSearchUrlTemplate === "string"
? values.customSearchUrlTemplate
: defaults.customSearchUrlTemplate,
customSearchSuggestionsProvider: isCustomSearchSuggestionsProviderId(values.customSearchSuggestionsProvider)
? values.customSearchSuggestionsProvider
: defaults.customSearchSuggestionsProvider,
duckduckgoAiEnabled:
typeof values.duckduckgoAiEnabled === "boolean" ? values.duckduckgoAiEnabled : defaults.duckduckgoAiEnabled
};
}

function buildSettingsChangedEvent(changedSettingIds: string[]): SettingsChangedEvent {
const includesSearchSettings = changedSettingIds.some((settingId) => isSearchSettingsSnapshotKey(settingId));

return {
changedSettingIds,
searchSettingsSnapshot: includesSearchSettings ? getSearchSettingsSnapshot() : undefined
};
}

function notifySettingsChanged(changedSettingIds: string[]) {
const event = buildSettingsChangedEvent(changedSettingIds);
fireOnSettingsChanged(event);
settingsEmitter.emit("settings-changed", event);
}

function getNextSearchSettingsSnapshot(settingId: string, value: unknown): SearchSettingsSnapshot | null {
if (!isSearchSettingsSnapshotKey(settingId)) {
return null;
}

return getSearchSettingsSnapshotFromValues({
...basicSettingsCurrentValues,
[settingId]: value as SettingType["defaultValue"]
});
}

function wouldCreateInvalidActiveSearchConfiguration(settingId: string, value: unknown): boolean {
const nextSearchSettings = getNextSearchSettingsSnapshot(settingId, value);
if (!nextSearchSettings) {
return false;
}

return !validateActiveSearchSettings(nextSearchSettings).valid;
}

function repairInvalidActiveSearchConfiguration() {
const searchSettings = getSearchSettingsSnapshotFromValues(basicSettingsCurrentValues);
if (validateActiveSearchSettings(searchSettings).valid) {
return;
}

basicSettingsCurrentValues.searchEngine = DEFAULT_SEARCH_SETTINGS_SNAPSHOT.searchEngine;
notifySettingsChanged(["searchEngine"]);
void SettingsDataStore.set("searchEngine", DEFAULT_SEARCH_SETTINGS_SNAPSHOT.searchEngine).catch(() => undefined);
}
Comment on lines +84 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing listener notification after startup repair

repairInvalidActiveSearchConfiguration mutates basicSettingsCurrentValues and persists the corrected engine to disk, but never calls fireOnSettingsChanged() or settingsEmitter.emit("settings-changed"). The repair runs after settingsCachedPromise resolves, which can happen after the renderer has already called getSearchSettingsSnapshotSync during module initialization. In that sequence the renderer initializes currentSearchSettings to the invalid (pre-repair) value and is never told to refresh it — so the settings UI continues to show the stale engine selection while the main process has silently moved to the default.


function validateSettingValue<T extends SettingType>(setting: T, value: unknown) {
if (setting.type === "boolean") {
return typeof value === "boolean";
}
if (setting.type === "enum") {
return setting.options.some((option) => option.id === value);
}
if (setting.type === "string") {
return typeof value === "string";
}
return false;
}

Expand All @@ -44,6 +122,7 @@ const settingsCachedPromise = new Promise<void>((resolve) => {
}

Promise.all(promises).then(() => {
repairInvalidActiveSearchConfiguration();
resolve();
});
});
Expand All @@ -55,17 +134,20 @@ export function getSettingValueById(settingId: string): SettingType["defaultValu
return basicSettingsCurrentValues[settingId];
}

export function getSearchSettingsSnapshot(): SearchSettingsSnapshot {
return getSearchSettingsSnapshotFromValues(basicSettingsCurrentValues);
}

// Export: Set Setting //
async function setSettingValue<T extends BasicSetting>(setting: T, value: unknown) {
if (validateSettingValue(setting, value)) {
if (validateSettingValue(setting, value) && !wouldCreateInvalidActiveSearchConfiguration(setting.id, value)) {
const saveSuccess = await SettingsDataStore.set(setting.id, value)
.then(() => true)
.catch(() => false);

if (saveSuccess) {
basicSettingsCurrentValues[setting.id] = value as T["defaultValue"];
fireOnSettingsChanged();
settingsEmitter.emit("settings-changed");
notifySettingsChanged([setting.id]);
return true;
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { FlowPasskeyAPI } from "~/flow/interfaces/browser/passkey";
import type { ConditionalPasskeyRequest, PasskeyCredential } from "~/types/passkey";
import { FlowPromptsAPI } from "~/flow/interfaces/browser/prompts";
import type { ActivePrompt } from "~/types/prompts";
import type { SettingsChangedEvent } from "~/flow/interfaces/settings/settings";

// const isIFrame = !process.isMainFrame;

Expand Down Expand Up @@ -682,7 +683,13 @@ const settingsAPI: FlowSettingsAPI = {
getBasicSettings: async () => {
return ipcRenderer.invoke("settings:get-basic-settings");
},
onSettingsChanged: (callback: () => void) => {
getSearchSettingsSnapshot: async () => {
return ipcRenderer.invoke("settings:get-search-settings-snapshot");
},
getSearchSettingsSnapshotSync: () => {
return ipcRenderer.sendSync("settings:get-search-settings-snapshot-sync");
},
onSettingsChanged: (callback: (event: SettingsChangedEvent) => void) => {
return listenOnIPCChannel("settings:on-changed", callback);
}
};
Expand Down
11 changes: 10 additions & 1 deletion src/renderer/src/components/onboarding/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ import { OnboardingInitialSpace } from "@/components/onboarding/stages/initial-s
import { OnboardingWelcome } from "@/components/onboarding/stages/welcome";
import { AnimatePresence } from "motion/react";
import { useState } from "react";
import { OnboardingSearchProvider } from "./stages/search-provider";

export type OnboardingAdvanceCallback = () => void;

const stages = [OnboardingWelcome, OnboardingInitialSpace, OnboardingIcon, OnboardingNewTab, OnboardingFinish];
const stages = [
OnboardingWelcome,
OnboardingInitialSpace,
OnboardingIcon,
OnboardingNewTab,
OnboardingSearchProvider,
OnboardingFinish
];

export function OnboardingMain() {
const [stage, setStage] = useState<number>(0);
Expand All @@ -21,6 +29,7 @@ export function OnboardingMain() {
const Stage = stages[stage];
if (!Stage) {
flow.onboarding.finish();
return null;
}

return (
Expand Down
Loading