Skip to content

Commit

Permalink
feat: add deeplink support
Browse files Browse the repository at this point in the history
  • Loading branch information
James committed May 9, 2024
1 parent 0406b51 commit 0888839
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 23 deletions.
3 changes: 3 additions & 0 deletions core/src/types/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum NativeRoute {
showMainWindow = 'showMainWindow',

quickAskSizeUpdated = 'quickAskSizeUpdated',
ackDeepLink = 'ackDeepLink',
}

/**
Expand All @@ -45,6 +46,8 @@ export enum AppEvent {

onUserSubmitQuickAsk = 'onUserSubmitQuickAsk',
onSelectedText = 'onSelectedText',

onDeepLink = 'onDeepLink',
}

export enum DownloadRoute {
Expand Down
4 changes: 4 additions & 0 deletions electron/handlers/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
}
41 changes: 33 additions & 8 deletions electron/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { app, BrowserWindow } from 'electron'

import { join } from 'path'
import { join, resolve } from 'path'
/**
* Managers
**/
Expand Down Expand Up @@ -39,15 +39,40 @@ 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 {
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
app.on('second-instance', (_event, commandLine, _workingDirectory) => {
const url = commandLine.pop()
if (url) {
windowManager.sendMainAppDeepLink(url)
}
})
}
}
})
.then(setupReactDevTool)
.then(setupCore)
.then(createUserSpace)
.then(migrateExtensions)
Expand All @@ -60,6 +85,7 @@ app
.then(registerGlobalShortcuts)
.then(() => {
if (!app.isPackaged) {
setupReactDevTool()
windowManager.mainWindow?.webContents.openDevTools()
}
})
Expand All @@ -75,11 +101,15 @@ app
})
})

app.on('open-url', (_event, url) => {
windowManager.sendMainAppDeepLink(url)
})

app.on('second-instance', (_event, _commandLine, _workingDirectory) => {
windowManager.showMainWindow()
})

app.on('before-quit', function (evt) {
app.on('before-quit', function (_event) {
trayManager.destroyCurrentTray()
})

Expand All @@ -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.
*/
Expand Down
24 changes: 24 additions & 0 deletions electron/managers/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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.
Expand Down Expand Up @@ -123,6 +124,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()
Expand All @@ -137,6 +154,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()
8 changes: 8 additions & 0 deletions electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@
"include": "scripts/uninstaller.nsh",
"deleteAppDataOnUninstall": true
},
"protocols": [
{
"name": "Jan",
"schemes": [
"jan"
]
}
],
"artifactName": "jan-${os}-${arch}-${version}.${ext}"
},
"scripts": {
Expand Down
24 changes: 10 additions & 14 deletions electron/utils/dev.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 3 additions & 0 deletions web/containers/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -69,6 +71,7 @@ const BaseLayout = () => {
<BottomBar />
</div>
</div>
<LoadingModal />
{importModelStage === 'SELECTING_MODEL' && <SelectingModelModal />}
{importModelStage === 'MODEL_SELECTED' && <ImportModelOptionModal />}
{importModelStage === 'IMPORTING_MODEL' && <ImportingModelModal />}
Expand Down
28 changes: 28 additions & 0 deletions web/containers/LoadingModal/index.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 72 additions & 0 deletions web/containers/Providers/DeepLinkListener.tsx
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion web/containers/Providers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Loader from '../Loader'

import DataLoader from './DataLoader'

import DeepLinkListener from './DeepLinkListener'
import KeyListener from './KeyListener'

import { extensionManager } from '@/extension'
Expand Down Expand Up @@ -78,7 +79,9 @@ const Providers = ({ children }: PropsWithChildren) => {
<KeyListener>
<EventListenerWrapper>
<TooltipProvider delayDuration={0}>
<DataLoader>{children}</DataLoader>
<DataLoader>
<DeepLinkListener>{children}</DeepLinkListener>
</DataLoader>
</TooltipProvider>
</EventListenerWrapper>
<Toaster />
Expand Down

0 comments on commit 0888839

Please sign in to comment.