diff --git a/doc/screenshot.png b/doc/screenshot.png index 4661409..b87185a 100644 Binary files a/doc/screenshot.png and b/doc/screenshot.png differ diff --git a/src/app/app.component.html b/src/app/app.component.html index c367ec4..b0dbe92 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -254,8 +254,7 @@
- @@ -343,7 +342,8 @@ image - + +
+ + + + \ No newline at end of file diff --git a/src/app/import/import.component.spec.ts b/src/app/import/import.component.spec.ts new file mode 100644 index 0000000..8f82c81 --- /dev/null +++ b/src/app/import/import.component.spec.ts @@ -0,0 +1,28 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; + +import { ImportComponent } from './import.component'; + +describe('ImportComponent', () => { + let component: ImportComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ MatDialogModule, MatIconModule ], + declarations: [ ImportComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ImportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/import/import.component.ts b/src/app/import/import.component.ts new file mode 100644 index 0000000..aea2d78 --- /dev/null +++ b/src/app/import/import.component.ts @@ -0,0 +1,89 @@ +import { Component, Output, EventEmitter, OnInit, Inject } from '@angular/core'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { kDefaultPath } from '../app.component'; +import { StorageService } from '../storage.service'; +import { Svg } from '../svg'; + +export class DialogData { + path?: string; +} + +@Component({ + selector: 'app-import-dialog', + templateUrl: 'import-dialog.component.html' +}) +export class ImportDialogComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DialogData + ) { + } + + onConfirm(): void { + this.dialogRef.close(true); + } + onCancel(): void { + this.dialogRef.close(); + } +} + +@Component({ + selector: 'app-import', + template: '' +}) +export class ImportComponent implements OnInit { + private urlPath?: string; + @Output() importPath = new EventEmitter(); + + constructor( + public dialog: MatDialog, + public storageService: StorageService, + ) { + this.urlPath = this.readPath(); + } + + private readPath(): string { + const fragment = decodeURIComponent(window.location.hash.slice(1)); + const check = /^P=[mMlLvVhHcCsSqQtTaAzZ0-9\-e._]+$/; + if(check.test(fragment)) { + const path = fragment.slice(2).replace(/_/g, ' '); + try { + const _ = new Svg(path); + return path; + } catch (e) {} + } + return ''; + } + + ngOnInit() { + const openedPath = this.storageService.getPath(); + const unsavedChanges = openedPath && openedPath.path !== kDefaultPath; + if(this.urlPath && this.urlPath !== openedPath?.path) { + if(unsavedChanges) { + this.openDialog(); + } else { + this.finalize(true); + } + } + } + + openDialog(): void { + const dialogRef = this.dialog.open(ImportDialogComponent, { + width: '800px', + panelClass: 'dialog', + autoFocus: false, + data: {path: this.urlPath} + }); + + dialogRef.afterClosed().subscribe((result: boolean) => { + this.finalize(result); + }); + } + + private finalize(result: boolean): void { + if(result) { + this.importPath.emit(this.urlPath); + } + window.location.hash = ''; + } +} diff --git a/src/app/path-preview/path-preview.component.html b/src/app/path-preview/path-preview.component.html new file mode 100644 index 0000000..2c2fb1a --- /dev/null +++ b/src/app/path-preview/path-preview.component.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/path-preview/path-preview.component.spec.ts b/src/app/path-preview/path-preview.component.spec.ts new file mode 100644 index 0000000..43a34e4 --- /dev/null +++ b/src/app/path-preview/path-preview.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PathPreviewComponent } from './path-preview.component'; + +describe('PathPreviewComponent', () => { + let component: PathPreviewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PathPreviewComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PathPreviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/path-preview/path-preview.component.ts b/src/app/path-preview/path-preview.component.ts new file mode 100644 index 0000000..4ed2cfc --- /dev/null +++ b/src/app/path-preview/path-preview.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { browserComputePathBoundingBox } from '../svg-bbox'; + +@Component({ + selector: 'app-path-preview', + templateUrl: './path-preview.component.html' +}) +export class PathPreviewComponent implements OnInit { + @Input() x?: number; + @Input() y?: number; + @Input() width?: number; + @Input() height?: number; + + @Input() fillColor?: string = '#000000'; + @Input() strokeColor?: string; + @Input() strokeWidth?: number; + @Input() path = ''; + + constructor() { } + + ngOnInit(): void { + if(this.x === undefined || this.y === undefined || this.width === undefined || this.height === undefined) { + const bbox = browserComputePathBoundingBox(this.path); + this.x = bbox.x; + this.y = bbox.y; + this.width = bbox.width; + this.height = bbox.height; + } + } + + patternScale(containterWidth: number, containerHeight: number): number { + return Math.max((this.width??0) / containterWidth, (this.height??0) / containerHeight); + } + +} diff --git a/src/app/share/share-dialog-snackbar.component.html b/src/app/share/share-dialog-snackbar.component.html new file mode 100644 index 0000000..27b307d --- /dev/null +++ b/src/app/share/share-dialog-snackbar.component.html @@ -0,0 +1,4 @@ +
+ Copied to clipboard + content_pasted +
\ No newline at end of file diff --git a/src/app/share/share-dialog.component.html b/src/app/share/share-dialog.component.html new file mode 100644 index 0000000..3f1f531 --- /dev/null +++ b/src/app/share/share-dialog.component.html @@ -0,0 +1,19 @@ +

Share path as URL

+
+
+ +
+

The current path can be shared using the following link:

+ + URL + + + +
+
+
+
+ +
\ No newline at end of file diff --git a/src/app/share/share.component.html b/src/app/share/share.component.html new file mode 100644 index 0000000..0d07549 --- /dev/null +++ b/src/app/share/share.component.html @@ -0,0 +1,8 @@ + diff --git a/src/app/share/share.component.spec.ts b/src/app/share/share.component.spec.ts new file mode 100644 index 0000000..4bf1acc --- /dev/null +++ b/src/app/share/share.component.spec.ts @@ -0,0 +1,28 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; + +import { ShareComponent } from './share.component'; + +describe('ShareComponent', () => { + let component: ShareComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ MatDialogModule, MatIconModule ], + declarations: [ ShareComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ShareComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/share/share.component.ts b/src/app/share/share.component.ts new file mode 100644 index 0000000..233c362 --- /dev/null +++ b/src/app/share/share.component.ts @@ -0,0 +1,84 @@ +import { Component, Output, EventEmitter, Inject, Input, ViewChild, AfterViewInit, ElementRef } from '@angular/core'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { StorageService } from '../storage.service'; + +export class DialogData { + path?: string; +} + +@Component({ + selector: 'app-share-snackbar', + templateUrl: 'share-dialog-snackbar.component.html' +}) +export class ShareDialogSnackbarComponent {} + + +@Component({ + selector: 'app-share-dialog', + templateUrl: 'share-dialog.component.html' +}) +export class ShareDialogComponent implements AfterViewInit { + @ViewChild('input') inputField?: ElementRef; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DialogData, + private snackBar: MatSnackBar + ) { + } + + ngAfterViewInit(): void { + setTimeout(() => this.selectText()); + } + + private selectText(): void { + const el = this.inputField?.nativeElement; + el?.focus(); + el?.select(); + } + + onCancel(): void { + this.dialogRef.close(); + } + + copy(): void { + this.selectText(); + navigator.clipboard.writeText(this.inputField?.nativeElement.value); + this.snackBar.openFromComponent(ShareDialogSnackbarComponent, { + horizontalPosition:'center', + verticalPosition: 'top', + duration: 2000 + }); + } + + getUrl(): string { + const loc = window.location; + const fragment = this.data.path?.replace(/ +/g, '_'); + return `${loc.protocol}//${loc.host}${loc.pathname}#P=${fragment}`; + } +} + +@Component({ + selector: 'app-share', + templateUrl: './share.component.html' +}) +export class ShareComponent { + @Input() path: string = ''; + @Output() importPath = new EventEmitter(); + + constructor( + public dialog: MatDialog, + public storageService: StorageService, + ) { + } + + openDialog(): void { + const dialogRef = this.dialog.open(ShareDialogComponent, { + width: '800px', + panelClass: 'dialog', + autoFocus: false, + data: {path: this.path} + }); + } +} diff --git a/src/styles.scss b/src/styles.scss index b5ea762..43d6202 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -107,7 +107,7 @@ button.mat-flat-button { font-size: 12px; } -button.mat-icon-button { +button.mat-icon-button.more-button { width:20px; height:20px; line-height: 20px !important;