227 lines
6.7 KiB
TypeScript
227 lines
6.7 KiB
TypeScript
import {Component} from '@angular/core';
|
||
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
|
||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||
import {MatInputModule} from '@angular/material/input';
|
||
import {HeaderComponent} from '@header/header.component';
|
||
import {MatCheckboxModule} from '@angular/material/checkbox';
|
||
import {FormsModule} from '@angular/forms';
|
||
import {MatDividerModule} from '@angular/material/divider';
|
||
import {MatButtonModule} from '@angular/material/button';
|
||
|
||
interface Part {
|
||
text: string;
|
||
gradient: boolean;
|
||
colorA: string;
|
||
colorB: string;
|
||
continuation: boolean;
|
||
invalid?: boolean;
|
||
}
|
||
|
||
@Component({
|
||
selector: 'app-nick-generator',
|
||
templateUrl: './nick-generator.component.html',
|
||
styleUrls: ['./nick-generator.component.scss'],
|
||
imports: [
|
||
MatFormFieldModule,
|
||
MatInputModule,
|
||
HeaderComponent,
|
||
MatCheckboxModule,
|
||
FormsModule,
|
||
MatDividerModule,
|
||
MatButtonModule,
|
||
]
|
||
})
|
||
export class NickGeneratorComponent {
|
||
parts: Part[] = [
|
||
{text: '', gradient: false, colorA: '#ffffff', colorB: '#ffffff', continuation: false}
|
||
];
|
||
|
||
tryCmd = '';
|
||
requestCmd = '';
|
||
previewHtml: SafeHtml = '';
|
||
showPreview = false;
|
||
showCommands = false;
|
||
|
||
constructor(private sanitizer: DomSanitizer) {
|
||
}
|
||
|
||
addPart(): void {
|
||
this.parts.push({text: '', gradient: false, colorA: '#ffffff', colorB: '#ffffff', continuation: false});
|
||
this.onInputChanged();
|
||
}
|
||
|
||
deletePart(): void {
|
||
if (this.parts.length > 1) {
|
||
this.parts.pop();
|
||
// If last part was a gradient, unset continuation on new last part
|
||
if (this.parts.length > 0) this.parts[this.parts.length - 1].continuation = false;
|
||
this.onInputChanged();
|
||
}
|
||
}
|
||
|
||
onGradientToggle(i: number): void {
|
||
// Toggling gradient affects availability of continuation for this & next part
|
||
if (!this.parts[i].gradient) {
|
||
// If gradient turned off, force continuation off for this index (not visible anymore)
|
||
this.parts[i].continuation = false;
|
||
}
|
||
if (i + 1 < this.parts.length && !this.parts[i + 1].gradient) {
|
||
this.parts[i + 1].continuation = false;
|
||
}
|
||
this.onInputChanged();
|
||
}
|
||
|
||
onContinuationToggle(_: number): void {
|
||
this.onInputChanged();
|
||
}
|
||
|
||
onInputChanged(): void {
|
||
let result = '';
|
||
let preview = '';
|
||
let valid = true;
|
||
let nickLen = 0;
|
||
let prevColorB = '#ffffff';
|
||
|
||
for (let i = 0; i < this.parts.length; i++) {
|
||
const p = this.parts[i];
|
||
const len = p.text.length;
|
||
nickLen += len;
|
||
|
||
const partValid =
|
||
(p.gradient && len >= 1 && len <= 16) ||
|
||
(!p.gradient && len > 0);
|
||
|
||
p.invalid = !partValid;
|
||
if (!partValid) {
|
||
valid = false;
|
||
continue;
|
||
}
|
||
|
||
if (p.gradient) {
|
||
// Continuation allowed only if previous & current are gradient
|
||
const contAllowed = i > 0 && this.parts[i - 1].gradient;
|
||
const cont = p.continuation && contAllowed;
|
||
|
||
if (cont) {
|
||
result += p.text;
|
||
preview += this.generateGradient(p.text, prevColorB, p.colorB);
|
||
} else {
|
||
result += `{${p.colorA}>}${p.text}`;
|
||
preview += this.generateGradient(p.text, p.colorA, p.colorB);
|
||
}
|
||
|
||
// Add closing/continuation marker
|
||
const nextContinuation = (i + 1 < this.parts.length) && this.parts[i + 1].continuation;
|
||
if (i < this.parts.length - 1) {
|
||
result += `{${p.colorB}<>}`;
|
||
} else {
|
||
result += `{${p.colorB}<}`;
|
||
}
|
||
prevColorB = p.colorB;
|
||
} else {
|
||
// Solid
|
||
result += `{${p.colorA}}${p.text}`;
|
||
preview += this.generateSolidColor(p.text, p.colorA);
|
||
}
|
||
}
|
||
|
||
this.tryCmd = '';
|
||
this.requestCmd = '';
|
||
this.showPreview = false;
|
||
this.showCommands = false;
|
||
|
||
if (valid && result.length > 0 && nickLen >= 3 && nickLen <= 16) {
|
||
this.tryCmd = `/nick try ${result}`;
|
||
this.requestCmd = `/nick request ${result}`;
|
||
this.previewHtml = this.sanitizer.bypassSecurityTrustHtml(
|
||
this.generateSolidColor('Nickname preview: ', '#ffffff') + preview
|
||
);
|
||
this.showPreview = true;
|
||
this.showCommands = true;
|
||
} else {
|
||
if (!valid && (this.parts.length > 1 || nickLen > 0)) {
|
||
this.previewHtml = this.sanitizer.bypassSecurityTrustHtml(
|
||
this.generateSolidColor('Invalid part(s) length', '#dd0000')
|
||
);
|
||
} else if (valid && (nickLen < 3 || nickLen > 16)) {
|
||
this.previewHtml = this.sanitizer.bypassSecurityTrustHtml(
|
||
this.generateSolidColor('Nickname needs to be 3–16 chars', '#dd0000')
|
||
);
|
||
} else {
|
||
this.previewHtml = this.sanitizer.bypassSecurityTrustHtml('');
|
||
}
|
||
this.showPreview = nickLen > 0;
|
||
}
|
||
}
|
||
|
||
tryCommandButtonContent = 'Copy';
|
||
requestCommandButtonContent = 'Copy';
|
||
|
||
copy(text: string, button: 'try' | 'request'): void {
|
||
navigator.clipboard.writeText(text);
|
||
if (button === 'try') {
|
||
this.tryCommandButtonContent = 'Copied!';
|
||
} else if (button === 'request') {
|
||
this.requestCommandButtonContent = 'Copied!';
|
||
}
|
||
setTimeout(() => {
|
||
if (button === 'try') {
|
||
this.tryCommandButtonContent = 'Copy';
|
||
} else if (button === 'request') {
|
||
this.requestCommandButtonContent = 'Copy';
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
generateSolidColor(text: string, color: string): string {
|
||
return `<span style="color:${color}">${this.escape(text)}</span>`;
|
||
}
|
||
|
||
generateGradient(text: string, colorA: string, colorB: string): string {
|
||
const len = text.length;
|
||
if (len === 0) return '';
|
||
const a = this.hexToRgb(colorA);
|
||
const b = this.hexToRgb(colorB);
|
||
if (!a || !b) return this.generateSolidColor(text, colorA);
|
||
|
||
const stepR = len > 1 ? (b.r - a.r) / (len - 1) : 0;
|
||
const stepG = len > 1 ? (b.g - a.g) / (len - 1) : 0;
|
||
const stepB = len > 1 ? (b.b - a.b) / (len - 1) : 0;
|
||
|
||
let res = '';
|
||
for (let i = 0; i < len; i++) {
|
||
const r = a.r + stepR * i;
|
||
const g = a.g + stepG * i;
|
||
const bl = a.b + stepB * i;
|
||
res += this.generateSolidColor(text[i], this.rgbToHex(r, g, bl));
|
||
}
|
||
return res;
|
||
}
|
||
|
||
hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
||
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||
return m
|
||
? {r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16)}
|
||
: null;
|
||
}
|
||
|
||
componentToHex(c: number): string {
|
||
const x = Math.round(c);
|
||
const h = x.toString(16);
|
||
return h.length === 1 ? '0' + h : h;
|
||
}
|
||
|
||
rgbToHex(r: number, g: number, b: number): string {
|
||
return (
|
||
'#' + this.componentToHex(r) + this.componentToHex(g) + this.componentToHex(b)
|
||
);
|
||
}
|
||
|
||
escape(s: string): string {
|
||
return s
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
}
|
||
}
|