Refactored electron code and added spotlight feature.
This commit is contained in:
parent
d86a0a3c09
commit
5e7e9510fb
@ -400,3 +400,10 @@ body {
|
||||
.ant-btn-variant-outlined.ant-btn.ant-btn-icon-only {
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.electron-spotlight-content .ant-input-outlined {
|
||||
background-color: transparent !important;
|
||||
border: 1px solid transparent !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"gcode-preview": "^2.18.0",
|
||||
"keycloak-js": "^26.2.0",
|
||||
"keytar": "^7.9.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"moment": "^2.30.1",
|
||||
"online-3d-viewer": "^0.16.0",
|
||||
@ -73,10 +74,10 @@
|
||||
"description": "3D Printer ERP and Control Software.",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development vite",
|
||||
"electron": "cross-env ELECTRON_START_URL=http://0.0.0.0:5173 && cross-env NODE_ENV=development && electron .",
|
||||
"electron": "cross-env ELECTRON_START_URL=http://0.0.0.0:5780 && cross-env NODE_ENV=development && electron .",
|
||||
"start": "serve -s build",
|
||||
"build": "vite build",
|
||||
"dev:electron": "concurrently \"cross-env NODE_ENV=development vite --no-open\" \"cross-env ELECTRON_START_URL=http://localhost:5173 cross-env NODE_ENV=development electron public/electron.js\"",
|
||||
"dev:electron": "concurrently \"cross-env NODE_ENV=development vite --no-open\" \"cross-env ELECTRON_START_URL=http://localhost:5780 cross-env NODE_ENV=development electron public/electron.js\"",
|
||||
"build:electron": "vite build && electron-builder",
|
||||
"deploy": "vite build && wrangler pages deploy"
|
||||
},
|
||||
|
||||
@ -1,62 +1,44 @@
|
||||
import { app, BrowserWindow, ipcMain, shell, Menu } from 'electron'
|
||||
import path, { dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { app, ipcMain, shell, globalShortcut } from 'electron'
|
||||
import { createRequire } from 'module'
|
||||
import {
|
||||
registerGlobalShortcuts,
|
||||
setupSpotlightIPC
|
||||
} from './spotlightWindow.js'
|
||||
import {
|
||||
createWindow,
|
||||
setupMainWindowIPC,
|
||||
setupMainWindowAppEvents,
|
||||
setupDevAuthServer
|
||||
} from './mainWindow.js'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
let win
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
frame: false,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
trafficLightPosition: { x: 14, y: 12 },
|
||||
backgroundColor: '#141414',
|
||||
icon: path.join(__dirname, './logo512.png'),
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
})
|
||||
|
||||
// Set up custom menu bar
|
||||
const env = (process.env.NODE_ENV || 'development').trim()
|
||||
if (env === 'development') {
|
||||
const devMenu = [
|
||||
{
|
||||
label: 'Developer',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Toggle Developer Tools',
|
||||
accelerator:
|
||||
process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||
click: () => {
|
||||
win.webContents.toggleDevTools()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
const menu = Menu.buildFromTemplate(devMenu)
|
||||
Menu.setApplicationMenu(menu)
|
||||
} else {
|
||||
Menu.setApplicationMenu(null)
|
||||
}
|
||||
|
||||
// For development, load from localhost; for production, load the built index.html
|
||||
if (process.env.ELECTRON_START_URL) {
|
||||
win.loadURL(process.env.ELECTRON_START_URL)
|
||||
} else {
|
||||
win.loadFile(path.join(__dirname, '../build/index.html'))
|
||||
}
|
||||
|
||||
setupWindowEvents()
|
||||
// --- Keytar-backed auth session storage (main process) ---
|
||||
const require = createRequire(import.meta.url)
|
||||
let keytar = null
|
||||
try {
|
||||
// keytar is a native module; in some dev environments it may not be built yet.
|
||||
keytar = require('keytar')
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'[keytar] Not available; auth session persistence will be disabled.',
|
||||
e?.message || e
|
||||
)
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow)
|
||||
const KEYTAR_SERVICE = app.name || 'Farm Control'
|
||||
const KEYTAR_ACCOUNT = 'authSession'
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
registerGlobalShortcuts()
|
||||
setupSpotlightIPC()
|
||||
setupMainWindowIPC()
|
||||
setupMainWindowAppEvents(app)
|
||||
setupDevAuthServer()
|
||||
})
|
||||
|
||||
app.on('will-quit', () => {
|
||||
globalShortcut.unregisterAll()
|
||||
})
|
||||
|
||||
// IPC handler to get OS
|
||||
ipcMain.handle('os-info', () => {
|
||||
@ -65,112 +47,46 @@ ipcMain.handle('os-info', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// IPC handler to get window state
|
||||
ipcMain.handle('window-state', () => {
|
||||
return {
|
||||
isFullScreen: win ? win.isFullScreen() : false,
|
||||
isMaximized: win ? win.isMaximized() : false
|
||||
ipcMain.handle('auth-session-get', async () => {
|
||||
try {
|
||||
if (!keytar) return null
|
||||
const raw = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT)
|
||||
if (!raw) return null
|
||||
return JSON.parse(raw)
|
||||
} catch (e) {
|
||||
console.warn('[keytar] Failed to read auth session.', e?.message || e)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
// Emit events to renderer when window is maximized/unmaximized
|
||||
function setupWindowEvents() {
|
||||
if (!win) return
|
||||
win.on('maximize', () => {
|
||||
win.webContents.send('window-state', {
|
||||
isMaximized: true
|
||||
})
|
||||
})
|
||||
win.on('enter-full-screen', () => {
|
||||
console.log('Entered fullscreen')
|
||||
win.webContents.send('window-state', {
|
||||
isFullScreen: true
|
||||
})
|
||||
})
|
||||
win.on('leave-full-screen', () => {
|
||||
win.webContents.send('window-state', {
|
||||
isFullScreen: false
|
||||
})
|
||||
})
|
||||
win.on('unmaximize', () => {
|
||||
win.webContents.send('window-state', {
|
||||
isMaximized: false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// IPC handlers for window controls
|
||||
ipcMain.on('window-control', (event, action) => {
|
||||
if (!win) return
|
||||
switch (action) {
|
||||
case 'minimize':
|
||||
win.minimize()
|
||||
break
|
||||
case 'maximize':
|
||||
if (win.isMaximized()) {
|
||||
win.unmaximize()
|
||||
} else {
|
||||
win.maximize()
|
||||
}
|
||||
break
|
||||
case 'close':
|
||||
win.close()
|
||||
break
|
||||
default:
|
||||
break
|
||||
ipcMain.handle('auth-session-set', async (event, session) => {
|
||||
try {
|
||||
if (!keytar) return false
|
||||
if (!session || typeof session !== 'object') return false
|
||||
await keytar.setPassword(
|
||||
KEYTAR_SERVICE,
|
||||
KEYTAR_ACCOUNT,
|
||||
JSON.stringify(session)
|
||||
)
|
||||
return true
|
||||
} catch (e) {
|
||||
console.warn('[keytar] Failed to write auth session.', e?.message || e)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// Add this after other ipcMain handlers
|
||||
ipcMain.handle('auth-session-clear', async () => {
|
||||
try {
|
||||
if (!keytar) return false
|
||||
return await keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT)
|
||||
} catch (e) {
|
||||
console.warn('[keytar] Failed to clear auth session.', e?.message || e)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// IPC handler for opening external URLs
|
||||
ipcMain.handle('open-external-url', (event, url) => {
|
||||
console.log('Opening external url...')
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
app.on('open-url', (event, url) => {
|
||||
event.preventDefault()
|
||||
console.log('App opened with URL:', url)
|
||||
if (url.startsWith('farmcontrol://app')) {
|
||||
// Extract the path/query after 'farmcontrol://app'
|
||||
const redirectPath = url.replace('farmcontrol://app', '') || '/'
|
||||
if (win && win.webContents) {
|
||||
win.webContents.send('navigate', redirectPath)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
|
||||
const env = (process.env.NODE_ENV || 'development').trim()
|
||||
console.log(env)
|
||||
if (env == 'development') {
|
||||
console.log('Starting development auth web server...')
|
||||
import('express').then(({ default: express }) => {
|
||||
const app = express()
|
||||
const port = 3500
|
||||
|
||||
app.use((req, res) => {
|
||||
const redirectPath = req.originalUrl
|
||||
res.send(
|
||||
`Open Farmcontrol to continue... (Redirect path: ${redirectPath})`
|
||||
)
|
||||
if (win && win.webContents) {
|
||||
win.webContents.send('navigate', redirectPath)
|
||||
win.show()
|
||||
win.focus()
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Dev auth server running on http://localhost:${port}`)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
console.log('Will use url scheme instead of auth server.')
|
||||
}
|
||||
|
||||
196
public/mainWindow.js
Normal file
196
public/mainWindow.js
Normal file
@ -0,0 +1,196 @@
|
||||
import { BrowserWindow, ipcMain, Menu } from 'electron'
|
||||
import path, { dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
let win
|
||||
|
||||
function attachKeyboardShortcuts(browserWindow) {
|
||||
if (!browserWindow) return
|
||||
// Keyboard shortcuts for the main window can be added here if needed
|
||||
}
|
||||
|
||||
function setupWindowEvents() {
|
||||
if (!win) return
|
||||
win.on('maximize', () => {
|
||||
win.webContents.send('window-state', {
|
||||
isMaximized: true
|
||||
})
|
||||
})
|
||||
win.on('enter-full-screen', () => {
|
||||
console.log('Entered fullscreen')
|
||||
win.webContents.send('window-state', {
|
||||
isFullScreen: true
|
||||
})
|
||||
})
|
||||
win.on('leave-full-screen', () => {
|
||||
win.webContents.send('window-state', {
|
||||
isFullScreen: false
|
||||
})
|
||||
})
|
||||
win.on('unmaximize', () => {
|
||||
win.webContents.send('window-state', {
|
||||
isMaximized: false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
frame: false,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
trafficLightPosition: { x: 14, y: 12 },
|
||||
backgroundColor: '#141414',
|
||||
icon: path.join(__dirname, './logo512.png'),
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
})
|
||||
|
||||
// Set up custom menu bar
|
||||
const env = (process.env.NODE_ENV || 'development').trim()
|
||||
if (env === 'development') {
|
||||
const devMenu = [
|
||||
{
|
||||
label: 'Developer',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Toggle Developer Tools',
|
||||
accelerator:
|
||||
process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||
click: () => {
|
||||
win.webContents.toggleDevTools()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
const menu = Menu.buildFromTemplate(devMenu)
|
||||
Menu.setApplicationMenu(menu)
|
||||
} else {
|
||||
Menu.setApplicationMenu(null)
|
||||
}
|
||||
|
||||
// For development, load from localhost; for production, load the built index.html
|
||||
if (process.env.ELECTRON_START_URL) {
|
||||
win.loadURL(process.env.ELECTRON_START_URL)
|
||||
} else {
|
||||
win.loadFile(path.join(__dirname, '../build/index.html'))
|
||||
}
|
||||
|
||||
setupWindowEvents()
|
||||
attachKeyboardShortcuts(win)
|
||||
}
|
||||
|
||||
export function getWindow() {
|
||||
return win
|
||||
}
|
||||
|
||||
export function setupMainWindowIPC() {
|
||||
// IPC handler to get window state
|
||||
ipcMain.handle('window-state', () => {
|
||||
if (!win || win.isDestroyed())
|
||||
return { isFullScreen: false, isMaximized: false }
|
||||
return {
|
||||
isFullScreen: win ? win.isFullScreen() : false,
|
||||
isMaximized: win ? win.isMaximized() : false
|
||||
}
|
||||
})
|
||||
|
||||
// IPC handlers for window controls
|
||||
ipcMain.on('window-control', (event, action) => {
|
||||
if (!win) return
|
||||
switch (action) {
|
||||
case 'minimize':
|
||||
win.minimize()
|
||||
break
|
||||
case 'maximize':
|
||||
if (win.isMaximized()) {
|
||||
win.unmaximize()
|
||||
} else {
|
||||
win.maximize()
|
||||
}
|
||||
break
|
||||
case 'close':
|
||||
win.close()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('open-internal-url', (event, url) => {
|
||||
if (!win || win.isDestroyed()) {
|
||||
createWindow()
|
||||
// Wait for window to finish loading before sending navigate event
|
||||
win.webContents.once('did-finish-load', () => {
|
||||
setTimeout(() => {
|
||||
win.webContents.send('navigate', url)
|
||||
win.show()
|
||||
win.focus()
|
||||
}, 100)
|
||||
})
|
||||
} else {
|
||||
win.webContents.send('navigate', url)
|
||||
win.show()
|
||||
win.focus()
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function setupMainWindowAppEvents(app) {
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
|
||||
app.on('open-url', (event, url) => {
|
||||
event.preventDefault()
|
||||
console.log('App opened with URL:', url)
|
||||
if (url.startsWith('farmcontrol://app')) {
|
||||
// Extract the path/query after 'farmcontrol://app'
|
||||
const redirectPath = url.replace('farmcontrol://app', '') || '/'
|
||||
if (win && win.webContents) {
|
||||
win.webContents.send('navigate', redirectPath)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function setupDevAuthServer() {
|
||||
const env = (process.env.NODE_ENV || 'development').trim()
|
||||
if (env == 'development') {
|
||||
console.log('Starting development auth web server...')
|
||||
import('express').then(({ default: express }) => {
|
||||
const app = express()
|
||||
const port = 3500
|
||||
|
||||
app.use((req, res) => {
|
||||
const redirectPath = req.originalUrl
|
||||
res.send(
|
||||
`Open Farmcontrol to continue... (Redirect path: ${redirectPath})`
|
||||
)
|
||||
if (win && win.webContents) {
|
||||
win.webContents.send('navigate', redirectPath)
|
||||
win.show()
|
||||
win.focus()
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Dev auth server running on http://localhost:${port}`)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
console.log('Will use url scheme instead of auth server.')
|
||||
}
|
||||
}
|
||||
136
public/spotlightWindow.js
Normal file
136
public/spotlightWindow.js
Normal file
@ -0,0 +1,136 @@
|
||||
import { BrowserWindow, ipcMain, globalShortcut } from 'electron'
|
||||
import path, { dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
let spotlightWin
|
||||
|
||||
function getSpotlightRouteUrl() {
|
||||
const routePath = '/dashboard/electron/spotlightcontent'
|
||||
|
||||
// Dev: BrowserRouter, so use a real path.
|
||||
if (process.env.ELECTRON_START_URL) {
|
||||
const base = String(process.env.ELECTRON_START_URL).replace(/\/$/, '')
|
||||
return `${base}${routePath}`
|
||||
}
|
||||
|
||||
// Prod (file://): App.jsx chooses HashRouter when `index.html` is in the URL.
|
||||
// So we must load `index.html#/electron/spotlightcontent`.
|
||||
return {
|
||||
filePath: path.join(__dirname, '../build/index.html'),
|
||||
hash: routePath
|
||||
}
|
||||
}
|
||||
|
||||
export function openSpotlightContentWindow() {
|
||||
// If already open, just focus it (avoids accidental window spam).
|
||||
if (spotlightWin && !spotlightWin.isDestroyed()) {
|
||||
spotlightWin.show()
|
||||
spotlightWin.focus()
|
||||
return
|
||||
}
|
||||
|
||||
spotlightWin = new BrowserWindow({
|
||||
width: 700,
|
||||
height: 40,
|
||||
frame: false,
|
||||
resizable: false,
|
||||
center: true,
|
||||
vibrancy: 'menu',
|
||||
transparent: true,
|
||||
icon: path.join(__dirname, './logo512.png'),
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
})
|
||||
|
||||
const target = getSpotlightRouteUrl()
|
||||
if (typeof target === 'string') {
|
||||
spotlightWin.loadURL(target)
|
||||
} else {
|
||||
spotlightWin.loadFile(target.filePath, { hash: target.hash })
|
||||
}
|
||||
|
||||
// Hide spotlight window instead of destroying it when closed
|
||||
spotlightWin.on('close', (event) => {
|
||||
event.preventDefault()
|
||||
if (spotlightWin && !spotlightWin.isDestroyed()) {
|
||||
spotlightWin.hide()
|
||||
}
|
||||
})
|
||||
|
||||
// Hide spotlight window when it loses focus (clicking outside)
|
||||
spotlightWin.on('blur', () => {
|
||||
if (spotlightWin && !spotlightWin.isDestroyed()) {
|
||||
spotlightWin.hide()
|
||||
}
|
||||
})
|
||||
|
||||
attachSpotlightKeyboardShortcuts(spotlightWin)
|
||||
}
|
||||
|
||||
function attachSpotlightKeyboardShortcuts(browserWindow) {
|
||||
if (!browserWindow) return
|
||||
|
||||
browserWindow.webContents.on('before-input-event', (event, input) => {
|
||||
// ESC -> hide SpotlightContent window (if it's the spotlight window)
|
||||
if (
|
||||
input?.type === 'keyDown' &&
|
||||
String(input?.key || '').toLowerCase() === 'escape' &&
|
||||
browserWindow === spotlightWin
|
||||
) {
|
||||
event.preventDefault()
|
||||
if (spotlightWin && !spotlightWin.isDestroyed()) {
|
||||
spotlightWin.hide()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Alt+Shift+Q -> open SpotlightContent window
|
||||
if (
|
||||
input?.type === 'keyDown' &&
|
||||
input?.alt === true &&
|
||||
input?.shift === true &&
|
||||
String(input?.key || '').toLowerCase() === 'q'
|
||||
) {
|
||||
event.preventDefault()
|
||||
openSpotlightContentWindow()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function registerGlobalShortcuts() {
|
||||
try {
|
||||
const registered = globalShortcut.register('Alt+Shift+Q', () => {
|
||||
openSpotlightContentWindow()
|
||||
})
|
||||
|
||||
if (!registered) {
|
||||
console.warn('[globalShortcut] Failed to register Alt+Shift+Q')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
'[globalShortcut] Error registering Alt+Shift+Q',
|
||||
e?.message || e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function setupSpotlightIPC() {
|
||||
// IPC handler to resize spotlight window
|
||||
ipcMain.handle('spotlight-window-resize', (event, height) => {
|
||||
if (!spotlightWin || spotlightWin.isDestroyed()) return false
|
||||
try {
|
||||
const currentBounds = spotlightWin.getBounds()
|
||||
spotlightWin.setSize(currentBounds.width, height)
|
||||
spotlightWin.center()
|
||||
return true
|
||||
} catch (e) {
|
||||
console.warn('[spotlight] Failed to resize window.', e?.message || e)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
15
src/App.jsx
15
src/App.jsx
@ -12,7 +12,10 @@ import PrivateRoute from './components/PrivateRoute'
|
||||
import '../assets/stylesheets/App.css'
|
||||
import { PrintServerProvider } from './components/Dashboard/context/PrintServerContext.jsx'
|
||||
import { AuthProvider } from './components/Dashboard/context/AuthContext.jsx'
|
||||
import { SpotlightProvider } from './components/Dashboard/context/SpotlightContext.jsx'
|
||||
import {
|
||||
SpotlightProvider,
|
||||
ElectronSpotlightContentPage
|
||||
} from './components/Dashboard/context/SpotlightContext.jsx'
|
||||
import { ActionsModalProvider } from './components/Dashboard/context/ActionsModalContext.jsx'
|
||||
|
||||
import {
|
||||
@ -58,6 +61,16 @@ const AppContent = () => {
|
||||
<ActionsModalProvider>
|
||||
<MessageProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
path='/dashboard/electron/spotlightcontent'
|
||||
element={
|
||||
<PrivateRoute
|
||||
component={() => (
|
||||
<ElectronSpotlightContentPage />
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/'
|
||||
element={
|
||||
|
||||
@ -57,7 +57,13 @@ const AuthProvider = ({ children }) => {
|
||||
const [showUnauthorizedModal, setShowUnauthorizedModal] = useState(false)
|
||||
const [showAuthErrorModal, setShowAuthErrorModal] = useState(false)
|
||||
const [authError, setAuthError] = useState(null)
|
||||
const { openExternalUrl, isElectron } = useContext(ElectronContext)
|
||||
const {
|
||||
openExternalUrl,
|
||||
isElectron,
|
||||
getAuthSession,
|
||||
setAuthSession,
|
||||
clearAuthSession
|
||||
} = useContext(ElectronContext)
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
|
||||
@ -71,23 +77,86 @@ const AuthProvider = ({ children }) => {
|
||||
redirectType = 'app-scheme'
|
||||
}
|
||||
|
||||
// Check if cookies are enabled and show warning if not
|
||||
const extractUserFromAuthData = (authData) => {
|
||||
if (!authData || typeof authData !== 'object') return null
|
||||
if (authData.user && typeof authData.user === 'object') return authData.user
|
||||
|
||||
// Some endpoints may return the "user" fields at the top-level. Only treat it
|
||||
// as a user object if it looks like one (avoid confusing token-only payloads).
|
||||
const looksLikeUser =
|
||||
authData._id || authData.username || authData.email || authData.name
|
||||
if (looksLikeUser) return authData
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const isSessionExpired = (session) => {
|
||||
if (!session?.expiresAt) return true
|
||||
const now = new Date()
|
||||
const expirationDate = new Date(session.expiresAt)
|
||||
return expirationDate <= now
|
||||
}
|
||||
|
||||
const persistSession = useCallback(
|
||||
async ({ token: nextToken, expiresAt: nextExpiresAt, user: nextUser }) => {
|
||||
if (isElectron) {
|
||||
return await setAuthSession({
|
||||
token: nextToken,
|
||||
expiresAt: nextExpiresAt,
|
||||
user: nextUser
|
||||
})
|
||||
}
|
||||
return setAuthCookies({
|
||||
access_token: nextToken,
|
||||
expires_at: nextExpiresAt,
|
||||
user: nextUser
|
||||
})
|
||||
},
|
||||
[isElectron, setAuthSession]
|
||||
)
|
||||
|
||||
const clearPersistedSession = useCallback(async () => {
|
||||
if (isElectron) {
|
||||
return await clearAuthSession()
|
||||
}
|
||||
clearAuthCookies()
|
||||
return true
|
||||
}, [isElectron, clearAuthSession])
|
||||
|
||||
// Check if cookies are enabled and show warning if not (web only)
|
||||
useEffect(() => {
|
||||
if (isElectron) return
|
||||
if (!areCookiesEnabled()) {
|
||||
messageApi.warning(
|
||||
'Cookies are disabled. Login state may not persist between tabs.'
|
||||
)
|
||||
}
|
||||
}, [messageApi])
|
||||
}, [messageApi, isElectron])
|
||||
|
||||
// Read token from cookies if present
|
||||
// Read token from cookies (web) or keytar (electron) if present
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
console.log(
|
||||
'Retreiving token from cookies...',
|
||||
getAuthCookies(),
|
||||
validateAuthCookies()
|
||||
)
|
||||
if (isElectron) {
|
||||
const session = await getAuthSession()
|
||||
if (
|
||||
!cancelled &&
|
||||
session &&
|
||||
session.token &&
|
||||
!isSessionExpired(session)
|
||||
) {
|
||||
setToken(session.token)
|
||||
setUserProfile(session.user)
|
||||
setExpiresAt(session.expiresAt)
|
||||
setAuthenticated(true)
|
||||
} else if (!cancelled) {
|
||||
setAuthenticated(false)
|
||||
setUserProfile(null)
|
||||
setShowUnauthorizedModal(true)
|
||||
}
|
||||
} else {
|
||||
// First validate existing cookies to clean up expired ones
|
||||
if (validateAuthCookies()) {
|
||||
const {
|
||||
@ -95,33 +164,41 @@ const AuthProvider = ({ children }) => {
|
||||
expiresAt: storedExpiresAt,
|
||||
user: storedUser
|
||||
} = getAuthCookies()
|
||||
console.log('Retrieved from cookies:', {
|
||||
storedUser,
|
||||
storedToken,
|
||||
storedExpiresAt
|
||||
})
|
||||
|
||||
if (!cancelled) {
|
||||
setToken(storedToken)
|
||||
setUserProfile(storedUser)
|
||||
setExpiresAt(storedExpiresAt)
|
||||
setAuthenticated(true)
|
||||
} else {
|
||||
}
|
||||
} else if (!cancelled) {
|
||||
setAuthenticated(false)
|
||||
setUserProfile(null)
|
||||
setShowUnauthorizedModal(true)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading auth cookies:', error)
|
||||
clearAuthCookies()
|
||||
console.error('Error loading persisted auth session:', error)
|
||||
await clearPersistedSession()
|
||||
if (!cancelled) {
|
||||
setAuthenticated(false)
|
||||
setUserProfile(null)
|
||||
setShowUnauthorizedModal(true)
|
||||
}
|
||||
setRetreivedTokenFromCookies(true)
|
||||
}, [])
|
||||
} finally {
|
||||
if (!cancelled) setRetreivedTokenFromCookies(true)
|
||||
}
|
||||
}
|
||||
|
||||
load()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [isElectron, getAuthSession, clearPersistedSession])
|
||||
|
||||
// Set up cookie synchronization between tabs
|
||||
useEffect(() => {
|
||||
if (isElectron) return
|
||||
const cleanupCookieSync = setupCookieSync(() => {
|
||||
// When cookies change in another tab, re-validate and update state
|
||||
try {
|
||||
@ -159,16 +236,19 @@ const AuthProvider = ({ children }) => {
|
||||
})
|
||||
|
||||
return cleanupCookieSync
|
||||
}, [token, expiresAt, userProfile])
|
||||
}, [token, expiresAt, userProfile, isElectron])
|
||||
|
||||
const logout = useCallback((redirectUri = '/login') => {
|
||||
const logout = useCallback(
|
||||
(redirectUri = '/login') => {
|
||||
setAuthenticated(false)
|
||||
setToken(null)
|
||||
setExpiresAt(null)
|
||||
setUserProfile(null)
|
||||
clearAuthCookies()
|
||||
clearPersistedSession()
|
||||
window.location.href = `${config.backendUrl}/auth/logout?redirect_uri=${encodeURIComponent(redirectUri)}`
|
||||
}, [])
|
||||
},
|
||||
[clearPersistedSession]
|
||||
)
|
||||
|
||||
// Login using query parameters
|
||||
const loginWithSSO = useCallback(
|
||||
@ -210,19 +290,23 @@ const AuthProvider = ({ children }) => {
|
||||
logger.debug('Got auth token!')
|
||||
const authData = response.data
|
||||
|
||||
setToken(authData.access_token)
|
||||
setExpiresAt(authData.expires_at)
|
||||
setUserProfile(authData)
|
||||
const nextToken = authData.access_token
|
||||
const nextExpiresAt = authData.expires_at
|
||||
const nextUser = extractUserFromAuthData(authData)
|
||||
|
||||
// Store in cookies for persistence between tabs
|
||||
const cookieSuccess = setAuthCookies({
|
||||
user: authData,
|
||||
access_token: authData.access_token,
|
||||
expires_at: authData.expires_at
|
||||
setToken(nextToken)
|
||||
setExpiresAt(nextExpiresAt)
|
||||
setUserProfile(nextUser)
|
||||
|
||||
// Persist session (cookies on web, keytar on electron)
|
||||
const persisted = await persistSession({
|
||||
token: nextToken,
|
||||
expiresAt: nextExpiresAt,
|
||||
user: nextUser
|
||||
})
|
||||
if (!cookieSuccess) {
|
||||
if (!persisted) {
|
||||
messageApi.warning(
|
||||
'Authentication successful but failed to save login state. You may need to log in again if you close this tab.'
|
||||
'Authentication successful but failed to save login state. You may need to log in again when you restart the app.'
|
||||
)
|
||||
}
|
||||
|
||||
@ -253,7 +337,14 @@ const AuthProvider = ({ children }) => {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[isElectron, navigate, location.search, location.pathname, messageApi]
|
||||
[
|
||||
isElectron,
|
||||
navigate,
|
||||
location.search,
|
||||
location.pathname,
|
||||
messageApi,
|
||||
persistSession
|
||||
]
|
||||
)
|
||||
|
||||
// Function to check if the user is logged in
|
||||
@ -272,13 +363,20 @@ const AuthProvider = ({ children }) => {
|
||||
logger.debug('Got auth token!')
|
||||
const authData = response.data
|
||||
|
||||
setToken(authData.access_token)
|
||||
setExpiresAt(authData.expires_at)
|
||||
setUserProfile(authData)
|
||||
const nextToken = authData.access_token
|
||||
const nextExpiresAt = authData.expires_at
|
||||
const nextUser = extractUserFromAuthData(authData)
|
||||
|
||||
// Update cookies with fresh data
|
||||
const cookieSuccess = setAuthCookies(authData)
|
||||
if (!cookieSuccess) {
|
||||
setToken(nextToken)
|
||||
setExpiresAt(nextExpiresAt)
|
||||
setUserProfile(nextUser)
|
||||
|
||||
const persisted = await persistSession({
|
||||
token: nextToken,
|
||||
expiresAt: nextExpiresAt,
|
||||
user: nextUser
|
||||
})
|
||||
if (!persisted) {
|
||||
messageApi.warning(
|
||||
'Failed to update login state. You may need to log in again if you close this tab.'
|
||||
)
|
||||
@ -300,13 +398,13 @@ const AuthProvider = ({ children }) => {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [token, messageApi])
|
||||
}, [token, messageApi, persistSession])
|
||||
|
||||
const setUnauthenticated = () => {
|
||||
setToken(null)
|
||||
setExpiresAt(null)
|
||||
setUserProfile(null)
|
||||
clearAuthCookies()
|
||||
clearPersistedSession()
|
||||
setAuthenticated(false)
|
||||
if (showSessionExpiredModal == false) {
|
||||
setShowUnauthorizedModal(true)
|
||||
@ -324,12 +422,19 @@ const AuthProvider = ({ children }) => {
|
||||
if (response.status === 200 && response.data) {
|
||||
const authData = response.data
|
||||
|
||||
setToken(authData.access_token)
|
||||
setExpiresAt(authData.expires_at)
|
||||
const nextToken = authData.access_token
|
||||
const nextExpiresAt = authData.expires_at
|
||||
const nextUser = extractUserFromAuthData(authData) || userProfile
|
||||
|
||||
// Update cookies with fresh token data
|
||||
const cookieSuccess = setAuthCookies(authData)
|
||||
if (!cookieSuccess) {
|
||||
setToken(nextToken)
|
||||
setExpiresAt(nextExpiresAt)
|
||||
|
||||
const persisted = await persistSession({
|
||||
token: nextToken,
|
||||
expiresAt: nextExpiresAt,
|
||||
user: nextUser
|
||||
})
|
||||
if (!persisted) {
|
||||
messageApi.warning(
|
||||
'Failed to update login state. You may need to log in again if you close this tab.'
|
||||
)
|
||||
@ -338,7 +443,7 @@ const AuthProvider = ({ children }) => {
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed', error)
|
||||
}
|
||||
}, [token, messageApi])
|
||||
}, [token, messageApi, persistSession, userProfile])
|
||||
|
||||
const handleSessionExpiredModalOk = () => {
|
||||
setShowSessionExpiredModal(false)
|
||||
@ -419,7 +524,8 @@ const AuthProvider = ({ children }) => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check cookies directly if expiresAt is not set in state
|
||||
// Check cookies directly if expiresAt is not set in state (web only)
|
||||
if (isElectron) return
|
||||
const expiryInfo = checkAuthCookiesExpiry(5) // Check if expiring within 5 minutes
|
||||
if (expiryInfo.isExpiringSoon && expiryInfo.minutesRemaining <= 1) {
|
||||
// Show notification for cookies expiring soon
|
||||
@ -478,7 +584,7 @@ const AuthProvider = ({ children }) => {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
}
|
||||
}, [expiresAt, authenticated, notificationApi, refreshToken])
|
||||
}, [expiresAt, authenticated, notificationApi, refreshToken, isElectron])
|
||||
|
||||
useEffect(() => {
|
||||
const authCode =
|
||||
|
||||
@ -47,6 +47,15 @@ const ElectronProvider = ({ children }) => {
|
||||
return false
|
||||
}
|
||||
|
||||
// Function to open internal URL via Electron
|
||||
const openInternalUrl = (url) => {
|
||||
if (electronAvailable && ipcRenderer) {
|
||||
ipcRenderer.invoke('open-internal-url', url)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!ipcRenderer) return
|
||||
|
||||
@ -96,6 +105,45 @@ const ElectronProvider = ({ children }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getAuthSession = async () => {
|
||||
if (!electronAvailable || !ipcRenderer) return null
|
||||
return await ipcRenderer.invoke('auth-session-get')
|
||||
}
|
||||
|
||||
const setAuthSession = async (session) => {
|
||||
if (!electronAvailable || !ipcRenderer) return false
|
||||
return await ipcRenderer.invoke('auth-session-set', session)
|
||||
}
|
||||
|
||||
const clearAuthSession = async () => {
|
||||
if (!electronAvailable || !ipcRenderer) return false
|
||||
return await ipcRenderer.invoke('auth-session-clear')
|
||||
}
|
||||
|
||||
// Backwards-compatible helpers
|
||||
const getToken = async () => {
|
||||
const session = await getAuthSession()
|
||||
return session?.token || null
|
||||
}
|
||||
|
||||
const setToken = async (token) => {
|
||||
const session = (await getAuthSession()) || {}
|
||||
return await setAuthSession({ ...session, token })
|
||||
}
|
||||
|
||||
const resizeSpotlightWindow = async (height) => {
|
||||
if (!electronAvailable || !ipcRenderer) return false
|
||||
try {
|
||||
return await ipcRenderer.invoke('spotlight-window-resize', height)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'[ElectronContext] Failed to resize spotlight window:',
|
||||
error
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ElectronContext.Provider
|
||||
value={{
|
||||
@ -104,7 +152,14 @@ const ElectronProvider = ({ children }) => {
|
||||
isFullScreen,
|
||||
isElectron: electronAvailable,
|
||||
handleWindowControl,
|
||||
openExternalUrl
|
||||
openExternalUrl,
|
||||
openInternalUrl,
|
||||
getAuthSession,
|
||||
setAuthSession,
|
||||
clearAuthSession,
|
||||
getToken,
|
||||
setToken,
|
||||
resizeSpotlightWindow
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -7,7 +7,9 @@ import {
|
||||
Spin,
|
||||
message,
|
||||
Form,
|
||||
Button
|
||||
Button,
|
||||
Card,
|
||||
Divider
|
||||
} from 'antd'
|
||||
import {
|
||||
createContext,
|
||||
@ -32,20 +34,27 @@ import {
|
||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||
import { ApiServerContext } from './ApiServerContext'
|
||||
import { AuthContext } from './AuthContext'
|
||||
import { ElectronContext } from './ElectronContext'
|
||||
|
||||
const SpotlightContext = createContext()
|
||||
|
||||
const SpotlightProvider = ({ children }) => {
|
||||
const SpotlightContent = ({
|
||||
isActive,
|
||||
openKey,
|
||||
defaultQuery,
|
||||
onRequestClose,
|
||||
isElectron
|
||||
}) => {
|
||||
const { Text } = Typography
|
||||
const navigate = useNavigate()
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [listData, setListData] = useState([])
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const [inputPrefix, setInputPrefix] = useState({ prefix: '', mode: null })
|
||||
const [inputPrefix, setInputPrefix] = useState(null)
|
||||
const { fetchSpotlightData } = useContext(ApiServerContext)
|
||||
const { token } = useContext(AuthContext)
|
||||
const { openInternalUrl } = useContext(ElectronContext)
|
||||
|
||||
// Refs for throttling/debouncing
|
||||
const lastFetchTime = useRef(0)
|
||||
@ -54,39 +63,6 @@ const SpotlightProvider = ({ children }) => {
|
||||
const inputRef = useRef(null)
|
||||
const formRef = useRef(null)
|
||||
|
||||
const showSpotlight = (defaultQuery = '') => {
|
||||
setQuery(defaultQuery)
|
||||
setShowModal(true)
|
||||
|
||||
// Set prefix based on default query if provided
|
||||
if (defaultQuery) {
|
||||
// Check if the default query contains a prefix
|
||||
const upperQuery = defaultQuery.toUpperCase()
|
||||
const prefixInfo = parsePrefix(upperQuery)
|
||||
|
||||
if (prefixInfo) {
|
||||
setInputPrefix(prefixInfo)
|
||||
// Set the query to only the part after the prefix and mode character
|
||||
const remainingValue = defaultQuery.substring(
|
||||
prefixInfo.prefix.length + 1
|
||||
)
|
||||
setQuery(remainingValue)
|
||||
checkAndFetchData(defaultQuery)
|
||||
} else {
|
||||
setInputPrefix(null)
|
||||
checkAndFetchData(defaultQuery)
|
||||
}
|
||||
} else {
|
||||
setInputPrefix(null)
|
||||
// Only clear data if we're opening with an empty query and no existing data
|
||||
if (listData.length === 0) {
|
||||
setListData([])
|
||||
}
|
||||
}
|
||||
|
||||
// Focus will be handled in useEffect for proper timing after modal renders
|
||||
}
|
||||
|
||||
// Helper function to parse prefix and mode from query
|
||||
const parsePrefix = (query) => {
|
||||
// Check for prefix format: XXX: or XXX? or XXX^
|
||||
@ -98,7 +74,7 @@ const SpotlightProvider = ({ children }) => {
|
||||
if ([':', '?', '^'].includes(modeChar)) {
|
||||
const prefixModel = getModelByPrefix(potentialPrefix)
|
||||
|
||||
if (prefixModel.prefix === potentialPrefix) {
|
||||
if (prefixModel && prefixModel.prefix === potentialPrefix) {
|
||||
return {
|
||||
...prefixModel,
|
||||
mode: modeChar
|
||||
@ -283,7 +259,7 @@ const SpotlightProvider = ({ children }) => {
|
||||
|
||||
// Helper to get row actions and default action for a model
|
||||
const getRowActions = (model) =>
|
||||
(model.actions || []).filter((action) => action.row === true)
|
||||
(model?.actions || []).filter((action) => action.row === true)
|
||||
|
||||
const getDefaultRowAction = (model) =>
|
||||
getRowActions(model).find((action) => action.default)
|
||||
@ -294,8 +270,12 @@ const SpotlightProvider = ({ children }) => {
|
||||
const itemId = item._id || item.id
|
||||
const url = action.url(itemId)
|
||||
if (url && url !== '#') {
|
||||
if (isElectron) {
|
||||
openInternalUrl(url)
|
||||
} else {
|
||||
navigate(url)
|
||||
setShowModal(false)
|
||||
}
|
||||
if (onRequestClose) onRequestClose()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -309,7 +289,7 @@ const SpotlightProvider = ({ children }) => {
|
||||
const item = listData[0]
|
||||
let type = item.type || item.objectType || inputPrefix?.type
|
||||
const model = getModelByName(type)
|
||||
const defaultAction = getDefaultRowAction(model)
|
||||
const defaultAction = model ? getDefaultRowAction(model) : null
|
||||
if (defaultAction) {
|
||||
triggerRowAction(defaultAction, item)
|
||||
}
|
||||
@ -323,7 +303,7 @@ const SpotlightProvider = ({ children }) => {
|
||||
const item = listData[index]
|
||||
let type = item.type || item.objectType || inputPrefix?.type
|
||||
const model = getModelByName(type)
|
||||
const defaultAction = getDefaultRowAction(model)
|
||||
const defaultAction = model ? getDefaultRowAction(model) : null
|
||||
if (defaultAction) {
|
||||
triggerRowAction(defaultAction, item)
|
||||
}
|
||||
@ -341,7 +321,7 @@ const SpotlightProvider = ({ children }) => {
|
||||
|
||||
// Focus and select text in input when modal becomes visible
|
||||
useEffect(() => {
|
||||
if (showModal && inputRef.current) {
|
||||
if (isActive && inputRef.current) {
|
||||
// Use a small timeout to ensure the modal is fully rendered and visible
|
||||
setTimeout(() => {
|
||||
const input = inputRef.current.input
|
||||
@ -351,25 +331,52 @@ const SpotlightProvider = ({ children }) => {
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
}, [showModal])
|
||||
}, [isActive])
|
||||
|
||||
// Focus input when inputPrefix changes
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
// Only clear data if there's no existing data and no current query
|
||||
if (listData.length === 0 && !query) {
|
||||
setListData([])
|
||||
}
|
||||
if (isActive) {
|
||||
focusInput()
|
||||
}
|
||||
}, [inputPrefix, showModal])
|
||||
}, [inputPrefix, isActive])
|
||||
|
||||
// Update form value when query changes
|
||||
useEffect(() => {
|
||||
if (showModal && formRef.current) {
|
||||
if (isActive && formRef.current) {
|
||||
formRef.current.setFieldsValue({ query: query })
|
||||
}
|
||||
}, [query, showModal])
|
||||
}, [query, isActive])
|
||||
|
||||
// Apply/refresh defaultQuery when the component is opened (modal) or mounted (standalone)
|
||||
useEffect(() => {
|
||||
if (!isActive) return
|
||||
|
||||
const nextDefaultQuery = defaultQuery || ''
|
||||
|
||||
// Set prefix based on default query if provided
|
||||
if (nextDefaultQuery) {
|
||||
const upperQuery = nextDefaultQuery.toUpperCase()
|
||||
const prefixInfo = parsePrefix(upperQuery)
|
||||
|
||||
if (prefixInfo) {
|
||||
setInputPrefix(prefixInfo)
|
||||
const remainingValue = nextDefaultQuery.substring(
|
||||
prefixInfo.prefix.length + 1
|
||||
)
|
||||
setQuery(remainingValue)
|
||||
checkAndFetchData(nextDefaultQuery)
|
||||
} else {
|
||||
setInputPrefix(null)
|
||||
setQuery(nextDefaultQuery)
|
||||
checkAndFetchData(nextDefaultQuery)
|
||||
}
|
||||
} else {
|
||||
setQuery('')
|
||||
setInputPrefix(null)
|
||||
// Keep previous listData behavior (don’t force-clear on open)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [openKey, isActive])
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
@ -394,18 +401,7 @@ const SpotlightProvider = ({ children }) => {
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SpotlightContext.Provider value={{ showSpotlight }}>
|
||||
{contextHolder}
|
||||
<Modal
|
||||
open={showModal}
|
||||
onCancel={() => setShowModal(false)}
|
||||
closeIcon={null}
|
||||
footer={null}
|
||||
width={700}
|
||||
styles={{ content: { padding: 0 } }}
|
||||
destroyOnHidden={true}
|
||||
>
|
||||
const spotlightContent = (
|
||||
<Flex vertical>
|
||||
<Form ref={formRef} onValuesChange={handleSpotlightChange}>
|
||||
<Form.Item name='query' initialValue={query} style={{ margin: 0 }}>
|
||||
@ -444,13 +440,15 @@ const SpotlightProvider = ({ children }) => {
|
||||
</Form>
|
||||
|
||||
{listData.length > 0 && (
|
||||
<>
|
||||
{isElectron && <Divider style={{ margin: 0 }} />}
|
||||
<div style={{ marginLeft: '18px', marginRight: '14px' }}>
|
||||
<List
|
||||
dataSource={listData}
|
||||
renderItem={(item, index) => {
|
||||
let type = item.objectType || inputPrefix?.type
|
||||
const model = getModelByName(type)
|
||||
const Icon = model.icon
|
||||
const Icon = model?.icon
|
||||
const rowActions = getRowActions(model)
|
||||
let shortcutText = ''
|
||||
if (index === 0) {
|
||||
@ -538,8 +536,62 @@ const SpotlightProvider = ({ children }) => {
|
||||
}}
|
||||
></List>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
{spotlightContent}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
SpotlightContent.propTypes = {
|
||||
isActive: PropTypes.bool,
|
||||
openKey: PropTypes.number,
|
||||
defaultQuery: PropTypes.string,
|
||||
onRequestClose: PropTypes.func,
|
||||
isElectron: PropTypes.bool
|
||||
}
|
||||
|
||||
SpotlightContent.defaultProps = {
|
||||
isActive: true,
|
||||
openKey: 0,
|
||||
defaultQuery: '',
|
||||
onRequestClose: null
|
||||
}
|
||||
|
||||
const SpotlightProvider = ({ children }) => {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [openKey, setOpenKey] = useState(0)
|
||||
const [defaultQuery, setDefaultQuery] = useState('')
|
||||
|
||||
const showSpotlight = (nextDefaultQuery = '') => {
|
||||
setDefaultQuery(nextDefaultQuery)
|
||||
setOpenKey((k) => k + 1)
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<SpotlightContext.Provider value={{ showSpotlight }}>
|
||||
<Modal
|
||||
open={showModal}
|
||||
onCancel={() => setShowModal(false)}
|
||||
closeIcon={null}
|
||||
footer={null}
|
||||
width={700}
|
||||
styles={{ content: { padding: 0 } }}
|
||||
destroyOnHidden={true}
|
||||
>
|
||||
<SpotlightContent
|
||||
isActive={showModal}
|
||||
openKey={openKey}
|
||||
defaultQuery={defaultQuery}
|
||||
onRequestClose={() => setShowModal(false)}
|
||||
/>
|
||||
</Modal>
|
||||
{children}
|
||||
</SpotlightContext.Provider>
|
||||
@ -550,4 +602,100 @@ SpotlightProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
}
|
||||
|
||||
export { SpotlightProvider, SpotlightContext }
|
||||
const ElectronSpotlightContentPage = () => {
|
||||
const cardRef = useRef(null)
|
||||
const resizeTimeoutRef = useRef(null)
|
||||
const { resizeSpotlightWindow, isElectron } = useContext(ElectronContext)
|
||||
|
||||
// Function to measure and resize window
|
||||
const updateWindowHeight = () => {
|
||||
if (!cardRef.current || !isElectron || !resizeSpotlightWindow) return
|
||||
|
||||
// Clear any pending resize
|
||||
if (resizeTimeoutRef.current) {
|
||||
clearTimeout(resizeTimeoutRef.current)
|
||||
}
|
||||
|
||||
// Debounce the resize to avoid too many calls
|
||||
resizeTimeoutRef.current = setTimeout(() => {
|
||||
try {
|
||||
const cardElement = cardRef.current
|
||||
if (!cardElement) return
|
||||
|
||||
// Get the scroll height of the card
|
||||
const scrollHeight = cardElement.scrollHeight
|
||||
|
||||
// Add some padding for better appearance (e.g., 10px top and bottom)
|
||||
const padding = 0
|
||||
const newHeight = scrollHeight + padding * 2
|
||||
|
||||
// Set minimum height to prevent window from being too small
|
||||
const minHeight = 30
|
||||
const finalHeight = Math.max(newHeight, minHeight)
|
||||
|
||||
// Call IPC to resize the window
|
||||
resizeSpotlightWindow(finalHeight)
|
||||
} catch (error) {
|
||||
console.warn('Failed to update spotlight window height:', error)
|
||||
}
|
||||
}, 100) // 100ms debounce
|
||||
}
|
||||
|
||||
// Use ResizeObserver to watch for content changes
|
||||
useEffect(() => {
|
||||
if (!cardRef.current || !isElectron || !resizeSpotlightWindow) return
|
||||
|
||||
const cardElement = cardRef.current
|
||||
|
||||
// Create ResizeObserver to watch for size changes
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateWindowHeight()
|
||||
})
|
||||
|
||||
resizeObserver.observe(cardElement)
|
||||
|
||||
// Initial measurement after a short delay to ensure content is rendered
|
||||
const initialTimeout = setTimeout(() => {
|
||||
updateWindowHeight()
|
||||
}, 200)
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
if (resizeTimeoutRef.current) {
|
||||
clearTimeout(resizeTimeoutRef.current)
|
||||
}
|
||||
clearTimeout(initialTimeout)
|
||||
}
|
||||
}, [isElectron, resizeSpotlightWindow])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
style={{
|
||||
maxWidth: 700,
|
||||
padding: 0
|
||||
}}
|
||||
className='electron-spotlight-content'
|
||||
>
|
||||
<Card
|
||||
styles={{ body: { padding: 0 } }}
|
||||
variant={'borderless'}
|
||||
style={{ border: 'none', backgroundColor: 'transparent' }}
|
||||
>
|
||||
<SpotlightContent
|
||||
isActive={true}
|
||||
openKey={1}
|
||||
defaultQuery=''
|
||||
isElectron={true}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
SpotlightProvider,
|
||||
SpotlightContext,
|
||||
SpotlightContent,
|
||||
ElectronSpotlightContentPage
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ export default defineConfig({
|
||||
server: {
|
||||
allowedHosts: ['dev.tombutcher.work'],
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
port: 5780,
|
||||
open: false
|
||||
}
|
||||
})
|
||||
|
||||
117
yarn.lock
117
yarn.lock
@ -3355,7 +3355,7 @@ baseline-browser-mapping@^2.8.25:
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz#5de72358cf363ac41e7d642af239f6ac5ed1270a"
|
||||
integrity sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==
|
||||
|
||||
bl@^4.1.0:
|
||||
bl@^4.0.3, bl@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
|
||||
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
|
||||
@ -3671,6 +3671,11 @@ character-reference-invalid@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9"
|
||||
integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==
|
||||
|
||||
chownr@^1.1.1:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
|
||||
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
|
||||
|
||||
chownr@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
|
||||
@ -4376,7 +4381,7 @@ dequal@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
||||
|
||||
detect-libc@^2.0.1:
|
||||
detect-libc@^2.0.0, detect-libc@^2.0.1:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad"
|
||||
integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
|
||||
@ -4635,7 +4640,7 @@ encoding@^0.1.13:
|
||||
dependencies:
|
||||
iconv-lite "^0.6.2"
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
|
||||
version "1.4.5"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c"
|
||||
integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==
|
||||
@ -5203,6 +5208,11 @@ execa@^5.1.1:
|
||||
signal-exit "^3.0.3"
|
||||
strip-final-newline "^2.0.0"
|
||||
|
||||
expand-template@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
|
||||
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
|
||||
|
||||
exponential-backoff@^3.1.1:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz#51cf92c1c0493c766053f9d3abee4434c244d2f6"
|
||||
@ -5480,6 +5490,11 @@ fresh@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4"
|
||||
integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==
|
||||
|
||||
fs-constants@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
|
||||
|
||||
fs-extra@^10.0.0, fs-extra@^10.1.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
|
||||
@ -5648,6 +5663,11 @@ get-symbol-description@^1.1.0:
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.6"
|
||||
|
||||
github-from-package@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
|
||||
integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
|
||||
|
||||
gl-matrix@^3.3.0, gl-matrix@^3.4.3:
|
||||
version "3.4.4"
|
||||
resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.4.4.tgz#7789ee4982f62c7a7af447ee488f3bd6b0c77003"
|
||||
@ -6505,6 +6525,14 @@ keycloak-js@^26.2.0:
|
||||
resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-26.2.1.tgz#644e8b8403268f7a39c3980183e22a6fb64f17db"
|
||||
integrity sha512-bZt6fQj/TLBAmivXSxSlqAJxBx/knNZDQGJIW4ensGYGN4N6tUKV8Zj3Y7/LOV8eIpvWsvqV70fbACihK8Ze0Q==
|
||||
|
||||
keytar@^7.9.0:
|
||||
version "7.9.0"
|
||||
resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.9.0.tgz#4c6225708f51b50cbf77c5aae81721964c2918cb"
|
||||
integrity sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==
|
||||
dependencies:
|
||||
node-addon-api "^4.3.0"
|
||||
prebuild-install "^7.0.1"
|
||||
|
||||
keyv@^4.0.0, keyv@^4.5.3, keyv@^4.5.4:
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
|
||||
@ -7301,7 +7329,7 @@ minimatch@^9.0.3, minimatch@^9.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
|
||||
minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
@ -7370,6 +7398,11 @@ minizlib@^2.1.1, minizlib@^2.1.2:
|
||||
minipass "^3.0.0"
|
||||
yallist "^4.0.0"
|
||||
|
||||
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
||||
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
|
||||
|
||||
mkdirp@^1.0.3, mkdirp@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
@ -7431,6 +7464,11 @@ nanopop@2.3.0:
|
||||
resolved "https://registry.yarnpkg.com/nanopop/-/nanopop-2.3.0.tgz#a5f672fba27d45d6ecbd0b59789c040072915123"
|
||||
integrity sha512-fzN+T2K7/Ah25XU02MJkPZ5q4Tj5FpjmIYq4rvoHX4yb16HzFdCO6JxFFn5Y/oBhQ8no8fUZavnyIv9/+xkBBw==
|
||||
|
||||
napi-build-utils@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e"
|
||||
integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
@ -7454,7 +7492,7 @@ no-case@^3.0.4:
|
||||
lower-case "^2.0.2"
|
||||
tslib "^2.0.3"
|
||||
|
||||
node-abi@^3.45.0:
|
||||
node-abi@^3.3.0, node-abi@^3.45.0:
|
||||
version "3.85.0"
|
||||
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.85.0.tgz#b115d575e52b2495ef08372b058e13d202875a7d"
|
||||
integrity sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==
|
||||
@ -7466,6 +7504,11 @@ node-addon-api@^1.6.3:
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d"
|
||||
integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==
|
||||
|
||||
node-addon-api@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
|
||||
integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
|
||||
|
||||
node-api-version@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/node-api-version/-/node-api-version-0.2.1.tgz#19bad54f6d65628cbee4e607a325e4488ace2de9"
|
||||
@ -7922,6 +7965,24 @@ postcss@^8.5.6:
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
prebuild-install@^7.0.1:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec"
|
||||
integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==
|
||||
dependencies:
|
||||
detect-libc "^2.0.0"
|
||||
expand-template "^2.0.3"
|
||||
github-from-package "0.0.0"
|
||||
minimist "^1.2.3"
|
||||
mkdirp-classic "^0.5.3"
|
||||
napi-build-utils "^2.0.0"
|
||||
node-abi "^3.3.0"
|
||||
pump "^3.0.0"
|
||||
rc "^1.2.7"
|
||||
simple-get "^4.0.0"
|
||||
tar-fs "^2.0.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
@ -8429,7 +8490,7 @@ rc-virtual-list@^3.14.2, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2:
|
||||
rc-resize-observer "^1.0.0"
|
||||
rc-util "^5.36.0"
|
||||
|
||||
rc@^1.0.1, rc@^1.1.6:
|
||||
rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
|
||||
@ -8544,7 +8605,7 @@ read-pkg@^2.0.0:
|
||||
normalize-package-data "^2.3.2"
|
||||
path-type "^2.0.0"
|
||||
|
||||
readable-stream@^3.4.0:
|
||||
readable-stream@^3.1.1, readable-stream@^3.4.0:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
|
||||
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
|
||||
@ -9072,6 +9133,20 @@ signal-exit@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
|
||||
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
|
||||
|
||||
simple-concat@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
|
||||
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
|
||||
|
||||
simple-get@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
|
||||
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
|
||||
dependencies:
|
||||
decompress-response "^6.0.0"
|
||||
once "^1.3.1"
|
||||
simple-concat "^1.0.0"
|
||||
|
||||
simple-swizzle@^0.2.2:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz#a8d11a45a11600d6a1ecdff6363329e3648c3667"
|
||||
@ -9563,6 +9638,27 @@ synckit@^0.11.7:
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.2.9"
|
||||
|
||||
tar-fs@^2.0.0:
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930"
|
||||
integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==
|
||||
dependencies:
|
||||
chownr "^1.1.1"
|
||||
mkdirp-classic "^0.5.2"
|
||||
pump "^3.0.0"
|
||||
tar-stream "^2.1.4"
|
||||
|
||||
tar-stream@^2.1.4:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
|
||||
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
|
||||
dependencies:
|
||||
bl "^4.0.3"
|
||||
end-of-stream "^1.4.1"
|
||||
fs-constants "^1.0.0"
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^3.1.1"
|
||||
|
||||
tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.2.1:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
|
||||
@ -9725,6 +9821,13 @@ tsparticles@^3.9.1:
|
||||
"@tsparticles/updater-twinkle" "3.9.1"
|
||||
"@tsparticles/updater-wobble" "3.9.1"
|
||||
|
||||
tunnel-agent@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
||||
integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user