Add file/folder selection to browsers, fix numeric sorting
This commit is contained in:
@@ -1,11 +1,19 @@
|
|||||||
<h2>
|
<h2>
|
||||||
@if(workspaceName()) { {{ workspaceName() }} } @else { No Worspace Selected}
|
@if(workspaceName()) { {{ workspaceName() }} } @else if(!isFileSelected()) {
|
||||||
|
No Worspace Selected}
|
||||||
</h2>
|
</h2>
|
||||||
@if(!selectedDirectory()) {
|
@if(!isFileSelected()) {
|
||||||
<div>
|
<div>
|
||||||
<button mat-button (click)="selectDirectory()" style="margin: auto">
|
<button mat-button (click)="selectDirectory()" style="margin: auto">
|
||||||
Open Folder
|
Open Folder
|
||||||
</button>
|
</button>
|
||||||
|
<input
|
||||||
|
#fileSelector
|
||||||
|
type="file"
|
||||||
|
(change)="selectFilesBrowser()"
|
||||||
|
webkitdirectory
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
}@else {
|
}@else {
|
||||||
<mat-tree [dataSource]="dataSource()" [treeControl]="treeControl">
|
<mat-tree [dataSource]="dataSource()" [treeControl]="treeControl">
|
||||||
|
|||||||
@@ -22,3 +22,7 @@ h2 {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { FlatTreeControl } from '@angular/cdk/tree';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
|
ElementRef,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
computed,
|
computed,
|
||||||
output,
|
output,
|
||||||
signal,
|
signal,
|
||||||
|
viewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
@@ -24,7 +26,7 @@ export interface FileOrFolder {
|
|||||||
isDirectory: boolean;
|
isDirectory: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
children?: FileOrFolder[];
|
children?: FileOrFolder[];
|
||||||
path: string;
|
path?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileNode {
|
interface FileNode {
|
||||||
@@ -43,6 +45,9 @@ interface FileNode {
|
|||||||
export class FileTreeComponent implements OnInit, OnDestroy {
|
export class FileTreeComponent implements OnInit, OnDestroy {
|
||||||
fileSelected = output<FileOrFolder>();
|
fileSelected = output<FileOrFolder>();
|
||||||
|
|
||||||
|
protected fileSelector =
|
||||||
|
viewChild<ElementRef<HTMLInputElement>>('fileSelector');
|
||||||
|
|
||||||
// File tree
|
// File tree
|
||||||
protected hasChild = (_: number, node: FileNode) => node.expandable;
|
protected hasChild = (_: number, node: FileNode) => node.expandable;
|
||||||
|
|
||||||
@@ -77,6 +82,7 @@ export class FileTreeComponent implements OnInit, OnDestroy {
|
|||||||
// Folder selection
|
// Folder selection
|
||||||
protected selectedDirectory = signal<string | null>(null);
|
protected selectedDirectory = signal<string | null>(null);
|
||||||
protected files = signal<FileOrFolder[]>([]);
|
protected files = signal<FileOrFolder[]>([]);
|
||||||
|
protected isFileSelected = signal<boolean>(false);
|
||||||
protected workspaceName = computed(() => {
|
protected workspaceName = computed(() => {
|
||||||
const directory = this.selectedDirectory();
|
const directory = this.selectedDirectory();
|
||||||
if (directory) {
|
if (directory) {
|
||||||
@@ -104,6 +110,15 @@ export class FileTreeComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async selectDirectory() {
|
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({
|
const selectedDirectory = await open({
|
||||||
directory: true,
|
directory: true,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
@@ -118,26 +133,69 @@ export class FileTreeComponent implements OnInit, OnDestroy {
|
|||||||
const entries = await readDir(selectedDirectory, {
|
const entries = await readDir(selectedDirectory, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
const splitNumbers = /(\d)+|(\D)+/;
|
|
||||||
this.files.set(
|
this.files.set(
|
||||||
entries
|
entries.sort(this.sortFiles).map((entry) => this.mapEntry(entry))
|
||||||
.sort(this.sortFiles(splitNumbers))
|
|
||||||
.map((entry) => this.mapEntry(entry, splitNumbers))
|
|
||||||
);
|
);
|
||||||
|
this.isFileSelected.set(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapEntry(entry: FileEntry, splitNumbers: RegExp): FileOrFolder {
|
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 {
|
return {
|
||||||
isDirectory: entry.children != null,
|
isDirectory: entry.children != null,
|
||||||
name: entry.name || '',
|
name: entry.name || '',
|
||||||
children: entry.children
|
children: entry.children
|
||||||
?.sort(this.sortFiles(splitNumbers))
|
?.sort(this.sortFiles)
|
||||||
.map((entry) => this.mapEntry(entry, splitNumbers)),
|
.map((entry) => this.mapEntry(entry)),
|
||||||
path: entry.path,
|
path: entry.path,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private sortFiles = (splitNumbers: RegExp) => (a: FileEntry, b: FileEntry) =>
|
private sortFiles = (a: { name?: string }, b: { name?: string }) =>
|
||||||
a.name?.localeCompare(b.name ?? '') ?? 0;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user