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.events = new Registry<ChannelEvents>();
this.subscribed = false;
this.properties = new ChannelProperties();
this.channelId = channelId;
this.properties.channel_name = channelName;
@ -712,6 +713,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
cached_password() { return this.cachedPasswordHash; }
async updateSubscribeMode() {
console.error("Update subscribe mode");
let shouldBeSubscribed = false;
switch (this.subscriptionMode) {
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 {LogCategory, logWarn} from "tc-shared/log";
import React = require("react");
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
const cssStyle = require("./SideBarRenderer.scss");
@ -52,6 +53,7 @@ const ContentRendererClientInfo = () => {
const contentData = useContentData("client-info");
if(!contentData) { return null; }
throw "XX";
return (
<ClientInfoRenderer
events={contentData.events}
@ -62,13 +64,25 @@ const ContentRendererClientInfo = () => {
const SideBarFrame = (props: { type: SideBarType }) => {
switch (props.type) {
case "channel":
return <ContentRendererChannel key={props.type} />;
return (
<ErrorBoundary key={props.type}>
<ContentRendererChannel />
</ErrorBoundary>
);
case "private-chat":
return <ContentRendererPrivateConversation key={props.type} />;
return (
<ErrorBoundary key={props.type}>
<ContentRendererPrivateConversation />
</ErrorBoundary>
);
case "client-info":
return <ContentRendererClientInfo key={props.type} />;
return (
<ErrorBoundary key={props.type}>
<ContentRendererClientInfo />
</ErrorBoundary>
);
case "music-manage":
/* 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 {ChannelDescriptionController} from "tc-shared/ui/frames/side/ChannelDescriptionController";
import {LocalClientEntry} from "tc-shared/tree/Client";
import {ChannelFileBrowserController} from "tc-shared/ui/frames/side/ChannelFileBrowserController";
export class ChannelBarController {
readonly uiEvents: Registry<ChannelBarUiEvents>;
private channelConversations: ChannelConversationController;
private description: ChannelDescriptionController;
private fileBrowser: ChannelFileBrowserController;
private currentConnection: ConnectionHandler;
private listenerConnection: (() => void)[];
@ -25,6 +27,7 @@ export class ChannelBarController {
this.channelConversations = new ChannelConversationController();
this.description = new ChannelDescriptionController();
this.fileBrowser = new ChannelFileBrowserController();
this.uiEvents.on("query_mode", () => this.notifyChannelMode());
this.uiEvents.on("query_channel_id", () => this.notifyChannelId());
@ -41,6 +44,9 @@ export class ChannelBarController {
this.currentChannel = undefined;
this.currentConnection = undefined;
this.fileBrowser?.destroy();
this.fileBrowser = undefined;
this.channelConversations?.destroy();
this.channelConversations = undefined;
@ -56,6 +62,7 @@ export class ChannelBarController {
}
this.channelConversations.setConnectionHandler(handler);
this.fileBrowser.setConnectionHandler(handler);
this.listenerConnection.forEach(callback => callback());
this.listenerConnection = [];
@ -96,6 +103,7 @@ export class ChannelBarController {
return;
}
this.fileBrowser.setChannel(channel);
this.description.setChannel(channel);
this.listenerChannel.forEach(callback => callback());
@ -185,9 +193,9 @@ export class ChannelBarController {
this.uiEvents.fire_react("notify_data", {
content: "file-transfer",
data: {
events: this.fileBrowser.uiEvents
}
});
/* TODO! */
break;
}
}

View File

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

View File

@ -5,6 +5,7 @@ import * as React from "react";
import {ConversationPanel} from "tc-shared/ui/frames/side/AbstractConversationRenderer";
import {useDependentState} from "tc-shared/ui/react-elements/Helper";
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 ChannelContext = React.createContext<{ channelId: number, handlerId: string }>(undefined);
@ -37,8 +38,7 @@ const ModeRenderer = () => {
return <ModeRendererDescription key={"description"} />;
case "file-transfer":
/* TODO! */
return null;
return <ModeRendererFileTransfer key={"file"} />;
case "none":
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> }) => {
const [ channelContext, setChannelContext ] = useState<{ channelId: number, handlerId: string }>(() => {
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
} from "../../../file/Transfer";
import {createErrorModal} from "../../../ui/elements/Modal";
import {ErrorCode} from "../../../connection/ErrorCode";
import {
avatarsPathPrefix,
channelPathPrefix,
FileBrowserEvents,
iconPathPrefix,
ListedFileInfo,
channelPathPrefix, FileBrowserEvents,
iconPathPrefix, ListedFileInfo,
PathInfo
} from "../../../ui/modal/transfer/ModalFileTransfer";
import {ErrorCode} from "../../../connection/ErrorCode";
} from "tc-shared/ui/modal/transfer/FileDefinitions";
function parsePath(path: string, connection: ConnectionHandler): PathInfo {
if (path === "/" || !path) {
@ -46,7 +44,7 @@ function parsePath(path: string, connection: ConnectionHandler): PathInfo {
const channel = connection.channelTree.findChannel(channelId);
if (!channel) {
throw tr("Channel not visible anymore");
throw tr("Invalid channel id");
}
return {
@ -79,13 +77,13 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
try {
const info = parsePath(event.path, connection);
events.fire_react("action_navigate_to_result", {
events.fire_react("notify_current_path", {
path: event.path || "/",
status: "success",
pathInfo: info
});
} catch (error) {
events.fire_react("action_navigate_to_result", {
events.fire_react("notify_current_path", {
path: event.path,
status: "error",
error: error
@ -284,8 +282,9 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
let sourcePath: PathInfo, targetPath: PathInfo;
try {
sourcePath = parsePath(event.oldPath, connection);
if (sourcePath.type !== "channel")
if (sourcePath.type !== "channel") {
throw tr("Icon/avatars could not be renamed");
}
} catch (error) {
events.fire_react("action_rename_file_result", {
oldPath: event.oldPath,
@ -297,8 +296,9 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
}
try {
targetPath = parsePath(event.newPath, connection);
if (sourcePath.type !== "channel")
if (sourcePath.type !== "channel") {
throw tr("Target path isn't a channel");
}
} catch (error) {
events.fire_react("action_rename_file_result", {
oldPath: event.oldPath,
@ -374,15 +374,22 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
let currentPath = "/";
let currentPathInfo: PathInfo;
let selection: { name: string, type: FileType }[] = [];
events.on("action_navigate_to_result", result => {
if (result.status !== "success")
events.on("notify_current_path", result => {
if (result.status !== "success") {
return;
}
currentPathInfo = result.pathInfo;
currentPath = result.path;
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 => {
if (result.status !== "success")
return;
@ -812,10 +819,10 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
});
const closeListener = () => unregisterEvents();
events.on("notify_modal_closed", closeListener);
events.on("notify_destroy", closeListener);
const unregisterEvents = () => {
events.off("notify_modal_closed", closeListener);
events.off("notify_destroy", closeListener);
transfer.events.off("notify_progress", progressListener);
};
};
@ -823,7 +830,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
const registeredListener = event => listenToTransfer(event.transfer);
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));
}

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 {useEffect, useRef, useState} from "react";
import {useContext, useEffect, useRef, useState} from "react";
import {FileType} from "tc-shared/file/FileManager";
import * as ppt from "tc-backend/ppt";
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 {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
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 {
FileBrowserEvents,
FileTransferUrlMediaType,
ListedFileInfo,
TransferStatus
} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
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");
} from "tc-shared/ui/modal/transfer/FileDefinitions";
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
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 {
currentPath: string;
initialPath: string;
events: Registry<FileBrowserEvents>;
}
@ -36,9 +57,11 @@ interface NavigationBarState {
state: "editing" | "navigating" | "normal";
}
const ArrowRight = () => <div className={cssStyle.arrow}>
<div className={cssStyle.inner}/>
</div>;
const ArrowRight = () => (
<div className={cssStyle.arrow}>
<div className={cssStyle.inner}/>
</div>
);
const NavigationEntry = (props: { events: Registry<FileBrowserEvents>, path: string, name: string }) => {
const [dragHovered, setDragHovered] = useState(false);
@ -56,11 +79,15 @@ const NavigationEntry = (props: { events: Registry<FileBrowserEvents>, path: str
<a
className={(props.name.length > 9 ? cssStyle.pathShrink : "") + " " + (dragHovered ? cssStyle.hovered : "")}
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 => {
const types = event.dataTransfer.types;
if (types.length !== 1)
if (types.length !== 1) {
return;
}
if (types[0] === FileTransferUrlMediaType) {
/* TODO: Detect if its remote move or internal move */
@ -77,8 +104,9 @@ const NavigationEntry = (props: { events: Registry<FileBrowserEvents>, path: str
onDragLeave={() => setDragHovered(false)}
onDrop={event => {
const types = event.dataTransfer.types;
if (types.length !== 1)
if (types.length !== 1) {
return;
}
/* TODO: Fix this code duplicate! */
if (types[0] === FileTransferUrlMediaType) {
@ -121,7 +149,7 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
protected defaultState(): NavigationBarState {
return {
currentPath: this.props.currentPath,
currentPath: this.props.initialPath,
state: "normal",
}
}
@ -134,67 +162,81 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
if (this.state.state === "editing") {
input = (
<BoxedInputField key={"nav-editing"}
ref={this.refInput}
defaultValue={path}
leftIcon={() =>
<div key={"left-icon"}
className={cssStyle.directoryIcon + " " + cssStyle.containerIcon}>
<div className={"icon_em client-file_home"}/>
</div>
}
<CustomClassContext.Consumer>
{customClasses => (
<BoxedInputField key={"nav-editing"}
ref={this.refInput}
defaultValue={path}
leftIcon={() =>
<div key={"left-icon"}
className={cssStyle.directoryIcon + " " + cssStyle.containerIcon}>
<div className={"icon_em client-file_home"}/>
</div>
}
rightIcon={() =>
<div key={"right-icon"}
className={cssStyle.refreshIcon + " " + cssStyle.containerIcon}>
<div className={"icon_em client-refresh"}/>
</div>
}
rightIcon={() =>
<div key={"right-icon"}
className={cssStyle.refreshIcon + " " + cssStyle.containerIcon}>
<div className={"icon_em client-refresh"}/>
</div>
}
onChange={path => this.onPathEntered(path)}
onBlur={() => this.onInputPathBluer()}
/>
onChange={path => this.onPathEntered(path)}
onBlur={() => this.onInputPathBluer()}
className={customClasses?.navigation?.boxedInput}
/>
)}
</CustomClassContext.Consumer>
);
} else if (this.state.state === "navigating" || this.state.state === "normal") {
input = (
<BoxedInputField key={"nav-rendered"}
ref={this.refInput}
leftIcon={() =>
<div key={"left-icon"}
className={cssStyle.directoryIcon + " " + cssStyle.containerIcon}
onClick={event => this.onPathClicked(event, -1)}>
<div className={"icon_em client-file_home"}/>
</div>
}
<CustomClassContext.Consumer>
{customClasses => (
<BoxedInputField key={"nav-rendered"}
ref={this.refInput}
leftIcon={() =>
<div key={"left-icon"}
className={cssStyle.directoryIcon + " " + cssStyle.containerIcon}
onClick={event => this.onPathClicked(event, -1)}>
<div className={"icon_em client-file_home"}/>
</div>
}
rightIcon={() =>
<div
key={"right-icon"}
className={cssStyle.refreshIcon + " " + (this.state.state === "normal" ? cssStyle.enabled : "") + " " + cssStyle.containerIcon}
onClick={() => this.onButtonRefreshClicked()}>
<div className={"icon_em client-refresh"}/>
</div>
}
rightIcon={() =>
<div
key={"right-icon"}
className={cssStyle.refreshIcon + " " + (this.state.state === "normal" ? cssStyle.enabled : "") + " " + cssStyle.containerIcon}
onClick={() => this.onButtonRefreshClicked()}>
<div className={"icon_em client-refresh"}/>
</div>
}
inputBox={() =>
<div key={"custom-input"} className={cssStyle.containerPath}
ref={this.refRendered}>
{this.state.currentPath.split("/").filter(e => !!e).map((e, index, arr) => [
<ArrowRight key={"arrow-right-" + index + "-" + e}/>,
<NavigationEntry key={"de-" + index + "-" + e}
path={"/" + arr.slice(0, index + 1).join("/") + "/"}
name={e} events={this.props.events}/>
])}
</div>
}
inputBox={() =>
<div key={"custom-input"} className={cssStyle.containerPath}
ref={this.refRendered}>
{this.state.currentPath.split("/").filter(e => !!e).map((e, index, arr) => [
<ArrowRight key={"arrow-right-" + index + "-" + e}/>,
<NavigationEntry key={"de-" + index + "-" + e}
path={"/" + arr.slice(0, index + 1).join("/") + "/"}
name={e} events={this.props.events}/>
])}
</div>
}
editable={this.state.state === "normal"}
onFocus={() => this.onRenderedPathClicked()}
/>
editable={this.state.state === "normal"}
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 {
@ -216,8 +258,9 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
}
private onRenderedPathClicked() {
if (this.state.state !== "normal")
if (this.state.state !== "normal") {
return;
}
this.setState({
state: "editing"
@ -234,8 +277,9 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
}
private onPathEntered(newPath: string) {
if (newPath === this.state.currentPath)
if (newPath === this.state.currentPath) {
return;
}
this.ignoreBlur = true;
this.props.events.fire("action_navigate_to", {path: newPath});
@ -258,10 +302,11 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
}, () => this.ignoreBlur = false);
}
@EventHandler<FileBrowserEvents>("action_navigate_to_result")
private handleNavigateResult(event: FileBrowserEvents["action_navigate_to_result"]) {
if (event.status === "success")
@EventHandler<FileBrowserEvents>("notify_current_path")
private handleCurrentPath(event: FileBrowserEvents["notify_current_path"]) {
if (event.status === "success") {
this.lastSucceededPath = event.path;
}
this.setState({
state: "normal",
@ -279,7 +324,7 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
}
interface FileListTableProperties {
currentPath: string;
initialPath: string;
events: Registry<FileBrowserEvents>;
}
@ -288,7 +333,8 @@ interface FileListTableState {
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 [fileName, setFileName] = useState(props.file.name);
const refInput = useRef<HTMLInputElement>();
@ -333,7 +379,7 @@ const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, fi
if (props.file.mode === "create") {
name = name || props.file.name;
props.events.fire("action_create_directory", {
events.fire("action_create_directory", {
path: props.path,
name: name
});
@ -342,7 +388,7 @@ const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, fi
props.file.mode = "creating";
} else {
if (name.length > 0 && name !== props.file.name) {
props.events.fire("action_rename_file", {
events.fire("action_rename_file", {
oldName: props.file.name,
newName: name,
oldPath: props.path,
@ -381,19 +427,19 @@ const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, fi
return;
event.stopPropagation();
props.events.fire("action_select_files", {
events.fire("action_select_files", {
mode: "exclusive",
files: [{name: props.file.name, type: props.file.type}]
});
props.events.fire("action_start_rename", {
events.fire("action_start_rename", {
path: props.path,
name: props.file.name
});
}}>{fileName}</a>;
}
props.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_start_rename", event => setEditing(event.name === props.file.name && event.path === props.path));
events.reactUse("action_rename_file_result", event => {
if (event.oldPath !== props.path || event.oldName !== props.file.name)
return;
@ -418,10 +464,11 @@ const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, fi
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);
props.events.reactUse("notify_transfer_status", event => {
events.reactUse("notify_transfer_status", event => {
if (event.id !== props.file.transfer?.id)
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)
return;
@ -450,13 +497,23 @@ const FileSize = (props: { path: string, events: Registry<FileBrowserEvents>, fi
setSize(event.fileSize);
});
if (size < 0 && (props.file.size < 0 || typeof props.file.size === "undefined"))
return <a key={"size-invalid"}><Translatable>unknown</Translatable></a>;
return <a key={"size"}>{network.format_bytes(size >= 0 ? size : props.file.size, {
unit: "B",
time: "",
exact: false
})}</a>;
if (size < 0 && (props.file.size < 0 || typeof props.file.size === "undefined")) {
return (
<a key={"size-invalid"}>
<Translatable>unknown</Translatable>
</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> }) => {
@ -520,6 +577,7 @@ const FileTransferIndicator = (props: { file: ListedFileInfo, events: Registry<F
color = cssStyle.hidden;
break;
}
return (
<div className={cssStyle.indicator + " " + color} style={{right: ((1 - transferProgress) * 100) + "%"}}>
<div className={cssStyle.status}/>
@ -532,18 +590,21 @@ const FileListEntry = (props: { row: TableRow<ListedFileInfo>, columns: TableCol
const [hidden, setHidden] = useState(false);
const [selected, setSelected] = useState(false);
const [dropHovered, setDropHovered] = useState(false);
const customClasses = useContext(CustomClassContext);
const onDoubleClicked = () => {
if (file.type === FileType.DIRECTORY) {
if (file.mode === "creating" || file.mode === "create")
if (file.mode === "creating" || file.mode === "create") {
return;
}
props.events.fire("action_navigate_to", {
path: file.path + file.name + "/"
});
} else {
if (file.mode === "uploading" || file.virtual)
if (file.mode === "uploading" || file.virtual) {
return;
}
props.events.fire("action_start_download", {
files: [{
@ -577,12 +638,19 @@ const FileListEntry = (props: { row: TableRow<ListedFileInfo>, columns: TableCol
});
}, !hidden);
if (hidden)
if (hidden) {
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 (
<TableRowElement
className={cssStyle.directoryEntry + " " + (selected ? cssStyle.selected : "") + " " + (dropHovered ? cssStyle.hovered : "")}
className={elementClassList}
rowData={props.row}
columns={props.columns}
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)
export class FileBrowser extends ReactComponentBase<FileListTableProperties, FileListTableState> {
export class FileBrowserRenderer extends ReactComponentBase<FileListTableProperties, FileListTableState> {
private refTable = React.createRef<Table>();
private currentPath: string;
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)) {
rows.push({
columns: {
"name": () => <FileName path={this.currentPath} events={this.props.events}
file={directory}/>,
"name": () => <FileName path={this.currentPath} 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
@ -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)) {
rows.push({
columns: {
"name": () => <FileName path={this.currentPath} events={this.props.events} file={file}/>,
"size": () => <FileSize path={this.currentPath} events={this.props.events} file={file}/>,
"name": () => <FileName path={this.currentPath} file={file}/>,
"size": () => <FileSize path={this.currentPath} 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
@ -752,74 +1068,84 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
}
return (
<Table
ref={this.refTable}
className={cssStyle.fileTable}
bodyClassName={cssStyle.body}
headerClassName={cssStyle.header}
columns={[
{
name: "name", header: () => [
<a key={"name-name"}><Translatable>Name</Translatable></a>,
<div key={"seperator-name"} className={cssStyle.seperator}/>
], width: 80, className: cssStyle.columnName
},
{
name: "type", header: () => [
<a key={"name-type"}><Translatable>Type</Translatable></a>,
<div key={"seperator-type"} className={cssStyle.seperator}/>
], fixedWidth: "8em", className: cssStyle.columnType
},
{
name: "size", header: () => [
<a key={"name-size"}><Translatable>Size</Translatable></a>,
<div key={"seperator-size"} className={cssStyle.seperator}/>
], fixedWidth: "8em", className: cssStyle.columnSize
},
{
name: "change-date", header: () => [
<a key={"name-date"}><Translatable>Last changed</Translatable></a>,
<div key={"seperator-date"} className={cssStyle.seperator}/>
], fixedWidth: "8em", className: cssStyle.columnChanged
},
]}
rows={rows}
<EventsContext.Provider value={this.props.events}>
<CustomClassContext.Consumer>
{classes => (
<Table
ref={this.refTable}
className={this.classList(cssStyle.fileTable, classes?.fileTable?.table)}
bodyClassName={this.classList(cssStyle.body, classes?.fileTable?.body)}
headerClassName={this.classList(cssStyle.header, classes?.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={overlayOnly}
bodyOverlay={overlay}
bodyOverlayOnly={overlayOnly}
bodyOverlay={overlay}
hiddenColumns={["type"]}
hiddenColumns={["type"]}
onHeaderContextMenu={e => this.onHeaderContextMenu(e)}
onBodyContextMenu={e => this.onBodyContextMenu(e)}
onDrop={e => this.onDrop(e)}
onDragOver={event => {
const types = event.dataTransfer.types;
if (types.length !== 1)
return;
onHeaderContextMenu={e => this.onHeaderContextMenu(e)}
onBodyContextMenu={e => this.onBodyContextMenu(e)}
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;
}
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();
}}
event.preventDefault();
}}
renderRow={(row: TableRow<ListedFileInfo>, columns, uniqueId) => <FileListEntry columns={columns}
row={row} key={uniqueId}
events={this.props.events}/>}
/>
renderRow={(row: TableRow<ListedFileInfo>, columns, uniqueId) => (
<FileListEntry columns={columns}
row={row} key={uniqueId}
events={this.props.events}/>
)}
/>
)}
</CustomClassContext.Consumer>
</EventsContext.Provider>
);
}
componentDidMount(): void {
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", {
path: this.currentPath
});
@ -827,21 +1153,22 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
private onDrop(event: React.DragEvent) {
const types = event.dataTransfer.types;
if (types.length !== 1)
if (types.length !== 1) {
return;
}
event.stopPropagation();
let targetPath;
{
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;
}
targetPath = currentTarget?.getAttribute("x-drag-upload-path") || this.currentPath;
console.log("Target: %o %s", currentTarget, targetPath);
}
if (types[0] === FileTransferUrlMediaType) {
/* TODO: If cross move upload! */
/* TODO: Test if we moved cross some boundaries */
console.error(event.dataTransfer.getData(FileTransferUrlMediaType));
const fileUrls = event.dataTransfer.getData(FileTransferUrlMediaType).split("&").map(e => decodeURIComponent(e));
for (const fileUrl of fileUrls) {
@ -903,13 +1230,14 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
private onBodyContextMenu(event: React.MouseEvent) {
event.preventDefault();
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")
private handleNavigationResult(event: FileBrowserEvents["action_navigate_to_result"]) {
if (event.status !== "success")
@EventHandler<FileBrowserEvents>("notify_current_path")
private handleNavigationResult(event: FileBrowserEvents["notify_current_path"]) {
if (event.status !== "success") {
return;
}
this.currentPath = event.path;
this.selection = [];
@ -983,8 +1311,9 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
@EventHandler<FileBrowserEvents>("action_start_create_directory")
private handleActionFileCreateBegin(event: FileBrowserEvents["action_start_create_directory"]) {
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++;
}
const name = event.defaultName + (index > 0 ? " (" + index + ")" : "");
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! */
this.forceUpdate(() => this.props.events.fire_react("action_select_files", {
files: [{
name: name,
type: FileType.DIRECTORY
}], mode: "exclusive"
}));
this.forceUpdate(() => {
this.props.events.fire_react("action_select_files", {
files: [{
name: name,
type: FileType.DIRECTORY
}], mode: "exclusive"
});
});
}
@EventHandler<FileBrowserEvents>("action_create_directory_result")
@ -1112,8 +1443,9 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
@EventHandler<FileBrowserEvents>("notify_transfer_status")
private handleTransferStatus(event: FileBrowserEvents["notify_transfer_status"]) {
const index = this.fileList.findIndex(e => e.transfer?.id === event.id);
if (index === -1)
if (index === -1) {
return;
}
let element = this.fileList[index];
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 {useEffect, useRef, useState} from "react";
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 {HTMLRenderer} from "tc-shared/ui/react-elements/HTMLRenderer";
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 {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
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 iconTransferUpload = require("./icon_transfer_upload.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 [expended, setExpended] = useState(props.extended);
@ -495,7 +448,7 @@ const ExtendedInfo = (props: { events: Registry<TransferInfoEvents> }) => {
</div>;
};
export const TransferInfo = (props: { events: Registry<TransferInfoEvents> }) => (
export const FileTransferInfo = (props: { events: Registry<TransferInfoEvents> }) => (
<div className={cssStyle.container}>
<ExtendedInfo events={props.events}/>
<BottomBar events={props.events}/>

View File

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

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 * as React from "react";
import {FileType} from "tc-shared/file/FileManager";
import {Registry} from "tc-shared/events";
import {FileBrowser, NavigationBar} from "tc-shared/ui/modal/transfer/FileBrowser";
import {TransferInfo, TransferInfoEvents} from "tc-shared/ui/modal/transfer/TransferInfo";
import {initializeRemoteFileBrowserController} from "tc-shared/ui/modal/transfer/RemoteFileBrowserController";
import {ChannelEntry} from "tc-shared/tree/Channel";
import {initializeTransferInfoController} from "tc-shared/ui/modal/transfer/TransferInfoController";
import {FileBrowserRenderer, NavigationBar} from "./FileBrowserRenderer";
import {FileTransferInfo} from "./FileTransferInfo";
import {initializeRemoteFileBrowserController} from "./FileBrowserControllerRemote";
import {initializeTransferInfoController} from "./FileTransferInfoController";
import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
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");
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
}
}
const cssStyle = require("./FileBrowserRenderer.scss");
class FileTransferModal extends InternalModal {
readonly remoteBrowseEvents = new Registry<FileBrowserEvents>();
@ -196,12 +33,12 @@ class FileTransferModal extends InternalModal {
protected onInitialize() {
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() {
this.remoteBrowseEvents.fire("notify_modal_closed");
this.transferInfoEvents.fire("notify_modal_closed");
this.remoteBrowseEvents.fire("notify_destroy");
this.transferInfoEvents.fire("notify_destroy");
}
title() {
@ -210,11 +47,13 @@ class FileTransferModal extends InternalModal {
renderBody() {
const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/";
return <div className={cssStyle.container}>
<NavigationBar events={this.remoteBrowseEvents} currentPath={path}/>
<FileBrowser events={this.remoteBrowseEvents} currentPath={path}/>
<TransferInfo events={this.transferInfoEvents}/>
</div>
return (
<div className={cssStyle.container}>
<NavigationBar events={this.remoteBrowseEvents} initialPath={path} />
<FileBrowserRenderer events={this.remoteBrowseEvents} initialPath={path} />
<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);
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";
onFocus?: () => void;
onFocus?: (event: React.FocusEvent | React.MouseEvent) => void;
onBlur?: () => 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);
});
if(this.props.bodyOverlay)
if(this.props.bodyOverlay) {
body.push(this.props.bodyOverlay());
}
}
return (