Files
bufpiv/src/app/file-tree/file-tree.component.ts

202 lines
5.6 KiB
TypeScript

import { FlatTreeControl } from '@angular/cdk/tree';
import { CommonModule } from '@angular/common';
import {
Component,
ElementRef,
OnDestroy,
OnInit,
computed,
output,
signal,
viewChild,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import {
MatTreeFlatDataSource,
MatTreeFlattener,
MatTreeModule,
} from '@angular/material/tree';
import { message, open } from '@tauri-apps/api/dialog';
import { UnlistenFn, listen } from '@tauri-apps/api/event';
import { FileEntry, readDir } from '@tauri-apps/api/fs';
import { OpenFolderMessage } from '../messages/openfolder.message';
export interface FileOrFolder {
isDirectory: boolean;
name: string;
children?: FileOrFolder[];
path?: string;
}
interface FileNode {
file: FileOrFolder;
expandable: boolean;
level: number;
}
@Component({
selector: 'app-file-tree',
standalone: true,
imports: [CommonModule, MatButtonModule, MatIconModule, MatTreeModule],
templateUrl: './file-tree.component.html',
styleUrl: './file-tree.component.scss',
})
export class FileTreeComponent implements OnInit, OnDestroy {
fileSelected = output<FileOrFolder>();
protected fileSelector =
viewChild<ElementRef<HTMLInputElement>>('fileSelector');
// File tree
protected hasChild = (_: number, node: FileNode) => node.expandable;
private _transformer = (node: FileOrFolder, level: number) => ({
expandable: !!node.children && node.children.length > 0,
file: node,
level: level,
});
protected treeControl = new FlatTreeControl<FileNode>(
(node) => node.level,
(node) => node.expandable
);
private treeFlattener = new MatTreeFlattener(
this._transformer,
(node) => node.level,
(node) => node.expandable,
(node) => node.children
);
// Use computed signals to create datasource
protected dataSource = computed(() => {
const dataSource = new MatTreeFlatDataSource(
this.treeControl,
this.treeFlattener
);
dataSource.data = this.files();
return dataSource;
});
// Folder selection
protected selectedDirectory = signal<string | null>(null);
protected files = signal<FileOrFolder[]>([]);
protected isFileSelected = signal<boolean>(false);
protected workspaceName = computed(() => {
const directory = this.selectedDirectory();
if (directory) {
const directorySplit = directory.split('/');
return directorySplit[directorySplit.length - 1];
}
return null;
});
private unlisten?: UnlistenFn;
async ngOnInit() {
this.unlisten = await listen(
'openfolder',
async (event: OpenFolderMessage) => {
await this.selectDirectory();
}
);
}
async ngOnDestroy() {
if (this.unlisten) {
this.unlisten();
}
}
async selectDirectory() {
try {
await this.selectDirectoryNative();
} catch (err) {
// Tauri failed, try using browser to select files
this.fileSelector()?.nativeElement.click();
}
}
private async selectDirectoryNative() {
const selectedDirectory = await open({
directory: true,
multiple: false,
});
if (Array.isArray(selectedDirectory)) {
message('Only a single folder can be selected at a time');
return;
} else {
this.selectedDirectory.set(selectedDirectory);
}
if (selectedDirectory) {
const entries = await readDir(selectedDirectory, {
recursive: true,
});
this.files.set(
entries.sort(this.sortFiles).map((entry) => this.mapEntry(entry))
);
this.isFileSelected.set(true);
}
}
protected selectFilesBrowser() {
const files = this.fileSelector()?.nativeElement.files;
if (files && files.length > 0) {
const mappedFiles: FileOrFolder[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.webkitRelativePath?.includes('/')) {
// Got a file in a folder, so put it into the appropriate folder in the tree
const splitFilePath = file.webkitRelativePath.split('/');
let currentChildren: FileOrFolder[] | undefined = mappedFiles;
for (let j = 0; j < splitFilePath.length - 1; j++) {
const relativePath = splitFilePath[j];
let matchingChild: FileOrFolder | undefined = currentChildren?.find(
(mappedFile) => mappedFile.name === relativePath
);
if (!matchingChild) {
matchingChild = {
isDirectory: true,
name: relativePath,
children: [],
};
currentChildren?.push(matchingChild);
}
currentChildren = matchingChild.children;
}
currentChildren?.push({ isDirectory: false, name: file.name });
} else {
mappedFiles.push({ isDirectory: false, name: file.name });
}
}
this.recursiveSort(mappedFiles);
this.files.set(mappedFiles);
this.isFileSelected.set(true);
}
}
private mapEntry(entry: FileEntry): FileOrFolder {
return {
isDirectory: entry.children != null,
name: entry.name || '',
children: entry.children
?.sort(this.sortFiles)
.map((entry) => this.mapEntry(entry)),
path: entry.path,
};
}
private sortFiles = (a: { name?: string }, b: { name?: string }) =>
new Intl.Collator(undefined, { numeric: true }).compare(a.name!, b.name!);
private recursiveSort(files: FileOrFolder[]) {
for (const file of files) {
if (file.children) {
this.recursiveSort(file.children);
}
}
files.sort(this.sortFiles);
}
}