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/preload": "^2.0.0",
|
||||||
"@electron-toolkit/utils": "^2.0.1",
|
"@electron-toolkit/utils": "^2.0.1",
|
||||||
"@fontsource/roboto": "^5.0.8",
|
"@fontsource/roboto": "^5.0.8",
|
||||||
"@it-incubator/md-bundler": "0.0.9",
|
"@it-incubator/md-bundler": "0.0.10",
|
||||||
"@it-incubator/mdx-components": "0.0.5",
|
"@it-incubator/mdx-components": "0.0.7",
|
||||||
"@it-incubator/ui-kit": "0.2.18",
|
"@it-incubator/ui-kit": "0.2.20",
|
||||||
"builtin-modules": "^3.3.0",
|
"builtin-modules": "^3.3.0",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
@@ -34,12 +34,9 @@
|
|||||||
"mdx-bundler": "^9.2.1",
|
"mdx-bundler": "^9.2.1",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
"rehype-pretty-code": "^0.10.1",
|
"rehype-pretty-code": "^0.10.1",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0"
|
||||||
"sass": "^1.69.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/eslint-config-prettier": "^1.0.1",
|
|
||||||
"@electron-toolkit/eslint-config-ts": "^1.0.0",
|
|
||||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
"@it-incubator/eslint-config": "^1.0.1",
|
"@it-incubator/eslint-config": "^1.0.1",
|
||||||
"@it-incubator/prettier-config": "^0.1.2",
|
"@it-incubator/prettier-config": "^0.1.2",
|
||||||
@@ -56,6 +53,7 @@
|
|||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"sass": "^1.69.4",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^4.5.0"
|
"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 fs from 'fs'
|
||||||
import path from 'node:path'
|
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
import { is } from '@electron-toolkit/utils'
|
||||||
import { bundleMdx, generateToc } from '@it-incubator/md-bundler'
|
import { BrowserWindow, app, ipcMain } from 'electron'
|
||||||
import { BrowserWindow, app, ipcMain, shell } from 'electron'
|
|
||||||
import Store from 'electron-store'
|
|
||||||
|
|
||||||
import icon from '../../resources/icon.png?asset'
|
import { bundleMdxAndSend } from './bundle-mdx-and-send'
|
||||||
|
import { handleAppReady } from './handlers/handle-app-ready'
|
||||||
const chokidar = require('chokidar')
|
import { handleWindowAllClosed } from './handlers/handle-window-all-closed'
|
||||||
const store = new Store()
|
import { prepareAndSendDir } from './prepare-and-send-dir'
|
||||||
|
import { setupWatcher } from './setup-watcher'
|
||||||
|
import { store } from './store'
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
function createWindow(): void {
|
function setMainWindow(win: BrowserWindow) {
|
||||||
// Create the browser window.
|
mainWindow = win
|
||||||
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'))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is.dev) {
|
if (is.dev) {
|
||||||
@@ -58,157 +22,35 @@ if (is.dev) {
|
|||||||
process.env.NODE_ENV = 'production'
|
process.env.NODE_ENV = 'production'
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
/**
|
||||||
// initialization and is ready to create browser windows.
|
* This method will be called when Electron has finished
|
||||||
// Some APIs can only be used after this event occurs.
|
* initialization and is ready to create browser windows.
|
||||||
|
* Some APIs can only be used after this event occurs.
|
||||||
|
*/
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
// Set app user model id for windows
|
handleAppReady(setMainWindow)()
|
||||||
electronApp.setAppUserModelId('com.electron')
|
const currentFilePath = store.getCurrentFilePath()
|
||||||
|
|
||||||
// Default open or close DevTools by F12 in development
|
const currentDirPath = store.getCurrentDirPath()
|
||||||
// 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)
|
|
||||||
})
|
|
||||||
|
|
||||||
createWindow()
|
const currentDirExists = currentDirPath && fs.existsSync(currentDirPath)
|
||||||
|
const currentFileExists = currentFilePath && fs.existsSync(currentFilePath)
|
||||||
|
|
||||||
app.on('activate', function () {
|
if (!currentDirExists && !currentFileExists) {
|
||||||
// On macOS it's common to re-create a window in the app when the
|
store.setCurrentContent(null)
|
||||||
// dock icon is clicked and there are no other windows open.
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
return
|
||||||
// for applications and their menu bar to stay active until the user quits
|
}
|
||||||
// explicitly with Cmd + Q.
|
|
||||||
app.on('window-all-closed', () => {
|
if (currentDirPath) {
|
||||||
if (process.platform !== 'darwin') {
|
prepareAndSendDir(currentDirPath, mainWindow)
|
||||||
app.quit()
|
setupWatcher(currentDirPath, mainWindow)
|
||||||
|
} else if (currentFilePath) {
|
||||||
|
setupWatcher(currentFilePath, mainWindow)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let watcher: any = null
|
app.on('window-all-closed', handleWindowAllClosed)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.on('dropped-file', (event, arg) => {
|
ipcMain.on('dropped-file', (event, arg) => {
|
||||||
console.warn('Dropped File(s):', arg)
|
console.warn('Dropped File(s):', arg)
|
||||||
@@ -221,115 +63,47 @@ ipcMain.on('dropped-file', (event, arg) => {
|
|||||||
|
|
||||||
if (fs.statSync(pathToCheck).isDirectory()) {
|
if (fs.statSync(pathToCheck).isDirectory()) {
|
||||||
// If it's a directory, get the list of files
|
// If it's a directory, get the list of files
|
||||||
prepareAndSendDir(pathToCheck)
|
prepareAndSendDir(pathToCheck, mainWindow)
|
||||||
setupWatcher(pathToCheck)
|
setupWatcher(pathToCheck, mainWindow)
|
||||||
} else {
|
} else {
|
||||||
setupWatcher(pathToCheck)
|
setupWatcher(pathToCheck, mainWindow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('get-current-content', event => {
|
ipcMain.on('get-current-content', event => {
|
||||||
event.reply('current-content', currentContent)
|
event.reply('current-content', store.getCurrentContent())
|
||||||
})
|
})
|
||||||
ipcMain.on('get-current-dir', () => {
|
ipcMain.on('get-current-dir', () => {
|
||||||
const lastOpenDir = store.get('lastOpenDir') as string | undefined
|
const lastOpenDir = store.getCurrentDirPath()
|
||||||
|
|
||||||
if (!lastOpenDir) {
|
if (!lastOpenDir) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
prepareAndSendDir(lastOpenDir)
|
prepareAndSendDir(lastOpenDir, mainWindow)
|
||||||
})
|
})
|
||||||
ipcMain.on('open-file', (_event, filePath) => {
|
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) {
|
process
|
||||||
prepareAndSendDir(lastOpenDir)
|
.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) {
|
if (!lastOpenDir) {
|
||||||
setupWatcher(lastFilePath)
|
return
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
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
|
setSelectedMdx: (s: string) => void
|
||||||
}
|
}
|
||||||
export const MdxFileSelector = ({ data, selectedMdx, setSelectedMdx }: Props) => {
|
export const MdxFileSelector = ({ data, selectedMdx, setSelectedMdx }: Props) => {
|
||||||
console.log(data)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={s.container}>
|
<div className={s.container}>
|
||||||
<FileTree>
|
<FileTree>
|
||||||
@@ -30,7 +28,7 @@ export const MdxFileSelector = ({ data, selectedMdx, setSelectedMdx }: Props) =>
|
|||||||
<RenderItem
|
<RenderItem
|
||||||
isFirst={index === 0 && !selectedMdx}
|
isFirst={index === 0 && !selectedMdx}
|
||||||
item={item}
|
item={item}
|
||||||
key={item.path}
|
key={selectedMdx}
|
||||||
onFileClick={setSelectedMdx}
|
onFileClick={setSelectedMdx}
|
||||||
selectedItemPath={selectedMdx}
|
selectedItemPath={selectedMdx}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export const View = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const directoryContentsListener: IpcRendererListener = (_event, content) => {
|
const directoryContentsListener: IpcRendererListener = (_event, content) => {
|
||||||
console.log('directoryContentsListener', content)
|
|
||||||
setDirectoryContents(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 (
|
return (
|
||||||
<div className={s.page}>
|
<div className={s.page}>
|
||||||
<ImagePreview onClose={() => setSrcPreview('')} open={!!srcPreview} src={srcPreview} />
|
<ImagePreview onClose={() => setSrcPreview('')} open={!!srcPreview} src={srcPreview} />
|
||||||
<div className={s.container}>
|
<div className={s.container}>
|
||||||
<div className={s.fileSelectorContainer}>
|
<div className={s.fileSelectorContainer}>
|
||||||
{directoryContents && (
|
{directoryContents?.data && (
|
||||||
<MdxFileSelector
|
<MdxFileSelector
|
||||||
data={directoryContents.data}
|
data={directoryContents.data}
|
||||||
selectedMdx={selectedFile}
|
selectedMdx={selectedFile}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"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",
|
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||||
"include": ["electron.vite.config.*", "src/main/*", "src/preload/*"],
|
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"types": ["electron-vite/node"]
|
"types": ["electron-vite/node"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user