Allowing to drag client tags

canary
WolverinDEV 2020-12-09 15:50:58 +01:00
parent b665d69a9f
commit a3b9b1b11e
8 changed files with 184 additions and 90 deletions

View File

@ -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

View File

@ -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 });

View File

@ -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);
}

View File

@ -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;

View File

@ -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[],
};

View File

@ -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 {

View File

@ -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>

View File

@ -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
});
}
}