Added file transfer to the side bar
parent
8480587f4f
commit
348dcdb923
|
@ -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:
|
||||||
|
|
|
@ -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! */
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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));
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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") {
|
|
@ -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: {},
|
||||||
|
}
|
|
@ -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}/>
|
|
@ -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));
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 };
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(" ");
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
Loading…
Reference in New Issue