Allowing to drag client tags
parent
0035e73179
commit
069c02a76e
|
@ -5,6 +5,7 @@
|
|||
- Improved channel conversation mode detection support
|
||||
- Added support for HTML encoded links (Example would be when copying from Edge the URL)
|
||||
- Enabled context menus for all clickable client tags
|
||||
- Allowing to drag client tags
|
||||
|
||||
* **08.12.20**
|
||||
- Fixed the permission editor not resolving unique ids
|
||||
|
|
|
@ -215,17 +215,19 @@ export abstract class AbstractChat<Events extends AbstractChatEvents> {
|
|||
return this.conversationMode === ChannelConversationMode.Private;
|
||||
}
|
||||
|
||||
protected setConversationMode(mode: ChannelConversationMode) {
|
||||
protected setConversationMode(mode: ChannelConversationMode, logChange: boolean) {
|
||||
if(this.conversationMode === mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerChatEvent({
|
||||
type: "mode-changed",
|
||||
uniqueId: guid() + "-mode-change",
|
||||
timestamp: Date.now(),
|
||||
newMode: mode === ChannelConversationMode.Public ? "normal" : mode === ChannelConversationMode.Private ? "private" : "none"
|
||||
}, true);
|
||||
if(logChange) {
|
||||
this.registerChatEvent({
|
||||
type: "mode-changed",
|
||||
uniqueId: guid() + "-mode-change",
|
||||
timestamp: Date.now(),
|
||||
newMode: mode === ChannelConversationMode.Public ? "normal" : mode === ChannelConversationMode.Private ? "private" : "none"
|
||||
}, true);
|
||||
}
|
||||
|
||||
this.conversationMode = mode;
|
||||
this.events.fire("notify_conversation_mode_changed", { newMode: mode });
|
||||
|
|
|
@ -165,18 +165,18 @@ export class ChannelConversation extends AbstractChat<ChannelConversationEvents>
|
|||
break;
|
||||
|
||||
case "private":
|
||||
this.setConversationMode(ChannelConversationMode.Private);
|
||||
this.setConversationMode(ChannelConversationMode.Private, true);
|
||||
this.setCurrentMode("normal");
|
||||
break;
|
||||
|
||||
case "success":
|
||||
this.setConversationMode(ChannelConversationMode.Public);
|
||||
this.setConversationMode(ChannelConversationMode.Public, true);
|
||||
this.setCurrentMode("normal");
|
||||
break;
|
||||
|
||||
case "unsupported":
|
||||
this.crossChannelChatSupported = false;
|
||||
this.setConversationMode(ChannelConversationMode.Private);
|
||||
this.setConversationMode(ChannelConversationMode.Private, true);
|
||||
this.setCurrentMode("normal");
|
||||
break;
|
||||
}
|
||||
|
@ -295,8 +295,8 @@ export class ChannelConversation extends AbstractChat<ChannelConversationEvents>
|
|||
this.handle.connection.settings.changeServer(Settings.FN_CHANNEL_CHAT_READ(this.conversationId), timestamp);
|
||||
}
|
||||
|
||||
public setConversationMode(mode: ChannelConversationMode) {
|
||||
super.setConversationMode(mode);
|
||||
public setConversationMode(mode: ChannelConversationMode, logChange: boolean) {
|
||||
super.setConversationMode(mode, logChange);
|
||||
}
|
||||
|
||||
public localClientSwitchedChannel(type: "join" | "leave") {
|
||||
|
@ -356,7 +356,7 @@ export class ChannelConversationManager extends AbstractChatManager<ChannelConve
|
|||
}
|
||||
|
||||
if("channel_conversation_mode" in event.updatedProperties) {
|
||||
conversation.setConversationMode(event.channel.properties.channel_conversation_mode);
|
||||
conversation.setConversationMode(event.channel.properties.channel_conversation_mode, true);
|
||||
conversation.updateAccessState();
|
||||
}
|
||||
}));
|
||||
|
@ -405,6 +405,10 @@ export class ChannelConversationManager extends AbstractChatManager<ChannelConve
|
|||
let conversation = this.findConversation(channelId);
|
||||
if(!conversation) {
|
||||
conversation = new ChannelConversation(this, channelId);
|
||||
const channel = this.connection.channelTree.findChannel(channelId);
|
||||
if(channel) {
|
||||
conversation.setConversationMode(channel.properties.channel_conversation_mode, false);
|
||||
}
|
||||
this.registerConversation(conversation);
|
||||
}
|
||||
|
||||
|
|
|
@ -781,7 +781,26 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
|||
}
|
||||
|
||||
const clients = event.entries.map(entryId => {
|
||||
const entry = channelTree.findEntryId(entryId);
|
||||
if(entryId.type !== "client") { return; }
|
||||
|
||||
let entry: ServerEntry | ChannelEntry | ClientEntry;
|
||||
if("uniqueTreeId" in entryId) {
|
||||
entry = channelTree.findEntryId(entryId.uniqueTreeId);
|
||||
} else {
|
||||
let clients = channelTree.clients.filter(client => client.properties.client_unique_identifier === entryId.clientUniqueId);
|
||||
if(entryId.clientId) {
|
||||
clients = clients.filter(client => client.clientId() === entryId.clientId);
|
||||
}
|
||||
|
||||
if(entryId.clientDatabaseId) {
|
||||
clients = clients.filter(client => client.properties.client_database_id === entryId.clientDatabaseId);
|
||||
}
|
||||
|
||||
if(clients.length === 1) {
|
||||
entry = clients[0];
|
||||
}
|
||||
}
|
||||
|
||||
if(!entry || !(entry instanceof ClientEntry)) {
|
||||
logWarn(LogCategory.CHANNEL, tr("Received client move notify with an entry id which isn't a client. Entry id: %o"), entryId);
|
||||
return undefined;
|
||||
|
@ -808,7 +827,15 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
|
|||
}
|
||||
|
||||
let channels = event.entries.map(entryId => {
|
||||
const entry = channelTree.findEntryId(entryId);
|
||||
if(entryId.type !== "channel") { return; }
|
||||
|
||||
let entry: ServerEntry | ChannelEntry | ClientEntry;
|
||||
if("uniqueTreeId" in entryId) {
|
||||
entry = channelTree.findEntryId(entryId.uniqueTreeId);
|
||||
} else {
|
||||
entry = channelTree.findChannel(entryId.channelId);
|
||||
}
|
||||
|
||||
if(!entry || !(entry instanceof ChannelEntry)) {
|
||||
logWarn(LogCategory.CHANNEL, tr("Received channel move notify with a channel id which isn't a channel. Entry id: %o"), entryId);
|
||||
return undefined;
|
||||
|
|
|
@ -37,8 +37,8 @@ export interface ChannelTreeUIEvents {
|
|||
action_channel_open_file_browser: { treeEntryId: number },
|
||||
action_client_double_click: { treeEntryId: number },
|
||||
action_client_name_submit: { treeEntryId: number, name: string },
|
||||
action_move_clients: { targetTreeEntry: number, entries: number[] },
|
||||
action_move_channels: { targetTreeEntry: number, mode: "before" | "after" | "child", entries: number[] },
|
||||
action_move_clients: { targetTreeEntry: number, entries: ChannelTreeDragEntry[] },
|
||||
action_move_channels: { targetTreeEntry: number, mode: "before" | "after" | "child", entries: ChannelTreeDragEntry[] },
|
||||
|
||||
/* queries */
|
||||
query_tree_entries: {},
|
||||
|
@ -82,11 +82,30 @@ export interface ChannelTreeUIEvents {
|
|||
notify_destroy: {}
|
||||
}
|
||||
|
||||
export type ChannelTreeDragEntry = {
|
||||
type: "channel",
|
||||
uniqueTreeId: number,
|
||||
} | {
|
||||
type: "channel",
|
||||
channelId: number
|
||||
} | {
|
||||
type: "server"
|
||||
} | {
|
||||
type: "client",
|
||||
|
||||
uniqueTreeId: number,
|
||||
} | {
|
||||
type: "client",
|
||||
|
||||
clientUniqueId: string,
|
||||
clientId?: number,
|
||||
clientDatabaseId?: number
|
||||
};
|
||||
|
||||
export type ChannelTreeDragData = {
|
||||
version: 1,
|
||||
handlerId: string,
|
||||
type: string,
|
||||
|
||||
entryIds: number[],
|
||||
entryTypes: ("server" | "channel" | "client")[]
|
||||
entries: ChannelTreeDragEntry[],
|
||||
};
|
|
@ -1,4 +1,3 @@
|
|||
import {RDPChannel, RDPChannelTree, RDPClient, RDPEntry, RDPServer} from "./RendererDataProvider";
|
||||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {
|
||||
|
@ -9,7 +8,7 @@ import {
|
|||
spriteWidth as kClientSpriteWidth,
|
||||
} from "svg-sprites/client-icons";
|
||||
import {LogCategory, logDebug} from "tc-shared/log";
|
||||
import {ChannelTreeDragData} from "tc-shared/ui/tree/Definitions";
|
||||
import {ChannelTreeDragData, ChannelTreeDragEntry} from "tc-shared/ui/tree/Definitions";
|
||||
|
||||
let spriteImage: HTMLImageElement;
|
||||
|
||||
|
@ -29,7 +28,12 @@ function paintClientIcon(context: CanvasRenderingContext2D, icon: ClientIcon, of
|
|||
context.drawImage(spriteImage, sprite.xOffset, sprite.yOffset, sprite.width, sprite.height, offsetX, offsetY, width, height);
|
||||
}
|
||||
|
||||
export function generateDragElement(entries: RDPEntry[]) : HTMLElement {
|
||||
export type DragImageEntryType = {
|
||||
icon: ClientIcon,
|
||||
name: string
|
||||
}
|
||||
|
||||
export function generateDragElement(entries: DragImageEntryType[]) : HTMLElement {
|
||||
const totalHeight = entries.length * 18 + 2; /* the two extra for "low" letters like "gyj" etc. */
|
||||
const totalWidth = 250;
|
||||
|
||||
|
@ -37,44 +41,19 @@ export function generateDragElement(entries: RDPEntry[]) : HTMLElement {
|
|||
let offsetX = 20;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
document.body.appendChild(canvas);
|
||||
canvas.height = totalHeight;
|
||||
canvas.width = totalWidth;
|
||||
|
||||
/* TODO: With font size? */
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
{
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "bottom";
|
||||
ctx.font = "700 16px Roboto, Helvetica, Arial, sans-serif";
|
||||
|
||||
for(const entry of entries) {
|
||||
let name: string;
|
||||
let icon: ClientIcon;
|
||||
|
||||
if(entry instanceof RDPClient) {
|
||||
name = entry.name.name;
|
||||
icon = entry.status;
|
||||
} else if(entry instanceof RDPChannel) {
|
||||
name = entry.info?.name;
|
||||
icon = entry.icon;
|
||||
} else if(entry instanceof RDPServer) {
|
||||
icon = ClientIcon.ServerGreen;
|
||||
|
||||
switch (entry.state.state) {
|
||||
case "connected":
|
||||
name = entry.state.name;
|
||||
break;
|
||||
|
||||
case "disconnected":
|
||||
name = tr("Not connected");
|
||||
break;
|
||||
|
||||
case "connecting":
|
||||
name = tr("Connecting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
ctx.strokeStyle = "red";
|
||||
ctx.moveTo(offsetX, offsetY);
|
||||
|
@ -83,8 +62,8 @@ export function generateDragElement(entries: RDPEntry[]) : HTMLElement {
|
|||
*/
|
||||
|
||||
ctx.fillStyle = "black";
|
||||
paintClientIcon(ctx, icon, offsetX + 1, offsetY + 1, 16, 16);
|
||||
ctx.fillText(name, offsetX + 20, offsetY + 19);
|
||||
paintClientIcon(ctx, entry.icon, offsetX + 1, offsetY + 1, 16, 16);
|
||||
ctx.fillText(entry.name, offsetX + 20, offsetY + 19);
|
||||
|
||||
offsetY += 18;
|
||||
|
||||
|
@ -98,9 +77,9 @@ export function generateDragElement(entries: RDPEntry[]) : HTMLElement {
|
|||
}
|
||||
|
||||
canvas.style.position = "absolute";
|
||||
canvas.style.left = "-100000000px";
|
||||
canvas.style.top = (Math.random() * 1000000).toFixed(0) + "px";
|
||||
document.body.appendChild(canvas);
|
||||
canvas.style.zIndex = "100000";
|
||||
canvas.style.top = "0";
|
||||
canvas.style.left = -canvas.width + "px";
|
||||
|
||||
setTimeout(() => {
|
||||
canvas.remove();
|
||||
|
@ -113,43 +92,19 @@ const kDragDataType = "application/x-teaspeak-channel-move";
|
|||
const kDragHandlerPrefix = "application/x-teaspeak-handler-";
|
||||
const kDragTypePrefix = "application/x-teaspeak-type-";
|
||||
|
||||
export function setupDragData(transfer: DataTransfer, tree: RDPChannelTree, entries: RDPEntry[], type: string) {
|
||||
export function setupDragData(transfer: DataTransfer, handlerId: string, entries: ChannelTreeDragEntry[], type: string) {
|
||||
let data: ChannelTreeDragData = {
|
||||
version: 1,
|
||||
handlerId: tree.handlerId,
|
||||
entryIds: entries.map(e => e.entryId),
|
||||
entryTypes: entries.map(entry => {
|
||||
if(entry instanceof RDPServer) {
|
||||
return "server";
|
||||
} else if(entry instanceof RDPClient) {
|
||||
return "client";
|
||||
} else {
|
||||
return "channel";
|
||||
}
|
||||
}),
|
||||
handlerId: handlerId,
|
||||
entries: entries,
|
||||
type: type
|
||||
};
|
||||
|
||||
transfer.effectAllowed = "all"
|
||||
transfer.dropEffect = "move";
|
||||
transfer.setData(kDragHandlerPrefix + tree.handlerId, "");
|
||||
transfer.setData(kDragHandlerPrefix + handlerId, "");
|
||||
transfer.setData(kDragTypePrefix + type, "");
|
||||
transfer.setData(kDragDataType, JSON.stringify(data));
|
||||
|
||||
{
|
||||
let texts = [];
|
||||
for(const entry of entries) {
|
||||
if(entry instanceof RDPClient) {
|
||||
texts.push(entry.name?.name);
|
||||
} else if(entry instanceof RDPChannel) {
|
||||
texts.push(entry.info?.name);
|
||||
} else if(entry instanceof RDPServer) {
|
||||
texts.push(entry.state.state === "connected" ? entry.state.name : undefined);
|
||||
}
|
||||
}
|
||||
transfer.setData("text/plain", texts.filter(e => !!e).join(", "));
|
||||
}
|
||||
/* TODO: Other things as well! */
|
||||
}
|
||||
|
||||
export function parseDragData(transfer: DataTransfer) : ChannelTreeDragData | undefined {
|
||||
|
|
|
@ -3,6 +3,8 @@ import * as loader from "tc-loader";
|
|||
import {Stage} from "tc-loader";
|
||||
import {getIpcInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
||||
import {Settings} from "tc-shared/settings";
|
||||
import {generateDragElement, setupDragData} from "tc-shared/ui/tree/DragHelper";
|
||||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
|
||||
const kIpcChannel = "entry-tags";
|
||||
const cssStyle = require("./EntryTags.scss");
|
||||
|
@ -24,6 +26,22 @@ export const ClientTag = (props: { clientName: string, clientUniqueId: string, h
|
|||
pageY: event.pageY
|
||||
});
|
||||
}}
|
||||
draggable={true}
|
||||
onDragStart={event => {
|
||||
/* clients only => move */
|
||||
event.dataTransfer.effectAllowed = "move"; /* prohibit copying */
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
event.dataTransfer.setDragImage(generateDragElement([{ icon: ClientIcon.PlayerOn, name: props.clientName }]), 0, 6);
|
||||
setupDragData(event.dataTransfer, props.handlerId, [
|
||||
{
|
||||
type: "client",
|
||||
clientUniqueId: props.clientUniqueId,
|
||||
clientId: props.clientId,
|
||||
clientDatabaseId: props.clientDatabaseId
|
||||
}
|
||||
], "client");
|
||||
event.dataTransfer.setData("text/plain", props.clientName);
|
||||
}}
|
||||
>
|
||||
{props.clientName}
|
||||
</div>
|
||||
|
@ -43,6 +61,19 @@ export const ChannelTag = (props: { channelName: string, channelId: number, hand
|
|||
pageY: event.pageY
|
||||
});
|
||||
}}
|
||||
draggable={true}
|
||||
onDragStart={event => {
|
||||
event.dataTransfer.effectAllowed = "all";
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
event.dataTransfer.setDragImage(generateDragElement([{ icon: ClientIcon.ChannelGreen, name: props.channelName }]), 0, 6);
|
||||
setupDragData(event.dataTransfer, props.handlerId, [
|
||||
{
|
||||
type: "channel",
|
||||
channelId: props.channelId
|
||||
}
|
||||
], "channel");
|
||||
event.dataTransfer.setData("text/plain", props.channelName);
|
||||
}}
|
||||
>
|
||||
{props.channelName}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {EventHandler, Registry} from "tc-shared/events";
|
||||
import {
|
||||
ChannelEntryInfo,
|
||||
ChannelIcons,
|
||||
ChannelIcons, ChannelTreeDragEntry,
|
||||
ChannelTreeUIEvents,
|
||||
ClientIcons,
|
||||
ClientNameInfo,
|
||||
|
@ -22,7 +22,13 @@ import {
|
|||
RendererClient
|
||||
} from "tc-shared/ui/tree/RendererClient";
|
||||
import {ServerRenderer} from "tc-shared/ui/tree/RendererServer";
|
||||
import {generateDragElement, getDragInfo, parseDragData, setupDragData} from "tc-shared/ui/tree/DragHelper";
|
||||
import {
|
||||
DragImageEntryType,
|
||||
generateDragElement,
|
||||
getDragInfo,
|
||||
parseDragData,
|
||||
setupDragData
|
||||
} from "tc-shared/ui/tree/DragHelper";
|
||||
import {createErrorModal} from "tc-shared/ui/elements/Modal";
|
||||
|
||||
function isEquivalent(a, b) {
|
||||
|
@ -64,6 +70,25 @@ function isEquivalent(a, b) {
|
|||
}
|
||||
}
|
||||
|
||||
function generateDragElementFromRdp(entries: RDPEntry[]) : HTMLElement {
|
||||
return generateDragElement(entries.map<DragImageEntryType>(entry => {
|
||||
if(entry instanceof RDPClient) {
|
||||
return { name: entry.name?.name, icon: entry.status };
|
||||
} else if(entry instanceof RDPChannel) {
|
||||
return { name: entry.info?.name, icon: entry.icon };
|
||||
} else if(entry instanceof RDPServer) {
|
||||
switch (entry.state.state) {
|
||||
case "connected":
|
||||
return { name: entry.state.name, icon: ClientIcon.ServerGreen };
|
||||
case "disconnected":
|
||||
return { name: tr("Not connected"), icon: ClientIcon.ServerGreen };
|
||||
case "connecting":
|
||||
return { name: tr("Connecting"), icon: ClientIcon.ServerGreen };
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* auto := Select/unselect/add/remove depending on the selected state & shift key state
|
||||
* exclusive := Only selected these entries
|
||||
|
@ -476,8 +501,38 @@ export class RDPChannelTree {
|
|||
}
|
||||
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
event.dataTransfer.setDragImage(generateDragElement(entries), 0, 6);
|
||||
setupDragData(event.dataTransfer, this, entries, dragType);
|
||||
event.dataTransfer.setDragImage(generateDragElementFromRdp(entries), 0, 6);
|
||||
setupDragData(event.dataTransfer, this.handlerId, entries.map<ChannelTreeDragEntry>(entry => {
|
||||
if(entry instanceof RDPClient) {
|
||||
return {
|
||||
type: "client",
|
||||
uniqueTreeId: entry.entryId
|
||||
};
|
||||
} else if(entry instanceof RDPChannel) {
|
||||
return {
|
||||
type: "channel",
|
||||
uniqueTreeId: entry.entryId
|
||||
};
|
||||
} else if(entry instanceof RDPServer) {
|
||||
return {
|
||||
type: "server",
|
||||
};
|
||||
}
|
||||
}).filter(entry => !!entry), dragType);
|
||||
|
||||
{
|
||||
let texts = [];
|
||||
for(const entry of entries) {
|
||||
if(entry instanceof RDPClient) {
|
||||
texts.push(entry.name?.name);
|
||||
} else if(entry instanceof RDPChannel) {
|
||||
texts.push(entry.info?.name);
|
||||
} else if(entry instanceof RDPServer) {
|
||||
texts.push(entry.state.state === "connected" ? entry.state.name : undefined);
|
||||
}
|
||||
}
|
||||
event.dataTransfer.setData("text/plain", texts.filter(e => !!e).join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -565,7 +620,7 @@ export class RDPChannelTree {
|
|||
}
|
||||
|
||||
this.events.fire("action_move_clients", {
|
||||
entries: data.entryIds,
|
||||
entries: data.entries,
|
||||
targetTreeEntry: target.entryId
|
||||
});
|
||||
} else if(data.type === "channel") {
|
||||
|
@ -577,14 +632,14 @@ export class RDPChannelTree {
|
|||
return;
|
||||
}
|
||||
|
||||
if(data.entryIds.indexOf(target.entryId) !== -1) {
|
||||
if(data.entries.findIndex(entry => entry.type === "channel" && "uniqueTreeId" in entry && entry.uniqueTreeId === target.entryId) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.events.fire("action_move_channels", {
|
||||
targetTreeEntry: target.entryId,
|
||||
mode: currentDragHint === "contain" ? "child" : currentDragHint === "top" ? "before" : "after",
|
||||
entries: data.entryIds
|
||||
entries: data.entries
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue