Added file transfer to the side bar

canary
WolverinDEV 2020-12-18 19:18:01 +01:00
parent 3412faf125
commit 5da2877c8b
24 changed files with 1388 additions and 814 deletions

View File

@ -202,6 +202,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
this.channelTree = channelTree; this.channelTree = channelTree;
this.events = new Registry<ChannelEvents>(); this.events = new Registry<ChannelEvents>();
this.subscribed = false;
this.properties = new ChannelProperties(); this.properties = new ChannelProperties();
this.channelId = channelId; this.channelId = channelId;
this.properties.channel_name = channelName; this.properties.channel_name = channelName;
@ -712,6 +713,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
cached_password() { return this.cachedPasswordHash; } cached_password() { return this.cachedPasswordHash; }
async updateSubscribeMode() { async updateSubscribeMode() {
console.error("Update subscribe mode");
let shouldBeSubscribed = false; let shouldBeSubscribed = false;
switch (this.subscriptionMode) { switch (this.subscriptionMode) {
case ChannelSubscribeMode.INHERITED: case ChannelSubscribeMode.INHERITED:

View File

@ -8,6 +8,7 @@ import {PrivateConversationsPanel} from "tc-shared/ui/frames/side/PrivateConvers
import {ChannelBarRenderer} from "tc-shared/ui/frames/side/ChannelBarRenderer"; import {ChannelBarRenderer} from "tc-shared/ui/frames/side/ChannelBarRenderer";
import {LogCategory, logWarn} from "tc-shared/log"; import {LogCategory, logWarn} from "tc-shared/log";
import React = require("react"); import React = require("react");
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
const cssStyle = require("./SideBarRenderer.scss"); const cssStyle = require("./SideBarRenderer.scss");
@ -52,6 +53,7 @@ const ContentRendererClientInfo = () => {
const contentData = useContentData("client-info"); const contentData = useContentData("client-info");
if(!contentData) { return null; } if(!contentData) { return null; }
throw "XX";
return ( return (
<ClientInfoRenderer <ClientInfoRenderer
events={contentData.events} events={contentData.events}
@ -62,13 +64,25 @@ const ContentRendererClientInfo = () => {
const SideBarFrame = (props: { type: SideBarType }) => { const SideBarFrame = (props: { type: SideBarType }) => {
switch (props.type) { switch (props.type) {
case "channel": case "channel":
return <ContentRendererChannel key={props.type} />; return (
<ErrorBoundary key={props.type}>
<ContentRendererChannel />
</ErrorBoundary>
);
case "private-chat": case "private-chat":
return <ContentRendererPrivateConversation key={props.type} />; return (
<ErrorBoundary key={props.type}>
<ContentRendererPrivateConversation />
</ErrorBoundary>
);
case "client-info": case "client-info":
return <ContentRendererClientInfo key={props.type} />; return (
<ErrorBoundary key={props.type}>
<ContentRendererClientInfo />
</ErrorBoundary>
);
case "music-manage": case "music-manage":
/* TODO! */ /* TODO! */

View File

@ -5,12 +5,14 @@ import {ChannelEntry, ChannelSidebarMode} from "tc-shared/tree/Channel";
import {ChannelConversationController} from "tc-shared/ui/frames/side/ChannelConversationController"; import {ChannelConversationController} from "tc-shared/ui/frames/side/ChannelConversationController";
import {ChannelDescriptionController} from "tc-shared/ui/frames/side/ChannelDescriptionController"; import {ChannelDescriptionController} from "tc-shared/ui/frames/side/ChannelDescriptionController";
import {LocalClientEntry} from "tc-shared/tree/Client"; import {LocalClientEntry} from "tc-shared/tree/Client";
import {ChannelFileBrowserController} from "tc-shared/ui/frames/side/ChannelFileBrowserController";
export class ChannelBarController { export class ChannelBarController {
readonly uiEvents: Registry<ChannelBarUiEvents>; readonly uiEvents: Registry<ChannelBarUiEvents>;
private channelConversations: ChannelConversationController; private channelConversations: ChannelConversationController;
private description: ChannelDescriptionController; private description: ChannelDescriptionController;
private fileBrowser: ChannelFileBrowserController;
private currentConnection: ConnectionHandler; private currentConnection: ConnectionHandler;
private listenerConnection: (() => void)[]; private listenerConnection: (() => void)[];
@ -25,6 +27,7 @@ export class ChannelBarController {
this.channelConversations = new ChannelConversationController(); this.channelConversations = new ChannelConversationController();
this.description = new ChannelDescriptionController(); this.description = new ChannelDescriptionController();
this.fileBrowser = new ChannelFileBrowserController();
this.uiEvents.on("query_mode", () => this.notifyChannelMode()); this.uiEvents.on("query_mode", () => this.notifyChannelMode());
this.uiEvents.on("query_channel_id", () => this.notifyChannelId()); this.uiEvents.on("query_channel_id", () => this.notifyChannelId());
@ -41,6 +44,9 @@ export class ChannelBarController {
this.currentChannel = undefined; this.currentChannel = undefined;
this.currentConnection = undefined; this.currentConnection = undefined;
this.fileBrowser?.destroy();
this.fileBrowser = undefined;
this.channelConversations?.destroy(); this.channelConversations?.destroy();
this.channelConversations = undefined; this.channelConversations = undefined;
@ -56,6 +62,7 @@ export class ChannelBarController {
} }
this.channelConversations.setConnectionHandler(handler); this.channelConversations.setConnectionHandler(handler);
this.fileBrowser.setConnectionHandler(handler);
this.listenerConnection.forEach(callback => callback()); this.listenerConnection.forEach(callback => callback());
this.listenerConnection = []; this.listenerConnection = [];
@ -96,6 +103,7 @@ export class ChannelBarController {
return; return;
} }
this.fileBrowser.setChannel(channel);
this.description.setChannel(channel); this.description.setChannel(channel);
this.listenerChannel.forEach(callback => callback()); this.listenerChannel.forEach(callback => callback());
@ -185,9 +193,9 @@ export class ChannelBarController {
this.uiEvents.fire_react("notify_data", { this.uiEvents.fire_react("notify_data", {
content: "file-transfer", content: "file-transfer",
data: { data: {
events: this.fileBrowser.uiEvents
} }
}); });
/* TODO! */
break; break;
} }
} }

View File

@ -1,6 +1,7 @@
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions"; import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions";
import {ChannelDescriptionUiEvents} from "tc-shared/ui/frames/side/ChannelDescriptionDefinitions"; import {ChannelDescriptionUiEvents} from "tc-shared/ui/frames/side/ChannelDescriptionDefinitions";
import {ChannelFileBrowserUiEvents} from "tc-shared/ui/frames/side/ChannelFileBrowserDefinitions";
export type ChannelBarMode = "conversation" | "description" | "file-transfer" | "none"; export type ChannelBarMode = "conversation" | "description" | "file-transfer" | "none";
@ -12,7 +13,7 @@ export interface ChannelBarModeData {
events: Registry<ChannelDescriptionUiEvents> events: Registry<ChannelDescriptionUiEvents>
}, },
"file-transfer": { "file-transfer": {
/* TODO! */ events: Registry<ChannelFileBrowserUiEvents>
}, },
"none": {} "none": {}
} }

View File

@ -5,6 +5,7 @@ import * as React from "react";
import {ConversationPanel} from "tc-shared/ui/frames/side/AbstractConversationRenderer"; import {ConversationPanel} from "tc-shared/ui/frames/side/AbstractConversationRenderer";
import {useDependentState} from "tc-shared/ui/react-elements/Helper"; import {useDependentState} from "tc-shared/ui/react-elements/Helper";
import {ChannelDescriptionRenderer} from "tc-shared/ui/frames/side/ChannelDescriptionRenderer"; import {ChannelDescriptionRenderer} from "tc-shared/ui/frames/side/ChannelDescriptionRenderer";
import {ChannelFileBrowser} from "tc-shared/ui/frames/side/ChannelFileBrowserRenderer";
const EventContext = React.createContext<Registry<ChannelBarUiEvents>>(undefined); const EventContext = React.createContext<Registry<ChannelBarUiEvents>>(undefined);
const ChannelContext = React.createContext<{ channelId: number, handlerId: string }>(undefined); const ChannelContext = React.createContext<{ channelId: number, handlerId: string }>(undefined);
@ -37,8 +38,7 @@ const ModeRenderer = () => {
return <ModeRendererDescription key={"description"} />; return <ModeRendererDescription key={"description"} />;
case "file-transfer": case "file-transfer":
/* TODO! */ return <ModeRendererFileTransfer key={"file"} />;
return null;
case "none": case "none":
default: default:
@ -72,6 +72,16 @@ const ModeRendererDescription = React.memo(() => {
); );
}); });
const ModeRendererFileTransfer = React.memo(() => {
const channelContext = useContext(ChannelContext);
const data = useModeData("file-transfer", [ channelContext ]);
if(!data) { return null; }
return (
<ChannelFileBrowser events={data.events} />
);
});
export const ChannelBarRenderer = (props: { events: Registry<ChannelBarUiEvents> }) => { export const ChannelBarRenderer = (props: { events: Registry<ChannelBarUiEvents> }) => {
const [ channelContext, setChannelContext ] = useState<{ channelId: number, handlerId: string }>(() => { const [ channelContext, setChannelContext ] = useState<{ channelId: number, handlerId: string }>(() => {
props.events.fire("query_channel_id"); props.events.fire("query_channel_id");

View File

@ -0,0 +1,64 @@
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {Registry} from "tc-shared/events";
import {channelPathPrefix, FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions";
import {initializeRemoteFileBrowserController} from "tc-shared/ui/modal/transfer/FileBrowserControllerRemote";
import {ChannelFileBrowserUiEvents} from "tc-shared/ui/frames/side/ChannelFileBrowserDefinitions";
import {ChannelEntry} from "tc-shared/tree/Channel";
export class ChannelFileBrowserController {
readonly uiEvents: Registry<ChannelFileBrowserUiEvents>;
private currentConnection: ConnectionHandler;
private remoteBrowseEvents: Registry<FileBrowserEvents>;
private currentChannel: ChannelEntry;
constructor() {
this.uiEvents = new Registry<ChannelFileBrowserUiEvents>();
this.uiEvents.on("query_events", () => this.notifyEvents());
}
destroy() {
this.currentChannel = undefined;
this.setConnectionHandler(undefined);
}
setConnectionHandler(connection: ConnectionHandler) {
if(this.currentConnection === connection) {
return;
}
if(this.remoteBrowseEvents) {
this.remoteBrowseEvents.fire("notify_destroy");
this.remoteBrowseEvents.destroy();
}
this.currentConnection = connection;
if(connection) {
this.remoteBrowseEvents = new Registry<FileBrowserEvents>();
initializeRemoteFileBrowserController(connection, this.remoteBrowseEvents);
}
this.setChannel(undefined);
this.notifyEvents();
}
setChannel(channel: ChannelEntry | undefined) {
if(channel === this.currentChannel) {
return;
}
this.currentChannel = channel;
if(channel) {
this.remoteBrowseEvents?.fire("action_navigate_to", { path: "/" + channelPathPrefix + channel.channelId + "/" });
} else {
this.remoteBrowseEvents?.fire("action_navigate_to", { path: "/" });
}
}
private notifyEvents() {
this.uiEvents.fire_react("notify_events", { browserEvents: this.remoteBrowseEvents, channelId: this.currentChannel?.channelId });
}
}

View File

@ -0,0 +1,11 @@
import {FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions";
import {Registry} from "tc-shared/events";
export interface ChannelFileBrowserUiEvents {
query_events: {},
notify_events: {
browserEvents: Registry<FileBrowserEvents>,
channelId: number
},
}

View File

@ -0,0 +1,45 @@
.container {
display: flex;
flex-direction: column;
justify-content: stretch;
height: 100%;
width: 100%;
color: #999;
.navbar {
flex-shrink: 0;
flex-grow: 0;
padding: .5em;
}
}
.fileTable {
border: none;
border-radius: 0;
background-color: var(--chat-background);
.header {
background-color: var(--chat-background);
}
}
.fileEntry:hover, .fileEntry.hovered {
background-color: var(--channel-tree-entry-hovered) !important;
}
.fileEntrySelected {
background-color: var(--channel-tree-entry-selected) !important;
}
.boxedInput {
background-color: var(--side-info-background);
border-color: #212121;
&:focus-within {
background-color: #242424;
}
}

View File

@ -0,0 +1,53 @@
import {useState} from "react";
import {Registry} from "tc-shared/events";
import {ChannelFileBrowserUiEvents} from "tc-shared/ui/frames/side/ChannelFileBrowserDefinitions";
import {channelPathPrefix, FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions";
import {
FileBrowserClassContext,
FileBrowserRenderer, FileBrowserRendererClasses,
NavigationBar
} from "tc-shared/ui/modal/transfer/FileBrowserRenderer";
import * as React from "react";
const cssStyle = require("./ChannelFileBrowserRenderer.scss");
const kFileBrowserClasses: FileBrowserRendererClasses = {
navigation: {
boxedInput: cssStyle.boxedInput
},
fileTable: {
table: cssStyle.fileTable,
header: cssStyle.header
},
fileEntry: {
entry: cssStyle.fileEntry,
dropHovered: cssStyle.hovered,
selected: cssStyle.fileEntrySelected
}
};
export const ChannelFileBrowser = (props: { events: Registry<ChannelFileBrowserUiEvents> }) => {
const [ events, setEvents ] = useState<{ events: Registry<FileBrowserEvents>, channelId: number }>(() => {
props.events.fire("query_events");
return undefined;
});
props.events.reactUse("notify_events", event => setEvents({
events: event.browserEvents,
channelId: event.channelId
}));
if(!events) {
return null;
}
return (
<div className={cssStyle.container}>
<FileBrowserClassContext.Provider value={kFileBrowserClasses}>
<div className={cssStyle.navbar}>
<NavigationBar events={events.events} initialPath={events.channelId > 0 ? "/" + channelPathPrefix + events.channelId + "/" : "/"} />
</div>
<FileBrowserRenderer initialPath={events.channelId > 0 ? "/" + channelPathPrefix + events.channelId + "/" : "/"} events={events.events} key={"browser"} />
</FileBrowserClassContext.Provider>
</div>
);
};

View File

@ -18,15 +18,13 @@ import {
TransferTargetType TransferTargetType
} from "../../../file/Transfer"; } from "../../../file/Transfer";
import {createErrorModal} from "../../../ui/elements/Modal"; import {createErrorModal} from "../../../ui/elements/Modal";
import {ErrorCode} from "../../../connection/ErrorCode";
import { import {
avatarsPathPrefix, avatarsPathPrefix,
channelPathPrefix, channelPathPrefix, FileBrowserEvents,
FileBrowserEvents, iconPathPrefix, ListedFileInfo,
iconPathPrefix,
ListedFileInfo,
PathInfo PathInfo
} from "../../../ui/modal/transfer/ModalFileTransfer"; } from "tc-shared/ui/modal/transfer/FileDefinitions";
import {ErrorCode} from "../../../connection/ErrorCode";
function parsePath(path: string, connection: ConnectionHandler): PathInfo { function parsePath(path: string, connection: ConnectionHandler): PathInfo {
if (path === "/" || !path) { if (path === "/" || !path) {
@ -46,7 +44,7 @@ function parsePath(path: string, connection: ConnectionHandler): PathInfo {
const channel = connection.channelTree.findChannel(channelId); const channel = connection.channelTree.findChannel(channelId);
if (!channel) { if (!channel) {
throw tr("Channel not visible anymore"); throw tr("Invalid channel id");
} }
return { return {
@ -79,13 +77,13 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
try { try {
const info = parsePath(event.path, connection); const info = parsePath(event.path, connection);
events.fire_react("action_navigate_to_result", { events.fire_react("notify_current_path", {
path: event.path || "/", path: event.path || "/",
status: "success", status: "success",
pathInfo: info pathInfo: info
}); });
} catch (error) { } catch (error) {
events.fire_react("action_navigate_to_result", { events.fire_react("notify_current_path", {
path: event.path, path: event.path,
status: "error", status: "error",
error: error error: error
@ -284,8 +282,9 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
let sourcePath: PathInfo, targetPath: PathInfo; let sourcePath: PathInfo, targetPath: PathInfo;
try { try {
sourcePath = parsePath(event.oldPath, connection); sourcePath = parsePath(event.oldPath, connection);
if (sourcePath.type !== "channel") if (sourcePath.type !== "channel") {
throw tr("Icon/avatars could not be renamed"); throw tr("Icon/avatars could not be renamed");
}
} catch (error) { } catch (error) {
events.fire_react("action_rename_file_result", { events.fire_react("action_rename_file_result", {
oldPath: event.oldPath, oldPath: event.oldPath,
@ -297,8 +296,9 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
} }
try { try {
targetPath = parsePath(event.newPath, connection); targetPath = parsePath(event.newPath, connection);
if (sourcePath.type !== "channel") if (sourcePath.type !== "channel") {
throw tr("Target path isn't a channel"); throw tr("Target path isn't a channel");
}
} catch (error) { } catch (error) {
events.fire_react("action_rename_file_result", { events.fire_react("action_rename_file_result", {
oldPath: event.oldPath, oldPath: event.oldPath,
@ -374,15 +374,22 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
let currentPath = "/"; let currentPath = "/";
let currentPathInfo: PathInfo; let currentPathInfo: PathInfo;
let selection: { name: string, type: FileType }[] = []; let selection: { name: string, type: FileType }[] = [];
events.on("action_navigate_to_result", result => { events.on("notify_current_path", result => {
if (result.status !== "success") if (result.status !== "success") {
return; return;
}
currentPathInfo = result.pathInfo; currentPathInfo = result.pathInfo;
currentPath = result.path; currentPath = result.path;
selection = []; selection = [];
}); });
events.on("query_current_path", () => events.fire_react("notify_current_path", {
status: "success",
path: currentPath,
pathInfo: currentPathInfo
}));
events.on("action_rename_file_result", result => { events.on("action_rename_file_result", result => {
if (result.status !== "success") if (result.status !== "success")
return; return;
@ -812,10 +819,10 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
}); });
const closeListener = () => unregisterEvents(); const closeListener = () => unregisterEvents();
events.on("notify_modal_closed", closeListener); events.on("notify_destroy", closeListener);
const unregisterEvents = () => { const unregisterEvents = () => {
events.off("notify_modal_closed", closeListener); events.off("notify_destroy", closeListener);
transfer.events.off("notify_progress", progressListener); transfer.events.off("notify_progress", progressListener);
}; };
}; };
@ -823,7 +830,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
const registeredListener = event => listenToTransfer(event.transfer); const registeredListener = event => listenToTransfer(event.transfer);
connection.fileManager.events.on("notify_transfer_registered", registeredListener); connection.fileManager.events.on("notify_transfer_registered", registeredListener);
events.on("notify_modal_closed", () => connection.fileManager.events.off("notify_transfer_registered", registeredListener)); events.on("notify_destroy", () => connection.fileManager.events.off("notify_transfer_registered", registeredListener));
connection.fileManager.registeredTransfers().forEach(transfer => listenToTransfer(transfer)); connection.fileManager.registeredTransfers().forEach(transfer => listenToTransfer(transfer));
} }

View File

@ -0,0 +1,377 @@
@import "../../../../css/static/mixin";
@import "../../../../css/static/properties";
html:root {
--modal-transfer-refresh-hover: #ffffff0e;
--modal-transfer-path-hover: #E6E6E6;
--modal-transfer-error-overlay-text: #9e9494;
--modal-transfer-filelist: #28292b;
--modal-transfer-filelist-border: #161616;
--modal-transfer-entry-hover: #2c2d2f;
--modal-transfer-entry-selected: #1a1a1b;
--modal-transfer-indicator-red: #a10000;
--modal-transfer-indicator-red-end: #e60000;
--modal-transfer-indicator-blue: #005fa1;
--modal-transfer-indicator-blue-end: #007acc;
--modal-transfer-indicator-green: #389738;
--modal-transfer-indicator-green-end: #4ecc4e;
--modal-transfer-indicator-hidden: #28292b00;
--modal-transfer-indicator-hidden-end: #28292b00;
}
.container {
position: relative;
padding: 1em 1em 4em; /* 4em for the transfer info */
display: flex;
flex-direction: column;
justify-content: stretch;
flex-shrink: 1;
flex-grow: 1;
width: 90em;
min-width: 10em;
max-width: 100%;
height: 55em;
max-height: 100%;
min-height: 10em;
}
.arrow {
width: 1em;
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: column;
justify-content: center;
.inner {
flex-grow: 0;
flex-shrink: 0;
align-self: center;
margin-left: -.09em;
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
display: inline-block;
border: solid var(--text);
border-width: 0 0.125em 0.125em 0;
padding: 0.15em;
height: 0.15em;
width: .15em;
}
}
.navigation {
flex-grow: 0;
flex-shrink: 0;
.containerIcon {
margin: auto .25em;
padding: .2em;
display: flex;
flex-direction: column;
justify-content: center;
cursor: pointer;
> div {
padding: .1em;
}
}
.refreshIcon {
border-radius: 1px;
@include transition(background-color $button_hover_animation_time ease-in-out);
> div {
padding: .1em;
}
&.enabled {
cursor: pointer;
&:hover {
background-color: var(--modal-transfer-refresh-hover);
}
}
}
.directoryIcon {
margin-right: -.25em;
}
input {
margin-left: .5em;
}
.containerPath {
@include user-select(none);
display: flex;
flex-direction: row;
justify-content: flex-start;
overflow: hidden;
white-space: nowrap;
width: calc(100% - 1em); /* some space for the text editing */
a.pathShrink {
flex-shrink: 1;
min-width: 5em;
@include text-dotdotdot();
}
a {
cursor: pointer;
@include transition(color $button_hover_animation_time ease-in-out);
&:hover, &.hovered {
color: var(--modal-transfer-path-hover);
}
}
}
}
.fileTable {
min-height: 5em;
flex-grow: 1;
flex-shrink: 1;
border: 1px var(--modal-transfer-filelist-border) solid;
border-radius: 0.2em;
background-color: var(--modal-transfer-filelist);
.header {
z-index: 1;
padding-top: .2em;
padding-bottom: .2em;
background-color: var(--modal-transfer-filelist);
.columnName, .columnSize, .columnType, .columnChanged {
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
.separator {
position: absolute;
right: .2em;
top: .2em;
bottom: .2em;
width: .1em;
background-color: var(--text);
}
}
.columnSize {
width: 8em;
text-align: end;
}
.columnName {
padding-left: .5em;
}
> div:last-of-type {
.separator {
display: none;
}
}
}
.body {
@include user-select(none);
@include chat-scrollbar-vertical();
.columnName {
padding-left: .5em;
display: flex;
flex-direction: row;
justify-content: flex-start;
a, div, img {
align-self: center;
margin-right: .5em;
@include text-dotdotdot();
}
img, div {
flex-shrink: 0;
height: 1em;
width: 1em;
}
input {
height: 1.3em;
align-self: center;
flex-grow: 1;
margin-right: .5em;
border-style: inherit;
padding: .1em;
}
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
a {
text-align: center;
}
}
.overlayError {
a {
font-size: 1.2em;
color: var(--modal-transfer-error-overlay-text);
}
}
.overlayEmptyFolder {
align-self: center;
margin-top: 1em;
}
.directoryEntry {
cursor: pointer;
&:hover {
background-color: var(--modal-transfer-entry-hover);
}
&.selected {
background-color: var(--modal-transfer-entry-selected);
}
/* drag hovered overrides selected */
&.hovered {
background-color: var(--modal-transfer-entry-hover);
}
$indicator_transform_time: .5s;
.indicator {
position: absolute;
left: 0;
right: 30%;
top: 0;
bottom: 0;
opacity: .4;
margin-right: 10px; /* for the gradient at the end */
.status {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
@include transition(all $indicator_transform_time ease-in-out);
}
&:after {
content: ' ';
position: absolute;
top: 0;
right: 0;
bottom: 0;
height: 100%;
width: 10px;
background-image: linear-gradient(to right, var(--modal-transfer-filelist), var(--modal-transfer-filelist));
@include transition(all $indicator_transform_time ease-in-out);
}
@include transition(all $indicator_transform_time ease-in-out);
@mixin define-indicator($color, $colorLight) {
background-color: $color;
.status {
background-color: $colorLight;
-webkit-box-shadow: 0 0 12px 3px $colorLight;
-moz-box-shadow: 0 0 12px 3px $colorLight;
box-shadow: 0 0 12px 3px $colorLight;
}
&:after {
background-image: linear-gradient(to right, $color, var(--modal-transfer-filelist));
}
}
&.red {
@include define-indicator(var(--modal-transfer-indicator-red), var(--modal-transfer-indicator-red-end));
}
&.blue {
@include define-indicator(var(--modal-transfer-indicator-blue), var(--modal-transfer-indicator-blue-end));
}
&.green {
@include define-indicator(var(--modal-transfer-indicator-green), var(--modal-transfer-indicator-green-end));
}
&.hidden {
@include define-indicator(var(--modal-transfer-indicator-hidden), var(--modal-transfer-indicator-hidden-end));
}
}
}
}
.columnSize {
text-align: end;
a {
margin-right: 1em;
}
}
.columnType {
text-align: center;
}
}

View File

@ -1,5 +1,5 @@
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
import {useEffect, useRef, useState} from "react"; import {useContext, useEffect, useRef, useState} from "react";
import {FileType} from "tc-shared/file/FileManager"; import {FileType} from "tc-shared/file/FileManager";
import * as ppt from "tc-backend/ppt"; import * as ppt from "tc-backend/ppt";
import {SpecialKey} from "tc-shared/PPTListener"; import {SpecialKey} from "tc-shared/PPTListener";
@ -12,21 +12,42 @@ import {Translatable} from "tc-shared/ui/react-elements/i18n";
import * as Moment from "moment"; import * as Moment from "moment";
import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu"; import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
import {BoxedInputField} from "tc-shared/ui/react-elements/InputField"; import {BoxedInputField} from "tc-shared/ui/react-elements/InputField";
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import React = require("react");
import { import {
FileBrowserEvents, FileBrowserEvents,
FileTransferUrlMediaType, FileTransferUrlMediaType,
ListedFileInfo, ListedFileInfo,
TransferStatus TransferStatus
} from "tc-shared/ui/modal/transfer/ModalFileTransfer"; } from "tc-shared/ui/modal/transfer/FileDefinitions";
import * as log from "tc-shared/log"; import {joinClassList} from "tc-shared/ui/react-elements/Helper";
import {LogCategory} from "tc-shared/log";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import React = require("react");
const cssStyle = require("./ModalFileTransfer.scss"); export interface FileBrowserRendererClasses {
navigation?: {
boxedInput?: string
},
fileTable?: {
table?: string,
header?: string,
body?: string
},
fileEntry?: {
entry?: string,
selected?: string,
dropHovered?: string
}
}
const EventsContext = React.createContext<Registry<FileBrowserEvents>>(undefined);
const CustomClassContext = React.createContext<FileBrowserRendererClasses>(undefined);
export const FileBrowserClassContext = CustomClassContext;
const cssStyle = require("./FileBrowserRenderer.scss");
interface NavigationBarProperties { interface NavigationBarProperties {
currentPath: string; initialPath: string;
events: Registry<FileBrowserEvents>; events: Registry<FileBrowserEvents>;
} }
@ -36,9 +57,11 @@ interface NavigationBarState {
state: "editing" | "navigating" | "normal"; state: "editing" | "navigating" | "normal";
} }
const ArrowRight = () => <div className={cssStyle.arrow}> const ArrowRight = () => (
<div className={cssStyle.inner}/> <div className={cssStyle.arrow}>
</div>; <div className={cssStyle.inner}/>
</div>
);
const NavigationEntry = (props: { events: Registry<FileBrowserEvents>, path: string, name: string }) => { const NavigationEntry = (props: { events: Registry<FileBrowserEvents>, path: string, name: string }) => {
const [dragHovered, setDragHovered] = useState(false); const [dragHovered, setDragHovered] = useState(false);
@ -56,11 +79,15 @@ const NavigationEntry = (props: { events: Registry<FileBrowserEvents>, path: str
<a <a
className={(props.name.length > 9 ? cssStyle.pathShrink : "") + " " + (dragHovered ? cssStyle.hovered : "")} className={(props.name.length > 9 ? cssStyle.pathShrink : "") + " " + (dragHovered ? cssStyle.hovered : "")}
title={props.name} title={props.name}
onClick={() => props.events.fire("action_navigate_to", {path: props.path})} onClick={event => {
event.preventDefault();
props.events.fire("action_navigate_to", {path: props.path});
}}
onDragOver={event => { onDragOver={event => {
const types = event.dataTransfer.types; const types = event.dataTransfer.types;
if (types.length !== 1) if (types.length !== 1) {
return; return;
}
if (types[0] === FileTransferUrlMediaType) { if (types[0] === FileTransferUrlMediaType) {
/* TODO: Detect if its remote move or internal move */ /* TODO: Detect if its remote move or internal move */
@ -77,8 +104,9 @@ const NavigationEntry = (props: { events: Registry<FileBrowserEvents>, path: str
onDragLeave={() => setDragHovered(false)} onDragLeave={() => setDragHovered(false)}
onDrop={event => { onDrop={event => {
const types = event.dataTransfer.types; const types = event.dataTransfer.types;
if (types.length !== 1) if (types.length !== 1) {
return; return;
}
/* TODO: Fix this code duplicate! */ /* TODO: Fix this code duplicate! */
if (types[0] === FileTransferUrlMediaType) { if (types[0] === FileTransferUrlMediaType) {
@ -121,7 +149,7 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
protected defaultState(): NavigationBarState { protected defaultState(): NavigationBarState {
return { return {
currentPath: this.props.currentPath, currentPath: this.props.initialPath,
state: "normal", state: "normal",
} }
} }
@ -134,67 +162,81 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
if (this.state.state === "editing") { if (this.state.state === "editing") {
input = ( input = (
<BoxedInputField key={"nav-editing"} <CustomClassContext.Consumer>
ref={this.refInput} {customClasses => (
defaultValue={path} <BoxedInputField key={"nav-editing"}
leftIcon={() => ref={this.refInput}
<div key={"left-icon"} defaultValue={path}
className={cssStyle.directoryIcon + " " + cssStyle.containerIcon}> leftIcon={() =>
<div className={"icon_em client-file_home"}/> <div key={"left-icon"}
</div> className={cssStyle.directoryIcon + " " + cssStyle.containerIcon}>
} <div className={"icon_em client-file_home"}/>
</div>
}
rightIcon={() => rightIcon={() =>
<div key={"right-icon"} <div key={"right-icon"}
className={cssStyle.refreshIcon + " " + cssStyle.containerIcon}> className={cssStyle.refreshIcon + " " + cssStyle.containerIcon}>
<div className={"icon_em client-refresh"}/> <div className={"icon_em client-refresh"}/>
</div> </div>
} }
onChange={path => this.onPathEntered(path)} onChange={path => this.onPathEntered(path)}
onBlur={() => this.onInputPathBluer()} onBlur={() => this.onInputPathBluer()}
/> className={customClasses?.navigation?.boxedInput}
/>
)}
</CustomClassContext.Consumer>
); );
} else if (this.state.state === "navigating" || this.state.state === "normal") { } else if (this.state.state === "navigating" || this.state.state === "normal") {
input = ( input = (
<BoxedInputField key={"nav-rendered"} <CustomClassContext.Consumer>
ref={this.refInput} {customClasses => (
leftIcon={() => <BoxedInputField key={"nav-rendered"}
<div key={"left-icon"} ref={this.refInput}
className={cssStyle.directoryIcon + " " + cssStyle.containerIcon} leftIcon={() =>
onClick={event => this.onPathClicked(event, -1)}> <div key={"left-icon"}
<div className={"icon_em client-file_home"}/> className={cssStyle.directoryIcon + " " + cssStyle.containerIcon}
</div> onClick={event => this.onPathClicked(event, -1)}>
} <div className={"icon_em client-file_home"}/>
</div>
}
rightIcon={() => rightIcon={() =>
<div <div
key={"right-icon"} key={"right-icon"}
className={cssStyle.refreshIcon + " " + (this.state.state === "normal" ? cssStyle.enabled : "") + " " + cssStyle.containerIcon} className={cssStyle.refreshIcon + " " + (this.state.state === "normal" ? cssStyle.enabled : "") + " " + cssStyle.containerIcon}
onClick={() => this.onButtonRefreshClicked()}> onClick={() => this.onButtonRefreshClicked()}>
<div className={"icon_em client-refresh"}/> <div className={"icon_em client-refresh"}/>
</div> </div>
} }
inputBox={() => inputBox={() =>
<div key={"custom-input"} className={cssStyle.containerPath} <div key={"custom-input"} className={cssStyle.containerPath}
ref={this.refRendered}> ref={this.refRendered}>
{this.state.currentPath.split("/").filter(e => !!e).map((e, index, arr) => [ {this.state.currentPath.split("/").filter(e => !!e).map((e, index, arr) => [
<ArrowRight key={"arrow-right-" + index + "-" + e}/>, <ArrowRight key={"arrow-right-" + index + "-" + e}/>,
<NavigationEntry key={"de-" + index + "-" + e} <NavigationEntry key={"de-" + index + "-" + e}
path={"/" + arr.slice(0, index + 1).join("/") + "/"} path={"/" + arr.slice(0, index + 1).join("/") + "/"}
name={e} events={this.props.events}/> name={e} events={this.props.events}/>
])} ])}
</div> </div>
} }
editable={this.state.state === "normal"} editable={this.state.state === "normal"}
onFocus={() => this.onRenderedPathClicked()} onFocus={event => !event.defaultPrevented && this.onRenderedPathClicked()}
/> className={customClasses?.navigation?.boxedInput}
/>
)}
</CustomClassContext.Consumer>
); );
} }
return <div className={cssStyle.navigation}>{input}</div>; return (
<div className={cssStyle.navigation}>
{input}
</div>
);
} }
componentDidUpdate(prevProps: Readonly<NavigationBarProperties>, prevState: Readonly<NavigationBarState>, snapshot?: any): void { componentDidUpdate(prevProps: Readonly<NavigationBarProperties>, prevState: Readonly<NavigationBarState>, snapshot?: any): void {
@ -216,8 +258,9 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
} }
private onRenderedPathClicked() { private onRenderedPathClicked() {
if (this.state.state !== "normal") if (this.state.state !== "normal") {
return; return;
}
this.setState({ this.setState({
state: "editing" state: "editing"
@ -234,8 +277,9 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
} }
private onPathEntered(newPath: string) { private onPathEntered(newPath: string) {
if (newPath === this.state.currentPath) if (newPath === this.state.currentPath) {
return; return;
}
this.ignoreBlur = true; this.ignoreBlur = true;
this.props.events.fire("action_navigate_to", {path: newPath}); this.props.events.fire("action_navigate_to", {path: newPath});
@ -258,10 +302,11 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
}, () => this.ignoreBlur = false); }, () => this.ignoreBlur = false);
} }
@EventHandler<FileBrowserEvents>("action_navigate_to_result") @EventHandler<FileBrowserEvents>("notify_current_path")
private handleNavigateResult(event: FileBrowserEvents["action_navigate_to_result"]) { private handleCurrentPath(event: FileBrowserEvents["notify_current_path"]) {
if (event.status === "success") if (event.status === "success") {
this.lastSucceededPath = event.path; this.lastSucceededPath = event.path;
}
this.setState({ this.setState({
state: "normal", state: "normal",
@ -279,7 +324,7 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
} }
interface FileListTableProperties { interface FileListTableProperties {
currentPath: string; initialPath: string;
events: Registry<FileBrowserEvents>; events: Registry<FileBrowserEvents>;
} }
@ -288,7 +333,8 @@ interface FileListTableState {
errorMessage?: string; errorMessage?: string;
} }
const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, file: ListedFileInfo }) => { const FileName = (props: { path: string, file: ListedFileInfo }) => {
const events = useContext(EventsContext);
const [editing, setEditing] = useState(props.file.mode === "create"); const [editing, setEditing] = useState(props.file.mode === "create");
const [fileName, setFileName] = useState(props.file.name); const [fileName, setFileName] = useState(props.file.name);
const refInput = useRef<HTMLInputElement>(); const refInput = useRef<HTMLInputElement>();
@ -333,7 +379,7 @@ const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, fi
if (props.file.mode === "create") { if (props.file.mode === "create") {
name = name || props.file.name; name = name || props.file.name;
props.events.fire("action_create_directory", { events.fire("action_create_directory", {
path: props.path, path: props.path,
name: name name: name
}); });
@ -342,7 +388,7 @@ const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, fi
props.file.mode = "creating"; props.file.mode = "creating";
} else { } else {
if (name.length > 0 && name !== props.file.name) { if (name.length > 0 && name !== props.file.name) {
props.events.fire("action_rename_file", { events.fire("action_rename_file", {
oldName: props.file.name, oldName: props.file.name,
newName: name, newName: name,
oldPath: props.path, oldPath: props.path,
@ -381,19 +427,19 @@ const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, fi
return; return;
event.stopPropagation(); event.stopPropagation();
props.events.fire("action_select_files", { events.fire("action_select_files", {
mode: "exclusive", mode: "exclusive",
files: [{name: props.file.name, type: props.file.type}] files: [{name: props.file.name, type: props.file.type}]
}); });
props.events.fire("action_start_rename", { events.fire("action_start_rename", {
path: props.path, path: props.path,
name: props.file.name name: props.file.name
}); });
}}>{fileName}</a>; }}>{fileName}</a>;
} }
props.events.reactUse("action_start_rename", event => setEditing(event.name === props.file.name && event.path === props.path)); events.reactUse("action_start_rename", event => setEditing(event.name === props.file.name && event.path === props.path));
props.events.reactUse("action_rename_file_result", event => { events.reactUse("action_rename_file_result", event => {
if (event.oldPath !== props.path || event.oldName !== props.file.name) if (event.oldPath !== props.path || event.oldName !== props.file.name)
return; return;
@ -418,10 +464,11 @@ const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, fi
return <>{icon} {name}</>; return <>{icon} {name}</>;
}; };
const FileSize = (props: { path: string, events: Registry<FileBrowserEvents>, file: ListedFileInfo }) => { const FileSize = (props: { path: string, file: ListedFileInfo }) => {
const events = useContext(EventsContext);
const [size, setSize] = useState(-1); const [size, setSize] = useState(-1);
props.events.reactUse("notify_transfer_status", event => { events.reactUse("notify_transfer_status", event => {
if (event.id !== props.file.transfer?.id) if (event.id !== props.file.transfer?.id)
return; return;
@ -440,7 +487,7 @@ const FileSize = (props: { path: string, events: Registry<FileBrowserEvents>, fi
} }
}); });
props.events.reactUse("notify_transfer_progress", event => { events.reactUse("notify_transfer_progress", event => {
if (event.id !== props.file.transfer?.id) if (event.id !== props.file.transfer?.id)
return; return;
@ -450,13 +497,23 @@ const FileSize = (props: { path: string, events: Registry<FileBrowserEvents>, fi
setSize(event.fileSize); setSize(event.fileSize);
}); });
if (size < 0 && (props.file.size < 0 || typeof props.file.size === "undefined")) if (size < 0 && (props.file.size < 0 || typeof props.file.size === "undefined")) {
return <a key={"size-invalid"}><Translatable>unknown</Translatable></a>; return (
return <a key={"size"}>{network.format_bytes(size >= 0 ? size : props.file.size, { <a key={"size-invalid"}>
unit: "B", <Translatable>unknown</Translatable>
time: "", </a>
exact: false );
})}</a>; }
return (
<a key={"size"}>
{network.format_bytes(size >= 0 ? size : props.file.size, {
unit: "B",
time: "",
exact: false
})}
</a>
);
}; };
const FileTransferIndicator = (props: { file: ListedFileInfo, events: Registry<FileBrowserEvents> }) => { const FileTransferIndicator = (props: { file: ListedFileInfo, events: Registry<FileBrowserEvents> }) => {
@ -520,6 +577,7 @@ const FileTransferIndicator = (props: { file: ListedFileInfo, events: Registry<F
color = cssStyle.hidden; color = cssStyle.hidden;
break; break;
} }
return ( return (
<div className={cssStyle.indicator + " " + color} style={{right: ((1 - transferProgress) * 100) + "%"}}> <div className={cssStyle.indicator + " " + color} style={{right: ((1 - transferProgress) * 100) + "%"}}>
<div className={cssStyle.status}/> <div className={cssStyle.status}/>
@ -532,18 +590,21 @@ const FileListEntry = (props: { row: TableRow<ListedFileInfo>, columns: TableCol
const [hidden, setHidden] = useState(false); const [hidden, setHidden] = useState(false);
const [selected, setSelected] = useState(false); const [selected, setSelected] = useState(false);
const [dropHovered, setDropHovered] = useState(false); const [dropHovered, setDropHovered] = useState(false);
const customClasses = useContext(CustomClassContext);
const onDoubleClicked = () => { const onDoubleClicked = () => {
if (file.type === FileType.DIRECTORY) { if (file.type === FileType.DIRECTORY) {
if (file.mode === "creating" || file.mode === "create") if (file.mode === "creating" || file.mode === "create") {
return; return;
}
props.events.fire("action_navigate_to", { props.events.fire("action_navigate_to", {
path: file.path + file.name + "/" path: file.path + file.name + "/"
}); });
} else { } else {
if (file.mode === "uploading" || file.virtual) if (file.mode === "uploading" || file.virtual) {
return; return;
}
props.events.fire("action_start_download", { props.events.fire("action_start_download", {
files: [{ files: [{
@ -577,12 +638,19 @@ const FileListEntry = (props: { row: TableRow<ListedFileInfo>, columns: TableCol
}); });
}, !hidden); }, !hidden);
if (hidden) if (hidden) {
return null; return null;
}
const elementClassList = joinClassList(
cssStyle.directoryEntry, customClasses?.fileEntry?.entry,
selected && cssStyle.selected, selected && customClasses?.fileEntry?.selected,
dropHovered && cssStyle.hovered, dropHovered && customClasses?.fileEntry?.dropHovered
);
return ( return (
<TableRowElement <TableRowElement
className={cssStyle.directoryEntry + " " + (selected ? cssStyle.selected : "") + " " + (dropHovered ? cssStyle.hovered : "")} className={elementClassList}
rowData={props.row} rowData={props.row}
columns={props.columns} columns={props.columns}
onDoubleClick={onDoubleClicked} onDoubleClick={onDoubleClicked}
@ -655,8 +723,257 @@ const FileListEntry = (props: { row: TableRow<ListedFileInfo>, columns: TableCol
); );
}; };
type FileListState = {
state: "querying" | "invalid-password"
} | {
state: "no-permissions",
failedPermission: string
} | {
state: "error",
reason: string
} | {
state: "normal",
files: ListedFileInfo[]
};
function fileTableHeaderContextMenu(event: React.MouseEvent, table: Table | undefined) {
event.preventDefault();
if(!table) {
return;
}
spawn_context_menu(event.pageX, event.pageY, {
type: MenuEntryType.CHECKBOX,
name: tr("Size"),
checkbox_checked: table.state.hiddenColumns.findIndex(e => e === "size") === -1,
callback: () => {
table.state.hiddenColumns.toggle("size");
table.forceUpdate();
}
}, {
type: MenuEntryType.CHECKBOX,
name: tr("Type"),
checkbox_checked: table.state.hiddenColumns.findIndex(e => e === "type") === -1,
callback: () => {
table.state.hiddenColumns.toggle("type");
table.forceUpdate();
}
}, {
type: MenuEntryType.CHECKBOX,
name: tr("Last changed"),
checkbox_checked: table.state.hiddenColumns.findIndex(e => e === "change-date") === -1,
callback: () => {
table.state.hiddenColumns.toggle("change-date");
table.forceUpdate();
}
})
}
// const FileListRenderer = React.memo((props: { path: string }) => {
// const events = useContext(EventsContext);
// const customClasses = useContext(CustomClassContext);
//
// const refTable = useRef<Table>();
//
// const [ state, setState ] = useState<FileListState>(() => {
// events.fire("query_files", { path: props.path });
// return { state: "querying" };
// });
//
// events.reactUse("query_files", event => {
// if(event.path === props.path) {
// setState({ state: "querying" });
// }
// });
//
// events.reactUse("query_files_result", event => {
// if(event.path !== props.path) {
// return;
// }
//
// switch(event.status) {
// case "no-permissions":
// setState({ state: "no-permissions", failedPermission: event.error });
// break;
//
// case "error":
// setState({ state: "error", reason: event.error });
// break;
//
// case "success":
// setState({ state: "normal", files: event.files });
// break;
//
// case "invalid-password":
// setState({ state: "invalid-password" });
// break;
//
// case "timeout":
// setState({ state: "error", reason: tr("query timeout") });
// break;
//
// default:
// setState({ state: "error", reason: tra("invalid query result state {}", event.status) });
// break;
// }
// });
//
// let rows: TableRow[] = [];
// let overlay;
//
// switch (state.state) {
// case "querying":
// overlay = () => (
// <div key={"loading"} className={cssStyle.overlay}>
// <a><Translatable>loading</Translatable><LoadingDots maxDots={3}/></a>
// </div>
// );
// break;
//
// case "error":
// overlay = () => (
// <div key={"query-error"} className={cssStyle.overlay + " " + cssStyle.overlayError}>
// <a><Translatable>Failed to query directory:</Translatable><br/>{state.reason}</a>
// </div>
// );
// break;
//
// case "no-permissions":
// overlay = () => (
// <div key={"no-permissions"} className={cssStyle.overlay + " " + cssStyle.overlayError}>
// <a><Translatable>Directory query failed on permission</Translatable><br/>{state.failedPermission}
// </a>
// </div>
// );
// break;
//
// case "invalid-password":
// /* TODO: Allow the user to enter a password */
// overlay = () => (
// <div key={"invalid-password"} className={cssStyle.overlay + " " + cssStyle.overlayError}>
// <a><Translatable>Directory query failed because it is password protected</Translatable></a>
// </div>
// );
// break;
//
// case "normal":
// if(state.files.length === 0) {
// overlay = () => (
// <div key={"no-files"} className={cssStyle.overlayEmptyFolder}>
// <a><Translatable>This folder is empty.</Translatable></a>
// </div>
// );
// } else {
// const directories = state.files.filter(e => e.type === FileType.DIRECTORY);
// const files = state.files.filter(e => e.type === FileType.FILE);
//
// for (const directory of directories.sort((a, b) => a.name > b.name ? 1 : -1)) {
// rows.push({
// columns: {
// "name": () => <FileName path={props.path} file={directory}/>,
// "type": () => <a key={"type"}><Translatable>Directory</Translatable></a>,
// "change-date": () => directory.datetime ?
// <a>{Moment(directory.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined
// },
// className: cssStyle.directoryEntry,
// userData: directory
// });
// }
//
// for (const file of files.sort((a, b) => a.name > b.name ? 1 : -1)) {
// rows.push({
// columns: {
// "name": () => <FileName path={props.path} file={file}/>,
// "size": () => <FileSize path={props.path} file={file}/>,
// "type": () => <a key={"type"}><Translatable>File</Translatable></a>,
// "change-date": () => file.datetime ?
// <a key={"date"}>{Moment(file.datetime).format("DD/MM/YYYY HH:mm")}</a> :
// undefined
// },
// className: cssStyle.directoryEntry,
// userData: file
// });
// }
// }
// break;
// }
//
// return (
// <Table
// ref={refTable}
// className={joinClassList(cssStyle.fileTable, customClasses?.fileTable?.table)}
// bodyClassName={joinClassList(cssStyle.body, customClasses?.fileTable?.body)}
// headerClassName={joinClassList(cssStyle.header, customClasses?.fileTable?.header)}
// columns={[
// {
// name: "name", header: () => [
// <a key={"name-name"}><Translatable>Name</Translatable></a>,
// <div key={"seperator-name"} className={cssStyle.separator}/>
// ], width: 80, className: cssStyle.columnName
// },
// {
// name: "type", header: () => [
// <a key={"name-type"}><Translatable>Type</Translatable></a>,
// <div key={"seperator-type"} className={cssStyle.separator}/>
// ], fixedWidth: "8em", className: cssStyle.columnType
// },
// {
// name: "size", header: () => [
// <a key={"name-size"}><Translatable>Size</Translatable></a>,
// <div key={"seperator-size"} className={cssStyle.separator}/>
// ], fixedWidth: "8em", className: cssStyle.columnSize
// },
// {
// name: "change-date", header: () => [
// <a key={"name-date"}><Translatable>Last changed</Translatable></a>,
// <div key={"seperator-date"} className={cssStyle.separator}/>
// ], fixedWidth: "8em", className: cssStyle.columnChanged
// },
// ]}
// rows={rows}
//
// bodyOverlayOnly={rows.length === 0}
// bodyOverlay={overlay}
//
// hiddenColumns={["type"]}
//
// onHeaderContextMenu={e => fileTableHeaderContextMenu(e, refTable.current)}
// onBodyContextMenu={event => {
// event.preventDefault();
// events.fire("action_select_files", { mode: "exclusive", files: [] });
// events.fire("action_selection_context_menu", { pageY: event.pageY, pageX: event.pageX });
// }}
// onDrop={e => this.onDrop(e)}
// onDragOver={event => {
// const types = event.dataTransfer.types;
// if (types.length !== 1)
// return;
//
// if (types[0] === FileTransferUrlMediaType) {
// /* TODO: Detect if its remote move or internal move */
// event.dataTransfer.effectAllowed = "move";
// } else if (types[0] === "Files") {
// event.dataTransfer.effectAllowed = "copy";
// } else {
// return;
// }
//
// event.preventDefault();
// }}
//
// renderRow={(row: TableRow<ListedFileInfo>, columns, uniqueId) => (
// <FileListEntry columns={columns}
// row={row} key={uniqueId}
// events={this.props.events}/>
// )}
// />
// );
// });
@ReactEventHandler(e => e.props.events) @ReactEventHandler(e => e.props.events)
export class FileBrowser extends ReactComponentBase<FileListTableProperties, FileListTableState> { export class FileBrowserRenderer extends ReactComponentBase<FileListTableProperties, FileListTableState> {
private refTable = React.createRef<Table>(); private refTable = React.createRef<Table>();
private currentPath: string; private currentPath: string;
private fileList: ListedFileInfo[]; private fileList: ListedFileInfo[];
@ -724,8 +1041,7 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
for (const directory of directories.sort((a, b) => a.name > b.name ? 1 : -1)) { for (const directory of directories.sort((a, b) => a.name > b.name ? 1 : -1)) {
rows.push({ rows.push({
columns: { columns: {
"name": () => <FileName path={this.currentPath} events={this.props.events} "name": () => <FileName path={this.currentPath} file={directory}/>,
file={directory}/>,
"type": () => <a key={"type"}><Translatable>Directory</Translatable></a>, "type": () => <a key={"type"}><Translatable>Directory</Translatable></a>,
"change-date": () => directory.datetime ? "change-date": () => directory.datetime ?
<a>{Moment(directory.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined <a>{Moment(directory.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined
@ -738,8 +1054,8 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
for (const file of files.sort((a, b) => a.name > b.name ? 1 : -1)) { for (const file of files.sort((a, b) => a.name > b.name ? 1 : -1)) {
rows.push({ rows.push({
columns: { columns: {
"name": () => <FileName path={this.currentPath} events={this.props.events} file={file}/>, "name": () => <FileName path={this.currentPath} file={file}/>,
"size": () => <FileSize path={this.currentPath} events={this.props.events} file={file}/>, "size": () => <FileSize path={this.currentPath} file={file}/>,
"type": () => <a key={"type"}><Translatable>File</Translatable></a>, "type": () => <a key={"type"}><Translatable>File</Translatable></a>,
"change-date": () => file.datetime ? "change-date": () => file.datetime ?
<a key={"date"}>{Moment(file.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined <a key={"date"}>{Moment(file.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined
@ -752,74 +1068,84 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
} }
return ( return (
<Table <EventsContext.Provider value={this.props.events}>
ref={this.refTable} <CustomClassContext.Consumer>
className={cssStyle.fileTable} {classes => (
bodyClassName={cssStyle.body} <Table
headerClassName={cssStyle.header} ref={this.refTable}
columns={[ className={this.classList(cssStyle.fileTable, classes?.fileTable?.table)}
{ bodyClassName={this.classList(cssStyle.body, classes?.fileTable?.body)}
name: "name", header: () => [ headerClassName={this.classList(cssStyle.header, classes?.fileTable?.header)}
<a key={"name-name"}><Translatable>Name</Translatable></a>, columns={[
<div key={"seperator-name"} className={cssStyle.seperator}/> {
], width: 80, className: cssStyle.columnName name: "name", header: () => [
}, <a key={"name-name"}><Translatable>Name</Translatable></a>,
{ <div key={"seperator-name"} className={cssStyle.separator}/>
name: "type", header: () => [ ], width: 80, className: cssStyle.columnName
<a key={"name-type"}><Translatable>Type</Translatable></a>, },
<div key={"seperator-type"} className={cssStyle.seperator}/> {
], fixedWidth: "8em", className: cssStyle.columnType name: "type", header: () => [
}, <a key={"name-type"}><Translatable>Type</Translatable></a>,
{ <div key={"seperator-type"} className={cssStyle.separator}/>
name: "size", header: () => [ ], fixedWidth: "8em", className: cssStyle.columnType
<a key={"name-size"}><Translatable>Size</Translatable></a>, },
<div key={"seperator-size"} className={cssStyle.seperator}/> {
], fixedWidth: "8em", className: cssStyle.columnSize name: "size", header: () => [
}, <a key={"name-size"}><Translatable>Size</Translatable></a>,
{ <div key={"seperator-size"} className={cssStyle.separator}/>
name: "change-date", header: () => [ ], fixedWidth: "8em", className: cssStyle.columnSize
<a key={"name-date"}><Translatable>Last changed</Translatable></a>, },
<div key={"seperator-date"} className={cssStyle.seperator}/> {
], fixedWidth: "8em", className: cssStyle.columnChanged name: "change-date", header: () => [
}, <a key={"name-date"}><Translatable>Last changed</Translatable></a>,
]} <div key={"seperator-date"} className={cssStyle.separator}/>
rows={rows} ], fixedWidth: "8em", className: cssStyle.columnChanged
},
]}
rows={rows}
bodyOverlayOnly={overlayOnly} bodyOverlayOnly={overlayOnly}
bodyOverlay={overlay} bodyOverlay={overlay}
hiddenColumns={["type"]} hiddenColumns={["type"]}
onHeaderContextMenu={e => this.onHeaderContextMenu(e)} onHeaderContextMenu={e => this.onHeaderContextMenu(e)}
onBodyContextMenu={e => this.onBodyContextMenu(e)} onBodyContextMenu={e => this.onBodyContextMenu(e)}
onDrop={e => this.onDrop(e)} onDrop={e => this.onDrop(e)}
onDragOver={event => { onDragOver={event => {
const types = event.dataTransfer.types; const types = event.dataTransfer.types;
if (types.length !== 1) if (types.length !== 1)
return; return;
if (types[0] === FileTransferUrlMediaType) { if (types[0] === FileTransferUrlMediaType) {
/* TODO: Detect if its remote move or internal move */ /* TODO: Detect if its remote move or internal move */
event.dataTransfer.effectAllowed = "move"; event.dataTransfer.effectAllowed = "move";
} else if (types[0] === "Files") { } else if (types[0] === "Files") {
event.dataTransfer.effectAllowed = "copy"; event.dataTransfer.effectAllowed = "copy";
} else { } else {
return; return;
} }
event.preventDefault(); event.preventDefault();
}} }}
renderRow={(row: TableRow<ListedFileInfo>, columns, uniqueId) => <FileListEntry columns={columns} renderRow={(row: TableRow<ListedFileInfo>, columns, uniqueId) => (
row={row} key={uniqueId} <FileListEntry columns={columns}
events={this.props.events}/>} row={row} key={uniqueId}
/> events={this.props.events}/>
)}
/>
)}
</CustomClassContext.Consumer>
</EventsContext.Provider>
); );
} }
componentDidMount(): void { componentDidMount(): void {
this.selection = []; this.selection = [];
this.currentPath = this.props.currentPath; this.currentPath = this.props.initialPath;
this.props.events.fire("query_current_path", {});
this.props.events.fire("query_files", { this.props.events.fire("query_files", {
path: this.currentPath path: this.currentPath
}); });
@ -827,21 +1153,22 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
private onDrop(event: React.DragEvent) { private onDrop(event: React.DragEvent) {
const types = event.dataTransfer.types; const types = event.dataTransfer.types;
if (types.length !== 1) if (types.length !== 1) {
return; return;
}
event.stopPropagation(); event.stopPropagation();
let targetPath; let targetPath;
{ {
let currentTarget = event.target as HTMLElement; let currentTarget = event.target as HTMLElement;
while (currentTarget && !currentTarget.hasAttribute("x-drag-upload-path")) while (currentTarget && !currentTarget.hasAttribute("x-drag-upload-path")) {
currentTarget = currentTarget.parentElement; currentTarget = currentTarget.parentElement;
}
targetPath = currentTarget?.getAttribute("x-drag-upload-path") || this.currentPath; targetPath = currentTarget?.getAttribute("x-drag-upload-path") || this.currentPath;
console.log("Target: %o %s", currentTarget, targetPath);
} }
if (types[0] === FileTransferUrlMediaType) { if (types[0] === FileTransferUrlMediaType) {
/* TODO: If cross move upload! */ /* TODO: Test if we moved cross some boundaries */
console.error(event.dataTransfer.getData(FileTransferUrlMediaType)); console.error(event.dataTransfer.getData(FileTransferUrlMediaType));
const fileUrls = event.dataTransfer.getData(FileTransferUrlMediaType).split("&").map(e => decodeURIComponent(e)); const fileUrls = event.dataTransfer.getData(FileTransferUrlMediaType).split("&").map(e => decodeURIComponent(e));
for (const fileUrl of fileUrls) { for (const fileUrl of fileUrls) {
@ -903,13 +1230,14 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
private onBodyContextMenu(event: React.MouseEvent) { private onBodyContextMenu(event: React.MouseEvent) {
event.preventDefault(); event.preventDefault();
this.props.events.fire("action_select_files", {mode: "exclusive", files: []}); this.props.events.fire("action_select_files", {mode: "exclusive", files: []});
this.props.events.fire("action_selection_context_menu", {pageY: event.pageY, pageX: event.pageX}); this.props.events.fire("action_selection_context_menu", { pageY: event.pageY, pageX: event.pageX });
} }
@EventHandler<FileBrowserEvents>("action_navigate_to_result") @EventHandler<FileBrowserEvents>("notify_current_path")
private handleNavigationResult(event: FileBrowserEvents["action_navigate_to_result"]) { private handleNavigationResult(event: FileBrowserEvents["notify_current_path"]) {
if (event.status !== "success") if (event.status !== "success") {
return; return;
}
this.currentPath = event.path; this.currentPath = event.path;
this.selection = []; this.selection = [];
@ -983,8 +1311,9 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
@EventHandler<FileBrowserEvents>("action_start_create_directory") @EventHandler<FileBrowserEvents>("action_start_create_directory")
private handleActionFileCreateBegin(event: FileBrowserEvents["action_start_create_directory"]) { private handleActionFileCreateBegin(event: FileBrowserEvents["action_start_create_directory"]) {
let index = 0; let index = 0;
while (this.fileList.find(e => e.name === (event.defaultName + (index > 0 ? " (" + index + ")" : "")))) while (this.fileList.find(e => e.name === (event.defaultName + (index > 0 ? " (" + index + ")" : "")))) {
index++; index++;
}
const name = event.defaultName + (index > 0 ? " (" + index + ")" : ""); const name = event.defaultName + (index > 0 ? " (" + index + ")" : "");
this.fileList.push({ this.fileList.push({
@ -998,12 +1327,14 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
}); });
/* fire_async because our children have to render first in order to have the row selected! */ /* fire_async because our children have to render first in order to have the row selected! */
this.forceUpdate(() => this.props.events.fire_react("action_select_files", { this.forceUpdate(() => {
files: [{ this.props.events.fire_react("action_select_files", {
name: name, files: [{
type: FileType.DIRECTORY name: name,
}], mode: "exclusive" type: FileType.DIRECTORY
})); }], mode: "exclusive"
});
});
} }
@EventHandler<FileBrowserEvents>("action_create_directory_result") @EventHandler<FileBrowserEvents>("action_create_directory_result")
@ -1112,8 +1443,9 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
@EventHandler<FileBrowserEvents>("notify_transfer_status") @EventHandler<FileBrowserEvents>("notify_transfer_status")
private handleTransferStatus(event: FileBrowserEvents["notify_transfer_status"]) { private handleTransferStatus(event: FileBrowserEvents["notify_transfer_status"]) {
const index = this.fileList.findIndex(e => e.transfer?.id === event.id); const index = this.fileList.findIndex(e => e.transfer?.id === event.id);
if (index === -1) if (index === -1) {
return; return;
}
let element = this.fileList[index]; let element = this.fileList[index];
if (event.status === "errored") { if (event.status === "errored") {

View File

@ -0,0 +1,164 @@
import {FileType} from "tc-shared/file/FileManager";
import {ChannelEntry} from "tc-shared/tree/Channel";
export const channelPathPrefix = tr("Channel") + " ";
export const iconPathPrefix = tr("Icons");
export const avatarsPathPrefix = tr("Avatars");
export const FileTransferUrlMediaType = "application/x-teaspeak-ft-urls";
export type TransferStatus = "pending" | "transferring" | "finished" | "errored" | "none";
export type FileMode = "password" | "empty" | "create" | "creating" | "normal" | "uploading";
export type ListedFileInfo = {
path: string;
name: string;
type: FileType;
datetime: number;
size: number;
virtual: boolean;
mode: FileMode;
transfer?: {
id: number;
direction: "upload" | "download";
status: TransferStatus;
percent: number;
} | undefined
};
export type PathInfo = {
channelId: number;
channel: ChannelEntry;
path: string;
type: "icon" | "avatar" | "channel" | "root";
}
export interface FileBrowserEvents {
action_navigate_to: {
path: string
},
action_delete_file: {
files: {
path: string,
name: string
}[] | "selection";
mode: "force" | "ask";
},
action_delete_file_result: {
results: {
path: string,
name: string,
status: "success" | "timeout" | "error";
error?: string;
}[],
},
action_start_create_directory: {
defaultName: string
},
action_create_directory: {
path: string,
name: string
},
action_create_directory_result: {
path: string,
name: string,
status: "success" | "timeout" | "error";
error?: string;
},
action_rename_file: {
oldPath: string,
oldName: string,
newPath: string;
newName: string
},
action_rename_file_result: {
oldPath: string,
oldName: string,
status: "success" | "timeout" | "error" | "no-changes";
newPath?: string,
newName?: string,
error?: string;
},
action_start_rename: {
path: string;
name: string;
},
action_select_files: {
files: {
name: string,
type: FileType
}[]
mode: "exclusive" | "toggle"
},
action_selection_context_menu: {
pageX: number,
pageY: number
},
action_start_download: {
files: {
path: string,
name: string
}[]
},
action_start_upload: {
path: string;
mode: "files" | "browse";
files?: File[];
},
query_files: { path: string },
query_files_result: {
path: string,
status: "success" | "timeout" | "error" | "no-permissions" | "invalid-password",
error?: string,
files?: ListedFileInfo[]
},
query_current_path: {},
notify_current_path: {
path: string,
status: "success" | "timeout" | "error";
error?: string;
pathInfo?: PathInfo
},
notify_transfer_start: {
path: string;
name: string;
id: number;
mode: "upload" | "download";
},
notify_transfer_status: {
id: number;
status: TransferStatus;
fileSize?: number;
},
notify_transfer_progress: {
id: number;
progress: number;
fileSize: number;
status: TransferStatus
}
notify_drag_ended: {},
/* Attention: Only use in sync mode! */
notify_drag_started: {
event: DragEvent
}
notify_destroy: {},
}

View File

@ -1,7 +1,6 @@
import * as React from "react"; import * as React from "react";
import {useEffect, useRef, useState} from "react"; import {useEffect, useRef, useState} from "react";
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
import {TransferStatus} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {HTMLRenderer} from "tc-shared/ui/react-elements/HTMLRenderer"; import {HTMLRenderer} from "tc-shared/ui/react-elements/HTMLRenderer";
import {ProgressBar} from "tc-shared/ui/react-elements/ProgressBar"; import {ProgressBar} from "tc-shared/ui/react-elements/ProgressBar";
@ -11,60 +10,14 @@ import {format_time, network} from "tc-shared/ui/frames/chat";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox"; import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
import {Button} from "tc-shared/ui/react-elements/Button"; import {Button} from "tc-shared/ui/react-elements/Button";
import {TransferStatus} from "tc-shared/ui/modal/transfer/FileDefinitions";
import {TransferInfoData, TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions";
const cssStyle = require("./TransferInfo.scss"); const cssStyle = require("./FileTransferInfoRenderer.scss");
const iconArrow = require("./icon_double_arrow.svg"); const iconArrow = require("./icon_double_arrow.svg");
const iconTransferUpload = require("./icon_transfer_upload.svg"); const iconTransferUpload = require("./icon_transfer_upload.svg");
const iconTransferDownload = require("./icon_transfer_download.svg"); const iconTransferDownload = require("./icon_transfer_download.svg");
export interface TransferInfoEvents {
query_transfers: {},
query_transfer_result: {
status: "success" | "error" | "timeout";
error?: string;
transfers?: TransferInfoData[],
showFinished?: boolean
}
action_toggle_expansion: { visible: boolean },
action_toggle_finished_transfers: { visible: boolean },
action_remove_finished: {},
notify_transfer_registered: { transfer: TransferInfoData },
notify_transfer_status: {
id: number,
status: TransferStatus,
error?: string
},
notify_transfer_progress: {
id: number;
status: TransferStatus,
progress: TransferProgress
},
notify_modal_closed: {}
}
export interface TransferInfoData {
id: number;
direction: "upload" | "download";
status: TransferStatus;
name: string;
path: string;
progress: number;
error?: string;
timestampRegistered: number;
timestampBegin: number;
timestampEnd: number;
transferredBytes: number;
}
const ExpendState = (props: { extended: boolean, events: Registry<TransferInfoEvents> }) => { const ExpendState = (props: { extended: boolean, events: Registry<TransferInfoEvents> }) => {
const [expended, setExpended] = useState(props.extended); const [expended, setExpended] = useState(props.extended);
@ -495,7 +448,7 @@ const ExtendedInfo = (props: { events: Registry<TransferInfoEvents> }) => {
</div>; </div>;
}; };
export const TransferInfo = (props: { events: Registry<TransferInfoEvents> }) => ( export const FileTransferInfo = (props: { events: Registry<TransferInfoEvents> }) => (
<div className={cssStyle.container}> <div className={cssStyle.container}>
<ExtendedInfo events={props.events}/> <ExtendedInfo events={props.events}/>
<BottomBar events={props.events}/> <BottomBar events={props.events}/>

View File

@ -7,14 +7,14 @@ import {
TransferProgress, TransferProgress,
TransferProperties TransferProperties
} from "../../../file/Transfer"; } from "../../../file/Transfer";
import {Settings, settings} from "../../../settings";
import { import {
avatarsPathPrefix, avatarsPathPrefix,
channelPathPrefix, channelPathPrefix,
iconPathPrefix, iconPathPrefix,
TransferStatus TransferStatus
} from "../../../ui/modal/transfer/ModalFileTransfer"; } from "tc-shared/ui/modal/transfer/FileDefinitions";
import {Settings, settings} from "../../../settings"; import {TransferInfoData, TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions";
import {TransferInfoData, TransferInfoEvents} from "../../../ui/modal/transfer/TransferInfo";
export const initializeTransferInfoController = (connection: ConnectionHandler, events: Registry<TransferInfoEvents>) => { export const initializeTransferInfoController = (connection: ConnectionHandler, events: Registry<TransferInfoEvents>) => {
const generateTransferPath = (properties: TransferProperties) => { const generateTransferPath = (properties: TransferProperties) => {
@ -128,10 +128,10 @@ export const initializeTransferInfoController = (connection: ConnectionHandler,
events.fire("notify_transfer_registered", {transfer: generateTransferInfo(transfer)}); events.fire("notify_transfer_registered", {transfer: generateTransferInfo(transfer)});
const closeListener = () => unregisterEvents(); const closeListener = () => unregisterEvents();
events.on("notify_modal_closed", closeListener); events.on("notify_destroy", closeListener);
const unregisterEvents = () => { const unregisterEvents = () => {
events.off("notify_modal_closed", closeListener); events.off("notify_destroy", closeListener);
transfer.events.off("notify_progress", progressListener); transfer.events.off("notify_progress", progressListener);
}; };
}; };
@ -139,7 +139,7 @@ export const initializeTransferInfoController = (connection: ConnectionHandler,
const registeredListener = event => listenToTransfer(event.transfer); const registeredListener = event => listenToTransfer(event.transfer);
connection.fileManager.events.on("notify_transfer_registered", registeredListener); connection.fileManager.events.on("notify_transfer_registered", registeredListener);
events.on("notify_modal_closed", () => connection.fileManager.events.off("notify_transfer_registered", registeredListener)); events.on("notify_destroy", () => connection.fileManager.events.off("notify_transfer_registered", registeredListener));
connection.fileManager.registeredTransfers().forEach(transfer => listenToTransfer(transfer)); connection.fileManager.registeredTransfers().forEach(transfer => listenToTransfer(transfer));
} }

View File

@ -0,0 +1,50 @@
import {TransferStatus} from "tc-shared/ui/modal/transfer/FileDefinitions";
import {TransferProgress} from "tc-shared/file/Transfer";
export interface TransferInfoEvents {
query_transfers: {},
query_transfer_result: {
status: "success" | "error" | "timeout";
error?: string;
transfers?: TransferInfoData[],
showFinished?: boolean
}
action_toggle_expansion: { visible: boolean },
action_toggle_finished_transfers: { visible: boolean },
action_remove_finished: {},
notify_transfer_registered: { transfer: TransferInfoData },
notify_transfer_status: {
id: number,
status: TransferStatus,
error?: string
},
notify_transfer_progress: {
id: number;
status: TransferStatus,
progress: TransferProgress
},
notify_destroy: {}
}
export interface TransferInfoData {
id: number;
direction: "upload" | "download";
status: TransferStatus;
name: string;
path: string;
progress: number;
error?: string;
timestampRegistered: number;
timestampBegin: number;
timestampEnd: number;
transferredBytes: number;
}

View File

@ -1,384 +0,0 @@
@import "../../../../css/static/mixin";
@import "../../../../css/static/properties";
html:root {
--modal-transfer-refresh-hover: #ffffff0e;
--modal-transfer-path-hover: #E6E6E6;
--modal-transfer-error-overlay-text: #9e9494;
--modal-transfer-filelist: #28292b;
--modal-transfer-filelist-border: #161616;
--modal-transfer-entry-hover: #2c2d2f;
--modal-transfer-entry-selected: #1a1a1b;
--modal-transfer-indicator-red: #a10000;
--modal-transfer-indicator-red-end: #e60000;
--modal-transfer-indicator-blue: #005fa1;
--modal-transfer-indicator-blue-end: #007acc;
--modal-transfer-indicator-green: #389738;
--modal-transfer-indicator-green-end: #4ecc4e;
--modal-transfer-indicator-hidden: #28292b00;
--modal-transfer-indicator-hidden-end: #28292b00;
}
.container {
padding: 1em;
position: relative;
padding-bottom: 4em; /* for the transfer info */
display: flex;
flex-direction: column;
justify-content: stretch;
flex-shrink: 1;
flex-grow: 1;
width: 90em;
min-width: 10em;
max-width: 100%;
height: 55em;
max-height: 100%;
min-height: 10em;
.navigation {
flex-grow: 0;
flex-shrink: 0;
.icon {
margin: auto .25em;
padding: .2em;
display: flex;
flex-direction: column;
justify-content: center;
cursor: pointer;
> div {
padding: .1em;
}
}
.refreshIcon {
border-radius: 1px;
@include transition(background-color $button_hover_animation_time ease-in-out);
> div {
padding: .1em;
}
&.enabled {
cursor: pointer;
&:hover {
background-color: var(--modal-transfer-refresh-hover);
}
}
}
.directoryIcon {
margin-right: -.25em;
}
input {
margin-left: .5em;
}
.containerPath {
@include user-select(none);
display: flex;
flex-direction: row;
justify-content: flex-start;
overflow: hidden;
white-space: nowrap;
width: calc(100% - 1em); /* some space for the text editing */
a.pathShrink {
flex-shrink: 1;
min-width: 5em;
@include text-dotdotdot();
}
a {
cursor: pointer;
@include transition(color $button_hover_animation_time ease-in-out);
&:hover, &.hovered {
color: var(--modal-transfer-path-hover);
}
}
}
}
.fileTable {
min-height: 5em;
flex-grow: 1;
flex-shrink: 1;
margin-top: 1em;
border: 1px var(--modal-transfer-filelist-border) solid;
border-radius: 0.2em;
background-color: var(--modal-transfer-filelist);
.header {
z-index: 1;
padding-top: .2em;
padding-bottom: .2em;
background-color: var(--modal-transfer-filelist);
.columnName, .columnSize, .columnType, .columnChanged {
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
.seperator {
position: absolute;
right: .2em;
top: .2em;
bottom: .2em;
width: .1em;
background-color: var(--text);
}
}
.columnSize {
width: 8em;
text-align: end;
}
.columnName {
padding-left: .5em;
}
> div:last-of-type {
.seperator {
display: none;
}
}
}
.body {
@include user-select(none);
@include chat-scrollbar-vertical();
.columnName {
padding-left: .5em;
display: flex;
flex-direction: row;
justify-content: flex-start;
a, div, img {
align-self: center;
margin-right: .5em;
@include text-dotdotdot();
}
img, div {
flex-shrink: 0;
height: 1em;
width: 1em;
}
input {
height: 1.3em;
align-self: center;
flex-grow: 1;
margin-right: .5em;
border-style: inherit;
padding: .1em;
}
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
a {
text-align: center;
}
}
.overlayError {
a {
font-size: 1.2em;
color: var(--modal-transfer-error-overlay-text);
}
}
.overlayEmptyFolder {
align-self: center;
margin-top: 1em;
}
.directoryEntry {
cursor: pointer;
&:hover {
background-color: var(--modal-transfer-entry-hover);
}
&.selected {
background-color: var(--modal-transfer-entry-selected);
}
/* drag hovered overrides selected */
&.hovered {
background-color: var(--modal-transfer-entry-hover);
}
$indicator_transform_time: .5s;
.indicator {
position: absolute;
left: 0;
right: 30%;
top: 0;
bottom: 0;
opacity: .4;
margin-right: 10px; /* for the gradient at the end */
.status {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
@include transition(all $indicator_transform_time ease-in-out);
}
&:after {
content: ' ';
position: absolute;
top: 0;
right: 0;
bottom: 0;
height: 100%;
width: 10px;
background-image: linear-gradient(to right, var(--modal-transfer-filelist), var(--modal-transfer-filelist));
@include transition(all $indicator_transform_time ease-in-out);
}
@include transition(all $indicator_transform_time ease-in-out);
@mixin define-indicator($color, $colorLight) {
background-color: $color;
.status {
background-color: $colorLight;
-webkit-box-shadow: 0 0 12px 3px $colorLight;
-moz-box-shadow: 0 0 12px 3px $colorLight;
box-shadow: 0 0 12px 3px $colorLight;
}
&:after {
background-image: linear-gradient(to right, $color, var(--modal-transfer-filelist));
}
}
&.red {
@include define-indicator(var(--modal-transfer-indicator-red), var(--modal-transfer-indicator-red-end));
}
&.blue {
@include define-indicator(var(--modal-transfer-indicator-blue), var(--modal-transfer-indicator-blue-end));
}
&.green {
@include define-indicator(var(--modal-transfer-indicator-green), var(--modal-transfer-indicator-green-end));
}
&.hidden {
@include define-indicator(var(--modal-transfer-indicator-hidden), var(--modal-transfer-indicator-hidden-end));
}
}
}
}
.columnSize {
text-align: end;
a {
margin-right: 1em;
}
}
.columnType {
text-align: center;
}
}
.row {
}
}
.arrow {
width: 1em;
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: column;
justify-content: center;
.inner {
flex-grow: 0;
flex-shrink: 0;
align-self: center;
margin-left: -.09em;
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
display: inline-block;
border: solid var(--text);
border-width: 0 0.125em 0.125em 0;
padding: 0.15em;
height: 0.15em;
width: .15em;
}
}

View File

@ -1,180 +1,17 @@
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal"; import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
import * as React from "react"; import * as React from "react";
import {FileType} from "tc-shared/file/FileManager";
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import {FileBrowser, NavigationBar} from "tc-shared/ui/modal/transfer/FileBrowser"; import {FileBrowserRenderer, NavigationBar} from "./FileBrowserRenderer";
import {TransferInfo, TransferInfoEvents} from "tc-shared/ui/modal/transfer/TransferInfo"; import {FileTransferInfo} from "./FileTransferInfo";
import {initializeRemoteFileBrowserController} from "tc-shared/ui/modal/transfer/RemoteFileBrowserController"; import {initializeRemoteFileBrowserController} from "./FileBrowserControllerRemote";
import {ChannelEntry} from "tc-shared/tree/Channel"; import {initializeTransferInfoController} from "./FileTransferInfoController";
import {initializeTransferInfoController} from "tc-shared/ui/modal/transfer/TransferInfoController";
import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
import {server_connections} from "tc-shared/ConnectionManager"; import {server_connections} from "tc-shared/ConnectionManager";
import {channelPathPrefix, FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions";
import {TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions";
const cssStyle = require("./ModalFileTransfer.scss"); const cssStyle = require("./FileBrowserRenderer.scss");
export const channelPathPrefix = tr("Channel") + " ";
export const iconPathPrefix = tr("Icons");
export const avatarsPathPrefix = tr("Avatars");
export const FileTransferUrlMediaType = "application/x-teaspeak-ft-urls";
export type TransferStatus = "pending" | "transferring" | "finished" | "errored" | "none";
export type FileMode = "password" | "empty" | "create" | "creating" | "normal" | "uploading";
export type ListedFileInfo = {
path: string;
name: string;
type: FileType;
datetime: number;
size: number;
virtual: boolean;
mode: FileMode;
transfer?: {
id: number;
direction: "upload" | "download";
status: TransferStatus;
percent: number;
} | undefined
};
export type PathInfo = {
channelId: number;
channel: ChannelEntry;
path: string;
type: "icon" | "avatar" | "channel" | "root";
}
export interface FileBrowserEvents {
query_files: { path: string },
query_files_result: {
path: string,
status: "success" | "timeout" | "error" | "no-permissions" | "invalid-password",
error?: string,
files?: ListedFileInfo[]
},
action_navigate_to: {
path: string
},
action_navigate_to_result: {
path: string,
status: "success" | "timeout" | "error";
error?: string;
pathInfo?: PathInfo
}
action_delete_file: {
files: {
path: string,
name: string
}[] | "selection";
mode: "force" | "ask";
},
action_delete_file_result: {
results: {
path: string,
name: string,
status: "success" | "timeout" | "error";
error?: string;
}[],
},
action_start_create_directory: {
defaultName: string
},
action_create_directory: {
path: string,
name: string
},
action_create_directory_result: {
path: string,
name: string,
status: "success" | "timeout" | "error";
error?: string;
},
action_rename_file: {
oldPath: string,
oldName: string,
newPath: string;
newName: string
},
action_rename_file_result: {
oldPath: string,
oldName: string,
status: "success" | "timeout" | "error" | "no-changes";
newPath?: string,
newName?: string,
error?: string;
},
action_start_rename: {
path: string;
name: string;
},
action_select_files: {
files: {
name: string,
type: FileType
}[]
mode: "exclusive" | "toggle"
},
action_selection_context_menu: {
pageX: number,
pageY: number
},
action_start_download: {
files: {
path: string,
name: string
}[]
},
action_start_upload: {
path: string;
mode: "files" | "browse";
files?: File[];
},
notify_transfer_start: {
path: string;
name: string;
id: number;
mode: "upload" | "download";
},
notify_transfer_status: {
id: number;
status: TransferStatus;
fileSize?: number;
},
notify_transfer_progress: {
id: number;
progress: number;
fileSize: number;
status: TransferStatus
}
notify_modal_closed: {},
notify_drag_ended: {},
/* Attention: Only use in sync mode! */
notify_drag_started: {
event: DragEvent
}
}
class FileTransferModal extends InternalModal { class FileTransferModal extends InternalModal {
readonly remoteBrowseEvents = new Registry<FileBrowserEvents>(); readonly remoteBrowseEvents = new Registry<FileBrowserEvents>();
@ -196,12 +33,12 @@ class FileTransferModal extends InternalModal {
protected onInitialize() { protected onInitialize() {
const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/"; const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/";
this.remoteBrowseEvents.fire("action_navigate_to", {path: path}); this.remoteBrowseEvents.fire("action_navigate_to", { path: path });
} }
protected onDestroy() { protected onDestroy() {
this.remoteBrowseEvents.fire("notify_modal_closed"); this.remoteBrowseEvents.fire("notify_destroy");
this.transferInfoEvents.fire("notify_modal_closed"); this.transferInfoEvents.fire("notify_destroy");
} }
title() { title() {
@ -210,11 +47,13 @@ class FileTransferModal extends InternalModal {
renderBody() { renderBody() {
const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/"; const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/";
return <div className={cssStyle.container}> return (
<NavigationBar events={this.remoteBrowseEvents} currentPath={path}/> <div className={cssStyle.container}>
<FileBrowser events={this.remoteBrowseEvents} currentPath={path}/> <NavigationBar events={this.remoteBrowseEvents} initialPath={path} />
<TransferInfo events={this.transferInfoEvents}/> <FileBrowserRenderer events={this.remoteBrowseEvents} initialPath={path} />
</div> <FileTransferInfo events={this.transferInfoEvents} />
</div>
)
} }
} }

View File

@ -0,0 +1,23 @@
import * as React from "react";
interface ErrorBoundaryState {
errorOccurred: boolean
}
export class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> {
render() {
if(this.state.errorOccurred) {
} else {
return this.props.children;
}
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error("Did catch: %o - %o", error, errorInfo);
}
static getDerivedStateFromError() : Partial<ErrorBoundaryState> {
return { errorOccurred: true };
}
}

View File

@ -23,4 +23,8 @@ export function useDependentState<S>(
}, inputs); }, inputs);
return [state, setState]; return [state, setState];
}
export function joinClassList(...classes: any[]) : string {
return classes.filter(value => typeof value === "string" && value.length > 0).join(" ");
} }

View File

@ -26,7 +26,7 @@ export interface BoxedInputFieldProperties {
size?: "normal" | "large" | "small"; size?: "normal" | "large" | "small";
onFocus?: () => void; onFocus?: (event: React.FocusEvent | React.MouseEvent) => void;
onBlur?: () => void; onBlur?: () => void;
onChange?: (newValue: string) => void; onChange?: (newValue: string) => void;

View File

@ -137,8 +137,9 @@ export class Table extends React.Component<TableProperties, TableState> {
return rowRenderer(row, columns, "tr-" + row.__rowIndex); return rowRenderer(row, columns, "tr-" + row.__rowIndex);
}); });
if(this.props.bodyOverlay) if(this.props.bodyOverlay) {
body.push(this.props.bodyOverlay()); body.push(this.props.bodyOverlay());
}
} }
return ( return (