feat: add deeplink support (#2883)
* feat: add deeplink support * fix windows not receive deeplink when not running --------- Co-authored-by: James <james@jan.ai>
This commit is contained in:
parent
efbc96dad9
commit
6af4a2d484
@ -19,6 +19,7 @@ export enum NativeRoute {
|
||||
showMainWindow = 'showMainWindow',
|
||||
|
||||
quickAskSizeUpdated = 'quickAskSizeUpdated',
|
||||
ackDeepLink = 'ackDeepLink',
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,6 +46,8 @@ export enum AppEvent {
|
||||
|
||||
onUserSubmitQuickAsk = 'onUserSubmitQuickAsk',
|
||||
onSelectedText = 'onSelectedText',
|
||||
|
||||
onDeepLink = 'onDeepLink',
|
||||
}
|
||||
|
||||
export enum DownloadRoute {
|
||||
|
||||
@ -151,4 +151,8 @@ export function handleAppIPCs() {
|
||||
async (_event, heightOffset: number): Promise<void> =>
|
||||
windowManager.expandQuickAskWindow(heightOffset)
|
||||
)
|
||||
|
||||
ipcMain.handle(NativeRoute.ackDeepLink, async (_event): Promise<void> => {
|
||||
windowManager.ackDeepLink()
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { app, BrowserWindow } from 'electron'
|
||||
|
||||
import { join } from 'path'
|
||||
import { join, resolve } from 'path'
|
||||
/**
|
||||
* Managers
|
||||
**/
|
||||
@ -39,15 +39,44 @@ const quickAskUrl = `${mainUrl}/search`
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
|
||||
if (process.defaultApp) {
|
||||
if (process.argv.length >= 2) {
|
||||
app.setAsDefaultProtocolClient('jan', process.execPath, [
|
||||
resolve(process.argv[1]),
|
||||
])
|
||||
}
|
||||
} else {
|
||||
app.setAsDefaultProtocolClient('jan')
|
||||
}
|
||||
|
||||
const createMainWindow = () => {
|
||||
const startUrl = app.isPackaged ? `file://${mainPath}` : mainUrl
|
||||
windowManager.createMainWindow(preloadPath, startUrl)
|
||||
}
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(() => {
|
||||
if (!gotTheLock) {
|
||||
app.quit()
|
||||
throw new Error('Another instance of the app is already running')
|
||||
} else {
|
||||
app.on(
|
||||
'second-instance',
|
||||
(_event, commandLine, _workingDirectory): void => {
|
||||
if (process.platform === 'win32' || process.platform === 'linux') {
|
||||
// this is for handling deeplink on windows and linux
|
||||
// since those OS will emit second-instance instead of open-url
|
||||
const url = commandLine.pop()
|
||||
if (url) {
|
||||
windowManager.sendMainAppDeepLink(url)
|
||||
}
|
||||
}
|
||||
windowManager.showMainWindow()
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
.then(setupReactDevTool)
|
||||
.then(setupCore)
|
||||
.then(createUserSpace)
|
||||
.then(migrateExtensions)
|
||||
@ -60,6 +89,7 @@ app
|
||||
.then(registerGlobalShortcuts)
|
||||
.then(() => {
|
||||
if (!app.isPackaged) {
|
||||
setupReactDevTool()
|
||||
windowManager.mainWindow?.webContents.openDevTools()
|
||||
}
|
||||
})
|
||||
@ -75,11 +105,11 @@ app
|
||||
})
|
||||
})
|
||||
|
||||
app.on('second-instance', (_event, _commandLine, _workingDirectory) => {
|
||||
windowManager.showMainWindow()
|
||||
app.on('open-url', (_event, url) => {
|
||||
windowManager.sendMainAppDeepLink(url)
|
||||
})
|
||||
|
||||
app.on('before-quit', function (evt) {
|
||||
app.on('before-quit', function (_event) {
|
||||
trayManager.destroyCurrentTray()
|
||||
})
|
||||
|
||||
@ -104,11 +134,6 @@ function createQuickAskWindow() {
|
||||
windowManager.createQuickAskWindow(preloadPath, startUrl)
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
const startUrl = app.isPackaged ? `file://${mainPath}` : mainUrl
|
||||
windowManager.createMainWindow(preloadPath, startUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles various IPC messages from the renderer process.
|
||||
*/
|
||||
|
||||
@ -14,9 +14,9 @@ class WindowManager {
|
||||
private _quickAskWindowVisible = false
|
||||
private _mainWindowVisible = false
|
||||
|
||||
private deeplink: string | undefined
|
||||
/**
|
||||
* Creates a new window instance.
|
||||
* @param {Electron.BrowserWindowConstructorOptions} options - The options to create the window with.
|
||||
* @returns The created window instance.
|
||||
*/
|
||||
createMainWindow(preloadPath: string, startUrl: string) {
|
||||
@ -29,6 +29,17 @@ class WindowManager {
|
||||
},
|
||||
})
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
/// This is work around for windows deeplink.
|
||||
/// second-instance event is not fired when app is not open, so the app
|
||||
/// does not received the deeplink.
|
||||
const commandLine = process.argv.slice(1)
|
||||
if (commandLine.length > 0) {
|
||||
const url = commandLine[0]
|
||||
this.sendMainAppDeepLink(url)
|
||||
}
|
||||
}
|
||||
|
||||
/* Load frontend app to the window */
|
||||
this.mainWindow.loadURL(startUrl)
|
||||
|
||||
@ -123,6 +134,22 @@ class WindowManager {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to send the deep link to the main app.
|
||||
*/
|
||||
sendMainAppDeepLink(url: string): void {
|
||||
this.deeplink = url
|
||||
const interval = setInterval(() => {
|
||||
if (!this.deeplink) clearInterval(interval)
|
||||
const mainWindow = this.mainWindow
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send(AppEvent.onDeepLink, this.deeplink)
|
||||
if (mainWindow.isMinimized()) mainWindow.restore()
|
||||
mainWindow.focus()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
cleanUp(): void {
|
||||
if (!this.mainWindow?.isDestroyed()) {
|
||||
this.mainWindow?.close()
|
||||
@ -137,6 +164,13 @@ class WindowManager {
|
||||
this._quickAskWindowVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledges that the window has received a deep link. We can remove it.
|
||||
*/
|
||||
ackDeepLink() {
|
||||
this.deeplink = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const windowManager = new WindowManager()
|
||||
|
||||
@ -61,6 +61,14 @@
|
||||
"include": "scripts/uninstaller.nsh",
|
||||
"deleteAppDataOnUninstall": true
|
||||
},
|
||||
"protocols": [
|
||||
{
|
||||
"name": "Jan",
|
||||
"schemes": [
|
||||
"jan"
|
||||
]
|
||||
}
|
||||
],
|
||||
"artifactName": "jan-${os}-${arch}-${version}.${ext}"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@ -1,17 +1,13 @@
|
||||
import { app } from 'electron'
|
||||
|
||||
export const setupReactDevTool = async () => {
|
||||
if (!app.isPackaged) {
|
||||
// Which means you're running from source code
|
||||
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
|
||||
'electron-devtools-installer'
|
||||
) // Don't use import on top level, since the installer package is dev-only
|
||||
try {
|
||||
const name = await installExtension(REACT_DEVELOPER_TOOLS)
|
||||
console.debug(`Added Extension: ${name}`)
|
||||
} catch (err) {
|
||||
console.error('An error occurred while installing devtools:', err)
|
||||
// Only log the error and don't throw it because it's not critical
|
||||
}
|
||||
// Which means you're running from source code
|
||||
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
|
||||
'electron-devtools-installer'
|
||||
) // Don't use import on top level, since the installer package is dev-only
|
||||
try {
|
||||
const name = await installExtension(REACT_DEVELOPER_TOOLS)
|
||||
console.debug(`Added Extension: ${name}`)
|
||||
} catch (err) {
|
||||
console.error('An error occurred while installing devtools:', err)
|
||||
// Only log the error and don't throw it because it's not critical
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,8 @@ import ImportModelOptionModal from '@/screens/Settings/ImportModelOptionModal'
|
||||
import ImportingModelModal from '@/screens/Settings/ImportingModelModal'
|
||||
import SelectingModelModal from '@/screens/Settings/SelectingModelModal'
|
||||
|
||||
import LoadingModal from '../LoadingModal'
|
||||
|
||||
import MainViewContainer from '../MainViewContainer'
|
||||
|
||||
import InstallingExtensionModal from './BottomBar/InstallingExtension/InstallingExtensionModal'
|
||||
@ -69,6 +71,7 @@ const BaseLayout = () => {
|
||||
<BottomBar />
|
||||
</div>
|
||||
</div>
|
||||
<LoadingModal />
|
||||
{importModelStage === 'SELECTING_MODEL' && <SelectingModelModal />}
|
||||
{importModelStage === 'MODEL_SELECTED' && <ImportModelOptionModal />}
|
||||
{importModelStage === 'IMPORTING_MODEL' && <ImportingModelModal />}
|
||||
|
||||
28
web/containers/LoadingModal/index.tsx
Normal file
28
web/containers/LoadingModal/index.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { Modal, ModalContent, ModalHeader, ModalTitle } from '@janhq/uikit'
|
||||
import { atom, useAtomValue } from 'jotai'
|
||||
|
||||
export type LoadingInfo = {
|
||||
title: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export const loadingModalVisibilityAtom = atom<LoadingInfo | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const ResettingModal: React.FC = () => {
|
||||
const loadingInfo = useAtomValue(loadingModalVisibilityAtom)
|
||||
|
||||
return (
|
||||
<Modal open={loadingInfo != null}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>{loadingInfo?.title}</ModalTitle>
|
||||
</ModalHeader>
|
||||
<p className="text-muted-foreground">{loadingInfo?.message}</p>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResettingModal
|
||||
72
web/containers/Providers/DeepLinkListener.tsx
Normal file
72
web/containers/Providers/DeepLinkListener.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { Fragment, ReactNode } from 'react'
|
||||
|
||||
import { useSetAtom } from 'jotai'
|
||||
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
|
||||
import { useGetHFRepoData } from '@/hooks/useGetHFRepoData'
|
||||
|
||||
import { loadingModalVisibilityAtom as loadingModalInfoAtom } from '../LoadingModal'
|
||||
import { toaster } from '../Toast'
|
||||
|
||||
import {
|
||||
importHuggingFaceModelStageAtom,
|
||||
importingHuggingFaceRepoDataAtom,
|
||||
} from '@/helpers/atoms/HuggingFace.atom'
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const DeepLinkListener: React.FC<Props> = ({ children }) => {
|
||||
const { getHfRepoData } = useGetHFRepoData()
|
||||
const setLoadingInfo = useSetAtom(loadingModalInfoAtom)
|
||||
const setImportingHuggingFaceRepoData = useSetAtom(
|
||||
importingHuggingFaceRepoDataAtom
|
||||
)
|
||||
const setImportHuggingFaceModelStage = useSetAtom(
|
||||
importHuggingFaceModelStageAtom
|
||||
)
|
||||
|
||||
const debounced = useDebouncedCallback(async (searchText) => {
|
||||
if (searchText.indexOf('/') === -1) {
|
||||
toaster({
|
||||
title: 'Failed to get Hugging Face models',
|
||||
description: 'Invalid Hugging Face model URL',
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoadingInfo({
|
||||
title: 'Getting Hugging Face models',
|
||||
message: 'Please wait..',
|
||||
})
|
||||
const data = await getHfRepoData(searchText)
|
||||
setImportingHuggingFaceRepoData(data)
|
||||
setImportHuggingFaceModelStage('REPO_DETAIL')
|
||||
setLoadingInfo(undefined)
|
||||
} catch (err) {
|
||||
setLoadingInfo(undefined)
|
||||
let errMessage = 'Unexpected Error'
|
||||
if (err instanceof Error) {
|
||||
errMessage = err.message
|
||||
}
|
||||
toaster({
|
||||
title: 'Failed to get Hugging Face models',
|
||||
description: errMessage,
|
||||
type: 'error',
|
||||
})
|
||||
console.error(err)
|
||||
}
|
||||
}, 300)
|
||||
window.electronAPI?.onDeepLink((_event: string, input: string) => {
|
||||
window.core?.api?.ackDeepLink()
|
||||
const url = input.replaceAll('jan://', '')
|
||||
debounced(url)
|
||||
})
|
||||
|
||||
return <Fragment>{children}</Fragment>
|
||||
}
|
||||
|
||||
export default DeepLinkListener
|
||||
@ -22,6 +22,7 @@ import Loader from '../Loader'
|
||||
|
||||
import DataLoader from './DataLoader'
|
||||
|
||||
import DeepLinkListener from './DeepLinkListener'
|
||||
import KeyListener from './KeyListener'
|
||||
|
||||
import { extensionManager } from '@/extension'
|
||||
@ -78,7 +79,9 @@ const Providers = ({ children }: PropsWithChildren) => {
|
||||
<KeyListener>
|
||||
<EventListenerWrapper>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DataLoader>{children}</DataLoader>
|
||||
<DataLoader>
|
||||
<DeepLinkListener>{children}</DeepLinkListener>
|
||||
</DataLoader>
|
||||
</TooltipProvider>
|
||||
</EventListenerWrapper>
|
||||
<Toaster />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user