feat: open popup url in external browser (#4446)

* feat: open popup url in external browser

* fix: allow google auth popup internal

* feat: add functionality(including settings) to open links in external browser for webviews

* fix: set useragent globally

* fix: remove setUserAgent in webview

* fix: set Chrome version to newest
This commit is contained in:
fullex 2025-04-25 09:45:54 +08:00 committed by GitHub
parent e5db6e79b3
commit 8c22c3c9d5
16 changed files with 166 additions and 42 deletions

View File

@ -20,6 +20,8 @@ export enum IpcChannel {
App_InstallUvBinary = 'app:install-uv-binary',
App_InstallBunBinary = 'app:install-bun-binary',
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
// Open
Open_Path = 'open:path',
Open_Website = 'open:website',

View File

@ -75,14 +75,6 @@ if (!app.requestSingleInstanceLock()) {
handleProtocolUrl(url)
})
registerProtocolClient(app)
// macOS specific: handle protocol when app is already running
app.on('open-url', (event, url) => {
event.preventDefault()
handleProtocolUrl(url)
})
// Listen for second instance
app.on('second-instance', (_event, argv) => {
windowService.showMainWindow()

View File

@ -25,6 +25,7 @@ import { ProxyConfig, proxyManager } from './services/ProxyManager'
import { searchService } from './services/SearchService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { setOpenLinkExternal } from './services/WebviewService'
import { windowService } from './services/WindowService'
import { getResourcePath } from './utils'
import { decrypt, encrypt } from './utils/aes'
@ -342,4 +343,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.SearchWindow_OpenUrl, async (_, uid: string, url: string) => {
return await searchService.openUrlInSearchWindow(uid, url)
})
// webview
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
setOpenLinkExternal(webviewId, isExternal)
)
}

View File

@ -0,0 +1,35 @@
import { session, shell, webContents } from 'electron'
/**
* init the useragent of the webview session
* remove the CherryStudio and Electron from the useragent
*/
export function initSessionUserAgent() {
const wvSession = session.fromPartition('persist:webview')
const newChromeVersion = '135.0.7049.96'
const originUA = wvSession.getUserAgent()
const newUA = originUA
.replace(/CherryStudio\/\S+\s/, '')
.replace(/Electron\/\S+\s/, '')
.replace(/Chrome\/\d+\.\d+\.\d+\.\d+/, `Chrome/${newChromeVersion}`)
wvSession.setUserAgent(newUA)
}
/**
* WebviewService handles the behavior of links opened from webview elements
* It controls whether links should be opened within the application or in an external browser
*/
export function setOpenLinkExternal(webviewId: number, isExternal: boolean) {
const webview = webContents.fromId(webviewId)
if (!webview) return
webview.setWindowOpenHandler(({ url }) => {
if (isExternal) {
shell.openExternal(url)
return { action: 'deny' }
} else {
return { action: 'allow' }
}
})
}

View File

@ -11,6 +11,7 @@ import icon from '../../../build/icon.png?asset'
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
import { locales } from '../utils/locales'
import { configManager } from './ConfigManager'
import { initSessionUserAgent } from './WebviewService'
export class WindowService {
private static instance: WindowService | null = null
@ -81,6 +82,9 @@ export class WindowService {
this.miniWindow = this.createMiniWindow(true)
}
//init the MinApp webviews' useragent
initSessionUserAgent()
return this.mainWindow
}

View File

@ -204,6 +204,9 @@ declare global {
closeSearchWindow: (uid: string) => Promise<string>
openUrlInSearchWindow: (uid: string, url: string) => Promise<string>
}
webview: {
setOpenLinkExternal: (webviewId: number, isExternal: boolean) => Promise<void>
}
}
}
}

View File

@ -185,6 +185,10 @@ const api = {
openSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Open, uid),
closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid),
openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url)
},
webview: {
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal)
}
}

View File

