diff --git a/src/_node/file/FileSystemApis.ts b/src/_node/file/FileSystemApis.ts new file mode 100644 index 00000000..713bd699 --- /dev/null +++ b/src/_node/file/FileSystemApis.ts @@ -0,0 +1,442 @@ +import { verifyFileHandlerPermission } from "@_services/main"; + +import { + _createIDBDirectory, + _getIDBDirectoryOrFileStat, + _readIDBDirectory, + _readIDBFile, + _removeIDBDirectoryOrFile, + _writeIDBFile, + getFileNameAndExtensionFromFullname, + TFileHandlerCollection, + TFileNodeData, + TFileNodeTreeData, + TNodeUid, +} from "../"; +import { FileSystemFileHandle } from "file-system-access"; + +const _path = window.Filer.path; + +// true: success, false: fail +const createLocalSingleDirectoryOrFile = async ({ + parentUid, + name, + kind, + fileTree, + fileHandlers, +}: { + parentUid: TNodeUid; + name: string; + kind: "file" | "directory"; + fileTree: TFileNodeTreeData; + fileHandlers: TFileHandlerCollection; +}): Promise => { + const parentNode = fileTree[parentUid]; + if (!parentNode) return false; + + const parentHandler = fileHandlers[parentUid] as FileSystemDirectoryHandle; + if (!(await verifyFileHandlerPermission(parentHandler))) return false; + + try { + if (kind === "directory") { + await parentHandler.getDirectoryHandle(name, { create: true }); + } else { + await parentHandler.getFileHandle(name, { create: true }); + } + return true; + } catch (err) { + return false; + } +}; +const createIDBSingleDirectoryOrFile = async ({ + parentUid, + name, + kind, + fileTree, +}: { + parentUid: TNodeUid; + name: string; + kind: "file" | "directory"; + fileTree: TFileNodeTreeData; +}): Promise => { + const parentNode = fileTree[parentUid]; + if (!parentNode) return false; + + const parentNodeData = parentNode.data; + try { + if (kind === "directory") { + await _createIDBDirectory(_path.join(parentNodeData.path, name)); + } else { + await _writeIDBFile(_path.join(parentNodeData.path, name), ""); + } + return true; + } catch (err) { + return false; + } +}; + +const removeSingleLocalDirectoryOrFile = async ({ + uid, + fileTree, + fileHandlers, +}: { + uid: TNodeUid; + fileTree: TFileNodeTreeData; + fileHandlers: TFileHandlerCollection; +}): Promise => { + const node = fileTree[uid]; + if (!node) return false; + + const parentNode = fileTree[node.parentUid as TNodeUid]; + if (!parentNode) return false; + + const parentHandler = fileHandlers[ + parentNode.uid + ] as FileSystemDirectoryHandle; + if (!(await verifyFileHandlerPermission(parentHandler))) return false; + + const nodeData = node.data; + try { + const entryName = + nodeData.kind === "directory" + ? nodeData.name + : `${nodeData.name}.${nodeData.ext}`; + await parentHandler.removeEntry(entryName, { recursive: true }); + return true; + } catch (err) { + return false; + } +}; +const removeSingleIDBDirectoryOrFile = async ({ + uid, + fileTree, +}: { + uid: TNodeUid; + fileTree: TFileNodeTreeData; +}): Promise => { + const node = fileTree[uid]; + if (!node) return false; + + const parentNode = fileTree[node.parentUid as TNodeUid]; + if (!parentNode) return false; + + const nodeData = node.data; + const parentNodeData = parentNode.data; + try { + const entryName = + nodeData.kind === "directory" + ? nodeData.name + : `${nodeData.name}.${nodeData.ext}`; + await _removeIDBDirectoryOrFile(_path.join(parentNodeData.path, entryName)); + return true; + } catch (err) { + return false; + } +}; + +const _moveLocalDirectory = async ( + source: FileSystemDirectoryHandle, + destination: FileSystemDirectoryHandle, + parentHandler: FileSystemDirectoryHandle, + isCopy: boolean, +) => { + const dirQueue = [ + { + source, + destination, + }, + ]; + try { + while (dirQueue.length) { + const { source, destination } = dirQueue.shift() as { + source: FileSystemDirectoryHandle; + destination: FileSystemDirectoryHandle; + }; + + for await (const entry of source.values()) { + if (entry.kind === "directory") { + const newDir = await destination.getDirectoryHandle(entry.name, { + create: true, + }); + dirQueue.push({ + source: entry as FileSystemDirectoryHandle, + destination: newDir, + }); + } else { + const newFile = await destination.getFileHandle(entry.name, { + create: true, + }); + const content = await (entry as FileSystemFileHandle).getFile(); + const writableStream = await newFile.createWritable(); + await writableStream.write(content); + await writableStream.close(); + } + } + } + + !isCopy && + (await parentHandler.removeEntry(source.name, { recursive: true })); + } catch (err) { + throw "Error while moving a local directory."; + } +}; +const _moveLocalFile = async ( + handler: FileSystemHandle, + parentHandler: FileSystemDirectoryHandle, + targetHandler: FileSystemDirectoryHandle, + newName: string, + isCopy: boolean, +) => { + try { + const newFile = await targetHandler.getFileHandle(newName, { + create: true, + }); + const content = await (handler as FileSystemFileHandle).getFile(); + const writableStream = await newFile.createWritable(); + await writableStream.write(content); + await writableStream.close(); + + !isCopy && + (await parentHandler.removeEntry(handler.name, { recursive: true })); + } catch (err) { + throw "Error while moving a local file."; + } +}; +const moveLocalSingleDirectoryOrFile = async ({ + uid, + targetUid, + newName, + fileTree, + fileHandlers, + isCopy, +}: { + uid: TNodeUid; + targetUid: TNodeUid; + newName: string; + fileTree: TFileNodeTreeData; + fileHandlers: TFileHandlerCollection; + isCopy: boolean; +}): Promise => { + const node = fileTree[uid]; + if (!node) return false; + + const parentNode = fileTree[node.parentUid as TNodeUid]; + if (!parentNode) return false; + + const targetNode = fileTree[targetUid]; + if (!targetNode) return false; + + const handler = fileHandlers[uid]; + const parentHandler = fileHandlers[ + parentNode.uid + ] as FileSystemDirectoryHandle; + const targetHandler = fileHandlers[targetUid] as FileSystemDirectoryHandle; + if ( + !(await verifyFileHandlerPermission(handler)) || + !(await verifyFileHandlerPermission(parentHandler)) || + !(await verifyFileHandlerPermission(targetHandler)) + ) + return false; + + const nodeData = node.data; + try { + if (nodeData.kind === "directory") { + const newHandler = await targetHandler.getDirectoryHandle(newName, { + create: true, + }); + await _moveLocalDirectory( + handler as FileSystemDirectoryHandle, + newHandler, + parentHandler, + isCopy, + ); + } else { + await _moveLocalFile( + handler as FileSystemFileHandle, + parentHandler, + targetHandler, + newName, + isCopy, + ); + } + return true; + } catch (err) { + return false; + } +}; +const _moveIDBDirectory = async ( + nodeData: TFileNodeData, + targetNodeData: TFileNodeData, + newName: string, + isCopy: boolean, +) => { + const dirQueue = [ + { + orgPath: nodeData.path, + newPath: _path.join(targetNodeData.path, newName), + }, + ]; + try { + while (dirQueue.length) { + const { orgPath, newPath } = dirQueue.shift() as { + orgPath: string; + newPath: string; + }; + await _createIDBDirectory(newPath); + + const entries = await _readIDBDirectory(orgPath); + await Promise.all( + entries.map(async (entry) => { + const c_orgPath = _path.join(orgPath, entry); + const c_newPath = _path.join(newPath, entry); + const stats = await _getIDBDirectoryOrFileStat(c_orgPath); + const c_kind = stats.type === "DIRECTORY" ? "directory" : "file"; + if (c_kind === "directory") { + dirQueue.push({ orgPath: c_orgPath, newPath: c_newPath }); + } else { + await _writeIDBFile(c_newPath, await _readIDBFile(c_orgPath)); + } + }), + ); + } + + !isCopy && (await _removeIDBDirectoryOrFile(nodeData.path)); + } catch (err) { + throw "Error while moving an idb directory."; + } +}; +const _moveIDBFile = async ( + nodeData: TFileNodeData, + targetNodeData: TFileNodeData, + newName: string, + isCopy: boolean, +) => { + try { + await _writeIDBFile( + _path.join(targetNodeData.path, newName), + await _readIDBFile(nodeData.path), + ); + + !isCopy && (await _removeIDBDirectoryOrFile(nodeData.path)); + } catch (err) { + throw "Error while moving an idb file."; + } +}; +const moveIDBSingleDirectoryOrFile = async ({ + uid, + targetUid, + newName, + fileTree, + isCopy, +}: { + uid: TNodeUid; + targetUid: TNodeUid; + newName: string; + fileTree: TFileNodeTreeData; + isCopy: boolean; +}): Promise => { + const node = fileTree[uid]; + if (!node) return false; + + const targetNode = fileTree[targetUid]; + if (!targetNode) return false; + + const nodeData = node.data; + const targetNodeData = targetNode.data; + try { + if (nodeData.kind === "directory") { + await _moveIDBDirectory(nodeData, targetNodeData, newName, isCopy); + } else { + await _moveIDBFile(nodeData, targetNodeData, newName, isCopy); + } + return true; + } catch (err) { + return false; + } +}; + +const generateNewNameForLocalDirectoryOrFile = async ({ + nodeData, + parentHandler, +}: { + nodeData: TFileNodeData; + parentHandler: FileSystemDirectoryHandle; +}): Promise => { + let newName = nodeData.name; + const { baseName, ext } = getFileNameAndExtensionFromFullname(nodeData.name); + let exists = true; + let index = -1; + while (exists) { + try { + if (nodeData.kind === "directory") { + await parentHandler.getDirectoryHandle(newName, { create: false }); + } else { + await parentHandler.getFileHandle(newName, { create: false }); + } + } catch (err) { + exists = false; + } + + if (exists) { + ++index; + newName = + nodeData.kind === "directory" + ? index === 0 + ? `${baseName} copy` + : `${baseName} copy (${index})` + : index === 0 + ? `${baseName} copy.${ext}` + : `${baseName} copy (${index}).${ext}`; + } + } + return newName; +}; +const generateNewNameForIDBDirectoryOrFile = async ({ + nodeData, + targetNodeData, +}: { + nodeData: TFileNodeData; + targetNodeData: TFileNodeData; +}): Promise => { + let newName = nodeData.name; + const { baseName, ext } = getFileNameAndExtensionFromFullname(nodeData.name); + let exists = true; + let index = -1; + while (exists) { + try { + await _getIDBDirectoryOrFileStat( + _path.join(targetNodeData.path, newName), + ); + } catch (err) { + exists = false; + } + + if (exists) { + ++index; + newName = + nodeData.kind === "directory" + ? index === 0 + ? `${baseName} copy` + : `${baseName} copy (${index})` + : index === 0 + ? `${baseName} copy.${ext}` + : `${baseName} copy (${index}).${ext}`; + } + } + return newName; +}; + +export const FileSystemApis = { + local: { + createSingleDirectoryOrFile: createLocalSingleDirectoryOrFile, + removeSingleDirectoryOrFile: removeSingleLocalDirectoryOrFile, + moveSingleDirectoryOrFile: moveLocalSingleDirectoryOrFile, + generateNewName: generateNewNameForLocalDirectoryOrFile, + }, + idb: { + createSingleDirectoryOrFile: createIDBSingleDirectoryOrFile, + removeSingleDirectoryOrFile: removeSingleIDBDirectoryOrFile, + moveSingleDirectoryOrFile: moveIDBSingleDirectoryOrFile, + generateNewName: generateNewNameForIDBDirectoryOrFile, + }, +}; diff --git a/src/_node/file/actions.ts b/src/_node/file/actions.ts index e91e2974..1fd0f5a6 100644 --- a/src/_node/file/actions.ts +++ b/src/_node/file/actions.ts @@ -1,12 +1,47 @@ import { LogAllow } from "@_constants/global"; -import { TFileApiPayload } from "../"; +import { + TFileApiPayload, + TFileHandlerCollection, + TFileNodeTreeData, + TNodeUid, +} from "../"; +import { TProjectContext } from "@_redux/main/fileTree"; +import { FileSystemApis } from "./FileSystemApis"; -const add = () => {}; -const remove = () => {}; +const create = () => {}; +const remove = async ({ + projectContext, + uids, + fileTree, + fileHandlers, +}: { + projectContext: TProjectContext; + uids: TNodeUid[]; + fileTree: TFileNodeTreeData; + fileHandlers?: TFileHandlerCollection; +}): Promise => { + return new Promise((resolve, reject) => { + try { + let allDone = true; + uids.map((uid) => { + const done = FileSystemApis[projectContext].removeSingleDirectoryOrFile( + { + uid, + fileTree, + fileHandlers: fileHandlers || {}, + }, + ); + if (!done) allDone = false; + }); + resolve(allDone); + } catch (err) { + reject(err); + } + }); +}; const cut = () => {}; const copy = () => {}; -const paste = () => {}; const duplicate = () => {}; const move = () => {}; const rename = () => {}; @@ -15,45 +50,52 @@ export const doFileActions = async ( params: TFileApiPayload, fb?: (...params: any[]) => void, cb?: (...params: any[]) => void, -): Promise => { - return new Promise((resolve, reject) => { - try { - const { action, osType = "Windows" } = params; - - switch (action) { - case "add": - add(); - break; - case "remove": - remove(); - break; - case "cut": - cut(); - break; - case "copy": - copy(); - break; - case "paste": - paste(); - break; - case "duplicate": - duplicate(); - break; - case "move": - move(); - break; - case "rename": - rename(); - break; - default: - break; - } +) => { + try { + const { + projectContext, + action, + uids, + fileTree, + fileHandlers, + osType = "Windows", + } = params; - resolve(); - } catch (err) { - LogAllow && console.error(err); - fb && fb(); - reject(); + let allDone = true; + switch (action) { + case "create": + create(); + break; + case "remove": + allDone = await remove({ + projectContext, + uids, + fileTree, + fileHandlers, + }); + break; + case "cut": + cut(); + break; + case "copy": + copy(); + break; + case "duplicate": + duplicate(); + break; + case "move": + move(); + break; + case "rename": + rename(); + break; + default: + break; } - }); + + cb && cb(allDone); + } catch (err) { + LogAllow && console.error(err); + fb && fb(); + } }; diff --git a/src/_node/file/apis.ts b/src/_node/file/apis.ts index b0361f9f..09575eda 100644 --- a/src/_node/file/apis.ts +++ b/src/_node/file/apis.ts @@ -25,12 +25,14 @@ import { TFileNodeData, TFileNodeTreeData, TFileParserResponse, + TNodeTreeData, TNodeUid, TProjectLoaderResponse, } from "../"; import { fileHandlers } from "./handlers/handlers"; import { getInitialFileUidToOpen, sortFilesByASC } from "./helpers"; import { TFileHandlerInfo, TFileHandlerInfoObj, TZipFileInfo } from "./types"; +import { FileSystemFileHandle } from "file-system-access"; export const _fs = window.Filer.fs; export const _path = window.Filer.path; @@ -38,10 +40,12 @@ export const _sh = new _fs.Shell(); export const initIDBProject = async (projectPath: string): Promise => { return new Promise(async (resolve, reject) => { - // remove original project + // remove original try { - await removeFileSystem(projectPath); - } catch (err) {} + await _removeIDBDirectoryOrFile(projectPath); + } catch (err) { + LogAllow && console.error("error while remove IDB project", err); + } // create a new project try { @@ -55,9 +59,6 @@ export const initIDBProject = async (projectPath: string): Promise => { export const createIDBProject = async (projectPath: string): Promise => { return new Promise(async (resolve, reject) => { try { - // create root directory - await createDirectory(projectPath); - const htmlElementsReferenceData: THtmlElementsReferenceData = {}; htmlRefElements.map((htmlRefElement: THtmlElementsReference) => { const pureTag = @@ -67,27 +68,25 @@ export const createIDBProject = async (projectPath: string): Promise => { htmlElementsReferenceData[pureTag] = htmlRefElement; }); + // create project directory + await _createIDBDirectory(projectPath); + // create index.html const indexHtmlPath = `${projectPath}/index.html`; - let doctype = "\n"; - let html = htmlElementsReferenceData["html"].Content + const doctype = "\n"; + const html = htmlElementsReferenceData["html"].Content ? `\n` + htmlElementsReferenceData["html"].Content + `\n` - : ""; - - // test - html = `Untitled

Heading 1

`; - + : `Untitled

Heading 1

