Allowing to drag client tags

master
WolverinDEV 2020-12-09 15:50:58 +01:00 committed by WolverinDEV
parent 0035e73179
commit 069c02a76e
8 changed files with 184 additions and 90 deletions

View File

@ -5,6 +5,7 @@
- Improved channel conversation mode detection support - Improved channel conversation mode detection support
- Added support for HTML encoded links (Example would be when copying from Edge the URL) - Added support for HTML encoded links (Example would be when copying from Edge the URL)
- Enabled context menus for all clickable client tags - Enabled context menus for all clickable client tags
- Allowing to drag client tags
* **08.12.20** * **08.12.20**
- Fixed the permission editor not resolving unique ids - Fixed the permission editor not resolving unique ids

View File

@ -215,17 +215,19 @@ export abstract class AbstractChat<Events extends AbstractChatEvents> {
return this.conversationMode === ChannelConversationMode.Private; return this.conversationMode === ChannelConversationMode.Private;
} }
protected setConversationMode(mode: ChannelConversationMode) { protected setConversationMode(mode: ChannelConversationMode, logChange: boolean) {
if(this.conversationMode === mode) { if(this.conversationMode === mode) {
return; return;
} }
this.registerChatEvent({ if(logChange) {
type: "mode-changed", this.registerChatEvent({
uniqueId: guid() + "-mode-change", type: "mode-changed",
timestamp: Date.now(), uniqueId: guid() + "-mode-change",
newMode: mode === ChannelConversationMode.Public ? "normal" : mode === ChannelConversationMode.Private ? "private" : "none" timestamp: Date.now(),
}, true); newMode: mode === ChannelConversationMode.Public ? "normal" : mode === ChannelConversationMode.Private ? "private" : "none"
}, true);
}
this.conversationMode = mode; this.conversationMode = mode;
this.events.fire("notify_conversation_mode_changed", { newMode: mode }); this.events.fire("notify_conversation_mode_changed", { newMode: mode });

View File

@ -165,18 +165,18 @@ export class ChannelConversation extends AbstractChat<ChannelConversationEvents>
break; break;
case "private": case "private":
this.setConversationMode(ChannelConversationMode.Private); this.setConversationMode(ChannelConversationMode.Private, true);
this.setCurrentMode("normal"); this.setCurrentMode("normal");
break; break;
case "success": case "success":
this.setConversationMode(ChannelConversationMode.Public); this.setConversationMode(ChannelConversationMode.Public, true);
this.setCurrentMode("normal"); this.setCurrentMode("normal");
break; break;
case "unsupported": case "unsupported":
this.crossChannelChatSupported = false; this.crossChannelChatSupported = false;
this.setConversationMode(ChannelConversationMode.Private); this.setConversationMode(ChannelConversationMode.Private, true);
this.setCurrentMode("normal"); this.setCurrentMode("normal");
break; break;
} }
@ -295,8 +295,8 @@ export class ChannelConversation extends AbstractChat<ChannelConversationEvents>
this.handle.connection.settings.changeServer(Settings.FN_CHANNEL_CHAT_READ(this.conversationId), timestamp); this.handle.connection.settings.changeServer(Settings.FN_CHANNEL_CHAT_READ(this.conversationId), timestamp);
} }
public setConversationMode(mode: ChannelConversationMode) { public setConversationMode(mode: ChannelConversationMode, logChange: boolean) {
super.setConversationMode(mode); super.setConversationMode(mode, logChange);
} }
public localClientSwitchedChannel(type: "join" | "leave") { public localClientSwitchedChannel(type: "join" | "leave") {
@ -356,7 +356,7 @@ export class ChannelConversationManager extends AbstractChatManager<ChannelConve
} }
if("channel_conversation_mode" in event.updatedProperties) { 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(); conversation.updateAccessState();
} }
})); }));
@ -405,6 +405,10 @@ export class ChannelConversationManager extends AbstractChatManager<ChannelConve
let conversation = this.findConversation(channelId); let conversation = this.findConversation(channelId);
if(!conversation) { if(!conversation) {
conversation = new ChannelConversation(this, channelId); 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); this.registerConversation(conversation);
} }

View File

