Allowing the user to easily edit the channel name mode

master
WolverinDEV 2021-01-22 17:38:23 +01:00
parent f80b9e9caa
commit 65d7051819
6 changed files with 177 additions and 30 deletions

View File

@ -114,55 +114,73 @@ export interface ChannelEvents extends ChannelTreeEntryEvents {
notify_description_changed: {}
}
export class ParsedChannelName {
export type ChannelNameAlignment = "center" | "right" | "left" | "normal" | "repetitive";
export class ChannelNameParser {
readonly originalName: string;
alignment: "center" | "right" | "left" | "normal" | "repetitive";
alignment: ChannelNameAlignment;
text: string; /* does not contain any alignment codes */
uniqueId: string;
constructor(name: string, hasParentChannel: boolean) {
this.originalName = name;
this.parse(hasParentChannel);
}
private parse(has_parent_channel: boolean) {
private parse(hasParentChannel: boolean) {
this.alignment = "normal";
if(this.originalName.length < 3) {
this.text = this.originalName;
return;
}
parse_type:
if(!has_parent_channel && this.originalName.charAt(0) == '[') {
parseType:
if(!hasParentChannel && this.originalName.charAt(0) == '[') {
let end = this.originalName.indexOf(']');
if(end === -1) break parse_type;
if(end === -1) {
break parseType;
}
let options = this.originalName.substr(1, end - 1);
if(options.indexOf("spacer") === -1) break parse_type;
options = options.substr(0, options.indexOf("spacer"));
const spacerIndex = options.indexOf("spacer");
if(spacerIndex === -1) break parseType;
this.uniqueId = options.substring(spacerIndex + 6);
options = options.substr(0, spacerIndex);
if(options.length == 0)
if(options.length == 0) {
options = "l";
else if(options.length > 1)
} else if(options.length > 1) {
options = options[0];
}
switch (options) {
case "r":
this.alignment = "right";
break;
case "l":
this.alignment = "left";
break;
case "c":
this.alignment = "center";
break;
case "*":
this.alignment = "repetitive";
break;
default:
break parse_type;
break parseType;
}
this.text = this.originalName.substr(end + 1);
}
if(!this.text && this.alignment === "normal")
if(!this.text && this.alignment === "normal") {
this.text = this.originalName;
}
}
}
export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
@ -177,7 +195,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
readonly events: Registry<ChannelEvents>;
parsed_channel_name: ParsedChannelName;
parsed_channel_name: ChannelNameParser;
private _family_index: number = 0;
@ -206,7 +224,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
this.properties = new ChannelProperties();
this.channelId = channelId;
this.properties.channel_name = channelName;
this.parsed_channel_name = new ParsedChannelName(channelName, false);
this.parsed_channel_name = new ChannelNameParser(channelName, false);
this.clientPropertyChangedListener = (event: ClientEvents["notify_properties_updated"]) => {
if("client_nickname" in event.updated_properties || "client_talk_power" in event.updated_properties) {
@ -627,7 +645,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
if(hasUpdate) {
if(key == "channel_name") {
this.parsed_channel_name = new ParsedChannelName(value, this.hasParent());
this.parsed_channel_name = new ChannelNameParser(value, this.hasParent());
} else if(key == "channel_order") {
let order = this.channelTree.findChannel(this.properties.channel_order);
this.channelTree.moveChannel(this, order, this.parent, false);

View File

@ -1,4 +1,4 @@
import {ChannelEntry, ChannelProperties, ChannelSidebarMode} from "tc-shared/tree/Channel";
import {ChannelEntry, ChannelNameParser, ChannelProperties, ChannelSidebarMode} from "tc-shared/tree/Channel";
import {ChannelEditableProperty} from "tc-shared/ui/modal/channel-edit/Definitions";
import {ChannelTree} from "tc-shared/tree/ChannelTree";
import {ServerFeature} from "tc-shared/connection/ServerFeatures";
@ -17,7 +17,41 @@ const SimplePropertyProvider = <P extends keyof ChannelProperties>(channelProper
};
}
ChannelPropertyProviders["name"] = SimplePropertyProvider("channel_name", "");
ChannelPropertyProviders["name"] = {
provider: async (properties, channel, parentChannel, channelTree) => {
let spacerUniqueId = 0;
const hasParent = !!(channel?.hasParent() || parentChannel);
if(!hasParent) {
const channels = channelTree.rootChannel();
while(true) {
let matchFound = false;
for(const channel of channels) {
if(channel.parsed_channel_name.uniqueId === spacerUniqueId.toString()) {
matchFound = true;
break;
}
}
if(!matchFound) {
break;
}
spacerUniqueId++;
}
}
const parsed = new ChannelNameParser(properties.channel_name, hasParent);
return {
rawName: properties.channel_name,
spacerUniqueId: parsed.uniqueId || spacerUniqueId.toString(),
hasParent,
maxNameLength: 30 - (parsed.originalName.length - parsed.text.length),
parsedAlignment: parsed.alignment,
parsedName: parsed.text
}
},
applier: (value, properties) => properties.channel_name = value.rawName
}
ChannelPropertyProviders["phoneticName"] = SimplePropertyProvider("channel_name_phonetic", "");
ChannelPropertyProviders["icon"] = {
provider: async (properties, _channel, _parentChannel, channelTree) => {

View File

@ -32,7 +32,14 @@ export type ChannelEditPermissionsState = {
};
export interface ChannelEditableProperty {
"name": string,
"name": {
rawName: string,
parsedName?: string,
parsedAlignment?: "center" | "right" | "left" | "normal" | "repetitive",
maxNameLength?: number,
hasParent?: boolean,
spacerUniqueId?: string
},
"phoneticName": string,
"icon": {

View File

@ -40,6 +40,26 @@
}
}
.channelName {
display: flex;
flex-direction: row;
justify-content: stretch;
width: 100%;
min-width: 10em;
.select {
margin-right: 1em;
width: 10em;
}
&.hasParent {
.select {
display: none;
}
}
}
.buttons {
margin-top: 1em;

View File

@ -1,4 +1,3 @@
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
import * as React from "react";
import {useContext, useEffect, useRef, useState} from "react";
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
@ -16,7 +15,7 @@ import {Switch} from "tc-shared/ui/react-elements/Switch";
import {Button} from "tc-shared/ui/react-elements/Button";
import {Tab, TabEntry} from "tc-shared/ui/react-elements/Tab";
import {Settings, settings} from "tc-shared/settings";
import {useTr} from "tc-shared/ui/react-elements/Helper";
import {joinClassList, useTr} from "tc-shared/ui/react-elements/Helper";
import {IconTooltip} from "tc-shared/ui/react-elements/Tooltip";
import {RadioButton} from "tc-shared/ui/react-elements/RadioButton";
import {Slider} from "tc-shared/ui/react-elements/Slider";
@ -24,6 +23,7 @@ import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
import {getIconManager} from "tc-shared/file/Icons";
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
import {ChannelNameAlignment, ChannelNameParser} from "tc-shared/tree/Channel";
const cssStyle = require("./Renderer.scss");
@ -122,6 +122,10 @@ function useValidationState<T extends keyof ChannelEditableProperty>(property: T
return valid;
}
const ChannelNameType = (props: { selected: ChannelNameAlignment }) => {
}
const ChannelName = React.memo(() => {
const modalType = useContext(ModalTypeContext);
@ -129,16 +133,72 @@ const ChannelName = React.memo(() => {
const editable = usePropertyPermission("name", modalType === "channel-create");
const valid = useValidationState("name");
const refSelect = useRef<HTMLSelectElement>();
const setValue = (text: string | undefined, localOnly: boolean) => {
let rawName;
switch(propertyValue.hasParent ? "normal" : refSelect.current.value) {
case "center":
rawName = "[cspacer" + propertyValue.spacerUniqueId + "]" + text;
break;
case "left":
rawName = "[lspacer" + propertyValue.spacerUniqueId + "]" + text;
break;
case "right":
rawName = "[rspacer" + propertyValue.spacerUniqueId + "]" + text;
break;
case "repetitive":
rawName ="[*spacer" + propertyValue.spacerUniqueId + "]" + text;
break;
default:
case "normal":
rawName = text;
break;
}
setPropertyValue({
rawName,
parsedName: text,
hasParent: propertyValue.hasParent,
spacerUniqueId: propertyValue.spacerUniqueId,
maxNameLength: propertyValue.maxNameLength,
parsedAlignment: propertyValue.parsedAlignment
}, localOnly);
}
return (
<div className={joinClassList(cssStyle.channelName, propertyValue?.hasParent && cssStyle.hasParent)}>
<Select
value={propertyValue?.parsedAlignment || "loading"}
className={cssStyle.select}
onChange={() => setValue(propertyValue.parsedName, false)}
type={"boxed"}
title={useTr("Channel name mode")}
refSelect={refSelect}
>
<option value={"loading"} style={{ display: "none" }}>{useTr("loading")}</option>
<option value={"normal"}>{useTr("Normal Name")}</option>
<option value={"center"}>{useTr("Centered")}</option>
<option value={"left"}>{useTr("Left Aligned")}</option>
<option value={"right"}>{useTr("Right Aligned")}</option>
<option value={"repetitive"}>{useTr("Repetitive")}</option>
</Select>
<BoxedInputField
className={cssStyle.input}
disabled={!editable || propertyState !== "normal"}
value={propertyValue || ""}
value={(propertyValue?.hasParent ? propertyValue?.rawName : propertyValue?.parsedName) || ""}
placeholder={propertyState === "normal" ? tr("Channel name") : tr("loading")}
onInput={value => setPropertyValue(value, true)}
onChange={value => setPropertyValue(value)}
onInput={value => setValue(value, true)}
onChange={value => setValue(value, false)}
isInvalid={!valid}
maxLength={propertyValue?.maxNameLength}
/>
</div>
);
});

View File

@ -23,6 +23,7 @@ export interface BoxedInputFieldProperties {
isInvalid?: boolean;
className?: string;
maxLength?: number,
size?: "normal" | "large" | "small";
type?: "text" | "password" | "number";
@ -86,6 +87,7 @@ export class BoxedInputField extends React.Component<BoxedInputFieldProperties,
disabled={this.state.disabled || this.props.disabled}
onInput={this.props.onInput && (event => this.props.onInput(event.currentTarget.value))}
onKeyDown={e => this.onKeyDown(e)}
maxLength={this.props.maxLength}
/>
}
{this.props.suffix ? <a key={"suffix"} className={cssStyle.suffix}>{this.props.suffix}</a> : undefined}
@ -399,6 +401,7 @@ export const ControlledSelect = (props: {
export interface SelectProperties {
type?: "flat" | "boxed";
refSelect?: React.RefObject<HTMLSelectElement>,
defaultValue?: string;
value?: string;
@ -416,6 +419,8 @@ export interface SelectProperties {
disabled?: boolean;
editable?: boolean;
title?: string,
onFocus?: () => void;
onBlur?: () => void;
@ -430,11 +435,13 @@ export interface SelectFieldState {
}
export class Select extends React.Component<SelectProperties, SelectFieldState> {
private refSelect = React.createRef<HTMLSelectElement>();
private refSelect;
constructor(props) {
super(props);
this.refSelect = this.props.refSelect || React.createRef<HTMLSelectElement>();
this.state = {
isInvalid: false,
invalidMessage: ""
@ -453,6 +460,7 @@ export class Select extends React.Component<SelectProperties, SelectFieldState>
value={this.props.value}
defaultValue={this.props.defaultValue}
disabled={disabled}
title={this.props.title}
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}