Skip to content

Commit 593e7a9

Browse files
committed
feat(file): open desktop files with drag and drop
1 parent b93cabf commit 593e7a9

File tree

15 files changed

+511
-138
lines changed

15 files changed

+511
-138
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "flippio",
3-
"version": "0.1.8",
3+
"version": "0.1.9",
44
"description": "Database viewer with device connection",
55
"author": "koliastanis",
66
"homepage": "https://github.com/groot007/flippio",
@@ -59,8 +59,8 @@
5959
"@types/react": "^18.3.18",
6060
"@types/react-dom": "^18.3.5",
6161
"@vitejs/plugin-react": "^4.3.4",
62-
"electron": "35.0.3",
63-
"electron-builder": "25.1.8",
62+
"electron": "35.1.4",
63+
"electron-builder": "26.0.12",
6464
"electron-vite": "^3.0.0",
6565
"eslint": "^9.20.1",
6666
"eslint-plugin-react-hooks": "^5.1.0",

src/main/index.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,9 @@ import { setupIpcDatabase } from './ipcDatabase'
1111
import { registerVirtualDeviceHandlers } from './ipcVirtualDevices'
1212
import './autoUpdaterEvents'
1313

14-
if (process.env.NODE_ENV === 'production') {
15-
Sentry.init({
16-
dsn: 'https://561d196b910f78c86856522f199f9ef6@o4509048883970048.ingest.de.sentry.io/4509048886132816',
17-
environment: 'production',
18-
})
19-
}
14+
Sentry.init({
15+
dsn: 'https://561d196b910f78c86856522f199f9ef6@o4509048883970048.ingest.de.sentry.io/4509048886132816',
16+
});
2017

