Files
bufpiv/src/app/editor/editor.component.ts

197 lines
5.9 KiB
TypeScript

import { CommonModule } from '@angular/common';
import {
Component,
ElementRef,
SecurityContext,
computed,
effect,
input,
signal,
viewChild,
inject,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSnackBar } from '@angular/material/snack-bar';
import { DomSanitizer } from '@angular/platform-browser';
import { readTextFile, writeTextFile } from '@tauri-apps/api/fs';
import hljs from 'highlight.js/lib/core';
import { ProtoMessage } from '../model/proto-message.model';
import { ProtoFieldComponent } from './proto-field/proto-field.component';
import { save } from '@tauri-apps/api/dialog';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MonacoEditorModule } from 'ngx-monaco-editor-v2';
import { FormsModule } from '@angular/forms';
import { FileOrFolder } from '../file-tree/file-tree.component';
declare const __TAURI__: any;
type PreviewType = 'raw' | 'edit' | 'diff';
@Component({
selector: 'app-editor',
imports: [
CommonModule,
ProtoFieldComponent,
MatButtonModule,
MatButtonToggleModule,
MatIconModule,
MonacoEditorModule,
FormsModule,
],
templateUrl: './editor.component.html',
styleUrl: './editor.component.scss',
})
export class EditorComponent {
private snackBar = inject(MatSnackBar);
selectedFile = input<FileOrFolder>();
selectedMessage = input.required<ProtoMessage>();
indentSize = input<number>(2);
showRaw = input<boolean>(true);
protected editorOptions = {
theme: 'vs-dark',
language: 'json',
automaticLayout: true,
};
protected values = signal<any>(undefined);
protected downloadName = computed(
() => this.selectedFile() ?? `${this.selectedMessage().name}.json`
);
protected serialisedValues = computed(() =>
JSON.stringify(this.values(), undefined, this.indentSize())
);
protected saveHref = computed(() => {
const blob = new Blob([this.serialisedValues()], {
type: 'application/json',
});
return URL.createObjectURL(blob);
});
protected downloader = viewChild<ElementRef<HTMLAnchorElement>>('downloader');
protected previewType = signal<PreviewType>('raw');
protected currentFileContents = signal<string>('');
protected originalModel = computed(() => ({
code: this.currentFileContents(),
language: 'json',
}));
protected currentModel = computed(() => ({
code: this.serialisedValues(),
language: 'json',
}));
private code = viewChild<ElementRef<HTMLElement>>('code');
constructor() {
const sanitizer = inject(DomSanitizer);
effect(() => {
const element = this.code()?.nativeElement;
if (element) {
if (!this.values()) {
return;
}
const json = this.serialisedValues();
const highlighted = hljs.highlightAuto(json, ['json']);
const sanitized = sanitizer.sanitize(
SecurityContext.HTML,
highlighted.value
);
if (sanitized) {
element.innerHTML = sanitized;
}
}
});
effect(
() => {
const message = this.selectedMessage();
this.values.set(
Object.fromEntries(
message.values.map((value) => [[value.name, undefined]])
)
);
},
{ allowSignalWrites: true }
);
effect(async () => {
const selectedFile = this.selectedFile();
let filePath: string | null = null;
let fileContents: string | null = null;
try {
if (selectedFile?.browserFile) {
fileContents = await selectedFile.browserFile.text();
filePath = selectedFile.browserFile.webkitRelativePath;
} else if (selectedFile?.path) {
fileContents = await readTextFile(selectedFile.path);
filePath = selectedFile.path;
}
} catch (err) {
console.error(err);
this.snackBar.open(
'Failed to read file contents, please check you have the correct permissions and the file still exists',
'Dismiss',
{ duration: 5000 }
);
}
if (filePath && fileContents) {
this.currentFileContents.set(fileContents);
this.updateValuesFromText(fileContents, filePath);
}
});
}
protected updateValuesFromText(text: string, selectedFile?: string) {
try {
const parsed = JSON.parse(text);
this.values.set(parsed);
} catch (err) {
if (selectedFile) {
console.error(`Failed to parse contents of file ${selectedFile}`, err);
this.snackBar.open(
'Failed to parse file contents as JSON, please ensure this is a valid JSON formatted file',
'Dismiss',
{ duration: 5000 }
);
}
}
}
protected updateValue(key: string, value: any) {
const existingValue = { ...this.values() };
existingValue[key] = value;
this.values.set(existingValue);
}
protected async copyToClipboard() {
await navigator.clipboard.writeText(this.serialisedValues());
this.snackBar.open('Successully copied to clipboard', 'Close', {
duration: 2000,
});
}
protected async save() {
const selectedFile = this.selectedFile();
if (typeof __TAURI__ !== 'undefined') {
const serialised = this.serialisedValues();
try {
// TODO: Tauri is bugged on mac atm, remove this when resolved: https://github.com/tauri-apps/tauri/issues/4633
if (!selectedFile?.browserFile && selectedFile?.path) {
await writeTextFile(selectedFile.path, serialised);
} else {
const filePath = await save({
defaultPath: `${this.selectedMessage().name}.json`,
});
if (filePath) {
await writeTextFile(filePath, serialised);
}
}
} catch (err) {
console.error(`Failed to save to file`, err);
}
} else {
this.downloader()?.nativeElement.click();
}
}
}