├── .nvmrc
├── .clinerules
├── src
├── renderer
│ ├── .gitignore
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── routes
│ │ │ ├── omnibox
│ │ │ │ ├── page.tsx
│ │ │ │ └── config.tsx
│ │ │ ├── main-ui
│ │ │ │ ├── page.tsx
│ │ │ │ └── config.tsx
│ │ │ ├── popup-ui
│ │ │ │ ├── page.tsx
│ │ │ │ └── config.tsx
│ │ │ ├── settings
│ │ │ │ ├── page.tsx
│ │ │ │ └── config.tsx
│ │ │ ├── pdf-viewer
│ │ │ │ ├── pdf-viewer
│ │ │ │ │ ├── README.md
│ │ │ │ │ ├── Toolbar
│ │ │ │ │ │ ├── Splitter.tsx
│ │ │ │ │ │ └── DocumentInfo.tsx
│ │ │ │ │ ├── Thumbsbar
│ │ │ │ │ │ └── Attachments.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── config.tsx
│ │ │ ├── new-tab
│ │ │ │ ├── page.tsx
│ │ │ │ └── config.tsx
│ │ │ ├── error
│ │ │ │ └── config.tsx
│ │ │ ├── omnibox-debug
│ │ │ │ └── config.tsx
│ │ │ ├── about
│ │ │ │ └── config.tsx
│ │ │ ├── games
│ │ │ │ └── config.tsx
│ │ │ ├── onboarding
│ │ │ │ ├── config.tsx
│ │ │ │ └── page.tsx
│ │ │ └── extensions
│ │ │ │ └── config.tsx
│ │ ├── types
│ │ │ └── routes.ts
│ │ ├── components
│ │ │ ├── react-icons
│ │ │ │ ├── README.md
│ │ │ │ └── iconContext.ts
│ │ │ ├── providers
│ │ │ │ └── nuqs-provider.tsx
│ │ │ ├── ui
│ │ │ │ ├── skeleton.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── progress.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── switch.tsx
│ │ │ │ ├── radio-group.tsx
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ └── popover.tsx
│ │ │ ├── settings
│ │ │ │ ├── settings-titlebar.tsx
│ │ │ │ ├── sections
│ │ │ │ │ ├── general
│ │ │ │ │ │ └── section.tsx
│ │ │ │ │ ├── spaces
│ │ │ │ │ │ ├── theme-editors
│ │ │ │ │ │ │ └── space-icon.tsx
│ │ │ │ │ │ └── space-card.tsx
│ │ │ │ │ ├── shortcuts
│ │ │ │ │ │ └── components
│ │ │ │ │ │ │ ├── search-header.tsx
│ │ │ │ │ │ │ └── reset-dialog.tsx
│ │ │ │ │ └── about
│ │ │ │ │ │ └── section.tsx
│ │ │ │ └── settings-sidebar.tsx
│ │ │ ├── onboarding
│ │ │ │ ├── stages
│ │ │ │ │ └── onboarding-drag-disabler.tsx
│ │ │ │ ├── screen.tsx
│ │ │ │ └── main.tsx
│ │ │ ├── logic
│ │ │ │ └── tab-disabler.tsx
│ │ │ ├── analytics
│ │ │ │ └── umami.tsx
│ │ │ ├── browser-ui
│ │ │ │ └── sidebar
│ │ │ │ │ ├── content
│ │ │ │ │ ├── space-title.tsx
│ │ │ │ │ └── new-tab-button.tsx
│ │ │ │ │ └── hover-detector.tsx
│ │ │ └── main
│ │ │ │ └── website-favicon.tsx
│ │ ├── hooks
│ │ │ ├── use-force-update.tsx
│ │ │ ├── use-debounce.tsx
│ │ │ └── use-mobile.ts
│ │ └── lib
│ │ │ ├── omnibox
│ │ │ ├── data-providers
│ │ │ │ ├── open-tabs.ts
│ │ │ │ ├── string-similarity.ts
│ │ │ │ └── history.ts
│ │ │ ├── base-provider.tsx
│ │ │ ├── types.ts
│ │ │ └── autocomplete-result.ts
│ │ │ ├── merge-refs.ts
│ │ │ ├── merge-button-refs.ts
│ │ │ ├── search.ts
│ │ │ └── phosphor-icons.tsx
│ └── public
│ │ ├── assets
│ │ ├── icon.png
│ │ └── favicon.ico
│ │ ├── chrome-dino-game
│ │ ├── favicon.ico
│ │ ├── icons
│ │ │ ├── icon-114.png
│ │ │ ├── icon-128.png
│ │ │ ├── icon-16.png
│ │ │ ├── icon-256.png
│ │ │ └── icon-32.png
│ │ ├── images
│ │ │ ├── default_100_percent
│ │ │ │ └── offline
│ │ │ │ │ ├── 100-disabled.png
│ │ │ │ │ ├── 100-error-offline.png
│ │ │ │ │ ├── 100-offline-sprite.png
│ │ │ │ │ ├── 100-olympics-surfing-sprite.png
│ │ │ │ │ ├── 100-olympic-firemedal-sprite.png
│ │ │ │ │ ├── 100-olympics-equestrian-sprite.png
│ │ │ │ │ ├── 100-olympics-gymnastics-sprite.png
│ │ │ │ │ ├── 100-olympics-hurdling-sprite.png
│ │ │ │ │ └── 100-olympics-swimming-sprite.png
│ │ │ └── default_200_percent
│ │ │ │ └── offline
│ │ │ │ ├── 200-disabled.png
│ │ │ │ ├── 200-error-offline.png
│ │ │ │ ├── 200-offline-sprite.png
│ │ │ │ ├── 200-olympics-surfing-sprite.png
│ │ │ │ ├── 200-olympic-firemedal-sprite.png
│ │ │ │ ├── 200-olympics-equestrian-sprite.png
│ │ │ │ ├── 200-olympics-gymnastics-sprite.png
│ │ │ │ ├── 200-olympics-hurdling-sprite.png
│ │ │ │ └── 200-olympics-swimming-sprite.png
│ │ ├── scripts
│ │ │ ├── strings.js
│ │ │ └── neterror.slim.js
│ │ └── appmanifest.json
│ │ ├── edge-surf-game-v2
│ │ ├── favicon.png
│ │ ├── icons
│ │ │ ├── icon-114.png
│ │ │ ├── icon-128.png
│ │ │ ├── icon-16.png
│ │ │ ├── icon-256.png
│ │ │ └── icon-32.png
│ │ ├── js
│ │ │ └── assert.js
│ │ ├── surf-iframe.html
│ │ └── index.html
│ │ └── edge-surf-game-v1
│ │ └── resources
│ │ ├── ski
│ │ ├── bg.png
│ │ ├── objects.png
│ │ └── player.png
│ │ ├── surf
│ │ ├── bg.png
│ │ ├── player.png
│ │ └── objects.png
│ │ ├── icons
│ │ ├── favicon.png
│ │ ├── apple-icon.png
│ │ ├── ms-icon-70x70.png
│ │ ├── ms-icon-144x144.png
│ │ ├── ms-icon-150x150.png
│ │ ├── ms-icon-310x310.png
│ │ ├── android-icon-36x36.png
│ │ ├── android-icon-48x48.png
│ │ ├── android-icon-72x72.png
│ │ ├── android-icon-96x96.png
│ │ ├── apple-icon-114x114.png
│ │ ├── apple-icon-120x120.png
│ │ ├── apple-icon-144x144.png
│ │ ├── apple-icon-152x152.png
│ │ ├── apple-icon-180x180.png
│ │ ├── apple-icon-57x57.png
│ │ ├── apple-icon-60x60.png
│ │ ├── apple-icon-72x72.png
│ │ ├── apple-icon-76x76.png
│ │ ├── android-icon-144x144.png
│ │ ├── android-icon-192x192.png
│ │ └── apple-icon-precomposed.png
│ │ └── js
│ │ ├── surf-error-reporting.js
│ │ └── base-error-reporting.js
├── main
│ ├── controllers
│ │ ├── sessions-controller
│ │ │ ├── protocols
│ │ │ │ ├── types.ts
│ │ │ │ ├── static-domains
│ │ │ │ │ └── types.ts
│ │ │ │ ├── utils.ts
│ │ │ │ ├── _protocols
│ │ │ │ │ ├── flow
│ │ │ │ │ │ ├── pdf-cache.ts
│ │ │ │ │ │ ├── favicon.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── assets.ts
│ │ │ │ │ │ └── extension-icon.ts
│ │ │ │ │ ├── flow-external
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── flow-internal
│ │ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ ├── preload-scripts
│ │ │ │ └── index.ts
│ │ │ ├── intercept-rules
│ │ │ │ ├── index.ts
│ │ │ │ ├── user-agent-transformer.ts
│ │ │ │ └── cors-bypass-custom-protocols.ts
│ │ │ ├── README.md
│ │ │ └── default-session
│ │ │ │ └── index.ts
│ │ ├── quit-controller
│ │ │ ├── handlers
│ │ │ │ ├── can-quit.ts
│ │ │ │ └── before-quit.ts
│ │ │ ├── README.md
│ │ │ └── index.ts
│ │ ├── app-menu-controller
│ │ │ ├── menu
│ │ │ │ ├── items
│ │ │ │ │ ├── window.ts
│ │ │ │ │ ├── edit.ts
│ │ │ │ │ ├── file.ts
│ │ │ │ │ ├── app.ts
│ │ │ │ │ └── archive.ts
│ │ │ │ └── helpers.ts
│ │ │ └── index.ts
│ │ ├── tabs-controller
│ │ │ └── tab-groups
│ │ │ │ ├── split.ts
│ │ │ │ └── glance.ts
│ │ ├── windows-controller
│ │ │ ├── types
│ │ │ │ ├── extension-popup.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── onboarding.ts
│ │ │ │ └── settings.ts
│ │ │ ├── README.md
│ │ │ ├── interfaces
│ │ │ │ ├── settings.ts
│ │ │ │ └── onboarding.ts
│ │ │ └── utils
│ │ │ │ └── close-preventer.ts
│ │ ├── posthog-controller
│ │ │ ├── posthog-error-capture-sdk
│ │ │ │ ├── README.md
│ │ │ │ ├── reduceable-cache.ts
│ │ │ │ └── type-checking.ts
│ │ │ └── identify.ts
│ │ ├── index.ts
│ │ └── default-browser-controller
│ │ │ └── index.ts
│ ├── modules
│ │ ├── types.ts
│ │ ├── electron-components.ts
│ │ ├── pdf-cache.ts
│ │ ├── paths.ts
│ │ ├── flags.ts
│ │ ├── user-agent.ts
│ │ ├── logs.ts
│ │ ├── output.ts
│ │ └── extensions
│ │ │ └── main.ts
│ ├── ipc
│ │ ├── app
│ │ │ ├── onboarding.ts
│ │ │ ├── actions.ts
│ │ │ ├── open-external.ts
│ │ │ ├── icons.ts
│ │ │ ├── app.ts
│ │ │ ├── shortcuts.ts
│ │ │ ├── updates.ts
│ │ │ └── new-tab.ts
│ │ ├── browser
│ │ │ ├── browser.ts
│ │ │ └── page.ts
│ │ ├── index.ts
│ │ ├── window
│ │ │ ├── settings.ts
│ │ │ └── omnibox.ts
│ │ ├── session
│ │ │ └── profiles.ts
│ │ └── README.md
│ ├── app
│ │ ├── instance.ts
│ │ ├── onboarding.ts
│ │ ├── lifecycle.ts
│ │ ├── platform.ts
│ │ └── urls.ts
│ ├── browser.ts
│ ├── saving
│ │ └── onboarding.ts
│ └── index.ts
└── shared
│ ├── flow
│ ├── interfaces
│ │ ├── browser
│ │ │ ├── newTab.ts
│ │ │ ├── omnibox.ts
│ │ │ ├── page.ts
│ │ │ ├── browser.ts
│ │ │ └── navigation.ts
│ │ ├── settings
│ │ │ ├── onboarding.ts
│ │ │ ├── openExternal.ts
│ │ │ ├── icons.ts
│ │ │ └── settings.ts
│ │ ├── app
│ │ │ ├── windows.ts
│ │ │ ├── actions.ts
│ │ │ ├── shortcuts.ts
│ │ │ ├── app.ts
│ │ │ ├── extensions.ts
│ │ │ └── updates.ts
│ │ └── sessions
│ │ │ ├── profiles.ts
│ │ │ └── spaces.ts
│ └── types.ts
│ └── types
│ ├── updates.ts
│ ├── shortcuts.ts
│ ├── extensions.ts
│ ├── settings.ts
│ └── tabs.ts
├── .vscode
├── extensions.json
├── settings.json
└── launch.json
├── .gitignore
├── .github
├── CODEOWNERS
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── .prettierrc
├── assets
└── public
│ └── icons
│ ├── 3d.png
│ ├── aquatic.png
│ ├── default.png
│ ├── digital.png
│ ├── dynamic.png
│ ├── glowy.png
│ ├── nature.png
│ ├── retro.png
│ ├── summer.png
│ ├── vibrant.png
│ ├── darkness.png
│ ├── galactic.png
│ ├── futuristic.png
│ └── minimal_flat.png
├── .prettierignore
├── docs
├── assets
│ └── screenshots
│ │ ├── beta-newtab-1.png
│ │ ├── beta-browser-1.png
│ │ ├── beta-command-1.png
│ │ ├── beta-settings-1.png
│ │ └── beta-onboarding-1.png
├── references
│ ├── tabs.md
│ ├── window-border.md
│ ├── view-indexes.md
│ ├── logs.md
│ └── omnibox-scores.md
├── api
│ └── extensions
│ │ └── index.md
└── contributing
│ ├── hot-reloading.md
│ └── updating-electron.md
├── scripts
├── temp-change-name
│ ├── revert.ts
│ ├── set.ts
│ └── module.ts
├── frontend-routes-generator
│ ├── common.ts
│ └── pruner.ts
└── electron-upgrader
│ ├── current.ts
│ └── next.ts
├── tsconfig.json
├── .editorconfig
├── tsconfig.scripts.json
├── tsconfig.node.json
├── tsconfig.web.json
├── components.json
├── eslint.config.mjs
└── electron.vite.config.ts
/.nvmrc:
--------------------------------------------------------------------------------
1 | v22
2 |
--------------------------------------------------------------------------------
/.clinerules:
--------------------------------------------------------------------------------
1 | .cursor/rules
--------------------------------------------------------------------------------
/src/renderer/.gitignore:
--------------------------------------------------------------------------------
1 | route-*.html
2 | src/routes/*/main.tsx
--------------------------------------------------------------------------------
/src/renderer/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | out
4 | .DS_Store
5 | .eslintcache
6 | *.log*
7 | .env
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | ## CORE TEAM ##
2 | # The creator and maintainer of the project.
3 | * @iamEvanYT
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "singleQuote": false,
4 | "trailingComma": "none"
5 | }
6 |
--------------------------------------------------------------------------------
/assets/public/icons/3d.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/3d.png
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | out
2 | dist
3 | bun.lock
4 | LICENSE.md
5 | tsconfig.json
6 | tsconfig.*.json
7 | src/renderer/public/*
--------------------------------------------------------------------------------
/assets/public/icons/aquatic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/aquatic.png
--------------------------------------------------------------------------------
/assets/public/icons/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/default.png
--------------------------------------------------------------------------------
/assets/public/icons/digital.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/digital.png
--------------------------------------------------------------------------------
/assets/public/icons/dynamic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/dynamic.png
--------------------------------------------------------------------------------
/assets/public/icons/glowy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/glowy.png
--------------------------------------------------------------------------------
/assets/public/icons/nature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/nature.png
--------------------------------------------------------------------------------
/assets/public/icons/retro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/retro.png
--------------------------------------------------------------------------------
/assets/public/icons/summer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/summer.png
--------------------------------------------------------------------------------
/assets/public/icons/vibrant.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/vibrant.png
--------------------------------------------------------------------------------
/assets/public/icons/darkness.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/darkness.png
--------------------------------------------------------------------------------
/assets/public/icons/galactic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/galactic.png
--------------------------------------------------------------------------------
/assets/public/icons/futuristic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/futuristic.png
--------------------------------------------------------------------------------
/assets/public/icons/minimal_flat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/assets/public/icons/minimal_flat.png
--------------------------------------------------------------------------------
/src/renderer/public/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/assets/icon.png
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/protocols/types.ts:
--------------------------------------------------------------------------------
1 | export type CustomProtocol = "flow" | "flow-internal" | "flow-external";
2 |
--------------------------------------------------------------------------------
/src/renderer/public/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/assets/favicon.ico
--------------------------------------------------------------------------------
/docs/assets/screenshots/beta-newtab-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/docs/assets/screenshots/beta-newtab-1.png
--------------------------------------------------------------------------------
/src/main/modules/types.ts:
--------------------------------------------------------------------------------
1 | export type MinimalEvent = {
2 | preventDefault: () => void;
3 | readonly defaultPrevented: boolean;
4 | };
5 |
--------------------------------------------------------------------------------
/docs/assets/screenshots/beta-browser-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/docs/assets/screenshots/beta-browser-1.png
--------------------------------------------------------------------------------
/docs/assets/screenshots/beta-command-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/docs/assets/screenshots/beta-command-1.png
--------------------------------------------------------------------------------
/docs/assets/screenshots/beta-settings-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/docs/assets/screenshots/beta-settings-1.png
--------------------------------------------------------------------------------
/docs/references/tabs.md:
--------------------------------------------------------------------------------
1 | ## Active Tab Modes
2 |
3 | - **Standard**: 1 standard
4 | - **Glance**: 1 Front & 1 Back
5 | - **Split**: Split Tabs
6 |
--------------------------------------------------------------------------------
/docs/assets/screenshots/beta-onboarding-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/docs/assets/screenshots/beta-onboarding-1.png
--------------------------------------------------------------------------------
/docs/references/window-border.md:
--------------------------------------------------------------------------------
1 | # Window Border Radius
2 |
3 | - 4px
4 | - 8px in setBorderRadius()
5 | - 8px padding in tab (browser-ui/main.tsx)
6 |
--------------------------------------------------------------------------------
/scripts/temp-change-name/revert.ts:
--------------------------------------------------------------------------------
1 | import { revertName } from "./module";
2 |
3 | // Revert the app name using the module function
4 | revertName();
5 |
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/favicon.ico
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v2/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v2/favicon.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/icons/icon-114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/icons/icon-114.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/icons/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/icons/icon-128.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/icons/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/icons/icon-16.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/icons/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/icons/icon-256.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/icons/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/icons/icon-32.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v2/icons/icon-114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v2/icons/icon-114.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v2/icons/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v2/icons/icon-128.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v2/icons/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v2/icons/icon-16.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v2/icons/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v2/icons/icon-256.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v2/icons/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v2/icons/icon-32.png
--------------------------------------------------------------------------------
/src/main/controllers/quit-controller/handlers/can-quit.ts:
--------------------------------------------------------------------------------
1 | // Insert Logic here to check if the app can quit
2 | export function canQuit(): boolean {
3 | return true;
4 | }
5 |
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/ski/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/ski/bg.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }, { "path": "./tsconfig.scripts.json" }]
4 | }
5 |
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/surf/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/surf/bg.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/ski/objects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/ski/objects.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/ski/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/ski/player.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/surf/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/surf/player.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/favicon.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/surf/objects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/surf/objects.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon.png
--------------------------------------------------------------------------------
/src/renderer/src/routes/omnibox/page.tsx:
--------------------------------------------------------------------------------
1 | import { OmniboxMain } from "@/components/omnibox/main";
2 |
3 | function Page() {
4 | return ;
5 | }
6 |
7 | export default Page;
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/ms-icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/ms-icon-70x70.png
--------------------------------------------------------------------------------
/src/renderer/src/types/routes.ts:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, ReactNode } from "react";
2 |
3 | export interface RouteConfigType {
4 | Providers: FunctionComponent<{ children: ReactNode }>;
5 | }
6 |
--------------------------------------------------------------------------------
/docs/references/view-indexes.md:
--------------------------------------------------------------------------------
1 | # View Indexes
2 |
3 | - 999 - Omnibox
4 | - 4 - Browser Overlay Components
5 | - 3 - Standard Browser Components
6 | - 2 - Tabs
7 | - 1 - UNUSED
8 | - 0 - Glance Back Tab
9 |
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/ms-icon-144x144.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/ms-icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/ms-icon-150x150.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/ms-icon-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/ms-icon-310x310.png
--------------------------------------------------------------------------------
/src/renderer/src/routes/main-ui/page.tsx:
--------------------------------------------------------------------------------
1 | import { BrowserUI } from "@/components/browser-ui/main";
2 |
3 | function Page() {
4 | return ;
5 | }
6 |
7 | export default Page;
8 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/popup-ui/page.tsx:
--------------------------------------------------------------------------------
1 | import { BrowserUI } from "@/components/browser-ui/main";
2 |
3 | function Page() {
4 | return ;
5 | }
6 |
7 | export default Page;
8 |
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/android-icon-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/android-icon-36x36.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/android-icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/android-icon-48x48.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/android-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/android-icon-72x72.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/android-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/android-icon-96x96.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-114x114.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-120x120.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-144x144.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-152x152.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-180x180.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-57x57.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-60x60.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-72x72.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-76x76.png
--------------------------------------------------------------------------------
/src/renderer/src/components/react-icons/README.md:
--------------------------------------------------------------------------------
1 | This is a stripped down version of the `react-icons` library.
2 | I used this instead as the `react-icons` library is 80MB in size and I only need a few icons.
3 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { SettingsLayout } from "@/components/settings/settings-layout";
2 |
3 | function Page() {
4 | return ;
5 | }
6 |
7 | export default Page;
8 |
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/android-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/android-icon-144x144.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/android-icon-192x192.png
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/edge-surf-game-v1/resources/icons/apple-icon-precomposed.png
--------------------------------------------------------------------------------
/src/renderer/src/hooks/use-force-update.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export function useForceUpdate() {
4 | const [, setState] = useState(0);
5 | return () => setState((state) => state + 1);
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/controllers/app-menu-controller/menu/items/window.ts:
--------------------------------------------------------------------------------
1 | import { MenuItemConstructorOptions } from "electron";
2 |
3 | export const createWindowMenu = (): MenuItemConstructorOptions => ({
4 | role: "windowMenu"
5 | });
6 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/browser/newTab.ts:
--------------------------------------------------------------------------------
1 | export type NewTabMode = "omnibox" | "tab";
2 |
3 | // API //
4 | export interface FlowNewTabAPI {
5 | /**
6 | * Opens a new tab
7 | */
8 | open: () => void;
9 | }
10 |
--------------------------------------------------------------------------------
/docs/references/logs.md:
--------------------------------------------------------------------------------
1 | # Logs from Flow
2 |
3 | Logs can be found in these directories:
4 |
5 | - on Linux: `~/.config/Flow/logs/`
6 | - on macOS: `~/Library/Logs/Flow/`
7 | - on Windows: `%USERPROFILE%\AppData\Roaming\Flow\logs\`
8 |
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-disabled.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-disabled.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-error-offline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-error-offline.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-error-offline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-error-offline.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-offline-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-offline-sprite.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-offline-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-offline-sprite.png
--------------------------------------------------------------------------------
/src/main/controllers/tabs-controller/tab-groups/split.ts:
--------------------------------------------------------------------------------
1 | import { BaseTabGroup } from "./index";
2 |
3 | export class SplitTabGroup extends BaseTabGroup {
4 | public mode: "split" = "split" as const;
5 |
6 | // TODO: Implement split tab group layout
7 | }
8 |
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-olympics-surfing-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-olympics-surfing-sprite.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-olympics-surfing-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-olympics-surfing-sprite.png
--------------------------------------------------------------------------------
/src/shared/types/updates.ts:
--------------------------------------------------------------------------------
1 | import type { UpdateInfo, ProgressInfo } from "electron-updater";
2 |
3 | export interface UpdateStatus {
4 | availableUpdate: UpdateInfo | null;
5 | downloadProgress: ProgressInfo | null;
6 | updateDownloaded: boolean;
7 | }
8 |
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-olympic-firemedal-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-olympic-firemedal-sprite.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-olympics-equestrian-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-olympics-equestrian-sprite.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-olympics-gymnastics-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-olympics-gymnastics-sprite.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-olympics-hurdling-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-olympics-hurdling-sprite.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-olympics-swimming-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_100_percent/offline/100-olympics-swimming-sprite.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-olympic-firemedal-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-olympic-firemedal-sprite.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-olympics-equestrian-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-olympics-equestrian-sprite.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-olympics-gymnastics-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-olympics-gymnastics-sprite.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-olympics-hurdling-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-olympics-hurdling-sprite.png
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-olympics-swimming-sprite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MultiboxLabs/flow-browser/HEAD/src/renderer/public/chrome-dino-game/images/default_200_percent/offline/200-olympics-swimming-sprite.png
--------------------------------------------------------------------------------
/src/renderer/src/routes/pdf-viewer/pdf-viewer/README.md:
--------------------------------------------------------------------------------
1 | # PDF Viewer
2 |
3 | This PDF viewer is taken from [pdfslick examples](https://github.com/pdfslick/pdfslick/tree/35535e16991d2ca3f82673456af9a7da82d32aa8/apps/web/examples/PDFViewerApp) and modified to work with Flow.
4 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/Splitter.tsx:
--------------------------------------------------------------------------------
1 | const Splitter = () => {
2 | return (
3 |
4 | );
5 | };
6 |
7 | export default Splitter;
8 |
--------------------------------------------------------------------------------
/src/renderer/src/components/providers/nuqs-provider.tsx:
--------------------------------------------------------------------------------
1 | import { NuqsAdapter } from "nuqs/adapters/react";
2 | import type { ReactNode } from "react";
3 |
4 | export function NuqsProvider({ children }: { children: ReactNode }) {
5 | return {children};
6 | }
7 |
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/js/surf-error-reporting.js:
--------------------------------------------------------------------------------
1 | // Copyright (C) Microsoft Corporation. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | hookErrorReporting('webui-surf-game');
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/settings/onboarding.ts:
--------------------------------------------------------------------------------
1 | // API //
2 | export interface FlowOnboardingAPI {
3 | /**
4 | * Finishes the onboarding process
5 | */
6 | finish: () => void;
7 |
8 | /**
9 | * Resets the onboarding process
10 | */
11 | reset: () => void;
12 | }
13 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/app/windows.ts:
--------------------------------------------------------------------------------
1 | // API //
2 | export interface FlowWindowsAPI {
3 | /**
4 | * Opens the settings window
5 | */
6 | openSettingsWindow: () => void;
7 |
8 | /**
9 | * Closes the settings window
10 | */
11 | closeSettingsWindow: () => void;
12 | }
13 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4 | return ;
5 | }
6 |
7 | export { Skeleton };
8 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/new-tab/page.tsx:
--------------------------------------------------------------------------------
1 | import { NewTabPage } from "@/components/new-tab/main";
2 |
3 | function Page() {
4 | return ;
5 | }
6 |
7 | function App() {
8 | return (
9 | <>
10 | New Tab
11 |
12 | >
13 | );
14 | }
15 |
16 | export default App;
17 |
--------------------------------------------------------------------------------
/src/main/ipc/app/onboarding.ts:
--------------------------------------------------------------------------------
1 | import { setOnboardingCompleted, resetOnboarding } from "@/saving/onboarding";
2 | import { ipcMain } from "electron";
3 |
4 | ipcMain.on("onboarding:finish", () => {
5 | return setOnboardingCompleted();
6 | });
7 |
8 | ipcMain.on("onboarding:reset", () => {
9 | return resetOnboarding();
10 | });
11 |
--------------------------------------------------------------------------------
/src/renderer/src/components/settings/settings-titlebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export function SettingsTitlebar() {
4 | return (
5 |
6 | Flow Settings
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/shared/flow/types.ts:
--------------------------------------------------------------------------------
1 | export type PageBounds = {
2 | x: number;
3 | y: number;
4 | width: number;
5 | height: number;
6 | };
7 |
8 | export type WindowState = {
9 | isMaximized: boolean;
10 | isFullscreen: boolean;
11 | };
12 |
13 | export type IPCListener = (callback: (...data: T) => void) => () => void;
14 |
--------------------------------------------------------------------------------
/src/renderer/src/lib/omnibox/data-providers/open-tabs.ts:
--------------------------------------------------------------------------------
1 | export async function getOpenTabsInSpace() {
2 | const spaceId = await flow.spaces.getUsingSpace();
3 | if (!spaceId) return [];
4 |
5 | const tabsData = await flow.tabs.getData();
6 |
7 | const tabs = tabsData.tabs.filter((tab) => tab.spaceId === spaceId);
8 | return tabs;
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/preload-scripts/index.ts:
--------------------------------------------------------------------------------
1 | import { PATHS } from "@/modules/paths";
2 | import type { Session } from "electron";
3 |
4 | export function registerPreloadScripts(session: Session) {
5 | session.registerPreloadScript({
6 | id: "flow-preload",
7 | type: "frame",
8 | filePath: PATHS.PRELOAD
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[typescript]": {
3 | "editor.defaultFormatter": "esbenp.prettier-vscode"
4 | },
5 | "[javascript]": {
6 | "editor.defaultFormatter": "esbenp.prettier-vscode"
7 | },
8 | "[json]": {
9 | "editor.defaultFormatter": "esbenp.prettier-vscode"
10 | },
11 | "typescript.tsdk": "node_modules/typescript/lib"
12 | }
13 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/app/actions.ts:
--------------------------------------------------------------------------------
1 | import { IPCListener } from "~/flow/types";
2 |
3 | // API //
4 | export interface FlowActionsAPI {
5 | /**
6 | * Listen for copy link action
7 | */
8 | onCopyLink: IPCListener<[]>;
9 |
10 | /**
11 | * Listen for generic incoming actions
12 | */
13 | onIncomingAction: IPCListener<[string]>;
14 | }
15 |
--------------------------------------------------------------------------------
/src/shared/types/shortcuts.ts:
--------------------------------------------------------------------------------
1 | export interface ShortcutAction {
2 | id: string; // e.g., "tabs.newTab", "navigation.goBack"
3 | name: string; // e.g., "Open New Tab", "Go Back"
4 | shortcut: string; // e.g., "CommandOrControl+T", "Alt+Left"
5 | category: string; // e.g., "Tabs", "Navigation"
6 | originalShortcut?: string; // To store the initial default shortcut
7 | }
8 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/error/config.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "@/components/main/theme";
2 | import { RouteConfigType } from "@/types/routes";
3 | import { ReactNode } from "react";
4 |
5 | export const RouteConfig: RouteConfigType = {
6 | Providers: ({ children }: { children: ReactNode }) => {
7 | return {children};
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/omnibox/config.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "@/components/main/theme";
2 | import { RouteConfigType } from "@/types/routes";
3 | import { ReactNode } from "react";
4 |
5 | export const RouteConfig: RouteConfigType = {
6 | Providers: ({ children }: { children: ReactNode }) => {
7 | return {children};
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/settings/config.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "@/components/main/theme";
2 | import { RouteConfigType } from "@/types/routes";
3 | import { ReactNode } from "react";
4 |
5 | export const RouteConfig: RouteConfigType = {
6 | Providers: ({ children }: { children: ReactNode }) => {
7 | return {children};
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/omnibox-debug/config.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "@/components/main/theme";
2 | import { RouteConfigType } from "@/types/routes";
3 | import { ReactNode } from "react";
4 |
5 | export const RouteConfig: RouteConfigType = {
6 | Providers: ({ children }: { children: ReactNode }) => {
7 | return {children};
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/tsconfig.scripts.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
3 | "include": ["electron.vite.config.*", "scripts/**/*"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "moduleResolution": "bundler",
7 | "types": ["electron-vite/node"],
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["scripts/*"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/main-ui/config.tsx:
--------------------------------------------------------------------------------
1 | import { PortalsProvider } from "@/components/portal/provider";
2 | import { RouteConfigType } from "@/types/routes";
3 | import { ReactNode } from "react";
4 |
5 | export const RouteConfig: RouteConfigType = {
6 | Providers: ({ children }: { children: ReactNode }) => {
7 | return {children};
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/new-tab/config.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "@/components/main/theme";
2 | import { RouteConfigType } from "@/types/routes";
3 | import { ReactNode } from "react";
4 |
5 | export const RouteConfig: RouteConfigType = {
6 | Providers: ({ children }: { children: ReactNode }) => {
7 | return {children};
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/popup-ui/config.tsx:
--------------------------------------------------------------------------------
1 | import { PortalsProvider } from "@/components/portal/provider";
2 | import { RouteConfigType } from "@/types/routes";
3 | import { ReactNode } from "react";
4 |
5 | export const RouteConfig: RouteConfigType = {
6 | Providers: ({ children }: { children: ReactNode }) => {
7 | return {children};
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/about/config.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "@/components/main/theme";
2 | import { RouteConfigType } from "@/types/routes";
3 | import { ReactNode } from "react";
4 |
5 | export const RouteConfig: RouteConfigType = {
6 | Providers: ({ children }: { children: ReactNode }) => {
7 | return {children};
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/games/config.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "@/components/main/theme";
2 | import { RouteConfigType } from "@/types/routes";
3 | import { ReactNode } from "react";
4 |
5 | export const RouteConfig: RouteConfigType = {
6 | Providers: ({ children }: { children: ReactNode }) => {
7 | return {children};
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/onboarding/config.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "@/components/main/theme";
2 | import { RouteConfigType } from "@/types/routes";
3 | import { ReactNode } from "react";
4 |
5 | export const RouteConfig: RouteConfigType = {
6 | Providers: ({ children }: { children: ReactNode }) => {
7 | return {children};
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/src/renderer/src/lib/omnibox/data-providers/string-similarity.ts:
--------------------------------------------------------------------------------
1 | import { stringSimilarity } from "string-similarity-js";
2 |
3 | const SIMILARITY_THRESHOLD = 0.4;
4 |
5 | export function getStringSimilarity(str1: string, str2: string): number {
6 | const similarity = stringSimilarity(str1, str2);
7 | if (similarity >= SIMILARITY_THRESHOLD) {
8 | return similarity;
9 | }
10 | return 0;
11 | }
12 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/browser/omnibox.ts:
--------------------------------------------------------------------------------
1 | import { PageBounds } from "~/flow/types";
2 |
3 | type QueryParams = { [key: string]: string };
4 |
5 | // API //
6 | export interface FlowOmniboxAPI {
7 | /**
8 | * Shows the omnibox
9 | */
10 | show: (bounds: PageBounds | null, params: QueryParams | null) => void;
11 |
12 | /**
13 | * Hides the omnibox
14 | */
15 | hide: () => void;
16 | }
17 |
--------------------------------------------------------------------------------
/src/renderer/src/lib/merge-refs.ts:
--------------------------------------------------------------------------------
1 | export function mergeRefs(refs: Array | undefined>): React.RefCallback {
2 | return (value) => {
3 | for (const ref of refs) {
4 | if (typeof ref === "function") {
5 | ref(value);
6 | } else if (ref != null) {
7 | (ref as React.MutableRefObject).current = value;
8 | }
9 | }
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/src/renderer/src/components/onboarding/stages/onboarding-drag-disabler.tsx:
--------------------------------------------------------------------------------
1 | export function OnboardingDragDisabler() {
2 | // Enable drag for Topbar
3 | // Disable drag for rest of the window
4 | return (
5 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/ipc/app/actions.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow } from "@/controllers/windows-controller/types";
2 | import { sendMessageToListeners } from "@/ipc/listeners-manager";
3 |
4 | export async function fireCopyLinkAction(win: BrowserWindow) {
5 | win.sendMessage("actions:on-copy-link");
6 | }
7 |
8 | export async function fireFrontendAction(action: string) {
9 | sendMessageToListeners("actions:on-incoming", action);
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/ipc/app/open-external.ts:
--------------------------------------------------------------------------------
1 | import { getAlwaysOpenExternal, unsetAlwaysOpenExternal } from "@/saving/open-external";
2 | import { ipcMain } from "electron";
3 |
4 | ipcMain.handle("open-external:get", () => {
5 | return getAlwaysOpenExternal();
6 | });
7 |
8 | ipcMain.handle("open-external:unset", (_, requestingURL: string, openingURL: string) => {
9 | return unsetAlwaysOpenExternal(requestingURL, openingURL);
10 | });
11 |
--------------------------------------------------------------------------------
/src/main/controllers/windows-controller/types/extension-popup.ts:
--------------------------------------------------------------------------------
1 | import { BaseWindow } from "@/controllers/windows-controller/types/base";
2 | import { BrowserWindow } from "electron";
3 |
4 | export class ExtensionPopupWindow extends BaseWindow {
5 | constructor(browserWindow: BrowserWindow) {
6 | // Uses the browserWindow that was already created by the extension handler
7 | super("extension-popup", browserWindow);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/controllers/posthog-controller/posthog-error-capture-sdk/README.md:
--------------------------------------------------------------------------------
1 | # PostHog Error Capture
2 |
3 | This is taken from a directory in [PostHog/posthog-js-lite](https://github.com/PostHog/posthog-js-lite/tree/6f67f8a3e9724479011173f7b0fc0c190806da2b/posthog-node/src/extensions/error-tracking) and forked.
4 |
5 | ## Modifications
6 |
7 | - Added a fallback distinct ID to use when no distinct ID is provided.
8 | - Use fallback distinct ID for autocapture.
9 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/onboarding/page.tsx:
--------------------------------------------------------------------------------
1 | import { OnboardingMain } from "@/components/onboarding/main";
2 | import { SettingsProvider } from "@/components/providers/settings-provider";
3 |
4 | function Page() {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
12 | function App() {
13 | return (
14 | <>
15 |
16 | >
17 | );
18 | }
19 | export default App;
20 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
3 | "include": ["src/main/**/*", "src/preload/**/*", "src/shared/**/*"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "moduleResolution": "bundler",
7 | "types": ["electron-vite/node"],
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": [
11 | "src/main/*"
12 | ],
13 | "~/*": ["src/shared/*"]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/docs/api/extensions/index.md:
--------------------------------------------------------------------------------
1 | # Extensions API
2 |
3 | The Extensions API provides functionalities for working with browser extensions in Flow Browser. This document serves as an index to the available extension-related APIs.
4 |
5 | ## Available APIs
6 |
7 | - [Permission Warnings](./permission-warnings.md): Converts extension manifest permissions into user-facing warning messages
8 | - [Locales](./locales.md): Handles internationalization and localization for extensions
9 |
--------------------------------------------------------------------------------
/src/renderer/src/components/logic/tab-disabler.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | export function TabDisabler() {
4 | useEffect(() => {
5 | const onKeyDown = (e: KeyboardEvent) => {
6 | if (e.key === "Tab") {
7 | e.preventDefault();
8 | }
9 | };
10 | window.addEventListener("keydown", onKeyDown);
11 | return () => window.removeEventListener("keydown", onKeyDown);
12 | }, []);
13 |
14 | return <>>;
15 | }
16 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/browser/page.ts:
--------------------------------------------------------------------------------
1 | import { PageBounds } from "~/flow/types";
2 |
3 | // API //
4 | export interface FlowPageAPI {
5 | /**
6 | * Sets the bounds of the page content
7 | * Similar to setTabBounds but specifically for the page content area
8 | * This can only be called from the Browser UI
9 | * @param bounds The bounds object containing position and dimensions
10 | */
11 | setPageBounds: (bounds: PageBounds) => void;
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/controllers/windows-controller/types/index.ts:
--------------------------------------------------------------------------------
1 | export { BaseWindow } from "@/controllers/windows-controller/types/base";
2 |
3 | export { SettingsWindow } from "@/controllers/windows-controller/types/settings";
4 | export { OnboardingWindow } from "@/controllers/windows-controller/types/onboarding";
5 | export { BrowserWindow } from "@/controllers/windows-controller/types/browser";
6 | export { ExtensionPopupWindow } from "@/controllers/windows-controller/types/extension-popup";
7 |
--------------------------------------------------------------------------------
/src/main/modules/electron-components.ts:
--------------------------------------------------------------------------------
1 | export async function waitForElectronComponentsToBeReady() {
2 | const electron = await import("electron");
3 |
4 | if ("components" in electron) {
5 | const { components } = electron;
6 |
7 | // @ts-ignore: This is defined in Widevine Electron, but not in the stock Electron
8 | return components
9 | .whenReady()
10 | .then(() => true)
11 | .catch(() => false);
12 | }
13 |
14 | return Promise.resolve(false);
15 | }
16 |
--------------------------------------------------------------------------------
/src/renderer/src/hooks/use-debounce.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useEffect } from "react";
3 |
4 | export function useDebounce(value: T, delay: number): T {
5 | const [debouncedValue, setDebouncedValue] = useState(value);
6 |
7 | useEffect(() => {
8 | const timer = setTimeout(() => {
9 | setDebouncedValue(value);
10 | }, delay);
11 |
12 | return () => {
13 | clearTimeout(timer);
14 | };
15 | }, [value, delay]);
16 |
17 | return debouncedValue;
18 | }
19 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/browser/browser.ts:
--------------------------------------------------------------------------------
1 | // API //
2 | export interface FlowBrowserAPI {
3 | /**
4 | * Loads a profile
5 | * @param profileId The id of the profile to load
6 | */
7 | loadProfile: (profileId: string) => Promise;
8 |
9 | /**
10 | * Unloads a profile
11 | * @param profileId The id of the profile to unload
12 | */
13 | unloadProfile: (profileId: string) => Promise;
14 |
15 | /**
16 | * Creates a new window
17 | */
18 | createWindow: () => void;
19 | }
20 |
--------------------------------------------------------------------------------
/scripts/frontend-routes-generator/common.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import fs from "fs/promises";
3 |
4 | // Constants //
5 | export const FRONTEND_PATH = path.resolve(__dirname, "..", "..", "src", "renderer");
6 | export const ROUTES_PATH = path.join(FRONTEND_PATH, "src/routes");
7 |
8 | // Helper Functions //
9 | export const getDirectories = async (source: string) =>
10 | (await fs.readdir(source, { withFileTypes: true }))
11 | .filter((dirent) => dirent.isDirectory())
12 | .map((dirent) => dirent.name);
13 |
--------------------------------------------------------------------------------
/scripts/temp-change-name/set.ts:
--------------------------------------------------------------------------------
1 | import { changeName, revertName } from "./module";
2 |
3 | const args = process.argv;
4 |
5 | // Grab Temp App Name
6 | const nameIndex = args.findIndex((val) => val === "--name");
7 | if (nameIndex === -1) {
8 | console.error("No name provided");
9 | process.exit(1);
10 | }
11 |
12 | const newName = args[nameIndex + 1];
13 |
14 | // Revert the app name before setting the new name
15 | revertName();
16 |
17 | // Change the app name using the module function
18 | changeName(newName);
19 |
--------------------------------------------------------------------------------
/src/renderer/src/components/analytics/umami.tsx:
--------------------------------------------------------------------------------
1 | export const UmamiScriptLoader = () => {
2 | if (process.env.NODE_ENV !== "production") return null;
3 |
4 | // Umami Analytics
5 | // Very simple, does not collect or store personal data
6 | // Does not log what websites you visit, or anything similar
7 | return (
8 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/shared/types/extensions.ts:
--------------------------------------------------------------------------------
1 | type ExtensionInspectView = "service_worker" | "background";
2 |
3 | export type ExtensionType = "unpacked" | "crx";
4 |
5 | export interface SharedExtensionData {
6 | type: ExtensionType;
7 | id: string;
8 | name: string;
9 | short_name?: string;
10 | description?: string;
11 | icon: string;
12 | enabled: boolean;
13 | pinned: boolean;
14 | version: string;
15 | path: string;
16 | size: number;
17 | permissions: string[];
18 | inspectViews: ExtensionInspectView[];
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.web.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
3 | "include": [
4 | "src/renderer/src/env.d.ts",
5 | "src/renderer/src/**/*",
6 | "src/renderer/src/**/*.tsx",
7 | "src/preload/*.d.ts",
8 | "src/shared/**/*"
9 | ],
10 | "compilerOptions": {
11 | "composite": true,
12 | "jsx": "react-jsx",
13 | "baseUrl": ".",
14 | "paths": {
15 | "@/*": [
16 | "src/renderer/src/*"
17 | ],
18 | "~/*": ["src/shared/*"]
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/renderer/src/index.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/pdf-viewer/page.tsx:
--------------------------------------------------------------------------------
1 | import { useQueryState } from "nuqs";
2 | import { PDFViewerApp } from "./pdf-viewer";
3 |
4 | import "@pdfslick/react/dist/pdf_viewer.css";
5 |
6 | function Page() {
7 | const [url] = useQueryState("url");
8 | const [cacheURL] = useQueryState("cacheURL");
9 | if (!url) {
10 | return null;
11 | }
12 |
13 | return (
14 | <>
15 | {url}
16 |
17 | >
18 | );
19 | }
20 |
21 | export default Page;
22 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/protocols/static-domains/types.ts:
--------------------------------------------------------------------------------
1 | import type { CustomProtocol } from "../types";
2 |
3 | type SubdirectoryActualDomainInfo = {
4 | type: "subdirectory";
5 | subdirectory: string;
6 | };
7 | type RoutedActualDomainInfo = {
8 | type: "route";
9 | route: string;
10 | };
11 |
12 | type ActualDomainInfo = SubdirectoryActualDomainInfo | RoutedActualDomainInfo;
13 |
14 | export type StaticDomainInfo = {
15 | protocol: CustomProtocol;
16 | hostname: string;
17 | actual: ActualDomainInfo;
18 | };
19 |
--------------------------------------------------------------------------------
/src/main/modules/pdf-cache.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * PDF Cache
3 | * This module is used to cache PDF responses for the custom PDF viewer
4 | */
5 |
6 | const PDF_CACHE = new Map();
7 |
8 | export function addPdfResponseToCache(key: string, response: Response) {
9 | PDF_CACHE.set(key, response);
10 | }
11 |
12 | export function getPdfResponseFromCache(key: string): Response | undefined {
13 | return PDF_CACHE.get(key);
14 | }
15 |
16 | export function removePdfResponseFromCache(key: string) {
17 | PDF_CACHE.delete(key);
18 | }
19 |
--------------------------------------------------------------------------------
/src/renderer/src/components/settings/sections/general/section.tsx:
--------------------------------------------------------------------------------
1 | import { BasicSettingsCards } from "@/components/settings/sections/general/basic-settings-cards";
2 |
3 | export function GeneralSettings() {
4 | return (
5 |
6 |
7 |
General
8 |
{"Manage your browser's general settings"}
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/extensions/config.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "@/components/main/theme";
2 | import { NuqsProvider } from "@/components/providers/nuqs-provider";
3 | import { RouteConfigType } from "@/types/routes";
4 | import { ReactNode } from "react";
5 |
6 | export const RouteConfig: RouteConfigType = {
7 | Providers: ({ children }: { children: ReactNode }) => {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/settings/openExternal.ts:
--------------------------------------------------------------------------------
1 | export interface ExternalAppPermission {
2 | requestingURL: string;
3 | openingProtocol: string;
4 | }
5 |
6 | // API //
7 | export interface FlowOpenExternalAPI {
8 | /**
9 | * Gets the list of always open external applications
10 | */
11 | getAlwaysOpenExternal: () => Promise;
12 |
13 | /**
14 | * Unsets an always open external application
15 | */
16 | unsetAlwaysOpenExternal: (requestingURL: string, openingURL: string) => Promise;
17 | }
18 |
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v2/js/assert.js:
--------------------------------------------------------------------------------
1 | // Copyright 2022 The Chromium Authors
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 | export function assert(value,message){if(value){return}throw new Error("Assertion failed"+(message?`: ${message}`:""))}export function assertInstanceof(value,type,message){if(value instanceof type){return}throw new Error(message||`Value ${value} is not of type ${type.name||typeof type}`)}export function assertNotReached(message="Unreachable code hit"){assert(false,message)}
--------------------------------------------------------------------------------
/src/renderer/src/components/react-icons/iconContext.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export interface IconContext {
4 | color?: string;
5 | size?: string;
6 | className?: string;
7 | style?: React.CSSProperties;
8 | attr?: React.SVGAttributes;
9 | }
10 |
11 | export const DefaultContext: IconContext = {
12 | color: undefined,
13 | size: undefined,
14 | className: undefined,
15 | style: undefined,
16 | attr: undefined
17 | };
18 |
19 | export const IconContext: React.Context = React.createContext && React.createContext(DefaultContext);
20 |
--------------------------------------------------------------------------------
/src/main/controllers/quit-controller/README.md:
--------------------------------------------------------------------------------
1 | # QuitController
2 |
3 | This creates control over the quitting behaviour of the application by letting you run callbacks before quitting.
4 |
5 | ## Components
6 |
7 | - `./index.ts` - Main interface exposed to check quit status and handle callbacks internally
8 | - `./handlers` - Stores the callbacks that should be ran before quitting
9 |
10 | ## External Usage
11 |
12 | - The only thing that should be required outside of this controller is the `index.ts` file.
13 | - All other files are internal and should not be used outside of this controller.
14 |
--------------------------------------------------------------------------------
/src/main/ipc/app/icons.ts:
--------------------------------------------------------------------------------
1 | import { icons, setCurrentIconId, supportedPlatforms, type IconId, getCurrentIconId } from "@/modules/icons";
2 | import { ipcMain } from "electron";
3 |
4 | ipcMain.handle("icons:get-all", () => {
5 | return icons;
6 | });
7 |
8 | ipcMain.handle("icons:is-platform-supported", () => {
9 | return supportedPlatforms.includes(process.platform);
10 | });
11 |
12 | ipcMain.handle("icons:get-current-icon-id", () => {
13 | return getCurrentIconId();
14 | });
15 |
16 | ipcMain.handle("icons:set-current-icon-id", (_, iconId: IconId) => {
17 | return setCurrentIconId(iconId);
18 | });
19 |
--------------------------------------------------------------------------------
/src/main/modules/paths.ts:
--------------------------------------------------------------------------------
1 | import { app } from "electron";
2 | import path from "path";
3 |
4 | // Constants
5 | const OUT_DIR = path.join(__dirname, "..");
6 | const ROOT_DIR = path.join(OUT_DIR, "..");
7 |
8 | // Paths
9 | interface Paths {
10 | ROOT: string;
11 | PRELOAD: string;
12 | VITE_WEBUI: string;
13 | ASSETS: string;
14 | }
15 |
16 | export const FLOW_DATA_DIR = app.getPath("userData");
17 |
18 | export const PATHS: Paths = {
19 | ROOT: ROOT_DIR,
20 | PRELOAD: path.join(OUT_DIR, "preload", "index.js"),
21 | VITE_WEBUI: path.join(OUT_DIR, "renderer"),
22 | ASSETS: path.join(ROOT_DIR, "assets")
23 | };
24 |
--------------------------------------------------------------------------------
/src/main/controllers/tabs-controller/tab-groups/glance.ts:
--------------------------------------------------------------------------------
1 | import { BaseTabGroup } from "./index";
2 |
3 | export class GlanceTabGroup extends BaseTabGroup {
4 | public frontTabId: number = -1;
5 | public mode: "glance" = "glance" as const;
6 |
7 | constructor(...args: ConstructorParameters) {
8 | super(...args);
9 |
10 | this.on("tab-removed", () => {
11 | if (this.tabIds.length !== 2) {
12 | // A glance tab group must have 2 tabs
13 | this.destroy();
14 | }
15 | });
16 | }
17 |
18 | public setFrontTab(tabId: number) {
19 | this.frontTabId = tabId;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/ipc/browser/browser.ts:
--------------------------------------------------------------------------------
1 | import { loadedProfilesController } from "@/controllers/loaded-profiles-controller";
2 | import { browserWindowsController } from "@/controllers/windows-controller/interfaces/browser";
3 | import { ipcMain } from "electron";
4 |
5 | ipcMain.on("browser:load-profile", async (_event, profileId: string) => {
6 | await loadedProfilesController.load(profileId);
7 | });
8 |
9 | ipcMain.on("browser:unload-profile", async (_event, profileId: string) => {
10 | loadedProfilesController.unload(profileId);
11 | });
12 |
13 | ipcMain.on("browser:create-window", async () => {
14 | browserWindowsController.create();
15 | });
16 |
--------------------------------------------------------------------------------
/src/main/ipc/browser/page.ts:
--------------------------------------------------------------------------------
1 | import { ipcMain } from "electron";
2 | import { browserWindowsController } from "@/controllers/windows-controller/interfaces/browser";
3 |
4 | export type PageBounds = {
5 | x: number;
6 | y: number;
7 | width: number;
8 | height: number;
9 | };
10 |
11 | export type PageBoundsWithWindow = PageBounds & {
12 | windowId: number;
13 | };
14 |
15 | ipcMain.on("page:set-bounds", async (event, bounds: PageBounds) => {
16 | const webContents = event.sender;
17 | const window = browserWindowsController.getWindowFromWebContents(webContents);
18 | if (!window) return;
19 |
20 | window.setPageBounds(bounds);
21 | });
22 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/settings/icons.ts:
--------------------------------------------------------------------------------
1 | export type IconData = {
2 | id: string;
3 | name: string;
4 | image_id: string;
5 | author?: string;
6 | };
7 |
8 | // API //
9 | export interface FlowIconsAPI {
10 | /**
11 | * Gets the icons
12 | */
13 | getIcons: () => Promise;
14 |
15 | /**
16 | * Checks if the platform is supported
17 | */
18 | isPlatformSupported: () => Promise;
19 |
20 | /**
21 | * Gets the current app icon
22 | */
23 | getCurrentIcon: () => Promise;
24 |
25 | /**
26 | * Sets the current app icon
27 | */
28 | setCurrentIcon: (iconId: string) => Promise;
29 | }
30 |
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v1/resources/js/base-error-reporting.js:
--------------------------------------------------------------------------------
1 | // Copyright (C) Microsoft Corporation. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 | function hookErrorReporting(component) {
5 | window.onerror = (message, source, lineno, columnNumber, error) => {
6 | const errorInfo = {
7 | column: columnNumber,
8 | component,
9 | line: lineno,
10 | message: error.message,
11 | name: error.name,
12 | source_url: source,
13 | stack: error.stack
14 | };
15 | chrome.errorReporting.reportError(errorInfo);
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v2/surf-iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Microsoft Edge Surf 2
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/renderer/src/hooks/use-mobile.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | const MOBILE_BREAKPOINT = 768;
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined);
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
12 | };
13 | mql.addEventListener("change", onChange);
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
15 | return () => mql.removeEventListener("change", onChange);
16 | }, []);
17 |
18 | return !!isMobile;
19 | }
20 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Label as LabelPrimitive } from "radix-ui";
3 |
4 | import { cn } from "@/lib/utils";
5 |
6 | function Label({ className, ...props }: React.ComponentProps) {
7 | return (
8 |
16 | );
17 | }
18 |
19 | export { Label };
20 |
--------------------------------------------------------------------------------
/src/main/app/instance.ts:
--------------------------------------------------------------------------------
1 | import { app } from "electron";
2 | import { handleOpenUrl, isValidOpenerUrl } from "@/app/urls";
3 | import { debugPrint } from "@/modules/output";
4 |
5 | function shouldCreateNewWindow(args: string[]): boolean {
6 | return args.includes("--new-window");
7 | }
8 |
9 | export function setupSecondInstanceHandling() {
10 | app.on("second-instance", async (_event, commandLine) => {
11 | const url = commandLine.pop();
12 | if (url && isValidOpenerUrl(url)) {
13 | const shouldCreate = shouldCreateNewWindow(commandLine);
14 | handleOpenUrl(shouldCreate, url);
15 | }
16 | });
17 |
18 | debugPrint("INITIALIZATION", "second instance handler initialized");
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/protocols/utils.ts:
--------------------------------------------------------------------------------
1 | import { getPathNoStrict } from "hono/utils/url";
2 |
3 | export interface AllowedDomains {
4 | [key: string]: true | string;
5 | }
6 |
7 | export interface ServeStaticFileOptions {
8 | overrideRouteName?: string;
9 | }
10 |
11 | export function transformPathForRequest(request: Request) {
12 | const realPath = getPathNoStrict(request);
13 | if (!realPath) {
14 | return realPath;
15 | }
16 |
17 | const url = URL.parse(request.url);
18 | // 'unknown-hostname' as default hostname to prevent weird behaviors when no hostname is present
19 | const hostname = url?.hostname ?? "unknown-hostname";
20 | return `/${hostname}${realPath}`;
21 | }
22 |
--------------------------------------------------------------------------------
/src/renderer/src/lib/merge-button-refs.ts:
--------------------------------------------------------------------------------
1 | /*
2 | This function merges multiple refs into a single ref callback.
3 | It ensures that the value is assigned to each ref, whether it's a function or a mutable ref object.
4 | This is useful when you need to merge external refs with an internal ref.
5 | */
6 |
7 | export function mergeButtonRefs(
8 | refs: Array | undefined>
9 | ): React.RefCallback {
10 | return (value) => {
11 | for (const ref of refs) {
12 | if (typeof ref === "function") {
13 | ref(value);
14 | } else if (ref != null) {
15 | (ref as React.MutableRefObject).current = value;
16 | }
17 | }
18 | };
19 | }
20 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/app/shortcuts.ts:
--------------------------------------------------------------------------------
1 | import { IPCListener } from "~/flow/types";
2 | import { ShortcutAction } from "~/types/shortcuts";
3 |
4 | export type ShortcutsData = ShortcutAction[];
5 |
6 | // API //
7 | export interface FlowShortcutsAPI {
8 | /**
9 | * Get all shortcuts
10 | */
11 | getShortcuts: () => Promise;
12 |
13 | /**
14 | * Set a shortcut
15 | */
16 | setShortcut: (actionId: string, shortcut: string) => Promise;
17 |
18 | /**
19 | * Reset a shortcut
20 | */
21 | resetShortcut: (actionId: string) => Promise;
22 |
23 | /**
24 | * Listen for shortcuts updates
25 | */
26 | onShortcutsUpdated: IPCListener<[ShortcutsData]>;
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/controllers/index.ts:
--------------------------------------------------------------------------------
1 | // Required by app-menu-controller
2 | import "./windows-controller";
3 |
4 | // Required by app-menu-controller, profiles-controller
5 | import "./spaces-controller";
6 |
7 | // Required by spaces-controller, loaded-profiles-controller, sessions-controller
8 | import "./profiles-controller";
9 |
10 | // Required by windows-controller
11 | import "./app-menu-controller";
12 |
13 | // Required by app-menu-controller
14 | import "./default-browser-controller";
15 |
16 | // No Dependents
17 | import "./posthog-controller";
18 | import "./quit-controller";
19 | import "./auto-update-controller";
20 | import "./loaded-profiles-controller";
21 | import "./tabs-controller";
22 | import "./sessions-controller";
23 |
--------------------------------------------------------------------------------
/src/main/ipc/app/app.ts:
--------------------------------------------------------------------------------
1 | import { defaultBrowserController } from "@/controllers/default-browser-controller";
2 | import { app, clipboard } from "electron";
3 | import { ipcMain } from "electron";
4 |
5 | ipcMain.handle("app:get-info", async () => {
6 | return {
7 | version: app.getVersion(),
8 | packaged: app.isPackaged
9 | };
10 | });
11 |
12 | ipcMain.on("app:write-text-to-clipboard", (_event, text: string) => {
13 | clipboard.writeText(text);
14 | });
15 |
16 | ipcMain.handle("app:set-default-browser", async () => {
17 | return await defaultBrowserController.setDefaultBrowser();
18 | });
19 |
20 | ipcMain.handle("app:get-default-browser", async () => {
21 | return defaultBrowserController.isDefaultBrowser();
22 | });
23 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/pdf-viewer/config.tsx:
--------------------------------------------------------------------------------
1 | import { ThemeProvider as ThemeProviderComponent } from "@/components/main/theme";
2 | import { NuqsProvider } from "@/components/providers/nuqs-provider";
3 | import { RouteConfigType } from "@/types/routes";
4 | import { Fragment, ReactNode } from "react";
5 |
6 | // Theme makes it go all weird...
7 | const THEME_PROVIDER_ENABLED = true;
8 |
9 | const ThemeProvider = THEME_PROVIDER_ENABLED ? ThemeProviderComponent : Fragment;
10 |
11 | export const RouteConfig: RouteConfigType = {
12 | Providers: ({ children }: { children: ReactNode }) => {
13 | return (
14 |
15 | {children}
16 |
17 | );
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[FEATURE] "
5 | labels: enhancement
6 | type: feature
7 | assignees: ""
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/protocols/_protocols/flow/pdf-cache.ts:
--------------------------------------------------------------------------------
1 | import { getPdfResponseFromCache, removePdfResponseFromCache } from "@/modules/pdf-cache";
2 | import { HonoApp } from ".";
3 |
4 | export function registerPdfCacheRoutes(app: HonoApp) {
5 | app.get("/pdf-cache", async (c) => {
6 | const pdfURL = c.req.query("url");
7 | const key = c.req.query("key");
8 | if (!pdfURL || !key) {
9 | return c.text("Invalid request path", 400);
10 | }
11 |
12 | const pdfResponse = getPdfResponseFromCache(key);
13 | if (!pdfResponse) {
14 | // redirect to actual url
15 | return c.redirect(pdfURL);
16 | }
17 |
18 | removePdfResponseFromCache(key);
19 | return pdfResponse;
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/ipc/index.ts:
--------------------------------------------------------------------------------
1 | // App APIs
2 | import "@/ipc/app/app";
3 | import "@/ipc/app/extensions";
4 | import "@/ipc/app/updates";
5 | import "@/ipc/app/shortcuts";
6 |
7 | // Browser APIs
8 | import "@/ipc/browser/browser";
9 | import "@/ipc/browser/tabs";
10 | import "@/ipc/browser/page";
11 | import "@/ipc/browser/navigation";
12 | import "@/ipc/browser/interface";
13 | import "@/ipc/window/omnibox";
14 | import "@/ipc/app/new-tab";
15 |
16 | // Session APIs
17 | import "@/ipc/session/profiles";
18 | import "@/ipc/session/spaces";
19 |
20 | // Settings APIs
21 | import "@/ipc/window/settings";
22 | import "@/ipc/app/icons";
23 | import "@/ipc/app/open-external";
24 | import "@/ipc/app/onboarding";
25 |
26 | // Special
27 | import "@/ipc/listeners-manager";
28 |
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/scripts/strings.js:
--------------------------------------------------------------------------------
1 | var pageData = {"altGameCommonImage1x":"images/default_100_percent/offline/100-olympic-firemedal-sprites.png","altGameCommonImage2x":"images/default_200_percent/offline/200-olympic-firemedal-sprites.png","dinoGameA11yAriaLabel":"","dinoGameA11yGameOver":"Game over, your score is $1.","dinoGameA11yHighScore":"Your highest score is $1.","dinoGameA11yJump":"Jump!","dinoGameA11ySpeedToggle":"Start slower","dinoGameA11yStartGame":"Game started.","enableAltGameMode":false,"errorCode":"","fontfamily":"'Segoe UI', Tahoma, sans-serif","fontsize":"75%","heading":{"hostName":"dino","msg":"Press space to play"},"iconClass":"icon-offline","language":"en","textdirection":"ltr","title":"chrome://dino/"};
2 | loadTimeData.data = pageData;
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: "bug"
6 | type: bug
7 | assignees: ""
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 |
16 | 1. Go to '...'
17 | 2. Click on '....'
18 | 3. Scroll down to '....'
19 | 4. See error
20 |
21 | **Expected behavior**
22 | A clear and concise description of what you expected to happen.
23 |
24 | **Screenshots**
25 | If applicable, add screenshots to help explain your problem.
26 |
27 | **Device Information:**
28 |
29 | - OS: [e.g. iOS]
30 | - App Version [e.g. 22]
31 |
32 | **Additional context**
33 | Add any other context about the problem here.
34 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/protocols/_protocols/flow-external/index.ts:
--------------------------------------------------------------------------------
1 | import { transformPathForRequest } from "../../utils";
2 | import { type Protocol } from "electron";
3 | import { Hono } from "hono/tiny";
4 | import { registerStaticDomainsRoutes } from "../../static-domains";
5 |
6 | // Create Hono App
7 | const app = new Hono({
8 | getPath: transformPathForRequest
9 | });
10 | export type HonoApp = typeof app;
11 |
12 | // Register Routes
13 | // None currently
14 |
15 | // Catch-all Route
16 | registerStaticDomainsRoutes("flow-external", app);
17 |
18 | // Export Protocol Handler
19 | export function registerFlowExternalProtocol(protocol: Protocol) {
20 | protocol.handle("flow-external", async (request) => {
21 | return app.fetch(request);
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/protocols/_protocols/flow/favicon.ts:
--------------------------------------------------------------------------------
1 | import { getFavicon, normalizeURL } from "@/modules/favicons";
2 | import { HonoApp } from ".";
3 | import { bufferToArrayBuffer } from "@/modules/utils";
4 |
5 | export function registerFaviconRoutes(app: HonoApp) {
6 | app.get("/favicon", async (c) => {
7 | const targetUrl = c.req.query("url");
8 | if (!targetUrl) {
9 | return c.text("No URL provided", 400);
10 | }
11 |
12 | const normalizedTargetUrl = normalizeURL(targetUrl);
13 | const favicon = await getFavicon(normalizedTargetUrl);
14 | if (!favicon) {
15 | return c.text("No favicon found", 404);
16 | }
17 |
18 | return c.body(bufferToArrayBuffer(favicon), 200, { "Content-Type": "image/png" });
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/sessions/profiles.ts:
--------------------------------------------------------------------------------
1 | export type Profile = {
2 | id: string;
3 | name: string;
4 | };
5 |
6 | // API //
7 | export interface FlowProfilesAPI {
8 | /**
9 | * Gets the profiles
10 | */
11 | getProfiles: () => Promise;
12 |
13 | /**
14 | * Creates a profile
15 | */
16 | createProfile: (profileName: string) => Promise;
17 |
18 | /**
19 | * Updates a profile
20 | */
21 | updateProfile: (profileId: string, profileData: Partial) => Promise;
22 |
23 | /**
24 | * Deletes a profile
25 | */
26 | deleteProfile: (profileId: string) => Promise;
27 |
28 | /**
29 | * Gets the profile id that is currently being used
30 | */
31 | getUsingProfile: () => Promise;
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/app/onboarding.ts:
--------------------------------------------------------------------------------
1 | import { debugPrint } from "@/modules/output";
2 | import { hasCompletedOnboarding } from "@/saving/onboarding";
3 | import { onboarding } from "@/controllers/windows-controller/interfaces/onboarding";
4 | import { createInitialWindow } from "@/saving/tabs";
5 |
6 | export function runOnboardingOrInitialWindow() {
7 | debugPrint("INITIALIZATION", "grabbing hasCompletedOnboarding()");
8 | hasCompletedOnboarding().then((completed) => {
9 | debugPrint("INITIALIZATION", "grabbed hasCompletedOnboarding()", completed);
10 | if (!completed) {
11 | onboarding.show();
12 | debugPrint("INITIALIZATION", "show onboarding window");
13 | } else {
14 | createInitialWindow();
15 | debugPrint("INITIALIZATION", "show browser window");
16 | }
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/settings/settings.ts:
--------------------------------------------------------------------------------
1 | import { IPCListener } from "~/flow/types";
2 | import type { BasicSetting, BasicSettingCard } from "~/types/settings";
3 |
4 | // API //
5 | export interface FlowSettingsAPI {
6 | /**
7 | * Gets the value of a setting
8 | */
9 | getSetting(settingId: string): Promise;
10 |
11 | /**
12 | * Sets the value of a setting
13 | */
14 | setSetting: (settingId: string, value: unknown) => Promise;
15 |
16 | /**
17 | * Gets the basic settings and cards
18 | */
19 | getBasicSettings: () => Promise<{
20 | settings: BasicSetting[];
21 | cards: BasicSettingCard[];
22 | }>;
23 |
24 | /**
25 | * Listens for changes to the settings */
26 | onSettingsChanged: IPCListener<[void]>;
27 | }
28 |
--------------------------------------------------------------------------------
/src/renderer/src/components/browser-ui/sidebar/content/space-title.tsx:
--------------------------------------------------------------------------------
1 | import { Space } from "~/flow/interfaces/sessions/spaces";
2 | import { SidebarGroup, SidebarMenuButton, useSidebar } from "@/components/ui/resizable-sidebar";
3 | import { SpaceIcon } from "@/lib/phosphor-icons";
4 |
5 | export function SpaceTitle({ space }: { space: Space }) {
6 | const { open } = useSidebar();
7 |
8 | if (!open) return null;
9 |
10 | return (
11 |
12 |
13 |
14 | {space.name}
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Progress as ProgressPrimitive } from "radix-ui";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | function Progress({ className, value, ...props }: React.ComponentProps) {
9 | return (
10 |
15 |
20 |
21 | );
22 | }
23 |
24 | export { Progress };
25 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Separator as SeparatorPrimitive } from "radix-ui";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | function Separator({
9 | className,
10 | orientation = "horizontal",
11 | decorative = true,
12 | ...props
13 | }: React.ComponentProps) {
14 | return (
15 |
25 | );
26 | }
27 |
28 | export { Separator };
29 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/protocols/_protocols/flow-internal/index.ts:
--------------------------------------------------------------------------------
1 | import { type Protocol } from "electron";
2 | import { Hono } from "hono/tiny";
3 | import { registerStaticDomainsRoutes } from "../../static-domains";
4 | import { transformPathForRequest } from "../../utils";
5 | import { registerActiveFaviconRoutes } from "./active-favicon";
6 |
7 | // Create Hono App
8 | const app = new Hono({
9 | getPath: transformPathForRequest
10 | });
11 | export type HonoApp = typeof app;
12 |
13 | // Register Routes
14 | registerActiveFaviconRoutes(app);
15 |
16 | // Catch-all Route
17 | registerStaticDomainsRoutes("flow-internal", app);
18 |
19 | // Export Protocol Handler
20 | export function registerFlowInternalProtocol(protocol: Protocol) {
21 | protocol.handle("flow-internal", async (request) => {
22 | return app.fetch(request);
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/app/app.ts:
--------------------------------------------------------------------------------
1 | // API //
2 | export interface FlowAppAPI {
3 | /**
4 | * Gets the app info
5 | */
6 | getAppInfo: () => Promise<{
7 | app_version: string;
8 | build_number: string;
9 | node_version: string;
10 | chrome_version: string;
11 | electron_version: string;
12 | os: string;
13 | update_channel: "Stable" | "Beta" | "Alpha" | "Development";
14 | }>;
15 |
16 | /**
17 | * Gets the platform of the current device
18 | */
19 | getPlatform: () => string;
20 |
21 | /**
22 | * Writes text to the clipboard
23 | */
24 | writeTextToClipboard: (text: string) => void;
25 |
26 | /**
27 | * Sets the default browser
28 | */
29 | setDefaultBrowser: () => Promise;
30 |
31 | /**
32 | * Gets the default browser
33 | */
34 | getDefaultBrowser: () => Promise;
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/intercept-rules/index.ts:
--------------------------------------------------------------------------------
1 | import { setupBetterPdfViewer } from "@/controllers/sessions-controller/intercept-rules/better-pdf-viewer";
2 | import { setupCorsBypassForCustomProtocols } from "@/controllers/sessions-controller/intercept-rules/cors-bypass-custom-protocols";
3 | import { setupUserAgentTransformer } from "@/controllers/sessions-controller/intercept-rules/user-agent-transformer";
4 | import type { Session } from "electron";
5 |
6 | // Setup intercept rules for the session
7 | export function setupInterceptRules(session: Session) {
8 | // Transform the User-Agent header
9 | setupUserAgentTransformer(session);
10 |
11 | // Bypass CORS for flow and flow-internal protocols
12 | setupCorsBypassForCustomProtocols(session);
13 |
14 | // Setup redirects required for the better PDF viewer
15 | setupBetterPdfViewer(session);
16 | }
17 |
--------------------------------------------------------------------------------
/src/shared/types/settings.ts:
--------------------------------------------------------------------------------
1 | // This is used to create a simple settings framework.
2 | // This will make it easier to add new settings and cards.
3 |
4 | // Setting Type: Boolean //
5 | type SettingTypeBoolean = {
6 | type: "boolean";
7 | defaultValue: boolean;
8 | };
9 |
10 | // Setting Type: Enum //
11 | type SettingTypeEnumOption = {
12 | id: string;
13 | name: string;
14 | };
15 |
16 | type SettingTypeEnum = {
17 | type: "enum";
18 | defaultValue: string;
19 | options: SettingTypeEnumOption[];
20 | };
21 |
22 | export type SettingType = SettingTypeBoolean | SettingTypeEnum;
23 |
24 | // Setting //
25 | export type BasicSetting = {
26 | id: string;
27 | name: string;
28 | showName?: boolean;
29 | } & SettingType;
30 |
31 | // Setting Card //
32 | export type BasicSettingCard = {
33 | title: string;
34 | subtitle: string;
35 | settings: string[];
36 | };
37 |
--------------------------------------------------------------------------------
/src/main/controllers/windows-controller/README.md:
--------------------------------------------------------------------------------
1 | # WindowsController
2 |
3 | This handles all `BrowserWindows` from `electron` and seperates them into different types of windows to easily manage them.
4 |
5 | ## Components
6 |
7 | - `./index.ts` - Main interface exposed to grab all windows (not creating new ones!)
8 | - `./type-manager.ts` - A helper class to manage and let you create new windows of a specific type
9 | - `./interfaces` - Interfaces for the windows controller for external use
10 | - `./types` - Window classes for each type of windows (and the base window class)
11 | - `./utils` - Handles the utilities for the windows
12 |
13 | ## External Usage
14 |
15 | - The only thing that should be required outside of this controller is the `index.ts` file and interfaces in the `./interfaces` directory.
16 | - All other files are internal and should not be used outside of this controller.
17 |
--------------------------------------------------------------------------------
/src/renderer/src/lib/omnibox/base-provider.tsx:
--------------------------------------------------------------------------------
1 | import { OmniboxUpdateCallback } from "@/lib/omnibox/omnibox";
2 | import { AutocompleteInput } from "@/lib/omnibox/types";
3 |
4 | /** Interface for components that generate autocomplete suggestions. */
5 | export interface AutocompleteProvider {
6 | name: string;
7 | /**
8 | * Starts generating suggestions for the given input.
9 | * Results are sent asynchronously via the onResults callback.
10 | */
11 | start(input: AutocompleteInput, onResults: OmniboxUpdateCallback): void;
12 | /**
13 | * Stops any ongoing asynchronous operations for the current query.
14 | */
15 | stop(): void;
16 | }
17 |
18 | export abstract class BaseProvider implements AutocompleteProvider {
19 | abstract name: string;
20 |
21 | abstract start(input: AutocompleteInput, onResults: OmniboxUpdateCallback): void;
22 |
23 | abstract stop(): void;
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/browser.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Main entrypoint after conditions met in index.ts
3 | */
4 |
5 | // Import everything
6 | import "@/controllers";
7 | import "@/ipc";
8 | import "@/modules/content-blocker";
9 | import "@/modules/extensions/main";
10 | import { setupPlatformIntegration } from "@/app/platform";
11 | import { processInitialUrl } from "@/app/urls";
12 | import { setupSecondInstanceHandling } from "@/app/instance";
13 | import { runOnboardingOrInitialWindow } from "@/app/onboarding";
14 | import { setupAppLifecycle } from "@/app/lifecycle";
15 |
16 | // Handle initial URL (runs asynchronously)
17 | processInitialUrl();
18 |
19 | // Setup second instance handler
20 | setupSecondInstanceHandling();
21 |
22 | // Setup platform specific features
23 | setupPlatformIntegration();
24 |
25 | // Open onboarding / create initial window
26 | runOnboardingOrInitialWindow();
27 |
28 | // App lifecycle events
29 | setupAppLifecycle();
30 |
--------------------------------------------------------------------------------
/src/main/app/lifecycle.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow } from "electron";
2 | import { handleOpenUrl } from "@/app/urls";
3 | import { hasCompletedOnboarding } from "@/saving/onboarding";
4 | import { browserWindowsController } from "@/controllers/windows-controller/interfaces/browser";
5 |
6 | export function setupAppLifecycle() {
7 | app.on("window-all-closed", () => {
8 | if (process.platform !== "darwin") {
9 | app.quit();
10 | return;
11 | }
12 |
13 | hasCompletedOnboarding().then((completed) => {
14 | if (!completed) {
15 | app.quit();
16 | }
17 | });
18 | });
19 |
20 | app.whenReady().then(() => {
21 | app.on("activate", () => {
22 | if (BrowserWindow.getAllWindows().length === 0) {
23 | browserWindowsController.create();
24 | }
25 | });
26 | });
27 |
28 | app.on("open-url", async (_event, url) => {
29 | handleOpenUrl(false, url);
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/README.md:
--------------------------------------------------------------------------------
1 | # SessionsController
2 |
3 | This handles the raw sessions from `electron` and configure them so they are useful for the browser.
4 |
5 | ## Components
6 |
7 | - `./index.ts` - Main interface exposed to grab and create sessions
8 | - `./default-session` - Handles the default session and its initialization
9 | - `./handlers` - Handles the callbacks and permissions for the sessions
10 | - `./intercept-rules` - Handles the intercept rules for the sessions
11 | - `./preload-scripts` - Handles the preload scripts for the sessions
12 | - `./protocols` - Handles all the custom protocols and let them be registered to sessions
13 | - `./web-requests` - Handles the web requests for the sessions
14 |
15 | ## External Usage
16 |
17 | - The only thing that should be required outside of this controller is the `index.ts` file.
18 | - All other files are internal and should not be used outside of this controller.
19 |
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/appmanifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Chrome Dino",
3 | "short_name": "Chrome Dino",
4 | "description": "Dino game. A pixelated dinosaur dodges cacti and pterodactyls as it runs across a desolate landscape.",
5 | "version": "2.0",
6 | "start_url": "index.html",
7 | "display": "fullscreen",
8 | "orientation": "any",
9 | "background_color": "#ffffff",
10 | "icons": [
11 | {
12 | "src": "icons/icon-16.png",
13 | "sizes": "16x16",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "icons/icon-32.png",
18 | "sizes": "32x32",
19 | "type": "image/png"
20 | },
21 | {
22 | "src": "icons/icon-114.png",
23 | "sizes": "114x114",
24 | "type": "image/png"
25 | },
26 | {
27 | "src": "icons/icon-128.png",
28 | "sizes": "128x128",
29 | "type": "image/png"
30 | },
31 | {
32 | "src": "icons/icon-256.png",
33 | "sizes": "256x256",
34 | "type": "image/png"
35 | }
36 | ]
37 | }
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/app/extensions.ts:
--------------------------------------------------------------------------------
1 | import { IPCListener } from "~/flow/types";
2 | import { SharedExtensionData } from "~/types/extensions";
3 |
4 | // API //
5 | export interface FlowExtensionsAPI {
6 | /**
7 | * Get all extensions in the current profile
8 | */
9 | getAllInCurrentProfile: () => Promise;
10 |
11 | /**
12 | * Listen for updates to the extensions in the current profile
13 | */
14 | onUpdated: IPCListener<[string, SharedExtensionData[]]>;
15 |
16 | /**
17 | * Set the enabled state of an extension
18 | */
19 | setExtensionEnabled: (extensionId: string, enabled: boolean) => Promise;
20 |
21 | /**
22 | * Uninstall an extension
23 | */
24 | uninstallExtension: (extensionId: string) => Promise;
25 |
26 | /**
27 | * Set the pinned state of an extension
28 | */
29 | setExtensionPinned: (extensionId: string, pinned: boolean) => Promise;
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/controllers/app-menu-controller/menu/items/edit.ts:
--------------------------------------------------------------------------------
1 | import { MenuItemConstructorOptions } from "electron";
2 | import { getFocusedBrowserWindow } from "../helpers";
3 | import { fireCopyLinkAction } from "@/ipc/app/actions";
4 | import { getCurrentShortcut } from "@/modules/shortcuts";
5 |
6 | export const createEditMenu = (): MenuItemConstructorOptions => ({
7 | label: "Edit",
8 | submenu: [
9 | { role: "undo" },
10 | { role: "redo" },
11 | { type: "separator" },
12 | { role: "cut" },
13 | { role: "copy" },
14 | {
15 | label: "Copy URL",
16 | accelerator: getCurrentShortcut("tab.copyUrl"),
17 | click: () => {
18 | const window = getFocusedBrowserWindow();
19 | if (!window) return;
20 | return fireCopyLinkAction(window);
21 | }
22 | },
23 | { role: "paste" },
24 | { role: "pasteAndMatchStyle" },
25 | { role: "delete" },
26 | { role: "selectAll" }
27 | ]
28 | });
29 |
--------------------------------------------------------------------------------
/src/main/ipc/app/shortcuts.ts:
--------------------------------------------------------------------------------
1 | import { sendMessageToListeners } from "@/ipc/listeners-manager";
2 | import { getShortcuts } from "@/modules/shortcuts";
3 | import { resetModifiedShortcut, shortcutsEmitter, updateModifiedShortcut } from "@/saving/shortcuts";
4 | import { ipcMain } from "electron";
5 |
6 | ipcMain.handle("shortcuts:get-all", () => {
7 | const shortcuts = getShortcuts();
8 | return shortcuts;
9 | });
10 |
11 | ipcMain.handle("shortcuts:set", (_event, actionId: string, shortcut: string) => {
12 | const success = updateModifiedShortcut(actionId, {
13 | newShortcut: shortcut
14 | });
15 | return success;
16 | });
17 |
18 | ipcMain.handle("shortcuts:reset", (_event, actionId: string) => {
19 | const success = resetModifiedShortcut(actionId);
20 | return success;
21 | });
22 |
23 | shortcutsEmitter.on("shortcuts-changed", () => {
24 | const shortcuts = getShortcuts();
25 | sendMessageToListeners("shortcuts:on-changed", shortcuts);
26 | });
27 |
--------------------------------------------------------------------------------
/scripts/electron-upgrader/current.ts:
--------------------------------------------------------------------------------
1 | import { findLatestCurrentMajorVersion, getCommitHashForTag } from "./_modules/github";
2 | import { updateBunLock, updatePackageJson } from "./_modules/updater";
3 |
4 | // GRAB RELEASE FROM GITHUB //
5 | const latestCurrentVersion = await findLatestCurrentMajorVersion();
6 |
7 | if (!latestCurrentVersion) {
8 | throw new Error("No version found in current major version");
9 | }
10 |
11 | const commitHash = await getCommitHashForTag(latestCurrentVersion);
12 |
13 | if (!commitHash) {
14 | throw new Error("No commit hash found");
15 | }
16 |
17 | console.log(`Latest version in current major version: ${latestCurrentVersion}`);
18 | console.log(`Commit hash: ${commitHash}`);
19 |
20 | // UPDATE PACKAGE.JSON //
21 | updatePackageJson(latestCurrentVersion);
22 |
23 | console.log("package.json updated!");
24 |
25 | // UPDATE BUN.LOCK //
26 | updateBunLock(latestCurrentVersion, commitHash);
27 |
28 | console.log("bun.lock updated!");
29 |
--------------------------------------------------------------------------------
/scripts/frontend-routes-generator/pruner.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { FRONTEND_PATH, ROUTES_PATH, getDirectories } from "./common";
3 | import fs from "fs/promises";
4 |
5 | const emptyFn = () => {};
6 |
7 | async function pruneRoutes() {
8 | // Grab all the routes
9 | const routes = await getDirectories(ROUTES_PATH);
10 |
11 | for (const route of routes) {
12 | // Remove all the route-*.html files
13 | const htmlPath = path.join(FRONTEND_PATH, `route-${route}.html`);
14 | await fs.rm(htmlPath).catch(emptyFn);
15 |
16 | // Remove all the main.tsx files
17 | const entrypointPath = path.join(ROUTES_PATH, route, "main.tsx");
18 | await fs.rm(entrypointPath).catch(emptyFn);
19 | }
20 | }
21 |
22 | pruneRoutes()
23 | .then(() => {
24 | console.log("Routes pruned successfully");
25 | process.exit(0);
26 | })
27 | .catch((err) => {
28 | console.error("Failed to prune routes:", err);
29 | process.exit(1);
30 | });
31 |
--------------------------------------------------------------------------------
/src/renderer/src/components/settings/sections/spaces/theme-editors/space-icon.tsx:
--------------------------------------------------------------------------------
1 | import { Space } from "~/flow/interfaces/sessions/spaces";
2 | import { SpaceIconPicker } from "../icon-picker";
3 | import { motion } from "motion/react";
4 |
5 | type SpaceIconEditorProps = {
6 | editedSpace: Space;
7 | updateEditedSpace: (updates: Partial) => void;
8 | };
9 |
10 | export function SpaceIconEditor({ editedSpace, updateEditedSpace }: SpaceIconEditorProps) {
11 | return (
12 |
18 | Space Icon
19 | updateEditedSpace({ icon: iconId })}
22 | />
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/controllers/app-menu-controller/menu/items/file.ts:
--------------------------------------------------------------------------------
1 | import { MenuItemConstructorOptions } from "electron";
2 | import { getFocusedBrowserWindow } from "../helpers";
3 | import { openNewTab } from "@/ipc/app/new-tab";
4 | import { getCurrentShortcut } from "@/modules/shortcuts";
5 | import { browserWindowsController } from "@/controllers/windows-controller/interfaces/browser";
6 |
7 | export const createFileMenu = (): MenuItemConstructorOptions => ({
8 | label: "File",
9 | submenu: [
10 | {
11 | label: "New Tab",
12 | accelerator: getCurrentShortcut("tabs.new"),
13 | click: () => {
14 | const window = getFocusedBrowserWindow();
15 | if (!window) return;
16 | return openNewTab(window);
17 | }
18 | },
19 | {
20 | label: "New Window",
21 | accelerator: getCurrentShortcut("browser.newWindow"),
22 | click: () => {
23 | browserWindowsController.create();
24 | }
25 | }
26 | ]
27 | });
28 |
--------------------------------------------------------------------------------
/src/main/saving/onboarding.ts:
--------------------------------------------------------------------------------
1 | import { browserWindowsController } from "@/controllers/windows-controller/interfaces/browser";
2 | import { onboarding } from "@/controllers/windows-controller/interfaces/onboarding";
3 | import { SettingsDataStore } from "@/saving/settings";
4 | import { app } from "electron";
5 |
6 | const ONBOARDING_KEY = "onboarding_version_completed";
7 | const ONBOARDING_VERSION = "v0";
8 |
9 | export async function hasCompletedOnboarding() {
10 | const onboardingData = await SettingsDataStore.get(ONBOARDING_KEY);
11 | return onboardingData === ONBOARDING_VERSION;
12 | }
13 |
14 | export async function setOnboardingCompleted() {
15 | await SettingsDataStore.set(ONBOARDING_KEY, ONBOARDING_VERSION);
16 | onboarding.hide();
17 |
18 | if (browserWindowsController.getWindows().length === 0) {
19 | browserWindowsController.create();
20 | }
21 | }
22 |
23 | export async function resetOnboarding() {
24 | await SettingsDataStore.remove(ONBOARDING_KEY);
25 | app.quit();
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/default-session/index.ts:
--------------------------------------------------------------------------------
1 | import { sleep } from "@/modules/utils";
2 | import { registerProtocolsWithSession } from "../protocols";
3 | import { app, session } from "electron";
4 | import { setupInterceptRules } from "@/controllers/sessions-controller/intercept-rules";
5 | import { registerPreloadScripts } from "@/controllers/sessions-controller/preload-scripts";
6 |
7 | function initializeDefaultSession() {
8 | const defaultSession = session.defaultSession;
9 |
10 | registerProtocolsWithSession(defaultSession, ["flow", "flow-internal", "flow-external"]);
11 |
12 | setupInterceptRules(defaultSession);
13 | registerPreloadScripts(defaultSession);
14 | }
15 |
16 | export let isDefaultSessionReady = false;
17 |
18 | export const defaultSessionReady = app.whenReady().then(async () => {
19 | initializeDefaultSession();
20 |
21 | // wait for 50 ms before returning
22 | await sleep(50);
23 |
24 | // Set the flag to true
25 | isDefaultSessionReady = true;
26 | });
27 |
--------------------------------------------------------------------------------
/src/main/controllers/windows-controller/types/onboarding.ts:
--------------------------------------------------------------------------------
1 | import { BaseWindow } from "@/controllers/windows-controller/types/base";
2 | import { BrowserWindow, nativeTheme } from "electron";
3 |
4 | export class OnboardingWindow extends BaseWindow {
5 | constructor() {
6 | const browserWindow = new BrowserWindow({
7 | width: 1000,
8 | height: 700,
9 |
10 | resizable: false,
11 | center: true,
12 | show: true,
13 | frame: false,
14 | roundedCorners: true,
15 |
16 | titleBarStyle: "hidden",
17 | titleBarOverlay: {
18 | height: 20,
19 | symbolColor: nativeTheme.shouldUseDarkColors ? "white" : "black",
20 | color: "rgba(0,0,0,0)"
21 | }
22 | });
23 | browserWindow.loadURL("flow-internal://onboarding/");
24 |
25 | // Use settings.hide's behavior instead of the default one
26 | browserWindow.on("close", () => {
27 | browserWindow.hide();
28 | });
29 |
30 | super("onboarding", browserWindow);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/index.ts:
--------------------------------------------------------------------------------
1 | import { debugPrint } from "@/modules/output";
2 | import { app } from "electron";
3 |
4 | function printHeader() {
5 | if (!app.isPackaged) {
6 | console.log("\n".repeat(75));
7 | }
8 |
9 | console.log("\x1b[34m%s\x1b[0m", "--- Flow Browser ---");
10 |
11 | if (app.isPackaged) {
12 | console.log("\x1b[32m%s\x1b[0m", `Production Build (${app.getVersion()})`);
13 | } else {
14 | console.log("\x1b[31m%s\x1b[0m", `Development Build (${app.getVersion()})`);
15 | }
16 |
17 | console.log("");
18 | }
19 |
20 | function initializeApp() {
21 | const gotTheLock = app.requestSingleInstanceLock();
22 | debugPrint("INITIALIZATION", "gotTheLock", gotTheLock);
23 |
24 | if (!gotTheLock) {
25 | return false;
26 | }
27 |
28 | // Print header
29 | printHeader();
30 |
31 | // Import everything
32 | import("@/browser");
33 |
34 | return true;
35 | }
36 |
37 | // Start the application
38 | const initialized = initializeApp();
39 | if (!initialized) {
40 | app.quit();
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/controllers/posthog-controller/identify.ts:
--------------------------------------------------------------------------------
1 | import { SettingsDataStore } from "@/saving/settings";
2 |
3 | const POSTHOG_IDENTIFIER_STORE_KEY = "posthog-anon-id";
4 |
5 | let posthogIdentifierPromise: Promise | undefined;
6 |
7 | /**
8 | * Get the PostHog identifier.
9 | * This should only be used internally by the Posthog controller.
10 | * @returns The PostHog identifier.
11 | */
12 | export async function _getPosthogIdentifier(): Promise {
13 | if (!posthogIdentifierPromise) {
14 | posthogIdentifierPromise = cachePosthogIdentifier();
15 | }
16 | return await posthogIdentifierPromise;
17 | }
18 |
19 | async function cachePosthogIdentifier(): Promise {
20 | const existingAnonId = await SettingsDataStore.get(POSTHOG_IDENTIFIER_STORE_KEY);
21 | if (!existingAnonId) {
22 | const newAnonUserId = crypto.randomUUID();
23 | await SettingsDataStore.set(POSTHOG_IDENTIFIER_STORE_KEY, newAnonUserId);
24 | return newAnonUserId;
25 | }
26 | return existingAnonId;
27 | }
28 |
--------------------------------------------------------------------------------
/src/renderer/src/components/browser-ui/sidebar/hover-detector.tsx:
--------------------------------------------------------------------------------
1 | import { SidebarSide } from "@/components/browser-ui/main";
2 | import { cn } from "@/lib/utils";
3 | import { useCallback, useEffect, useRef } from "react";
4 |
5 | export function SidebarHoverDetector({ side, started }: { side: SidebarSide; started: () => void }) {
6 | const ref = useRef(null);
7 |
8 | const onMouseEnter = useCallback(() => {
9 | started();
10 | }, [started]);
11 |
12 | useEffect(() => {
13 | const component = ref.current;
14 |
15 | if (component) {
16 | component.addEventListener("mouseenter", onMouseEnter);
17 | }
18 |
19 | return () => {
20 | if (component) {
21 | component.removeEventListener("mouseenter", onMouseEnter);
22 | }
23 | };
24 | }, [onMouseEnter]);
25 |
26 | return (
27 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
18 | );
19 | }
20 |
21 | export { Input };
22 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Debug Main Process",
6 | "type": "node",
7 | "request": "launch",
8 | "cwd": "${workspaceRoot}",
9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
10 | "windows": {
11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
12 | },
13 | "runtimeArgs": ["--sourcemap"],
14 | "env": {
15 | "REMOTE_DEBUGGING_PORT": "9222"
16 | }
17 | },
18 | {
19 | "name": "Debug Renderer Process",
20 | "port": 9222,
21 | "request": "attach",
22 | "type": "chrome",
23 | "webRoot": "${workspaceFolder}/src/renderer",
24 | "timeout": 60000,
25 | "presentation": {
26 | "hidden": true
27 | }
28 | }
29 | ],
30 | "compounds": [
31 | {
32 | "name": "Debug All",
33 | "configurations": ["Debug Main Process", "Debug Renderer Process"],
34 | "presentation": {
35 | "order": 1
36 | }
37 | }
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/protocols/_protocols/flow/index.ts:
--------------------------------------------------------------------------------
1 | import { registerFaviconRoutes } from "./favicon";
2 | import { registerAssetsRoutes } from "./assets";
3 | import { registerExtensionIconRoutes } from "./extension-icon";
4 | import { registerPdfCacheRoutes } from "./pdf-cache";
5 | import { transformPathForRequest } from "../../utils";
6 | import { type Protocol } from "electron";
7 | import { Hono } from "hono/tiny";
8 | import { registerStaticDomainsRoutes } from "../../static-domains";
9 |
10 | // Create Hono App
11 | const app = new Hono({
12 | getPath: transformPathForRequest
13 | });
14 | export type HonoApp = typeof app;
15 |
16 | // Register Routes
17 | registerFaviconRoutes(app);
18 | registerAssetsRoutes(app);
19 | registerExtensionIconRoutes(app);
20 | registerPdfCacheRoutes(app);
21 |
22 | // Catch-all Route
23 | registerStaticDomainsRoutes("flow", app);
24 |
25 | // Export Protocol Handler
26 | export function registerFlowProtocol(protocol: Protocol) {
27 | protocol.handle("flow", async (request) => {
28 | return app.fetch(request);
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/ipc/window/settings.ts:
--------------------------------------------------------------------------------
1 | import { sendMessageToListeners } from "@/ipc/listeners-manager";
2 | import { BasicSettings, BasicSettingCards } from "@/modules/basic-settings";
3 | import { getSettingValueById, setSettingValueById } from "@/saving/settings";
4 | import { settings } from "@/controllers/windows-controller/interfaces/settings";
5 | import { ipcMain } from "electron";
6 |
7 | ipcMain.on("settings:open", () => {
8 | settings.show();
9 | });
10 |
11 | ipcMain.on("settings:close", () => {
12 | settings.hide();
13 | });
14 |
15 | ipcMain.handle("settings:get-setting", (_event, settingId: string) => {
16 | return getSettingValueById(settingId);
17 | });
18 |
19 | ipcMain.handle("settings:set-setting", (_event, settingId: string, value: unknown) => {
20 | return setSettingValueById(settingId, value);
21 | });
22 |
23 | ipcMain.handle("settings:get-basic-settings", () => {
24 | return {
25 | settings: BasicSettings,
26 | cards: BasicSettingCards
27 | };
28 | });
29 |
30 | export function fireOnSettingsChanged() {
31 | sendMessageToListeners("settings:on-changed");
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/controllers/windows-controller/types/settings.ts:
--------------------------------------------------------------------------------
1 | import { BaseWindow } from "@/controllers/windows-controller/types/base";
2 | import { BrowserWindow, nativeTheme } from "electron";
3 |
4 | export class SettingsWindow extends BaseWindow {
5 | constructor() {
6 | const browserWindow = new BrowserWindow({
7 | width: 800,
8 | minWidth: 800,
9 | height: 600,
10 | minHeight: 600,
11 |
12 | center: true,
13 | show: false,
14 | frame: false,
15 | roundedCorners: true,
16 |
17 | titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "hidden",
18 | titleBarOverlay: {
19 | height: 40,
20 | symbolColor: nativeTheme.shouldUseDarkColors ? "white" : "black",
21 | color: "rgba(0,0,0,0)"
22 | }
23 | });
24 | browserWindow.loadURL("flow-internal://settings/");
25 |
26 | // Use settings.hide's behavior instead of the default one
27 | browserWindow.on("close", () => {
28 | browserWindow.hide();
29 | });
30 |
31 | super("settings", browserWindow, { deferShowUntilAfterLoad: true });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/renderer/src/lib/search.ts:
--------------------------------------------------------------------------------
1 | export function createSearchUrl(query: string) {
2 | return `https://www.google.com/search?q=${encodeURIComponent(query)}`;
3 | }
4 |
5 | type SearchSuggestions = string[];
6 |
7 | interface GoogleSuggestResponse {
8 | 0: string; // Original query
9 | 1: string[]; // Suggested queries
10 | 2: string[]; // Description/unused array
11 | 3: unknown[]; // Unknown/unused array
12 | 4: {
13 | // Metadata
14 | "google:clientdata": {
15 | bpc: boolean;
16 | tlw: boolean;
17 | };
18 | "google:suggestrelevance": number[];
19 | "google:suggestsubtypes": number[][];
20 | "google:suggesttype": string[];
21 | "google:verbatimrelevance": number;
22 | };
23 | }
24 |
25 | export async function getSearchSuggestions(query: string, signal?: AbortSignal): Promise {
26 | const baseURL = `https://suggestqueries.google.com/complete/search?client=chrome&q=${encodeURIComponent(query)}`;
27 | const response = await fetch(baseURL, { signal });
28 | const data = (await response.json()) as GoogleSuggestResponse;
29 | return data[1];
30 | }
31 |
--------------------------------------------------------------------------------
/docs/contributing/hot-reloading.md:
--------------------------------------------------------------------------------
1 | # Hot Reloading
2 |
3 | This guide explains how to set up hot reloading for Flow Browser development.
4 |
5 | ## Prerequisites
6 |
7 | - Ensure you have installed all the dependencies as mentioned in the main [CONTRIBUTING.md](../../CONTRIBUTING.md).
8 | - Make sure port 5173 is available on your system.
9 |
10 | ## Setting Up Hot Reloading
11 |
12 | Hot reloading allows you to see your changes in real-time without manually restarting the application. To enable hot reloading:
13 |
14 | 1. Make sure port 5173 is empty and ready.
15 |
16 | 2. Run the development server in a separate terminal:
17 |
18 | ```bash
19 | bun run dev:server
20 | ```
21 |
22 | 3. In another terminal, start Flow Browser:
23 | ```bash
24 | bun run start
25 | ```
26 |
27 | ## Troubleshooting
28 |
29 | - If you encounter errors about port 5173 being in use, find and terminate the process using that port.
30 | - For macOS/Linux:
31 | ```bash
32 | lsof -i :5173
33 | kill -9
34 | ```
35 | - For Windows:
36 | ```cmd
37 | netstat -ano | findstr :5173
38 | taskkill /PID /F
39 | ```
40 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import tseslint from "@electron-toolkit/eslint-config-ts";
2 | import eslintPluginReact from "eslint-plugin-react";
3 | import eslintPluginReactHooks from "eslint-plugin-react-hooks";
4 | import eslintPluginReactRefresh from "eslint-plugin-react-refresh";
5 |
6 | export default tseslint.config(
7 | { ignores: ["**/node_modules", "**/dist", "**/out", "**/public"] },
8 | tseslint.configs.recommended,
9 | eslintPluginReact.configs.flat.recommended,
10 | eslintPluginReact.configs.flat["jsx-runtime"],
11 | {
12 | settings: {
13 | react: {
14 | version: "detect"
15 | }
16 | }
17 | },
18 | {
19 | files: ["**/*.{ts,tsx}"],
20 | plugins: {
21 | "react-hooks": eslintPluginReactHooks,
22 | "react-refresh": eslintPluginReactRefresh
23 | },
24 | rules: {
25 | ...eslintPluginReactHooks.configs.recommended.rules,
26 | ...eslintPluginReactRefresh.configs.vite.rules
27 | }
28 | },
29 | {
30 | rules: {
31 | "@typescript-eslint/explicit-function-return-type": "off",
32 | "react-refresh/only-export-components": "off"
33 | }
34 | }
35 | );
36 |
--------------------------------------------------------------------------------
/src/renderer/public/edge-surf-game-v2/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Edge Surf v2
14 |
15 |
16 |
17 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/scripts/electron-upgrader/next.ts:
--------------------------------------------------------------------------------
1 | import { findLatestNextMajorVersion, getCommitHashForTag } from "./_modules/github";
2 | import { incrementElectronUpdaterVersionConfiguration, updateBunLock, updatePackageJson } from "./_modules/updater";
3 |
4 | // GRAB RELEASE FROM GITHUB //
5 | const latestNextVersion = await findLatestNextMajorVersion();
6 |
7 | if (!latestNextVersion) {
8 | throw new Error("No version found in next major version");
9 | }
10 |
11 | const commitHash = await getCommitHashForTag(latestNextVersion);
12 |
13 | if (!commitHash) {
14 | throw new Error("No commit hash found");
15 | }
16 |
17 | console.log(`Latest version in next major version: ${latestNextVersion}`);
18 | console.log(`Commit hash: ${commitHash}`);
19 |
20 | // UPDATE PACKAGE.JSON //
21 | updatePackageJson(latestNextVersion);
22 |
23 | console.log("package.json updated!");
24 |
25 | // UPDATE BUN.LOCK //
26 | updateBunLock(latestNextVersion, commitHash);
27 |
28 | console.log("bun.lock updated!");
29 |
30 | // UPDATE ELECTRON UPDATER VERSION CONFIGURATION //
31 | incrementElectronUpdaterVersionConfiguration();
32 |
33 | console.log("Updater Version Configuration updated!");
34 |
--------------------------------------------------------------------------------
/src/renderer/src/lib/omnibox/types.ts:
--------------------------------------------------------------------------------
1 | /** Represents the input state for an autocomplete query. */
2 | export interface AutocompleteInput {
3 | text: string; // The text entered by the user
4 | currentURL?: string; // The URL of the current page (context)
5 | type: "focus" | "keystroke"; // Why the query is being run
6 | preventInlineAutocomplete?: boolean; // Hint to providers
7 | }
8 |
9 | /** Represents a single autocomplete suggestion. */
10 | export interface AutocompleteMatch {
11 | providerName: string; // Name of the provider that generated this match
12 | relevance: number; // Score indicating importance (higher is better)
13 | contents: string; // Text displayed in the main line of the suggestion
14 | description?: string; // Text displayed in the second line (optional)
15 | destinationUrl: string; // The URL to navigate to or the search query URL
16 | type: "history-url" | "zero-suggest" | "verbatim" | "url-what-you-typed" | "search-query" | "open-tab" | "pedal";
17 | isDefault?: boolean; // Hint if this could be the default action on Enter
18 | inlineCompletion?: string; // Text suggested for inline completion in the omnibox
19 | }
20 |
--------------------------------------------------------------------------------
/src/renderer/src/lib/phosphor-icons.tsx:
--------------------------------------------------------------------------------
1 | import { IconEntry, icons } from "@phosphor-icons/core";
2 | import * as IconComponents from "@phosphor-icons/react";
3 | import type { IconProps } from "@phosphor-icons/react";
4 | import { ComponentProps } from "react";
5 |
6 | export const PhosphorIcons = icons as unknown as IconEntry[];
7 |
8 | type IconId = keyof typeof IconComponents;
9 |
10 | export function PhosphorIcon({
11 | id,
12 | fallbackId,
13 | ...props
14 | }: { id: IconId | (string & Record); fallbackId?: string } & IconProps) {
15 | const Icon = IconComponents[id as IconId];
16 |
17 | // Check if Icon is a function (component type)
18 | if ((Icon && typeof Icon === "function") || typeof Icon === "object") {
19 | const IconComponent = Icon as React.ComponentType;
20 | return ;
21 | }
22 |
23 | if (fallbackId && fallbackId !== id) {
24 | return ;
25 | }
26 |
27 | return null;
28 | }
29 |
30 | export function SpaceIcon({ ...props }: ComponentProps) {
31 | return ;
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/controllers/windows-controller/interfaces/settings.ts:
--------------------------------------------------------------------------------
1 | // This is for other controllers to interface with the settings window
2 | import { windowsController } from "@/controllers/windows-controller";
3 |
4 | const settingsWindowManager = () => windowsController.settings;
5 |
6 | export const settings = {
7 | show: async () => {
8 | const window = settingsWindowManager().getSingletonWindow();
9 | await window.show();
10 | },
11 | hide: () => {
12 | const window = settingsWindowManager().getSingletonWindow();
13 | window.destroy();
14 | },
15 | isVisible: () => {
16 | const window = settingsWindowManager().getExistingSingletonWindow();
17 | if (!window) return false;
18 | return window.isVisible();
19 | },
20 | toggle: () => {
21 | if (settings.isVisible()) {
22 | settings.hide();
23 | } else {
24 | settings.show();
25 | }
26 | },
27 |
28 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
29 | sendMessage: (channel: string, ...args: any[]) => {
30 | const window = settingsWindowManager().getExistingSingletonWindow();
31 | if (!window) return;
32 | window.sendMessage(channel, ...args);
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/src/main/controllers/windows-controller/interfaces/onboarding.ts:
--------------------------------------------------------------------------------
1 | // This is for other controllers to interface with the onboarding window
2 | import { windowsController } from "@/controllers/windows-controller";
3 |
4 | const onboardingWindowManager = () => windowsController.onboarding;
5 |
6 | export const onboarding = {
7 | show: async () => {
8 | const window = onboardingWindowManager().getSingletonWindow();
9 | await window.show();
10 | },
11 | hide: () => {
12 | const window = onboardingWindowManager().getSingletonWindow();
13 | window.destroy();
14 | },
15 | isVisible: () => {
16 | const window = onboardingWindowManager().getExistingSingletonWindow();
17 | if (!window) return false;
18 | return window.isVisible();
19 | },
20 | toggle: () => {
21 | if (onboarding.isVisible()) {
22 | onboarding.hide();
23 | } else {
24 | onboarding.show();
25 | }
26 | },
27 |
28 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
29 | sendMessage: (channel: string, ...args: any[]) => {
30 | const window = onboardingWindowManager().getExistingSingletonWindow();
31 | if (!window) return;
32 | window.sendMessage(channel, ...args);
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/intercept-rules/user-agent-transformer.ts:
--------------------------------------------------------------------------------
1 | import { transformUserAgentHeader } from "@/modules/user-agent";
2 | import { createBetterWebRequest } from "@/controllers/sessions-controller/web-requests";
3 | import type { Session } from "electron";
4 |
5 | export function setupUserAgentTransformer(session: Session) {
6 | const webRequest = createBetterWebRequest(session.webRequest, "user-agent-transformer");
7 |
8 | webRequest.onBeforeSendHeaders((details, callback) => {
9 | let updated = false;
10 |
11 | const url = URL.parse(details.url);
12 |
13 | const requestHeaders = details.requestHeaders;
14 | const newHeaders = { ...requestHeaders };
15 | for (const header of Object.keys(requestHeaders)) {
16 | if (header.toLowerCase() == "user-agent") {
17 | const oldValue = requestHeaders[header];
18 | const newValue = transformUserAgentHeader(oldValue, url);
19 | if (oldValue !== newValue) {
20 | newHeaders[header] = newValue;
21 | updated = true;
22 | }
23 | }
24 | }
25 |
26 | if (updated) {
27 | callback({ requestHeaders: newHeaders });
28 | } else {
29 | callback({});
30 | }
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/controllers/app-menu-controller/menu/items/app.ts:
--------------------------------------------------------------------------------
1 | import { MenuItemConstructorOptions } from "electron";
2 | import { settings } from "@/controllers/windows-controller/interfaces/settings";
3 | import { getCurrentShortcut } from "@/modules/shortcuts";
4 | import { defaultBrowserController } from "@/controllers/default-browser-controller";
5 |
6 | export const createAppMenu = (): MenuItemConstructorOptions => ({
7 | role: "appMenu",
8 | submenu: [
9 | { role: "about" },
10 | { type: "separator" },
11 | {
12 | label: "Settings",
13 | accelerator: getCurrentShortcut("browser.openSettings"),
14 | click: () => {
15 | settings.show();
16 | }
17 | },
18 | {
19 | type: "checkbox",
20 | label: "Set as Default Browser",
21 | click: () => {
22 | defaultBrowserController.setDefaultBrowser();
23 | },
24 | checked: defaultBrowserController.isDefaultBrowser(),
25 | enabled: !defaultBrowserController.isDefaultBrowser()
26 | },
27 | { role: "services" },
28 | { type: "separator" },
29 | { role: "hide" },
30 | { role: "hideOthers" },
31 | { role: "showAllTabs" },
32 | { type: "separator" },
33 | { role: "quit" }
34 | ]
35 | });
36 |
--------------------------------------------------------------------------------
/src/main/controllers/windows-controller/utils/close-preventer.ts:
--------------------------------------------------------------------------------
1 | // Prevent Ctrl+W on Windows from closing the entire windows
2 | // So we can process it ourselves by closing the focused tab.
3 |
4 | import { app, type WebContents, webContents } from "electron";
5 | import { menuCloseTab } from "@/controllers/app-menu-controller/menu/items/view";
6 |
7 | const enabled = process.platform === "win32";
8 |
9 | const registeredWebContentIds: Set = new Set();
10 |
11 | function newWebContents(webContents: WebContents) {
12 | if (!enabled) return;
13 |
14 | if (registeredWebContentIds.has(webContents.id)) return;
15 | registeredWebContentIds.add(webContents.id);
16 |
17 | webContents.on("before-input-event", (event, input) => {
18 | if (input.key === "w" && input.control) {
19 | event.preventDefault();
20 |
21 | if (input.type === "keyDown") {
22 | menuCloseTab();
23 | }
24 | }
25 | });
26 | }
27 |
28 | function scan() {
29 | webContents.getAllWebContents().forEach((webContents) => {
30 | newWebContents(webContents);
31 | });
32 | }
33 |
34 | if (enabled) {
35 | scan();
36 | app.on("web-contents-created", (_event, webContents) => {
37 | newWebContents(webContents);
38 | });
39 | }
40 |
--------------------------------------------------------------------------------
/src/renderer/src/components/main/website-favicon.tsx:
--------------------------------------------------------------------------------
1 | import { GlobeIcon } from "lucide-react";
2 | import { useState } from "react";
3 |
4 | export function WebsiteFavicon({ url, favicon, className }: { url: string; favicon?: string; className?: string }) {
5 | const [useFlowUtility, setUseFlowUtility] = useState(true);
6 | const [useCustomFavicon, setUseCustomFavicon] = useState(false);
7 |
8 | if (useFlowUtility) {
9 | const srcUrl = new URL("flow://favicon");
10 | srcUrl.searchParams.set("url", url);
11 | return (
12 |
{
17 | setUseFlowUtility(false);
18 | if (favicon) {
19 | setUseCustomFavicon(true);
20 | }
21 | }}
22 | />
23 | );
24 | }
25 |
26 | if (useCustomFavicon && favicon) {
27 | return (
28 |
setUseCustomFavicon(false)}
33 | crossOrigin="anonymous"
34 | referrerPolicy="no-referrer"
35 | />
36 | );
37 | }
38 |
39 | return ;
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/controllers/quit-controller/handlers/before-quit.ts:
--------------------------------------------------------------------------------
1 | import { loadedProfilesController } from "@/controllers/loaded-profiles-controller";
2 | import { sleep } from "@/modules/utils";
3 |
4 | async function flushSessionsData() {
5 | const promises: Promise[] = [];
6 |
7 | const loadedProfileSessions = loadedProfilesController.loadedProfileSessions;
8 |
9 | for (const session of loadedProfileSessions) {
10 | // Flush storage data
11 | session.flushStorageData();
12 |
13 | // Flush cookies
14 | const cookies = session.cookies;
15 | promises.push(cookies.flushStore());
16 | }
17 |
18 | console.log("Flushed data for", loadedProfileSessions.size, "sessions");
19 |
20 | await Promise.all(promises);
21 | await sleep(50);
22 |
23 | return true;
24 | }
25 |
26 | // Insert Logic here to handle before the app quits
27 | // If the handler returns true, the app will quit normally
28 | // If the handler returns false, the quit will be cancelled
29 | export function beforeQuit(): boolean | Promise {
30 | const flushSessionsDataPromise = flushSessionsData()
31 | .then(() => true)
32 | .catch(() => true);
33 |
34 | return Promise.all([flushSessionsDataPromise]).then((results) => {
35 | return results.every((result) => result);
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/docs/references/omnibox-scores.md:
--------------------------------------------------------------------------------
1 | ## Omnibox Scores
2 |
3 | This document outlines the approximate relevance score ranges used by different omnibox providers. Higher scores appear higher in the suggestion list.
4 |
5 | - **~1100 - 1500:** Open Tab (Switch) - _Encourages switching to existing tabs. Score scales with title/URL similarity._
6 | - **1400 - 1450+:** History URL (Exact Match) - _Very high score for exact URL matches in history._
7 | - **1300:** Verbatim Search / Typed URL - _Fixed score for the direct "Search for..." or typed URL entry._
8 | - **1100 - 1200:** Pedal - _Browser actions triggered by keywords (e.g., "settings"). Score scales with trigger similarity._
9 | - **~900 - 1450:** History URL (Similarity/Prefix Match) - _Score based on URL/title similarity, visit/typed counts, and prefix matching._
10 | - **~600 - 1000:** Search Suggestion - _Fetched suggestions, ranked by provider order and similarity to input._
11 | - **350 - 800:** Zero Suggest (Recent Tabs) - _Suggestions shown before typing, ranked by tab recency._
12 | - **500 - 700:** Zero Suggest (Most Visited History) - _Suggestions shown before typing, ranked by visit count._
13 |
14 | _Note: Ranges starting with '~' indicate approximate values as the final score can depend dynamically on factors like string similarity or history counts._
15 |
--------------------------------------------------------------------------------
/src/main/modules/flags.ts:
--------------------------------------------------------------------------------
1 | import { app } from "electron";
2 | import { DEBUG_AREA } from "./output";
3 |
4 | type Flags = {
5 | SCRUBBED_USER_AGENT: boolean;
6 | ERROR_PAGE_LOAD_MODE: "replace" | "load";
7 | SHOW_DEBUG_PRINTS: boolean;
8 | SHOW_DEBUG_ERRORS: boolean | DEBUG_AREA[];
9 | DEBUG_DISABLE_TAB_VIEW: boolean;
10 | DEBUG_HOT_RELOAD_FRONTEND: boolean;
11 | SHOW_DEBUG_DEVTOOLS: boolean;
12 | GLANCE_ENABLED: boolean;
13 | FAVICONS_REMOVE_PATH: boolean;
14 | };
15 |
16 | export const FLAGS: Flags = {
17 | // Transform the user agent
18 | SCRUBBED_USER_AGENT: true,
19 |
20 | // Replace - Use window.location.replace to load the error page.
21 | // Load - Add the page to the history stack by loading it normally.
22 | ERROR_PAGE_LOAD_MODE: "replace",
23 |
24 | // Debug: Prints & Errors
25 | SHOW_DEBUG_PRINTS: true,
26 | SHOW_DEBUG_ERRORS: true,
27 | SHOW_DEBUG_DEVTOOLS: !app.isPackaged,
28 |
29 | // Debug: Disable the tab view
30 | DEBUG_DISABLE_TAB_VIEW: false,
31 |
32 | // Debug: Enable the hot reload feature for frontend (Experimental / Unstable)
33 | DEBUG_HOT_RELOAD_FRONTEND: true,
34 |
35 | // Glance: Enable the glance feature
36 | GLANCE_ENABLED: false,
37 |
38 | // Favicons: Remove the path from the favicon URL
39 | FAVICONS_REMOVE_PATH: true
40 | };
41 |
--------------------------------------------------------------------------------
/src/shared/types/tabs.ts:
--------------------------------------------------------------------------------
1 | export type TabGroupMode = "normal" | "glance" | "split";
2 |
3 | export type NavigationEntry = {
4 | title: string;
5 | url: string;
6 | };
7 |
8 | export type TabData = {
9 | id: number;
10 | uniqueId: string;
11 | createdAt: number;
12 | lastActiveAt: number;
13 | position: number;
14 |
15 | profileId: string;
16 | spaceId: string;
17 | windowId: number;
18 |
19 | title: string;
20 | url: string;
21 | isLoading: boolean;
22 | audible: boolean;
23 | muted: boolean;
24 | fullScreen: boolean;
25 | isPictureInPicture: boolean;
26 | faviconURL: string | null;
27 | asleep: boolean;
28 |
29 | navHistory: NavigationEntry[];
30 | navHistoryIndex: number;
31 | };
32 |
33 | export type TabGroupData = {
34 | id: number;
35 | mode: TabGroupMode;
36 | profileId: string;
37 | spaceId: string;
38 | tabIds: number[];
39 | glanceFrontTabId?: number;
40 | position: number;
41 | };
42 |
43 | export type WindowFocusedTabIds = {
44 | [spaceId: string]: number;
45 | };
46 |
47 | export type WindowActiveTabIds = {
48 | [spaceId: string]: number[];
49 | };
50 |
51 | export type WindowTabsData = {
52 | tabs: TabData[];
53 | tabGroups: TabGroupData[];
54 | focusedTabIds: WindowFocusedTabIds;
55 | activeTabIds: WindowActiveTabIds;
56 | };
57 |
--------------------------------------------------------------------------------
/src/renderer/src/components/settings/settings-sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "motion/react";
4 | import { LucideIcon } from "lucide-react";
5 |
6 | interface Section {
7 | id: string;
8 | label: string;
9 | icon: React.ReactElement;
10 | }
11 |
12 | interface SettingsSidebarProps {
13 | activeSection: string;
14 | setActiveSection: (section: string) => void;
15 | sections: Section[];
16 | }
17 |
18 | export function SettingsSidebar({ activeSection, setActiveSection, sections }: SettingsSidebarProps) {
19 | return (
20 |
21 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Switch as SwitchPrimitive } from "radix-ui";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | function Switch({ className, ...props }: React.ComponentProps) {
9 | return (
10 |
18 |
24 |
25 | );
26 | }
27 |
28 | export { Switch };
29 |
--------------------------------------------------------------------------------
/src/renderer/src/components/browser-ui/sidebar/content/new-tab-button.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "motion/react";
2 | import { useState } from "react";
3 | import { SidebarMenuButton } from "@/components/ui/resizable-sidebar";
4 | import { PlusIcon } from "lucide-react";
5 | import { SIDEBAR_HOVER_COLOR } from "@/components/browser-ui/browser-sidebar";
6 | import { cn } from "@/lib/utils";
7 |
8 | const MotionSidebarMenuButton = motion(SidebarMenuButton);
9 |
10 | export function NewTabButton() {
11 | const [isPressed, setIsPressed] = useState(false);
12 |
13 | const handleMouseDown = () => {
14 | setIsPressed(true);
15 | };
16 |
17 | const handleNewTab = () => {
18 | flow.newTab.open();
19 | };
20 |
21 | return (
22 | setIsPressed(false)}
29 | transition={{
30 | scale: { type: "spring", stiffness: 600, damping: 20 }
31 | }}
32 | className={cn(SIDEBAR_HOVER_COLOR, "text-black/50 dark:text-muted-foreground")}
33 | >
34 |
35 | New Tab
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/renderer/src/components/onboarding/screen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export function OnboardingScreen({ children }: { children?: React.ReactNode }) {
4 | return (
5 |
6 | {/* Set document title */}
7 |
Onboarding | Flow Browser
8 |
9 | {/* Static gradient orbs */}
10 |
14 |
15 |
19 |
20 | {/* Container for the actual screen content */}
21 |
22 | {/* Render children passed to the component */}
23 | {children}
24 |
25 |
26 | );
27 | }
28 |
29 | // Optional: export default if this is the primary export of the file
30 | export default OnboardingScreen;
31 |
--------------------------------------------------------------------------------
/src/main/ipc/app/updates.ts:
--------------------------------------------------------------------------------
1 | import { autoUpdateController } from "@/controllers/auto-update-controller";
2 | import { sendMessageToListeners } from "@/ipc/listeners-manager";
3 | import { ipcMain } from "electron";
4 | import { UpdateStatus } from "~/types/updates";
5 |
6 | ipcMain.handle("updates:is-auto-update-supported", () => {
7 | return autoUpdateController.isAutoUpdateSupported(process.platform);
8 | });
9 |
10 | ipcMain.handle("updates:get-update-status", () => {
11 | return autoUpdateController.getUpdateStatus();
12 | });
13 |
14 | ipcMain.handle("updates:check-for-updates", async () => {
15 | try {
16 | const result = await autoUpdateController.checkForUpdates();
17 | if (result?.isUpdateAvailable) {
18 | return true;
19 | }
20 | return false;
21 | } catch {
22 | return false;
23 | }
24 | });
25 |
26 | ipcMain.handle("updates:download-update", () => {
27 | return autoUpdateController.downloadUpdate();
28 | });
29 |
30 | ipcMain.handle("updates:install-update", () => {
31 | return autoUpdateController.installUpdate();
32 | });
33 |
34 | export function fireUpdateStatusChanged(updateStatus: UpdateStatus) {
35 | sendMessageToListeners("updates:on-update-status-changed", updateStatus);
36 | }
37 | autoUpdateController.on("status-changed", () => {
38 | fireUpdateStatusChanged(autoUpdateController.getUpdateStatus());
39 | });
40 |
--------------------------------------------------------------------------------
/src/main/ipc/session/profiles.ts:
--------------------------------------------------------------------------------
1 | import { profilesController, ProfileData } from "@/controllers/profiles-controller";
2 | import { ipcMain } from "electron";
3 | import { spacesController } from "@/controllers/spaces-controller";
4 | import { browserWindowsController } from "@/controllers/windows-controller/interfaces/browser";
5 |
6 | ipcMain.handle("profiles:get-all", async () => {
7 | return await profilesController.getAll();
8 | });
9 |
10 | ipcMain.handle("profiles:create", async (_event, profileName: string) => {
11 | return await profilesController.create(profileName);
12 | });
13 |
14 | ipcMain.handle("profiles:update", async (_event, profileId: string, profileData: Partial) => {
15 | console.log("Updating profile:", profileId, profileData);
16 | return await profilesController.update(profileId, profileData);
17 | });
18 |
19 | ipcMain.handle("profiles:delete", async (_event, profileId: string) => {
20 | return await profilesController.delete(profileId);
21 | });
22 |
23 | ipcMain.handle("profile:get-using", async (event) => {
24 | const window = browserWindowsController.getWindowFromWebContents(event.sender);
25 | if (window) {
26 | const spaceId = window.currentSpaceId;
27 | if (spaceId) {
28 | const space = await spacesController.get(spaceId);
29 | return space?.profileId;
30 | }
31 | }
32 | return null;
33 | });
34 |
--------------------------------------------------------------------------------
/src/main/ipc/app/new-tab.ts:
--------------------------------------------------------------------------------
1 | import { getSettingValueById } from "@/saving/settings";
2 | import { spacesController } from "@/controllers/spaces-controller";
3 | import { ipcMain } from "electron";
4 | import { browserWindowsController } from "@/controllers/windows-controller/interfaces/browser";
5 | import { BrowserWindow } from "@/controllers/windows-controller/types";
6 | import { tabsController } from "@/controllers/tabs-controller";
7 |
8 | export function openNewTab(window: BrowserWindow) {
9 | const omnibox = window.omnibox;
10 |
11 | if (getSettingValueById("newTabMode") === "omnibox") {
12 | if (omnibox.isVisible()) {
13 | omnibox.hide();
14 | } else {
15 | omnibox.loadInterface(null);
16 | omnibox.setBounds(null);
17 | omnibox.show();
18 | }
19 | } else {
20 | const spaceId = window.currentSpaceId;
21 | if (!spaceId) return;
22 |
23 | spacesController.get(spaceId).then(async (space) => {
24 | if (!space) return;
25 |
26 | const tab = await tabsController.createTab(window.id, space.profileId, spaceId);
27 | tabsController.setActiveTab(tab);
28 | });
29 | }
30 | }
31 |
32 | ipcMain.on("new-tab:open", (event) => {
33 | const webContents = event.sender;
34 | const win = browserWindowsController.getWindowFromWebContents(webContents);
35 | if (!win) return;
36 |
37 | return openNewTab(win);
38 | });
39 |
--------------------------------------------------------------------------------
/src/renderer/src/components/settings/sections/shortcuts/components/search-header.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from "@/components/ui/input";
2 | import { Button } from "@/components/ui/button";
3 | import { RotateCcwIcon, SearchIcon } from "lucide-react";
4 |
5 | interface SearchHeaderProps {
6 | searchTerm: string;
7 | onSearchChange: (value: string) => void;
8 | onResetClick: () => void;
9 | isLoading: boolean;
10 | }
11 |
12 | export function SearchHeader({ searchTerm, onSearchChange, onResetClick, isLoading }: SearchHeaderProps) {
13 | return (
14 |
15 |
16 |
17 | onSearchChange(e.target.value)}
23 | />
24 |
25 |
26 |
30 |
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/controllers/app-menu-controller/menu/items/archive.ts:
--------------------------------------------------------------------------------
1 | import { MenuItemConstructorOptions } from "electron";
2 | import { getTabWcFromFocusedWindow } from "../helpers";
3 | import { getCurrentShortcut } from "@/modules/shortcuts";
4 |
5 | export const createArchiveMenu = (): MenuItemConstructorOptions => ({
6 | label: "Archive", // Consider renaming to "History" or "Navigation" if more appropriate
7 | submenu: [
8 | {
9 | label: "Go Back",
10 | accelerator: getCurrentShortcut("navigation.goBack"),
11 | click: () => {
12 | const tabWc = getTabWcFromFocusedWindow();
13 | if (!tabWc) return;
14 |
15 | const navigationHistory = tabWc.navigationHistory;
16 | // Check if back navigation is possible before calling goBack
17 | if (navigationHistory.canGoBack()) {
18 | navigationHistory.goBack();
19 | }
20 | }
21 | },
22 | {
23 | label: "Go Forward",
24 | accelerator: getCurrentShortcut("navigation.goForward"),
25 | click: () => {
26 | const tabWc = getTabWcFromFocusedWindow();
27 | if (!tabWc) return;
28 |
29 | const navigationHistory = tabWc.navigationHistory;
30 | // Check if forward navigation is possible before calling goForward
31 | if (navigationHistory.canGoForward()) {
32 | navigationHistory.goForward();
33 | }
34 | }
35 | }
36 | ]
37 | });
38 |
--------------------------------------------------------------------------------
/src/main/app/platform.ts:
--------------------------------------------------------------------------------
1 | import { app, Menu, MenuItem } from "electron";
2 | import { debugPrint } from "@/modules/output";
3 | import { browserWindowsController } from "@/controllers/windows-controller/interfaces/browser";
4 |
5 | function setupWindowsUserTasks() {
6 | app.setUserTasks([
7 | {
8 | program: process.execPath,
9 | arguments: "--new-window",
10 | iconPath: process.execPath,
11 | iconIndex: 0,
12 | title: "New Window",
13 | description: "Create a new window"
14 | }
15 | ]);
16 | }
17 |
18 | function setupMacOSDock() {
19 | const dockMenu = new Menu();
20 |
21 | dockMenu.append(
22 | new MenuItem({
23 | label: "New Window",
24 | click: () => {
25 | browserWindowsController.create();
26 | }
27 | })
28 | );
29 |
30 | dockMenu.append(
31 | new MenuItem({
32 | label: "New Incognito Window",
33 | enabled: false
34 | })
35 | );
36 |
37 | app.whenReady().then(() => {
38 | if ("dock" in app) {
39 | app.dock?.setMenu(dockMenu);
40 | }
41 | });
42 | }
43 |
44 | export function setupPlatformIntegration() {
45 | if (process.platform === "win32") {
46 | setupWindowsUserTasks();
47 | debugPrint("INITIALIZATION", "setup windows user tasks finished");
48 | } else if (process.platform === "darwin") {
49 | setupMacOSDock();
50 | debugPrint("INITIALIZATION", "setup macOS dock finished");
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/intercept-rules/cors-bypass-custom-protocols.ts:
--------------------------------------------------------------------------------
1 | import { createBetterWebRequest } from "@/controllers/sessions-controller/web-requests";
2 | import type { Session } from "electron";
3 |
4 | /**
5 | * Setup CORS bypass for whitelisted protocols
6 | * @param session
7 | */
8 | export function setupCorsBypassForCustomProtocols(session: Session) {
9 | const webRequest = createBetterWebRequest(session.webRequest, "bypass-cors");
10 |
11 | const WHITELISTED_PROTOCOLS = ["flow:", "flow-internal:"];
12 |
13 | webRequest.onHeadersReceived((details, callback) => {
14 | const currentUrl = details.webContents?.getURL();
15 | const protocol = URL.parse(currentUrl ?? "")?.protocol;
16 |
17 | if (protocol && WHITELISTED_PROTOCOLS.includes(protocol)) {
18 | const newResponseHeaders = { ...details.responseHeaders };
19 |
20 | // Remove all Access-Control-Allow-Origin headers in different cases
21 | for (const header of Object.keys(newResponseHeaders)) {
22 | if (header.toLowerCase() == "access-control-allow-origin") {
23 | newResponseHeaders[header] = [];
24 | }
25 | }
26 |
27 | // Add the Access-Control-Allow-Origin header back with a wildcard
28 | newResponseHeaders["Access-Control-Allow-Origin"] = ["*"];
29 |
30 | callback({ responseHeaders: newResponseHeaders });
31 | return;
32 | }
33 |
34 | callback({});
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/renderer/src/components/settings/sections/shortcuts/components/reset-dialog.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogContent,
4 | DialogDescription,
5 | DialogFooter,
6 | DialogHeader,
7 | DialogTitle
8 | } from "@/components/ui/dialog";
9 | import { Button } from "@/components/ui/button";
10 |
11 | interface ResetDialogProps {
12 | isOpen: boolean;
13 | onOpenChange: (open: boolean) => void;
14 | onConfirm: () => void;
15 | }
16 |
17 | export function ResetDialog({ isOpen, onOpenChange, onConfirm }: ResetDialogProps) {
18 | return (
19 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/app/updates.ts:
--------------------------------------------------------------------------------
1 | import type { IPCListener } from "~/flow/types";
2 | import type { UpdateStatus } from "~/types/updates";
3 |
4 | // API //
5 | export interface FlowUpdatesAPI {
6 | /**
7 | * Checks if automatic updates are supported on the current platform
8 | * @returns Promise resolving to a boolean indicating support status
9 | */
10 | isAutoUpdateSupported: () => Promise;
11 |
12 | /**
13 | * Gets the current update status
14 | * @returns Promise resolving to the current UpdateStatus
15 | */
16 | getUpdateStatus: () => Promise;
17 |
18 | /**
19 | * Listener for update status change events
20 | * @param callback Function called when update status changes
21 | * @returns Cleanup function
22 | */
23 | onUpdateStatusChanged: IPCListener<[UpdateStatus]>;
24 |
25 | /**
26 | * Initiates a check for application updates
27 | * @returns Promise resolving to a boolean indicating if an update is available
28 | */
29 | checkForUpdates: () => Promise;
30 |
31 | /**
32 | * Downloads the latest update
33 | * @returns Promise resolving to a boolean indicating if the download was started
34 | */
35 | downloadUpdate: () => Promise;
36 |
37 | /**
38 | * Installs the latest update
39 | * @returns Promise resolving to a boolean indicating if the installation was started
40 | */
41 | installUpdate: () => Promise;
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/controllers/posthog-controller/posthog-error-capture-sdk/reduceable-cache.ts:
--------------------------------------------------------------------------------
1 | // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
2 | // Licensed under the MIT License
3 |
4 | /** A simple Least Recently Used map */
5 | export class ReduceableCache {
6 | private readonly _cache: Map;
7 |
8 | public constructor(private readonly _maxSize: number) {
9 | this._cache = new Map();
10 | }
11 |
12 | /** Get an entry or undefined if it was not in the cache. Re-inserts to update the recently used order */
13 | public get(key: K): V | undefined {
14 | const value = this._cache.get(key);
15 | if (value === undefined) {
16 | return undefined;
17 | }
18 | // Remove and re-insert to update the order
19 | this._cache.delete(key);
20 | this._cache.set(key, value);
21 | return value;
22 | }
23 |
24 | /** Insert an entry and evict an older entry if we've reached maxSize */
25 | public set(key: K, value: V): void {
26 | this._cache.set(key, value);
27 | }
28 |
29 | /** Remove an entry and return the entry if it was in the cache */
30 | public reduce(): void {
31 | while (this._cache.size >= this._maxSize) {
32 | const value = this._cache.keys().next().value;
33 | if (value) {
34 | // keys() returns an iterator in insertion order so keys().next() gives us the oldest key
35 | this._cache.delete(value);
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/pdf-viewer/pdf-viewer/Toolbar/DocumentInfo.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import type { TUsePDFSlickStore } from "@pdfslick/react";
3 | import { VscInfo } from "@/components/react-icons/vsc";
4 | import DocumentInfoModal from "./DocumentInfoModal";
5 | import Tooltip from "../Tooltip";
6 |
7 | type DocumentInfoProps = {
8 | usePDFSlickStore: TUsePDFSlickStore;
9 | };
10 |
11 | export default function DocumentInfo({ usePDFSlickStore }: DocumentInfoProps) {
12 | const [isOpen, setIsOpen] = useState(false);
13 |
14 | function closeModal() {
15 | setIsOpen(false);
16 | }
17 |
18 | function openModal() {
19 | setIsOpen(true);
20 | }
21 |
22 | return (
23 | <>
24 |
35 |
36 |
37 | >
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/controllers/posthog-controller/posthog-error-capture-sdk/type-checking.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | // Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
4 | // Licensed under the MIT License
5 |
6 | import { PolymorphicEvent } from "./types";
7 |
8 | export function isEvent(candidate: unknown): candidate is PolymorphicEvent {
9 | return typeof Event !== "undefined" && isInstanceOf(candidate, Event);
10 | }
11 |
12 | export function isPlainObject(candidate: unknown): candidate is Record {
13 | return isBuiltin(candidate, "Object");
14 | }
15 |
16 | export function isError(candidate: unknown): candidate is Error {
17 | switch (Object.prototype.toString.call(candidate)) {
18 | case "[object Error]":
19 | case "[object Exception]":
20 | case "[object DOMException]":
21 | case "[object WebAssembly.Exception]":
22 | return true;
23 | default:
24 | return isInstanceOf(candidate, Error);
25 | }
26 | }
27 |
28 | export function isInstanceOf(candidate: unknown, base: any): boolean {
29 | try {
30 | return candidate instanceof base;
31 | } catch {
32 | return false;
33 | }
34 | }
35 |
36 | export function isErrorEvent(event: unknown): boolean {
37 | return isBuiltin(event, "ErrorEvent");
38 | }
39 |
40 | export function isBuiltin(candidate: unknown, className: string): boolean {
41 | return Object.prototype.toString.call(candidate) === `[object ${className}]`;
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/modules/user-agent.ts:
--------------------------------------------------------------------------------
1 | import { FLAGS } from "@/modules/flags";
2 | import { app } from "electron";
3 |
4 | const EDGE_USER_AGENT = "Edg/136.0.3240.76";
5 |
6 | export function transformUserAgentHeader(userAgent: string, url: URL | null) {
7 | if (!FLAGS.SCRUBBED_USER_AGENT) {
8 | return userAgent;
9 | }
10 |
11 | // Edge User Agent:
12 | // - Causes a few issues with Google Auth
13 | const addEdgeUserAgent = false;
14 |
15 | // Remove Electron User Agent:
16 | // - Causes issues with Spotify
17 | const removeElectronUserAgent = true;
18 |
19 | // Remove App User Agent:
20 | // - Flow will be less identifiable
21 | let removeAppUserAgent = false;
22 |
23 | if (url) {
24 | const hostname = url.hostname.toLowerCase();
25 |
26 | // WhatsApp does not like the 'Flow' User Agent
27 | // Removing it fixes the issue and finally lets us use https://web.whatsapp.com/
28 | if (hostname.endsWith(".whatsapp.com")) {
29 | removeAppUserAgent = true;
30 | }
31 | }
32 |
33 | if (removeElectronUserAgent) {
34 | userAgent = userAgent.replace(/\sElectron\/\S+/, "");
35 | }
36 |
37 | if (removeAppUserAgent) {
38 | const appName = app.getName();
39 | userAgent = userAgent.replace(new RegExp(`\\s${appName}/\\S+`, "i"), "");
40 | }
41 |
42 | const hasEdgeUserAgent = userAgent.includes(EDGE_USER_AGENT);
43 | if (addEdgeUserAgent && !hasEdgeUserAgent) {
44 | userAgent = `${userAgent} ${EDGE_USER_AGENT}`;
45 | }
46 |
47 | return userAgent;
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/app/urls.ts:
--------------------------------------------------------------------------------
1 | import { tabsController } from "@/controllers/tabs-controller";
2 | import { browserWindowsController } from "@/controllers/windows-controller/interfaces/browser";
3 | import { debugPrint } from "@/modules/output";
4 |
5 | export function isValidOpenerUrl(url: string): boolean {
6 | const urlObject = URL.parse(url);
7 | if (!urlObject) return false;
8 |
9 | const VALID_PROTOCOLS = ["http:", "https:"];
10 | if (!VALID_PROTOCOLS.includes(urlObject.protocol)) return false;
11 |
12 | return true;
13 | }
14 |
15 | export async function handleOpenUrl(useNewWindow: boolean, url: string) {
16 | // Find a window to use, show + focus it
17 | const windows = browserWindowsController.getWindows();
18 | const focusedWindow = browserWindowsController.getFocusedWindow();
19 | const hasWindows = windows.length > 0;
20 |
21 | const shouldCreate = useNewWindow || !hasWindows;
22 | const window = shouldCreate ? await browserWindowsController.create() : focusedWindow ? focusedWindow : windows[0];
23 |
24 | window.show(true);
25 |
26 | // Create a new tab
27 | const tab = await tabsController.createTab(window.id);
28 | tab.loadURL(url);
29 | tabsController.setActiveTab(tab);
30 | }
31 |
32 | export function processInitialUrl() {
33 | const commandLine = process.argv.slice(1);
34 | const targetUrl = commandLine.pop();
35 | if (targetUrl && isValidOpenerUrl(targetUrl)) {
36 | handleOpenUrl(false, targetUrl);
37 | debugPrint("INITIALIZATION", "initial URL handled");
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/controllers/quit-controller/index.ts:
--------------------------------------------------------------------------------
1 | // This controller executes quit handlers before letting the app quit.
2 |
3 | import { app } from "electron";
4 | import { canQuit } from "./handlers/can-quit";
5 | import { beforeQuit } from "./handlers/before-quit";
6 |
7 | type BeforeQuitHandlerState = "idle" | "running" | "completed";
8 |
9 | class QuitController {
10 | public beforeQuitHandlerState: BeforeQuitHandlerState;
11 |
12 | constructor() {
13 | this.beforeQuitHandlerState = "idle";
14 | this._handleBeforeQuit();
15 | }
16 |
17 | private _handleBeforeQuit() {
18 | app.on("before-quit", (event) => {
19 | if (this.beforeQuitHandlerState === "completed") {
20 | // Let the app quit normally
21 | return;
22 | }
23 |
24 | // Prevent the app from quitting if the handler is not completed
25 | event.preventDefault();
26 |
27 | // If the handler is idle and the app can quit, run it
28 | if (this.beforeQuitHandlerState === "idle" && canQuit()) {
29 | this.beforeQuitHandlerState = "running";
30 |
31 | const handleBeforeQuit = async () => {
32 | const result = await beforeQuit();
33 | if (result) {
34 | this.beforeQuitHandlerState = "completed";
35 | app.quit();
36 | } else {
37 | this.beforeQuitHandlerState = "idle";
38 | }
39 | };
40 | handleBeforeQuit();
41 | }
42 | });
43 | }
44 | }
45 |
46 | export const quitController = new QuitController();
47 |
--------------------------------------------------------------------------------
/src/renderer/src/components/settings/sections/spaces/space-card.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "motion/react";
2 | import type { Space } from "~/flow/interfaces/sessions/spaces";
3 | import { SpaceIcon } from "@/lib/phosphor-icons";
4 |
5 | // ==============================
6 | // Space Card Component
7 | // ==============================
8 | interface SpaceCardProps {
9 | space: Space;
10 | activateEdit: () => void;
11 | }
12 |
13 | export function SpaceCard({ space, activateEdit }: SpaceCardProps) {
14 | return (
15 | activateEdit()}
21 | >
22 |
31 |
32 |
33 |
34 |
{space.name}
35 |
ID: {space.id}
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/ipc/window/omnibox.ts:
--------------------------------------------------------------------------------
1 | import { browserWindowsManager, windowsController } from "@/controllers/windows-controller";
2 | import { debugPrint } from "@/modules/output";
3 | import { ipcMain } from "electron";
4 |
5 | ipcMain.on("omnibox:show", (event, bounds: Electron.Rectangle | null, params: { [key: string]: string } | null) => {
6 | debugPrint(
7 | "OMNIBOX",
8 | `IPC: show-omnibox received with bounds: ${JSON.stringify(bounds)} and params: ${JSON.stringify(params)}`
9 | );
10 |
11 | const parentWindow = windowsController.getWindowFromWebContents(event.sender);
12 | if (!parentWindow) {
13 | debugPrint("OMNIBOX", "Parent window not found");
14 | return;
15 | }
16 | if (!browserWindowsManager.isInstanceOf(parentWindow)) {
17 | debugPrint("OMNIBOX", "Parent window is not a BrowserWindow");
18 | return;
19 | }
20 |
21 | const omnibox = parentWindow.omnibox;
22 | omnibox.setBounds(bounds);
23 | omnibox.loadInterface(params);
24 | omnibox.show();
25 | });
26 |
27 | ipcMain.on("omnibox:hide", (event) => {
28 | debugPrint("OMNIBOX", "IPC: hide-omnibox received");
29 |
30 | const parentWindow = windowsController.getWindowFromWebContents(event.sender);
31 | if (!parentWindow) {
32 | debugPrint("OMNIBOX", "Parent window not found");
33 | return;
34 | }
35 | if (!browserWindowsManager.isInstanceOf(parentWindow)) {
36 | debugPrint("OMNIBOX", "Parent window is not a BrowserWindow");
37 | return;
38 | }
39 |
40 | const omnibox = parentWindow.omnibox;
41 | omnibox.hide();
42 | });
43 |
--------------------------------------------------------------------------------
/src/renderer/src/components/onboarding/main.tsx:
--------------------------------------------------------------------------------
1 | import { OnboardingScreen } from "@/components/onboarding/screen";
2 | import { OnboardingFinish } from "@/components/onboarding/stages/finish";
3 | import { OnboardingIcon } from "@/components/onboarding/stages/icon";
4 | import { OnboardingNewTab } from "@/components/onboarding/stages/new-tab";
5 | import { OnboardingInitialSpace } from "@/components/onboarding/stages/initial-space/main";
6 | import { OnboardingSidebarCollapseMode } from "@/components/onboarding/stages/sidebar-collapse-mode";
7 | import { OnboardingWelcome } from "@/components/onboarding/stages/welcome";
8 | import { AnimatePresence } from "motion/react";
9 | import { useState } from "react";
10 |
11 | export type OnboardingAdvanceCallback = () => void;
12 |
13 | const stages = [
14 | // Start
15 | OnboardingWelcome,
16 |
17 | // Create Initial Space
18 | OnboardingInitialSpace,
19 |
20 | // Customizations
21 | OnboardingIcon,
22 | OnboardingNewTab,
23 | OnboardingSidebarCollapseMode,
24 |
25 | // Finish
26 | OnboardingFinish
27 | ];
28 |
29 | export function OnboardingMain() {
30 | const [stage, setStage] = useState(0);
31 |
32 | const advance = () => {
33 | setStage(stage + 1);
34 | };
35 |
36 | const Stage = stages[stage];
37 | if (!Stage) {
38 | flow.onboarding.finish();
39 | }
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
5 | import { CircleIcon } from "lucide-react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | function RadioGroup({ className, ...props }: React.ComponentProps) {
10 | return ;
11 | }
12 |
13 | function RadioGroupItem({ className, ...props }: React.ComponentProps) {
14 | return (
15 |
23 |
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | export { RadioGroup, RadioGroupItem };
34 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/pdf-viewer/pdf-viewer/Thumbsbar/Attachments.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useRef } from "react";
2 | import { type TUsePDFSlickStore } from "@pdfslick/react";
3 | import clsx from "clsx";
4 |
5 | type AttachmentsProps = {
6 | usePDFSlickStore: TUsePDFSlickStore;
7 | show: boolean;
8 | };
9 |
10 | type AttachmentButtonProps = {
11 | usePDFSlickStore: TUsePDFSlickStore;
12 | filename: string;
13 | content: Uint8Array;
14 | };
15 |
16 | const AttachmentButton: FC = ({ usePDFSlickStore, filename, content }) => {
17 | const pdfSlick = usePDFSlickStore((s) => s.pdfSlick);
18 | const ref = useRef(null);
19 |
20 | return (
21 |
28 | );
29 | };
30 |
31 | const Attachments = ({ usePDFSlickStore, show }: AttachmentsProps) => {
32 | const attachments = usePDFSlickStore((s) => s.attachments);
33 |
34 | return (
35 |
36 |
37 | {Array.from(attachments.entries()).map(([key, { filename, content }]) => (
38 |
39 | ))}
40 |
41 |
42 | );
43 | };
44 |
45 | export default Attachments;
46 |
--------------------------------------------------------------------------------
/src/main/controllers/app-menu-controller/menu/helpers.ts:
--------------------------------------------------------------------------------
1 | import { tabsController } from "@/controllers/tabs-controller";
2 | import { browserWindowsManager, windowsController } from "@/controllers/windows-controller";
3 | import { BaseWindow } from "@/controllers/windows-controller/types";
4 | import { WebContents } from "electron";
5 |
6 | export const getFocusedWindow = () => {
7 | return windowsController.getFocused();
8 | };
9 |
10 | export const getFocusedBrowserWindow = () => {
11 | const window = getFocusedWindow();
12 |
13 | if (!window) return null;
14 | if (!browserWindowsManager.isInstanceOf(window)) {
15 | return null;
16 | }
17 |
18 | return window;
19 | };
20 |
21 | export const getTab = (window?: BaseWindow) => {
22 | if (!window) return null;
23 | if (!browserWindowsManager.isInstanceOf(window)) {
24 | return null;
25 | }
26 |
27 | const windowId = window.id;
28 |
29 | const spaceId = window.currentSpaceId;
30 | if (!spaceId) return null;
31 |
32 | const tab = tabsController.getFocusedTab(windowId, spaceId);
33 | if (!tab) return null;
34 | return tab;
35 | };
36 |
37 | export const getTabFromFocusedWindow = () => {
38 | const winData = getFocusedWindow();
39 | if (!winData) return null;
40 | return getTab(winData);
41 | };
42 |
43 | export const getTabWc = (window: BaseWindow): WebContents | null => {
44 | const tab = getTab(window);
45 | if (!tab) return null;
46 | return tab.webContents;
47 | };
48 |
49 | export const getTabWcFromFocusedWindow = (): WebContents | null => {
50 | const window = getFocusedWindow();
51 | if (!window) return null;
52 | return getTabWc(window);
53 | };
54 |
--------------------------------------------------------------------------------
/src/main/controllers/default-browser-controller/index.ts:
--------------------------------------------------------------------------------
1 | import { app } from "electron";
2 | import { registerAppForCurrentUserOnWindows } from "./windows-handler";
3 | import { exec } from "child_process";
4 |
5 | class DefaultBrowserController {
6 | public isDefaultBrowser(): boolean {
7 | if (process.platform === "win32") {
8 | return false;
9 | }
10 |
11 | const httpIsDefault = app.isDefaultProtocolClient("http");
12 | const httpsIsDefault = app.isDefaultProtocolClient("https");
13 |
14 | return httpIsDefault && httpsIsDefault;
15 | }
16 |
17 | public setDefaultBrowser(): Promise {
18 | app.setAsDefaultProtocolClient("http");
19 | app.setAsDefaultProtocolClient("https");
20 |
21 | return new Promise((resolve) => {
22 | if (process.platform === "linux" || process.platform.includes("bsd")) {
23 | exec("xdg-settings set default-web-browser flow.desktop", (err) => {
24 | if (err?.message) {
25 | resolve(false);
26 | } else {
27 | resolve(true);
28 | }
29 | });
30 | return;
31 | } else if (process.platform === "win32") {
32 | registerAppForCurrentUserOnWindows().then(resolve);
33 | return;
34 | } else if (process.platform === "darwin") {
35 | // Electron API should be enough to show a popup for default app request
36 | resolve(true);
37 | return;
38 | }
39 |
40 | // If we don't know how to set the default browser, return false
41 | resolve(false);
42 | });
43 | }
44 | }
45 |
46 | export const defaultBrowserController = new DefaultBrowserController();
47 |
--------------------------------------------------------------------------------
/src/renderer/src/components/settings/sections/about/section.tsx:
--------------------------------------------------------------------------------
1 | import { BrowserInfoCard } from "./browser-info-card";
2 | import { Button } from "@/components/ui/button";
3 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
4 |
5 | export function AboutSettings() {
6 | return (
7 |
8 |
9 |
About
10 |
Information about your browser
11 |
12 |
13 |
14 |
15 | {/* eslint-disable-next-line no-constant-binary-expression */}
16 | {false && (
17 | <>
18 |
19 |
20 | Troubleshooting
21 | Tools to help resolve issues
22 |
23 |
24 |
27 |
30 |
33 |
36 |
37 |
38 | >
39 | )}
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/ipc/README.md:
--------------------------------------------------------------------------------
1 | # IPC Namespaces
2 |
3 | This directory contains all `ipcMain` registrations that the Flow browser exposes to renderer processes. Each module keeps the handlers for a single area of the app (e.g. sessions, windows, browser state) and is imported by `src/main/ipc/main.ts`. Importing a module is enough to register its listeners.
4 |
5 | > **Renderer opt-in:** Renderers that expect push-style updates must emit `listeners:add`/`listeners:remove` (see below) so the main process can deliver broadcasts only to windows that are listening.
6 |
7 | ## Bootstrapping
8 |
9 | - `main.ts` — central entry point that imports every namespace. Add new IPC files here to ensure handlers run.
10 |
11 | ## Listener management
12 |
13 | - `listeners-manager.ts`
14 | - Tracks which `WebContents` listens to which channel via the `listeners:add` / `listeners:remove` events. Renderer messages should pass a unique listener id so they can unsubscribe cleanly.
15 | - Exposes helpers:
16 | - `sendMessageToListeners(channel, ...args)` — broadcast to every subscribed renderer.
17 | - `sendMessageToListenersWithWebContents(webContents[], channel, ...args)` — scoped broadcast.
18 |
19 | ## Adding a new namespace
20 |
21 | 1. Create a new file (or folder) under `src/main/ipc/` and register handlers with `ipcMain.on` / `ipcMain.handle`.
22 | 2. Add any broadcast helpers that other main-process modules can call instead of importing `ipcMain` directly.
23 | 3. If renderers should receive push updates, leverage `listeners-manager` and document the channel name. Remember to clean up listeners when renderer views unmount.
24 | 4. Import the new module from `src/main/ipc/main.ts`.
25 |
--------------------------------------------------------------------------------
/src/renderer/public/chrome-dino-game/scripts/neterror.slim.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 The Chromium Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | const HIDDEN_CLASS = 'hidden';
6 |
7 | // Subframes use a different layout but the same html file. This is to make it
8 | // easier to support platforms that load the error page via different
9 | // mechanisms (Currently just iOS). We also use the subframe style for portals
10 | // as they are embedded like subframes and can't be interacted with by the user.
11 | let isSubFrame = false;
12 | if (window.top.location !== window.location || window.portalHost) {
13 | document.documentElement.setAttribute('subframe', '');
14 | isSubFrame = true;
15 | }
16 |
17 | // Adds an icon class to the list and removes classes previously set.
18 | function updateIconClass(newClass) {
19 | const frameSelector = isSubFrame ? '#sub-frame-error' : '#main-frame-error';
20 | const iconEl = document.querySelector(frameSelector + ' .icon');
21 |
22 | if (iconEl.classList.contains(newClass)) {
23 | return;
24 | }
25 |
26 | iconEl.className = 'icon ' + newClass;
27 | }
28 |
29 |
30 | function onDocumentLoad() {
31 | const iconClass = loadTimeData.valueExists('iconClass') &&
32 | loadTimeData.getValue('iconClass');
33 | updateIconClass(iconClass);
34 | if (!isSubFrame && iconClass === 'icon-offline') {
35 | document.documentElement.classList.add('offline');
36 | new Runner('.interstitial-wrapper');
37 | }
38 | }
39 | document.addEventListener('DOMContentLoaded', onDocumentLoad);
--------------------------------------------------------------------------------
/src/main/modules/logs.ts:
--------------------------------------------------------------------------------
1 | import { app } from "electron";
2 | import fs from "fs";
3 | import path from "path";
4 | import os from "os";
5 | import { FLOW_DATA_DIR } from "@/modules/paths";
6 |
7 | function getAppLogPath() {
8 | const appName = app.getName();
9 |
10 | if (process.platform === "darwin") {
11 | return path.join(os.homedir(), "Library/Logs", appName);
12 | }
13 |
14 | return path.join(FLOW_DATA_DIR, "logs");
15 | }
16 |
17 | const appLogPath = getAppLogPath();
18 |
19 | if (!fs.existsSync(appLogPath)) {
20 | fs.mkdirSync(appLogPath, { recursive: true });
21 | }
22 |
23 | // Get app version and format startup time for the log filename
24 | const appVersion = app.getVersion();
25 | const startupTime = new Date().toISOString().replace(/[:.]/g, "").slice(0, -1); // Format: YYYYMMDDTHHMMSSZ
26 | const logFileName = `${appVersion}_${startupTime}.log`;
27 |
28 | const logStream = fs.createWriteStream(path.join(appLogPath, logFileName), { flags: "a" });
29 | const originalStdoutWrite = process.stdout.write;
30 |
31 | type Callback = (err?: Error) => void;
32 |
33 | function newStdoutWrite(
34 | chunk: Uint8Array | string,
35 | encodingOrCallback?: BufferEncoding | Callback,
36 | callback?: Callback
37 | ) {
38 | let decoloredChunk = chunk;
39 | if (typeof chunk === "string") {
40 | // remove ANSI escape codes
41 | // eslint-disable-next-line no-control-regex
42 | decoloredChunk = chunk.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
43 | }
44 |
45 | logStream.write(decoloredChunk);
46 |
47 | // @ts-expect-error: This is a workaround to log to the log file
48 | return originalStdoutWrite.call(process.stdout, chunk, encodingOrCallback, callback);
49 | }
50 | process.stdout.write = newStdoutWrite;
51 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/protocols/_protocols/flow/assets.ts:
--------------------------------------------------------------------------------
1 | import { PATHS } from "@/modules/paths";
2 | import { bufferToArrayBuffer, getContentType } from "@/modules/utils";
3 | import path from "path";
4 | import fsPromises from "fs/promises";
5 | import { HonoApp } from ".";
6 |
7 | export function registerAssetsRoutes(app: HonoApp) {
8 | app.get("/asset/*", async (c) => {
9 | // Extract the path after /asset
10 | let assetPath = c.req.path.replace(/^\/asset/, "");
11 | if (!assetPath || assetPath === "/") {
12 | return c.text("Asset path required", 400);
13 | }
14 |
15 | // Remove leading slash if present
16 | if (assetPath.startsWith("/")) {
17 | assetPath = assetPath.slice(1);
18 | }
19 |
20 | // Normalize the path to prevent directory traversal attacks
21 | const normalizedPath = path.normalize(assetPath).replace(/^(\.\.(\/|\\|$))+/, "");
22 |
23 | const filePath = path.join(PATHS.ASSETS, "public", normalizedPath);
24 |
25 | // Ensure the requested path is within the allowed directory
26 | const assetsDir = path.normalize(path.join(PATHS.ASSETS, "public"));
27 | if (!path.normalize(filePath).startsWith(assetsDir)) {
28 | return c.text("Access denied", 403);
29 | }
30 |
31 | try {
32 | // Read file contents
33 | const buffer = await fsPromises.readFile(filePath);
34 |
35 | // Determine content type based on file extension
36 | const contentType = getContentType(filePath);
37 | return c.body(bufferToArrayBuffer(buffer), 200, { "Content-Type": contentType });
38 | } catch (error) {
39 | console.error("Error serving asset:", error);
40 | return c.text("Asset not found", 404);
41 | }
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/protocols/_protocols/flow/extension-icon.ts:
--------------------------------------------------------------------------------
1 | import { getExtensionIcon } from "@/modules/extensions/management";
2 | import { loadedProfilesController } from "@/controllers/loaded-profiles-controller";
3 | import { HonoApp } from ".";
4 | import { bufferToArrayBuffer } from "@/modules/utils";
5 |
6 | export function registerExtensionIconRoutes(app: HonoApp) {
7 | app.get("/extension-icon", async (c) => {
8 | try {
9 | const extensionId = c.req.query("id");
10 | const profileId = c.req.query("profile");
11 |
12 | if (!extensionId || !profileId) {
13 | return c.text("Missing arguments", 400);
14 | }
15 |
16 | const loadedProfile = loadedProfilesController.get(profileId);
17 | if (!loadedProfile) {
18 | return c.text("No loaded profile found", 404);
19 | }
20 |
21 | const { extensionsManager } = loadedProfile;
22 |
23 | const extData = extensionsManager.getExtensionDataFromCache(extensionId);
24 | if (!extData) {
25 | return c.text("No extension data found", 404);
26 | }
27 |
28 | const extensionPath = await extensionsManager.getExtensionPath(extensionId, extData);
29 | if (!extensionPath) {
30 | return c.text("No extension path found", 404);
31 | }
32 |
33 | const icon = await getExtensionIcon(extensionPath);
34 | if (!icon) {
35 | return c.text("Extension icon not found", 404);
36 | }
37 |
38 | return c.body(bufferToArrayBuffer(icon.toPNG()), 200, { "Content-Type": "image/png" });
39 | } catch (error) {
40 | console.error("Error retrieving extension icon:", error);
41 | return c.text("Internal server error", 500);
42 | }
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/src/renderer/src/lib/omnibox/data-providers/history.ts:
--------------------------------------------------------------------------------
1 | interface HistoryEntry {
2 | id: number;
3 | url: string;
4 | title: string;
5 | visitCount: number;
6 | typedCount: number; // How often typed directly
7 | lastVisitTime: number; // Timestamp
8 | }
9 |
10 | const MOCK_HISTORY_ENABLED = false;
11 |
12 | const MOCK_HISTORY: HistoryEntry[] = [
13 | {
14 | id: 1,
15 | url: "https://www.google.com/",
16 | title: "Google",
17 | visitCount: 100,
18 | typedCount: 20,
19 | lastVisitTime: Date.now() - 86400000 * 1
20 | },
21 | {
22 | id: 2,
23 | url: "https://github.com/",
24 | title: "GitHub",
25 | visitCount: 50,
26 | typedCount: 10,
27 | lastVisitTime: Date.now() - 86400000 * 2
28 | },
29 | {
30 | id: 3,
31 | url: "https://stackoverflow.com/questions",
32 | title: "Stack Overflow - Questions",
33 | visitCount: 80,
34 | typedCount: 5,
35 | lastVisitTime: Date.now() - 3600000 * 5
36 | },
37 | {
38 | id: 4,
39 | url: "https://developer.mozilla.org/en-US/",
40 | title: "MDN Web Docs",
41 | visitCount: 30,
42 | typedCount: 2,
43 | lastVisitTime: Date.now() - 86400000 * 7
44 | },
45 | {
46 | id: 5,
47 | url: "http://localhost:3000/",
48 | title: "Local Dev Server",
49 | visitCount: 200,
50 | typedCount: 50,
51 | lastVisitTime: Date.now() - 3600000 * 1
52 | },
53 | {
54 | id: 6,
55 | url: "https://news.ycombinator.com/",
56 | title: "Hacker News",
57 | visitCount: 60,
58 | typedCount: 8,
59 | lastVisitTime: Date.now() - 86400000 * 3
60 | }
61 | ];
62 |
63 | export async function getHistory() {
64 | if (MOCK_HISTORY_ENABLED) {
65 | return MOCK_HISTORY;
66 | }
67 | return [];
68 | }
69 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/browser/navigation.ts:
--------------------------------------------------------------------------------
1 | export type NavigationEntry = {
2 | title: string;
3 | url: string;
4 | };
5 |
6 | export type TabNavigationStatus = {
7 | // Index 0: Represents the earliest visited page.
8 | // Index N: Represents the most recent page visited.
9 | navigationHistory: NavigationEntry[];
10 | activeIndex: number;
11 | canGoBack: boolean;
12 | canGoForward: boolean;
13 | };
14 |
15 | // API //
16 | export interface FlowNavigationAPI {
17 | /**
18 | * Gets the navigation status of a tab
19 | * This can only be called from the Browser UI
20 | * @param tabId The id of the tab to get the navigation status of
21 | */
22 | getTabNavigationStatus: (tabId: number) => Promise;
23 |
24 | /**
25 | * Navigates to a specific URL
26 | * This can only be called from the Browser UI
27 | * @param tabId The id of the tab to navigate to
28 | * @param url The URL to navigate to
29 | */
30 | goTo: (url: string, tabId?: number) => void;
31 |
32 | /**
33 | * Stops loading a tab
34 | * This can only be called from the Browser UI
35 | * @param tabId The id of the tab to stop loading
36 | */
37 | stopLoadingTab: (tabId: number) => void;
38 |
39 | /**
40 | * Reloads a tab
41 | * This can only be called from the Browser UI
42 | * @param tabId The id of the tab to reload
43 | */
44 | reloadTab: (tabId: number) => void;
45 |
46 | /**
47 | * Navigates to a specific navigation entry
48 | * This can only be called from the Browser UI
49 | * @param tabId The id of the tab to navigate to
50 | * @param index The index of the navigation entry to navigate to
51 | */
52 | goToNavigationEntry: (tabId: number, index: number) => void;
53 | }
54 |
--------------------------------------------------------------------------------
/docs/contributing/updating-electron.md:
--------------------------------------------------------------------------------
1 | # Electron Dependency
2 |
3 | We use [Castlabs's Electron for Content Security](https://github.com/castlabs/electron-releases), which is a fork of Electron with support for Widevine Content Decryption Module (CDM).
4 |
5 | ## Updating Electron Manually
6 |
7 | 1. Find the latest version of Electron in the [Castlabs Electron Releases](https://github.com/castlabs/electron-releases/releases) page.
8 | 2. Update the `electron` version in the `package.json` file in this format:
9 |
10 | ```json
11 | // Template
12 | "electron": "https://github.com/castlabs/electron-releases#",
13 |
14 | // Example
15 | "electron": "https://github.com/castlabs/electron-releases#v35.3.0+wvcus",
16 | ```
17 |
18 | 3. Run `bun install` to update `bun.lock` file.
19 |
20 | 4. Find the electron entry in the `bun.lock` file and update it:
21 |
22 | ```json
23 | // Template
24 | "electron": ["electron@github:castlabs/electron-releases#", "...", "castlabs-electron-releases-"],
25 |
26 | // Example
27 | "electron": ["electron@github:castlabs/electron-releases#4fab3f1", "...", "castlabs-electron-releases-4fab3f1"],
28 | ```
29 |
30 | 5. Run `bun install` again to make sure the `bun.lock` file isn't overwritten.
31 |
32 | 6. You're all set!
33 |
34 | 7. Additionally, you can delete bun's cache at `~/.bun/install/cache`, delete `node_modules`, and re-run `bun install` to make sure everything would work as expected.
35 |
36 | ## Why do we have to do all that?
37 |
38 | This is because of a [bun issue](https://github.com/oven-sh/bun/issues/19585), which causes `git clone` of the fork to fail.
39 |
40 | ## Additional Notes
41 |
42 | - Make sure `electron` is in the devDependencies, not the dependencies!
43 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot as SlotPrimitive } from "radix-ui";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const badgeVariants = cva(
8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9 | {
10 | variants: {
11 | variant: {
12 | default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
13 | secondary: "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
14 | destructive:
15 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 | outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground"
17 | }
18 | },
19 | defaultVariants: {
20 | variant: "default"
21 | }
22 | }
23 | );
24 |
25 | function Badge({
26 | className,
27 | variant,
28 | asChild = false,
29 | ...props
30 | }: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) {
31 | const Comp = asChild ? SlotPrimitive.Slot : "span";
32 |
33 | return ;
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
--------------------------------------------------------------------------------
/scripts/temp-change-name/module.ts:
--------------------------------------------------------------------------------
1 | import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
2 | import path from "path";
3 | import * as jju from "jju";
4 | import { PackageJson } from "@/_types/package-json";
5 |
6 | const rootDir = ".";
7 | const packageJsonPath = path.join(rootDir, "package.json");
8 | const backupPath = path.join(rootDir, "package.json.old");
9 |
10 | export function changeName(newName: string) {
11 | // Grab package.json
12 | const packageJSONString = readFileSync(packageJsonPath, "utf8");
13 | let packageJSON: PackageJson;
14 |
15 | try {
16 | packageJSON = jju.parse(packageJSONString);
17 | } catch (error) {
18 | console.error("Error parsing package.json", error);
19 | process.exit(1);
20 | }
21 |
22 | // Change App Name
23 | packageJSON["productName"] = newName;
24 |
25 | // Save old package.json
26 | writeFileSync("package.json.old", packageJSONString);
27 |
28 | // Write package.json with preserved formatting
29 | const updatedContent = jju.update(packageJSONString, packageJSON, { mode: "json", indent: 2 });
30 | writeFileSync(packageJsonPath, updatedContent);
31 |
32 | console.log("Successfully changed app name to", newName);
33 | }
34 |
35 | export function revertName() {
36 | // Check if backup exists
37 | if (!existsSync(backupPath)) {
38 | console.error("No backup file found (package.json.old)");
39 | process.exit(1);
40 | }
41 |
42 | // Read the backup file
43 | const backupContent = readFileSync(backupPath, "utf8");
44 |
45 | // Restore the original package.json
46 | writeFileSync(packageJsonPath, backupContent);
47 |
48 | // Remove the backup file
49 | unlinkSync(backupPath);
50 |
51 | console.log("Successfully reverted package.json to original state");
52 | }
53 |
--------------------------------------------------------------------------------
/src/shared/flow/interfaces/sessions/spaces.ts:
--------------------------------------------------------------------------------
1 | import { IPCListener } from "~/flow/types";
2 |
3 | export type Space = {
4 | id: string;
5 | name: string;
6 | profileId: string;
7 | bgStartColor: string;
8 | bgEndColor: string;
9 | icon: string;
10 | };
11 |
12 | // API //
13 | export interface FlowSpacesAPI {
14 | /**
15 | * Gets the spaces
16 | */
17 | getSpaces: () => Promise;
18 |
19 | /**
20 | * Gets the spaces from a profile
21 | */
22 | getSpacesFromProfile: (profileId: string) => Promise;
23 |
24 | /**
25 | * Creates a space
26 | */
27 | createSpace: (profileId: string, spaceName: string) => Promise;
28 |
29 | /**
30 | * Deletes a space
31 | */
32 | deleteSpace: (profileId: string, spaceId: string) => Promise;
33 |
34 | /**
35 | * Updates a space
36 | */
37 | updateSpace: (profileId: string, spaceId: string, spaceData: Partial) => Promise;
38 |
39 | /**
40 | * Sets the space that is currently being used
41 | */
42 | setUsingSpace: (profileId: string, spaceId: string) => Promise;
43 |
44 | /**
45 | * Gets the space id that is currently being used
46 | */
47 | getUsingSpace: () => Promise;
48 |
49 | /**
50 | * Gets the last used space
51 | */
52 | getLastUsedSpace: () => Promise;
53 |
54 | /**
55 | * Reorders the spaces
56 | */
57 | reorderSpaces: (orderMap: { profileId: string; spaceId: string; order: number }[]) => Promise;
58 |
59 | /**
60 | * Listens for changes to the spaces
61 | */
62 | onSpacesChanged: IPCListener<[void]>;
63 |
64 | /**
65 | * Listens for changes to the space that the current window is using
66 | */
67 | onSetWindowSpace: IPCListener<[string]>;
68 | }
69 |
--------------------------------------------------------------------------------
/electron.vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "path";
2 | import { defineConfig, externalizeDepsPlugin } from "electron-vite";
3 | import react from "@vitejs/plugin-react";
4 | import tailwindcss from "@tailwindcss/vite";
5 | import { generateRoutes } from "./scripts/frontend-routes-generator/generator";
6 |
7 | const routes = await generateRoutes();
8 |
9 | function isProductionBuild() {
10 | return process.env.PRODUCTION_BUILD === "true";
11 | }
12 |
13 | const mainAliases: Record = {
14 | "@": resolve("src/main")
15 | };
16 |
17 | const rendererAliases: Record = {
18 | "@": resolve("src/renderer/src")
19 | };
20 |
21 | const sharedAliases: Record = {
22 | "~": resolve("src/shared")
23 | };
24 |
25 | const commonOptions = {
26 | build: {
27 | minify: isProductionBuild() ? "esbuild" : false
28 | }
29 | } as const;
30 |
31 | export default defineConfig({
32 | main: {
33 | ...commonOptions,
34 | plugins: [externalizeDepsPlugin({ exclude: ["electron-context-menu", "hono"] })],
35 | resolve: {
36 | alias: {
37 | ...mainAliases,
38 | ...sharedAliases
39 | }
40 | }
41 | },
42 | preload: {
43 | ...commonOptions,
44 | plugins: [externalizeDepsPlugin({ exclude: ["electron-chrome-extensions"] })],
45 | resolve: {
46 | alias: {
47 | ...mainAliases,
48 | ...sharedAliases
49 | }
50 | }
51 | },
52 | renderer: {
53 | ...commonOptions,
54 | resolve: {
55 | alias: {
56 | ...rendererAliases,
57 | ...sharedAliases
58 | }
59 | },
60 | build: {
61 | ...commonOptions.build,
62 | rollupOptions: {
63 | input: {
64 | ...routes
65 | }
66 | }
67 | },
68 | plugins: [react(), tailwindcss()]
69 | }
70 | });
71 |
--------------------------------------------------------------------------------
/src/main/controllers/app-menu-controller/index.ts:
--------------------------------------------------------------------------------
1 | import { Menu } from "electron";
2 | import { createAppMenu } from "./menu/items/app";
3 | import { createArchiveMenu } from "./menu/items/archive";
4 | import { createEditMenu } from "./menu/items/edit";
5 | import { createFileMenu } from "./menu/items/file";
6 | import { createSpacesMenu } from "./menu/items/spaces";
7 | import { createViewMenu } from "./menu/items/view";
8 | import { createWindowMenu } from "./menu/items/window";
9 | import { MenuItem, MenuItemConstructorOptions } from "electron";
10 | import { shortcutsEmitter } from "@/saving/shortcuts";
11 | import { spacesController } from "@/controllers/spaces-controller";
12 | import { windowsController } from "@/controllers/windows-controller";
13 |
14 | class AppMenuController {
15 | constructor() {
16 | this.render();
17 |
18 | spacesController.on("space-created", this.render);
19 | spacesController.on("space-updated", this.render);
20 | spacesController.on("space-deleted", this.render);
21 |
22 | shortcutsEmitter.on("shortcuts-changed", this.render);
23 |
24 | // This module hasn't loaded yet, so we have to wait
25 | setImmediate(() => {
26 | windowsController.on("window-focused", this.render);
27 | });
28 | }
29 |
30 | public async render() {
31 | const isMac = process.platform === "darwin";
32 |
33 | const template: Array = [
34 | ...(isMac ? [createAppMenu()] : []),
35 | createFileMenu(),
36 | createEditMenu(),
37 | createViewMenu(),
38 | await createSpacesMenu(),
39 | createArchiveMenu(),
40 | createWindowMenu()
41 | ];
42 |
43 | const menu = Menu.buildFromTemplate(template);
44 | Menu.setApplicationMenu(menu);
45 | }
46 | }
47 |
48 | export const appMenuController = new AppMenuController();
49 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ScrollArea as ScrollAreaPrimitive } from "radix-ui";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | function ScrollArea({ className, children, ...props }: React.ComponentProps) {
9 | return (
10 |
11 |
15 | {children}
16 |
17 |
18 |
19 |
20 | );
21 | }
22 |
23 | function ScrollBar({
24 | className,
25 | orientation = "vertical",
26 | ...props
27 | }: React.ComponentProps) {
28 | return (
29 |
40 |
44 |
45 | );
46 | }
47 |
48 | export { ScrollArea, ScrollBar };
49 |
--------------------------------------------------------------------------------
/src/main/controllers/sessions-controller/protocols/index.ts:
--------------------------------------------------------------------------------
1 | import { registerFlowProtocol } from "./_protocols/flow";
2 | import { registerFlowInternalProtocol } from "./_protocols/flow-internal";
3 | import { registerFlowExternalProtocol } from "./_protocols/flow-external";
4 | import { protocol, Session } from "electron";
5 | import type { CustomProtocol } from "./types";
6 |
7 | protocol.registerSchemesAsPrivileged([
8 | {
9 | scheme: "flow",
10 | privileges: {
11 | standard: true,
12 | secure: true,
13 | bypassCSP: false,
14 | allowServiceWorkers: false,
15 | supportFetchAPI: false,
16 | corsEnabled: true,
17 | stream: false,
18 | codeCache: true
19 | }
20 | },
21 | {
22 | scheme: "flow-internal",
23 | privileges: {
24 | standard: true,
25 | secure: true,
26 | bypassCSP: false,
27 | allowServiceWorkers: false,
28 | supportFetchAPI: false,
29 | corsEnabled: true,
30 | stream: false,
31 | codeCache: true
32 | }
33 | },
34 | {
35 | scheme: "flow-external",
36 | privileges: {
37 | standard: true,
38 | secure: true,
39 | bypassCSP: false,
40 | allowServiceWorkers: false,
41 | supportFetchAPI: false,
42 | corsEnabled: true,
43 | stream: true,
44 | codeCache: true
45 | }
46 | }
47 | ]);
48 |
49 | // Register protocols for normal sessions
50 | export function registerProtocolsWithSession(session: Session, protocols: CustomProtocol[]) {
51 | const protocol = session.protocol;
52 |
53 | if (protocols.includes("flow")) {
54 | registerFlowProtocol(protocol);
55 | }
56 | if (protocols.includes("flow-internal")) {
57 | registerFlowInternalProtocol(protocol);
58 | }
59 | if (protocols.includes("flow-external")) {
60 | registerFlowExternalProtocol(protocol);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/modules/output.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 |
3 | import { FLAGS } from "./flags";
4 | import "@/modules/logs";
5 |
6 | const DEBUG_AREAS = {
7 | INITIALIZATION: true, // @/main/index.ts
8 | FAVICONS: false, // @/modules/favicons.ts
9 | PERMISSIONS: false, // @/browser/main.ts
10 | VITE_UI_EXTENSION: false, // @/browser/main.ts
11 | EXTENSION_SERVER_WORKERS: false, // @/browser/main.ts
12 | WEB_CONTENTS_CREATED: false, // @/browser/main.ts
13 | OMNIBOX: false, // @/browser/omnibox.ts
14 | DATASTORE: false, // @/saving/datastore.ts
15 | PROFILES: false, // @/controllers/profiles-controller (originally @/modules/profiles.ts)
16 | SPACES: false, // @/controllers/spaces-controller (originally @/sessions/spaces.ts)
17 | ICONS: false, // @/modules/icons.ts
18 | PORTAL_COMPONENTS: false, // @/browser/components/portal-component-windows.ts
19 | AUTO_UPDATER: false, // @/modules/auto-update.ts
20 | CONTENT_BLOCKER: false, // @/modules/content-blocker.ts
21 | WEB_REQUESTS_INTERCEPTION: false, // @/browser/utility/web-requests.ts
22 | WEB_REQUESTS: false, // @/browser/utility/web-requests.ts
23 | MATCH_PATTERN: false, // @/browser/utility/match-pattern.ts
24 | WINDOWS: true // @/controllers/windows-controller
25 | } as const;
26 |
27 | export type DEBUG_AREA = keyof typeof DEBUG_AREAS;
28 |
29 | export function debugPrint(area: DEBUG_AREA, ...message: any[]) {
30 | if (!FLAGS.SHOW_DEBUG_PRINTS) return;
31 |
32 | if (DEBUG_AREAS[area]) {
33 | console.log(`\x1b[32m[${area}]\x1b[0m`, ...message);
34 | }
35 | }
36 |
37 | export function debugError(area: DEBUG_AREA, ...message: any[]) {
38 | if (FLAGS.SHOW_DEBUG_ERRORS === false) return;
39 |
40 | if (Array.isArray(FLAGS.SHOW_DEBUG_ERRORS)) {
41 | if (!FLAGS.SHOW_DEBUG_ERRORS.includes(area)) return;
42 | }
43 |
44 | console.error(`\x1b[31m[${area}]\x1b[0m`, ...message);
45 | }
46 |
--------------------------------------------------------------------------------
/src/renderer/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Popover as PopoverPrimitive } from "radix-ui";
3 |
4 | import { cn } from "@/lib/utils";
5 | import { Fragment } from "react";
6 |
7 | function Popover({ ...props }: React.ComponentProps) {
8 | return ;
9 | }
10 |
11 | function PopoverTrigger({ ...props }: React.ComponentProps) {
12 | return ;
13 | }
14 |
15 | function PopoverContent({
16 | className,
17 | align = "center",
18 | sideOffset = 4,
19 | portal = true,
20 | ...props
21 | }: React.ComponentProps & { portal?: boolean }) {
22 | const Portal = portal ? PopoverPrimitive.Portal : Fragment;
23 |
24 | return (
25 |
26 |
36 |
37 | );
38 | }
39 |
40 | function PopoverAnchor({ ...props }: React.ComponentProps) {
41 | return ;
42 | }
43 |
44 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
45 |
--------------------------------------------------------------------------------
/src/main/modules/extensions/main.ts:
--------------------------------------------------------------------------------
1 | import { sessionsController } from "@/controllers/sessions-controller";
2 | import { app, protocol, Session, session } from "electron";
3 | import { ElectronChromeExtensions, setPartitionSessionGrabber } from "electron-chrome-extensions";
4 |
5 | // A hack to load profiles rather than partitions
6 | const partitionSessionGrabber = (partition: string) => {
7 | // custom: grab the session from the profile
8 | const PROFILE_PREFIX = "profile:";
9 | if (partition.startsWith(PROFILE_PREFIX)) {
10 | const profileId = partition.slice(PROFILE_PREFIX.length);
11 | const session = sessionsController.getIfExists(profileId);
12 | if (!session) {
13 | throw new Error(`Session not found for profile ${profileId}`);
14 | }
15 |
16 | return session;
17 | }
18 |
19 | return session.fromPartition(partition);
20 | };
21 |
22 | setPartitionSessionGrabber(partitionSessionGrabber);
23 |
24 | // Register CRX protocol in default session
25 | app.whenReady().then(() => {
26 | protocol.handle("crx", async (request) => {
27 | const url = URL.parse(request.url);
28 |
29 | if (!url) {
30 | return new Response("Invalid URL", { status: 404 });
31 | }
32 |
33 | const partition = url?.searchParams.get("partition");
34 |
35 | if (!partition) {
36 | return new Response("No partition", { status: 400 });
37 | }
38 |
39 | let session: Session | null = null;
40 | try {
41 | session = partitionSessionGrabber(partition);
42 | } catch {
43 | // Not found, return 404 below
44 | }
45 |
46 | if (!session) {
47 | return new Response("Session not found", { status: 404 });
48 | }
49 |
50 | const extensions = ElectronChromeExtensions.fromSession(session);
51 |
52 | if (!extensions) {
53 | return new Response("Extensions not found", { status: 404 });
54 | }
55 |
56 | return extensions.handleCrxRequest(request);
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/src/renderer/src/lib/omnibox/autocomplete-result.ts:
--------------------------------------------------------------------------------
1 | import { AutocompleteMatch } from "@/lib/omnibox/types";
2 |
3 | export class AutocompleteResult {
4 | private matches: AutocompleteMatch[] = [];
5 | private static MAX_RESULTS = 8; // Default limit for suggestions shown
6 |
7 | addMatch(match: AutocompleteMatch): void {
8 | this.matches.push(match);
9 | }
10 |
11 | addMatches(newMatches: AutocompleteMatch[]): void {
12 | this.matches.push(...newMatches);
13 | }
14 |
15 | clear(): void {
16 | this.matches = [];
17 | }
18 |
19 | // Simple deduplication: prioritize higher relevance for the same destinationUrl
20 | deduplicate(): void {
21 | const uniqueMatches = new Map();
22 | // Sort first to process higher relevance scores first
23 | this.matches.sort((a, b) => b.relevance - a.relevance);
24 |
25 | for (const match of this.matches) {
26 | const key = match.destinationUrl; // Use destination URL as the primary key
27 | if (!uniqueMatches.has(key)) {
28 | uniqueMatches.set(key, match);
29 | }
30 | // If a duplicate exists but the current one is a different type we might want to keep it?
31 | // The summary mentions merging, e.g. bookmark + history. This simple dedupe replaces.
32 | // A more complex logic could merge properties or keep both if types differ significantly.
33 | }
34 | this.matches = Array.from(uniqueMatches.values());
35 | }
36 |
37 | sort(): void {
38 | // Primary sort by relevance (descending)
39 | this.matches.sort((a, b) => {
40 | if (b.relevance !== a.relevance) {
41 | return b.relevance - a.relevance;
42 | }
43 | // Add secondary sort criteria if needed (e.g., provider type, alphabetical)
44 | return 0;
45 | });
46 | }
47 |
48 | getTopMatches(limit: number = AutocompleteResult.MAX_RESULTS): AutocompleteMatch[] {
49 | return this.matches.slice(0, limit);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/renderer/src/routes/pdf-viewer/pdf-viewer/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { usePDFSlick } from "@pdfslick/react";
3 | import Toolbar from "./Toolbar";
4 | import Thumbsbar from "./Thumbsbar";
5 |
6 | type PDFViewerAppProps = {
7 | pdfFilePath: string;
8 | };
9 |
10 | export function PDFViewerApp({ pdfFilePath }: PDFViewerAppProps) {
11 | const [isThumbsbarOpen, setIsThumbsbarOpen] = useState(false);
12 | const [loadedPerc, setLoadedPerc] = useState(0);
13 | const { isDocumentLoaded, viewerRef, thumbsRef, usePDFSlickStore, PDFSlickViewer } = usePDFSlick(pdfFilePath, {
14 | getDocumentParams: {
15 | disableAutoFetch: false /** pages need to be loaded for printing, otherwise we get `Expected print service to be initialized.` */,
16 | disableFontFace: false,
17 | disableRange: false,
18 | disableStream: true,
19 | verbosity: 0
20 | },
21 | onProgress: ({ total, loaded }) => {
22 | setLoadedPerc((100 * loaded) / total);
23 | }
24 | });
25 |
26 | useEffect(() => {
27 | if (isDocumentLoaded) {
28 | setIsThumbsbarOpen(true);
29 | }
30 | }, [isDocumentLoaded]);
31 |
32 | return (
33 | <>
34 |
44 | {loadedPerc < 100 && (
45 |
49 | )}
50 | >
51 | );
52 | }
53 |
--------------------------------------------------------------------------------