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',
|
showMainWindow = 'showMainWindow',
|
||||||
|
|
||||||
quickAskSizeUpdated = 'quickAskSizeUpdated',
|
quickAskSizeUpdated = 'quickAskSizeUpdated',
|
||||||
|
ackDeepLink = 'ackDeepLink',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,6 +46,8 @@ export enum AppEvent {
|
|||||||
|
|
||||||
onUserSubmitQuickAsk = 'onUserSubmitQuickAsk',
|
onUserSubmitQuickAsk = 'onUserSubmitQuickAsk',
|
||||||
onSelectedText = 'onSelectedText',
|
onSelectedText = 'onSelectedText',
|
||||||
|
|
||||||
|
onDeepLink = 'onDeepLink',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DownloadRoute {
|
export enum DownloadRoute {
|
||||||
|
|||||||
@ -151,4 +151,8 @@ export function handleAppIPCs() {
|
|||||||
async (_event, heightOffset: number): Promise<void> =>
|
async (_event, heightOffset: number): Promise<void> =>
|
||||||
windowManager.expandQuickAskWindow(heightOffset)
|
windowManager.expandQuickAskWindow(heightOffset)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ipcMain.handle(NativeRoute.ackDeepLink, async (_event): Promise<void> => {
|
||||||
|
windowManager.ackDeepLink()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { app, BrowserWindow } from 'electron'
|
import { app, BrowserWindow } from 'electron'
|
||||||
|
|
||||||
import { join } from 'path'
|
import { join, resolve } from 'path'
|
||||||
/**
|
/**
|
||||||
* Managers
|
* Managers
|
||||||
**/
|
**/
|
||||||
@ -39,15 +39,44 @@ const quickAskUrl = `${mainUrl}/search`
|
|||||||
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
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
|
app
|
||||||
.whenReady()
|
.whenReady()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!gotTheLock) {
|
if (!gotTheLock) {
|
||||||
app.quit()
|
app.quit()
|
||||||
throw new Error('Another instance of the app is already running')
|
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(setupCore)
|
||||||
.then(createUserSpace)
|
.then(createUserSpace)
|
||||||
.then(migrateExtensions)
|
.then(migrateExtensions)
|
||||||
@ -60,6 +89,7 @@ app
|
|||||||
.then(registerGlobalShortcuts)
|
.then(registerGlobalShortcuts)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!app.isPackaged) {
|
if (!app.isPackaged) {
|
||||||
|
setupReactDevTool()
|
||||||
windowManager.mainWindow?.webContents.openDevTools()
|
windowManager.mainWindow?.webContents.openDevTools()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -75,11 +105,11 @@ app
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('second-instance', (_event, _commandLine, _workingDirectory) => {
|
app.on('open-url', (_event, url) => {
|
||||||
windowManager.showMainWindow()
|
windowManager.sendMainAppDeepLink(url)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('before-quit', function (evt) {
|
app.on('before-quit', function (_event) {
|
||||||
trayManager.destroyCurrentTray()
|
trayManager.destroyCurrentTray()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -104,11 +134,6 @@ function createQuickAskWindow() {
|
|||||||
windowManager.createQuickAskWindow(preloadPath, startUrl)
|
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.
|
* Handles various IPC messages from the renderer process.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -14,9 +14,9 @@ class WindowManager {
|
|||||||
private _quickAskWindowVisible = false
|
private _quickAskWindowVisible = false
|
||||||
private _mainWindowVisible = false
|
private _mainWindowVisible = false
|
||||||
|
|
||||||
|
private deeplink: string | undefined
|
||||||
/**
|
/**
|
||||||
* Creates a new window instance.
|
* Creates a new window instance.
|
||||||
* @param {Electron.BrowserWindowConstructorOptions} options - The options to create the window with.
|
|
||||||
* @returns The created window instance.
|
* @returns The created window instance.
|
||||||
*/
|
*/
|
||||||
createMainWindow(preloadPath: string, startUrl: string) {
|
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 */
|
/* Load frontend app to the window */
|
||||||
this.mainWindow.loadURL(startUrl)
|
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 {
|
cleanUp(): void {
|
||||||
if (!this.mainWindow?.isDestroyed()) {
|
if (!this.mainWindow?.isDestroyed()) {
|
||||||
this.mainWindow?.close()
|
this.mainWindow?.close()
|
||||||
@ -137,6 +164,13 @@ class WindowManager {
|
|||||||
this._quickAskWindowVisible = false
|
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()
|
export const windowManager = new WindowManager()
|
||||||
|
|||||||
@ -61,6 +61,14 @@
|
|||||||
"include": "scripts/uninstaller.nsh",
|
"include": "scripts/uninstaller.nsh",
|
||||||
"deleteAppDataOnUninstall": true
|
"deleteAppDataOnUninstall": true
|
||||||
},
|
},
|
||||||
|
"protocols": [
|
||||||
|
{
|
||||||
|
"name": "Jan",
|
||||||
|
"schemes": [
|
||||||
|
"jan"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"artifactName": "jan-${os}-${arch}-${version}.${ext}"
|
"artifactName": "jan-${os}-${arch}-${version}.${ext}"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
import { app } from 'electron'
|
|
||||||
|
|
||||||
export const setupReactDevTool = async () => {
|
export const setupReactDevTool = async () => {
|
||||||
if (!app.isPackaged) {
|
|
||||||
// Which means you're running from source code
|
// Which means you're running from source code
|
||||||
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
|
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
|
||||||
'electron-devtools-installer'
|
'electron-devtools-installer'
|
||||||
@ -14,4 +11,3 @@ export const setupReactDevTool = async () => {
|
|||||||
// Only log the error and don't throw it because it's not critical
|
// 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 ImportingModelModal from '@/screens/Settings/ImportingModelModal'
|
||||||
import SelectingModelModal from '@/screens/Settings/SelectingModelModal'
|
import SelectingModelModal from '@/screens/Settings/SelectingModelModal'
|
||||||
|
|
||||||
|
import LoadingModal from '../LoadingModal'
|
||||||
|
|
||||||
import MainViewContainer from '../MainViewContainer'
|
import MainViewContainer from '../MainViewContainer'
|
||||||
|
|
||||||
import InstallingExtensionModal from './BottomBar/InstallingExtension/InstallingExtensionModal'
|
import InstallingExtensionModal from './BottomBar/InstallingExtension/InstallingExtensionModal'
|
||||||
@ -69,6 +71,7 @@ const BaseLayout = () => {
|
|||||||
<BottomBar />
|
<BottomBar />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<LoadingModal />
|
||||||
{importModelStage === 'SELECTING_MODEL' && <SelectingModelModal />}
|
{importModelStage === 'SELECTING_MODEL' && <SelectingModelModal />}
|
||||||
{importModelStage === 'MODEL_SELECTED' && <ImportModelOptionModal />}
|
{importModelStage === 'MODEL_SELECTED' && <ImportModelOptionModal />}
|
||||||
{importModelStage === 'IMPORTING_MODEL' && <ImportingModelModal />}
|
{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 DataLoader from './DataLoader'
|
||||||
|
|
||||||
|
import DeepLinkListener from './DeepLinkListener'
|
||||||
import KeyListener from './KeyListener'
|
import KeyListener from './KeyListener'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { extensionManager } from '@/extension'
|
||||||
@ -78,7 +79,9 @@ const Providers = ({ children }: PropsWithChildren) => {
|
|||||||
<KeyListener>
|
<KeyListener>
|
||||||
<EventListenerWrapper>
|
<EventListenerWrapper>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DataLoader>{children}</DataLoader>
|
<DataLoader>
|
||||||
|
<DeepLinkListener>{children}</DeepLinkListener>
|
||||||
|
</DataLoader>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</EventListenerWrapper>
|
</EventListenerWrapper>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user