mirror of
https://github.com/ershisan99/md-preview-desktop.git
synced 2025-12-16 20:59:24 +00:00
chore: finish refactoring
feat: make watcher track directory changes
This commit is contained in:
2735
package-lock.json
generated
2735
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -23,9 +23,9 @@
|
||||
"@electron-toolkit/preload": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^2.0.1",
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@it-incubator/md-bundler": "0.0.9",
|
||||
"@it-incubator/mdx-components": "0.0.5",
|
||||
"@it-incubator/ui-kit": "0.2.18",
|
||||
"@it-incubator/md-bundler": "0.0.10",
|
||||
"@it-incubator/mdx-components": "0.0.7",
|
||||
"@it-incubator/ui-kit": "0.2.20",
|
||||
"builtin-modules": "^3.3.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron-store": "^8.1.0",
|
||||
@@ -34,12 +34,9 @@
|
||||
"mdx-bundler": "^9.2.1",
|
||||
"react-toastify": "^9.1.3",
|
||||
"rehype-pretty-code": "^0.10.1",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"sass": "^1.69.4"
|
||||
"rehype-slug": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config-prettier": "^1.0.1",
|
||||
"@electron-toolkit/eslint-config-ts": "^1.0.0",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@it-incubator/eslint-config": "^1.0.1",
|
||||
"@it-incubator/prettier-config": "^0.1.2",
|
||||
@@ -56,6 +53,7 @@
|
||||
"prettier": "^3.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.69.4",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.0"
|
||||
}
|
||||
|
||||
31
src/main/bundle-mdx-and-send.ts
Normal file
31
src/main/bundle-mdx-and-send.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import { bundleMdx, generateToc } from '@it-incubator/md-bundler'
|
||||
import { BrowserWindow } from 'electron'
|
||||
|
||||
import { store } from './store'
|
||||
|
||||
export const bundleMdxAndSend = (mainWindow: BrowserWindow | null) => async (path: string) => {
|
||||
const currentFilePath = store.getCurrentFilePath()
|
||||
|
||||
if (currentFilePath !== path) {
|
||||
return
|
||||
}
|
||||
fs.readFile(path, 'utf8', async (err, content) => {
|
||||
if (err) {
|
||||
console.error('Error reading the file:', err)
|
||||
|
||||
return
|
||||
}
|
||||
// Send file content to renderer
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
const bundled = await bundleMdx(content)
|
||||
const toc = await generateToc(content, {})
|
||||
const newContent = { ...bundled, fileName: path, toc }
|
||||
|
||||
store.setCurrentContent(newContent)
|
||||
|
||||
mainWindow.webContents.send('current-content', newContent)
|
||||
}
|
||||
})
|
||||
}
|
||||
45
src/main/create-window.ts
Normal file
45
src/main/create-window.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { join } from 'path'
|
||||
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { BrowserWindow, shell } from 'electron'
|
||||
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
|
||||
export function createWindow(): BrowserWindow {
|
||||
const mainWindow = new BrowserWindow({
|
||||
autoHideMenuBar: true,
|
||||
height: 670,
|
||||
show: false,
|
||||
width: 900,
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
},
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow?.show()
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler(details => {
|
||||
shell.openExternal(details.url)
|
||||
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
|
||||
return mainWindow
|
||||
}
|
||||
64
src/main/get-files-recursive.ts
Normal file
64
src/main/get-files-recursive.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import fs from 'fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { FileOrDirectory, FsEntryType } from './types'
|
||||
|
||||
export function getFilesRecursive(
|
||||
directory: string,
|
||||
allowedExtensions: string[] = [],
|
||||
ignoredPaths: string[] = [],
|
||||
includeParent = true,
|
||||
prefix = ''
|
||||
): FileOrDirectory[] {
|
||||
const fileList: FileOrDirectory[] = []
|
||||
|
||||
const filesAndDirs = fs.readdirSync(directory)
|
||||
|
||||
for (const fileOrDir of filesAndDirs) {
|
||||
const absolutePath = path.join(directory, fileOrDir)
|
||||
const relativePath = path.join(prefix, fileOrDir)
|
||||
|
||||
// Skip dotfiles and dot directories
|
||||
if (fileOrDir.startsWith('.')) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip ignored files and directories
|
||||
if (ignoredPaths.some(ignoredPath => absolutePath.includes(ignoredPath))) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (fs.statSync(absolutePath).isDirectory()) {
|
||||
const nestedFiles = getFilesRecursive(
|
||||
absolutePath,
|
||||
allowedExtensions,
|
||||
ignoredPaths,
|
||||
includeParent,
|
||||
relativePath + '/'
|
||||
)
|
||||
|
||||
fileList.push({
|
||||
children: nestedFiles,
|
||||
name: fileOrDir,
|
||||
path: relativePath,
|
||||
type: FsEntryType.Directory,
|
||||
})
|
||||
} else {
|
||||
const extension = path.extname(fileOrDir).toLowerCase()
|
||||
|
||||
// Check the file has an allowed extension
|
||||
if (
|
||||
allowedExtensions.length === 0 ||
|
||||
allowedExtensions.map(e => e.toLowerCase()).includes(extension)
|
||||
) {
|
||||
fileList.push({
|
||||
name: fileOrDir,
|
||||
path: relativePath,
|
||||
type: FsEntryType.File,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fileList
|
||||
}
|
||||
25
src/main/handlers/handle-app-ready.ts
Normal file
25
src/main/handlers/handle-app-ready.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { BrowserWindow, app } from 'electron'
|
||||
|
||||
import { createWindow } from '../create-window'
|
||||
|
||||
export function handleAppReady(onMainWindowCreated: (win: BrowserWindow) => void) {
|
||||
return () => {
|
||||
electronApp.setAppUserModelId('com.electron')
|
||||
onMainWindowCreated(createWindow())
|
||||
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
onMainWindowCreated(createWindow())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
12
src/main/handlers/handle-window-all-closed.ts
Normal file
12
src/main/handlers/handle-window-all-closed.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { app } from 'electron'
|
||||
|
||||
/**
|
||||
* Quit when all windows are closed, except on macOS. There, it's common
|
||||
* for applications and their menu bar to stay active until the user quits
|
||||
* explicitly with Cmd + Q.
|
||||
*/
|
||||
export function handleWindowAllClosed(): void {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,19 @@
|
||||
import fs from 'fs'
|
||||
import path from 'node:path'
|
||||
import { join } from 'path'
|
||||
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||
import { bundleMdx, generateToc } from '@it-incubator/md-bundler'
|
||||
import { BrowserWindow, app, ipcMain, shell } from 'electron'
|
||||
import Store from 'electron-store'
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { BrowserWindow, app, ipcMain } from 'electron'
|
||||
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
|
||||
const chokidar = require('chokidar')
|
||||
const store = new Store()
|
||||
import { bundleMdxAndSend } from './bundle-mdx-and-send'
|
||||
import { handleAppReady } from './handlers/handle-app-ready'
|
||||
import { handleWindowAllClosed } from './handlers/handle-window-all-closed'
|
||||
import { prepareAndSendDir } from './prepare-and-send-dir'
|
||||
import { setupWatcher } from './setup-watcher'
|
||||
import { store } from './store'
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
function createWindow(): void {
|
||||
// Create the browser window.
|
||||
mainWindow = new BrowserWindow({
|
||||
autoHideMenuBar: true,
|
||||
height: 670,
|
||||
show: false,
|
||||
width: 900,
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
},
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow?.show()
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler(details => {
|
||||
shell.openExternal(details.url)
|
||||
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
event.preventDefault()
|
||||
shell.openExternal(url)
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
function setMainWindow(win: BrowserWindow) {
|
||||
mainWindow = win
|
||||
}
|
||||
|
||||
if (is.dev) {
|
||||
@@ -58,157 +22,35 @@ if (is.dev) {
|
||||
process.env.NODE_ENV = 'production'
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
/**
|
||||
* This method will be called when Electron has finished
|
||||
* initialization and is ready to create browser windows.
|
||||
* Some APIs can only be used after this event occurs.
|
||||
*/
|
||||
app.whenReady().then(() => {
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId('com.electron')
|
||||
handleAppReady(setMainWindow)()
|
||||
const currentFilePath = store.getCurrentFilePath()
|
||||
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
const currentDirPath = store.getCurrentDirPath()
|
||||
|
||||
createWindow()
|
||||
const currentDirExists = currentDirPath && fs.existsSync(currentDirPath)
|
||||
const currentFileExists = currentFilePath && fs.existsSync(currentFilePath)
|
||||
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
})
|
||||
if (!currentDirExists && !currentFileExists) {
|
||||
store.setCurrentContent(null)
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
return
|
||||
}
|
||||
|
||||
if (currentDirPath) {
|
||||
prepareAndSendDir(currentDirPath, mainWindow)
|
||||
setupWatcher(currentDirPath, mainWindow)
|
||||
} else if (currentFilePath) {
|
||||
setupWatcher(currentFilePath, mainWindow)
|
||||
}
|
||||
})
|
||||
|
||||
let watcher: any = null
|
||||
let currentContent: any = null
|
||||
|
||||
function setupWatcher(filePath: string) {
|
||||
const isDir = fs.statSync(filePath).isDirectory()
|
||||
|
||||
store.set(isDir ? 'lastOpenDir' : 'lastFilePath', filePath)
|
||||
// Close the existing watcher if it exists
|
||||
if (watcher) {
|
||||
watcher.close()
|
||||
}
|
||||
|
||||
watcher = chokidar.watch(filePath, {
|
||||
ignored: path => {
|
||||
if (path.includes('node_modules')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Ignore if it's not a directory and does not end with .mdx
|
||||
return !path.endsWith('.mdx') && !fs.lstatSync(path).isDirectory()
|
||||
},
|
||||
persistent: true,
|
||||
})
|
||||
|
||||
const bundleAndSend = async (path: string) => {
|
||||
if (isDir) {
|
||||
const lastOpenDir = store.get('lastOpenDir') as string | undefined
|
||||
|
||||
if (!lastOpenDir) {
|
||||
return
|
||||
}
|
||||
|
||||
prepareAndSendDir(lastOpenDir)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
console.log('change', path)
|
||||
fs.readFile(path, 'utf8', async (err, content) => {
|
||||
if (err) {
|
||||
console.error('Error reading the file:', err)
|
||||
|
||||
return
|
||||
}
|
||||
// Send file content to renderer
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
const bundled = await bundleMdx(content)
|
||||
const toc = await generateToc(content, {})
|
||||
const newContent = { ...bundled, fileName: path, toc }
|
||||
|
||||
currentContent = newContent
|
||||
|
||||
mainWindow.webContents.send('current-content', newContent)
|
||||
}
|
||||
})
|
||||
// await shell.openPath(path)
|
||||
}
|
||||
|
||||
bundleAndSend(filePath)
|
||||
// Add your event listeners
|
||||
watcher
|
||||
.on('add', async (path: string) => {
|
||||
console.log('file added', path)
|
||||
await bundleAndSend(path)
|
||||
|
||||
console.warn(`File ${path} has been added`)
|
||||
})
|
||||
.on('addDir', async () => {
|
||||
console.log('add dir')
|
||||
const lastOpenDir = store.get('lastOpenDir') as string | undefined
|
||||
|
||||
if (!lastOpenDir) {
|
||||
return
|
||||
}
|
||||
|
||||
prepareAndSendDir(lastOpenDir)
|
||||
})
|
||||
.on('unlinkDir', async () => {
|
||||
const lastOpenDir = store.get('lastOpenDir') as string | undefined
|
||||
|
||||
if (!lastOpenDir) {
|
||||
return
|
||||
}
|
||||
|
||||
prepareAndSendDir(lastOpenDir)
|
||||
})
|
||||
.on('change', bundleAndSend)
|
||||
.on('unlink', (path: string) => console.warn(`File ${path} has been removed`))
|
||||
|
||||
const watchedPaths = watcher.getWatched()
|
||||
|
||||
console.log(watchedPaths)
|
||||
}
|
||||
|
||||
function prepareAndSendDir(dir: string) {
|
||||
console.log('prepareAndSendDir', dir)
|
||||
const files = fs.readdirSync(dir)
|
||||
const dirName = path.basename(dir)
|
||||
const data = [
|
||||
{
|
||||
children: getFilesRecursive(
|
||||
dir,
|
||||
['.md', '.mdx'],
|
||||
['node_modules', 'README.md'],
|
||||
true,
|
||||
dir + '/'
|
||||
),
|
||||
name: dirName,
|
||||
path: dir,
|
||||
type: FsEntryType.Directory,
|
||||
},
|
||||
]
|
||||
|
||||
// Send the list of files to the renderer process
|
||||
mainWindow?.webContents.send('directory-contents', { data, dir, dirName, files })
|
||||
store.set('lastOpenDir', dir)
|
||||
}
|
||||
app.on('window-all-closed', handleWindowAllClosed)
|
||||
|
||||
ipcMain.on('dropped-file', (event, arg) => {
|
||||
console.warn('Dropped File(s):', arg)
|
||||
@@ -221,115 +63,47 @@ ipcMain.on('dropped-file', (event, arg) => {
|
||||
|
||||
if (fs.statSync(pathToCheck).isDirectory()) {
|
||||
// If it's a directory, get the list of files
|
||||
prepareAndSendDir(pathToCheck)
|
||||
setupWatcher(pathToCheck)
|
||||
prepareAndSendDir(pathToCheck, mainWindow)
|
||||
setupWatcher(pathToCheck, mainWindow)
|
||||
} else {
|
||||
setupWatcher(pathToCheck)
|
||||
setupWatcher(pathToCheck, mainWindow)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('get-current-content', event => {
|
||||
event.reply('current-content', currentContent)
|
||||
event.reply('current-content', store.getCurrentContent())
|
||||
})
|
||||
ipcMain.on('get-current-dir', () => {
|
||||
const lastOpenDir = store.get('lastOpenDir') as string | undefined
|
||||
const lastOpenDir = store.getCurrentDirPath()
|
||||
|
||||
if (!lastOpenDir) {
|
||||
return
|
||||
}
|
||||
prepareAndSendDir(lastOpenDir)
|
||||
prepareAndSendDir(lastOpenDir, mainWindow)
|
||||
})
|
||||
ipcMain.on('open-file', (_event, filePath) => {
|
||||
setupWatcher(filePath)
|
||||
store.setCurrentFilePath(filePath)
|
||||
bundleMdxAndSend(mainWindow)(filePath)
|
||||
})
|
||||
const lastFilePath = store.get('lastFilePath') as string | undefined
|
||||
const lastOpenDir = store.get('lastOpenDir') as string | undefined
|
||||
|
||||
if (lastOpenDir) {
|
||||
prepareAndSendDir(lastOpenDir)
|
||||
}
|
||||
process
|
||||
.on('unhandledRejection', (reason, p) => {
|
||||
console.error(reason, 'Unhandled Rejection at Promise', p)
|
||||
})
|
||||
.on('uncaughtException', err => {
|
||||
// https://github.com/paulmillr/chokidar/issues/566
|
||||
// this has been open for over 7 years, still hasn't been fixed.
|
||||
// for some reason it doesn't even go into the chokidar error handler, so had to do it here
|
||||
if ('code' in err && err.code === 'ENOENT') {
|
||||
const lastOpenDir = store.getCurrentDirPath()
|
||||
|
||||
if (lastFilePath) {
|
||||
setupWatcher(lastFilePath)
|
||||
}
|
||||
|
||||
function getFilesRecursive(
|
||||
directory: string,
|
||||
allowedExtensions: string[] = [],
|
||||
ignoredPaths: string[] = [],
|
||||
includeParent = true,
|
||||
prefix = ''
|
||||
): FileOrDirectory[] {
|
||||
const fileList: FileOrDirectory[] = []
|
||||
|
||||
const filesAndDirs = fs.readdirSync(directory)
|
||||
|
||||
for (const fileOrDir of filesAndDirs) {
|
||||
const absolutePath = path.join(directory, fileOrDir)
|
||||
const relativePath = path.join(prefix, fileOrDir)
|
||||
|
||||
// Skip dotfiles and dot directories
|
||||
if (fileOrDir.startsWith('.')) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip ignored files and directories
|
||||
if (ignoredPaths.some(ignoredPath => absolutePath.includes(ignoredPath))) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (fs.statSync(absolutePath).isDirectory()) {
|
||||
const nestedFiles = getFilesRecursive(
|
||||
absolutePath,
|
||||
allowedExtensions,
|
||||
ignoredPaths,
|
||||
includeParent,
|
||||
relativePath + '/'
|
||||
)
|
||||
|
||||
fileList.push({
|
||||
children: nestedFiles,
|
||||
name: fileOrDir,
|
||||
path: relativePath,
|
||||
type: FsEntryType.Directory,
|
||||
})
|
||||
} else {
|
||||
const extension = path.extname(fileOrDir).toLowerCase()
|
||||
|
||||
// Check the file has an allowed extension
|
||||
if (
|
||||
allowedExtensions.length === 0 ||
|
||||
allowedExtensions.map(e => e.toLowerCase()).includes(extension)
|
||||
) {
|
||||
fileList.push({
|
||||
name: fileOrDir,
|
||||
path: relativePath,
|
||||
type: FsEntryType.File,
|
||||
})
|
||||
if (!lastOpenDir) {
|
||||
return
|
||||
}
|
||||
prepareAndSendDir(lastOpenDir, mainWindow)
|
||||
} else {
|
||||
console.error(err, 'Uncaught Exception thrown')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
return fileList
|
||||
}
|
||||
|
||||
enum FsEntryType {
|
||||
Directory = 'directory',
|
||||
File = 'file',
|
||||
}
|
||||
|
||||
type File = {
|
||||
name: string
|
||||
path: string
|
||||
type: FsEntryType.File
|
||||
}
|
||||
|
||||
type Directory = {
|
||||
children: Array<FileOrDirectory>
|
||||
name: string
|
||||
path: string
|
||||
type: FsEntryType.Directory
|
||||
}
|
||||
|
||||
type FileOrDirectory = Directory | File
|
||||
})
|
||||
|
||||
31
src/main/prepare-and-send-dir.ts
Normal file
31
src/main/prepare-and-send-dir.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import fs from 'fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { type BrowserWindow } from 'electron'
|
||||
|
||||
import { getFilesRecursive } from './get-files-recursive'
|
||||
import { store } from './store'
|
||||
import { FsEntryType } from './types'
|
||||
|
||||
export function prepareAndSendDir(dir: string, mainWindow: BrowserWindow | null) {
|
||||
const files = fs.readdirSync(dir)
|
||||
const dirName = path.basename(dir)
|
||||
const data = [
|
||||
{
|
||||
children: getFilesRecursive(
|
||||
dir,
|
||||
['.md', '.mdx'],
|
||||
['node_modules', 'README.md'],
|
||||
true,
|
||||
dir + '/'
|
||||
),
|
||||
name: dirName,
|
||||
path: dir,
|
||||
type: FsEntryType.Directory,
|
||||
},
|
||||
]
|
||||
|
||||
// Send the list of files to the renderer process
|
||||
mainWindow?.webContents.send('directory-contents', { data, dir, dirName, files })
|
||||
store.setCurrentDirPath(dir)
|
||||
}
|
||||
39
src/main/setup-watcher.ts
Normal file
39
src/main/setup-watcher.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import { BrowserWindow } from 'electron'
|
||||
|
||||
import { bundleMdxAndSend } from './bundle-mdx-and-send'
|
||||
import { prepareAndSendDir } from './prepare-and-send-dir'
|
||||
import { store } from './store'
|
||||
import FileWatcher from './watcher'
|
||||
|
||||
export async function setupWatcher(filePath: string, mainWindow: BrowserWindow | null) {
|
||||
const fileWatcher = FileWatcher.getInstance()
|
||||
const lastOpenDir = store.getCurrentDirPath()
|
||||
|
||||
const isDir = fs.statSync(filePath).isDirectory()
|
||||
|
||||
const path = isDir ? filePath : lastOpenDir || filePath
|
||||
|
||||
fileWatcher.close()
|
||||
fileWatcher.setPaths([path])
|
||||
fileWatcher.start()
|
||||
|
||||
fileWatcher.on('add', reloadDirs(mainWindow))
|
||||
fileWatcher.on('addDir', reloadDirs(mainWindow))
|
||||
fileWatcher.on('unlinkDir', reloadDirs(mainWindow))
|
||||
fileWatcher.on('change', bundleMdxAndSend(mainWindow))
|
||||
fileWatcher.on('unlink', reloadDirs(mainWindow))
|
||||
}
|
||||
|
||||
function reloadDirs(mainWindow: BrowserWindow | null) {
|
||||
return () => {
|
||||
const lastOpenDir = store.getCurrentDirPath()
|
||||
|
||||
if (!lastOpenDir) {
|
||||
return
|
||||
}
|
||||
|
||||
prepareAndSendDir(lastOpenDir, mainWindow)
|
||||
}
|
||||
}
|
||||
37
src/main/store.ts
Normal file
37
src/main/store.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import Store from 'electron-store'
|
||||
|
||||
enum StoreKey {
|
||||
CurrentContent = 'currentContent',
|
||||
CurrentDirPath = 'currentDirPath',
|
||||
CurrentFilePath = 'currentFile',
|
||||
}
|
||||
|
||||
type StoreSchema = {
|
||||
[StoreKey.CurrentContent]: any
|
||||
[StoreKey.CurrentDirPath]: string
|
||||
[StoreKey.CurrentFilePath]: string
|
||||
}
|
||||
|
||||
const _store = new Store<StoreSchema>()
|
||||
|
||||
export const store = {
|
||||
getCurrentContent(): any {
|
||||
return _store.get(StoreKey.CurrentContent)
|
||||
},
|
||||
getCurrentDirPath(): string | undefined {
|
||||
return _store.get(StoreKey.CurrentDirPath)
|
||||
},
|
||||
getCurrentFilePath(): string | undefined {
|
||||
return _store.get(StoreKey.CurrentFilePath)
|
||||
},
|
||||
setCurrentContent(content: any) {
|
||||
_store.set(StoreKey.CurrentContent, content)
|
||||
},
|
||||
|
||||
setCurrentDirPath(dir: string) {
|
||||
_store.set(StoreKey.CurrentDirPath, dir)
|
||||
},
|
||||
setCurrentFilePath(filePath: string) {
|
||||
_store.set(StoreKey.CurrentFilePath, filePath)
|
||||
},
|
||||
}
|
||||
19
src/main/types.ts
Normal file
19
src/main/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export enum FsEntryType {
|
||||
Directory = 'directory',
|
||||
File = 'file',
|
||||
}
|
||||
|
||||
export type File = {
|
||||
name: string
|
||||
path: string
|
||||
type: FsEntryType.File
|
||||
}
|
||||
|
||||
export type Directory = {
|
||||
children: Array<FileOrDirectory>
|
||||
name: string
|
||||
path: string
|
||||
type: FsEntryType.Directory
|
||||
}
|
||||
|
||||
export type FileOrDirectory = Directory | File
|
||||
64
src/main/watcher.ts
Normal file
64
src/main/watcher.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import fs from 'fs'
|
||||
|
||||
import chokidar, { FSWatcher } from 'chokidar'
|
||||
|
||||
class FileWatcher {
|
||||
private static instance: FileWatcher
|
||||
private pathsToWatch: string[]
|
||||
private watcher: FSWatcher | null
|
||||
|
||||
private constructor() {
|
||||
this.pathsToWatch = []
|
||||
this.watcher = null
|
||||
}
|
||||
|
||||
public static getInstance(): FileWatcher {
|
||||
if (!FileWatcher.instance) {
|
||||
FileWatcher.instance = new FileWatcher()
|
||||
}
|
||||
|
||||
return FileWatcher.instance
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
if (this.watcher) {
|
||||
this.watcher
|
||||
.close()
|
||||
.then(() => {
|
||||
console.log('File watcher closed successfully.')
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error closing file watcher:', error)
|
||||
})
|
||||
this.watcher = null
|
||||
}
|
||||
}
|
||||
|
||||
public on(event: string, callback: (path: string) => void): void {
|
||||
this.watcher?.on(event, callback)
|
||||
}
|
||||
|
||||
public setPaths(paths: string[]): void {
|
||||
this.pathsToWatch = paths
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
if (!this.watcher) {
|
||||
this.watcher = chokidar.watch(this.pathsToWatch, {
|
||||
ignored: path => {
|
||||
if (path.includes('node_modules')) {
|
||||
return true
|
||||
}
|
||||
if (path.match(/(^|[/\\])\../)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Ignore if it's not a directory and does not end with .mdx
|
||||
return !path.endsWith('.mdx') && !fs.lstatSync(path).isDirectory()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FileWatcher
|
||||
@@ -20,8 +20,6 @@ type Props = {
|
||||
setSelectedMdx: (s: string) => void
|
||||
}
|
||||
export const MdxFileSelector = ({ data, selectedMdx, setSelectedMdx }: Props) => {
|
||||
console.log(data)
|
||||
|
||||
return (
|
||||
<div className={s.container}>
|
||||
<FileTree>
|
||||
@@ -30,7 +28,7 @@ export const MdxFileSelector = ({ data, selectedMdx, setSelectedMdx }: Props) =>
|
||||
<RenderItem
|
||||
isFirst={index === 0 && !selectedMdx}
|
||||
item={item}
|
||||
key={item.path}
|
||||
key={selectedMdx}
|
||||
onFileClick={setSelectedMdx}
|
||||
selectedItemPath={selectedMdx}
|
||||
/>
|
||||
|
||||
@@ -38,7 +38,6 @@ export const View = () => {
|
||||
}
|
||||
|
||||
const directoryContentsListener: IpcRendererListener = (_event, content) => {
|
||||
console.log('directoryContentsListener', content)
|
||||
setDirectoryContents(content)
|
||||
}
|
||||
|
||||
@@ -55,12 +54,16 @@ export const View = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!directoryContents && !code) {
|
||||
return <div>Drag and drop a directory into this window</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={s.page}>
|
||||
<ImagePreview onClose={() => setSrcPreview('')} open={!!srcPreview} src={srcPreview} />
|
||||
<div className={s.container}>
|
||||
<div className={s.fileSelectorContainer}>
|
||||
{directoryContents && (
|
||||
{directoryContents?.data && (
|
||||
<MdxFileSelector
|
||||
data={directoryContents.data}
|
||||
selectedMdx={selectedFile}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
|
||||
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }],
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||
"include": ["electron.vite.config.*", "src/main/*", "src/preload/*"],
|
||||
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["electron-vite/node"]
|
||||
"types": ["electron-vite/node"],
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user