import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, OnChanges, OnInit, QueryList, SimpleChanges, ViewChild, ViewChildren } from '@angular/core'; import { CdkDrag, CdkDragDrop, CdkDropList, CdkDropListGroup, moveItemInArray, } from '@angular/cdk/drag-drop'; import { CommonModule } from '@angular/common'; import { WorldArea } from '../_models/world-area'; import { Plan, PlanElement } from '../_models/plan'; import { WorldAreaService } from '../_services/world-area.service'; import { FormsModule } from '@angular/forms'; import { Fuzzr } from '../fuzzr/fuzzr'; import { BehaviorSubject, first, from, skip } from 'rxjs'; import { save } from '@tauri-apps/plugin-dialog'; import { PlanService } from '../_services/plan.service'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { EditNotesComponentDialog } from './notes/notes.component'; import { open } from '@tauri-apps/plugin-dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatButtonModule } from '@angular/material/button'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; interface Act { value: number; name: string; } @Component({ selector: 'plan-editor', templateUrl: './editor.component.html', styleUrls: ['./editor.component.scss'], standalone: true, imports: [ CommonModule, FormsModule, CdkDropListGroup, CdkDropList, CdkDrag, MatDialogModule, MatFormFieldModule, MatInputModule, MatSelectModule, MatButtonModule, MatSlideToggleModule, ], providers: [] }) export class EditorComponent implements OnInit { areas?: WorldArea[]; planAreas: WorldArea[]; areasMap?: Map; areaSearchString: string = ""; planSearchString: string = ""; planFuzzer: Fuzzr; filterAct: Act; planFilterAct: Act; acts: Act[]; @ViewChild('planListElement') planListElement!: ElementRef; autoScrollToEnd: boolean; reverseDisplay: boolean; disabledPlanDD: boolean; original: PlanElement[] = []; latestList: BehaviorSubject = new BehaviorSubject([]); constructor(public worldAreaService: WorldAreaService, private cdr: ChangeDetectorRef, private planService: PlanService, public dialog: MatDialog) { this.disabledPlanDD = false; this.autoScrollToEnd = false; this.latestList = new BehaviorSubject([]); this.planFuzzer = new Fuzzr(this.original, { toString: (e: PlanElement) => { return this.areasMap?.get(e.area_key)?.name; } }); this.planAreas = []; this.reverseDisplay = false; this.acts = []; for (let i = 0; i <= 10; i++) { if (i == 0) { this.acts.push({ value: i, name: "All" }); } else { this.acts.push({ value: i, name: "Act " + i.toString() }); } } this.filterAct = this.acts[0]; this.planFilterAct = this.acts[0]; } planSearchStringChange(value: string) { this.planSearchString = value; this.filterPlanElements(); } planFilterActChange(value: Act) { this.planFilterAct = value; this.filterPlanElements(); } reverseDisplayChange(value: boolean) { this.reverseDisplay = value; this.filterPlanElements(); } ngOnInit(): void { this.worldAreaService.getWorldAreas().subscribe(worldAreas => { this.areas = [...worldAreas.values()]; this.areasMap = worldAreas; }); } sortPredicate(index: number, _item: CdkDrag | CdkDrag) { return !(this.planElemFilterBounds() && index == 0) } dropHandler(event: CdkDragDrop | CdkDragDrop) { if (event.previousContainer === event.container && !isWorldAreaEvent(event)) { const realCurrent = this.original.indexOf(event.previousContainer.data[event.currentIndex]); const realPrev = this.original.indexOf(event.previousContainer.data[event.previousIndex]); moveItemInArray(this.original, realPrev, realCurrent); } else { if (this.areas && isWorldAreaEvent(event)) { if (event.container.data.length > 0 && 'connections_world_areas_keys' in event.container.data[0]) { return; } const bounds = this.planElemFilterBounds(); let index = event.currentIndex; if (bounds) { index += bounds[0]; } this.original.splice(index, 0, this.planItemFromArea(event.previousContainer.data[event.previousIndex])); } } this.latestList.pipe(skip(1)).pipe(first()).subscribe(() => { this.scrollToEnd(); }); this.filterPlanElements(); } dropEndHandler(event: CdkDragDrop | CdkDragDrop | null) { if (event == null) return; if (isWorldAreaEvent(event) && this.areas) { this.original.splice(this.getEnd(), 0, this.planItemFromArea(event.previousContainer.data[event.previousIndex])); } else { moveItemInArray(this.original, event.previousIndex, this.getEnd()); } this.latestList.pipe(skip(1)).pipe(first()).subscribe(() => { this.scrollToEnd(); }); this.filterPlanElements(); } getEnd() { let bounds = this.planElemFilterBounds(); if (bounds) { return bounds[1]; } else { return this.original.length; } } remove(item: PlanElement) { this.original.splice(this.planIndexOf(item), 1); this.filterPlanElements(); } canDrop = () => { return !this.disabledPlanDD; } planItemFromArea(area: WorldArea): PlanElement { return { area_key: area.named_id, notes: undefined, uuid: undefined, edited: false }; } filterAreas() { if (this.areaSearchString !== "" || this.filterAct.value != 0) { return this.worldAreaService.matcher!.search(this.areaSearchString).map(({ item }) => { return item[1]; }).filter(item => item.act == this.filterAct.value || this.filterAct.value == 0); } else { return this.areas!; } } scrollToEnd() { if (!this.autoScrollToEnd) { return; } this.cdr.detectChanges(); if (!this.reverseDisplay) { this.planListElement.nativeElement.scrollTop = this.planListElement.nativeElement.scrollHeight; } else { this.planListElement.nativeElement.scrollTop = 0; } } doubleClickArea(item: WorldArea) { this.original.splice(this.original.length, 0, this.planItemFromArea(item)); this.latestList.pipe(skip(1)).pipe(first()).subscribe((_) => { this.scrollToEnd(); }); this.filterPlanElements(); } planElemFilterBounds() { if (this.planFilterAct.value !== 0) { let bounds = this.original.filter(item => item.anchor_act === this.planFilterAct.value || this.planFilterAct.value + 1 === item.anchor_act).map((value) => this.planIndexOf(value)); if (bounds.length == 2) { return bounds; } if (bounds.length == 1 && this.planFilterAct.value == 10) { bounds[1] = this.original.length; return bounds; } } return undefined; } filterPlanElements() { const value = (): any[] => { if (this.planSearchString !== "") { this.disabledPlanDD = true; } else { this.disabledPlanDD = false; } if (this.planSearchString !== "" || this.planFilterAct.value != 0) { let bounds = this.planElemFilterBounds(); const searched = this.planFuzzer.search(this.planSearchString).map(({ item }) => item); if (bounds) { const definedBounds: number[] = bounds; return searched.filter(item => { const index = this.planIndexOf(item); return index >= definedBounds[0] && index < definedBounds[1]; }); } else { return searched.filter(item => { return this.areasMap?.get(item.area_key)?.act === this.planFilterAct.value || this.planFilterAct.value === 0; }); } } else { return this.original; } } this.latestList.next([... (this.reverseDisplay ? value().slice().reverse() : value())]); } planIndexOf(planElement: PlanElement) { return this.original.indexOf(planElement); } clearPlan() { while (this.original.length > 0) { this.original.pop(); } this.filterPlanElements(); this.cdr.detectChanges(); } save() { from(save({ filters: [{ name: 'JSON (.json)', extensions: ['json'] }] })).subscribe(file => { if (file) { const plan = new Plan(); plan.plan = [...this.original]; this.planService.savePlanAtPath(file, plan).subscribe(); } }); } openPlan() { from(open({ multiple: false, filters: [ { name: "JSON (.json)", extensions: ['json'] } ] })).subscribe(file => { if (file) { // We disallow multiple but interface still says it can be multiple, thus the cast. this.planService.loadPlanFromPath(file.path as string, false).subscribe(plan => { while (this.original.length > 0) { this.original.pop(); } plan.plan.forEach(item => this.original.push(item)); this.filterPlanElements(); }); } }); } loadBasePlan() { this.planService.getBasePlan().subscribe(plan => { while (this.original.length > 0) { this.original.pop(); } plan.plan.forEach(item => this.original.push(item)); this.filterPlanElements(); }) } addNote(event: MouseEvent, item: PlanElement) { event.preventDefault(); const dialogRef = this.dialog.open(EditNotesComponentDialog, { data: { note: item.notes }, disableClose: true },) dialogRef.afterClosed().subscribe(note => { if (note != undefined && note != null) { if (item.notes !== note) { item.edited = true; } item.notes = note; } }) } } function isWorldAreaEvent(event: any): event is CdkDragDrop { return (event.previousContainer.data.length > 0 && 'connections_world_areas_keys' in event.previousContainer.data[0]); }