@ -781,7 +781,26 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
} }
const clients = event.entries.map(entryId => { 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)) { 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); logWarn(LogCategory.CHANNEL, tr("Received client move notify with an entry id which isn't a client. Entry id: %o"), entryId);
return undefined; return undefined;
@ -808,7 +827,15 @@ export function initializeChannelTreeController(events: Registry<ChannelTreeUIEv
} }
let channels = event.entries.map(entryId => { 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)) { 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); logWarn(LogCategory.CHANNEL, tr("Received channel move notify with a channel id which isn't a channel. Entry id: %o"), entryId);
return undefined; return undefined;

View File

@ -37,8 +37,8 @@ export interface ChannelTreeUIEvents {
action_channel_open_file_browser: { treeEntryId: number }, action_channel_open_file_browser: { treeEntryId: number },
action_client_double_click: { treeEntryId: number }, action_client_double_click: { treeEntryId: number },
action_client_name_submit: { treeEntryId: number, name: string }, action_client_name_submit: { treeEntryId: number, name: string },
action_move_clients: { targetTreeEntry: number, entries: number[] }, action_move_clients: { targetTreeEntry: number, entries: ChannelTreeDragEntry[] },
action_move_channels: { targetTreeEntry: number, mode: "before" | "after" | "child", entries: number[] }, action_move_channels: { targetTreeEntry: number, mode: "before" | "after" | "child", entries: ChannelTreeDragEntry[] },
/* queries */ /* queries */
query_tree_entries: {}, query_tree_entries: {},
@ -82,11 +82,30 @@ export interface ChannelTreeUIEvents {
notify_destroy: {} 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 = { export type ChannelTreeDragData = {
version: 1, version: 1,
handlerId: string, handlerId: string,
type: string, type: string,
entryIds: number[], entries: ChannelTreeDragEntry[],
entryTypes: ("server" | "channel" | "client")[]
}; };

View File

@ -1,4 +1,3 @@
import {RDPChannel, RDPChannelTree, RDPClient, RDPEntry, RDPServer} from "./RendererDataProvider";
import * as loader from "tc-loader"; import * as loader from "tc-loader";
import {Stage} from "tc-loader"; import {Stage} from "tc-loader";
import { import {
@ -9,7 +8,7 @@ import {
spriteWidth as kClientSpriteWidth, spriteWidth as kClientSpriteWidth,
} from "svg-sprites/client-icons"; } from "svg-sprites/client-icons";
import {LogCategory, logDebug} from "tc-shared/log"; 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; 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); 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 totalHeight = entries.length * 18 + 2; /* the two extra for "low" letters like "gyj" etc. */
const totalWidth = 250; const totalWidth = 250;
@ -37,44 +41,19 @@ export function generateDragElement(entries: RDPEntry[]) : HTMLElement {
let offsetX = 20; let offsetX = 20;
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
canvas.height = totalHeight; canvas.height = totalHeight;
canvas.width = totalWidth; canvas.width = totalWidth;
/* TODO: With font size? */ /* TODO: With font size? */
const ctx = canvas.getContext("2d");
{ {
const ctx = canvas.getContext("2d");
ctx.textAlign = "left"; ctx.textAlign = "left";
ctx.textBaseline = "bottom"; ctx.textBaseline = "bottom";
ctx.font = "700 16px Roboto, Helvetica, Arial, sans-serif"; ctx.font = "700 16px Roboto, Helvetica, Arial, sans-serif";
for(const entry of entries) { 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.strokeStyle = "red";
ctx.moveTo(offsetX, offsetY); ctx.moveTo(offsetX, offsetY);
@ -83,8 +62,8 @@ export function generateDragElement(entries: RDPEntry[]) : HTMLElement {
*/ */
ctx.fillStyle = "black"; ctx.fillStyle = "black";
paintClientIcon(ctx, icon, offsetX + 1, offsetY + 1, 16, 16); paintClientIcon(ctx, entry.icon, offsetX + 1, offsetY + 1, 16, 16);
ctx.fillText(name, offsetX + 20, offsetY + 19); ctx.fillText(entry.name, offsetX + 20, offsetY + 19);
offsetY += 18; offsetY += 18;
@ -98,9 +77,9 @@ export function generateDragElement(entries: RDPEntry[]) : HTMLElement {
} }
canvas.style.position = "absolute"; canvas.style.position = "absolute";
canvas.style.left = "-100000000px"; canvas.style.zIndex = "100000";
canvas.style.top = (Math.random() * 1000000).toFixed(0) + "px"; canvas.style.top = "0";
document.body.appendChild(canvas); canvas.style.left = -canvas.width + "px";
setTimeout(() => { setTimeout(() => {
canvas.remove(); canvas.remove();
@ -113,43 +92,19 @@ const kDragDataType = "application/x-teaspeak-channel-move";
const kDragHandlerPrefix = "application/x-teaspeak-handler-"; const kDragHandlerPrefix = "application/x-teaspeak-handler-";
const kDragTypePrefix = "application/x-teaspeak-type-"; 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 = { let data: ChannelTreeDragData = {
version: 1, version: 1,
handlerId: tree.handlerId, handlerId: handlerId,
entryIds: entries.map(e => e.entryId), entries: entries,
entryTypes: entries.map(entry => {
if(entry instanceof RDPServer) {
return "server";
} else if(entry instanceof RDPClient) {
return "client";
} else {
return "channel";
}
}),
type: type type: type
}; };
transfer.effectAllowed = "all" transfer.effectAllowed = "all"
transfer.dropEffect = "move"; transfer.dropEffect = "move";
transfer.setData(kDragHandlerPrefix + tree.handlerId, ""); transfer.setData(kDragHandlerPrefix + handlerId, "");
transfer.setData(kDragTypePrefix + type, ""); transfer.setData(kDragTypePrefix + type, "");
transfer.setData(kDragDataType, JSON.stringify(data)); 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 { export function parseDragData(transfer: DataTransfer) : ChannelTreeDragData | undefined {

View File

@ -3,6 +3,8 @@ import * as loader from "tc-loader";
import {Stage} from "tc-loader"; import {Stage} from "tc-loader";
import {getIpcInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC"; import {getIpcInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC";
import {Settings} from "tc-shared/settings"; 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 kIpcChannel = "entry-tags";
const cssStyle = require("./EntryTags.scss"); const cssStyle = require("./EntryTags.scss");
@ -24,6 +26,22 @@ export const ClientTag = (props: { clientName: string, clientUniqueId: string, h
pageY: event.pageY 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} {props.clientName}
</div> </div>
@ -43,6 +61,19 @@ export const ChannelTag = (props: { channelName: string, channelId: number, hand
pageY: event.pageY 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} {props.channelName}
</div> </div>

View File

@ -1,7 +1,7 @@
import {EventHandler, Registry} from "tc-shared/events"; import {EventHandler, Registry} from "tc-shared/events";
import { import {
ChannelEntryInfo, ChannelEntryInfo,
ChannelIcons, ChannelIcons, ChannelTreeDragEntry,
ChannelTreeUIEvents, ChannelTreeUIEvents,
ClientIcons, ClientIcons,
ClientNameInfo, ClientNameInfo,
@ -22,7 +22,13 @@ import {
RendererClient RendererClient
} from "tc-shared/ui/tree/RendererClient"; } from "tc-shared/ui/tree/RendererClient";
import {ServerRenderer} from "tc-shared/ui/tree/RendererServer"; 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"; import {createErrorModal} from "tc-shared/ui/elements/Modal";
function isEquivalent(a, b) { 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 * auto := Select/unselect/add/remove depending on the selected state & shift key state
* exclusive := Only selected these entries * exclusive := Only selected these entries
@ -476,8 +501,38 @@ export class RDPChannelTree {
} }
event.dataTransfer.dropEffect = "move"; event.dataTransfer.dropEffect = "move";
event.dataTransfer.setDragImage(generateDragElement(entries), 0, 6); event.dataTransfer.setDragImage(generateDragElementFromRdp(entries), 0, 6);
setupDragData(event.dataTransfer, this, entries, dragType); 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", { this.events.fire("action_move_clients", {
entries: data.entryIds, entries: data.entries,
targetTreeEntry: target.entryId targetTreeEntry: target.entryId
}); });
} else if(data.type === "channel") { } else if(data.type === "channel") {
@ -577,14 +632,14 @@ export class RDPChannelTree {
return; 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; return;
} }
this.events.fire("action_move_channels", { this.events.fire("action_move_channels", {
targetTreeEntry: target.entryId, targetTreeEntry: target.entryId,
mode: currentDragHint === "contain" ? "child" : currentDragHint === "top" ? "before" : "after", mode: currentDragHint === "contain" ? "child" : currentDragHint === "top" ? "before" : "after",
entries: data.entryIds entries: data.entries
}); });
} }
} }