2118
(async () => {
2219
const { default: fixPath } = await import('fix-path')

src/main/ipcADB.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ interface DatabaseFile {
1010
packageName: string
1111
filename: string
1212
location: string
13-
deviceType: 'android' | 'iphone'
13+
deviceType: 'android' | 'iphone' | 'desktop'
1414
}
1515

1616
export function setupIpcADB() {

src/main/ipcCommon.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from 'node:fs'
12
import { dialog, ipcMain } from 'electron'
23

34
export function setupIpcCommon() {
@@ -7,7 +8,12 @@ export function setupIpcCommon() {
78
})
89

910
ipcMain.handle('dialog:saveFile', async (_event, options) => {
10-
const result = await dialog.showSaveDialog(options)
11-
return result
11+
const { canceled, filePath } = await dialog.showSaveDialog(options)
12+
13+
if (!canceled && filePath) {
14+
fs.copyFileSync(options.dbFilePath, filePath)
15+
}
16+
17+
return filePath
1218
})
1319
}

src/preload/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import process from 'node:process'
22
import { electronAPI } from '@electron-toolkit/preload'
3-
import { contextBridge, ipcRenderer } from 'electron'
3+
import { contextBridge, ipcRenderer, webUtils } from 'electron'
44

55
// Custom APIs for renderer
66
const api = {
@@ -25,6 +25,9 @@ const api = {
2525
executeQuery: (query: string) =>
2626
ipcRenderer.invoke('db:executeQuery', query),
2727

28+
openFile: () => ipcRenderer.invoke('dialog:selectFile'),
29+
exportFile: file => ipcRenderer.invoke('dialog:saveFile', file),
30+
webUtils,
2831
// Virtual device methods
2932
getAndroidEmulators: () => ipcRenderer.invoke('getAndroidEmulators'),
3033
getIOSSimulators: () => ipcRenderer.invoke('getIOSSimulators'),

src/renderer/src/components/DataGrid.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export function DataGrid() {
132132
<Box
133133
height="100%"
134134
width="100%"
135+
onDragOver={e => e.preventDefault()}
135136
>
136137
<AgGridReact
137138
ref={gridRef}
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import { Box, Flex, Icon, Text } from '@chakra-ui/react'
2+
import { keyframes } from '@emotion/react'
3+
import { useCurrentDatabaseSelection } from '@renderer/store'
4+
import { useColorMode } from '@renderer/ui/color-mode'
5+
// import { webUtils } from 'electron'
6+
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
7+
import { LuDatabase, LuFileType, LuUpload } from 'react-icons/lu'
8+
import { toaster } from '../ui/toaster'
9+
10+
// Create a context for drag and drop functionality
11+
interface DragAndDropContextType {
12+
handleFile: (file: File) => void
13+
isProcessingFile: boolean
14+
}
15+
16+
const DragAndDropContext = createContext<DragAndDropContextType | null>(null)
17+
18+
export function useDragAndDrop() {
19+
const context = useContext(DragAndDropContext)
20+
if (!context) {
21+
throw new Error('useDragAndDrop must be used within a DragAndDropProvider')
22+
}
23+
return context
24+
}
25+
26+
// Define animations with enhanced smoothness
27+
const pulseAnimation = keyframes`
28+
0% { opacity: 0.7; transform: scale(1); }
29+
50% { opacity: 0.9; transform: scale(1.03); }
30+
100% { opacity: 0.7; transform: scale(1); }
31+
`
32+
33+
const floatingAnimation = keyframes`
34+
0% { transform: translateY(0px) rotate(0deg); }
35+
25% { transform: translateY(-8px) rotate(-2deg); }
36+
50% { transform: translateY(-12px) rotate(0deg); }
37+
75% { transform: translateY(-8px) rotate(2deg); }
38+
100% { transform: translateY(0px) rotate(0deg); }
39+
`
40+
41+
const fadeInAnimation = keyframes`
42+
0% { opacity: 0; backdrop-filter: blur(0); transform: scale(0.98); }
43+
100% { opacity: 1; backdrop-filter: blur(8px); transform: scale(1); }
44+
`
45+
46+
const borderFlashAnimation = keyframes`
47+
0% { border-color: var(--chakra-colors-flipioPrimary); }
48+
50% { border-color: var(--chakra-colors-flipioSecondary); }
49+
100% { border-color: var(--chakra-colors-flipioPrimary); }
50+
`
51+
52+
const SUPPORTED_FILE_EXTENSIONS = ['.sqlite', '.db', '.sql', '.sqlite3', '.sqlitedb']
53+
54+
interface DragAndDropProviderProps {
55+
children: React.ReactNode
56+
}
57+
58+
export const DragAndDropProvider: React.FC<DragAndDropProviderProps> = ({ children }) => {
59+
const [isDragging, setIsDragging] = useState(false)
60+
const [isProcessingFile, setIsProcessingFile] = useState(false)
61+
const dragCounterRef = useRef(0)
62+
const { colorMode } = useColorMode()
63+
const isDark = colorMode === 'dark'
64+
const setSelectedDatabaseFile = useCurrentDatabaseSelection(state => state.setSelectedDatabaseFile)
65+
66+
const handleFile = useCallback(async (file: File) => {
67+
if (!file)
68+
return
69+
70+
const filterExt = SUPPORTED_FILE_EXTENSIONS.some(ext => file.name.toLowerCase().endsWith(ext))
71+
72+
if (!filterExt) {
73+
toaster.create({
74+
title: 'Unsupported file type',
75+
description: 'Please drop SQLite database files only (.db, .sqlite, .sql)',
76+
status: 'warning',
77+
duration: 3000,
78+
isClosable: true,
79+
})
80+
return
81+
}
82+
83+
setIsProcessingFile(true)
84+
85+
const filePath = window.api.webUtils.getPathForFile(file)
86+
87+
setSelectedDatabaseFile({
88+
path: filePath,
89+
filename: file.name,
90+
deviceType: 'desktop',
91+
packageName: '',
92+
})
93+
toaster.create({
94+
title: 'Database opened',
95+
description: `Successfully opened ${file.name}`,
96+
status: 'success',
97+
duration: 3000,
98+
isClosable: true,
99+
})
100+
101+
setIsProcessingFile(false)
102+
}, [])
103+
104+
const handleDragEnter = useCallback((e: DragEvent) => {
105+
e.preventDefault()
106+
e.stopPropagation()
107+
108+
dragCounterRef.current += 1
109+
if (dragCounterRef.current === 1) {
110+
setIsDragging(true)
111+
}
112+
}, [])
113+
114+
const handleDragOver = useCallback((e: DragEvent) => {
115+
e.preventDefault()
116+
e.stopPropagation()
117+
118+
if (e.dataTransfer) {
119+
e.dataTransfer.dropEffect = 'copy'
120+
}
121+
}, [])
122+
123+
const handleDragLeave = useCallback((e: DragEvent) => {
124+
e.preventDefault()
125+
e.stopPropagation()
126+
127+
dragCounterRef.current -= 1
128+
if (dragCounterRef.current === 0) {
129+
setIsDragging(false)
130+
}
131+
}, [])
132+
133+
const handleDrop = useCallback((e: DragEvent) => {
134+
e.preventDefault()
135+
e.stopPropagation()
136+
137+
dragCounterRef.current = 0
138+
setIsDragging(false)
139+
140+
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
141+
handleFile(e.dataTransfer.files[0])
142+
}
143+
}, [handleFile])
144+
145+
useEffect(() => {
146+
window.addEventListener('dragenter', handleDragEnter)
147+
window.addEventListener('dragover', handleDragOver)
148+
window.addEventListener('dragleave', handleDragLeave)
149+
window.addEventListener('drop', handleDrop)
150+
151+
return () => {
152+
window.removeEventListener('dragenter', handleDragEnter)
153+
window.removeEventListener('dragover', handleDragOver)
154+
window.removeEventListener('dragleave', handleDragLeave)
155+
window.removeEventListener('drop', handleDrop)
156+
}
157+
}, [handleDragEnter, handleDragOver, handleDragLeave, handleDrop])
158+
159+
return (
160+
<DragAndDropContext.Provider value={{ handleFile, isProcessingFile }}>
161+
{children}
162+
{isDragging && (
163+
<Flex
164+
position="fixed"
165+
top={0}
166+
left={0}
167+
right={0}
168+
bottom={0}
169+
zIndex={9999}
170+
justifyContent="center"
171+
alignItems="center"
172+
bg={isDark ? 'rgba(18, 18, 18, 0.85)' : 'rgba(255, 255, 255, 0.85)'}
173+
backdropFilter="blur(8px)"
174+
animation={`${fadeInAnimation} 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)`}
175+
transition="all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)"
176+
>
177+
<Box
178+
p={8}
179+
borderRadius="xl"
180+
bg={isDark ? 'rgba(18, 18, 18, 0.9)' : 'rgba(255, 255, 255, 0.9)'}
181+
borderWidth="3px"
182+
borderStyle="dashed"
183+
borderColor="flipioPrimary"
184+
textAlign="center"
185+
boxShadow="0 8px 32px rgba(0, 0, 0, 0.15)"
186+
maxWidth="400px"
187+
animation={`${pulseAnimation} 3s infinite ease-in-out, ${borderFlashAnimation} 4s infinite ease-in-out`}
188+
transform="translateY(0)"
189+
transition="transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)"
190+
_hover={{
191+
transform: 'translateY(-5px)',
192+
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.2)',
193+
}}
194+
>
195+
<Icon
196+
as={LuDatabase}
197+
boxSize={20}
198+
mb={6}
199+
color="flipioPrimary"
200+
animation={`${floatingAnimation} 4s infinite ease-in-out`}
201+
filter="drop-shadow(0 5px 15px rgba(17, 147, 160, 0.5))"
202+
transition="all 0.3s ease"
203+
_hover={{
204+
color: 'flipioSecondary',
205+
transform: 'scale(1.1)',
206+
}}
207+
/>
208+
<Text
209+
fontSize="2xl"
210+
fontWeight="bold"
211+
mb={3}
212+
bgGradient="linear(to-r, flipioPrimary, flipioSecondary)"
213+
bgClip="text"
214+
letterSpacing="tight"
215+
>
216+
Drop Database Files Here
217+
</Text>
218+
<Text
219+
color={isDark ? 'gray.300' : 'gray.600'}
220+
fontSize="md"
221+
fontWeight="medium"
222+
>
223+
Release to open SQLite database files
224+
</Text>
225+
<Flex
226+
align="center"
227+
justify="center"
228+
mt={5}
229+
p={3}
230+
bg={isDark ? 'whiteAlpha.100' : 'blackAlpha.50'}
231+
borderRadius="md"
232+
borderWidth="1px"
233+
borderColor={isDark ? 'whiteAlpha.200' : 'blackAlpha.100'}
234+
>
235+
<Icon
236+
as={LuUpload}
237+
mr={3}
238+
color="flipioSecondary"
239+
boxSize={5}
240+
animation={`${pulseAnimation} 2.5s infinite ease-in-out`}
241+
/>
242+
<Text color="flipioSecondary" fontWeight="medium">
243+
Ready to import
244+
</Text>
245+
</Flex>
246+
<Flex
247+
mt={6}
248+
fontSize="sm"
249+
justify="center"
250+
flexWrap="wrap"
251+
gap={2}
252+
>
253+
{SUPPORTED_FILE_EXTENSIONS.map((ext, i) => (
254+
<Flex
255+
key={i}
256+
align="center"
257+
bg={isDark ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.05)'}
258+
px={3}
259+
py={1}
260+
borderRadius="full"
261+
boxShadow="sm"
262+
>
263+
<Icon as={LuFileType} mr={1} fontSize="xs" />
264+
<Text fontWeight="medium">{ext}</Text>
265+
</Flex>
266+
))}
267+
</Flex>
268+
</Box>
269+
</Flex>
270+
)}
271+
</DragAndDropContext.Provider>
272+
)
273+
}

0 commit comments

Comments
 (0)