Reload projects when there are external changes on the file-system (#4077)
This commit is contained in:
71
src/hooks/useFileSystemWatcher.tsx
Normal file
71
src/hooks/useFileSystemWatcher.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
|
||||
type Path = string
|
||||
|
||||
// Not having access to NodeJS functions has influenced the design a little.
|
||||
// There is some indirection going on because we can only pass data between
|
||||
// the NodeJS<->Browser boundary. The actual functions need to run on the
|
||||
// NodeJS side. Because EventEmitters come bundled with their listener
|
||||
// methods it complicates things because we can't just do
|
||||
// watcher.addListener(() => { ... }).
|
||||
|
||||
export const useFileSystemWatcher = (
|
||||
callback: (path: Path) => void,
|
||||
dependencyArray: Path[]
|
||||
): void => {
|
||||
// Track a ref to the callback. This is how we get the callback updated
|
||||
// across the NodeJS<->Browser boundary.
|
||||
const callbackRef = useRef<{ fn: (path: Path) => void }>({
|
||||
fn: (_path) => {},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
callbackRef.current.fn = callback
|
||||
}, [callback])
|
||||
|
||||
// Used to track if dependencyArrray changes.
|
||||
const [dependencyArrayTracked, setDependencyArrayTracked] = useState<Path[]>(
|
||||
[]
|
||||
)
|
||||
|
||||
// On component teardown obliterate all watchers.
|
||||
useEffect(() => {
|
||||
// The hook is useless on web.
|
||||
if (!isDesktop()) return
|
||||
|
||||
return () => {
|
||||
window.electron.watchFileObliterate()
|
||||
}
|
||||
}, [])
|
||||
|
||||
function difference<T>(l1: T[], l2: T[]): [T[], T[]] {
|
||||
return [
|
||||
l1.filter((x) => Boolean(!l2.find((x2) => x2 === x))),
|
||||
l1.filter((x) => Boolean(l2.find((x2) => x2 === x))),
|
||||
]
|
||||
}
|
||||
|
||||
// Removing 1 watcher at a time is only possible because in a filesystem,
|
||||
// a path is unique (there can never be two paths with the same name).
|
||||
// Otherwise we would have to obliterate() the whole list and reconstruct it.
|
||||
useEffect(() => {
|
||||
// The hook is useless on web.
|
||||
if (!isDesktop()) return
|
||||
|
||||
const [pathsRemoved, pathsRemaining] = difference(
|
||||
dependencyArrayTracked,
|
||||
dependencyArray
|
||||
)
|
||||
for (let path of pathsRemoved) {
|
||||
window.electron.watchFileOff(path)
|
||||
}
|
||||
const [pathsAdded] = difference(dependencyArray, dependencyArrayTracked)
|
||||
for (let path of pathsAdded) {
|
||||
window.electron.watchFileOn(path, (_eventType: string, path: Path) =>
|
||||
callbackRef.current.fn(path)
|
||||
)
|
||||
}
|
||||
setDependencyArrayTracked(pathsRemaining.concat(pathsAdded))
|
||||
}, [difference(dependencyArray, dependencyArrayTracked)[0].length !== 0])
|
||||
}
|
41
src/hooks/useProjectsLoader.tsx
Normal file
41
src/hooks/useProjectsLoader.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { trap } from 'lib/trap'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ensureProjectDirectoryExists, listProjects } from 'lib/desktop'
|
||||
import { loadAndValidateSettings } from 'lib/settings/settingsUtils'
|
||||
import { Project } from 'lib/project'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
|
||||
// Hook uses [number] to give users familiarity. It is meant to mimic a
|
||||
// dependency array, but is intended to only ever be used with 1 value.
|
||||
export const useProjectsLoader = (deps?: [number]) => {
|
||||
const [lastTs, setLastTs] = useState(-1)
|
||||
const [projectPaths, setProjectPaths] = useState<Project[]>([])
|
||||
const [projectsDir, setProjectsDir] = useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
// Useless on web, until we get fake filesystems over there.
|
||||
if (!isDesktop) return
|
||||
|
||||
if (deps && deps[0] === lastTs) return
|
||||
|
||||
if (deps) {
|
||||
setLastTs(deps[0])
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
const { configuration } = await loadAndValidateSettings()
|
||||
const _projectsDir = await ensureProjectDirectoryExists(configuration)
|
||||
setProjectsDir(_projectsDir)
|
||||
|
||||
if (projectsDir) {
|
||||
const _projectPaths = await listProjects(configuration)
|
||||
setProjectPaths(_projectPaths)
|
||||
}
|
||||
})().catch(trap)
|
||||
}, deps ?? [])
|
||||
|
||||
return {
|
||||
projectPaths,
|
||||
projectsDir,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user