import { writable, type Readable } from "svelte/store"; import { dequal } from "dequal"; export interface Node { type: "file" | "folder"; path: string; children?: Node[]; checked: boolean; children_visible: boolean; last?: Node | null; parent: Node | null; previous?: Node | null; } export type SerialisedNode = Omit< Node, "checked" | "children_visible" | "children" > & { children?: SerialisedNode[] }; interface FSStore { subscribe: Readable["subscribe"]; create_fs_graph: (serialised_node: SerialisedNode[]) => void; set_checked: ( indices: number[], checked: boolean, checked_paths: string[][], file_count: "single" | "multiple" ) => string[][]; set_checked_from_paths: (checked_paths: string[][]) => string[][]; } export const make_fs_store = (): FSStore => { const { subscribe, set } = writable(null); let root: Node = { type: "folder", path: "", checked: false, children_visible: false, parent: null }; let tree_updated = false; function create_fs_graph(serialised_node: SerialisedNode[]): void { root.children = process_tree(serialised_node); tree_updated = true; set(root.children); } let old_checked_paths: string[][] = []; function set_checked_from_paths(checked_paths: string[][]): string[][] { if (dequal(checked_paths, old_checked_paths) && !tree_updated) { return checked_paths; } old_checked_paths = checked_paths; check_node_and_children(root.children, false, []); const new_checked_paths: string[][] = []; const seen_nodes = new Set(); for (let i = 0; i < checked_paths.length; i++) { let _node = root; let _path = []; for (let j = 0; j < checked_paths[i].length; j++) { if (!_node?.children) { continue; } _path.push(checked_paths[i][j]); _node = _node.children!.find((v) => v.path === checked_paths[i][j])!; } if (!_node) { continue; } _node.checked = true; ensure_visible(_node); const nodes = check_node_and_children(_node.children, true, [_node]); check_parent(_node); nodes.forEach((node) => { const path = get_full_path(node); const normalized_path = path.join("/"); // let normalized_path = path.join("/"); // normalized_path = normalized_path.endsWith("/") ? normalized_path.slice(0, -1) : normalized_path; if (node.type === "file") { if (seen_nodes.has(normalized_path)) { return; } new_checked_paths.push(path); seen_nodes.add(normalized_path); } }); } set(root.children!); tree_updated = false; return new_checked_paths; } function set_checked( indices: number[], checked: boolean, checked_paths: string[][], file_count: "single" | "multiple" ): string[][] { let _node = root; if (file_count === "single") { check_node_and_children(root.children, false, []); set(root.children!); } for (let i = 0; i < indices.length; i++) { _node = _node.children![indices[i]]; } _node.checked = checked; const nodes = check_node_and_children(_node.children, checked, [_node]); let new_checked_paths = new Map(checked_paths.map((v) => [v.join("/"), v])); for (let i = 0; i < nodes.length; i++) { const _path = get_full_path(nodes[i]); if (!nodes[i].checked) { new_checked_paths.delete(_path.join("/")); } else if (nodes[i].checked) { if (file_count === "single") { new_checked_paths = new Map(); } new_checked_paths.set(_path.join("/"), _path); } } check_parent(_node); set(root.children!); old_checked_paths = Array.from(new_checked_paths).map((v) => v[1]); return old_checked_paths; } return { subscribe, create_fs_graph, set_checked, set_checked_from_paths }; }; function ensure_visible(node: Node): void { if (node.parent) { node.parent.children_visible = true; ensure_visible(node.parent); } } function process_tree( node: SerialisedNode[], depth = 0, path_segments: string[] = [], parent: Node | null = null ): Node[] { const folders: Node[] = []; const files: Node[] = []; for (let i = 0; i < node.length; i++) { let n: (typeof node)[number] = node[i]; if (n.type === "file") { let index = files.findIndex( (v) => v.path.toLocaleLowerCase() >= n.path.toLocaleLowerCase() ); const _node: Node = { children: undefined, type: "file", path: n.path, checked: false, children_visible: false, parent: parent }; files.splice(index === -1 ? files.length : index, 0, _node); } else { let index = folders.findIndex( (v) => v.path.toLocaleLowerCase() >= n.path.toLocaleLowerCase() ); const _node: Node = { type: "folder", path: n.path, checked: false, children_visible: false, parent: parent }; const children = process_tree( n.children!, depth + 1, [...path_segments, n.path], _node ); _node.children = children; folders.splice(index === -1 ? folders.length : index, 0, _node); } } const last = files[files.length - 1] || folders[folders.length - 1]; for (let i = 0; i < folders.length; i++) { folders[i].last = last; folders[i].previous = folders[i - 1] || null; } for (let i = 0; i < files.length; i++) { if (i === 0) { files[i].previous = folders[folders.length - 1] || null; } else { files[i].previous = files[i - 1] || null; } files[i].last = last; } return Array().concat(folders, files); } function get_full_path(node: Node, path: string[] = []): string[] { const new_path = [node.path, ...path]; if (node.parent) { return get_full_path(node.parent, new_path); } return new_path; } function check_node_and_children( node: Node[] | null | undefined, checked: boolean, checked_nodes: Node[] ): Node[] { if (node === null || node === undefined) return checked_nodes; for (let i = 0; i < node.length; i++) { node[i].checked = checked; checked_nodes.push(node[i]); if (checked) ensure_visible(node[i]); checked_nodes.concat( check_node_and_children(node[i].children, checked, checked_nodes) ); } return checked_nodes; } function check_parent(node: Node | null | undefined): void { if (node === null || node === undefined || !node.parent) return; let _node = node.last; let nodes_checked = []; while (_node) { nodes_checked.push(_node.checked); _node = _node.previous; } if (nodes_checked.every((v) => v === true)) { node.parent!.checked = true; check_parent(node?.parent); } else if (nodes_checked.some((v) => v === false)) { node.parent!.checked = false; check_parent(node?.parent); } }