`; const indexHtmlContent = doctype + html; - - await writeFile(indexHtmlPath, indexHtmlContent); + await _writeIDBFile(indexHtmlPath, indexHtmlContent); resolve(); } catch (err) { + LogAllow && console.error("error while create IDB project", err); reject(err); } }); }; - export const loadIDBProject = async ( projectPath: string, isReload: boolean = false, @@ -95,7 +94,7 @@ export const loadIDBProject = async ( ): Promise => { return new Promise(async (resolve, reject) => { try { - let deletedUidsObj: { [uid: TNodeUid]: true } = {}; + const deletedUidsObj: { [uid: TNodeUid]: true } = {}; if (isReload) { getSubNodeUidsByBfs( RootNodeUid, @@ -124,7 +123,7 @@ export const loadIDBProject = async ( const { uid: p_uid, path: p_path } = dirHandlers.shift() as TFileHandlerInfo; - const entries = await readDir(p_path); + const entries = await _readIDBDirectory(p_path); await Promise.all( entries.map(async (entry) => { // skip stage preview files & hidden files @@ -135,7 +134,7 @@ export const loadIDBProject = async ( const c_uid = _path.join(p_uid, entry) as string; const c_path = _path.join(p_path, entry) as string; - const stats = await getStat(c_path); + const stats = await _getIDBDirectoryOrFileStat(c_path); const c_kind = stats.type === "DIRECTORY" ? "directory" : "file"; const nameArr = entry.split("."); @@ -143,7 +142,7 @@ export const loadIDBProject = async ( const c_name = nameArr.join("."); const c_content = - c_kind === "directory" ? undefined : await readFile(c_path); + c_kind === "directory" ? undefined : await _readIDBFile(c_path); const c_handlerInfo: TFileHandlerInfo = { uid: c_uid, @@ -215,12 +214,11 @@ export const loadIDBProject = async ( resolve({ _fileTree, _initialFileUidToOpen, - deletedUidsObj, deletedUids: Object.keys(deletedUidsObj), }); } catch (err) { - LogAllow && console.log("ERROR from loadIDBProject API", err); + LogAllow && console.error("error in loadIDBProject API", err); reject(err); } }); @@ -237,7 +235,7 @@ export const loadLocalProject = async ( if (!(await verifyFileHandlerPermission(projectHandle))) throw "project handler permission error"; - let deletedUidsObj: { [uid: TNodeUid]: true } = {}; + const deletedUidsObj: { [uid: TNodeUid]: true } = {}; if (isReload) { getSubNodeUidsByBfs( RootNodeUid, @@ -370,15 +368,13 @@ export const loadLocalProject = async ( resolve({ handlerArr, _fileHandlers, - _fileTree, _initialFileUidToOpen, - deletedUidsObj, deletedUids: Object.keys(deletedUidsObj), }); } catch (err) { - LogAllow && console.log("ERROR from loadLocalProject API", err); + LogAllow && console.log("error in loadLocalProject API", err); reject(err); } }); @@ -387,22 +383,21 @@ export const buildNohostIDB = async ( handlerArr: TFileHandlerInfo[], ): Promise => { return new Promise(async (resolve, reject) => { - // build nohost idb try { await Promise.all( handlerArr.map(async (_handler) => { const { kind, path, content } = _handler; if (kind === "directory") { try { - await createDirectory(path); + await _createIDBDirectory(path); } catch (err) { - console.log(err); + console.error("error in buildNohostIDB API", err); } } else { try { - await writeFile(path, content as Uint8Array); + await _writeIDBFile(path, content as Uint8Array); } catch (err) { - console.log(err); + console.error("error in buildNohostIDB API", err); } } }), @@ -410,13 +405,14 @@ export const buildNohostIDB = async ( resolve(); } catch (err) { - LogAllow && console.log("ERROR from buildNohostIDB API", err); + LogAllow && console.error("error in buildNohostIDB API", err); reject(err); } }); }; - -export const downloadProject = async (projectPath: string): Promise => { +export const downloadIDBProject = async ( + projectPath: string, +): Promise => { return new Promise(async (resolve, reject) => { try { const zip = new JSZip(); @@ -434,7 +430,7 @@ export const downloadProject = async (projectPath: string): Promise => { while (dirHandlers.length) { const { path, zip } = dirHandlers.shift() as TZipFileInfo; - const entries = await readDir(path); + const entries = await _readIDBDirectory(path); await Promise.all( entries.map(async (entry) => { // skip stage preview files @@ -442,7 +438,7 @@ export const downloadProject = async (projectPath: string): Promise => { // build handler const c_path = _path.join(path, entry) as string; - const stats = await getStat(c_path); + const stats = await _getIDBDirectoryOrFileStat(c_path); const c_name = entry; const c_kind = stats.type === "DIRECTORY" ? "directory" : "file"; @@ -450,7 +446,7 @@ export const downloadProject = async (projectPath: string): Promise => { if (c_kind === "directory") { c_zip = zip?.folder(c_name); } else { - const content = await readFile(c_path); + const content = await _readIDBFile(c_path); c_zip = zip?.file(c_name, content); } @@ -468,26 +464,27 @@ export const downloadProject = async (projectPath: string): Promise => { resolve(); } catch (err) { + console.error("error in downloadIDBProject API"); reject(err); } }); }; -export const createDirectory = async (path: string): Promise => { +export const _createIDBDirectory = async (path: string): Promise => { return new Promise((resolve, reject) => { _fs.mkdir(path, (err: any) => { err ? reject(err) : resolve(); }); }); }; -export const readFile = async (path: string): Promise => { +export const _readIDBFile = async (path: string): Promise => { return new Promise((resolve, reject) => { _fs.readFile(path, (err: any, data: Buffer) => { err ? reject(err) : resolve(data); }); }); }; -export const writeFile = async ( +export const _writeIDBFile = async ( path: string, content: Uint8Array | string, ): Promise => { @@ -497,21 +494,25 @@ export const writeFile = async ( }); }); }; -export const removeFileSystem = async (path: string): Promise => { +export const _removeIDBDirectoryOrFile = async ( + path: string, +): Promise => { return new Promise((resolve, reject) => { _sh.rm(path, { recursive: true }, (err: any) => { err ? reject(err) : resolve(); }); }); }; -export const readDir = async (path: string): Promise => { +export const _readIDBDirectory = async (path: string): Promise => { return new Promise((resolve, reject) => { _fs.readdir(path, (err: any, files: string[]) => { err ? reject(err) : resolve(files); }); }); }; -export const getStat = async (path: string): Promise => { +export const _getIDBDirectoryOrFileStat = async ( + path: string, +): Promise => { return new Promise((resolve, reject) => { _fs.stat(path, (err: any, stats: any) => { err ? reject(err) : resolve(stats); @@ -529,7 +530,6 @@ export const getNormalizedPath = ( const normalizedPath = _path.normalize(path); return { isAbsolutePath, normalizedPath }; }; - export const parseFile = ( ext: string, content: string, diff --git a/src/_node/file/helpers.ts b/src/_node/file/helpers.ts index efac2e20..1ae4a285 100644 --- a/src/_node/file/helpers.ts +++ b/src/_node/file/helpers.ts @@ -1,4 +1,4 @@ -import { RootNodeUid } from "@_constants/main"; +import { FileChangeAlertMessage, RootNodeUid } from "@_constants/main"; import { TFileHandlerInfoObj, @@ -48,23 +48,33 @@ export const getInitialFileUidToOpen = (handlerObj: TFileHandlerInfoObj) => { }; export const isUnsavedProject = (fileTree: TFileNodeTreeData) => { - for (let uid in fileTree) { - const _file = fileTree[uid]; - const _fileData = _file.data as TFileNodeData; - if (_fileData && _fileData.changed) { + for (const uid in fileTree) { + const file = fileTree[uid]; + const fileData = file.data as TFileNodeData; + if (fileData && fileData.changed) { return true; } } return false; }; -export const triggerFileChangeAlert = () => { - const message = `Your changes will be lost if you don't save them. Are you sure you want to continue without saving?`; - if (!window.confirm(message)) { - return; +export const confirmAlert = (msg: string): boolean => { + if (!window.confirm(msg)) { + return false; } + return true; +}; +export const confirmFileChanges = (fileTree: TFileNodeTreeData): boolean => { + return isUnsavedProject(fileTree) + ? confirmAlert(FileChangeAlertMessage) + : true; }; -export const confirmFileChanges = (fileTree: TFileNodeTreeData) => { - isUnsavedProject(fileTree) && triggerFileChangeAlert(); +export const getFileNameAndExtensionFromFullname = ( + name: string, +): { baseName: string; ext: string } => { + const nameArr = name.split("."); + const ext = nameArr.length > 1 ? (nameArr.pop() as string) : ""; + const baseName = nameArr.join("."); + return { baseName, ext }; }; diff --git a/src/_node/file/types.ts b/src/_node/file/types.ts index 60d1589e..0b09e071 100644 --- a/src/_node/file/types.ts +++ b/src/_node/file/types.ts @@ -9,6 +9,7 @@ import { TNodeTreeData, TNodeUid, } from "../"; +import { TFileActionType, TProjectContext } from "@_redux/main/fileTree"; export type TFileNode = TNode & { data: TFileNodeData; @@ -69,17 +70,26 @@ export type TProjectLoaderResponse = { deletedUidsObj: { [uid: TNodeUid]: true }; }; -export type TFileApiPayload = { - action: TNodeActionType; +export type TFileApiPayloadBase = { + projectContext: TProjectContext; + action: TFileActionType; + fileTree: TFileNodeTreeData; + fileHandlers?: TFileHandlerCollection; osType?: TOsType; }; -// -------------------- -export type TFile = { - uid: TNodeUid; - content: string; -}; +export type TFileApiPayload = TFileApiPayloadBase & + ( + | { action: Extract; uids: TNodeUid[] } + | { action: Exclude; uids?: never } + ); export type TZipFileInfo = { path: string; zip: JSZip | null | undefined; }; + +// -------------------- +export type TFile = { + uid: TNodeUid; + content: string; +}; diff --git a/src/_node/helpers.ts b/src/_node/helpers.ts index 001c21ff..8d4298b8 100644 --- a/src/_node/helpers.ts +++ b/src/_node/helpers.ts @@ -1,5 +1,7 @@ import { + AddFileActionPrefix, AddNodeActionPrefix, + RenameFileActionPrefix, RenameNodeActionPrefix, RootNodeUid, } from "@_constants/main"; @@ -167,6 +169,12 @@ export const isAddNodeAction = (actionName: string): boolean => { export const isRenameNodeAction = (actionName: string): boolean => { return actionName.startsWith(RenameNodeActionPrefix) ? true : false; }; +export const isAddFileAction = (actionName: string): boolean => { + return actionName.startsWith(AddFileActionPrefix) ? true : false; +}; +export const isRenameFileAction = (actionName: string): boolean => { + return actionName.startsWith(RenameFileActionPrefix) ? true : false; +}; // ------------------------------- export const getPrevSiblingNodeUid = ( diff --git a/src/_redux/main/cmdk/constants.ts b/src/_redux/main/cmdk/constants.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/src/_redux/main/cmdk/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/_redux/main/cmdk/index.ts b/src/_redux/main/cmdk/index.ts index d9281948..5a9b1dba 100644 --- a/src/_redux/main/cmdk/index.ts +++ b/src/_redux/main/cmdk/index.ts @@ -1,3 +1,2 @@ export * from "./types"; -export * from "./constants"; export * from "./slice"; diff --git a/src/_redux/main/codeView/constants.ts b/src/_redux/main/codeView/constants.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/src/_redux/main/codeView/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/_redux/main/codeView/index.ts b/src/_redux/main/codeView/index.ts index d9281948..5a9b1dba 100644 --- a/src/_redux/main/codeView/index.ts +++ b/src/_redux/main/codeView/index.ts @@ -1,3 +1,2 @@ export * from "./types"; -export * from "./constants"; export * from "./slice"; diff --git a/src/_redux/main/context.ts b/src/_redux/main/context.ts index 1596752c..66b0e21e 100644 --- a/src/_redux/main/context.ts +++ b/src/_redux/main/context.ts @@ -12,33 +12,23 @@ export const MainContext: Context = createContext({ }, cmdkReferenceData: {}, + projectHandlers: {}, + setProjectHandlers: () => {}, currentProjectFileHandle: null, setCurrentProjectFileHandle: () => {}, - fileHandlers: {}, setFileHandlers: () => {}, monacoEditorRef: { current: null }, setMonacoEditorRef: () => {}, - iframeRefRef: { current: null }, setIframeRefRef: () => {}, - - // code view isContentProgrammaticallyChanged: { current: false, }, setIsContentProgrammaticallyChanged: () => {}, - setCodeViewOffsetTop: () => {}, - - // import project importProject: () => {}, - - // close all panel - closeAllPanel: () => {}, - - //undo/redo onUndo: () => {}, onRedo: () => {}, }); diff --git a/src/_redux/main/fileTree/constants.ts b/src/_redux/main/fileTree/constants.ts deleted file mode 100644 index cb0ff5c3..00000000 --- a/src/_redux/main/fileTree/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/src/_redux/main/fileTree/event/types.ts b/src/_redux/main/fileTree/event/types.ts index 4d3bff53..f4566373 100644 --- a/src/_redux/main/fileTree/event/types.ts +++ b/src/_redux/main/fileTree/event/types.ts @@ -9,7 +9,7 @@ export type TFileAction = { }; export type TFileActionType = | "create" - | "delete" + | "remove" | "move" | "rename" | "duplicate" diff --git a/src/_redux/main/fileTree/index.ts b/src/_redux/main/fileTree/index.ts index f201a875..8db21402 100644 --- a/src/_redux/main/fileTree/index.ts +++ b/src/_redux/main/fileTree/index.ts @@ -1,4 +1,3 @@ export * from "./types"; -export * from "./constants"; export * from "./slice"; export * from "./event"; diff --git a/src/_redux/main/fileTree/slice.ts b/src/_redux/main/fileTree/slice.ts index efc7da8e..41101a1b 100644 --- a/src/_redux/main/fileTree/slice.ts +++ b/src/_redux/main/fileTree/slice.ts @@ -8,7 +8,7 @@ import { TFileTreeReducerState, TProject, TWorkspace } from "./types"; const fileTreeReducerInitialState: TFileTreeReducerState = { workspace: { name: "local", projects: [] }, - project: { context: "idb", name: "welcome", handler: null, favicon: null }, + project: { context: "idb", name: "welcome", favicon: null }, fileTree: {}, initialFileUidToOpen: "", prevFileUid: "", @@ -36,7 +36,7 @@ const fileTreeSlice = createSlice({ const workspace = action.payload; state.workspace = workspace; }, - setProject(state, action: PayloadAction) { + setProject(state, action: PayloadAction>) { const project = action.payload; state.project = project; }, diff --git a/src/_redux/main/fileTree/types.ts b/src/_redux/main/fileTree/types.ts index 9906d1bf..f388575d 100644 --- a/src/_redux/main/fileTree/types.ts +++ b/src/_redux/main/fileTree/types.ts @@ -6,7 +6,7 @@ import { TFileAction } from "./event"; export type TFileTreeReducerState = { workspace: TWorkspace; - project: TProject; + project: Omit; fileTree: TFileNodeTreeData; initialFileUidToOpen: TNodeUid; prevFileUid: TNodeUid; @@ -22,7 +22,7 @@ export type TFileTreeReducerState = { export type TWorkspace = { name: string; - projects: TProject[]; + projects: Omit[]; }; export type TProject = { diff --git a/src/_redux/main/processor/slice.ts b/src/_redux/main/processor/slice.ts index 7002cd94..a086719a 100644 --- a/src/_redux/main/processor/slice.ts +++ b/src/_redux/main/processor/slice.ts @@ -8,6 +8,8 @@ import { } from "./types"; const processorReducerInitialState: TProcessorReducerState = { + doingAction: false, + navigatorDropdownType: null, favicon: "", @@ -17,6 +19,8 @@ const processorReducerInitialState: TProcessorReducerState = { showActionsPanel: true, showCodeView: true, + autoSave: false, + didUndo: false, didRedo: false, }; @@ -24,15 +28,19 @@ const processorSlice = createSlice({ name: "processor", initialState: processorReducerInitialState, reducers: { + setDoingAction(state, action: PayloadAction) { + const doingAction = action.payload; + state.doingAction = doingAction; + }, setNavigatorDropdownType( state, - actions: PayloadAction, + action: PayloadAction, ) { - const navigatorDropdownType = actions.payload; + const navigatorDropdownType = action.payload; state.navigatorDropdownType = navigatorDropdownType; }, - setFavicon(state, actions: PayloadAction) { - const favicon = actions.payload; + setFavicon(state, action: PayloadAction) { + const favicon = action.payload; state.favicon = favicon; }, @@ -54,6 +62,11 @@ const processorSlice = createSlice({ state.showCodeView = showCodeView; }, + setAutoSave(state, action: PayloadAction) { + const autoSave = action.payload; + state.autoSave = autoSave; + }, + setDidUndo(state, action: PayloadAction) { const didUndo = action.payload; state.didUndo = didUndo; @@ -65,6 +78,8 @@ const processorSlice = createSlice({ }, }); export const { + setDoingAction, + setNavigatorDropdownType, setFavicon, @@ -74,6 +89,8 @@ export const { setShowActionsPanel, setShowCodeView, + setAutoSave, + setDidUndo, setDidRedo, } = processorSlice.actions; diff --git a/src/_redux/main/processor/types.ts b/src/_redux/main/processor/types.ts index a4e6b7d7..241ea2a7 100644 --- a/src/_redux/main/processor/types.ts +++ b/src/_redux/main/processor/types.ts @@ -1,6 +1,8 @@ import { TNode, TNodeTreeData, TNodeUid } from "@_node/types"; export type TProcessorReducerState = { + doingAction: boolean; + navigatorDropdownType: TNavigatorDropdownType; favicon: string; @@ -10,6 +12,8 @@ export type TProcessorReducerState = { showActionsPanel: boolean; showCodeView: boolean; + autoSave: boolean; + didUndo: boolean; didRedo: boolean; }; diff --git a/src/_redux/main/types.ts b/src/_redux/main/types.ts index 44ee9047..cc64646a 100644 --- a/src/_redux/main/types.ts +++ b/src/_redux/main/types.ts @@ -41,11 +41,12 @@ export type TMainContext = { htmlReferenceData: THtmlReferenceData; cmdkReferenceData: TCmdkReferenceData; + projectHandlers: TFileHandlerCollection; + setProjectHandlers: (projectHandlerObj: TFileHandlerCollection) => void; currentProjectFileHandle: FileSystemDirectoryHandle | null; setCurrentProjectFileHandle: ( fileHandler: FileSystemDirectoryHandle | null, ) => void; - fileHandlers: TFileHandlerCollection; setFileHandlers: (fileHandlerObj: TFileHandlerCollection) => void; @@ -53,24 +54,15 @@ export type TMainContext = { setMonacoEditorRef: ( editorInstance: editor.IStandaloneCodeEditor | null, ) => void; - iframeRefRef: MutableRefObject; setIframeRefRef: (iframeRef: HTMLIFrameElement | null) => void; - - // code view isContentProgrammaticallyChanged: React.RefObject; setIsContentProgrammaticallyChanged: (value: boolean) => void; - setCodeViewOffsetTop: (offsetTop: string) => void; - - // import project importProject: ( fsType: TProjectContext, projectHandle?: FileSystemDirectoryHandle | null, ) => void; - closeAllPanel: () => void; - - //undo/redo onUndo: () => void; onRedo: () => void; }; diff --git a/src/_redux/useAppState.tsx b/src/_redux/useAppState.tsx index 70590ae3..7457b527 100644 --- a/src/_redux/useAppState.tsx +++ b/src/_redux/useAppState.tsx @@ -57,12 +57,14 @@ export const useAppState = () => { }, codeView: { codeViewTabSize }, processor: { + doingAction, navigatorDropdownType, favicon, activePanel, clipboardData, showActionsPanel, showCodeView, + autoSave, didUndo, didRedo, }, @@ -138,6 +140,8 @@ export const useAppState = () => { codeViewTabSize, + doingAction, + navigatorDropdownType, favicon, @@ -147,6 +151,8 @@ export const useAppState = () => { showActionsPanel, showCodeView, + autoSave, + didUndo, didRedo, diff --git a/src/components/main/actionsPanel/navigatorPanel/components/AdditionalPanel.tsx b/src/components/main/actionsPanel/navigatorPanel/components/AdditionalPanel.tsx index 68cab886..a88023e7 100644 --- a/src/components/main/actionsPanel/navigatorPanel/components/AdditionalPanel.tsx +++ b/src/components/main/actionsPanel/navigatorPanel/components/AdditionalPanel.tsx @@ -1,4 +1,4 @@ -import React, { FC, useRef } from "react"; +import React, { FC, useContext, useRef } from "react"; import cx from "classnames"; @@ -8,6 +8,7 @@ import { useAppState } from "@_redux/useAppState"; import { isSelected } from "../helpers"; import { useNavigatorPanelHandlers } from "../hooks"; +import { MainContext } from "@_redux/main"; interface AdditionalPanelProps { navigatorPanel: HTMLDivElement | null; @@ -16,13 +17,8 @@ interface AdditionalPanelProps { export const AdditionalPanel: FC = ({ navigatorPanel, }) => { - const { - workspace, - project, - fileTree, - currentFileUid, - navigatorDropdownType, - } = useAppState(); + const { workspace, project, navigatorDropdownType } = useAppState(); + const { projectHandlers } = useContext(MainContext); const navigatorDropDownRef = useRef(null); @@ -30,10 +26,13 @@ export const AdditionalPanel: FC = ({ const onClick = ( e: React.MouseEvent, - project: TProject, + project: Omit, ) => { e.stopPropagation(); - onOpenProject(project); + onOpenProject({ + ...project, + handler: projectHandlers[project.name] as FileSystemDirectoryHandle, + }); }; return ( diff --git a/src/components/main/actionsPanel/navigatorPanel/helpers/helpers.ts b/src/components/main/actionsPanel/navigatorPanel/helpers/helpers.ts index 60beba03..6c0e8ad3 100644 --- a/src/components/main/actionsPanel/navigatorPanel/helpers/helpers.ts +++ b/src/components/main/actionsPanel/navigatorPanel/helpers/helpers.ts @@ -8,10 +8,11 @@ export const isHomeIcon = (node: TNode) => node.data.name == "index" && node.parentUid === "ROOT"; -export const isSelected = (_project: TProject, project: TProject) => { - return _project.context === project.context && - _project.name === project.name && - _project.handler === project.handler +export const isSelected = ( + _project: Omit, + project: Omit, +) => { + return _project.context === project.context && _project.name === project.name ? "selected" : ""; }; @@ -27,7 +28,7 @@ export const getFileExtension = (node: TNode) => export const setWorkspaceFavicon = ( validNodeTree: TNodeTreeData, - project: TProject, + project: Omit, workspace: TWorkspace, setWorkspace: (ws: TWorkspace) => void, ) => { diff --git a/src/components/main/actionsPanel/nodeTreeView/hooks/useNodeViewState.ts b/src/components/main/actionsPanel/nodeTreeView/hooks/useNodeViewState.ts index a7376f8f..67144a17 100644 --- a/src/components/main/actionsPanel/nodeTreeView/hooks/useNodeViewState.ts +++ b/src/components/main/actionsPanel/nodeTreeView/hooks/useNodeViewState.ts @@ -40,7 +40,7 @@ export function useNodeViewState() { if (_uids.length === selectedItems.length) { let same = true; for (const _uid of _uids) { - if (selectedItemsObj[_uid] === undefined) { + if (!selectedItemsObj[_uid]) { same = false; break; } diff --git a/src/components/main/actionsPanel/workspaceTreeView/WorkspaceTreeView.tsx b/src/components/main/actionsPanel/workspaceTreeView/WorkspaceTreeView.tsx index dcdcce01..3a922d47 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/WorkspaceTreeView.tsx +++ b/src/components/main/actionsPanel/workspaceTreeView/WorkspaceTreeView.tsx @@ -12,17 +12,14 @@ import { DraggingPositionItem } from "react-complex-tree"; import { useDispatch } from "react-redux"; import { SVGIconI, TreeView } from "@_components/common"; -import { TreeViewData } from "@_components/common/treeView/types"; -import { AddFileActionPrefix, RootNodeUid, ShortDelay } from "@_constants/main"; import { _path, getNormalizedPath, TFileNodeData } from "@_node/file"; import { TNode, TNodeUid } from "@_node/types"; -import { scrollToElement } from "@_pages/main/helper"; import { MainContext } from "@_redux/main"; import { setHoveredFileUid } from "@_redux/main/fileTree"; import { FileTree_Event_ClearActionType } from "@_redux/main/fileTree/event"; import { setActivePanel, setDidRedo, setDidUndo } from "@_redux/main/processor"; import { useAppState } from "@_redux/useAppState"; -import { addClass, generateQuerySelector, removeClass } from "@_services/main"; +import { generateQuerySelector } from "@_services/main"; import { TFilesReference } from "@_types/main"; import { @@ -31,11 +28,12 @@ import { useInvalidNodes, useNodeActionsHandler, useNodeViewState, + useSync, useTemporaryNodes, } from "./hooks"; import { Container, ItemArrow } from "./workSpaceTreeComponents"; -const AutoExpandDelay = 1 * 1000; +const AutoExpandDelayOnDnD = 1 * 1000; export default function WorkspaceTreeView() { const dispatch = useDispatch(); const { @@ -55,69 +53,65 @@ export default function WorkspaceTreeView() { fSelectedItemsObj: selectedItemsObj, hoveredFileUid, - doingFileAction, lastFileAction, fileAction, - fileEventPast, - fileEventPastLength, - fileEventFuture, - fileEventFutureLength, - - nodeTree, - validNodeTree, - - nFocusedItem, - nExpandedItems, - nExpandedItemsObj, - nSelectedItems, - nSelectedItemsObj, - hoveredNodeUid, - - currentFileContent, - selectedNodeUids, - - nodeEventPast, - nodeEventPastLength, - - nodeEventFuture, - nodeEventFutureLength, - - iframeSrc, - iframeLoading, - needToReloadIframe, linkToOpen, - codeViewTabSize, - navigatorDropdownType, - favicon, activePanel, - clipboardData, - - showActionsPanel, - showCodeView, didUndo, didRedo, - - cmdkOpen, - cmdkPages, - currentCmdkPage, - - cmdkSearchContent, currentCommand, } = useAppState(); const { addRunningActions, removeRunningActions, filesReferenceData } = useContext(MainContext); // invalid - can't do any actions on the nodes - const { invalidNodes } = useInvalidNodes(); + const { invalidNodes, addInvalidNodes, removeInvalidNodes } = + useInvalidNodes(); // temporary - don't display the nodes - const { setTemporaryNodes, removeTemporaryNodes } = useTemporaryNodes(); - // -------------------------------------------------------------- hms -------------------------------------------------------------- - const { _copy, _create, _cut, _delete, _rename } = useFileOperations(); + const { temporaryNodes, addTemporaryNodes, removeTemporaryNodes } = + useTemporaryNodes(); + const { focusedItemRef, fileTreeViewData } = useSync(); + + const { cb_collapseNode, cb_expandNode, cb_focusNode, cb_selectNode } = + useNodeViewState({ invalidNodes }); + const { _create, _delete, _copy, _cut, _rename } = useFileOperations({ + invalidNodes, + addInvalidNodes, + removeInvalidNodes, + temporaryNodes, + addTemporaryNodes, + removeTemporaryNodes, + }); + const openFileUid = useRef(""); + const { + cb_startRenamingNode, + cb_abortRenamingNode, + cb_renameNode, + cb_moveNode, + cb_readNode, + } = useNodeActionsHandler({ + invalidNodes, + addInvalidNodes, + removeInvalidNodes, + temporaryNodes, + addTemporaryNodes, + removeTemporaryNodes, + openFileUid, + }); + useCmdk({ + invalidNodes, + addInvalidNodes, + removeInvalidNodes, + temporaryNodes, + addTemporaryNodes, + removeTemporaryNodes, + openFileUid, + }); useEffect(() => { if (!didUndo && !didRedo) return; @@ -132,7 +126,7 @@ export default function WorkspaceTreeView() { const { orgName, newName } = param2; const currentUid = `${parentUid}/${newName}`; (async () => { - setTemporaryNodes(currentUid); + addTemporaryNodes(currentUid); await _rename(currentUid, orgName); removeTemporaryNodes(currentUid); })(); @@ -160,7 +154,7 @@ export default function WorkspaceTreeView() { uids.push(`${targetUid}/${name}`); }); _delete(uids); - } else if (type === "delete") { + } else if (type === "remove") { } } else { const { type, param1, param2 } = lastFileAction; @@ -170,7 +164,7 @@ export default function WorkspaceTreeView() { const { uid } = param1; const { newName } = param2; (async () => { - setTemporaryNodes(uid); + addTemporaryNodes(uid); await _rename(uid, newName); removeTemporaryNodes(uid); })(); @@ -191,73 +185,13 @@ export default function WorkspaceTreeView() { names.push(_uid.name); }); _copy(uids, names, targetUids); - } else if (type === "delete") { + } else if (type === "remove") { } } dispatch(setDidUndo(false)); dispatch(setDidRedo(false)); }, [didUndo, didRedo]); - // -------------------------------------------------------------- sync -------------------------------------------------------------- - // outline the hovered item - const hoveredItemRef = useRef(hoveredFileUid); - useEffect(() => { - if (hoveredItemRef.current === hoveredFileUid) return; - - const curHoveredElement = document.querySelector( - `#FileTreeView-${generateQuerySelector(hoveredItemRef.current)}`, - ); - curHoveredElement?.setAttribute( - "class", - removeClass(curHoveredElement.getAttribute("class") || "", "outline"), - ); - const newHoveredElement = document.querySelector( - `#FileTreeView-${generateQuerySelector(hoveredFileUid)}`, - ); - newHoveredElement?.setAttribute( - "class", - addClass(newHoveredElement.getAttribute("class") || "", "outline"), - ); - - hoveredItemRef.current = hoveredFileUid; - }, [hoveredFileUid]); - - // scroll to the focused item - const debouncedScrollToElement = useCallback( - debounce(scrollToElement, ShortDelay), - [], - ); - const focusedItemRef = useRef(focusedItem); - useEffect(() => { - if (focusedItemRef.current === focusedItem) return; - - const focusedElement = document.querySelector( - `#FileTreeView-${generateQuerySelector(focusedItem)}`, - ); - focusedElement && debouncedScrollToElement(focusedElement, "auto"); - - focusedItemRef.current = focusedItem; - }, [focusedItem]); - - // build fileTreeViewData - const fileTreeViewData = useMemo(() => { - const data: TreeViewData = {}; - for (const uid in fileTree) { - const node: TNode = fileTree[uid]; - data[uid] = { - index: uid, - data: node, - children: node.children, - isFolder: !node.isEntity, - canMove: uid !== RootNodeUid, - canRename: uid !== RootNodeUid, - }; - } - return data; - }, [fileTree]); - - const { cb_collapseNode, cb_expandNode, cb_focusNode, cb_selectNode } = - useNodeViewState(); // open default initial html file useEffect(() => { @@ -274,15 +208,6 @@ export default function WorkspaceTreeView() { } }, [initialFileUidToOpen]); - const openFileUid = useRef(""); - const { - cb_startRenamingNode, - cb_abortRenamingNode, - cb_renameNode, - cb_moveNode, - cb_readNode, - } = useNodeActionsHandler(openFileUid); - useEffect(() => { if ( fileTree[openFileUid.current] && @@ -325,47 +250,8 @@ export default function WorkspaceTreeView() { openFile(fileUidToOpen); } }, [linkToOpen]); - // -------------------------------------------------------------- cmdk -------------------------------------------------------------- - const { onDelete, onCut, onCopy, onPaste, onDuplicate, onAddNode } = - useCmdk(openFileUid); - - useEffect(() => { - if (!currentCommand) return; - - if (isAddFileAction(currentCommand.action)) { - onAddNode(currentCommand.action); - return; - } - - if (activePanel !== "file") return; - - switch (currentCommand.action) { - case "Cut": - onCut(); - break; - case "Copy": - onCopy(); - break; - case "Paste": - onPaste(); - break; - case "Delete": - onDelete(); - break; - case "Duplicate": - onDuplicate(); - break; - default: - break; - } - }, [currentCommand]); - - const isAddFileAction = (actionName: string): boolean => { - return actionName.startsWith(AddFileActionPrefix) ? true : false; - }; - // -------------------------------------------------------------- own -------------------------------------------------------------- - const onPanelClick = useCallback((e: React.MouseEvent) => { + const onPanelClick = useCallback(() => { dispatch(setActivePanel("file")); }, []); @@ -482,7 +368,7 @@ export default function WorkspaceTreeView() { }; const debouncedExpand = useCallback( - debounce(cb_expandNode, AutoExpandDelay), + debounce(cb_expandNode, AutoExpandDelayOnDnD), [cb_expandNode], ); const onDragEnter = (e: React.DragEvent) => { diff --git a/src/components/main/actionsPanel/workspaceTreeView/helpers/createFileOrFolder.ts b/src/components/main/actionsPanel/workspaceTreeView/helpers/createFileOrFolder.ts index 93dfaf48..b17e47dc 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/helpers/createFileOrFolder.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/helpers/createFileOrFolder.ts @@ -1,8 +1,8 @@ import { - createDirectory, + _createIDBDirectory, TFileHandlerCollection, TFileNodeData, - writeFile, + _writeIDBFile, } from "@_node/file"; import { TNodeTreeData, TNodeUid } from "@_node/types"; import { TProject } from "@_redux/main/fileTree"; @@ -13,7 +13,7 @@ export const createFileOrFolder = async ( parentUid: TNodeUid, name: string, type: TFileNodeType, - project: TProject, + project: Omit, ffTree: TNodeTreeData, fileHandlers: TFileHandlerCollection, ) => { @@ -37,9 +37,9 @@ export const createFileOrFolder = async ( } } else if (project.context === "idb") { if (type === "*folder") { - await createDirectory(`${parentNodeData.path}/${name}`); + await _createIDBDirectory(`${parentNodeData.path}/${name}`); } else { - await writeFile(`${parentNodeData.path}/${name}`, ""); + await _writeIDBFile(`${parentNodeData.path}/${name}`, ""); } } } catch (err) { diff --git a/src/components/main/actionsPanel/workspaceTreeView/helpers/deleteFileOrFolder.ts b/src/components/main/actionsPanel/workspaceTreeView/helpers/deleteFileOrFolder.ts index 4c10744a..364d2562 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/helpers/deleteFileOrFolder.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/helpers/deleteFileOrFolder.ts @@ -1,5 +1,5 @@ import { - removeFileSystem, + _removeIDBDirectoryOrFile, TFileHandlerCollection, TFileNodeData, } from "@_node/file"; @@ -46,7 +46,7 @@ export const deleteFileOrFolder = async ( nodeData.kind === "directory" ? nodeData.name : `${nodeData.name}${nodeData.ext}`; - await removeFileSystem(`${parentNodeData.path}/${entryName}`); + await _removeIDBDirectoryOrFile(`${parentNodeData.path}/${entryName}`); } catch (err) { console.error(err); } diff --git a/src/components/main/actionsPanel/workspaceTreeView/helpers/duplicateNode.ts b/src/components/main/actionsPanel/workspaceTreeView/helpers/duplicateNode.ts index c63090b0..92cb98db 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/helpers/duplicateNode.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/helpers/duplicateNode.ts @@ -13,7 +13,7 @@ export const duplicateNode = async ( ffTree: TNodeTreeData, fileHandlers: TFileHandlerCollection, addMessage: (message: TToast) => void, - setInvalidNodes: any, + addInvalidNodes: any, invalidNodes: { [uid: string]: boolean }, ) => { const { moveLocalFF } = moveActions(addMessage); @@ -42,7 +42,7 @@ export const duplicateNode = async ( ); const newUid = `${node.parentUid}/${newName}`; - setInvalidNodes({ ...invalidNodes, [uid]: true, [newUid]: true }); + addInvalidNodes({ ...invalidNodes, [uid]: true, [newUid]: true }); try { await moveLocalFF( @@ -58,7 +58,7 @@ export const duplicateNode = async ( delete invalidNodes[uid]; delete invalidNodes[newUid]; - setInvalidNodes({ ...invalidNodes }); + addInvalidNodes({ ...invalidNodes }); return { uid, name: newName }; }; diff --git a/src/components/main/actionsPanel/workspaceTreeView/helpers/index.ts b/src/components/main/actionsPanel/workspaceTreeView/helpers/index.ts index db3925d8..c88c8b73 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/helpers/index.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/helpers/index.ts @@ -6,7 +6,6 @@ export * from "./createFileOrFolder"; export * from "./deleteFileOrFolder"; export * from "./generateNewName"; export * from "./renameNode"; -export * from "./validateAndDeleteNode"; export * from "./validateAndMoveNode"; export * from "./generateNewNameMoveNode"; export * from "./duplicateNode"; diff --git a/src/components/main/actionsPanel/workspaceTreeView/helpers/moveActions.ts b/src/components/main/actionsPanel/workspaceTreeView/helpers/moveActions.ts index 5f0ec33a..1224f7c2 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/helpers/moveActions.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/helpers/moveActions.ts @@ -1,9 +1,9 @@ import { - getStat, - readFile, - removeFileSystem, + _getIDBDirectoryOrFileStat, + _readIDBFile, + _removeIDBDirectoryOrFile, TFileNodeData, - writeFile, + _writeIDBFile, } from "@_node/file"; import { TToast } from "@_types/global"; @@ -73,7 +73,7 @@ export const moveActions = (addMessage: (message: TToast) => void) => { // validate if the new name exists let exists = true; try { - await getStat(`${targetNodeData.path}/${newName}`); + await _getIDBDirectoryOrFileStat(`${targetNodeData.path}/${newName}`); exists = true; } catch (err) { exists = false; @@ -106,7 +106,7 @@ export const moveActions = (addMessage: (message: TToast) => void) => { // validate if the new name exists let exists = true; try { - await getStat(`${targetNodeData.path}/${newName}`); + await _getIDBDirectoryOrFileStat(`${targetNodeData.path}/${newName}`); exists = true; } catch (err) { exists = false; @@ -122,13 +122,13 @@ export const moveActions = (addMessage: (message: TToast) => void) => { // create a new file with the new name and write the content try { - await writeFile( + await _writeIDBFile( `${targetNodeData.path}/${newName}`, - await readFile(nodeData.path), + await _readIDBFile(nodeData.path), ); // handle copy(optional) - !copy && (await removeFileSystem(nodeData.path)); + !copy && (await _removeIDBDirectoryOrFile(nodeData.path)); } catch (err) { throw "error"; } diff --git a/src/components/main/actionsPanel/workspaceTreeView/helpers/moveDirectory.ts b/src/components/main/actionsPanel/workspaceTreeView/helpers/moveDirectory.ts index 53dd81a2..2489480b 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/helpers/moveDirectory.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/helpers/moveDirectory.ts @@ -1,11 +1,11 @@ import { - createDirectory, - getStat, - readDir, - readFile, - removeFileSystem, + _createIDBDirectory, + _getIDBDirectoryOrFileStat, + _readIDBDirectory, + _readIDBFile, + _removeIDBDirectoryOrFile, TFileNodeData, - writeFile, + _writeIDBFile, } from "@_node/file"; export const moveDirectory = async ( @@ -26,24 +26,24 @@ export const moveDirectory = async ( orgPath: string; newPath: string; }; - await createDirectory(newPath); + await _createIDBDirectory(newPath); - const entries = await readDir(orgPath); + const entries = await _readIDBDirectory(orgPath); await Promise.all( entries.map(async (entry) => { const c_orgPath = `${orgPath}/${entry}`; const c_newPath = `${newPath}/${entry}`; - const stats = await getStat(c_orgPath); + const stats = await _getIDBDirectoryOrFileStat(c_orgPath); const c_kind = stats.type === "DIRECTORY" ? "directory" : "file"; if (c_kind === "directory") { dirs.push({ orgPath: c_orgPath, newPath: c_newPath }); } else { - await writeFile(c_newPath, await readFile(c_orgPath)); + await _writeIDBFile(c_newPath, await _readIDBFile(c_orgPath)); } }), ); } // handle copy(optional) - !copy && (await removeFileSystem(nodeData.path)); + !copy && (await _removeIDBDirectoryOrFile(nodeData.path)); }; diff --git a/src/components/main/actionsPanel/workspaceTreeView/helpers/renameNode.ts b/src/components/main/actionsPanel/workspaceTreeView/helpers/renameNode.ts index b9a813f1..c4d3fa14 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/helpers/renameNode.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/helpers/renameNode.ts @@ -30,7 +30,7 @@ export const renameNode = async ( const { removeRunningActions, fileHandlers } = useContext(MainContext); - const { removeInvalidNodes, setInvalidNodes } = useInvalidNodes(); + const { removeInvalidNodes, addInvalidNodes } = useInvalidNodes(); const { moveIDBFF, moveLocalFF } = moveActions(() => {}); @@ -54,7 +54,7 @@ export const renameNode = async ( return; } - setInvalidNodes(newUid); + addInvalidNodes(newUid); try { await moveLocalFF( @@ -74,7 +74,7 @@ export const renameNode = async ( return; } } else if (project.context === "idb") { - setInvalidNodes(newUid); + addInvalidNodes(newUid); try { await moveIDBFF(nodeData, parentNodeData, _newName, false, true); diff --git a/src/components/main/actionsPanel/workspaceTreeView/helpers/validateAndDeleteNode.ts b/src/components/main/actionsPanel/workspaceTreeView/helpers/validateAndDeleteNode.ts deleted file mode 100644 index 209b31a3..00000000 --- a/src/components/main/actionsPanel/workspaceTreeView/helpers/validateAndDeleteNode.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { TFileHandlerCollection, TFileNodeData } from "@_node/file"; -import { TNodeTreeData, TNodeUid } from "@_node/types"; -import { verifyFileHandlerPermission } from "@_services/main"; - -export const validateAndDeleteNode = async ( - uid: string, - ffTree: TNodeTreeData, - fileHandlers: TFileHandlerCollection, -) => { - const node = ffTree[uid]; - - if (node === undefined) { - return false; - } - - const nodeData = node.data as TFileNodeData; - const parentNode = ffTree[node.parentUid as TNodeUid]; - - if (parentNode === undefined) { - return false; - } - - const parentHandler = fileHandlers[ - parentNode.uid - ] as FileSystemDirectoryHandle; - - if (!(await verifyFileHandlerPermission(parentHandler))) { - return false; - } - - try { - const entryName = - nodeData.kind === "directory" - ? nodeData.name - : `${nodeData.name}${nodeData.ext}`; - await parentHandler.removeEntry(entryName, { recursive: true }); - return true; - } catch (err) { - return false; - } -}; diff --git a/src/components/main/actionsPanel/workspaceTreeView/helpers/validateAndMoveNode.ts b/src/components/main/actionsPanel/workspaceTreeView/helpers/validateAndMoveNode.ts index 3cd57327..7aa4a24e 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/helpers/validateAndMoveNode.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/helpers/validateAndMoveNode.ts @@ -19,7 +19,7 @@ export const validateAndMoveNode = async ( const { fileHandlers } = useContext(MainContext); - const { setInvalidNodes }: any = useInvalidNodes(); + const { addInvalidNodes }: any = useInvalidNodes(); const { moveIDBFF, moveLocalFF } = moveActions(() => {}); @@ -55,7 +55,7 @@ export const validateAndMoveNode = async ( )}`; // update invalidNodes - setInvalidNodes((prevState: Record) => ({ + addInvalidNodes((prevState: Record) => ({ ...prevState, [uid]: true, [newUid]: true, @@ -75,7 +75,7 @@ export const validateAndMoveNode = async ( return false; } finally { // update invalidNodes - setInvalidNodes((prevState: Record) => { + addInvalidNodes((prevState: Record) => { delete prevState[uid]; delete prevState[newUid]; return { ...prevState }; diff --git a/src/components/main/actionsPanel/workspaceTreeView/hooks/index.ts b/src/components/main/actionsPanel/workspaceTreeView/hooks/index.ts index 4c7bd8ec..fce824a6 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/hooks/index.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/hooks/index.ts @@ -4,3 +4,4 @@ export * from "./useFileOperations"; export * from "./useNodeActionsHandler"; export * from "./useNodeViewState"; export * from "./useCmdk"; +export * from "./useSync"; diff --git a/src/components/main/actionsPanel/workspaceTreeView/hooks/useCmdk.ts b/src/components/main/actionsPanel/workspaceTreeView/hooks/useCmdk.ts index 0253fad5..50567ffb 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/hooks/useCmdk.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/hooks/useCmdk.ts @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useEffect } from "react"; import { useDispatch } from "react-redux"; @@ -7,25 +7,53 @@ import { setClipboardData } from "@_redux/main/processor"; import { useAppState } from "@_redux/useAppState"; import { TFileNodeType } from "@_types/main"; -import { useInvalidNodes } from "./useInvalidNodes"; import { useNodeActionsHandler } from "./useNodeActionsHandler"; +import { isAddFileAction } from "@_node/helpers"; -export const useCmdk = (openFileUid: React.MutableRefObject) => { +interface IUseCmdk { + invalidNodes: { + [uid: string]: true; + }; + addInvalidNodes: (...uids: string[]) => void; + removeInvalidNodes: (...uids: string[]) => void; + temporaryNodes: { + [uid: string]: true; + }; + addTemporaryNodes: (...uids: string[]) => void; + removeTemporaryNodes: (...uids: string[]) => void; + openFileUid: React.MutableRefObject; +} +export const useCmdk = ({ + invalidNodes, + addInvalidNodes, + removeInvalidNodes, + temporaryNodes, + addTemporaryNodes, + removeTemporaryNodes, + openFileUid, +}: IUseCmdk) => { const dispatch = useDispatch(); - const { + activePanel, currentFileUid, fFocusedItem: focusedItem, fSelectedItems: selectedItems, clipboardData, fileTree, nodeTree, + currentCommand, } = useAppState(); const { createTmpFFNode, cb_deleteNode, cb_moveNode, cb_duplicateNode } = - useNodeActionsHandler(openFileUid); - - const { invalidNodes } = useInvalidNodes(); + useNodeActionsHandler({ + invalidNodes, + addInvalidNodes, + removeInvalidNodes, + temporaryNodes, + addTemporaryNodes, + removeTemporaryNodes, + openFileUid, + }); const onAddNode = useCallback( (actionName: string) => { @@ -94,12 +122,32 @@ export const useCmdk = (openFileUid: React.MutableRefObject) => { cb_duplicateNode(); }, [cb_duplicateNode]); - return { - onAddNode, - onDelete, - onCut, - onCopy, - onPaste, - onDuplicate, - }; + useEffect(() => { + if (!currentCommand) return; + if (isAddFileAction(currentCommand.action)) { + onAddNode(currentCommand.action); + return; + } + if (activePanel !== "file") return; + + switch (currentCommand.action) { + case "Delete": + onDelete(); + break; + case "Cut": + onCut(); + break; + case "Copy": + onCopy(); + break; + case "Paste": + onPaste(); + break; + case "Duplicate": + onDuplicate(); + break; + default: + break; + } + }, [currentCommand]); }; diff --git a/src/components/main/actionsPanel/workspaceTreeView/hooks/useFileOperations.ts b/src/components/main/actionsPanel/workspaceTreeView/hooks/useFileOperations.ts index a9ee2f31..81a571c2 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/hooks/useFileOperations.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/hooks/useFileOperations.ts @@ -12,18 +12,31 @@ import { deleteFileOrFolder, moveActions, } from "../helpers"; -import { useInvalidNodes } from "./useInvalidNodes"; -import { useTemporaryNodes } from "./useTemporaryNodes"; -export const useFileOperations = () => { +interface IUseFileOperations { + invalidNodes: { + [uid: string]: true; + }; + addInvalidNodes: (...uids: string[]) => void; + removeInvalidNodes: (...uids: string[]) => void; + temporaryNodes: { + [uid: string]: true; + }; + addTemporaryNodes: (...uids: string[]) => void; + removeTemporaryNodes: (...uids: string[]) => void; +} +export const useFileOperations = ({ + invalidNodes, + addInvalidNodes, + removeInvalidNodes, + temporaryNodes, + addTemporaryNodes, + removeTemporaryNodes, +}: IUseFileOperations) => { const { project, fileTree } = useAppState(); const { addRunningActions, removeRunningActions, fileHandlers } = useContext(MainContext); - const { invalidNodes, removeInvalidNodes, setInvalidNodes } = - useInvalidNodes(); - const { setTemporaryNodes, removeTemporaryNodes } = useTemporaryNodes(); - const { moveIDBFF, moveLocalFF } = moveActions(() => {}); const _create = useCallback( @@ -58,11 +71,10 @@ export const useFileOperations = () => { fileHandlers, ], ); - const _delete = useCallback( async (uids: TNodeUid[]) => { addRunningActions(["fileTreeView-delete"]); - setInvalidNodes(...uids); + addInvalidNodes(...uids); await Promise.all( uids.map(async (uid) => { @@ -86,7 +98,7 @@ export const useFileOperations = () => { addRunningActions, removeRunningActions, project.context, - setInvalidNodes, + addInvalidNodes, removeInvalidNodes, fileTree, fileHandlers, @@ -106,8 +118,8 @@ export const useFileOperations = () => { const parentNodeData = parentNode.data as TFileNodeData; const newUid = `${parentNode.uid}/${newName}`; - setTemporaryNodes(uid); - setInvalidNodes(newUid); + addTemporaryNodes(uid); + addInvalidNodes(newUid); if (project.context === "local") { const handler = fileHandlers[uid], @@ -134,9 +146,9 @@ export const useFileOperations = () => { addRunningActions, removeRunningActions, project.context, - setTemporaryNodes, + addTemporaryNodes, removeTemporaryNodes, - setInvalidNodes, + addInvalidNodes, removeInvalidNodes, fileTree, fileHandlers, @@ -165,7 +177,7 @@ export const useFileOperations = () => { const newUid = `${targetUid}/${nodeData.name}`; _invalidNodes[uid] = true; _invalidNodes[newUid] = true; - setInvalidNodes(...Object.keys(_invalidNodes)); + addInvalidNodes(...Object.keys(_invalidNodes)); if (project.context === "local") { const handler = fileHandlers[uid], @@ -196,7 +208,7 @@ export const useFileOperations = () => { delete _invalidNodes[uid]; delete _invalidNodes[newUid]; - setInvalidNodes(...Object.keys(_invalidNodes)); + addInvalidNodes(...Object.keys(_invalidNodes)); }), ); removeRunningActions(["fileTreeView-move"]); @@ -206,7 +218,7 @@ export const useFileOperations = () => { removeRunningActions, project.context, invalidNodes, - setInvalidNodes, + addInvalidNodes, fileTree, fileHandlers, ], @@ -235,7 +247,7 @@ export const useFileOperations = () => { const newUid = `${targetUid}/${name}`; _invalidNodes[uid] = true; _invalidNodes[newUid] = true; - setInvalidNodes(...Object.keys(_invalidNodes)); + addInvalidNodes(...Object.keys(_invalidNodes)); if (project.context === "local") { const handler = fileHandlers[uid], @@ -269,7 +281,7 @@ export const useFileOperations = () => { delete _invalidNodes[uid]; delete _invalidNodes[newUid]; - setInvalidNodes(...Object.keys(_invalidNodes)); + addInvalidNodes(...Object.keys(_invalidNodes)); }), ); removeRunningActions(["fileTreeView-duplicate"]); @@ -279,7 +291,7 @@ export const useFileOperations = () => { removeRunningActions, project.context, invalidNodes, - setInvalidNodes, + addInvalidNodes, fileTree, fileHandlers, ], diff --git a/src/components/main/actionsPanel/workspaceTreeView/hooks/useInvalidNodes.ts b/src/components/main/actionsPanel/workspaceTreeView/hooks/useInvalidNodes.ts index 8ba7d328..aae875c2 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/hooks/useInvalidNodes.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/hooks/useInvalidNodes.ts @@ -3,31 +3,25 @@ import { useCallback, useState } from "react"; import { TNodeUid } from "@_node/types"; export const useInvalidNodes = () => { - const [invalidNodes, _setInvalidNodes] = useState<{ + const [invalidNodes, _addInvalidNodes] = useState<{ [uid: TNodeUid]: true; }>({}); - - const setInvalidNodes = useCallback( + const addInvalidNodes = useCallback( (...uids: TNodeUid[]) => { const _invalidNodes = { ...invalidNodes }; uids.map((uid) => (_invalidNodes[uid] = true)); - _setInvalidNodes(_invalidNodes); + _addInvalidNodes(_invalidNodes); }, [invalidNodes], ); - const removeInvalidNodes = useCallback( (...uids: TNodeUid[]) => { const _invalidNodes = { ...invalidNodes }; uids.map((uid) => delete _invalidNodes[uid]); - _setInvalidNodes(_invalidNodes); + _addInvalidNodes(_invalidNodes); }, [invalidNodes], ); - return { - setInvalidNodes, - removeInvalidNodes, - invalidNodes, - }; + return { invalidNodes, addInvalidNodes, removeInvalidNodes }; }; diff --git a/src/components/main/actionsPanel/workspaceTreeView/hooks/useNodeActionsHandler.ts b/src/components/main/actionsPanel/workspaceTreeView/hooks/useNodeActionsHandler.ts index 99861b08..bc9b932a 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/hooks/useNodeActionsHandler.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/hooks/useNodeActionsHandler.ts @@ -3,12 +3,17 @@ import { useCallback, useContext } from "react"; import { TreeItem } from "react-complex-tree"; import { useDispatch } from "react-redux"; -import { RednerableFileTypes, RootNodeUid, TmpNodeUid } from "@_constants/main"; import { - createDirectory, + FileChangeAlertMessage, + RednerableFileTypes, + RootNodeUid, + TmpNodeUid, +} from "@_constants/main"; +import { + _createIDBDirectory, TFileNodeData, - triggerFileChangeAlert, - writeFile, + _writeIDBFile, + confirmAlert, } from "@_node/file"; import { getValidNodeUids } from "@_node/helpers"; import { TNode, TNodeTreeData, TNodeUid } from "@_node/types"; @@ -34,15 +39,32 @@ import { duplicateNode, generateNewName, renameNode, - validateAndDeleteNode, validateAndMoveNode, } from "../helpers"; -import { useInvalidNodes } from "./useInvalidNodes"; -import { useTemporaryNodes } from "./useTemporaryNodes"; +import { callFileApi } from "@_node/apis"; -export const useNodeActionsHandler = ( - openFileUid: React.MutableRefObject, -) => { +interface IUseNodeActionsHandler { + invalidNodes: { + [uid: string]: true; + }; + addInvalidNodes: (...uids: string[]) => void; + removeInvalidNodes: (...uids: string[]) => void; + temporaryNodes: { + [uid: string]: true; + }; + addTemporaryNodes: (...uids: string[]) => void; + removeTemporaryNodes: (...uids: string[]) => void; + openFileUid: React.MutableRefObject; +} +export const useNodeActionsHandler = ({ + invalidNodes, + addInvalidNodes, + removeInvalidNodes, + temporaryNodes, + addTemporaryNodes, + removeTemporaryNodes, + openFileUid, +}: IUseNodeActionsHandler) => { const dispatch = useDispatch(); const { project, @@ -61,11 +83,6 @@ export const useNodeActionsHandler = ( htmlReferenceData, } = useContext(MainContext); - const { removeInvalidNodes, setInvalidNodes, invalidNodes } = - useInvalidNodes(); - const { temporaryNodes, setTemporaryNodes, removeTemporaryNodes } = - useTemporaryNodes(); - const createFFNode = useCallback( async (parentUid: TNodeUid, ffType: TFileNodeType, ffName: string) => { let newName: string = ""; @@ -99,9 +116,9 @@ export const useNodeActionsHandler = ( // create the directory or file with generated name try { if (ffType === "*folder") { - await createDirectory(`${parentNodeData.path}/${newName}`); + await _createIDBDirectory(`${parentNodeData.path}/${newName}`); } else { - await writeFile(`${parentNodeData.path}/${newName}`, ""); + await _writeIDBFile(`${parentNodeData.path}/${newName}`, ""); } } catch (err) { removeRunningActions(["fileTreeView-create"]); @@ -166,7 +183,7 @@ export const useNodeActionsHandler = ( tmpTree[tmpNode.uid] = tmpNode; // setFFTree(tmpTree) - setInvalidNodes(tmpNode.uid); + addInvalidNodes(tmpNode.uid); await createFFNode( node.uid as TNodeUid, tmpNode.data.type, @@ -184,7 +201,7 @@ export const useNodeActionsHandler = ( fileTree, focusedItem, expandedItemsObj, - setInvalidNodes, + addInvalidNodes, createFFNode, removeInvalidNodes, ], @@ -196,9 +213,9 @@ export const useNodeActionsHandler = ( removeInvalidNodes(uid); return; } - setInvalidNodes(uid); + addInvalidNodes(uid); }, - [invalidNodes, setInvalidNodes, removeInvalidNodes], + [invalidNodes, addInvalidNodes, removeInvalidNodes], ); const cb_abortRenamingNode = useCallback( (item: TreeItem) => { @@ -234,7 +251,7 @@ export const useNodeActionsHandler = ( addRunningActions, removeRunningActions, project.context, - setInvalidNodes, + addInvalidNodes, removeInvalidNodes, fileTree, fileHandlers, @@ -244,28 +261,23 @@ export const useNodeActionsHandler = ( async (item: TreeItem, newName: string) => { const node = item.data as TNode; const nodeData = node.data as TFileNodeData; - if (!invalidNodes[node.uid]) return; + if (invalidNodes[node.uid]) return; if (nodeData.valid) { - const _file = fileTree[node.uid]; - const _fileData = _file.data as TFileNodeData; - - if (_file && _fileData.changed) { - // confirm + const file = fileTree[node.uid]; + const fileData = file.data; + if (file && fileData.changed) { const message = `Your changes will be lost if you don't save them. Are you sure you want to continue without saving?`; - if (!window.confirm(message)) { - removeInvalidNodes(node.uid); - return; - } + if (!confirmAlert(message)) return; } - setTemporaryNodes(_file.uid); + addTemporaryNodes(file.uid); await _cb_renameNode( - _file.uid, + file.uid, newName, - _fileData.kind === "directory" ? "*folder" : _fileData.ext, + fileData.kind === "directory" ? "*folder" : fileData.ext, ); - removeTemporaryNodes(_file.uid); + removeTemporaryNodes(file.uid); } else { await createFFNode( node.parentUid as TNodeUid, @@ -278,7 +290,7 @@ export const useNodeActionsHandler = ( [ invalidNodes, _cb_renameNode, - setTemporaryNodes, + addTemporaryNodes, removeTemporaryNodes, fileTree, fileHandlers, @@ -293,41 +305,41 @@ export const useNodeActionsHandler = ( if (uids.length === 0) return; const message = `Are you sure you want to delete them? This action cannot be undone!`; - if (!window.confirm(message)) return; - - addRunningActions(["fileTreeView-delete"]); - setInvalidNodes(...uids); - - if (project.context === "local") { - const allDone = await Promise.all( - uids.map((uid) => validateAndDeleteNode(uid, fileTree, fileHandlers)), - ).then((results) => results.every(Boolean)); - - if (!allDone) { - } - } else if (project.context === "idb") { - const allDone = await Promise.all( - uids.map((uid) => validateAndDeleteNode(uid, fileTree, fileHandlers)), - ).then((results) => results.every(Boolean)); - - if (!allDone) { - // addMessage(deletingWarning); - } + if (!window.confirm(message)) { + return; } + addRunningActions(["fileTreeView-delete"]); + addInvalidNodes(...uids); + await callFileApi( + { + projectContext: project.context, + action: "remove", + fileTree, + fileHandlers, + uids, + }, + () => { + console.error("error while removing file system"); + }, + (allDone: boolean) => { + console.log( + allDone ? "all is successfully removed" : "some is not removed", + ); + }, + ); removeInvalidNodes(...uids); removeRunningActions(["fileTreeView-delete"]); }, [ - addRunningActions, - removeRunningActions, selectedItems, invalidNodes, - setInvalidNodes, + addRunningActions, + removeRunningActions, + addInvalidNodes, removeInvalidNodes, - project.context, + project, fileTree, fileHandlers, - currentFileUid, ]); const cb_moveNode = useCallback( @@ -352,9 +364,7 @@ export const useNodeActionsHandler = ( return _file && _fileData.changed; }); - if (hasChangedFile) { - triggerFileChangeAlert(); - } + if (hasChangedFile && !confirmAlert(FileChangeAlertMessage)) return; addRunningActions(["fileTreeView-move"]); @@ -380,7 +390,7 @@ export const useNodeActionsHandler = ( removeRunningActions, project.context, invalidNodes, - setInvalidNodes, + addInvalidNodes, fileTree, fileHandlers, ], @@ -423,7 +433,7 @@ export const useNodeActionsHandler = ( fileTree, fileHandlers, () => {}, - setInvalidNodes, + addInvalidNodes, invalidNodes, ); if (result) { @@ -453,7 +463,7 @@ export const useNodeActionsHandler = ( removeRunningActions, project.context, invalidNodes, - setInvalidNodes, + addInvalidNodes, selectedItems, fileTree, fileHandlers, diff --git a/src/components/main/actionsPanel/workspaceTreeView/hooks/useNodeViewState.ts b/src/components/main/actionsPanel/workspaceTreeView/hooks/useNodeViewState.ts index 9a3995f0..a480f13f 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/hooks/useNodeViewState.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/hooks/useNodeViewState.ts @@ -1,10 +1,9 @@ -import { useCallback, useContext } from "react"; +import { useCallback } from "react"; import { useDispatch } from "react-redux"; import { getValidNodeUids } from "@_node/helpers"; import { TNodeUid } from "@_node/types"; -import { MainContext } from "@_redux/main"; import { collapseFileTreeNodes, expandFileTreeNodes, @@ -13,9 +12,12 @@ import { } from "@_redux/main/fileTree"; import { useAppState } from "@_redux/useAppState"; -import { useInvalidNodes } from "./"; - -export const useNodeViewState = () => { +interface IUseNodeViewState { + invalidNodes: { + [uid: string]: true; + }; +} +export const useNodeViewState = ({ invalidNodes }: IUseNodeViewState) => { const dispatch = useDispatch(); const { fileTree, @@ -24,117 +26,64 @@ export const useNodeViewState = () => { fSelectedItems: selectedItems, fSelectedItemsObj: selectedItemsObj, } = useAppState(); - const { addRunningActions, removeRunningActions } = useContext(MainContext); - - const { invalidNodes } = useInvalidNodes(); const cb_focusNode = useCallback( (uid: TNodeUid) => { - if ( - invalidNodes[uid] || - focusedItem === uid || - fileTree[uid] === undefined - ) { - removeRunningActions(["fileTreeView-focus"]); - return; - } + if (invalidNodes[uid] || focusedItem === uid || !fileTree[uid]) return; - addRunningActions(["fileTreeView-focus"]); dispatch(focusFileTreeNode(uid)); - removeRunningActions(["fileTreeView-focus"]); }, - [ - addRunningActions, - removeRunningActions, - invalidNodes, - focusedItem, - fileTree, - ], + [invalidNodes, focusedItem, fileTree], ); const cb_selectNode = useCallback( (uids: TNodeUid[]) => { let _uids = [...uids]; _uids = _uids.filter((_uid) => !invalidNodes[_uid] && fileTree[_uid]); - if (_uids.length === 0) { - removeRunningActions(["fileTreeView-select"]); - return; - } + if (_uids.length === 0) return; _uids = getValidNodeUids(fileTree, _uids); if (_uids.length === selectedItems.length) { let same = true; for (const _uid of _uids) { - if (selectedItemsObj[_uid] === undefined) { + if (!selectedItemsObj[_uid]) { same = false; break; } } - if (same) { - removeRunningActions(["fileTreeView-select"]); - return; - } + if (same) return; } - addRunningActions(["fileTreeView-select"]); dispatch(selectFileTreeNodes(_uids)); - removeRunningActions(["fileTreeView-select"]); }, - [ - addRunningActions, - removeRunningActions, - fileTree, - invalidNodes, - selectedItems, - selectedItemsObj, - ], + [invalidNodes, fileTree, selectedItems, selectedItemsObj], ); const cb_expandNode = useCallback( (uid: TNodeUid) => { if ( invalidNodes[uid] || - fileTree[uid] === undefined || + !fileTree[uid] || fileTree[uid].isEntity || expandedItemsObj[uid] - ) { - removeRunningActions(["fileTreeView-expand"]); + ) return; - } - addRunningActions(["fileTreeView-expand"]); dispatch(expandFileTreeNodes([uid])); - removeRunningActions(["fileTreeView-expand"]); }, - [ - addRunningActions, - removeRunningActions, - invalidNodes, - fileTree, - expandedItemsObj, - ], + [invalidNodes, fileTree, expandedItemsObj], ); const cb_collapseNode = useCallback( (uid: TNodeUid) => { if ( invalidNodes[uid] || - fileTree[uid] === undefined || + !fileTree[uid] || fileTree[uid].isEntity || !expandedItemsObj[uid] - ) { - removeRunningActions(["fileTreeView-collapse"]); + ) return; - } - addRunningActions(["fileTreeView-collapse"]); dispatch(collapseFileTreeNodes([uid])); - removeRunningActions(["fileTreeView-collapse"]); }, - [ - addRunningActions, - removeRunningActions, - invalidNodes, - fileTree, - expandedItemsObj, - ], + [invalidNodes, fileTree, expandedItemsObj], ); return { diff --git a/src/components/main/actionsPanel/workspaceTreeView/hooks/useSync.ts b/src/components/main/actionsPanel/workspaceTreeView/hooks/useSync.ts new file mode 100644 index 00000000..0f41a3f8 --- /dev/null +++ b/src/components/main/actionsPanel/workspaceTreeView/hooks/useSync.ts @@ -0,0 +1,74 @@ +import { useCallback, useEffect, useMemo, useRef } from "react"; + +import { debounce } from "lodash"; + +import { TreeViewData } from "@_components/common/treeView/types"; +import { RootNodeUid, ShortDelay } from "@_constants/main"; +import { _path } from "@_node/file"; +import { TNode, TNodeUid } from "@_node/types"; +import { scrollToElement } from "@_pages/main/helper"; +import { useAppState } from "@_redux/useAppState"; +import { addClass, generateQuerySelector, removeClass } from "@_services/main"; + +export const useSync = () => { + const { fileTree, fFocusedItem: focusedItem, hoveredFileUid } = useAppState(); + + // outline the hovered item + const hoveredItemRef = useRef(hoveredFileUid); + useEffect(() => { + if (hoveredItemRef.current === hoveredFileUid) return; + + const curHoveredElement = document.querySelector( + `#FileTreeView-${generateQuerySelector(hoveredItemRef.current)}`, + ); + curHoveredElement?.setAttribute( + "class", + removeClass(curHoveredElement.getAttribute("class") || "", "outline"), + ); + const newHoveredElement = document.querySelector( + `#FileTreeView-${generateQuerySelector(hoveredFileUid)}`, + ); + newHoveredElement?.setAttribute( + "class", + addClass(newHoveredElement.getAttribute("class") || "", "outline"), + ); + + hoveredItemRef.current = hoveredFileUid; + }, [hoveredFileUid]); + + // scroll to the focused item + const debouncedScrollToElement = useCallback( + debounce(scrollToElement, ShortDelay), + [], + ); + const focusedItemRef = useRef(focusedItem); + useEffect(() => { + if (focusedItemRef.current === focusedItem) return; + + const focusedElement = document.querySelector( + `#FileTreeView-${generateQuerySelector(focusedItem)}`, + ); + focusedElement && debouncedScrollToElement(focusedElement, "auto"); + + focusedItemRef.current = focusedItem; + }, [focusedItem]); + + // build fileTreeViewData + const fileTreeViewData = useMemo(() => { + const data: TreeViewData = {}; + for (const uid in fileTree) { + const node: TNode = fileTree[uid]; + data[uid] = { + index: uid, + data: node, + children: node.children, + isFolder: !node.isEntity, + canMove: uid !== RootNodeUid, + canRename: uid !== RootNodeUid, + }; + } + return data; + }, [fileTree]); + + return { focusedItemRef, fileTreeViewData }; +}; diff --git a/src/components/main/actionsPanel/workspaceTreeView/hooks/useTemporaryNodes.ts b/src/components/main/actionsPanel/workspaceTreeView/hooks/useTemporaryNodes.ts index dfd96d82..0b3adcf9 100644 --- a/src/components/main/actionsPanel/workspaceTreeView/hooks/useTemporaryNodes.ts +++ b/src/components/main/actionsPanel/workspaceTreeView/hooks/useTemporaryNodes.ts @@ -3,27 +3,25 @@ import { useCallback, useState } from "react"; import { TNodeUid } from "@_node/types"; export const useTemporaryNodes = () => { - const [temporaryNodes, _setTemporaryNodes] = useState<{ + const [temporaryNodes, _addTemporaryNodes] = useState<{ [uid: TNodeUid]: true; }>({}); - - const setTemporaryNodes = useCallback( + const addTemporaryNodes = useCallback( (...uids: TNodeUid[]) => { const _temporaryNodes = { ...temporaryNodes }; - uids.forEach((uid) => (_temporaryNodes[uid] = true)); - _setTemporaryNodes(_temporaryNodes); + uids.map((uid) => (_temporaryNodes[uid] = true)); + _addTemporaryNodes(_temporaryNodes); }, [temporaryNodes], ); - const removeTemporaryNodes = useCallback( (...uids: TNodeUid[]) => { const _temporaryNodes = { ...temporaryNodes }; - uids.forEach((uid) => delete _temporaryNodes[uid]); - _setTemporaryNodes(_temporaryNodes); + uids.map((uid) => delete _temporaryNodes[uid]); + _addTemporaryNodes(_temporaryNodes); }, [temporaryNodes], ); - return { temporaryNodes, setTemporaryNodes, removeTemporaryNodes }; + return { temporaryNodes, addTemporaryNodes, removeTemporaryNodes }; }; diff --git a/src/components/main/stageView/iFrame/hooks/useCmdk.ts b/src/components/main/stageView/iFrame/hooks/useCmdk.ts index c8d8c156..a1705155 100644 --- a/src/components/main/stageView/iFrame/hooks/useCmdk.ts +++ b/src/components/main/stageView/iFrame/hooks/useCmdk.ts @@ -85,7 +85,8 @@ export const useCmdk = ({ action === "Save" || action === "Download" || action === "Duplicate" || - action === "Group" + action === "Group" || + action === "UnGroup" ) { e.preventDefault(); } diff --git a/src/constants/main.ts b/src/constants/main.ts index d8b3a9dd..b82042b7 100644 --- a/src/constants/main.ts +++ b/src/constants/main.ts @@ -31,5 +31,6 @@ export const ShortDelay = 50; export const NodePathSplitter: string = "?"; +export const FileChangeAlertMessage = `Your changes will be lost if you don't save them. Are you sure you want to continue without saving?`; // ---------------------- export const TmpNodeUid = "tmp:node:uid"; diff --git a/src/pages/main/MainPage.tsx b/src/pages/main/MainPage.tsx index 9a1e3ceb..5cdee720 100644 --- a/src/pages/main/MainPage.tsx +++ b/src/pages/main/MainPage.tsx @@ -1,838 +1,149 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { useEffect } from "react"; -import cx from 'classnames'; -import { Command } from 'cmdk'; -import { - CustomDirectoryPickerOptions, -} from 'file-system-access/lib/showDirectoryPicker'; -import { - delMany, - getMany, - setMany, -} from 'idb-keyval'; -import { editor } from 'monaco-editor'; -import { useDispatch } from 'react-redux'; +import cx from "classnames"; +import { Command } from "cmdk"; +import { useDispatch } from "react-redux"; -import { SVGIcon } from '@_components/common'; -import { - ActionsPanel, - CodeView, - StageView, -} from '@_components/main'; -import { LogAllow } from '@_constants/global'; -import { - AddActionPrefix, - DefaultProjectPath, - RecentProjectCount, - RenameActionPrefix, -} from '@_constants/main'; +import { SVGIcon } from "@_components/common"; +import { ActionsPanel, CodeView, StageView } from "@_components/main"; +import { LogAllow } from "@_constants/global"; +import { AddActionPrefix, RenameActionPrefix } from "@_constants/main"; import { - buildNohostIDB, confirmFileChanges, - downloadProject, - initIDBProject, - loadIDBProject, - loadLocalProject, - TFileHandlerCollection, + isUnsavedProject, TFileNodeData, -} from '@_node/file'; -import { - setOsType, - setTheme, -} from '@_redux/global'; -import { MainContext } from '@_redux/main'; +} from "@_node/file"; +import { MainContext } from "@_redux/main"; import { setCmdkOpen, setCmdkPages, setCmdkSearchContent, - setCurrentCmdkPage, setCurrentCommand, -} from '@_redux/main/cmdk'; -import { - setDoingFileAction, - setFileTree, - setInitialFileUidToOpen, - setProject, - setWorkspace, - TProject, - TProjectContext, -} from '@_redux/main/fileTree'; -import { - FileTree_Event_RedoActionType, - FileTree_Event_UndoActionType, - setFileAction, -} from '@_redux/main/fileTree/event'; -import { - NodeTree_Event_RedoActionType, - NodeTree_Event_UndoActionType, -} from '@_redux/main/nodeTree/event'; -import { - setDidRedo, - setDidUndo, - setNavigatorDropdownType, - setShowActionsPanel, - setShowCodeView, -} from '@_redux/main/processor'; -import { useAppState } from '@_redux/useAppState'; -// @ts-ignore -import cmdkRefActions from '@_ref/cmdk.ref/Actions.csv'; -// @ts-ignore -import cmdkRefJumpstart from '@_ref/cmdk.ref/Jumpstart.csv'; -// @ts-ignore -import filesRef from '@_ref/rfrncs/Files.csv'; -// @ts-ignore -import htmlRefElements from '@_ref/rfrncs/HTML Elements.csv'; -import { - TCmdkContext, - TCmdkContextScope, - TCmdkGroupData, - TCmdkKeyMap, - TCmdkReference, - TCmdkReferenceData, - TFilesReference, - TFilesReferenceData, - THtmlElementsReference, - THtmlElementsReferenceData, - THtmlReferenceData, - TSession, -} from '@_types/main'; +} from "@_redux/main/cmdk"; +import { setFileAction } from "@_redux/main/fileTree"; +import { useAppState } from "@_redux/useAppState"; +import { TCmdkContext, TCmdkKeyMap, TCmdkReference } from "@_types/main"; +import { getCommandKey } from "../../services/global"; import { - getCommandKey, - isChromeOrEdge, -} from '../../services/global'; -import { - addDefaultCmdkActions, - clearProjectSession, - elementsCmdk, - fileCmdk, -} from './helper'; -import Processor from './processor'; + useCmdk, + useCmdkModal, + useCmdkReferenceData, + useFileHandlers, + useHandlers, + useInit, + usePanels, + useRecentProjects, + useReferenceData, + useReferneces, + useRunningActions, +} from "./hooks"; +import Processor from "./processor"; export default function MainPage() { - // ***************************************** Reducer Begin ***************************************** + // redux const dispatch = useDispatch(); const { osType, theme, - workspace, - project, - initialFileUidToOpen, currentFileUid, fileTree, - fFocusedItem, - fExpandedItems, - fExpandedItemsObj, - fSelectedItems, - fSelectedItemsObj, - hoveredFileUid, - - doingFileAction, - lastFileAction, - fileAction, - fileEventPast, - fileEventPastLength, - fileEventFuture, fileEventFutureLength, - nodeTree, - validNodeTree, - - nFocusedItem, - nExpandedItems, - nExpandedItemsObj, - nSelectedItems, - nSelectedItemsObj, - hoveredNodeUid, - - currentFileContent, - selectedNodeUids, - - nodeEventPast, - nodeEventPastLength, - - nodeEventFuture, - nodeEventFutureLength, - - iframeSrc, - iframeLoading, - needToReloadIframe, - linkToOpen, - - codeViewTabSize, - - navigatorDropdownType, - favicon, - activePanel, - clipboardData, showActionsPanel, - showCodeView, - - didUndo, - didRedo, + autoSave, cmdkOpen, cmdkPages, currentCmdkPage, cmdkSearchContent, - currentCommand, } = useAppState(); - // ***************************************** Reducer End ***************************************** - - // ***************************************** Context Begin ***************************************** - const [doingAction, setDoingAction] = useState(false); - const runningActions = useRef<{ [actionName: string]: true }>({}); - const hasRunningAction = useCallback( - () => (Object.keys(runningActions.current).length === 0 ? false : true), - [], - ); - const addRunningActions = useCallback((actionNames: string[]) => { - let found = false; - for (const actionName of actionNames) { - if (!runningActions.current[actionName]) { - runningActions.current[actionName] = true; - found = true; - } - } - if (!found) return; - - setDoingAction(true); - }, []); - const removeRunningActions = useCallback( - (actionNames: string[]) => { - let found = false; - for (const actionName of actionNames) { - if (runningActions.current[actionName]) { - delete runningActions.current[actionName]; - found = true; - } - } - if (!found) return; - - !hasRunningAction() && setDoingAction(false); - }, - [hasRunningAction], - ); - - const [recentProjectContexts, setRecentProjectContexts] = useState< - TProjectContext[] - >([]); - const [recentProjectNames, setRecentProjectNames] = useState([]); - const [recentProjectHandlers, setRecentProjectHandlers] = useState< - (FileSystemDirectoryHandle | null)[] - >([]); - - const [currentProjectFileHandle, setCurrentProjectFileHandle] = - useState(null); - const [fileHandlers, setFileHandlers] = useState({}); - - const monacoEditorRef = useRef(null); - const setMonacoEditorRef = ( - editorInstance: editor.IStandaloneCodeEditor | null, - ) => { - monacoEditorRef.current = editorInstance; - }; - - const iframeRefRef = useRef(null); - const setIframeRefRef = (iframeRef: HTMLIFrameElement | null) => { - iframeRefRef.current = iframeRef; - }; - - const [filesReferenceData, setFilesReferenceData] = - useState({}); - const [htmlReferenceData, setHtmlReferenceData] = - useState({ - elements: {}, - }); - const [cmdkReferenceData, setCmdkReferenceData] = - useState({}); - const [cmdkReferenceJumpstart, setCmdkReferenceJumpstart] = - useState({}); - const [cmdkReferenceActions, setCmdkReferenceActions] = - useState({}); - - const isContentProgrammaticallyChanged = useRef(false); - const setIsContentProgrammaticallyChanged = (value: boolean) => { - isContentProgrammaticallyChanged.current = value; - }; - - const guideRef = useRef(null); - // TODO End - - // ***************************************** Context End ***************************************** - - // ***************************************** Init Begin ***************************************** - useEffect(() => { - // TODO Begin - if (!isChromeOrEdge()) { - const message = `Browser is unsupported. rnbw works in the latest versions of Google Chrome and Microsoft Edge.`; - if (!window.confirm(message)) { - return; - } - } - // TODO End - - document.addEventListener("contextmenu", (e) => { - e.preventDefault(); - }); - window.document.addEventListener("contextmenu", (e) => { - e.preventDefault(); - }); - }, []); - - useEffect(() => { - (async () => { - addRunningActions([ - "detect-os", - "reference-files", - "reference-html-elements", - "reference-cmdk-jumpstart", - "reference-cmdk-actions", - ]); - - // detect os - LogAllow && console.log("navigator: ", navigator.userAgent); - if (navigator.userAgent.indexOf("Mac OS X") !== -1) { - dispatch(setOsType("Mac")); - } else if (navigator.userAgent.indexOf("Linux") !== -1) { - dispatch(setOsType("Linux")); - } else { - dispatch(setOsType("Windows")); - } - - // reference-files - const _filesReferenceData: TFilesReferenceData = {}; - filesRef.map((fileRef: TFilesReference) => { - _filesReferenceData[fileRef.Extension] = fileRef; - }); - setFilesReferenceData(_filesReferenceData); - LogAllow && console.log("files reference data: ", _filesReferenceData); - - // reference-html-elements - const htmlElementsReferenceData: THtmlElementsReferenceData = {}; - htmlRefElements.map((htmlRefElement: THtmlElementsReference) => { - const pureTag = - htmlRefElement["Name"] === "Comment" - ? "comment" - : htmlRefElement["Tag"]?.slice(1, htmlRefElement["Tag"].length - 1); - htmlElementsReferenceData[pureTag] = htmlRefElement; - }); - - LogAllow && - console.log( - "html elements reference data: ", - htmlElementsReferenceData, - ); - - setHtmlReferenceData({ elements: htmlElementsReferenceData }); - - // add default cmdk actions - const _cmdkReferenceData: TCmdkReferenceData = {}; - addDefaultCmdkActions(_cmdkReferenceData); - - // reference-cmdk-jumpstart - const _cmdkRefJumpstartData: TCmdkGroupData = {}; - await Promise.all( - cmdkRefJumpstart.map(async (command: TCmdkReference) => { - const shortcuts = (command["Keyboard Shortcut"] as string)?.split( - " ", - ); - const keyObjects: TCmdkKeyMap[] = []; - - shortcuts?.forEach((shortcut) => { - const keys = shortcut.split("+"); - const keyObj = { - cmd: false, - shift: false, - alt: false, - key: "", - click: false, - }; - keys?.forEach((key) => { - if ( - key === "cmd" || - key === "shift" || - key === "alt" || - key === "click" - ) { - keyObj[key] = true; - } else { - keyObj.key = key; - } - }); - keyObjects.push(keyObj); - }); - - const _command: TCmdkReference = JSON.parse(JSON.stringify(command)); - _command["Keyboard Shortcut"] = keyObjects; - - const groupName = _command["Group"]; - if (_cmdkRefJumpstartData[groupName] !== undefined) { - _cmdkRefJumpstartData[groupName].push(_command); - } else { - _cmdkRefJumpstartData[groupName] = [_command]; - } - if ( - groupName === "Projects" && - _cmdkRefJumpstartData["Recent"] === undefined - ) { - _cmdkRefJumpstartData["Recent"] = []; - // restore last edit session - try { - const sessionInfo = await getMany([ - "recent-project-context", - "recent-project-name", - "recent-project-handler", - ]); - if (sessionInfo[0] && sessionInfo[1] && sessionInfo[2]) { - const _session: TSession = { - "recent-project-context": sessionInfo[0], - "recent-project-name": sessionInfo[1], - "recent-project-handler": sessionInfo[2], - }; - setRecentProjectContexts(_session["recent-project-context"]); - setRecentProjectNames(_session["recent-project-name"]); - setRecentProjectHandlers(_session["recent-project-handler"]); - - for ( - let index = 0; - index < _session["recent-project-context"].length; - ++index - ) { - const _recentProjectCommand = { - Name: _session["recent-project-name"][index], - Icon: "folder", - Description: "", - "Keyboard Shortcut": [ - { - cmd: false, - shift: false, - alt: false, - key: "", - click: false, - }, - ], - Group: "Recent", - Context: index.toString(), - } as TCmdkReference; - _cmdkRefJumpstartData["Recent"].push(_recentProjectCommand); - } - LogAllow && console.log("last session loaded", _session); - } else { - LogAllow && console.log("has no last session"); - } - } catch (err) { - LogAllow && console.log("failed to load last session"); - } - } - - _cmdkReferenceData[_command["Name"]] = _command; - }), - ); - - // if (_cmdkRefJumpstartData['Recent'].length === 0) { - // delete _cmdkRefJumpstartData['Recent'] - // } - setCmdkReferenceJumpstart(_cmdkRefJumpstartData); - LogAllow && - console.log("cmdk jumpstart reference data: ", _cmdkRefJumpstartData); - - // reference-cmdk-actions - const _cmdkRefActionsData: TCmdkGroupData = {}; - cmdkRefActions.map((command: TCmdkReference) => { - const contexts: TCmdkContextScope[] = (command["Context"] as string) - ?.replace(/ /g, "") - .split(",") - .map((scope: string) => scope as TCmdkContextScope); - const contextObj: TCmdkContext = { - all: false, - file: false, - html: false, - }; - contexts?.map((context: TCmdkContextScope) => { - contextObj[context] = true; - }); - - const shortcuts: string[] = ( - command["Keyboard Shortcut"] as string - )?.split(" "); - - const keyObjects: TCmdkKeyMap[] = []; - - shortcuts?.forEach((shortcut) => { - const keys = shortcut.split("+"); - const keyObj = { - cmd: false, - shift: false, - alt: false, - key: "", - click: false, - }; - keys?.forEach((key) => { - if ( - key === "cmd" || - key === "shift" || - key === "alt" || - key === "click" - ) { - keyObj[key] = true; - } else { - keyObj.key = key; - } - }); - keyObjects.push(keyObj); - }); - - const _command: TCmdkReference = JSON.parse(JSON.stringify(command)); - _command["Context"] = contextObj; - _command["Keyboard Shortcut"] = keyObjects; - - const groupName = _command["Group"]; - if (_cmdkRefActionsData[groupName] !== undefined) { - _cmdkRefActionsData[groupName].push(_command); - } else { - _cmdkRefActionsData[groupName] = [_command]; - } - - _cmdkReferenceData[_command["Name"]] = _command; - }); - setCmdkReferenceActions(_cmdkRefActionsData); - LogAllow && - console.log("cmdk actions reference data: ", _cmdkRefActionsData); - - // set cmdk map - setCmdkReferenceData(_cmdkReferenceData); - LogAllow && console.log("cmdk map: ", _cmdkReferenceData); - - removeRunningActions([ - "detect-os", - "reference-files", - "reference-html-elements", - "reference-cmdk-jumpstart", - "reference-cmdk-actions", - ]); - })(); - }, []); - // newbie flag - useEffect(() => { - const isNewbie = localStorage.getItem("newbie"); - LogAllow && console.log("isNewbie: ", isNewbie === null ? true : false); - if (!isNewbie) { - localStorage.setItem("newbie", "false"); - // init/open Untitled project - (async () => { - dispatch(setDoingFileAction(true)); - try { - await initIDBProject(DefaultProjectPath); - await onImportProject("idb"); - LogAllow && console.log("inited/loaded Untitled project"); - } catch (err) { - LogAllow && console.log("failed to init/load Untitled project"); - } - dispatch(setDoingFileAction(false)); - })(); - } - // always show default project when do refresh - else { - onNew(); - } - - // set initial codeview height & offset - // const offsetTop = localStorage.getItem("offsetTop") - // const codeViewHeight = localStorage.getItem("codeViewHeight") - // if (offsetTop) { - // setCodeViewOffsetTop(offsetTop) - // } - // else { - // setCodeViewOffsetTop('66') - // } - }, []); - useEffect(() => { - const storedTheme = localStorage.getItem("theme"); - LogAllow && console.log("storedTheme: ", storedTheme); - if (storedTheme) { - document.documentElement.setAttribute("data-theme", storedTheme); - dispatch(setTheme(storedTheme === "dark" ? "Dark" : "Light")); - } else { - setSystemTheme(); - window - .matchMedia("(prefers-color-scheme: dark)") - .addEventListener("change", setSystemTheme); - } - - return () => - window - .matchMedia("(prefers-color-scheme: dark)") - .removeEventListener("change", setSystemTheme); - }, []); - // ***************************************** Init End ***************************************** - - // ***************************************** Process Begin ***************************************** - const firstLoaded = useRef(0); - useEffect(() => { - if (!cmdkOpen && firstLoaded.current === 2 && !showActionsPanel) { - dispatch(setShowActionsPanel(true)); - dispatch(setShowCodeView(true)); - } - ++firstLoaded.current; - }, [cmdkOpen]); - - useEffect(() => { - dispatch(setCurrentCmdkPage([...cmdkPages].pop() || "")); - }, [cmdkPages]); - - const cmdkReferenceAdd = useMemo(() => { - const data: TCmdkGroupData = { - Files: [], - Elements: [], - Recent: [], - }; - - // Files - fileCmdk({ - fileTree, - fFocusedItem, - filesRef, - data, - cmdkSearchContent, - groupName: "Add", - }); - - // Elements - elementsCmdk({ - nodeTree, - nFocusedItem, - htmlReferenceData, - data, - cmdkSearchContent, - groupName: "Add", - }); - - // Recent - delete data["Recent"]; - - return data; - }, [fileTree, fFocusedItem, nodeTree, nFocusedItem, htmlReferenceData]); - - const cmdkReferenceRename = useMemo(() => { - const data: TCmdkGroupData = { - Files: [], - Elements: [], - Recent: [], - }; - - // Elements - elementsCmdk({ - nodeTree, - nFocusedItem, - htmlReferenceData, - data, - cmdkSearchContent, - groupName: "Turn into", - }); - - // Recent - delete data["Recent"]; - - return data; - }, [fileTree, fFocusedItem, nodeTree, nFocusedItem, htmlReferenceData]); - - const cmdkReferneceRecentProject = useMemo(() => { - const _projects: TProject[] = []; - const _cmdkReferneceRecentProject: TCmdkReference[] = []; - recentProjectContexts.map((_context, index) => { - if (_context != "idb") { - _projects.push({ - context: recentProjectContexts[index], - name: recentProjectNames[index], - handler: recentProjectHandlers[index], - favicon: null, - }); - _cmdkReferneceRecentProject.push({ - Name: recentProjectNames[index], - Icon: "folder", - Description: "", - "Keyboard Shortcut": [ - { - cmd: false, - shift: false, - alt: false, - key: "", - click: false, - }, - ], - Group: "Recent", - Context: index.toString(), - }); - } - }); - setWorkspace({ name: workspace.name, projects: _projects }); - return _cmdkReferneceRecentProject; - }, [recentProjectContexts, recentProjectNames, recentProjectHandlers]); - - const KeyDownEventListener = useCallback( - (e: KeyboardEvent) => { - // cmdk obj for the current command - const cmdk: TCmdkKeyMap = { - cmd: getCommandKey(e, osType), - shift: e.shiftKey, - alt: e.altKey, - key: e.code, - click: false, - }; - if (e.key === "Escape") { - closeAllPanel(); - return; - } - if (cmdk.shift && cmdk.cmd && cmdk.key === "KeyR") { - onClear(); - } - if (cmdk.cmd && cmdk.key === "KeyG") { - e.preventDefault(); - e.stopPropagation(); - // return - } - if (cmdkOpen) return; - - // skip inline rename input in file-tree-view - const targetId = e.target && (e.target as HTMLElement).id; - if (targetId === "FileTreeView-RenameInput") { - return; - } - - // skip monaco-editor shortkeys and general coding - if (activePanel === "code") { - if (!(cmdk.cmd && !cmdk.shift && !cmdk.alt && cmdk.key === "KeyS")) { - return; - } - } - - // detect action - let action: string | null = null; - for (const actionName in cmdkReferenceData) { - const _cmdkArray = cmdkReferenceData[actionName][ - "Keyboard Shortcut" - ] as TCmdkKeyMap[]; - - let matched = false; - - for (const keyObj of _cmdkArray) { - const key = - keyObj.key.length === 0 - ? "" - : keyObj.key === "\\" - ? "Backslash" - : (keyObj.key.length === 1 ? "Key" : "") + - keyObj.key[0].toUpperCase() + - keyObj.key.slice(1); - - if ( - cmdk.cmd === keyObj.cmd && - cmdk.shift === keyObj.shift && - cmdk.alt === keyObj.alt && - cmdk.key === key - ) { - action = actionName; - matched = true; - break; // Match found, exit the inner loop - } - } - - if (matched) { - break; // Match found, exit the outer loop - } - } - if (action === null) return; - - LogAllow && console.log("action to be run by cmdk: ", action); - - // prevent chrome default short keys - if ( - action === "Save" || - action === "Download" || - action === "Duplicate" - ) { - e.preventDefault(); - } - - dispatch(setCurrentCommand({ action })); - }, - [cmdkOpen, cmdkReferenceData, activePanel, osType], - ); - - useEffect(() => { - document.addEventListener("keydown", KeyDownEventListener); - return () => document.removeEventListener("keydown", KeyDownEventListener); - }, [KeyDownEventListener]); - - useEffect(() => { - if (!currentCommand) return; - - switch (currentCommand.action) { - case "Jumpstart": - onJumpstart(); - break; - case "New": - onNew(); - toogleCodeView(); - !showActionsPanel && dispatch(setShowActionsPanel(true)); - break; - case "Open": - onOpen(); - break; - case "Theme": - onToggleTheme(); - break; - case "Clear": - onClear(); - break; - case "Undo": - onUndo(); - break; - case "Redo": - onRedo(); - break; - case "Actions": - onActions(); - break; - case "Add": - onAdd(); - break; - case "Code": - toogleCodeView(); - break; - case "Design": - toogleActionsPanel(); - break; - case "Guide": - openGuidePage(); - break; - case "Download": - onDownload(); - break; - case "Turn into": - onTurnInto(); - break; - case "Autosave": - onToggleAutoSave(); - break; - default: - return; - } - }, [currentCommand]); + // custom hooks + const { addRunningActions, removeRunningActions } = useRunningActions(); + const { + projectHandlers, + setProjectHandlers, + currentProjectFileHandle, + setCurrentProjectFileHandle, + fileHandlers, + setFileHandlers, + } = useFileHandlers(); + const { + recentProjectContexts, + recentProjectNames, + recentProjectHandlers, + setRecentProjectContexts, + setRecentProjectNames, + setRecentProjectHandlers, + } = useRecentProjects(); + const { filesReferenceData, htmlReferenceData } = useReferenceData(); + const { + cmdkReferenceData, + cmdkReferenceJumpstart, + cmdkReferenceActions, + cmdkReferneceRecentProject, + cmdkReferenceAdd, + cmdkReferenceRename, + } = useCmdkReferenceData({ + addRunningActions, + removeRunningActions, + + recentProjectContexts, + recentProjectNames, + recentProjectHandlers, + setRecentProjectContexts, + setRecentProjectNames, + setRecentProjectHandlers, + + htmlReferenceData, + }); + const { importProject, closeNavigator } = useHandlers({ + setCurrentProjectFileHandle, + setFileHandlers, + + recentProjectContexts, + recentProjectNames, + recentProjectHandlers, + setRecentProjectContexts, + setRecentProjectNames, + setRecentProjectHandlers, + }); + const { onJumpstart, onNew, onClear, onUndo, onRedo } = useCmdk({ + cmdkReferenceData, + importProject, + }); + useInit({ importProject, onNew }); + const { + monacoEditorRef, + setMonacoEditorRef, + iframeRefRef, + setIframeRefRef, + isContentProgrammaticallyChanged, + setIsContentProgrammaticallyChanged, + } = useReferneces(); + const { validMenuItemCount, hoveredMenuItemDescription } = useCmdkModal(); + const { + actionsPanelOffsetTop, + actionsPanelOffsetLeft, + actionsPanelWidth, + codeViewOffsetTop, + codeViewOffsetBottom, + codeViewOffsetLeft, + codeViewHeight, + codeViewDragging, + dragCodeView, + dragEndCodeView, + dropCodeView, + } = usePanels(); + + // file event hms useEffect(() => { // reset fileAction in the new history fileEventFutureLength === 0 && @@ -840,462 +151,18 @@ export default function MainPage() { dispatch(setFileAction({ type: null })); }, [fileEventFutureLength]); - // ***************************************** Process End ***************************************** - - // ***************************************** Handler Begin ***************************************** - const closeAllPanel = useCallback(() => { - dispatch(setShowActionsPanel(false)); - dispatch(setShowCodeView(false)); - }, []); - - const onClear = useCallback(async () => { - window.localStorage.clear(); - await delMany([ - "recent-project-context", - "recent-project-name", - "recent-project-handler", - ]); - }, []); - - const importProject = useCallback( - async ( - fsType: TProjectContext, - projectHandle?: FileSystemDirectoryHandle | null, - ) => { - if (fsType === "local") { - dispatch(setDoingFileAction(true)); - try { - const { - handlerArr, - _fileHandlers, - - _fileTree, - _initialFileUidToOpen, - } = await loadLocalProject( - projectHandle as FileSystemDirectoryHandle, - osType, - ); - - clearProjectSession(dispatch); - - // build nohost idb - handlerArr && buildNohostIDB(handlerArr); - - dispatch( - setProject({ - context: "local", - name: (projectHandle as FileSystemDirectoryHandle).name, - handler: null, - favicon: null, - }), - ); - setCurrentProjectFileHandle( - projectHandle as FileSystemDirectoryHandle, - ); - - dispatch(setFileTree(_fileTree)); - dispatch(setInitialFileUidToOpen(_initialFileUidToOpen)); - setFileHandlers(_fileHandlers as TFileHandlerCollection); - - if (_initialFileUidToOpen === "") { - dispatch(setShowActionsPanel(false)); - dispatch(setNavigatorDropdownType(null)); - } - - await saveRecentProject( - fsType, - projectHandle as FileSystemDirectoryHandle, - ); - } catch (err) { - LogAllow && console.log("ERROR while importing local project", err); - } - dispatch(setDoingFileAction(false)); - } else if (fsType === "idb") { - dispatch(setDoingFileAction(true)); - try { - const { _fileTree, _initialFileUidToOpen } = - await loadIDBProject(DefaultProjectPath); - - clearProjectSession(dispatch); - - dispatch( - setProject({ - context: "idb", - name: "Welcome", - handler: null, - favicon: null, - }), - ); - setCurrentProjectFileHandle(null); - - dispatch(setFileTree(_fileTree)); - dispatch(setInitialFileUidToOpen(_initialFileUidToOpen)); - setFileHandlers({}); - - // await saveRecentProject(fsType); - } catch (err) { - LogAllow && console.log("ERROR while importing IDB project", err); - } - dispatch(setDoingFileAction(false)); - } - }, - [ - osType, - recentProjectContexts, - recentProjectNames, - recentProjectHandlers, - fileTree, - ], - ); - const saveRecentProject = useCallback( - async ( - fsType: TProjectContext, - projectHandle: FileSystemDirectoryHandle, - ) => { - const _recentProjectContexts = [...recentProjectContexts]; - const _recentProjectNames = [...recentProjectNames]; - const _recentProjectHandlers = [...recentProjectHandlers]; - for (let index = 0; index < _recentProjectContexts.length; ++index) { - if ( - _recentProjectContexts[index] === fsType && - projectHandle?.name === _recentProjectNames[index] - ) { - _recentProjectContexts.splice(index, 1); - _recentProjectNames.splice(index, 1); - _recentProjectHandlers.splice(index, 1); - break; - } - } - if (_recentProjectContexts.length === RecentProjectCount) { - _recentProjectContexts.pop(); - _recentProjectNames.pop(); - _recentProjectHandlers.pop(); - } - _recentProjectContexts.unshift(fsType); - _recentProjectNames.unshift( - (projectHandle as FileSystemDirectoryHandle).name, - ); - _recentProjectHandlers.unshift( - projectHandle as FileSystemDirectoryHandle, - ); - setRecentProjectContexts(_recentProjectContexts); - setRecentProjectNames(_recentProjectNames); - setRecentProjectHandlers(_recentProjectHandlers); - await setMany([ - ["recent-project-context", _recentProjectContexts], - ["recent-project-name", _recentProjectNames], - ["recent-project-handler", _recentProjectHandlers], - ]); - }, - [recentProjectContexts, recentProjectNames, recentProjectHandlers], - ); - const onImportProject = useCallback( - async (fsType: TProjectContext = "local"): Promise => { - return new Promise(async (resolve, reject) => { - if (fsType === "local") { - try { - const projectHandle = await showDirectoryPicker({ - _preferPolyfill: false, - mode: "readwrite", - } as CustomDirectoryPickerOptions); - await importProject(fsType, projectHandle); - } catch (err) { - reject(err); - } - } else if (fsType === "idb") { - try { - await importProject(fsType, null); - } catch (err) { - reject(err); - } - } - resolve(); - }); - }, - [importProject], - ); - const onNew = useCallback(async () => { - confirmFileChanges(fileTree); - - dispatch(setDoingFileAction(true)); - try { - await initIDBProject(DefaultProjectPath); - await onImportProject("idb"); - } catch (err) { - LogAllow && console.log("failed to init/load Untitled project"); - } - dispatch(setDoingFileAction(false)); - }, [onImportProject, fileTree]); - const onOpen = useCallback(async () => { - confirmFileChanges(fileTree); - - dispatch(setDoingFileAction(true)); - try { - await onImportProject(); - } catch (err) { - LogAllow && console.log("failed to open project"); - } - dispatch(setDoingFileAction(false)); - }, [onImportProject, fileTree]); - - const onActions = useCallback(() => { - if (cmdkOpen) return; - dispatch(setCmdkPages(["Actions"])); - dispatch(setCmdkOpen(true)); - }, [cmdkOpen]); - - const onAdd = useCallback(() => { - dispatch(setCmdkPages([...cmdkPages, "Add"])); - dispatch(setCmdkOpen(true)); - }, [cmdkPages]); - - const onTurnInto = useCallback(() => { - dispatch(setCmdkPages([...cmdkPages, "Turn into"])); - dispatch(setCmdkOpen(true)); - }, [cmdkPages]); - - const onDownload = useCallback(async () => { - if (project.context !== "idb") return; - - try { - await downloadProject(DefaultProjectPath); - } catch (err) { - LogAllow && console.log("failed to download project"); - } - }, [project.context]); - - const onJumpstart = useCallback(() => { - if (cmdkOpen) return; - dispatch(setCmdkPages(["Jumpstart"])); - dispatch(setCmdkOpen(true)); - }, [cmdkOpen]); - - // History - Undo/Redo - const onUndo = useCallback(() => { - if (doingAction || doingFileAction || iframeLoading) return; - - if (activePanel === "file") { - if (fileEventPastLength === 1) { - LogAllow && console.log("Undo - FileTree - it is the origin state"); - return; - } - - dispatch({ type: FileTree_Event_UndoActionType }); - } else { - if (nodeEventPastLength === 1) { - LogAllow && console.log("Undo - NodeTree - it is the origin state"); - return; - } - - dispatch({ type: NodeTree_Event_UndoActionType }); - } - - dispatch(setDidUndo(true)); - }, [ - doingAction, - doingFileAction, - iframeLoading, - - activePanel, - - fileEventPastLength, - nodeEventPastLength, - ]); - const onRedo = useCallback(() => { - if (doingAction || doingFileAction || iframeLoading) return; - - if (activePanel === "file") { - if (fileEventFutureLength === 0) { - LogAllow && console.log("Redo - FileTree - it is the latest state"); - return; - } - - dispatch({ type: FileTree_Event_RedoActionType }); - } else { - if (nodeEventFutureLength === 0) { - LogAllow && console.log("Redo - NodeTree - it is the latest state"); - return; - } - - dispatch({ type: NodeTree_Event_RedoActionType }); - } - - dispatch(setDidRedo(true)); - }, [ - doingAction, - doingFileAction, - iframeLoading, - - activePanel, - - fileEventFutureLength, - nodeEventFutureLength, - ]); - - // toogle code view - const toogleCodeView = useCallback(() => { - dispatch(setShowCodeView(!showCodeView)); - }, [showCodeView]); - - // toogle actions panel - const toogleActionsPanel = useCallback(() => { - dispatch(setShowActionsPanel(!showActionsPanel)); - }, [showActionsPanel]); - - // open guide page - const openGuidePage = useCallback(() => { - window.open("https://guide.rnbw.dev", "_blank", "noreferrer"); - }, [currentCommand]); - - // -------------------------------------------------------------- pos/size for panels -------------------------------------------------------------- - const [actionsPanelOffsetTop, setActionsPanelOffsetTop] = useState(12); - const [actionsPanelOffsetLeft, setActionsPanelOffsetLeft] = useState(12); - const [actionsPanelWidth, setActionsPanelWidth] = useState(240); - - const [codeViewOffsetBottom, setCodeViewOffsetBottom] = useState("12"); - const [codeViewOffsetTop, setCodeViewOffsetTop] = - useState("calc(60vh - 12px)"); - const [codeViewOffsetLeft, setCodeViewOffsetLeft] = useState(12); - const [codeViewHeight, setCodeViewHeight] = useState("40"); - const [codeViewDragging, setCodeViewDragging] = useState(false); - // -------------------------------------------------------------- other -------------------------------------------------------------- - useEffect(() => { - // wait until "cmdkReferenceJumpstart" is ready - Object.keys(cmdkReferenceJumpstart).length !== 0 && onJumpstart(); - }, [cmdkReferenceJumpstart]); - - // theme - const setSystemTheme = useCallback(() => { - dispatch(setTheme("System")); - if ( - window.matchMedia && - window.matchMedia("(prefers-color-scheme: dark)").matches - ) { - document.documentElement.setAttribute("data-theme", "dark"); - } else { - document.documentElement.setAttribute("data-theme", "light"); - } - }, []); - const onToggleTheme = useCallback(() => { - switch (theme) { - case "System": - document.documentElement.setAttribute("data-theme", "light"); - localStorage.setItem("theme", "light"); - dispatch(setTheme("Light")); - break; - case "Light": - document.documentElement.setAttribute("data-theme", "dark"); - localStorage.setItem("theme", "dark"); - dispatch(setTheme("Dark")); - break; - case "Dark": - document.documentElement.removeAttribute("data-theme"); - localStorage.removeItem("theme"); - setSystemTheme(); - break; - default: - break; - } - }, [theme]); - - const [autoSave, setAutoSave] = useState(false); - const onToggleAutoSave = () => { - setAutoSave(!autoSave); - }; - // web-tab close event handler useEffect(() => { - let changed = false; - - const uids = Object.keys(fileTree); - for (const uid of uids) { - const node = fileTree[uid]; - const nodeData = node.data as TFileNodeData; - - if (nodeData.changed) { - changed = true; - break; - } - } - - window.onbeforeunload = changed - ? () => { - return "changed"; - } - : null; - + window.onbeforeunload = isUnsavedProject(fileTree) ? () => "changed" : null; return () => { window.onbeforeunload = null; }; }, [fileTree]); - // cmdk modal handle - const [hoveredMenuItemDescription, setHoverMenuItemDescription] = useState< - string | null | undefined - >(); - - const [validMenuItemCount, setValidMenuItemCount] = useState(); - + // open Jumpstart menu on startup useEffect(() => { - let hoveredMenuItemDetecter: NodeJS.Timeout; - if (cmdkOpen) { - // detect hovered menu item in cmdk modal if its open - hoveredMenuItemDetecter = setInterval(() => { - const menuItems = document.querySelectorAll(".rnbw-cmdk-menu-item"); - setValidMenuItemCount(menuItems.length); - - const description = - currentCmdkPage === "Add" || - currentCmdkPage === "Turn into" || - currentCmdkPage === "Jumpstart" - ? document - .querySelector('.rnbw-cmdk-menu-item[aria-selected="true"]') - ?.getAttribute("rnbw-cmdk-menu-item-description") - : ""; - setHoverMenuItemDescription(description); - }, 10); - } else { - // clear cmdk pages and search text when close the modal - dispatch(setCmdkPages([])); - dispatch(setCmdkSearchContent("")); - // setValidMenuItemCount(undefined) - } - - return () => clearInterval(hoveredMenuItemDetecter); - }, [cmdkOpen]); - - // drag & dragend code view event - const dragCodeView = useCallback((e: React.DragEvent) => { - e.preventDefault(); - setCodeViewOffsetTop( - ((e.clientY / document.documentElement.clientHeight) * 100 < 1 - ? 1 - : (e.clientY / document.documentElement.clientHeight) * 100 - ).toString(), - ); - if (!codeViewDragging) { - setCodeViewDragging(true); - } - }, []); - const dragEndCodeView = useCallback((e: React.DragEvent) => { - e.preventDefault(); - const offsetTop = ( - (e.clientY / document.documentElement.clientHeight) * 100 < 1 - ? 1 - : (e.clientY / document.documentElement.clientHeight) * 100 - ).toString(); - setCodeViewOffsetTop(offsetTop); - setCodeViewDragging(false); - localStorage.setItem("offsetTop", offsetTop); - }, []); - const dropCodeView = useCallback((e: React.DragEvent) => { - e.preventDefault(); - }, []); - - // close navigator - const onCloseDropDown = useCallback(() => { - navigatorDropdownType !== null && dispatch(setNavigatorDropdownType(null)); - }, [navigatorDropdownType]); + Object.keys(cmdkReferenceJumpstart).length !== 0 && onJumpstart(); + }, [cmdkReferenceJumpstart]); return ( <> @@ -1308,29 +175,21 @@ export default function MainPage() { htmlReferenceData, cmdkReferenceData, + projectHandlers, + setProjectHandlers, currentProjectFileHandle, setCurrentProjectFileHandle, - fileHandlers, setFileHandlers, - // code view - isContentProgrammaticallyChanged, - setIsContentProgrammaticallyChanged, - setCodeViewOffsetTop, - - // load project - importProject, - - // close all panel - closeAllPanel, monacoEditorRef, setMonacoEditorRef, - iframeRefRef, setIframeRefRef, + isContentProgrammaticallyChanged, + setIsContentProgrammaticallyChanged, - //undo/Redo + importProject, onUndo, onRedo, }} @@ -1340,7 +199,7 @@ export default function MainPage() { id="MainPage" className={"view background-primary"} style={{ display: "relative" }} - onClick={onCloseDropDown} + onClick={closeNavigator} > { - let groupNameShow: boolean = false; + let groupNameShow = false; (currentCmdkPage === "Jumpstart" ? groupName !== "Recent" ? cmdkReferenceJumpstart[groupName] @@ -1488,8 +347,7 @@ export default function MainPage() { ? cmdkReferenceAdd[groupName] : [] ).map((command: TCmdkReference) => { - const context: TCmdkContext = - command.Context as TCmdkContext; + const context = command.Context as TCmdkContext; groupNameShow = currentCmdkPage === "Jumpstart" || (currentCmdkPage === "Actions" && @@ -1537,7 +395,7 @@ export default function MainPage() { )?.map((command: TCmdkReference, index) => { const context: TCmdkContext = command.Context as TCmdkContext; - const show: boolean = + const show = currentCmdkPage === "Jumpstart" || (currentCmdkPage === "Actions" && (command.Name === "Add" || @@ -1576,7 +434,9 @@ export default function MainPage() { dispatch(setCmdkOpen(false)); if (command.Name === "Guide") { - guideRef.current?.click(); + dispatch( + setCurrentCommand({ action: "Guide" }), + ); } else if (command.Group === "Add") { dispatch( setCurrentCommand({ @@ -1598,25 +458,12 @@ export default function MainPage() { recentProjectContexts[index]; const projectHandler = recentProjectHandlers[index]; - if (fileTree) { - // confirm files' changes - let hasChangedFile = false; - for (let x in fileTree) { - const _file = fileTree[x]; - const _fileData = - _file.data as TFileNodeData; - if (_file && _fileData.changed) { - hasChangedFile = true; - } - } - if (hasChangedFile) { - const message = `Your changes will be lost if you don't save them. Are you sure you want to continue without saving?`; - if (!window.confirm(message)) { - return; - } - } - } - importProject(projectContext, projectHandler); + + confirmFileChanges(fileTree) && + importProject( + projectContext, + projectHandler, + ); } else if ( (currentCmdkPage === "Add" && command.Group === "Recent") || @@ -1632,10 +479,10 @@ export default function MainPage() { >
- {/* detect Theme Group and render check boxes */} {currentCmdkPage === "Jumpstart" && command.Name === "Theme" ? ( <> + {/* detect Theme Group and render check boxes */}
@@ -1651,6 +498,7 @@ export default function MainPage() { ) : command.Name === "Autosave" ? ( <> + {/* detect Autosave */}
@@ -1732,13 +580,7 @@ export default function MainPage() {
- {/* Guide link */} - + {/* description - right panel */} {(currentCmdkPage === "Add" || currentCmdkPage === "Jumpstart") && false && ( diff --git a/src/pages/main/helper.ts b/src/pages/main/helper.ts index 1a9295ca..fd27ef69 100644 --- a/src/pages/main/helper.ts +++ b/src/pages/main/helper.ts @@ -23,7 +23,12 @@ import { NodeTree_Event_JumpToPastActionType, } from "@_redux/main/nodeTree/event"; import { setIframeSrc } from "@_redux/main/stageView"; -import { TCmdkReferenceData, TFilesReference } from "@_types/main"; +import { + TCmdkKeyMap, + TCmdkReference, + TCmdkReferenceData, + TFilesReference, +} from "@_types/main"; import { AnyAction } from "@reduxjs/toolkit"; export const addDefaultCmdkActions = ( @@ -98,13 +103,11 @@ export const addDefaultCmdkActions = ( Context: "all", }; }; - export const clearProjectSession = (dispatch: Dispatch) => { dispatch( setProject({ context: "idb", name: "", - handler: null, favicon: null, }), ); @@ -118,7 +121,6 @@ export const clearProjectSession = (dispatch: Dispatch) => { clearFileSession(dispatch); }; - export const clearFileSession = (dispatch: Dispatch) => { dispatch(setNodeTree({})); dispatch(setValidNodeTree({})); @@ -128,7 +130,72 @@ export const clearFileSession = (dispatch: Dispatch) => { dispatch(setIframeSrc(null)); }; - +export const getKeyObjectsFromCommand = (command: TCmdkReference) => { + const shortcuts = (command["Keyboard Shortcut"] as string)?.split(" "); + const keyObjects: TCmdkKeyMap[] = []; + shortcuts?.forEach((shortcut) => { + const keys = shortcut.split("+"); + const keyObj = { + cmd: false, + shift: false, + alt: false, + key: "", + click: false, + }; + keys?.forEach((key) => { + if ( + key === "cmd" || + key === "shift" || + key === "alt" || + key === "click" + ) { + keyObj[key] = true; + } else { + keyObj.key = key; + } + }); + keyObjects.push(keyObj); + }); + return keyObjects; +}; +export const fileCmdk = ({ + fileTree, + fFocusedItem, + filesRef, + data, + cmdkSearchContent, + groupName, +}: any) => { + const fileNode = fileTree[fFocusedItem]; + if (fileNode) { + filesRef.map((fileRef: TFilesReference) => { + fileRef.Name && + data["Files"].push({ + Featured: fileRef.Featured === "Yes", + Name: fileRef.Name, + Icon: fileRef.Icon, + Description: fileRef.Description, + "Keyboard Shortcut": [ + { + cmd: false, + shift: false, + alt: false, + key: "", + click: false, + }, + ], + Group: groupName, + Context: `File-${fileRef.Extension}`, + }); + }); + } + data["Files"] = data["Files"].filter( + (element: any) => element.Featured || !!cmdkSearchContent, + ); + if (data["Files"].length === 0) { + delete data["Files"]; + } +}; export const elementsCmdk = ({ nodeTree, nFocusedItem, @@ -239,46 +306,6 @@ export const elementsCmdk = ({ delete data["Elements"]; } }; - -export const fileCmdk = ({ - fileTree, - fFocusedItem, - filesRef, - data, - cmdkSearchContent, - groupName, -}: any) => { - const fileNode = fileTree[fFocusedItem]; - if (fileNode) { - filesRef.map((fileRef: TFilesReference) => { - fileRef.Name && - data["Files"].push({ - Featured: fileRef.Featured === "Yes", - Name: fileRef.Name, - Icon: fileRef.Icon, - Description: fileRef.Description, - "Keyboard Shortcut": [ - { - cmd: false, - shift: false, - alt: false, - key: "", - click: false, - }, - ], - Group: groupName, - Context: `File-${fileRef.Extension}`, - }); - }); - } - data["Files"] = data["Files"].filter( - (element: any) => element.Featured || !!cmdkSearchContent, - ); - if (data["Files"].length === 0) { - delete data["Files"]; - } -}; - export const scrollToElement = (element: Element, behavior: ScrollBehavior) => { element.scrollIntoView({ block: "nearest", @@ -286,3 +313,13 @@ export const scrollToElement = (element: Element, behavior: ScrollBehavior) => { behavior, }); }; +export const setSystemTheme = () => { + if ( + window.matchMedia && + window.matchMedia("(prefers-color-scheme: dark)").matches + ) { + document.documentElement.setAttribute("data-theme", "dark"); + } else { + document.documentElement.setAttribute("data-theme", "light"); + } +}; diff --git a/src/pages/main/hooks/index.ts b/src/pages/main/hooks/index.ts new file mode 100644 index 00000000..f6f87e57 --- /dev/null +++ b/src/pages/main/hooks/index.ts @@ -0,0 +1,11 @@ +export * from "./useCmdk"; +export * from "./useCmdkModal"; +export * from "./useCmdkReferenceData"; +export * from "./useFileHandlers"; +export * from "./useHandlers"; +export * from "./useInit"; +export * from "./usePanels"; +export * from "./useRecentProjects"; +export * from "./useReferenceData"; +export * from "./useReferences"; +export * from "./useRunningActions"; diff --git a/src/pages/main/hooks/useCmdk.ts b/src/pages/main/hooks/useCmdk.ts new file mode 100644 index 00000000..1fe15b7c --- /dev/null +++ b/src/pages/main/hooks/useCmdk.ts @@ -0,0 +1,377 @@ +import { useCallback, useEffect } from "react"; + +import { CustomDirectoryPickerOptions } from "file-system-access/lib/showDirectoryPicker"; +import { delMany } from "idb-keyval"; +import { useDispatch } from "react-redux"; + +import { LogAllow } from "@_constants/global"; +import { DefaultProjectPath } from "@_constants/main"; +import { + confirmFileChanges, + downloadIDBProject, + initIDBProject, +} from "@_node/index"; +import { setTheme } from "@_redux/global"; +import { + setCmdkOpen, + setCmdkPages, + setCurrentCommand, +} from "@_redux/main/cmdk"; +import { + FileTree_Event_RedoActionType, + FileTree_Event_UndoActionType, + TProjectContext, + setDoingFileAction, +} from "@_redux/main/fileTree"; +import { + NodeTree_Event_RedoActionType, + NodeTree_Event_UndoActionType, +} from "@_redux/main/nodeTree"; +import { + setAutoSave, + setDidRedo, + setDidUndo, + setShowActionsPanel, + setShowCodeView, +} from "@_redux/main/processor"; +import { useAppState } from "@_redux/useAppState"; +import { getCommandKey } from "@_services/global"; +import { TCmdkKeyMap, TCmdkReferenceData } from "@_types/main"; + +import { setSystemTheme } from "../helper"; + +interface IUseCmdk { + cmdkReferenceData: TCmdkReferenceData; + importProject: ( + fsType: TProjectContext, + projectHandle?: FileSystemDirectoryHandle | null | undefined, + ) => Promise; +} +export const useCmdk = ({ cmdkReferenceData, importProject }: IUseCmdk) => { + const dispatch = useDispatch(); + const { + osType, + theme, + project, + fileTree, + doingFileAction, + fileEventPastLength, + fileEventFutureLength, + nodeEventPastLength, + nodeEventFutureLength, + iframeLoading, + doingAction, + activePanel, + showActionsPanel, + showCodeView, + autoSave, + cmdkOpen, + cmdkPages, + currentCommand, + } = useAppState(); + + const onClear = useCallback(async () => { + window.localStorage.clear(); + await delMany([ + "recent-project-context", + "recent-project-name", + "recent-project-handler", + ]); + }, []); + const onJumpstart = useCallback(() => { + if (cmdkOpen) return; + dispatch(setCmdkPages(["Jumpstart"])); + dispatch(setCmdkOpen(true)); + }, [cmdkOpen]); + const onNew = useCallback(async () => { + if (!confirmFileChanges(fileTree)) return; + + dispatch(setDoingFileAction(true)); + try { + await initIDBProject(DefaultProjectPath); + await importProject("idb"); + } catch (err) { + LogAllow && console.log("failed to init/load idb project"); + } + dispatch(setDoingFileAction(false)); + }, [fileTree, importProject]); + const onOpen = useCallback(async () => { + if (!confirmFileChanges(fileTree)) return; + + dispatch(setDoingFileAction(true)); + try { + const projectHandle = await showDirectoryPicker({ + _preferPolyfill: false, + mode: "readwrite", + } as CustomDirectoryPickerOptions); + await importProject("local", projectHandle); + } catch (err) { + LogAllow && console.log("failed to open local project"); + } + dispatch(setDoingFileAction(false)); + }, [importProject, fileTree]); + const onActions = useCallback(() => { + if (cmdkOpen) return; + dispatch(setCmdkPages(["Actions"])); + dispatch(setCmdkOpen(true)); + }, [cmdkOpen]); + const onAdd = useCallback(() => { + dispatch(setCmdkPages([...cmdkPages, "Add"])); + dispatch(setCmdkOpen(true)); + }, [cmdkPages]); + const onTurnInto = useCallback(() => { + dispatch(setCmdkPages([...cmdkPages, "Turn into"])); + dispatch(setCmdkOpen(true)); + }, [cmdkPages]); + const onDownload = useCallback(async () => { + if (project.context !== "idb") return; + try { + await downloadIDBProject(DefaultProjectPath); + } catch (err) { + LogAllow && console.log("failed to download project"); + } + }, [project]); + const onUndo = useCallback(() => { + if (doingAction || doingFileAction || iframeLoading) return; + + if (activePanel === "file") { + if (fileEventPastLength === 1) { + LogAllow && console.log("Undo - FileTree - it is the origin state"); + return; + } + dispatch({ type: FileTree_Event_UndoActionType }); + } else { + if (nodeEventPastLength === 1) { + LogAllow && console.log("Undo - NodeTree - it is the origin state"); + return; + } + dispatch({ type: NodeTree_Event_UndoActionType }); + } + + dispatch(setDidUndo(true)); + }, [ + doingAction, + doingFileAction, + iframeLoading, + activePanel, + fileEventPastLength, + nodeEventPastLength, + ]); + const onRedo = useCallback(() => { + if (doingAction || doingFileAction || iframeLoading) return; + + if (activePanel === "file") { + if (fileEventFutureLength === 0) { + LogAllow && console.log("Redo - FileTree - it is the latest state"); + return; + } + dispatch({ type: FileTree_Event_RedoActionType }); + } else { + if (nodeEventFutureLength === 0) { + LogAllow && console.log("Redo - NodeTree - it is the latest state"); + return; + } + dispatch({ type: NodeTree_Event_RedoActionType }); + } + + dispatch(setDidRedo(true)); + }, [ + doingAction, + doingFileAction, + iframeLoading, + activePanel, + fileEventFutureLength, + nodeEventFutureLength, + ]); + const onToogleCodeView = useCallback(() => { + dispatch(setShowCodeView(!showCodeView)); + }, [showCodeView]); + const onToogleActionsPanel = useCallback(() => { + dispatch(setShowActionsPanel(!showActionsPanel)); + }, [showActionsPanel]); + const onOpenGuidePage = useCallback(() => { + window.open("https://guide.rnbw.dev", "_blank", "noreferrer"); + }, []); + const onToggleTheme = useCallback(() => { + switch (theme) { + case "System": + document.documentElement.setAttribute("data-theme", "light"); + localStorage.setItem("theme", "light"); + dispatch(setTheme("Light")); + break; + case "Light": + document.documentElement.setAttribute("data-theme", "dark"); + localStorage.setItem("theme", "dark"); + dispatch(setTheme("Dark")); + break; + case "Dark": + localStorage.removeItem("theme"); + dispatch(setTheme("System")); + setSystemTheme(); + break; + default: + break; + } + }, [theme]); + const onToggleAutoSave = useCallback(() => { + dispatch(setAutoSave(!autoSave)); + }, [autoSave]); + + const closeAllPanel = useCallback(() => { + dispatch(setShowActionsPanel(false)); + dispatch(setShowCodeView(false)); + }, []); + + const KeyDownEventListener = useCallback( + (e: KeyboardEvent) => { + // cmdk obj for the current command + const cmdk: TCmdkKeyMap = { + cmd: getCommandKey(e, osType), + shift: e.shiftKey, + alt: e.altKey, + key: e.code, + click: false, + }; + + if (e.key === "Escape") { + closeAllPanel(); + return; + } + if (cmdk.cmd && cmdk.shift && cmdk.key === "KeyR") { + onClear(); + } + if (cmdk.cmd && cmdk.key === "KeyG") { + e.preventDefault(); + e.stopPropagation(); + } + if (cmdkOpen) return; + + // skip inline rename input in file-tree-view + const targetId = e.target && (e.target as HTMLElement).id; + if (targetId === "FileTreeView-RenameInput") { + return; + } + + // skip monaco-editor shortkeys and general coding + if (activePanel === "code") { + if (!(cmdk.cmd && !cmdk.shift && !cmdk.alt && cmdk.key === "KeyS")) { + return; + } + } + + // detect action + let action: string | null = null; + for (const actionName in cmdkReferenceData) { + const _cmdkArray = cmdkReferenceData[actionName][ + "Keyboard Shortcut" + ] as TCmdkKeyMap[]; + + let matched = false; + + for (const keyObj of _cmdkArray) { + const key = + keyObj.key.length === 0 + ? "" + : keyObj.key === "\\" + ? "Backslash" + : (keyObj.key.length === 1 ? "Key" : "") + + keyObj.key[0].toUpperCase() + + keyObj.key.slice(1); + + if ( + cmdk.cmd === keyObj.cmd && + cmdk.shift === keyObj.shift && + cmdk.alt === keyObj.alt && + cmdk.key === key + ) { + action = actionName; + matched = true; + break; // Match found, exit the inner loop + } + } + + if (matched) { + break; // Match found, exit the outer loop + } + } + if (action === null) return; + + LogAllow && console.log("action to be run by cmdk: ", action); + + // prevent chrome default short keys + if ( + action === "Save" || + action === "Download" || + action === "Duplicate" || + action === "Group" || + action === "UnGroup" + ) { + e.preventDefault(); + } + + dispatch(setCurrentCommand({ action })); + }, + [cmdkOpen, cmdkReferenceData, activePanel, osType], + ); + useEffect(() => { + document.addEventListener("keydown", KeyDownEventListener); + return () => document.removeEventListener("keydown", KeyDownEventListener); + }, [KeyDownEventListener]); + useEffect(() => { + if (!currentCommand) return; + + switch (currentCommand.action) { + case "Jumpstart": + onJumpstart(); + break; + case "New": + onNew(); + dispatch(setShowActionsPanel(true)); + dispatch(setShowCodeView(true)); + break; + case "Open": + onOpen(); + break; + case "Theme": + onToggleTheme(); + break; + case "Clear": + onClear(); + break; + case "Undo": + onUndo(); + break; + case "Redo": + onRedo(); + break; + case "Actions": + onActions(); + break; + case "Add": + onAdd(); + break; + case "Code": + onToogleCodeView(); + break; + case "Design": + onToogleActionsPanel(); + break; + case "Guide": + onOpenGuidePage(); + break; + case "Download": + onDownload(); + break; + case "Turn into": + onTurnInto(); + break; + case "Autosave": + onToggleAutoSave(); + break; + default: + return; + } + }, [currentCommand]); + + return { onJumpstart, onNew, onClear, onUndo, onRedo }; +}; diff --git a/src/pages/main/hooks/useCmdkModal.ts b/src/pages/main/hooks/useCmdkModal.ts new file mode 100644 index 00000000..a5df03a6 --- /dev/null +++ b/src/pages/main/hooks/useCmdkModal.ts @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react"; + +import { useDispatch } from "react-redux"; + +import { + setCmdkPages, + setCmdkSearchContent, + setCurrentCmdkPage, +} from "@_redux/main/cmdk"; +import { useAppState } from "@_redux/useAppState"; + +export const useCmdkModal = () => { + const dispatch = useDispatch(); + const { cmdkOpen, cmdkPages, currentCmdkPage } = useAppState(); + + useEffect(() => { + dispatch(setCurrentCmdkPage([...cmdkPages].pop() || "")); + }, [cmdkPages]); + + const [validMenuItemCount, setValidMenuItemCount] = useState(); + const [hoveredMenuItemDescription, setHoverMenuItemDescription] = useState< + string | null | undefined + >(); + useEffect(() => { + let hoveredMenuItemDetecter: NodeJS.Timeout; + if (cmdkOpen) { + // detect hovered menu item in cmdk modal if its open + hoveredMenuItemDetecter = setInterval(() => { + const menuItems = document.querySelectorAll(".rnbw-cmdk-menu-item"); + setValidMenuItemCount(menuItems.length); + + const description = + currentCmdkPage === "Add" || + currentCmdkPage === "Turn into" || + currentCmdkPage === "Jumpstart" + ? document + .querySelector('.rnbw-cmdk-menu-item[aria-selected="true"]') + ?.getAttribute("rnbw-cmdk-menu-item-description") + : ""; + setHoverMenuItemDescription(description); + }, 10); + } else { + // clear cmdk pages and search text when close the modal + dispatch(setCmdkPages([])); + dispatch(setCmdkSearchContent("")); + // setValidMenuItemCount(undefined) + } + + return () => clearInterval(hoveredMenuItemDetecter); + }, [cmdkOpen]); + + return { + validMenuItemCount, + hoveredMenuItemDescription, + }; +}; diff --git a/src/pages/main/hooks/useCmdkReferenceData.ts b/src/pages/main/hooks/useCmdkReferenceData.ts new file mode 100644 index 00000000..e620019b --- /dev/null +++ b/src/pages/main/hooks/useCmdkReferenceData.ts @@ -0,0 +1,290 @@ +import { useEffect, useMemo, useState } from "react"; + +import { getMany } from "idb-keyval"; + +import { LogAllow } from "@_constants/global"; +import { TProjectContext } from "@_redux/main/fileTree"; +import { useAppState } from "@_redux/useAppState"; +// @ts-ignore +import cmdkRefActions from "@_ref/cmdk.ref/Actions.csv"; +// @ts-ignore +import cmdkRefJumpstart from "@_ref/cmdk.ref/Jumpstart.csv"; +// @ts-ignore +import filesRef from "@_ref/rfrncs/Files.csv"; +import { + TCmdkContext, + TCmdkContextScope, + TCmdkGroupData, + TCmdkReference, + TCmdkReferenceData, + THtmlReferenceData, + TSession, +} from "@_types/main"; + +import { + addDefaultCmdkActions, + elementsCmdk, + fileCmdk, + getKeyObjectsFromCommand, +} from "../helper"; + +interface IUseCmdkReferenceData { + addRunningActions: (actionNames: string[]) => void; + removeRunningActions: (actionNames: string[]) => void; + + recentProjectContexts: TProjectContext[]; + recentProjectNames: string[]; + recentProjectHandlers: (FileSystemDirectoryHandle | null)[]; + setRecentProjectContexts: React.Dispatch< + React.SetStateAction + >; + setRecentProjectNames: React.Dispatch>; + setRecentProjectHandlers: React.Dispatch< + React.SetStateAction<(FileSystemDirectoryHandle | null)[]> + >; + + htmlReferenceData: THtmlReferenceData; +} +export const useCmdkReferenceData = ({ + addRunningActions, + removeRunningActions, + + recentProjectContexts, + recentProjectNames, + recentProjectHandlers, + setRecentProjectContexts, + setRecentProjectNames, + setRecentProjectHandlers, + + htmlReferenceData, +}: IUseCmdkReferenceData) => { + const { fileTree, fFocusedItem, nodeTree, nFocusedItem, cmdkSearchContent } = + useAppState(); + + const [cmdkReferenceData, setCmdkReferenceData] = + useState({}); + const [cmdkReferenceJumpstart, setCmdkReferenceJumpstart] = + useState({}); + const [cmdkReferenceActions, setCmdkReferenceActions] = + useState({}); + + // reference-cmdk + useEffect(() => { + (async () => { + addRunningActions(["reference-cmdk"]); + + // add default cmdk actions + const _cmdkReferenceData: TCmdkReferenceData = {}; + addDefaultCmdkActions(_cmdkReferenceData); + + // reference-cmdk-jumpstart + const _cmdkRefJumpstartData: TCmdkGroupData = {}; + await Promise.all( + cmdkRefJumpstart.map(async (command: TCmdkReference) => { + const _command = structuredClone(command); + _command["Keyboard Shortcut"] = getKeyObjectsFromCommand(command); + + const groupName = _command["Group"]; + if (_cmdkRefJumpstartData[groupName] !== undefined) { + _cmdkRefJumpstartData[groupName].push(_command); + } else { + _cmdkRefJumpstartData[groupName] = [_command]; + } + + if ( + groupName === "Projects" && + _cmdkRefJumpstartData["Recent"] === undefined + ) { + _cmdkRefJumpstartData["Recent"] = []; + // restore last edit session + try { + const sessionInfo = await getMany([ + "recent-project-context", + "recent-project-name", + "recent-project-handler", + ]); + if (sessionInfo[0] && sessionInfo[1] && sessionInfo[2]) { + const _session: TSession = { + "recent-project-context": sessionInfo[0], + "recent-project-name": sessionInfo[1], + "recent-project-handler": sessionInfo[2], + }; + setRecentProjectContexts(_session["recent-project-context"]); + setRecentProjectNames(_session["recent-project-name"]); + setRecentProjectHandlers(_session["recent-project-handler"]); + + for ( + let index = 0; + index < _session["recent-project-context"].length; + ++index + ) { + const _recentProjectCommand = { + Name: _session["recent-project-name"][index], + Icon: "folder", + Description: "", + "Keyboard Shortcut": [ + { + cmd: false, + shift: false, + alt: false, + key: "", + click: false, + }, + ], + Group: "Recent", + Context: index.toString(), + } as TCmdkReference; + _cmdkRefJumpstartData["Recent"].push(_recentProjectCommand); + } + LogAllow && console.info("last session loaded", _session); + } else { + LogAllow && console.log("has no last session"); + } + } catch (err) { + LogAllow && console.error("failed to load last session"); + } + } + + _cmdkReferenceData[_command["Name"]] = _command; + }), + ); + setCmdkReferenceJumpstart(_cmdkRefJumpstartData); + LogAllow && + console.log("cmdk jumpstart reference data: ", _cmdkRefJumpstartData); + + // reference-cmdk-actions + const _cmdkRefActionsData: TCmdkGroupData = {}; + cmdkRefActions.map((command: TCmdkReference) => { + const contexts: TCmdkContextScope[] = (command["Context"] as string) + ?.replace(/ /g, "") + .split(",") + .map((scope: string) => scope as TCmdkContextScope); + const contextObj: TCmdkContext = { + all: false, + file: false, + html: false, + }; + contexts?.map((context: TCmdkContextScope) => { + contextObj[context] = true; + }); + const keyObjects = getKeyObjectsFromCommand(command); + const _command: TCmdkReference = structuredClone(command); + _command["Context"] = contextObj; + _command["Keyboard Shortcut"] = keyObjects; + + const groupName = _command["Group"]; + if (_cmdkRefActionsData[groupName] !== undefined) { + _cmdkRefActionsData[groupName].push(_command); + } else { + _cmdkRefActionsData[groupName] = [_command]; + } + + _cmdkReferenceData[_command["Name"]] = _command; + }); + setCmdkReferenceActions(_cmdkRefActionsData); + LogAllow && + console.log("cmdk actions reference data: ", _cmdkRefActionsData); + + // set cmdk map + setCmdkReferenceData(_cmdkReferenceData); + LogAllow && console.log("cmdk map: ", _cmdkReferenceData); + + removeRunningActions(["reference-cmdk"]); + })(); + }, []); + + const cmdkReferneceRecentProject = useMemo(() => { + const _cmdkReferneceRecentProject: TCmdkReference[] = []; + recentProjectContexts.map((_context, index) => { + if (_context != "idb") { + _cmdkReferneceRecentProject.push({ + Name: recentProjectNames[index], + Icon: "folder", + Description: "", + "Keyboard Shortcut": [ + { + cmd: false, + shift: false, + alt: false, + key: "", + click: false, + }, + ], + Group: "Recent", + Context: index.toString(), + }); + } + }); + return _cmdkReferneceRecentProject; + }, [recentProjectContexts, recentProjectNames, recentProjectHandlers]); + const cmdkReferenceAdd = useMemo(() => { + const data: TCmdkGroupData = { + Files: [], + Elements: [], + Recent: [], + }; + + // Files + fileCmdk({ + fileTree, + fFocusedItem, + filesRef, + data, + cmdkSearchContent, + groupName: "Add", + }); + + // Elements + elementsCmdk({ + nodeTree, + nFocusedItem, + htmlReferenceData, + data, + cmdkSearchContent, + groupName: "Add", + }); + + // Recent + delete data["Recent"]; + + return data; + }, [ + fileTree, + fFocusedItem, + nodeTree, + nFocusedItem, + htmlReferenceData, + cmdkSearchContent, + ]); + const cmdkReferenceRename = useMemo(() => { + const data: TCmdkGroupData = { + Files: [], + Elements: [], + Recent: [], + }; + + // Elements + elementsCmdk({ + nodeTree, + nFocusedItem, + htmlReferenceData, + data, + cmdkSearchContent, + groupName: "Turn into", + }); + + // Recent + delete data["Recent"]; + + return data; + }, [nodeTree, nFocusedItem, htmlReferenceData, cmdkSearchContent]); + + return { + cmdkReferenceData, + cmdkReferenceJumpstart, + cmdkReferenceActions, + cmdkReferneceRecentProject, + cmdkReferenceAdd, + cmdkReferenceRename, + }; +}; diff --git a/src/pages/main/hooks/useFileHandlers.ts b/src/pages/main/hooks/useFileHandlers.ts new file mode 100644 index 00000000..da9ece86 --- /dev/null +++ b/src/pages/main/hooks/useFileHandlers.ts @@ -0,0 +1,20 @@ +import { useState } from "react"; + +import { TFileHandlerCollection } from "@_node/index"; + +export const useFileHandlers = () => { + const [projectHandlers, setProjectHandlers] = + useState({}); + const [currentProjectFileHandle, setCurrentProjectFileHandle] = + useState(null); + const [fileHandlers, setFileHandlers] = useState({}); + + return { + projectHandlers, + setProjectHandlers, + currentProjectFileHandle, + setCurrentProjectFileHandle, + fileHandlers, + setFileHandlers, + }; +}; diff --git a/src/pages/main/hooks/useHandlers.ts b/src/pages/main/hooks/useHandlers.ts new file mode 100644 index 00000000..64f18015 --- /dev/null +++ b/src/pages/main/hooks/useHandlers.ts @@ -0,0 +1,191 @@ +import { useCallback } from "react"; +import { setMany } from "idb-keyval"; +import { useDispatch } from "react-redux"; + +import { LogAllow } from "@_constants/global"; +import { DefaultProjectPath, RecentProjectCount } from "@_constants/main"; +import { + buildNohostIDB, + loadIDBProject, + loadLocalProject, + TFileHandlerCollection, +} from "@_node/file"; +import { + setDoingFileAction, + setFileTree, + setInitialFileUidToOpen, + setProject, + TProjectContext, +} from "@_redux/main/fileTree"; +import { + setNavigatorDropdownType, + setShowActionsPanel, +} from "@_redux/main/processor"; +import { useAppState } from "@_redux/useAppState"; + +import { clearProjectSession } from "../helper"; + +interface IUseHandlers { + setCurrentProjectFileHandle: React.Dispatch< + React.SetStateAction + >; + setFileHandlers: React.Dispatch>; + + recentProjectContexts: TProjectContext[]; + recentProjectNames: string[]; + recentProjectHandlers: (FileSystemDirectoryHandle | null)[]; + setRecentProjectContexts: React.Dispatch< + React.SetStateAction + >; + setRecentProjectNames: React.Dispatch>; + setRecentProjectHandlers: React.Dispatch< + React.SetStateAction<(FileSystemDirectoryHandle | null)[]> + >; +} +export const useHandlers = ({ + setCurrentProjectFileHandle, + setFileHandlers, + + recentProjectContexts, + recentProjectNames, + recentProjectHandlers, + setRecentProjectContexts, + setRecentProjectNames, + setRecentProjectHandlers, +}: IUseHandlers) => { + const dispatch = useDispatch(); + const { osType, navigatorDropdownType } = useAppState(); + + const saveRecentProject = useCallback( + async ( + fsType: TProjectContext, + projectHandle: FileSystemDirectoryHandle, + ) => { + const _recentProjectContexts = [...recentProjectContexts]; + const _recentProjectNames = [...recentProjectNames]; + const _recentProjectHandlers = [...recentProjectHandlers]; + for (let index = 0; index < _recentProjectContexts.length; ++index) { + if ( + _recentProjectContexts[index] === fsType && + projectHandle?.name === _recentProjectNames[index] + ) { + _recentProjectContexts.splice(index, 1); + _recentProjectNames.splice(index, 1); + _recentProjectHandlers.splice(index, 1); + break; + } + } + if (_recentProjectContexts.length === RecentProjectCount) { + _recentProjectContexts.pop(); + _recentProjectNames.pop(); + _recentProjectHandlers.pop(); + } + _recentProjectContexts.unshift(fsType); + _recentProjectNames.unshift( + (projectHandle as FileSystemDirectoryHandle).name, + ); + _recentProjectHandlers.unshift( + projectHandle as FileSystemDirectoryHandle, + ); + setRecentProjectContexts(_recentProjectContexts); + setRecentProjectNames(_recentProjectNames); + setRecentProjectHandlers(_recentProjectHandlers); + await setMany([ + ["recent-project-context", _recentProjectContexts], + ["recent-project-name", _recentProjectNames], + ["recent-project-handler", _recentProjectHandlers], + ]); + }, + [recentProjectContexts, recentProjectNames, recentProjectHandlers], + ); + const importProject = useCallback( + async ( + fsType: TProjectContext, + projectHandle?: FileSystemDirectoryHandle | null, + ) => { + if (fsType === "local") { + dispatch(setDoingFileAction(true)); + try { + const { + handlerArr, + _fileHandlers, + + _fileTree, + _initialFileUidToOpen, + } = await loadLocalProject( + projectHandle as FileSystemDirectoryHandle, + osType, + ); + + clearProjectSession(dispatch); + + // build nohost idb + handlerArr && buildNohostIDB(handlerArr); + + dispatch( + setProject({ + context: "local", + name: (projectHandle as FileSystemDirectoryHandle).name, + favicon: null, + }), + ); + setCurrentProjectFileHandle( + projectHandle as FileSystemDirectoryHandle, + ); + + dispatch(setFileTree(_fileTree)); + dispatch(setInitialFileUidToOpen(_initialFileUidToOpen)); + setFileHandlers(_fileHandlers as TFileHandlerCollection); + + if (_initialFileUidToOpen === "") { + dispatch(setShowActionsPanel(false)); + dispatch(setNavigatorDropdownType(null)); + } + + await saveRecentProject( + fsType, + projectHandle as FileSystemDirectoryHandle, + ); + } catch (err) { + LogAllow && console.log("ERROR while importing local project", err); + } + dispatch(setDoingFileAction(false)); + } else if (fsType === "idb") { + dispatch(setDoingFileAction(true)); + try { + const { _fileTree, _initialFileUidToOpen } = + await loadIDBProject(DefaultProjectPath); + + clearProjectSession(dispatch); + + dispatch( + setProject({ + context: "idb", + name: "Welcome", + favicon: null, + }), + ); + setCurrentProjectFileHandle(null); + + dispatch(setFileTree(_fileTree)); + dispatch(setInitialFileUidToOpen(_initialFileUidToOpen)); + setFileHandlers({}); + + // await saveRecentProject(fsType, null); + } catch (err) { + LogAllow && console.log("ERROR while importing IDB project", err); + } + dispatch(setDoingFileAction(false)); + } + }, + [osType, saveRecentProject], + ); + const closeNavigator = useCallback(() => { + navigatorDropdownType !== null && dispatch(setNavigatorDropdownType(null)); + }, [navigatorDropdownType]); + + return { + importProject, + closeNavigator, + }; +}; diff --git a/src/pages/main/hooks/useInit.ts b/src/pages/main/hooks/useInit.ts new file mode 100644 index 00000000..f0a52efd --- /dev/null +++ b/src/pages/main/hooks/useInit.ts @@ -0,0 +1,88 @@ +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { LogAllow } from "@_constants/global"; +import { setOsType, setTheme } from "@_redux/global"; +import { TProjectContext, setDoingFileAction } from "@_redux/main/fileTree"; +import { isChromeOrEdge } from "@_services/global"; +import { setSystemTheme } from "../helper"; + +interface IUseInit { + importProject: ( + fsType: TProjectContext, + projectHandle?: FileSystemDirectoryHandle | null | undefined, + ) => Promise; + onNew: () => Promise; +} +export const useInit = ({ importProject, onNew }: IUseInit) => { + const dispatch = useDispatch(); + + // detect os + useEffect(() => { + LogAllow && console.log("navigator: ", navigator.userAgent); + if (navigator.userAgent.indexOf("Mac OS X") !== -1) { + dispatch(setOsType("Mac")); + } else if (navigator.userAgent.indexOf("Linux") !== -1) { + dispatch(setOsType("Linux")); + } else { + dispatch(setOsType("Windows")); + } + }, []); + + // browser + useEffect(() => { + if (!isChromeOrEdge()) { + const message = `Browser is unsupported. rnbw works in the latest versions of Google Chrome and Microsoft Edge.`; + if (!window.confirm(message)) return; + } + + document.addEventListener("contextmenu", (e) => { + e.preventDefault(); + }); + window.document.addEventListener("contextmenu", (e) => { + e.preventDefault(); + }); + }, []); + + // theme + useEffect(() => { + const storedTheme = localStorage.getItem("theme"); + LogAllow && console.log("storedTheme: ", storedTheme); + if (storedTheme) { + document.documentElement.setAttribute("data-theme", storedTheme); + dispatch(setTheme(storedTheme === "dark" ? "Dark" : "Light")); + } else { + setSystemTheme(); + window + .matchMedia("(prefers-color-scheme: dark)") + .addEventListener("change", setSystemTheme); + } + + return () => + window + .matchMedia("(prefers-color-scheme: dark)") + .removeEventListener("change", setSystemTheme); + }, []); + + // newbie + useEffect(() => { + const newbie = localStorage.getItem("newbie"); + const isNewbie = newbie === null ? true : false; + LogAllow && console.log("isNewbie: ", isNewbie); + if (!isNewbie) { + localStorage.setItem("newbie", "false"); + // load idb project + (async () => { + dispatch(setDoingFileAction(true)); + try { + await importProject("idb"); + LogAllow && console.log("loaded idb project"); + } catch (err) { + LogAllow && console.log("failed to load idb project"); + } + dispatch(setDoingFileAction(false)); + })(); + } else { + onNew(); + } + }, []); +}; diff --git a/src/pages/main/hooks/usePanels.ts b/src/pages/main/hooks/usePanels.ts new file mode 100644 index 00000000..bb611e09 --- /dev/null +++ b/src/pages/main/hooks/usePanels.ts @@ -0,0 +1,54 @@ +import React, { useCallback, useState } from "react"; + +export const usePanels = () => { + const [actionsPanelOffsetTop, setActionsPanelOffsetTop] = useState(12); + const [actionsPanelOffsetLeft, setActionsPanelOffsetLeft] = useState(12); + const [actionsPanelWidth, setActionsPanelWidth] = useState(240); + + const [codeViewOffsetBottom, setCodeViewOffsetBottom] = useState("12"); + const [codeViewOffsetTop, setCodeViewOffsetTop] = + useState("calc(60vh - 12px)"); + const [codeViewOffsetLeft, setCodeViewOffsetLeft] = useState(12); + const [codeViewHeight, setCodeViewHeight] = useState("40"); + const [codeViewDragging, setCodeViewDragging] = useState(false); + const dragCodeView = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setCodeViewOffsetTop( + ((e.clientY / document.documentElement.clientHeight) * 100 < 1 + ? 1 + : (e.clientY / document.documentElement.clientHeight) * 100 + ).toString(), + ); + if (!codeViewDragging) { + setCodeViewDragging(true); + } + }, []); + const dragEndCodeView = useCallback((e: React.DragEvent) => { + e.preventDefault(); + const offsetTop = ( + (e.clientY / document.documentElement.clientHeight) * 100 < 1 + ? 1 + : (e.clientY / document.documentElement.clientHeight) * 100 + ).toString(); + setCodeViewOffsetTop(offsetTop); + setCodeViewDragging(false); + localStorage.setItem("offsetTop", offsetTop); + }, []); + const dropCodeView = useCallback((e: React.DragEvent) => { + e.preventDefault(); + }, []); + + return { + actionsPanelOffsetTop, + actionsPanelOffsetLeft, + actionsPanelWidth, + codeViewOffsetBottom, + codeViewOffsetTop, + codeViewOffsetLeft, + codeViewHeight, + codeViewDragging, + dragCodeView, + dragEndCodeView, + dropCodeView, + }; +}; diff --git a/src/pages/main/hooks/useRecentProjects.ts b/src/pages/main/hooks/useRecentProjects.ts new file mode 100644 index 00000000..38f1e70c --- /dev/null +++ b/src/pages/main/hooks/useRecentProjects.ts @@ -0,0 +1,22 @@ +import { useState } from "react"; + +import { TProjectContext } from "@_redux/main/fileTree"; + +export const useRecentProjects = () => { + const [recentProjectContexts, setRecentProjectContexts] = useState< + TProjectContext[] + >([]); + const [recentProjectNames, setRecentProjectNames] = useState([]); + const [recentProjectHandlers, setRecentProjectHandlers] = useState< + (FileSystemDirectoryHandle | null)[] + >([]); + + return { + recentProjectContexts, + recentProjectNames, + recentProjectHandlers, + setRecentProjectContexts, + setRecentProjectNames, + setRecentProjectHandlers, + }; +}; diff --git a/src/pages/main/hooks/useReferenceData.ts b/src/pages/main/hooks/useReferenceData.ts new file mode 100644 index 00000000..8b85efe2 --- /dev/null +++ b/src/pages/main/hooks/useReferenceData.ts @@ -0,0 +1,53 @@ +import { useEffect, useState } from "react"; + +import { LogAllow } from "@_constants/global"; +// @ts-ignore +import filesRef from "@_ref/rfrncs/Files.csv"; +// @ts-ignore +import htmlRefElements from "@_ref/rfrncs/HTML Elements.csv"; +import { + TFilesReference, + TFilesReferenceData, + THtmlElementsReference, + THtmlElementsReferenceData, + THtmlReferenceData, +} from "@_types/main"; + +export const useReferenceData = () => { + const [filesReferenceData, setFilesReferenceData] = + useState({}); + const [htmlReferenceData, setHtmlReferenceData] = + useState({ + elements: {}, + }); + + // reference-files + useEffect(() => { + const _filesReferenceData: TFilesReferenceData = {}; + filesRef.map((fileRef: TFilesReference) => { + _filesReferenceData[fileRef.Extension] = fileRef; + }); + setFilesReferenceData(_filesReferenceData); + LogAllow && console.info("files reference data: ", _filesReferenceData); + }, []); + + // reference-html-elements + useEffect(() => { + const htmlElementsReferenceData: THtmlElementsReferenceData = {}; + htmlRefElements.map((htmlRefElement: THtmlElementsReference) => { + const pureTag = + htmlRefElement["Name"] === "Comment" + ? "comment" + : htmlRefElement["Tag"]?.slice(1, htmlRefElement["Tag"].length - 1); + htmlElementsReferenceData[pureTag] = htmlRefElement; + }); + setHtmlReferenceData({ elements: htmlElementsReferenceData }); + LogAllow && + console.info("html elements reference data: ", htmlElementsReferenceData); + }, []); + + return { + filesReferenceData, + htmlReferenceData, + }; +}; diff --git a/src/pages/main/hooks/useReferences.ts b/src/pages/main/hooks/useReferences.ts new file mode 100644 index 00000000..be9d365e --- /dev/null +++ b/src/pages/main/hooks/useReferences.ts @@ -0,0 +1,29 @@ +import { useRef } from "react"; + +import { editor } from "monaco-editor"; + +export const useReferneces = () => { + const monacoEditorRef = useRef(null); + const setMonacoEditorRef = ( + editorInstance: editor.IStandaloneCodeEditor | null, + ) => { + monacoEditorRef.current = editorInstance; + }; + const iframeRefRef = useRef(null); + const setIframeRefRef = (iframeRef: HTMLIFrameElement | null) => { + iframeRefRef.current = iframeRef; + }; + const isContentProgrammaticallyChanged = useRef(false); + const setIsContentProgrammaticallyChanged = (value: boolean) => { + isContentProgrammaticallyChanged.current = value; + }; + + return { + monacoEditorRef, + setMonacoEditorRef, + iframeRefRef, + setIframeRefRef, + isContentProgrammaticallyChanged, + setIsContentProgrammaticallyChanged, + }; +}; diff --git a/src/pages/main/hooks/useRunningActions.ts b/src/pages/main/hooks/useRunningActions.ts new file mode 100644 index 00000000..5028c926 --- /dev/null +++ b/src/pages/main/hooks/useRunningActions.ts @@ -0,0 +1,43 @@ +import { useCallback, useRef } from "react"; + +import { useDispatch } from "react-redux"; + +import { setDoingAction } from "@_redux/main/processor"; + +export const useRunningActions = () => { + const dispatch = useDispatch(); + + const runningActions = useRef<{ [actionName: string]: true }>({}); + const hasRunningAction = useCallback( + () => (Object.keys(runningActions.current).length === 0 ? false : true), + [], + ); + const addRunningActions = useCallback((actionNames: string[]) => { + let added = false; + for (const actionName of actionNames) { + if (!runningActions.current[actionName]) { + runningActions.current[actionName] = true; + added = true; + } + } + added && dispatch(setDoingAction(true)); + }, []); + const removeRunningActions = useCallback( + (actionNames: string[]) => { + let removed = false; + for (const actionName of actionNames) { + if (runningActions.current[actionName]) { + delete runningActions.current[actionName]; + removed = true; + } + } + removed && !hasRunningAction() && dispatch(setDoingAction(false)); + }, + [hasRunningAction], + ); + + return { + addRunningActions, + removeRunningActions, + }; +}; diff --git a/src/pages/main/processor/helpers.ts b/src/pages/main/processor/helpers.ts index 85b0827d..d087e395 100644 --- a/src/pages/main/processor/helpers.ts +++ b/src/pages/main/processor/helpers.ts @@ -10,7 +10,7 @@ import { TFileNode, TFileNodeData, TFileNodeTreeData, - writeFile, + _writeIDBFile, } from "@_node/file"; import { getNodeChildIndex, getSubNodeUidsByBfs } from "@_node/helpers"; import { THtmlNodeData } from "@_node/node"; @@ -19,7 +19,7 @@ import { setFileTreeNodes, TProject } from "@_redux/main/fileTree"; import { AnyAction } from "@reduxjs/toolkit"; export const saveFileContent = async ( - project: TProject, + project: Omit, fileHandlers: TFileHandlerCollection, uid: string, fileData: TFileNodeData, @@ -33,7 +33,7 @@ export const saveFileContent = async ( await writableStream.close(); } - await writeFile(fileData.path, fileData.content); + await _writeIDBFile(fileData.path, fileData.content); fileData.changed = false; fileData.orgContent = fileData.content; }; diff --git a/src/pages/main/processor/hooks/useNodeTreeEvent.ts b/src/pages/main/processor/hooks/useNodeTreeEvent.ts index c513c87c..b6dcca1c 100644 --- a/src/pages/main/processor/hooks/useNodeTreeEvent.ts +++ b/src/pages/main/processor/hooks/useNodeTreeEvent.ts @@ -9,7 +9,7 @@ import { parseFile, PreserveRnbwNode, StageNodeIdAttr, - writeFile, + _writeIDBFile, } from "@_node/file"; import { getNodeUidsFromPaths } from "@_node/helpers"; import { TNodeUid } from "@_node/types"; @@ -119,7 +119,7 @@ export const useNodeTreeEvent = () => { dispatch(setDoingFileAction(true)); try { const previewPath = getPreviewPath(fileTree, file); - await writeFile(previewPath, fileData.contentInApp as string); + await _writeIDBFile(previewPath, fileData.contentInApp as string); if (fileData.ext === "html") { dispatch(setIframeSrc(`rnbw${previewPath}`)); } @@ -244,7 +244,6 @@ export const useNodeTreeEvent = () => { ), ); } else { - LogAllow && console.log("it's a rnbw-change"); const validExpandedItems = nExpandedItems.filter( (uid) => _validNodeTree[uid] && _validNodeTree[uid].isEntity === false, );