Extract display name logic to NameFormatService, refactor colored name parsing, and improve channel list scrolling.
This commit is contained in:
parent
5987ee4133
commit
5be3072b26
|
|
@ -11,7 +11,7 @@
|
||||||
[class.selected]="selectedChannel()?.name === channel.name && selectedChannel()?.type === channel.type"
|
[class.selected]="selectedChannel()?.name === channel.name && selectedChannel()?.type === channel.type"
|
||||||
(click)="selectedChannelChange.emit(channel)">
|
(click)="selectedChannelChange.emit(channel)">
|
||||||
<span class="channel-name">
|
<span class="channel-name">
|
||||||
@for (segment of parseColoredName(getDisplayName(channel)); track $index) {
|
@for (segment of getDisplayName(channel); track $index) {
|
||||||
<span [style.color]="segment.color">{{ segment.text }}</span>
|
<span [style.color]="segment.color">{{ segment.text }}</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
.channel-list {
|
.channel-list {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #172133;
|
background-color: #172133;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
.channel {
|
.channel {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -44,8 +47,13 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.scroll {
|
.scroll {
|
||||||
overflow: scroll;
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(255, 255, 255, 0.35) transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
import {Component, computed, effect, inject, input, output, signal} from '@angular/core';
|
import {Component, computed, effect, inject, input, output, signal} from '@angular/core';
|
||||||
import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object';
|
import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object';
|
||||||
import {CircleSvgComponent} from '@pages/altitude/chat/svg/circle.svg';
|
import {CircleSvgComponent} from '@pages/altitude/chat/svg/circle.svg';
|
||||||
import {ChatService} from '@pages/altitude/chat/service/chat.service';
|
import {NameFormatService} from '@pages/altitude/chat/service/name-format.service';
|
||||||
|
import {ColoredNameSegment} from '@pages/altitude/chat/objects/colored-name-segment.object';
|
||||||
interface ColoredNameSegment {
|
|
||||||
text: string;
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-channel-list',
|
selector: 'app-channel-list',
|
||||||
|
|
@ -17,7 +13,7 @@ interface ColoredNameSegment {
|
||||||
styleUrl: './channel-list.component.scss'
|
styleUrl: './channel-list.component.scss'
|
||||||
})
|
})
|
||||||
export class ChannelListComponent {
|
export class ChannelListComponent {
|
||||||
protected readonly chatService = inject(ChatService);
|
protected readonly nameFormatService: NameFormatService = inject(NameFormatService);
|
||||||
public readonly channels = input.required<ChatChannel[]>();
|
public readonly channels = input.required<ChatChannel[]>();
|
||||||
public readonly selectedChannel = input.required<ChatChannel | null>();
|
public readonly selectedChannel = input.required<ChatChannel | null>();
|
||||||
public readonly selectedChannelChange = output<ChatChannel>();
|
public readonly selectedChannelChange = output<ChatChannel>();
|
||||||
|
|
@ -58,121 +54,7 @@ export class ChannelListComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getDisplayName(channel: ChatChannel): string {
|
protected getDisplayName(chatChannel: ChatChannel): ColoredNameSegment[] {
|
||||||
if (channel.type === 'PARTY') {
|
return this.nameFormatService.getDisplayName(chatChannel);
|
||||||
return this.chatService.partieMap().get(channel.name) ?? channel.name;
|
|
||||||
}
|
}
|
||||||
if (channel.type === 'DM' || channel.type === 'SPY') {
|
|
||||||
return this.chatService.userNameMap().get(channel.name) ?? channel.name;
|
|
||||||
}
|
|
||||||
return channel.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected parseColoredName(name: string): ColoredNameSegment[] {
|
|
||||||
const markerRegex = /\{#([0-9a-fA-F]{6})(<>|[<>]?)}/g;
|
|
||||||
const segments: ColoredNameSegment[] = [];
|
|
||||||
let currentColor: string | undefined;
|
|
||||||
let gradientStartColor: string | undefined;
|
|
||||||
let gradientStartSegmentIndex: number | undefined;
|
|
||||||
let lastIndex = 0;
|
|
||||||
let match: RegExpExecArray | null;
|
|
||||||
|
|
||||||
while ((match = markerRegex.exec(name)) !== null) {
|
|
||||||
this.addColoredTextSegment(segments, name.slice(lastIndex, match.index), currentColor);
|
|
||||||
|
|
||||||
const markerColor = `#${match[1]}`;
|
|
||||||
const markerType = match[2];
|
|
||||||
|
|
||||||
if (markerType === '>') {
|
|
||||||
currentColor = markerColor;
|
|
||||||
gradientStartColor = markerColor;
|
|
||||||
gradientStartSegmentIndex = segments.length;
|
|
||||||
} else if (markerType === '<') {
|
|
||||||
if (gradientStartColor !== undefined && gradientStartSegmentIndex !== undefined) {
|
|
||||||
this.applyGradient(segments, gradientStartSegmentIndex, gradientStartColor, markerColor);
|
|
||||||
}
|
|
||||||
currentColor = markerColor;
|
|
||||||
gradientStartColor = undefined;
|
|
||||||
gradientStartSegmentIndex = undefined;
|
|
||||||
} else if (markerType === '<>') {
|
|
||||||
if (gradientStartColor !== undefined && gradientStartSegmentIndex !== undefined) {
|
|
||||||
this.applyGradient(segments, gradientStartSegmentIndex, gradientStartColor, markerColor);
|
|
||||||
}
|
|
||||||
currentColor = markerColor;
|
|
||||||
gradientStartColor = markerColor;
|
|
||||||
gradientStartSegmentIndex = segments.length;
|
|
||||||
} else {
|
|
||||||
currentColor = markerColor;
|
|
||||||
gradientStartColor = undefined;
|
|
||||||
gradientStartSegmentIndex = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastIndex = markerRegex.lastIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addColoredTextSegment(segments, name.slice(lastIndex), currentColor);
|
|
||||||
|
|
||||||
return segments.length > 0 ? segments : [{text: name}];
|
|
||||||
}
|
|
||||||
|
|
||||||
private addColoredTextSegment(segments: ColoredNameSegment[], text: string, color?: string): void {
|
|
||||||
if (text.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
segments.push({text, color});
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyGradient(
|
|
||||||
segments: ColoredNameSegment[],
|
|
||||||
startSegmentIndex: number,
|
|
||||||
startColor: string,
|
|
||||||
endColor: string
|
|
||||||
): void {
|
|
||||||
const text = segments.slice(startSegmentIndex).map((segment) => segment.text).join('');
|
|
||||||
const length = text.length;
|
|
||||||
const start = this.hexToRgb(startColor);
|
|
||||||
const end = this.hexToRgb(endColor);
|
|
||||||
|
|
||||||
if (length === 0 || start === null || end === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gradientSegments: ColoredNameSegment[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
const ratio = length === 1 ? 0 : i / (length - 1);
|
|
||||||
gradientSegments.push({
|
|
||||||
text: text[i],
|
|
||||||
color: this.rgbToHex(
|
|
||||||
start.r + (end.r - start.r) * ratio,
|
|
||||||
start.g + (end.g - start.g) * ratio,
|
|
||||||
start.b + (end.b - start.b) * ratio
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
segments.splice(startSegmentIndex, segments.length - startSegmentIndex, ...gradientSegments);
|
|
||||||
}
|
|
||||||
|
|
||||||
private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
||||||
const match = /^#?([0-9a-fA-F]{6})$/.exec(hex);
|
|
||||||
if (!match) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = Number.parseInt(match[1], 16);
|
|
||||||
return {
|
|
||||||
r: (value >> 16) & 255,
|
|
||||||
g: (value >> 8) & 255,
|
|
||||||
b: value & 255
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private rgbToHex(r: number, g: number, b: number): string {
|
|
||||||
return `#${[r, g, b]
|
|
||||||
.map((value) => Math.round(value).toString(16).padStart(2, '0'))
|
|
||||||
.join('')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
@if (selectedChannel(); as chatChannel) {
|
||||||
<div class="full-width"
|
<div class="full-width"
|
||||||
[style.background-color]="getServerColor(selectedChannel()?.name ?? '')">
|
[style.background-color]="getServerColor(chatChannel.name ?? '')">
|
||||||
<span class="channel" [style.color]="getServerTextColor(selectedChannel()?.name ?? '')">
|
<span class="channel" [style.color]="getServerTextColor(chatChannel.name ?? '')">
|
||||||
{{ selectedChannel()?.name }}
|
@for (segment of getDisplayName(chatChannel); track $index) {
|
||||||
|
<span [style.color]="segment.color">{{ segment.text }}</span>
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
<div class="notification-icon">
|
<div class="notification-icon">
|
||||||
@if (notificationEnabled()) {
|
@if (notificationEnabled()) {
|
||||||
|
|
@ -11,3 +14,4 @@
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import {NotificationService} from '@pages/altitude/chat/service/chat-notificatio
|
||||||
import {NotificationEnabledSvgComponent} from '@pages/altitude/chat/svg/notification-enabled.svg';
|
import {NotificationEnabledSvgComponent} from '@pages/altitude/chat/svg/notification-enabled.svg';
|
||||||
import {NotificationDisabledSvgComponent} from '@pages/altitude/chat/svg/notification-disabled.svg';
|
import {NotificationDisabledSvgComponent} from '@pages/altitude/chat/svg/notification-disabled.svg';
|
||||||
import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object';
|
import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object';
|
||||||
|
import {ColoredNameSegment} from '@pages/altitude/chat/objects/colored-name-segment.object';
|
||||||
|
import {NameFormatService} from '@pages/altitude/chat/service/name-format.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-chat-info',
|
selector: 'app-chat-info',
|
||||||
|
|
@ -16,6 +18,7 @@ import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object';
|
||||||
})
|
})
|
||||||
export class ChatInfoComponent {
|
export class ChatInfoComponent {
|
||||||
private readonly notificationService: NotificationService = inject(NotificationService);
|
private readonly notificationService: NotificationService = inject(NotificationService);
|
||||||
|
private readonly nameFormatService: NameFormatService = inject(NameFormatService);
|
||||||
|
|
||||||
public readonly selectedChannel = input.required<ChatChannel | null>();
|
public readonly selectedChannel = input.required<ChatChannel | null>();
|
||||||
protected readonly channelKey = computed(() => {
|
protected readonly channelKey = computed(() => {
|
||||||
|
|
@ -34,4 +37,8 @@ export class ChatInfoComponent {
|
||||||
this.notificationService.disableNotifications(this.channelKey());
|
this.notificationService.disableNotifications(this.channelKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getDisplayName(chatChannel: ChatChannel): ColoredNameSegment[] {
|
||||||
|
return this.nameFormatService.getDisplayName(chatChannel);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface ColoredNameSegment {
|
||||||
|
text: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
import {inject, Injectable} from '@angular/core';
|
||||||
|
import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object';
|
||||||
|
import {ColoredNameSegment} from '@pages/altitude/chat/objects/colored-name-segment.object';
|
||||||
|
import {ChatService} from "./chat.service";
|
||||||
|
|
||||||
|
@Injectable({providedIn: 'root'})
|
||||||
|
export class NameFormatService {
|
||||||
|
|
||||||
|
private readonly chatService: ChatService = inject(ChatService);
|
||||||
|
|
||||||
|
public getDisplayName(channel: ChatChannel): ColoredNameSegment[] {
|
||||||
|
return this.parseColoredName(this.getNameFromType(channel));
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNameFromType(channel: ChatChannel): string {
|
||||||
|
if (channel.type === 'PARTY') {
|
||||||
|
return this.chatService.partieMap().get(channel.name) ?? channel.name;
|
||||||
|
}
|
||||||
|
if (channel.type === 'DM' || channel.type === 'SPY') {
|
||||||
|
return this.chatService.userNameMap().get(channel.name) ?? channel.name;
|
||||||
|
}
|
||||||
|
return channel.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseColoredName(name: string): ColoredNameSegment[] {
|
||||||
|
const markerRegex = /\{#([0-9a-fA-F]{6})(<>|[<>]?)}/g;
|
||||||
|
const segments: ColoredNameSegment[] = [];
|
||||||
|
let currentColor: string | undefined;
|
||||||
|
let gradientStartColor: string | undefined;
|
||||||
|
let gradientStartSegmentIndex: number | undefined;
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((match = markerRegex.exec(name)) !== null) {
|
||||||
|
this.addColoredTextSegment(segments, name.slice(lastIndex, match.index), currentColor);
|
||||||
|
|
||||||
|
const markerColor = `#${match[1]}`;
|
||||||
|
const markerType = match[2];
|
||||||
|
|
||||||
|
if (markerType === '>') {
|
||||||
|
currentColor = markerColor;
|
||||||
|
gradientStartColor = markerColor;
|
||||||
|
gradientStartSegmentIndex = segments.length;
|
||||||
|
} else if (markerType === '<') {
|
||||||
|
if (gradientStartColor !== undefined && gradientStartSegmentIndex !== undefined) {
|
||||||
|
this.applyGradient(segments, gradientStartSegmentIndex, gradientStartColor, markerColor);
|
||||||
|
}
|
||||||
|
currentColor = markerColor;
|
||||||
|
gradientStartColor = undefined;
|
||||||
|
gradientStartSegmentIndex = undefined;
|
||||||
|
} else if (markerType === '<>') {
|
||||||
|
if (gradientStartColor !== undefined && gradientStartSegmentIndex !== undefined) {
|
||||||
|
this.applyGradient(segments, gradientStartSegmentIndex, gradientStartColor, markerColor);
|
||||||
|
}
|
||||||
|
currentColor = markerColor;
|
||||||
|
gradientStartColor = markerColor;
|
||||||
|
gradientStartSegmentIndex = segments.length;
|
||||||
|
} else {
|
||||||
|
currentColor = markerColor;
|
||||||
|
gradientStartColor = undefined;
|
||||||
|
gradientStartSegmentIndex = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = markerRegex.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addColoredTextSegment(segments, name.slice(lastIndex), currentColor);
|
||||||
|
|
||||||
|
return segments.length > 0 ? segments : [{text: name}];
|
||||||
|
}
|
||||||
|
|
||||||
|
private addColoredTextSegment(segments: ColoredNameSegment[], text: string, color?: string): void {
|
||||||
|
if (text.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push({text, color});
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyGradient(
|
||||||
|
segments: ColoredNameSegment[],
|
||||||
|
startSegmentIndex: number,
|
||||||
|
startColor: string,
|
||||||
|
endColor: string
|
||||||
|
): void {
|
||||||
|
const text = segments.slice(startSegmentIndex).map((segment) => segment.text).join('');
|
||||||
|
const length = text.length;
|
||||||
|
const start = this.hexToRgb(startColor);
|
||||||
|
const end = this.hexToRgb(endColor);
|
||||||
|
|
||||||
|
if (length === 0 || start === null || end === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gradientSegments: ColoredNameSegment[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const ratio = length === 1 ? 0 : i / (length - 1);
|
||||||
|
gradientSegments.push({
|
||||||
|
text: text[i],
|
||||||
|
color: this.rgbToHex(
|
||||||
|
start.r + (end.r - start.r) * ratio,
|
||||||
|
start.g + (end.g - start.g) * ratio,
|
||||||
|
start.b + (end.b - start.b) * ratio
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.splice(startSegmentIndex, segments.length - startSegmentIndex, ...gradientSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
||||||
|
const match = /^#?([0-9a-fA-F]{6})$/.exec(hex);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = Number.parseInt(match[1], 16);
|
||||||
|
return {
|
||||||
|
r: (value >> 16) & 255,
|
||||||
|
g: (value >> 8) & 255,
|
||||||
|
b: value & 255
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private rgbToHex(r: number, g: number, b: number): string {
|
||||||
|
return `#${[r, g, b]
|
||||||
|
.map((value) => Math.round(value).toString(16).padStart(2, '0'))
|
||||||
|
.join('')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ export function getServerColor(server: string) {
|
||||||
} else if (server === 'creative') {
|
} else if (server === 'creative') {
|
||||||
return "#FFA500";
|
return "#FFA500";
|
||||||
}
|
}
|
||||||
return "white";
|
return "#0290e8";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServerTextColor(server: string) {
|
export function getServerTextColor(server: string) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user