@ -3,6 +3,7 @@ import {
CodeOutlined,
CopyOutlined,
ExportOutlined,
LinkOutlined,
MinusOutlined,
PushpinOutlined,
ReloadOutlined
@ -14,6 +15,9 @@ import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { setMinappsOpenLinkExternal } from '@renderer/store/settings'
import { MinAppType } from '@renderer/types'
import { delay } from '@renderer/utils'
import { Avatar, Drawer, Tooltip } from 'antd'
@ -40,6 +44,7 @@ const MinappPopupContainer: React.FC = () => {
const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation()
const backgroundColor = useNavBackgroundColor()
const dispatch = useAppDispatch()
/** control the drawer open or close */
const [isPopupShow, setIsPopupShow] = useState(true)
@ -57,6 +62,8 @@ const MinappPopupContainer: React.FC = () => {
const webviewRefs = useRef<Map<string, WebviewTag | null>>(new Map())
/** indicate whether the webview has loaded */
const webviewLoadedRefs = useRef<Map<string, boolean>>(new Map())
/** whether the minapps open link external is enabled */
const { minappsOpenLinkExternal } = useSettings()
const isInDevelopment = process.env.NODE_ENV === 'development'
@ -107,9 +114,14 @@ const MinappPopupContainer: React.FC = () => {
webviewLoadedRefs.current.forEach((_, appid) => {
if (!webviewRefs.current.has(appid)) {
webviewLoadedRefs.current.delete(appid)
} else if (appid === currentMinappId) {
const webviewId = webviewRefs.current.get(appid)?.getWebContentsId()
if (webviewId) {
window.api.webview.setOpenLinkExternal(webviewId, minappsOpenLinkExternal)
}
}
})
}, [currentMinappId])
}, [currentMinappId, minappsOpenLinkExternal])
/** only the keepalive minapp can be minimized */
const canMinimize = !(openedOneOffMinapp && openedOneOffMinapp.id == currentMinappId)
@ -175,6 +187,10 @@ const MinappPopupContainer: React.FC = () => {
/** the callback function to set the webviews loaded indicator */
const handleWebviewLoaded = (appid: string) => {
webviewLoadedRefs.current.set(appid, true)
const webviewId = webviewRefs.current.get(appid)?.getWebContentsId()
if (webviewId) {
window.api.webview.setOpenLinkExternal(webviewId, minappsOpenLinkExternal)
}
if (appid == currentMinappId) {
setTimeout(() => setIsReady(true), 200)
}
@ -220,6 +236,11 @@ const MinappPopupContainer: React.FC = () => {
updatePinnedMinapps(newPinned)
}
/** set the open external status */
const handleToggleOpenExternal = () => {
dispatch(setMinappsOpenLinkExternal(!minappsOpenLinkExternal))
}
/** Title bar of the popup */
const Title = ({ appInfo, url }: { appInfo: AppInfo | null; url: string | null }) => {
if (!appInfo) return null
@ -238,7 +259,7 @@ const MinappPopupContainer: React.FC = () => {
}
return (
<TitleContainer style={{ backgroundColor: backgroundColor, justifyContent: 'space-between' }}>
<TitleContainer style={{ backgroundColor: backgroundColor }}>
<Tooltip
title={
<TitleTextTooltip>
@ -256,6 +277,14 @@ const MinappPopupContainer: React.FC = () => {
}}>
<TitleText onContextMenu={(e) => handleCopyUrl(e, url ?? appInfo.url)}>{appInfo.name}</TitleText>
</Tooltip>
{appInfo.canOpenExternalLink && (
<Tooltip title={t('minapp.popup.openExternal')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handleOpenLink(url ?? appInfo.url)}>
<ExportOutlined />
</Button>
</Tooltip>
)}
<Spacer />
<ButtonsGroup className={isWindows ? 'windows' : ''}>
<Tooltip title={t('minapp.popup.refresh')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handleReload(appInfo.id)}>
@ -272,13 +301,18 @@ const MinappPopupContainer: React.FC = () => {
</Button>
</Tooltip>
)}
{appInfo.canOpenExternalLink && (
<Tooltip title={t('minapp.popup.openExternal')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handleOpenLink(url ?? appInfo.url)}>
<ExportOutlined />
</Button>
</Tooltip>
)}
<Tooltip
title={
minappsOpenLinkExternal
? t('minapp.popup.open_link_external_on')
: t('minapp.popup.open_link_external_off')
}
mouseEnterDelay={0.8}
placement="bottom">
<Button onClick={handleToggleOpenExternal} className={minappsOpenLinkExternal ? 'open-external' : ''}>
<LinkOutlined />
</Button>
</Tooltip>
{isInDevelopment && (
<Tooltip title={t('minapp.popup.devtools')} mouseEnterDelay={0.8} placement="bottom">
<Button onClick={() => handleOpenDevTools(appInfo.id)}>
@ -367,8 +401,8 @@ const TitleText = styled.div`
font-weight: bold;
font-size: 14px;
color: var(--color-text-1);
margin-right: 10px;
-webkit-app-region: no-drag;
margin-right: 5px;
`
const TitleTextTooltip = styled.span`
@ -407,6 +441,7 @@ const Button = styled.div`
color: var(--color-text-2);
transition: all 0.2s ease;
font-size: 14px;
-webkit-app-region: no-drag;
&:hover {
color: var(--color-text-1);
background-color: var(--color-background-mute);
@ -415,6 +450,10 @@ const Button = styled.div`
color: var(--color-primary);
background-color: var(--color-primary-bg);
}
&.open-external {
color: var(--color-primary);
background-color: var(--color-primary-bg);
}
`
const EmptyView = styled.div`
@ -428,4 +467,8 @@ const EmptyView = styled.div`
background-color: var(--color-background);
`
const Spacer = styled.div`
flex: 1;
`
export default MinappPopupContainer

View File

@ -60,9 +60,6 @@ const WebviewContainer = memo(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appid, url])
//remove the tag of CherryStudio and Electron
const userAgent = navigator.userAgent.replace(/CherryStudio\/\S+\s/, '').replace(/Electron\/\S+\s/, '')
return (
<webview
key={appid}
@ -70,7 +67,6 @@ const WebviewContainer = memo(
style={WebviewStyle}
allowpopups={'true' as any}
partition="persist:webview"
useragent={userAgent}
/>
)
}

View File

@ -585,7 +585,9 @@
"minimize": "Minimize MinApp",
"devtools": "Developer Tools",
"openExternal": "Open in Browser",
"rightclick_copyurl": "Right-click to copy URL"
"rightclick_copyurl": "Right-click to copy URL",
"open_link_external_on": "Current: Open links in browser",
"open_link_external_off": "Current: Open links in default window"
},
"sidebar.add.title": "Add to sidebar",
"sidebar.remove.title": "Remove from sidebar",
@ -1020,6 +1022,9 @@
"disabled": "Hidden Mini Apps",
"empty": "Drag mini apps from the left to hide them",
"visible": "Visible Mini Apps",
"open_link_external": {
"title": "Open new-window links in browser"
},
"cache_settings": "Cache Settings",
"cache_title": "Mini App Cache Limit",
"cache_description": "Set the maximum number of active mini apps to keep in memory",

View File

@ -585,7 +585,9 @@
"minimize": "ミニアプリを最小化",
"devtools": "開発者ツール",
"openExternal": "ブラウザで開く",
"rightclick_copyurl": "右クリックでURLをコピー"
"rightclick_copyurl": "右クリックでURLをコピー",
"open_link_external_on": "現在:ブラウザで開く",
"open_link_external_off": "現在:デフォルトのウィンドウで開く"
},
"sidebar.add.title": "サイドバーに追加",
"sidebar.remove.title": "サイドバーから削除",
@ -1020,6 +1022,9 @@
"disabled": "非表示のミニアプリ",
"empty": "非表示にするミニアプリを左側からここにドラッグしてください",
"visible": "表示するミニアプリ",
"open_link_external": {
"title": "新視窗のリンクをブラウザで開く"
},
"cache_settings": "キャッシュ設定",
"cache_title": "ミニアプリのキャッシュ数",
"cache_description": "メモリに保持するアクティブなミニアプリの最大数を設定します",

View File

@ -585,7 +585,9 @@
"minimize": "Свернуть встроенное приложение",
"devtools": "Инструменты разработчика",
"openExternal": "Открыть в браузере",
"rightclick_copyurl": "ПКМ → Копировать URL"
"rightclick_copyurl": "ПКМ → Копировать URL",
"open_link_external_on": "Текущий: Открыть ссылки в браузере",
"open_link_external_off": "Текущий: Открыть ссылки в окне по умолчанию"
},
"sidebar.add.title": "Добавить в боковую панель",
"sidebar.remove.title": "Удалить из боковой панели",
@ -1020,6 +1022,9 @@
"disabled": "Скрытые мини-приложения",
"empty": "Перетащите мини-приложения слева, чтобы скрыть их",
"visible": "Отображаемые мини-приложения",
"open_link_external": {
"title": "Открывать новые окна в браузере"
},
"cache_settings": "Настройки кэша",
"cache_title": "Количество кэшируемых мини-приложений",
"cache_description": "Установить максимальное количество активных мини-приложений в памяти",

View File

@ -585,7 +585,9 @@
"minimize": "最小化小程序",
"devtools": "开发者工具",
"openExternal": "在浏览器中打开",
"rightclick_copyurl": "右键复制URL"
"rightclick_copyurl": "右键复制URL",
"open_link_external_on": "当前:在浏览器中打开链接",
"open_link_external_off": "当前:使用默认窗口打开链接"
},
"sidebar.add.title": "添加到侧边栏",
"sidebar.remove.title": "从侧边栏移除",
@ -1020,6 +1022,9 @@
"disabled": "隐藏的小程序",
"empty": "把要隐藏的小程序从左侧拖拽到这里",
"visible": "显示的小程序",
"open_link_external": {
"title": "在浏览器中打开新窗口链接"
},
"cache_settings": "缓存设置",
"cache_title": "小程序缓存数量",
"cache_description": "设置同时保持活跃状态的小程序最大数量",

View File

@ -585,7 +585,9 @@
"minimize": "最小化小工具",
"devtools": "開發者工具",
"openExternal": "在瀏覽器中開啟",
"rightclick_copyurl": "右鍵複製URL"
"rightclick_copyurl": "右鍵複製URL",
"open_link_external_on": "当前:在瀏覽器中開啟連結",
"open_link_external_off": "当前:使用預設視窗開啟連結"
},
"sidebar.add.title": "新增到側邊欄",
"sidebar.remove.title": "從側邊欄移除",
@ -1020,6 +1022,9 @@
"disabled": "隱藏的小程式",
"empty": "把要隱藏的小程式從左側拖拽到這裡",
"visible": "顯示的小程式",
"open_link_external": {
"title": "在瀏覽器中打開新視窗連結"
},
"cache_settings": "緩存設置",
"cache_title": "小程式緩存數量",
"cache_description": "設置同時保持活躍狀態的小程式最大數量",

View File

@ -4,7 +4,11 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { setMaxKeepAliveMinapps, setShowOpenedMinappsInSidebar } from '@renderer/store/settings'
import {
setMaxKeepAliveMinapps,
setMinappsOpenLinkExternal,
setShowOpenedMinappsInSidebar
} from '@renderer/store/settings'
import { Button, message, Slider, Switch, Tooltip } from 'antd'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -20,7 +24,7 @@ const MiniAppSettings: FC = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const dispatch = useAppDispatch()
const { maxKeepAliveMinapps, showOpenedMinappsInSidebar } = useSettings()
const { maxKeepAliveMinapps, showOpenedMinappsInSidebar, minappsOpenLinkExternal } = useSettings()
const { minapps, disabled, updateMinapps, updateDisabledMinapps } = useMinapps()
const [visibleMiniApps, setVisibleMiniApps] = useState(minapps)
@ -89,9 +93,19 @@ const MiniAppSettings: FC = () => {
/>
</BorderedContainer>
<SettingDivider />
<SettingRow>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.open_link_external.title')}</SettingRowTitle>
</SettingLabelGroup>
<Switch
checked={minappsOpenLinkExternal}
onChange={(checked) => dispatch(setMinappsOpenLinkExternal(checked))}
/>
</SettingRow>
<SettingDivider />
{/* 缓存小程序数量设置 */}
<CacheSettingRow>
<SettingRow>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.cache_title')}</SettingRowTitle>
<SettingDescription>{t('settings.miniapps.cache_description')}</SettingDescription>
@ -117,9 +131,9 @@ const MiniAppSettings: FC = () => {
/>
</SliderWithResetContainer>
</CacheSettingControls>
</CacheSettingRow>
</SettingRow>
<SettingDivider />
<SidebarSettingRow>
<SettingRow>
<SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.sidebar_title')}</SettingRowTitle>
<SettingDescription>{t('settings.miniapps.sidebar_description')}</SettingDescription>
@ -128,14 +142,14 @@ const MiniAppSettings: FC = () => {
checked={showOpenedMinappsInSidebar}
onChange={(checked) => dispatch(setShowOpenedMinappsInSidebar(checked))}
/>
</SidebarSettingRow>
</SettingRow>
</SettingGroup>
</SettingContainer>
)
}
// 修改和新增样式
const CacheSettingRow = styled.div`
const SettingRow = styled.div`
display: flex;
align-items: flex-start;
justify-content: space-between;
@ -206,14 +220,6 @@ const ResetButtonWrapper = styled.div`
justify-content: center;
`
// 新增侧边栏设置行样式
const SidebarSettingRow = styled.div`
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
`
// 新增: 带边框的容器组件
const BorderedContainer = styled.div`
border: 1px solid var(--color-border);

View File

@ -109,8 +109,10 @@ export interface SettingsState {
siyuanToken: string | null
siyuanBoxId: string | null
siyuanRootPath: string | null
// MinApps
maxKeepAliveMinapps: number
showOpenedMinappsInSidebar: boolean
minappsOpenLinkExternal: boolean
// 隐私设置
enableDataCollection: boolean
enableQuickPanelTriggers: boolean
@ -211,8 +213,10 @@ export const initialState: SettingsState = {
siyuanToken: null,
siyuanBoxId: null,
siyuanRootPath: null,
// MinApps
maxKeepAliveMinapps: 3,
showOpenedMinappsInSidebar: true,
minappsOpenLinkExternal: false,
enableDataCollection: false,
enableQuickPanelTriggers: false,
enableBackspaceDeleteModel: true,
@ -482,6 +486,9 @@ const settingsSlice = createSlice({
setShowOpenedMinappsInSidebar: (state, action: PayloadAction<boolean>) => {
state.showOpenedMinappsInSidebar = action.payload
},
setMinappsOpenLinkExternal: (state, action: PayloadAction<boolean>) => {
state.minappsOpenLinkExternal = action.payload
},
setEnableDataCollection: (state, action: PayloadAction<boolean>) => {
state.enableDataCollection = action.payload
},
@ -579,6 +586,7 @@ export const {
setSiyuanRootPath,
setMaxKeepAliveMinapps,
setShowOpenedMinappsInSidebar,
setMinappsOpenLinkExternal,
setEnableDataCollection,
setEnableQuickPanelTriggers,
setExportMenuOptions,