A lot of updates
parent
0abd6c3178
commit
7871d7c189
|
@ -1,4 +1,8 @@
|
||||||
# Changelog:
|
# Changelog:
|
||||||
|
* **21.04.20**
|
||||||
|
- Clicking on the music bot does not longer results in the insufficient permission sound when the client has no permissions
|
||||||
|
- Fixed permission editor overflow
|
||||||
|
|
||||||
* **18.04.20**
|
* **18.04.20**
|
||||||
- Recoded the channel tree using React
|
- Recoded the channel tree using React
|
||||||
- Heavily improved channel tree performance on large servers (fluent scroll & updates)
|
- Heavily improved channel tree performance on large servers (fluent scroll & updates)
|
||||||
|
|
|
@ -345,10 +345,6 @@ loader.register_task(loader.Stage.SETUP, {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.setAttribute('id', "mouse-move");
|
container.setAttribute('id', "mouse-move");
|
||||||
|
|
||||||
const inner_container = document.createElement("div");
|
|
||||||
inner_container.classList.add("container");
|
|
||||||
container.append(inner_container);
|
|
||||||
|
|
||||||
body.append(container);
|
body.append(container);
|
||||||
}
|
}
|
||||||
/* tooltip container */
|
/* tooltip container */
|
||||||
|
|
|
@ -257,22 +257,6 @@ $animation_seperator_length: .1s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#mouse-move {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 10000;
|
|
||||||
|
|
||||||
.container {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
border: 2px solid gray;
|
|
||||||
-webkit-border-radius: 2px;
|
|
||||||
-moz-border-radius: 2px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,6 +166,7 @@
|
||||||
width: 25%;
|
width: 25%;
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
min-height: 10em;
|
min-height: 10em;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
background-color: #222226;
|
background-color: #222226;
|
||||||
|
|
||||||
|
|
|
@ -262,7 +262,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
request_playlist_songs(playlist_id: number) : Promise<PlaylistSong[]> {
|
request_playlist_songs(playlist_id: number, process_result?: boolean) : Promise<PlaylistSong[]> {
|
||||||
let bulked_response = false;
|
let bulked_response = false;
|
||||||
let bulk_index = 0;
|
let bulk_index = 0;
|
||||||
|
|
||||||
|
@ -314,7 +314,7 @@ export class CommandHelper extends AbstractCommandHandler {
|
||||||
};
|
};
|
||||||
this.handler_boss.register_single_handler(single_handler);
|
this.handler_boss.register_single_handler(single_handler);
|
||||||
|
|
||||||
this.connection.send_command("playlistsonglist", {playlist_id: playlist_id}).catch(error => {
|
this.connection.send_command("playlistsonglist", {playlist_id: playlist_id}, { process_result: process_result }).catch(error => {
|
||||||
this.handler_boss.remove_single_handler(single_handler);
|
this.handler_boss.remove_single_handler(single_handler);
|
||||||
if(error instanceof CommandResult) {
|
if(error instanceof CommandResult) {
|
||||||
if(error.id == ErrorID.EMPTY_RESULT) {
|
if(error.id == ErrorID.EMPTY_RESULT) {
|
||||||
|
|
|
@ -493,14 +493,15 @@ export class ServerSettings extends SettingsBase {
|
||||||
|
|
||||||
server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
server?<T>(key: string | SettingsKey<T>, _default?: T) : T {
|
||||||
if(this._destroyed) throw "destroyed";
|
if(this._destroyed) throw "destroyed";
|
||||||
return StaticSettings.resolveKey(Settings.keyify(key), _default, key => this.cacheServer[key]);
|
const kkey = Settings.keyify(key);
|
||||||
|
return StaticSettings.resolveKey(kkey, typeof _default === "undefined" ? kkey.default_value : _default, key => this.cacheServer[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeServer<T>(key: string | SettingsKey<T>, value?: T) {
|
changeServer<T>(key: string | SettingsKey<T>, value?: T) {
|
||||||
if(this._destroyed) throw "destroyed";
|
if(this._destroyed) throw "destroyed";
|
||||||
key = Settings.keyify(key);
|
key = Settings.keyify(key);
|
||||||
|
|
||||||
if(this.cacheServer[key.key] == value) return;
|
if(this.cacheServer[key.key] === value) return;
|
||||||
|
|
||||||
this._server_settings_updated = true;
|
this._server_settings_updated = true;
|
||||||
this.cacheServer[key.key] = StaticSettings.transformOtS(value);
|
this.cacheServer[key.key] = StaticSettings.transformOtS(value);
|
||||||
|
|
|
@ -1,167 +0,0 @@
|
||||||
import {ChannelTree} from "tc-shared/ui/view";
|
|
||||||
import * as log from "tc-shared/log";
|
|
||||||
import {LogCategory} from "tc-shared/log";
|
|
||||||
import {ClientEntry} from "tc-shared/ui/client";
|
|
||||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
|
||||||
|
|
||||||
export class ClientMover {
|
|
||||||
static readonly listener_root = $(document);
|
|
||||||
static readonly move_element = $("#mouse-move");
|
|
||||||
readonly channel_tree: ChannelTree;
|
|
||||||
|
|
||||||
selected_client: ClientEntry | ClientEntry[];
|
|
||||||
|
|
||||||
hovered_channel: HTMLDivElement;
|
|
||||||
callback: (channel?: ChannelEntry) => any;
|
|
||||||
|
|
||||||
enabled: boolean = true;
|
|
||||||
|
|
||||||
private _bound_finish;
|
|
||||||
private _bound_move;
|
|
||||||
private _active: boolean = false;
|
|
||||||
|
|
||||||
private origin_point: {x: number, y: number} = undefined;
|
|
||||||
|
|
||||||
constructor(tree: ChannelTree) {
|
|
||||||
this.channel_tree = tree;
|
|
||||||
}
|
|
||||||
|
|
||||||
is_active() { return this._active; }
|
|
||||||
|
|
||||||
private hover_text() {
|
|
||||||
if($.isArray(this.selected_client)) {
|
|
||||||
return this.selected_client.filter(client => !!client).map(client => client.clientNickName()).join(", ");
|
|
||||||
} else if(this.selected_client) {
|
|
||||||
return (<ClientEntry>this.selected_client).clientNickName();
|
|
||||||
} else
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private bbcode_text() {
|
|
||||||
if($.isArray(this.selected_client)) {
|
|
||||||
return this.selected_client.filter(client => !!client).map(client => client.create_bbcode()).join(", ");
|
|
||||||
} else if(this.selected_client) {
|
|
||||||
return (<ClientEntry>this.selected_client).create_bbcode();
|
|
||||||
} else
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
activate(client: ClientEntry | ClientEntry[], callback: (channel?: ChannelEntry) => any, event: any) {
|
|
||||||
this.finish_listener(undefined);
|
|
||||||
|
|
||||||
if(!this.enabled)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
this.selected_client = client;
|
|
||||||
this.callback = callback;
|
|
||||||
log.debug(LogCategory.GENERAL, tr("Starting mouse move"));
|
|
||||||
|
|
||||||
ClientMover.listener_root.on('mouseup', this._bound_finish = this.finish_listener.bind(this)).on('mousemove', this._bound_move = this.move_listener.bind(this));
|
|
||||||
|
|
||||||
{
|
|
||||||
const content = ClientMover.move_element.find(".container");
|
|
||||||
content.empty();
|
|
||||||
content.append($.spawn("a").text(this.hover_text()));
|
|
||||||
}
|
|
||||||
this.move_listener(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private move_listener(event) {
|
|
||||||
if(!this.enabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
//console.log("Mouse move: " + event.pageX + " - " + event.pageY);
|
|
||||||
if(!event.pageX || !event.pageY) return;
|
|
||||||
if(!this.origin_point)
|
|
||||||
this.origin_point = {x: event.pageX, y: event.pageY};
|
|
||||||
|
|
||||||
ClientMover.move_element.css({
|
|
||||||
"top": (event.pageY - 1) + "px",
|
|
||||||
"left": (event.pageX + 10) + "px"
|
|
||||||
});
|
|
||||||
|
|
||||||
if(!this._active) {
|
|
||||||
const d_x = this.origin_point.x - event.pageX;
|
|
||||||
const d_y = this.origin_point.y - event.pageY;
|
|
||||||
this._active = Math.sqrt(d_x * d_x + d_y * d_y) > 5 * 5;
|
|
||||||
|
|
||||||
if(this._active) {
|
|
||||||
if($.isArray(this.selected_client)) {
|
|
||||||
this.channel_tree.onSelect(this.selected_client[0], true);
|
|
||||||
for(const client of this.selected_client.slice(1))
|
|
||||||
this.channel_tree.onSelect(client, false, true);
|
|
||||||
} else {
|
|
||||||
this.channel_tree.onSelect(this.selected_client, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
ClientMover.move_element.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const elements = document.elementsFromPoint(event.pageX, event.pageY);
|
|
||||||
while(elements.length > 0) {
|
|
||||||
if(elements[0].classList.contains("container-channel")) break;
|
|
||||||
elements.pop_front();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.hovered_channel) {
|
|
||||||
this.hovered_channel.classList.remove("move-selected");
|
|
||||||
this.hovered_channel = undefined;
|
|
||||||
}
|
|
||||||
if(elements.length > 0) {
|
|
||||||
elements[0].classList.add("move-selected");
|
|
||||||
this.hovered_channel = elements[0] as HTMLDivElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private finish_listener(event) {
|
|
||||||
ClientMover.move_element.hide();
|
|
||||||
log.debug(LogCategory.GENERAL, tr("Finishing mouse move"));
|
|
||||||
|
|
||||||
const channel_id = this.hovered_channel ? parseInt(this.hovered_channel.getAttribute("channel-id")) : 0;
|
|
||||||
ClientMover.listener_root.unbind('mouseleave', this._bound_finish);
|
|
||||||
ClientMover.listener_root.unbind('mouseup', this._bound_finish);
|
|
||||||
ClientMover.listener_root.unbind('mousemove', this._bound_move);
|
|
||||||
if(this.hovered_channel) {
|
|
||||||
this.hovered_channel.classList.remove("move-selected");
|
|
||||||
this.hovered_channel = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.origin_point = undefined;
|
|
||||||
if(!this._active) {
|
|
||||||
this.selected_client = undefined;
|
|
||||||
this.callback = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._active = false;
|
|
||||||
if(this.callback) {
|
|
||||||
if(!channel_id)
|
|
||||||
this.callback(undefined);
|
|
||||||
else {
|
|
||||||
this.callback(this.channel_tree.findChannel(channel_id));
|
|
||||||
}
|
|
||||||
this.callback = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* test for the chat box */
|
|
||||||
{
|
|
||||||
const elements = document.elementsFromPoint(event.pageX, event.pageY);
|
|
||||||
console.error(elements);
|
|
||||||
while(elements.length > 0) {
|
|
||||||
if(elements[0].classList.contains("client-chat-box-field")) break;
|
|
||||||
elements.pop_front();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(elements.length > 0) {
|
|
||||||
const element = $(<HTMLTextAreaElement>elements[0]);
|
|
||||||
element.val((element.val() || "") + this.bbcode_text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deactivate() {
|
|
||||||
this.callback = undefined;
|
|
||||||
this.finish_listener(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,6 +16,7 @@ html:root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: var(--menu-bar-background);
|
background: var(--menu-bar-background);
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
/* tmp fix for ultra small devices */
|
/* tmp fix for ultra small devices */
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
|
|
|
@ -736,7 +736,7 @@ export class MusicInfo {
|
||||||
this._current_bot.updateClientVariables(true).catch(error => {
|
this._current_bot.updateClientVariables(true).catch(error => {
|
||||||
log.warn(LogCategory.CLIENT, tr("Failed to update music bot variables: %o"), error);
|
log.warn(LogCategory.CLIENT, tr("Failed to update music bot variables: %o"), error);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.handle.handle.serverConnection.command_helper.request_playlist_songs(this._current_bot.properties.client_playlist_id).then(songs => {
|
this.handle.handle.serverConnection.command_helper.request_playlist_songs(this._current_bot.properties.client_playlist_id, false).then(songs => {
|
||||||
this.playlist_subscribe(false); /* we're allowed to see the playlist */
|
this.playlist_subscribe(false); /* we're allowed to see the playlist */
|
||||||
if(!songs) {
|
if(!songs) {
|
||||||
this._container_playlist.find(".overlay-empty").removeClass("hidden");
|
this._container_playlist.find(".overlay-empty").removeClass("hidden");
|
||||||
|
|
|
@ -244,7 +244,7 @@ export class ChannelEntryView extends TreeEntry<ChannelEntryViewProperties, {}>
|
||||||
const collapsed_indicator = this.props.channel.child_channel_head || this.props.channel.clients(false).length > 0;
|
const collapsed_indicator = this.props.channel.child_channel_head || this.props.channel.clients(false).length > 0;
|
||||||
return <div className={this.classList(viewStyle.treeEntry, channelStyle.channelEntry, this.props.channel.isSelected() && viewStyle.selected)}
|
return <div className={this.classList(viewStyle.treeEntry, channelStyle.channelEntry, this.props.channel.isSelected() && viewStyle.selected)}
|
||||||
style={{ paddingLeft: this.props.depth * 16 + 2, top: this.props.offset }}
|
style={{ paddingLeft: this.props.depth * 16 + 2, top: this.props.offset }}
|
||||||
onMouseDown={e => this.onMouseDown(e as any)}
|
onMouseUp={e => this.onMouseUp(e as any)}
|
||||||
onDoubleClick={() => this.onDoubleClick()}
|
onDoubleClick={() => this.onDoubleClick()}
|
||||||
onContextMenu={e => this.onContextMenu(e as any)}
|
onContextMenu={e => this.onContextMenu(e as any)}
|
||||||
>
|
>
|
||||||
|
@ -260,10 +260,12 @@ export class ChannelEntryView extends TreeEntry<ChannelEntryViewProperties, {}>
|
||||||
this.props.channel.collapsed = !this.props.channel.collapsed;
|
this.props.channel.collapsed = !this.props.channel.collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMouseDown(event: MouseEvent) {
|
private onMouseUp(event: MouseEvent) {
|
||||||
if(event.button !== 0) return; /* only left mouse clicks */
|
if(event.button !== 0) return; /* only left mouse clicks */
|
||||||
|
|
||||||
const channel = this.props.channel;
|
const channel = this.props.channel;
|
||||||
|
if(channel.channelTree.isClientMoveActive()) return;
|
||||||
|
|
||||||
channel.channelTree.events.fire("action_select_entries", {
|
channel.channelTree.events.fire("action_select_entries", {
|
||||||
entries: [ channel ],
|
entries: [ channel ],
|
||||||
mode: "auto"
|
mode: "auto"
|
||||||
|
|
|
@ -364,7 +364,7 @@ export class ClientEntry extends TreeEntry<ClientEntryProperties, ClientEntrySta
|
||||||
<div className={this.classList(clientStyle.clientEntry, viewStyle.treeEntry, this.props.client.isSelected() && viewStyle.selected)}
|
<div className={this.classList(clientStyle.clientEntry, viewStyle.treeEntry, this.props.client.isSelected() && viewStyle.selected)}
|
||||||
style={{ paddingLeft: (this.props.depth * 16 + 2) + "px", top: this.props.offset }}
|
style={{ paddingLeft: (this.props.depth * 16 + 2) + "px", top: this.props.offset }}
|
||||||
onDoubleClick={() => this.onDoubleClick()}
|
onDoubleClick={() => this.onDoubleClick()}
|
||||||
onMouseDown={e => this.onMouseDown(e as any)}
|
onMouseUp={e => this.onMouseUp(e as any)}
|
||||||
onContextMenu={e => this.onContextMenu(e as any)}
|
onContextMenu={e => this.onContextMenu(e as any)}
|
||||||
>
|
>
|
||||||
<UnreadMarker entry={this.props.client} />
|
<UnreadMarker entry={this.props.client} />
|
||||||
|
@ -403,10 +403,11 @@ export class ClientEntry extends TreeEntry<ClientEntryProperties, ClientEntrySta
|
||||||
this.setState({ rename: false });
|
this.setState({ rename: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMouseDown(event: MouseEvent) {
|
private onMouseUp(event: MouseEvent) {
|
||||||
if(event.button !== 0) return; /* only left mouse clicks */
|
if(event.button !== 0) return; /* only left mouse clicks */
|
||||||
|
|
||||||
const tree = this.props.client.channelTree;
|
const tree = this.props.client.channelTree;
|
||||||
|
if(tree.isClientMoveActive()) return;
|
||||||
|
|
||||||
tree.events.fire("action_select_entries", { entries: [this.props.client], mode: "auto" });
|
tree.events.fire("action_select_entries", { entries: [this.props.client], mode: "auto" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ export class ServerEntry extends TreeEntry<ServerEntryProperties, ServerEntrySta
|
||||||
|
|
||||||
return <div className={this.classList(serverStyle.serverEntry, viewStyle.treeEntry, this.props.server.isSelected() && viewStyle.selected )}
|
return <div className={this.classList(serverStyle.serverEntry, viewStyle.treeEntry, this.props.server.isSelected() && viewStyle.selected )}
|
||||||
style={{ top: this.props.offset }}
|
style={{ top: this.props.offset }}
|
||||||
onMouseDown={e => this.onMouseDown(e as any)}
|
onMouseUp={e => this.onMouseUp(e as any)}
|
||||||
onContextMenu={e => this.onContextMenu(e as any)}
|
onContextMenu={e => this.onContextMenu(e as any)}
|
||||||
>
|
>
|
||||||
<UnreadMarker entry={this.props.server} />
|
<UnreadMarker entry={this.props.server} />
|
||||||
|
@ -87,8 +87,9 @@ export class ServerEntry extends TreeEntry<ServerEntryProperties, ServerEntrySta
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMouseDown(event: MouseEvent) {
|
private onMouseUp(event: MouseEvent) {
|
||||||
if(event.button !== 0) return; /* only left mouse clicks */
|
if(event.button !== 0) return; /* only left mouse clicks */
|
||||||
|
if(this.props.server.channelTree.isClientMoveActive()) return;
|
||||||
|
|
||||||
this.props.server.channelTree.events.fire("action_select_entries", {
|
this.props.server.channelTree.events.fire("action_select_entries", {
|
||||||
entries: [ this.props.server ],
|
entries: [ this.props.server ],
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
html:root {
|
||||||
|
--channel-tree-move-color: hsla(220, 5%, 2%, 1);
|
||||||
|
--channel-tree-move-background: hsla(0, 0%, 25%, 1);
|
||||||
|
--channel-tree-move-border: hsla(220, 4%, 40%, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.moveContainer {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
border: 2px solid var(--channel-tree-move-border);
|
||||||
|
background-color: var(--channel-tree-move-background);
|
||||||
|
|
||||||
|
z-index: 10000;
|
||||||
|
margin-left: 5px;
|
||||||
|
|
||||||
|
padding-left: .25em;
|
||||||
|
padding-right: .25em;
|
||||||
|
|
||||||
|
border-radius: 2px;
|
||||||
|
color: var(--channel-tree-move-color);
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ReactDOM from "react-dom";
|
||||||
|
import {ChannelTreeView} from "tc-shared/ui/tree/View";
|
||||||
|
const moveStyle = require("./TreeEntryMove.scss");
|
||||||
|
|
||||||
|
export interface TreeEntryMoveProps {
|
||||||
|
onMoveEnd: (point: { x: number, y: number }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreeEntryMoveState {
|
||||||
|
tree_view: ChannelTreeView;
|
||||||
|
|
||||||
|
begin: { x: number, y: number };
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TreeEntryMove extends ReactComponentBase<TreeEntryMoveProps, TreeEntryMoveState> {
|
||||||
|
private readonly domContainer;
|
||||||
|
private readonly document_mouse_out_listener;
|
||||||
|
private readonly document_mouse_listener;
|
||||||
|
private readonly ref_container: React.RefObject<HTMLDivElement>;
|
||||||
|
|
||||||
|
private current: { x: number, y: number };
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.ref_container = React.createRef();
|
||||||
|
this.domContainer = document.getElementById("mouse-move");
|
||||||
|
this.document_mouse_out_listener = (e: MouseEvent) => {
|
||||||
|
if(e.type === "mouseup") {
|
||||||
|
if(e.button !== 0) return;
|
||||||
|
|
||||||
|
this.props.onMoveEnd({ x: e.pageX, y: e.pageY });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disableEntryMove();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.document_mouse_listener = (e: MouseEvent) => {
|
||||||
|
this.current = { x: e.pageX, y: e.pageY };
|
||||||
|
const container = this.ref_container.current;
|
||||||
|
if(!container) return;
|
||||||
|
|
||||||
|
container.style.top = e.pageY + "px";
|
||||||
|
container.style.left = e.pageX + "px";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
enableEntryMove(view: ChannelTreeView, description: string, begin: { x: number, y: null }, current: { x: number, y: null }, callback_enabled?: () => void) {
|
||||||
|
this.setState({
|
||||||
|
tree_view: view,
|
||||||
|
begin: begin,
|
||||||
|
description: description
|
||||||
|
}, callback_enabled);
|
||||||
|
|
||||||
|
this.current = current;
|
||||||
|
document.addEventListener("mousemove", this.document_mouse_listener);
|
||||||
|
document.addEventListener("mouseleave", this.document_mouse_out_listener);
|
||||||
|
document.addEventListener("mouseup", this.document_mouse_out_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private disableEntryMove() {
|
||||||
|
this.setState({
|
||||||
|
tree_view: null
|
||||||
|
});
|
||||||
|
document.removeEventListener("mousemove", this.document_mouse_listener);
|
||||||
|
document.removeEventListener("mouseleave", this.document_mouse_out_listener);
|
||||||
|
document.removeEventListener("mouseup", this.document_mouse_out_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected defaultState(): TreeEntryMoveState {
|
||||||
|
return {
|
||||||
|
tree_view: null,
|
||||||
|
begin: { x: 0, y: 0},
|
||||||
|
description: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive() { return !!this.state.tree_view; }
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if(!this.state.tree_view)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(this.renderPortal(), this.domContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderPortal() {
|
||||||
|
return <div style={{ top: this.current.y, left: this.current.x }} className={moveStyle.moveContainer} ref={this.ref_container} >
|
||||||
|
{this.state.description}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
@import "../../../css/static/mixin";
|
@import "../../../css/static/mixin";
|
||||||
|
|
||||||
html:root {
|
html:root {
|
||||||
|
--channel-tree-entry-move: #313235;
|
||||||
--channel-tree-entry-selected: #2d2d2d;
|
--channel-tree-entry-selected: #2d2d2d;
|
||||||
--channel-tree-entry-hovered: #393939;
|
--channel-tree-entry-hovered: #393939;
|
||||||
--channel-tree-entry-color: #828282;
|
--channel-tree-entry-color: #828282;
|
||||||
|
@ -41,7 +42,6 @@ html:root {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.treeEntry {
|
.treeEntry {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -98,6 +98,12 @@ html:root {
|
||||||
@include transition(opacity $button_hover_animation_time);
|
@include transition(opacity $button_hover_animation_time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.move {
|
||||||
|
.treeEntry.selected {
|
||||||
|
background-color: var(--channel-tree-entry-move);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.channelTreeContainer {
|
.channelTreeContainer {
|
||||||
|
|
|
@ -16,13 +16,15 @@ import {ClientEntry as ClientEntryView} from "./Client";
|
||||||
|
|
||||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
import {ChannelEntry} from "tc-shared/ui/channel";
|
||||||
import {ServerEntry} from "tc-shared/ui/server";
|
import {ServerEntry} from "tc-shared/ui/server";
|
||||||
import {ClientEntry, LocalClientEntry} from "tc-shared/ui/client";
|
import {ClientEntry, ClientType} from "tc-shared/ui/client";
|
||||||
|
|
||||||
const viewStyle = require("./View.scss");
|
const viewStyle = require("./View.scss");
|
||||||
|
|
||||||
|
|
||||||
export interface ChannelTreeViewProperties {
|
export interface ChannelTreeViewProperties {
|
||||||
tree: ChannelTree;
|
tree: ChannelTree;
|
||||||
|
onMoveStart: (start: { x: number, y: number }, current: { x: number, y: number }) => void;
|
||||||
|
moveThreshold?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChannelTreeViewState {
|
export interface ChannelTreeViewState {
|
||||||
|
@ -52,6 +54,9 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
private listener_state_collapsed;
|
private listener_state_collapsed;
|
||||||
private update_timeout;
|
private update_timeout;
|
||||||
|
|
||||||
|
private mouse_move: { x: number, y: number, down: boolean, fired: boolean } = { x: 0, y: 0, down: false, fired: false };
|
||||||
|
private document_mouse_listener;
|
||||||
|
|
||||||
private in_view_callbacks: {
|
private in_view_callbacks: {
|
||||||
index: number,
|
index: number,
|
||||||
callback: () => void,
|
callback: () => void,
|
||||||
|
@ -95,6 +100,26 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
this.listener_client_change = () => this.handleTreeUpdate();
|
this.listener_client_change = () => this.handleTreeUpdate();
|
||||||
this.listener_channel_change = () => this.handleTreeUpdate();
|
this.listener_channel_change = () => this.handleTreeUpdate();
|
||||||
this.listener_state_collapsed = () => this.handleTreeUpdate();
|
this.listener_state_collapsed = () => this.handleTreeUpdate();
|
||||||
|
|
||||||
|
this.document_mouse_listener = (e: MouseEvent) => {
|
||||||
|
if(e.type !== "mouseleave" && e.button !== 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.mouse_move.down = false;
|
||||||
|
this.mouse_move.fired = false;
|
||||||
|
|
||||||
|
this.removeDocumentMouseListener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerDocumentMouseListener() {
|
||||||
|
document.addEventListener("mouseleave", this.document_mouse_listener);
|
||||||
|
document.addEventListener("mouseup", this.document_mouse_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeDocumentMouseListener() {
|
||||||
|
document.removeEventListener("mouseleave", this.document_mouse_listener);
|
||||||
|
document.removeEventListener("mouseup", this.document_mouse_listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleTreeUpdate() {
|
private handleTreeUpdate() {
|
||||||
|
@ -137,8 +162,8 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={viewStyle.channelTreeContainer} onScroll={() => this.onScroll()} ref={this.ref_container} >
|
<div className={viewStyle.channelTreeContainer} onScroll={() => this.onScroll()} ref={this.ref_container} onMouseDown={e => this.onMouseDown(e as any)} onMouseMove={e => this.onMouseMove(e as any)} >
|
||||||
<div className={viewStyle.channelTree} style={{height: (this.flat_tree.length * ChannelTreeView.EntryHeight) + "px"}}>
|
<div className={this.classList(viewStyle.channelTree, this.props.tree.isClientMoveActive() && viewStyle.move)} style={{height: (this.flat_tree.length * ChannelTreeView.EntryHeight) + "px"}}>
|
||||||
{elements}
|
{elements}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -157,7 +182,10 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
});
|
});
|
||||||
|
|
||||||
if(entry.collapsed) return;
|
if(entry.collapsed) return;
|
||||||
this.flat_tree.push(...entry.clients(false).map(e => {
|
let clients = entry.clients(false);
|
||||||
|
if(!this.props.tree.areServerQueriesShown())
|
||||||
|
clients = clients.filter(e => e.properties.client_type_exact !== ClientType.CLIENT_QUERY);
|
||||||
|
this.flat_tree.push(...clients.map(e => {
|
||||||
return {
|
return {
|
||||||
entry: e,
|
entry: e,
|
||||||
rendered: <ClientEntryView key={"client-" + e.clientId()} client={e} offset={this.build_top_offset += ChannelTreeView.EntryHeight} depth={depth + 1} ref={e.view} />
|
rendered: <ClientEntryView key={"client-" + e.clientId()} client={e} offset={this.build_top_offset += ChannelTreeView.EntryHeight} depth={depth + 1} ref={e.view} />
|
||||||
|
@ -195,12 +223,44 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
this.handleTreeUpdate();
|
this.handleTreeUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventHandler<ChannelTreeEvents>("notify_query_view_state_changed")
|
||||||
|
private handleQueryViewStateChange() {
|
||||||
|
this.handleTreeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<ChannelTreeEvents>("notify_entry_move_begin")
|
||||||
|
private handleEntryMoveBegin() {
|
||||||
|
this.handleTreeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler<ChannelTreeEvents>("notify_entry_move_end")
|
||||||
|
private handleEntryMoveEnd() {
|
||||||
|
this.handleTreeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
private onScroll() {
|
private onScroll() {
|
||||||
this.setState({
|
this.setState({
|
||||||
scroll_offset: this.ref_container.current.scrollTop
|
scroll_offset: this.ref_container.current.scrollTop
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onMouseDown(e: MouseEvent) {
|
||||||
|
if(e.button !== 0) return; /* left button only */
|
||||||
|
|
||||||
|
this.mouse_move.down = true;
|
||||||
|
this.mouse_move.x = e.pageX;
|
||||||
|
this.mouse_move.y = e.pageY;
|
||||||
|
this.registerDocumentMouseListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMouseMove(e: MouseEvent) {
|
||||||
|
if(!this.mouse_move.down || this.mouse_move.fired) return;
|
||||||
|
if(Math.abs((this.mouse_move.x - e.pageX) * (this.mouse_move.y - e.pageY)) > (this.props.moveThreshold || 9)) {
|
||||||
|
this.mouse_move.fired = true;
|
||||||
|
this.props.onMoveStart({x: this.mouse_move.x, y: this.mouse_move.y}, {x: e.pageX, y: e.pageY});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
scrollEntryInView(entry: TreeEntry, callback?: () => void) {
|
scrollEntryInView(entry: TreeEntry, callback?: () => void) {
|
||||||
const index = this.flat_tree.findIndex(e => e.entry === entry);
|
const index = this.flat_tree.findIndex(e => e.entry === entry);
|
||||||
if(index === -1) {
|
if(index === -1) {
|
||||||
|
@ -234,4 +294,22 @@ export class ChannelTreeView extends ReactComponentBase<ChannelTreeViewPropertie
|
||||||
this.in_view_callbacks.push(cb);
|
this.in_view_callbacks.push(cb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getEntryFromPoint(pageX: number, pageY: number) {
|
||||||
|
const container = this.ref_container.current;
|
||||||
|
if(!container) return;
|
||||||
|
|
||||||
|
const bounds = container.getBoundingClientRect();
|
||||||
|
pageY -= bounds.y;
|
||||||
|
pageX -= bounds.x;
|
||||||
|
|
||||||
|
if(pageX < 0 || pageY < 0)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
if(pageX > container.clientWidth)
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
const total_offset = container.scrollTop + pageY;
|
||||||
|
return this.flat_tree[Math.floor(total_offset / ChannelTreeView.EntryHeight)].entry;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -9,7 +9,6 @@ import {Sound} from "tc-shared/sound/Sounds";
|
||||||
import {Group} from "tc-shared/permission/GroupManager";
|
import {Group} from "tc-shared/permission/GroupManager";
|
||||||
import * as server_log from "tc-shared/ui/frames/server_log";
|
import * as server_log from "tc-shared/ui/frames/server_log";
|
||||||
import {ServerAddress, ServerEntry} from "tc-shared/ui/server";
|
import {ServerAddress, ServerEntry} from "tc-shared/ui/server";
|
||||||
import {ClientMover} from "tc-shared/ui/client_move";
|
|
||||||
import {ChannelEntry, ChannelSubscribeMode} from "tc-shared/ui/channel";
|
import {ChannelEntry, ChannelSubscribeMode} from "tc-shared/ui/channel";
|
||||||
import {ClientEntry, LocalClientEntry, MusicClientEntry} from "tc-shared/ui/client";
|
import {ClientEntry, LocalClientEntry, MusicClientEntry} from "tc-shared/ui/client";
|
||||||
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||||
|
@ -27,6 +26,7 @@ import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient";
|
||||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||||
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
|
||||||
import {tra} from "tc-shared/i18n/localize";
|
import {tra} from "tc-shared/i18n/localize";
|
||||||
|
import {TreeEntryMove} from "tc-shared/ui/tree/TreeEntryMove";
|
||||||
|
|
||||||
export interface ChannelTreeEvents {
|
export interface ChannelTreeEvents {
|
||||||
action_select_entries: {
|
action_select_entries: {
|
||||||
|
@ -41,7 +41,11 @@ export interface ChannelTreeEvents {
|
||||||
},
|
},
|
||||||
|
|
||||||
notify_selection_changed: {},
|
notify_selection_changed: {},
|
||||||
notify_root_channel_changed: {}
|
notify_root_channel_changed: {},
|
||||||
|
notify_query_view_state_changed: { queries_shown: boolean },
|
||||||
|
|
||||||
|
notify_entry_move_begin: {},
|
||||||
|
notify_entry_move_end: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChannelTreeEntrySelect {
|
export class ChannelTreeEntrySelect {
|
||||||
|
@ -187,9 +191,8 @@ export class ChannelTree {
|
||||||
channels: ChannelEntry[] = [];
|
channels: ChannelEntry[] = [];
|
||||||
clients: ClientEntry[] = [];
|
clients: ClientEntry[] = [];
|
||||||
|
|
||||||
readonly client_mover: ClientMover;
|
|
||||||
|
|
||||||
readonly view: React.RefObject<ChannelTreeView>;
|
readonly view: React.RefObject<ChannelTreeView>;
|
||||||
|
readonly view_move: React.RefObject<TreeEntryMove>;
|
||||||
readonly selection: ChannelTreeEntrySelect;
|
readonly selection: ChannelTreeEntrySelect;
|
||||||
|
|
||||||
private readonly _tag_container: JQuery;
|
private readonly _tag_container: JQuery;
|
||||||
|
@ -206,14 +209,17 @@ export class ChannelTree {
|
||||||
this.events = new Registry<ChannelTreeEvents>();
|
this.events = new Registry<ChannelTreeEvents>();
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.view = React.createRef();
|
this.view = React.createRef();
|
||||||
|
this.view_move = React.createRef();
|
||||||
|
|
||||||
this.server = new ServerEntry(this, "undefined", undefined);
|
this.server = new ServerEntry(this, "undefined", undefined);
|
||||||
this.selection = new ChannelTreeEntrySelect(this);
|
this.selection = new ChannelTreeEntrySelect(this);
|
||||||
|
|
||||||
this._tag_container = $.spawn("div").addClass("channel-tree-container");
|
this._tag_container = $.spawn("div").addClass("channel-tree-container");
|
||||||
ReactDOM.render(<ChannelTreeView tree={this} ref={this.view} />, this._tag_container[0]);
|
ReactDOM.render([
|
||||||
|
<ChannelTreeView key={"tree"} onMoveStart={(a,b) => this.onChannelEntryMove(a, b)} tree={this} ref={this.view} />,
|
||||||
|
<TreeEntryMove key={"move"} onMoveEnd={(point) => this.onMoveEnd(point.x, point.y)} ref={this.view_move} />
|
||||||
|
], this._tag_container[0]);
|
||||||
|
|
||||||
this.client_mover = new ClientMover(this);
|
|
||||||
this.reset();
|
this.reset();
|
||||||
|
|
||||||
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
|
if(!settings.static(Settings.KEY_DISABLE_CONTEXT_MENU, false)) {
|
||||||
|
@ -291,6 +297,7 @@ export class ChannelTree {
|
||||||
invalidPermission: !channelCreate,
|
invalidPermission: !channelCreate,
|
||||||
callback: () => this.spawnCreateChannel()
|
callback: () => this.spawnCreateChannel()
|
||||||
},
|
},
|
||||||
|
contextmenu.Entry.HR(),
|
||||||
{
|
{
|
||||||
type: contextmenu.MenuEntryType.ENTRY,
|
type: contextmenu.MenuEntryType.ENTRY,
|
||||||
icon_class: "client-channel_collapse_all",
|
icon_class: "client-channel_collapse_all",
|
||||||
|
@ -1001,20 +1008,9 @@ export class ChannelTree {
|
||||||
if(this._show_queries == flag) return;
|
if(this._show_queries == flag) return;
|
||||||
this._show_queries = flag;
|
this._show_queries = flag;
|
||||||
|
|
||||||
//TODO: FIXME!
|
this.events.fire("notify_query_view_state_changed", { queries_shown: flag });
|
||||||
/*
|
|
||||||
const channels: ChannelEntry[] = []
|
|
||||||
for(const client of this.clients)
|
|
||||||
if(client.properties.client_type == ClientType.CLIENT_QUERY) {
|
|
||||||
if(this._show_queries)
|
|
||||||
client.tag.show();
|
|
||||||
else
|
|
||||||
client.tag.hide();
|
|
||||||
if(channels.indexOf(client.currentChannel()) == -1)
|
|
||||||
channels.push(client.currentChannel());
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
areServerQueriesShown() { return this._show_queries; }
|
||||||
|
|
||||||
get_first_channel?() : ChannelEntry {
|
get_first_channel?() : ChannelEntry {
|
||||||
return this.channel_first;
|
return this.channel_first;
|
||||||
|
@ -1081,4 +1077,53 @@ export class ChannelTree {
|
||||||
this.collapse_channels(child);
|
this.collapse_channels(child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onChannelEntryMove(start, current) {
|
||||||
|
const move = this.view_move.current;
|
||||||
|
if(!move) return;
|
||||||
|
|
||||||
|
const target = this.view.current.getEntryFromPoint(start.x, start.y);
|
||||||
|
if(target && this.selection.selected_entries.findIndex(e => e === target) === -1)
|
||||||
|
this.events.fire("action_select_entries", { mode: "auto", entries: [ target ]});
|
||||||
|
|
||||||
|
const selection = this.selection.selected_entries;
|
||||||
|
if(selection.length === 0 || selection.filter(e => !(e instanceof ClientEntry)).length > 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
move.enableEntryMove(this.view.current, selection.map(e => e as ClientEntry).map(e => e.clientNickName()).join(","), start, current, () => {
|
||||||
|
this.events.fire("notify_entry_move_begin");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMoveEnd(x: number, y: number) {
|
||||||
|
batch_updates(BatchUpdateType.CHANNEL_TREE);
|
||||||
|
try {
|
||||||
|
this.events.fire("notify_entry_move_end");
|
||||||
|
|
||||||
|
const selection = this.selection.selected_entries.filter(e => e instanceof ClientEntry) as ClientEntry[];
|
||||||
|
if(selection.length === 0) return;
|
||||||
|
this.selection.clear_selection();
|
||||||
|
|
||||||
|
const target = this.view.current.getEntryFromPoint(x, y);
|
||||||
|
let target_channel: ChannelEntry;
|
||||||
|
if(target instanceof ClientEntry)
|
||||||
|
target_channel = target.currentChannel();
|
||||||
|
else if(target instanceof ChannelEntry)
|
||||||
|
target_channel = target;
|
||||||
|
if(!target_channel) return;
|
||||||
|
|
||||||
|
selection.filter(e => e.currentChannel() !== target_channel).forEach(e => {
|
||||||
|
this.client.serverConnection.send_command("clientmove", {
|
||||||
|
clid: e.clientId(),
|
||||||
|
cid: target_channel.channelId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
flush_batched_updates(BatchUpdateType.CHANNEL_TREE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isClientMoveActive() {
|
||||||
|
return !!this.view_move.current?.isActive();
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue