Added file transfer to the side bar
parent
3412faf125
commit
5da2877c8b
|
@ -202,6 +202,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
this.channelTree = channelTree;
|
||||
this.events = new Registry<ChannelEvents>();
|
||||
|
||||
this.subscribed = false;
|
||||
this.properties = new ChannelProperties();
|
||||
this.channelId = channelId;
|
||||
this.properties.channel_name = channelName;
|
||||
|
@ -712,6 +713,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
cached_password() { return this.cachedPasswordHash; }
|
||||
|
||||
async updateSubscribeMode() {
|
||||
console.error("Update subscribe mode");
|
||||
let shouldBeSubscribed = false;
|
||||
switch (this.subscriptionMode) {
|
||||
case ChannelSubscribeMode.INHERITED:
|
||||
|
|
|
@ -8,6 +8,7 @@ import {PrivateConversationsPanel} from "tc-shared/ui/frames/side/PrivateConvers
|
|||
import {ChannelBarRenderer} from "tc-shared/ui/frames/side/ChannelBarRenderer";
|
||||
import {LogCategory, logWarn} from "tc-shared/log";
|
||||
import React = require("react");
|
||||
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
||||
|
||||
const cssStyle = require("./SideBarRenderer.scss");
|
||||
|
||||
|
@ -52,6 +53,7 @@ const ContentRendererClientInfo = () => {
|
|||
const contentData = useContentData("client-info");
|
||||
if(!contentData) { return null; }
|
||||
|
||||
throw "XX";
|
||||
return (
|
||||
<ClientInfoRenderer
|
||||
events={contentData.events}
|
||||
|
@ -62,13 +64,25 @@ const ContentRendererClientInfo = () => {
|
|||
const SideBarFrame = (props: { type: SideBarType }) => {
|
||||
switch (props.type) {
|
||||
case "channel":
|
||||
return <ContentRendererChannel key={props.type} />;
|
||||
return (
|
||||
<ErrorBoundary key={props.type}>
|
||||
<ContentRendererChannel />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
case "private-chat":
|
||||
return <ContentRendererPrivateConversation key={props.type} />;
|
||||
return (
|
||||
<ErrorBoundary key={props.type}>
|
||||
<ContentRendererPrivateConversation />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
case "client-info":
|
||||
return <ContentRendererClientInfo key={props.type} />;
|
||||
return (
|
||||
<ErrorBoundary key={props.type}>
|
||||
<ContentRendererClientInfo />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
case "music-manage":
|
||||
/* TODO! */
|
||||
|
|
|
@ -5,12 +5,14 @@ import {ChannelEntry, ChannelSidebarMode} from "tc-shared/tree/Channel";
|
|||
import {ChannelConversationController} from "tc-shared/ui/frames/side/ChannelConversationController";
|
||||
import {ChannelDescriptionController} from "tc-shared/ui/frames/side/ChannelDescriptionController";
|
||||
import {LocalClientEntry} from "tc-shared/tree/Client";
|
||||
import {ChannelFileBrowserController} from "tc-shared/ui/frames/side/ChannelFileBrowserController";
|
||||
|
||||
export class ChannelBarController {
|
||||
readonly uiEvents: Registry<ChannelBarUiEvents>;
|
||||
|
||||
private channelConversations: ChannelConversationController;
|
||||
private description: ChannelDescriptionController;
|
||||
private fileBrowser: ChannelFileBrowserController;
|
||||
|
||||
private currentConnection: ConnectionHandler;
|
||||
private listenerConnection: (() => void)[];
|
||||
|
@ -25,6 +27,7 @@ export class ChannelBarController {
|
|||
|
||||
this.channelConversations = new ChannelConversationController();
|
||||
this.description = new ChannelDescriptionController();
|
||||
this.fileBrowser = new ChannelFileBrowserController();
|
||||
|
||||
this.uiEvents.on("query_mode", () => this.notifyChannelMode());
|
||||
this.uiEvents.on("query_channel_id", () => this.notifyChannelId());
|
||||
|
@ -41,6 +44,9 @@ export class ChannelBarController {
|
|||
this.currentChannel = undefined;
|
||||
this.currentConnection = undefined;
|
||||
|
||||
this.fileBrowser?.destroy();
|
||||
this.fileBrowser = undefined;
|
||||
|
||||
this.channelConversations?.destroy();
|
||||
this.channelConversations = undefined;
|
||||
|
||||
|
@ -56,6 +62,7 @@ export class ChannelBarController {
|
|||
}
|
||||
|
||||
this.channelConversations.setConnectionHandler(handler);
|
||||
this.fileBrowser.setConnectionHandler(handler);
|
||||
|
||||
this.listenerConnection.forEach(callback => callback());
|
||||
this.listenerConnection = [];
|
||||
|
@ -96,6 +103,7 @@ export class ChannelBarController {
|
|||
return;
|
||||
}
|
||||
|
||||
this.fileBrowser.setChannel(channel);
|
||||
this.description.setChannel(channel);
|
||||
|
||||
this.listenerChannel.forEach(callback => callback());
|
||||
|
@ -185,9 +193,9 @@ export class ChannelBarController {
|
|||
this.uiEvents.fire_react("notify_data", {
|
||||
content: "file-transfer",
|
||||
data: {
|
||||
events: this.fileBrowser.uiEvents
|
||||
}
|
||||
});
|
||||
/* TODO! */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions";
|
||||
import {ChannelDescriptionUiEvents} from "tc-shared/ui/frames/side/ChannelDescriptionDefinitions";
|
||||
import {ChannelFileBrowserUiEvents} from "tc-shared/ui/frames/side/ChannelFileBrowserDefinitions";
|
||||
|
||||
export type ChannelBarMode = "conversation" | "description" | "file-transfer" | "none";
|
||||
|
||||
|
@ -12,7 +13,7 @@ export interface ChannelBarModeData {
|
|||
events: Registry<ChannelDescriptionUiEvents>
|
||||
},
|
||||
"file-transfer": {
|
||||
/* TODO! */
|
||||
events: Registry<ChannelFileBrowserUiEvents>
|
||||
},
|
||||
"none": {}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import * as React from "react";
|
|||
import {ConversationPanel} from "tc-shared/ui/frames/side/AbstractConversationRenderer";
|
||||
import {useDependentState} from "tc-shared/ui/react-elements/Helper";
|
||||
import {ChannelDescriptionRenderer} from "tc-shared/ui/frames/side/ChannelDescriptionRenderer";
|
||||
import {ChannelFileBrowser} from "tc-shared/ui/frames/side/ChannelFileBrowserRenderer";
|
||||
|
||||
const EventContext = React.createContext<Registry<ChannelBarUiEvents>>(undefined);
|
||||
const ChannelContext = React.createContext<{ channelId: number, handlerId: string }>(undefined);
|
||||
|
@ -37,8 +38,7 @@ const ModeRenderer = () => {
|
|||
return <ModeRendererDescription key={"description"} />;
|
||||
|
||||
case "file-transfer":
|
||||
/* TODO! */
|
||||
return null;
|
||||
return <ModeRendererFileTransfer key={"file"} />;
|
||||
|
||||
case "none":
|
||||
default:
|
||||
|
@ -72,6 +72,16 @@ const ModeRendererDescription = React.memo(() => {
|
|||
);
|
||||
});
|
||||
|
||||
const ModeRendererFileTransfer = React.memo(() => {
|
||||
const channelContext = useContext(ChannelContext);
|
||||
const data = useModeData("file-transfer", [ channelContext ]);
|
||||
if(!data) { return null; }
|
||||
|
||||
return (
|
||||
<ChannelFileBrowser events={data.events} />
|
||||
);
|
||||
});
|
||||
|
||||
export const ChannelBarRenderer = (props: { events: Registry<ChannelBarUiEvents> }) => {
|
||||
const [ channelContext, setChannelContext ] = useState<{ channelId: number, handlerId: string }>(() => {
|
||||
props.events.fire("query_channel_id");
|
||||
|
|
|
@ -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
|
||||
} from "../../../file/Transfer";
|
||||
import {createErrorModal} from "../../../ui/elements/Modal";
|
||||
import {ErrorCode} from "../../../connection/ErrorCode";
|
||||
import {
|
||||
avatarsPathPrefix,
|
||||
channelPathPrefix,
|
||||
FileBrowserEvents,
|
||||
iconPathPrefix,
|
||||
ListedFileInfo,
|
||||
channelPathPrefix, FileBrowserEvents,
|
||||
iconPathPrefix, ListedFileInfo,
|
||||
PathInfo
|
||||
} from "../../../ui/modal/transfer/ModalFileTransfer";
|
||||
import {ErrorCode} from "../../../connection/ErrorCode";
|
||||
} from "tc-shared/ui/modal/transfer/FileDefinitions";
|
||||
|
||||
function parsePath(path: string, connection: ConnectionHandler): PathInfo {
|
||||
if (path === "/" || !path) {
|
||||
|
@ -46,7 +44,7 @@ function parsePath(path: string, connection: ConnectionHandler): PathInfo {
|
|||
|
||||
const channel = connection.channelTree.findChannel(channelId);
|
||||
if (!channel) {
|
||||
throw tr("Channel not visible anymore");
|
||||
throw tr("Invalid channel id");
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -79,13 +77,13 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
try {
|
||||
const info = parsePath(event.path, connection);
|
||||
|
||||
events.fire_react("action_navigate_to_result", {
|
||||
events.fire_react("notify_current_path", {
|
||||
path: event.path || "/",
|
||||
status: "success",
|
||||
pathInfo: info
|
||||
});
|
||||
} catch (error) {
|
||||
events.fire_react("action_navigate_to_result", {
|
||||
events.fire_react("notify_current_path", {
|
||||
path: event.path,
|
||||
status: "error",
|
||||
error: error
|
||||
|
@ -284,8 +282,9 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
let sourcePath: PathInfo, targetPath: PathInfo;
|
||||
try {
|
||||
sourcePath = parsePath(event.oldPath, connection);
|
||||
if (sourcePath.type !== "channel")
|
||||
if (sourcePath.type !== "channel") {
|
||||
throw tr("Icon/avatars could not be renamed");
|
||||
}
|
||||
} catch (error) {
|
||||
events.fire_react("action_rename_file_result", {
|
||||
oldPath: event.oldPath,
|
||||
|
@ -297,8 +296,9 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
}
|
||||
try {
|
||||
targetPath = parsePath(event.newPath, connection);
|
||||
if (sourcePath.type !== "channel")
|
||||
if (sourcePath.type !== "channel") {
|
||||
throw tr("Target path isn't a channel");
|
||||
}
|
||||
} catch (error) {
|
||||
events.fire_react("action_rename_file_result", {
|
||||
oldPath: event.oldPath,
|
||||
|
@ -374,15 +374,22 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
let currentPath = "/";
|
||||
let currentPathInfo: PathInfo;
|
||||
let selection: { name: string, type: FileType }[] = [];
|
||||
events.on("action_navigate_to_result", result => {
|
||||
if (result.status !== "success")
|
||||
events.on("notify_current_path", result => {
|
||||
if (result.status !== "success") {
|
||||
return;
|
||||
}
|
||||
|
||||
currentPathInfo = result.pathInfo;
|
||||
currentPath = result.path;
|
||||
selection = [];
|
||||
});
|
||||
|
||||
events.on("query_current_path", () => events.fire_react("notify_current_path", {
|
||||
status: "success",
|
||||
path: currentPath,
|
||||
pathInfo: currentPathInfo
|
||||
}));
|
||||
|
||||
events.on("action_rename_file_result", result => {
|
||||
if (result.status !== "success")
|
||||
return;
|
||||
|
@ -812,10 +819,10 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
});
|
||||
|
||||
const closeListener = () => unregisterEvents();
|
||||
events.on("notify_modal_closed", closeListener);
|
||||
events.on("notify_destroy", closeListener);
|
||||
|
||||
const unregisterEvents = () => {
|
||||
events.off("notify_modal_closed", closeListener);
|
||||
events.off("notify_destroy", closeListener);
|
||||
transfer.events.off("notify_progress", progressListener);
|
||||
};
|
||||
};
|
||||
|
@ -823,7 +830,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
|
|||
|
||||
const registeredListener = event => listenToTransfer(event.transfer);
|
||||
connection.fileManager.events.on("notify_transfer_registered", registeredListener);
|
||||
events.on("notify_modal_closed", () => connection.fileManager.events.off("notify_transfer_registered", registeredListener));
|
||||
events.on("notify_destroy", () => connection.fileManager.events.off("notify_transfer_registered", registeredListener));
|
||||
|
||||
connection.fileManager.registeredTransfers().forEach(transfer => listenToTransfer(transfer));
|
||||
}
|
|
@ -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 {useEffect, useRef, useState} from "react";
|
||||
import {useContext, useEffect, useRef, useState} from "react";
|
||||
import {FileType} from "tc-shared/file/FileManager";
|
||||
import * as ppt from "tc-backend/ppt";
|
||||
import {SpecialKey} from "tc-shared/PPTListener";
|
||||
|
@ -12,21 +12,42 @@ import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
|||
import * as Moment from "moment";
|
||||
import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
|
||||
import {BoxedInputField} from "tc-shared/ui/react-elements/InputField";
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import React = require("react");
|
||||
import {
|
||||
FileBrowserEvents,
|
||||
FileTransferUrlMediaType,
|
||||
ListedFileInfo,
|
||||
TransferStatus
|
||||
} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import React = require("react");
|
||||
} from "tc-shared/ui/modal/transfer/FileDefinitions";
|
||||
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
|
||||
|
||||
const cssStyle = require("./ModalFileTransfer.scss");
|
||||
export interface FileBrowserRendererClasses {
|
||||
navigation?: {
|
||||
boxedInput?: string
|
||||
},
|
||||
fileTable?: {
|
||||
table?: string,
|
||||
header?: string,
|
||||
body?: string
|
||||
},
|
||||
fileEntry?: {
|
||||
entry?: string,
|
||||
selected?: string,
|
||||
dropHovered?: string
|
||||
}
|
||||
}
|
||||
|
||||
const EventsContext = React.createContext<Registry<FileBrowserEvents>>(undefined);
|
||||
const CustomClassContext = React.createContext<FileBrowserRendererClasses>(undefined);
|
||||
export const FileBrowserClassContext = CustomClassContext;
|
||||
|
||||
const cssStyle = require("./FileBrowserRenderer.scss");
|
||||
|
||||
interface NavigationBarProperties {
|
||||
currentPath: string;
|
||||
initialPath: string;
|
||||
events: Registry<FileBrowserEvents>;
|
||||
}
|
||||
|
||||
|
@ -36,9 +57,11 @@ interface NavigationBarState {
|
|||
state: "editing" | "navigating" | "normal";
|
||||
}
|
||||
|
||||
const ArrowRight = () => <div className={cssStyle.arrow}>
|
||||
<div className={cssStyle.inner}/>
|
||||
</div>;
|
||||
const ArrowRight = () => (
|
||||
<div className={cssStyle.arrow}>
|
||||
<div className={cssStyle.inner}/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const NavigationEntry = (props: { events: Registry<FileBrowserEvents>, path: string, name: string }) => {
|
||||
const [dragHovered, setDragHovered] = useState(false);
|
||||
|
@ -56,11 +79,15 @@ const NavigationEntry = (props: { events: Registry<FileBrowserEvents>, path: str
|
|||
<a
|
||||
className={(props.name.length > 9 ? cssStyle.pathShrink : "") + " " + (dragHovered ? cssStyle.hovered : "")}
|
||||
title={props.name}
|
||||
onClick={() => props.events.fire("action_navigate_to", {path: props.path})}
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
props.events.fire("action_navigate_to", {path: props.path});
|
||||
}}
|
||||
onDragOver={event => {
|
||||
const types = event.dataTransfer.types;
|
||||
if (types.length !== 1)
|
||||
if (types.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (types[0] === FileTransferUrlMediaType) {
|
||||
/* TODO: Detect if its remote move or internal move */
|
||||
|
@ -77,8 +104,9 @@ const NavigationEntry = (props: { events: Registry<FileBrowserEvents>, path: str
|
|||
onDragLeave={() => setDragHovered(false)}
|
||||
onDrop={event => {
|
||||
const types = event.dataTransfer.types;
|
||||
if (types.length !== 1)
|
||||
if (types.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* TODO: Fix this code duplicate! */
|
||||
if (types[0] === FileTransferUrlMediaType) {
|
||||
|
@ -121,7 +149,7 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
|
|||
|
||||
protected defaultState(): NavigationBarState {
|
||||
return {
|
||||
currentPath: this.props.currentPath,
|
||||
currentPath: this.props.initialPath,
|
||||
state: "normal",
|
||||
}
|
||||
}
|
||||
|
@ -134,67 +162,81 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
|
|||
|
||||
if (this.state.state === "editing") {
|
||||
input = (
|
||||
<BoxedInputField key={"nav-editing"}
|
||||
ref={this.refInput}
|
||||
defaultValue={path}
|
||||
leftIcon={() =>
|
||||
<div key={"left-icon"}
|
||||
className={cssStyle.directoryIcon + " " + cssStyle.containerIcon}>
|
||||
<div className={"icon_em client-file_home"}/>
|
||||
</div>
|
||||
}
|
||||
<CustomClassContext.Consumer>
|
||||
{customClasses => (
|
||||
<BoxedInputField key={"nav-editing"}
|
||||
ref={this.refInput}
|
||||
defaultValue={path}
|
||||
leftIcon={() =>
|
||||
<div key={"left-icon"}
|
||||
className={cssStyle.directoryIcon + " " + cssStyle.containerIcon}>
|
||||
<div className={"icon_em client-file_home"}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
rightIcon={() =>
|
||||
<div key={"right-icon"}
|
||||
className={cssStyle.refreshIcon + " " + cssStyle.containerIcon}>
|
||||
<div className={"icon_em client-refresh"}/>
|
||||
</div>
|
||||
}
|
||||
rightIcon={() =>
|
||||
<div key={"right-icon"}
|
||||
className={cssStyle.refreshIcon + " " + cssStyle.containerIcon}>
|
||||
<div className={"icon_em client-refresh"}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
onChange={path => this.onPathEntered(path)}
|
||||
onBlur={() => this.onInputPathBluer()}
|
||||
/>
|
||||
onChange={path => this.onPathEntered(path)}
|
||||
onBlur={() => this.onInputPathBluer()}
|
||||
className={customClasses?.navigation?.boxedInput}
|
||||
/>
|
||||
)}
|
||||
</CustomClassContext.Consumer>
|
||||
);
|
||||
} else if (this.state.state === "navigating" || this.state.state === "normal") {
|
||||
input = (
|
||||
<BoxedInputField key={"nav-rendered"}
|
||||
ref={this.refInput}
|
||||
leftIcon={() =>
|
||||
<div key={"left-icon"}
|
||||
className={cssStyle.directoryIcon + " " + cssStyle.containerIcon}
|
||||
onClick={event => this.onPathClicked(event, -1)}>
|
||||
<div className={"icon_em client-file_home"}/>
|
||||
</div>
|
||||
}
|
||||
<CustomClassContext.Consumer>
|
||||
{customClasses => (
|
||||
<BoxedInputField key={"nav-rendered"}
|
||||
ref={this.refInput}
|
||||
leftIcon={() =>
|
||||
<div key={"left-icon"}
|
||||
className={cssStyle.directoryIcon + " " + cssStyle.containerIcon}
|
||||
onClick={event => this.onPathClicked(event, -1)}>
|
||||
<div className={"icon_em client-file_home"}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
rightIcon={() =>
|
||||
<div
|
||||
key={"right-icon"}
|
||||
className={cssStyle.refreshIcon + " " + (this.state.state === "normal" ? cssStyle.enabled : "") + " " + cssStyle.containerIcon}
|
||||
onClick={() => this.onButtonRefreshClicked()}>
|
||||
<div className={"icon_em client-refresh"}/>
|
||||
</div>
|
||||
}
|
||||
rightIcon={() =>
|
||||
<div
|
||||
key={"right-icon"}
|
||||
className={cssStyle.refreshIcon + " " + (this.state.state === "normal" ? cssStyle.enabled : "") + " " + cssStyle.containerIcon}
|
||||
onClick={() => this.onButtonRefreshClicked()}>
|
||||
<div className={"icon_em client-refresh"}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
inputBox={() =>
|
||||
<div key={"custom-input"} className={cssStyle.containerPath}
|
||||
ref={this.refRendered}>
|
||||
{this.state.currentPath.split("/").filter(e => !!e).map((e, index, arr) => [
|
||||
<ArrowRight key={"arrow-right-" + index + "-" + e}/>,
|
||||
<NavigationEntry key={"de-" + index + "-" + e}
|
||||
path={"/" + arr.slice(0, index + 1).join("/") + "/"}
|
||||
name={e} events={this.props.events}/>
|
||||
])}
|
||||
</div>
|
||||
}
|
||||
inputBox={() =>
|
||||
<div key={"custom-input"} className={cssStyle.containerPath}
|
||||
ref={this.refRendered}>
|
||||
{this.state.currentPath.split("/").filter(e => !!e).map((e, index, arr) => [
|
||||
<ArrowRight key={"arrow-right-" + index + "-" + e}/>,
|
||||
<NavigationEntry key={"de-" + index + "-" + e}
|
||||
path={"/" + arr.slice(0, index + 1).join("/") + "/"}
|
||||
name={e} events={this.props.events}/>
|
||||
])}
|
||||
</div>
|
||||
}
|
||||
|
||||
editable={this.state.state === "normal"}
|
||||
onFocus={() => this.onRenderedPathClicked()}
|
||||
/>
|
||||
editable={this.state.state === "normal"}
|
||||
onFocus={event => !event.defaultPrevented && this.onRenderedPathClicked()}
|
||||
className={customClasses?.navigation?.boxedInput}
|
||||
/>
|
||||
)}
|
||||
</CustomClassContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={cssStyle.navigation}>{input}</div>;
|
||||
return (
|
||||
<div className={cssStyle.navigation}>
|
||||
{input}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<NavigationBarProperties>, prevState: Readonly<NavigationBarState>, snapshot?: any): void {
|
||||
|
@ -216,8 +258,9 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
|
|||
}
|
||||
|
||||
private onRenderedPathClicked() {
|
||||
if (this.state.state !== "normal")
|
||||
if (this.state.state !== "normal") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
state: "editing"
|
||||
|
@ -234,8 +277,9 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
|
|||
}
|
||||
|
||||
private onPathEntered(newPath: string) {
|
||||
if (newPath === this.state.currentPath)
|
||||
if (newPath === this.state.currentPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.ignoreBlur = true;
|
||||
this.props.events.fire("action_navigate_to", {path: newPath});
|
||||
|
@ -258,10 +302,11 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
|
|||
}, () => this.ignoreBlur = false);
|
||||
}
|
||||
|
||||
@EventHandler<FileBrowserEvents>("action_navigate_to_result")
|
||||
private handleNavigateResult(event: FileBrowserEvents["action_navigate_to_result"]) {
|
||||
if (event.status === "success")
|
||||
@EventHandler<FileBrowserEvents>("notify_current_path")
|
||||
private handleCurrentPath(event: FileBrowserEvents["notify_current_path"]) {
|
||||
if (event.status === "success") {
|
||||
this.lastSucceededPath = event.path;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
state: "normal",
|
||||
|
@ -279,7 +324,7 @@ export class NavigationBar extends ReactComponentBase<NavigationBarProperties, N
|
|||
}
|
||||
|
||||
interface FileListTableProperties {
|
||||
currentPath: string;
|
||||
initialPath: string;
|
||||
events: Registry<FileBrowserEvents>;
|
||||
}
|
||||
|
||||
|
@ -288,7 +333,8 @@ interface FileListTableState {
|
|||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, file: ListedFileInfo }) => {
|
||||
const FileName = (props: { path: string, file: ListedFileInfo }) => {
|
||||
const events = useContext(EventsContext);
|
||||
const [editing, setEditing] = useState(props.file.mode === "create");
|
||||
const [fileName, setFileName] = useState(props.file.name);
|
||||
const refInput = useRef<HTMLInputElement>();
|
||||
|
@ -333,7 +379,7 @@ const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, fi
|
|||
if (props.file.mode === "create") {
|
||||
name = name || props.file.name;
|
||||
|
||||
props.events.fire("action_create_directory", {
|
||||
events.fire("action_create_directory", {
|
||||
path: props.path,
|
||||
name: name
|
||||
});
|
||||
|
@ -342,7 +388,7 @@ const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, fi
|
|||
props.file.mode = "creating";
|
||||
} else {
|
||||
if (name.length > 0 && name !== props.file.name) {
|
||||
props.events.fire("action_rename_file", {
|
||||
events.fire("action_rename_file", {
|
||||
oldName: props.file.name,
|
||||
newName: name,
|
||||
oldPath: props.path,
|
||||
|
@ -381,19 +427,19 @@ const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, fi
|
|||
return;
|
||||
|
||||
event.stopPropagation();
|
||||
props.events.fire("action_select_files", {
|
||||
events.fire("action_select_files", {
|
||||
mode: "exclusive",
|
||||
files: [{name: props.file.name, type: props.file.type}]
|
||||
});
|
||||
props.events.fire("action_start_rename", {
|
||||
events.fire("action_start_rename", {
|
||||
path: props.path,
|
||||
name: props.file.name
|
||||
});
|
||||
}}>{fileName}</a>;
|
||||
}
|
||||
|
||||
props.events.reactUse("action_start_rename", event => setEditing(event.name === props.file.name && event.path === props.path));
|
||||
props.events.reactUse("action_rename_file_result", event => {
|
||||
events.reactUse("action_start_rename", event => setEditing(event.name === props.file.name && event.path === props.path));
|
||||
events.reactUse("action_rename_file_result", event => {
|
||||
if (event.oldPath !== props.path || event.oldName !== props.file.name)
|
||||
return;
|
||||
|
||||
|
@ -418,10 +464,11 @@ const FileName = (props: { path: string, events: Registry<FileBrowserEvents>, fi
|
|||
return <>{icon} {name}</>;
|
||||
};
|
||||
|
||||
const FileSize = (props: { path: string, events: Registry<FileBrowserEvents>, file: ListedFileInfo }) => {
|
||||
const FileSize = (props: { path: string, file: ListedFileInfo }) => {
|
||||
const events = useContext(EventsContext);
|
||||
const [size, setSize] = useState(-1);
|
||||
|
||||
props.events.reactUse("notify_transfer_status", event => {
|
||||
events.reactUse("notify_transfer_status", event => {
|
||||
if (event.id !== props.file.transfer?.id)
|
||||
return;
|
||||
|
||||
|
@ -440,7 +487,7 @@ const FileSize = (props: { path: string, events: Registry<FileBrowserEvents>, fi
|
|||
}
|
||||
});
|
||||
|
||||
props.events.reactUse("notify_transfer_progress", event => {
|
||||
events.reactUse("notify_transfer_progress", event => {
|
||||
if (event.id !== props.file.transfer?.id)
|
||||
return;
|
||||
|
||||
|
@ -450,13 +497,23 @@ const FileSize = (props: { path: string, events: Registry<FileBrowserEvents>, fi
|
|||
setSize(event.fileSize);
|
||||
});
|
||||
|
||||
if (size < 0 && (props.file.size < 0 || typeof props.file.size === "undefined"))
|
||||
return <a key={"size-invalid"}><Translatable>unknown</Translatable></a>;
|
||||
return <a key={"size"}>{network.format_bytes(size >= 0 ? size : props.file.size, {
|
||||
unit: "B",
|
||||
time: "",
|
||||
exact: false
|
||||
})}</a>;
|
||||
if (size < 0 && (props.file.size < 0 || typeof props.file.size === "undefined")) {
|
||||
return (
|
||||
<a key={"size-invalid"}>
|
||||
<Translatable>unknown</Translatable>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a key={"size"}>
|
||||
{network.format_bytes(size >= 0 ? size : props.file.size, {
|
||||
unit: "B",
|
||||
time: "",
|
||||
exact: false
|
||||
})}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const FileTransferIndicator = (props: { file: ListedFileInfo, events: Registry<FileBrowserEvents> }) => {
|
||||
|
@ -520,6 +577,7 @@ const FileTransferIndicator = (props: { file: ListedFileInfo, events: Registry<F
|
|||
color = cssStyle.hidden;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cssStyle.indicator + " " + color} style={{right: ((1 - transferProgress) * 100) + "%"}}>
|
||||
<div className={cssStyle.status}/>
|
||||
|
@ -532,18 +590,21 @@ const FileListEntry = (props: { row: TableRow<ListedFileInfo>, columns: TableCol
|
|||
const [hidden, setHidden] = useState(false);
|
||||
const [selected, setSelected] = useState(false);
|
||||
const [dropHovered, setDropHovered] = useState(false);
|
||||
const customClasses = useContext(CustomClassContext);
|
||||
|
||||
const onDoubleClicked = () => {
|
||||
if (file.type === FileType.DIRECTORY) {
|
||||
if (file.mode === "creating" || file.mode === "create")
|
||||
if (file.mode === "creating" || file.mode === "create") {
|
||||
return;
|
||||
}
|
||||
|
||||
props.events.fire("action_navigate_to", {
|
||||
path: file.path + file.name + "/"
|
||||
});
|
||||
} else {
|
||||
if (file.mode === "uploading" || file.virtual)
|
||||
if (file.mode === "uploading" || file.virtual) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.events.fire("action_start_download", {
|
||||
files: [{
|
||||
|
@ -577,12 +638,19 @@ const FileListEntry = (props: { row: TableRow<ListedFileInfo>, columns: TableCol
|
|||
});
|
||||
}, !hidden);
|
||||
|
||||
if (hidden)
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elementClassList = joinClassList(
|
||||
cssStyle.directoryEntry, customClasses?.fileEntry?.entry,
|
||||
selected && cssStyle.selected, selected && customClasses?.fileEntry?.selected,
|
||||
dropHovered && cssStyle.hovered, dropHovered && customClasses?.fileEntry?.dropHovered
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRowElement
|
||||
className={cssStyle.directoryEntry + " " + (selected ? cssStyle.selected : "") + " " + (dropHovered ? cssStyle.hovered : "")}
|
||||
className={elementClassList}
|
||||
rowData={props.row}
|
||||
columns={props.columns}
|
||||
onDoubleClick={onDoubleClicked}
|
||||
|
@ -655,8 +723,257 @@ const FileListEntry = (props: { row: TableRow<ListedFileInfo>, columns: TableCol
|
|||
);
|
||||
};
|
||||
|
||||
type FileListState = {
|
||||
state: "querying" | "invalid-password"
|
||||
} | {
|
||||
state: "no-permissions",
|
||||
failedPermission: string
|
||||
} | {
|
||||
state: "error",
|
||||
reason: string
|
||||
} | {
|
||||
state: "normal",
|
||||
files: ListedFileInfo[]
|
||||
};
|
||||
|
||||
|
||||
function fileTableHeaderContextMenu(event: React.MouseEvent, table: Table | undefined) {
|
||||
event.preventDefault();
|
||||
|
||||
if(!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
spawn_context_menu(event.pageX, event.pageY, {
|
||||
type: MenuEntryType.CHECKBOX,
|
||||
name: tr("Size"),
|
||||
checkbox_checked: table.state.hiddenColumns.findIndex(e => e === "size") === -1,
|
||||
callback: () => {
|
||||
table.state.hiddenColumns.toggle("size");
|
||||
table.forceUpdate();
|
||||
}
|
||||
}, {
|
||||
type: MenuEntryType.CHECKBOX,
|
||||
name: tr("Type"),
|
||||
checkbox_checked: table.state.hiddenColumns.findIndex(e => e === "type") === -1,
|
||||
callback: () => {
|
||||
table.state.hiddenColumns.toggle("type");
|
||||
table.forceUpdate();
|
||||
}
|
||||
}, {
|
||||
type: MenuEntryType.CHECKBOX,
|
||||
name: tr("Last changed"),
|
||||
checkbox_checked: table.state.hiddenColumns.findIndex(e => e === "change-date") === -1,
|
||||
callback: () => {
|
||||
table.state.hiddenColumns.toggle("change-date");
|
||||
table.forceUpdate();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// const FileListRenderer = React.memo((props: { path: string }) => {
|
||||
// const events = useContext(EventsContext);
|
||||
// const customClasses = useContext(CustomClassContext);
|
||||
//
|
||||
// const refTable = useRef<Table>();
|
||||
//
|
||||
// const [ state, setState ] = useState<FileListState>(() => {
|
||||
// events.fire("query_files", { path: props.path });
|
||||
// return { state: "querying" };
|
||||
// });
|
||||
//
|
||||
// events.reactUse("query_files", event => {
|
||||
// if(event.path === props.path) {
|
||||
// setState({ state: "querying" });
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// events.reactUse("query_files_result", event => {
|
||||
// if(event.path !== props.path) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// switch(event.status) {
|
||||
// case "no-permissions":
|
||||
// setState({ state: "no-permissions", failedPermission: event.error });
|
||||
// break;
|
||||
//
|
||||
// case "error":
|
||||
// setState({ state: "error", reason: event.error });
|
||||
// break;
|
||||
//
|
||||
// case "success":
|
||||
// setState({ state: "normal", files: event.files });
|
||||
// break;
|
||||
//
|
||||
// case "invalid-password":
|
||||
// setState({ state: "invalid-password" });
|
||||
// break;
|
||||
//
|
||||
// case "timeout":
|
||||
// setState({ state: "error", reason: tr("query timeout") });
|
||||
// break;
|
||||
//
|
||||
// default:
|
||||
// setState({ state: "error", reason: tra("invalid query result state {}", event.status) });
|
||||
// break;
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// let rows: TableRow[] = [];
|
||||
// let overlay;
|
||||
//
|
||||
// switch (state.state) {
|
||||
// case "querying":
|
||||
// overlay = () => (
|
||||
// <div key={"loading"} className={cssStyle.overlay}>
|
||||
// <a><Translatable>loading</Translatable><LoadingDots maxDots={3}/></a>
|
||||
// </div>
|
||||
// );
|
||||
// break;
|
||||
//
|
||||
// case "error":
|
||||
// overlay = () => (
|
||||
// <div key={"query-error"} className={cssStyle.overlay + " " + cssStyle.overlayError}>
|
||||
// <a><Translatable>Failed to query directory:</Translatable><br/>{state.reason}</a>
|
||||
// </div>
|
||||
// );
|
||||
// break;
|
||||
//
|
||||
// case "no-permissions":
|
||||
// overlay = () => (
|
||||
// <div key={"no-permissions"} className={cssStyle.overlay + " " + cssStyle.overlayError}>
|
||||
// <a><Translatable>Directory query failed on permission</Translatable><br/>{state.failedPermission}
|
||||
// </a>
|
||||
// </div>
|
||||
// );
|
||||
// break;
|
||||
//
|
||||
// case "invalid-password":
|
||||
// /* TODO: Allow the user to enter a password */
|
||||
// overlay = () => (
|
||||
// <div key={"invalid-password"} className={cssStyle.overlay + " " + cssStyle.overlayError}>
|
||||
// <a><Translatable>Directory query failed because it is password protected</Translatable></a>
|
||||
// </div>
|
||||
// );
|
||||
// break;
|
||||
//
|
||||
// case "normal":
|
||||
// if(state.files.length === 0) {
|
||||
// overlay = () => (
|
||||
// <div key={"no-files"} className={cssStyle.overlayEmptyFolder}>
|
||||
// <a><Translatable>This folder is empty.</Translatable></a>
|
||||
// </div>
|
||||
// );
|
||||
// } else {
|
||||
// const directories = state.files.filter(e => e.type === FileType.DIRECTORY);
|
||||
// const files = state.files.filter(e => e.type === FileType.FILE);
|
||||
//
|
||||
// for (const directory of directories.sort((a, b) => a.name > b.name ? 1 : -1)) {
|
||||
// rows.push({
|
||||
// columns: {
|
||||
// "name": () => <FileName path={props.path} file={directory}/>,
|
||||
// "type": () => <a key={"type"}><Translatable>Directory</Translatable></a>,
|
||||
// "change-date": () => directory.datetime ?
|
||||
// <a>{Moment(directory.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined
|
||||
// },
|
||||
// className: cssStyle.directoryEntry,
|
||||
// userData: directory
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// for (const file of files.sort((a, b) => a.name > b.name ? 1 : -1)) {
|
||||
// rows.push({
|
||||
// columns: {
|
||||
// "name": () => <FileName path={props.path} file={file}/>,
|
||||
// "size": () => <FileSize path={props.path} file={file}/>,
|
||||
// "type": () => <a key={"type"}><Translatable>File</Translatable></a>,
|
||||
// "change-date": () => file.datetime ?
|
||||
// <a key={"date"}>{Moment(file.datetime).format("DD/MM/YYYY HH:mm")}</a> :
|
||||
// undefined
|
||||
// },
|
||||
// className: cssStyle.directoryEntry,
|
||||
// userData: file
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// return (
|
||||
// <Table
|
||||
// ref={refTable}
|
||||
// className={joinClassList(cssStyle.fileTable, customClasses?.fileTable?.table)}
|
||||
// bodyClassName={joinClassList(cssStyle.body, customClasses?.fileTable?.body)}
|
||||
// headerClassName={joinClassList(cssStyle.header, customClasses?.fileTable?.header)}
|
||||
// columns={[
|
||||
// {
|
||||
// name: "name", header: () => [
|
||||
// <a key={"name-name"}><Translatable>Name</Translatable></a>,
|
||||
// <div key={"seperator-name"} className={cssStyle.separator}/>
|
||||
// ], width: 80, className: cssStyle.columnName
|
||||
// },
|
||||
// {
|
||||
// name: "type", header: () => [
|
||||
// <a key={"name-type"}><Translatable>Type</Translatable></a>,
|
||||
// <div key={"seperator-type"} className={cssStyle.separator}/>
|
||||
// ], fixedWidth: "8em", className: cssStyle.columnType
|
||||
// },
|
||||
// {
|
||||
// name: "size", header: () => [
|
||||
// <a key={"name-size"}><Translatable>Size</Translatable></a>,
|
||||
// <div key={"seperator-size"} className={cssStyle.separator}/>
|
||||
// ], fixedWidth: "8em", className: cssStyle.columnSize
|
||||
// },
|
||||
// {
|
||||
// name: "change-date", header: () => [
|
||||
// <a key={"name-date"}><Translatable>Last changed</Translatable></a>,
|
||||
// <div key={"seperator-date"} className={cssStyle.separator}/>
|
||||
// ], fixedWidth: "8em", className: cssStyle.columnChanged
|
||||
// },
|
||||
// ]}
|
||||
// rows={rows}
|
||||
//
|
||||
// bodyOverlayOnly={rows.length === 0}
|
||||
// bodyOverlay={overlay}
|
||||
//
|
||||
// hiddenColumns={["type"]}
|
||||
//
|
||||
// onHeaderContextMenu={e => fileTableHeaderContextMenu(e, refTable.current)}
|
||||
// onBodyContextMenu={event => {
|
||||
// event.preventDefault();
|
||||
// events.fire("action_select_files", { mode: "exclusive", files: [] });
|
||||
// events.fire("action_selection_context_menu", { pageY: event.pageY, pageX: event.pageX });
|
||||
// }}
|
||||
// onDrop={e => this.onDrop(e)}
|
||||
// onDragOver={event => {
|
||||
// const types = event.dataTransfer.types;
|
||||
// if (types.length !== 1)
|
||||
// return;
|
||||
//
|
||||
// if (types[0] === FileTransferUrlMediaType) {
|
||||
// /* TODO: Detect if its remote move or internal move */
|
||||
// event.dataTransfer.effectAllowed = "move";
|
||||
// } else if (types[0] === "Files") {
|
||||
// event.dataTransfer.effectAllowed = "copy";
|
||||
// } else {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// event.preventDefault();
|
||||
// }}
|
||||
//
|
||||
// renderRow={(row: TableRow<ListedFileInfo>, columns, uniqueId) => (
|
||||
// <FileListEntry columns={columns}
|
||||
// row={row} key={uniqueId}
|
||||
// events={this.props.events}/>
|
||||
// )}
|
||||
// />
|
||||
// );
|
||||
// });
|
||||
|
||||
@ReactEventHandler(e => e.props.events)
|
||||
export class FileBrowser extends ReactComponentBase<FileListTableProperties, FileListTableState> {
|
||||
export class FileBrowserRenderer extends ReactComponentBase<FileListTableProperties, FileListTableState> {
|
||||
private refTable = React.createRef<Table>();
|
||||
private currentPath: string;
|
||||
private fileList: ListedFileInfo[];
|
||||
|
@ -724,8 +1041,7 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
|
|||
for (const directory of directories.sort((a, b) => a.name > b.name ? 1 : -1)) {
|
||||
rows.push({
|
||||
columns: {
|
||||
"name": () => <FileName path={this.currentPath} events={this.props.events}
|
||||
file={directory}/>,
|
||||
"name": () => <FileName path={this.currentPath} file={directory}/>,
|
||||
"type": () => <a key={"type"}><Translatable>Directory</Translatable></a>,
|
||||
"change-date": () => directory.datetime ?
|
||||
<a>{Moment(directory.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined
|
||||
|
@ -738,8 +1054,8 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
|
|||
for (const file of files.sort((a, b) => a.name > b.name ? 1 : -1)) {
|
||||
rows.push({
|
||||
columns: {
|
||||
"name": () => <FileName path={this.currentPath} events={this.props.events} file={file}/>,
|
||||
"size": () => <FileSize path={this.currentPath} events={this.props.events} file={file}/>,
|
||||
"name": () => <FileName path={this.currentPath} file={file}/>,
|
||||
"size": () => <FileSize path={this.currentPath} file={file}/>,
|
||||
"type": () => <a key={"type"}><Translatable>File</Translatable></a>,
|
||||
"change-date": () => file.datetime ?
|
||||
<a key={"date"}>{Moment(file.datetime).format("DD/MM/YYYY HH:mm")}</a> : undefined
|
||||
|
@ -752,74 +1068,84 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
|
|||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
ref={this.refTable}
|
||||
className={cssStyle.fileTable}
|
||||
bodyClassName={cssStyle.body}
|
||||
headerClassName={cssStyle.header}
|
||||
columns={[
|
||||
{
|
||||
name: "name", header: () => [
|
||||
<a key={"name-name"}><Translatable>Name</Translatable></a>,
|
||||
<div key={"seperator-name"} className={cssStyle.seperator}/>
|
||||
], width: 80, className: cssStyle.columnName
|
||||
},
|
||||
{
|
||||
name: "type", header: () => [
|
||||
<a key={"name-type"}><Translatable>Type</Translatable></a>,
|
||||
<div key={"seperator-type"} className={cssStyle.seperator}/>
|
||||
], fixedWidth: "8em", className: cssStyle.columnType
|
||||
},
|
||||
{
|
||||
name: "size", header: () => [
|
||||
<a key={"name-size"}><Translatable>Size</Translatable></a>,
|
||||
<div key={"seperator-size"} className={cssStyle.seperator}/>
|
||||
], fixedWidth: "8em", className: cssStyle.columnSize
|
||||
},
|
||||
{
|
||||
name: "change-date", header: () => [
|
||||
<a key={"name-date"}><Translatable>Last changed</Translatable></a>,
|
||||
<div key={"seperator-date"} className={cssStyle.seperator}/>
|
||||
], fixedWidth: "8em", className: cssStyle.columnChanged
|
||||
},
|
||||
]}
|
||||
rows={rows}
|
||||
<EventsContext.Provider value={this.props.events}>
|
||||
<CustomClassContext.Consumer>
|
||||
{classes => (
|
||||
<Table
|
||||
ref={this.refTable}
|
||||
className={this.classList(cssStyle.fileTable, classes?.fileTable?.table)}
|
||||
bodyClassName={this.classList(cssStyle.body, classes?.fileTable?.body)}
|
||||
headerClassName={this.classList(cssStyle.header, classes?.fileTable?.header)}
|
||||
columns={[
|
||||
{
|
||||
name: "name", header: () => [
|
||||
<a key={"name-name"}><Translatable>Name</Translatable></a>,
|
||||
<div key={"seperator-name"} className={cssStyle.separator}/>
|
||||
], width: 80, className: cssStyle.columnName
|
||||
},
|
||||
{
|
||||
name: "type", header: () => [
|
||||
<a key={"name-type"}><Translatable>Type</Translatable></a>,
|
||||
<div key={"seperator-type"} className={cssStyle.separator}/>
|
||||
], fixedWidth: "8em", className: cssStyle.columnType
|
||||
},
|
||||
{
|
||||
name: "size", header: () => [
|
||||
<a key={"name-size"}><Translatable>Size</Translatable></a>,
|
||||
<div key={"seperator-size"} className={cssStyle.separator}/>
|
||||
], fixedWidth: "8em", className: cssStyle.columnSize
|
||||
},
|
||||
{
|
||||
name: "change-date", header: () => [
|
||||
<a key={"name-date"}><Translatable>Last changed</Translatable></a>,
|
||||
<div key={"seperator-date"} className={cssStyle.separator}/>
|
||||
], fixedWidth: "8em", className: cssStyle.columnChanged
|
||||
},
|
||||
]}
|
||||
rows={rows}
|
||||
|
||||
bodyOverlayOnly={overlayOnly}
|
||||
bodyOverlay={overlay}
|
||||
bodyOverlayOnly={overlayOnly}
|
||||
bodyOverlay={overlay}
|
||||
|
||||
hiddenColumns={["type"]}
|
||||
hiddenColumns={["type"]}
|
||||
|
||||
onHeaderContextMenu={e => this.onHeaderContextMenu(e)}
|
||||
onBodyContextMenu={e => this.onBodyContextMenu(e)}
|
||||
onDrop={e => this.onDrop(e)}
|
||||
onDragOver={event => {
|
||||
const types = event.dataTransfer.types;
|
||||
if (types.length !== 1)
|
||||
return;
|
||||
onHeaderContextMenu={e => this.onHeaderContextMenu(e)}
|
||||
onBodyContextMenu={e => this.onBodyContextMenu(e)}
|
||||
onDrop={e => this.onDrop(e)}
|
||||
onDragOver={event => {
|
||||
const types = event.dataTransfer.types;
|
||||
if (types.length !== 1)
|
||||
return;
|
||||
|
||||
if (types[0] === FileTransferUrlMediaType) {
|
||||
/* TODO: Detect if its remote move or internal move */
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
} else if (types[0] === "Files") {
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (types[0] === FileTransferUrlMediaType) {
|
||||
/* TODO: Detect if its remote move or internal move */
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
} else if (types[0] === "Files") {
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}}
|
||||
event.preventDefault();
|
||||
}}
|
||||
|
||||
renderRow={(row: TableRow<ListedFileInfo>, columns, uniqueId) => <FileListEntry columns={columns}
|
||||
row={row} key={uniqueId}
|
||||
events={this.props.events}/>}
|
||||
/>
|
||||
renderRow={(row: TableRow<ListedFileInfo>, columns, uniqueId) => (
|
||||
<FileListEntry columns={columns}
|
||||
row={row} key={uniqueId}
|
||||
events={this.props.events}/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CustomClassContext.Consumer>
|
||||
</EventsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.selection = [];
|
||||
this.currentPath = this.props.currentPath;
|
||||
this.currentPath = this.props.initialPath;
|
||||
|
||||
this.props.events.fire("query_current_path", {});
|
||||
this.props.events.fire("query_files", {
|
||||
path: this.currentPath
|
||||
});
|
||||
|
@ -827,21 +1153,22 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
|
|||
|
||||
private onDrop(event: React.DragEvent) {
|
||||
const types = event.dataTransfer.types;
|
||||
if (types.length !== 1)
|
||||
if (types.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
let targetPath;
|
||||
{
|
||||
let currentTarget = event.target as HTMLElement;
|
||||
while (currentTarget && !currentTarget.hasAttribute("x-drag-upload-path"))
|
||||
while (currentTarget && !currentTarget.hasAttribute("x-drag-upload-path")) {
|
||||
currentTarget = currentTarget.parentElement;
|
||||
}
|
||||
targetPath = currentTarget?.getAttribute("x-drag-upload-path") || this.currentPath;
|
||||
console.log("Target: %o %s", currentTarget, targetPath);
|
||||
}
|
||||
|
||||
if (types[0] === FileTransferUrlMediaType) {
|
||||
/* TODO: If cross move upload! */
|
||||
/* TODO: Test if we moved cross some boundaries */
|
||||
console.error(event.dataTransfer.getData(FileTransferUrlMediaType));
|
||||
const fileUrls = event.dataTransfer.getData(FileTransferUrlMediaType).split("&").map(e => decodeURIComponent(e));
|
||||
for (const fileUrl of fileUrls) {
|
||||
|
@ -903,13 +1230,14 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
|
|||
private onBodyContextMenu(event: React.MouseEvent) {
|
||||
event.preventDefault();
|
||||
this.props.events.fire("action_select_files", {mode: "exclusive", files: []});
|
||||
this.props.events.fire("action_selection_context_menu", {pageY: event.pageY, pageX: event.pageX});
|
||||
this.props.events.fire("action_selection_context_menu", { pageY: event.pageY, pageX: event.pageX });
|
||||
}
|
||||
|
||||
@EventHandler<FileBrowserEvents>("action_navigate_to_result")
|
||||
private handleNavigationResult(event: FileBrowserEvents["action_navigate_to_result"]) {
|
||||
if (event.status !== "success")
|
||||
@EventHandler<FileBrowserEvents>("notify_current_path")
|
||||
private handleNavigationResult(event: FileBrowserEvents["notify_current_path"]) {
|
||||
if (event.status !== "success") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPath = event.path;
|
||||
this.selection = [];
|
||||
|
@ -983,8 +1311,9 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
|
|||
@EventHandler<FileBrowserEvents>("action_start_create_directory")
|
||||
private handleActionFileCreateBegin(event: FileBrowserEvents["action_start_create_directory"]) {
|
||||
let index = 0;
|
||||
while (this.fileList.find(e => e.name === (event.defaultName + (index > 0 ? " (" + index + ")" : ""))))
|
||||
while (this.fileList.find(e => e.name === (event.defaultName + (index > 0 ? " (" + index + ")" : "")))) {
|
||||
index++;
|
||||
}
|
||||
|
||||
const name = event.defaultName + (index > 0 ? " (" + index + ")" : "");
|
||||
this.fileList.push({
|
||||
|
@ -998,12 +1327,14 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
|
|||
});
|
||||
|
||||
/* fire_async because our children have to render first in order to have the row selected! */
|
||||
this.forceUpdate(() => this.props.events.fire_react("action_select_files", {
|
||||
files: [{
|
||||
name: name,
|
||||
type: FileType.DIRECTORY
|
||||
}], mode: "exclusive"
|
||||
}));
|
||||
this.forceUpdate(() => {
|
||||
this.props.events.fire_react("action_select_files", {
|
||||
files: [{
|
||||
name: name,
|
||||
type: FileType.DIRECTORY
|
||||
}], mode: "exclusive"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler<FileBrowserEvents>("action_create_directory_result")
|
||||
|
@ -1112,8 +1443,9 @@ export class FileBrowser extends ReactComponentBase<FileListTableProperties, Fil
|
|||
@EventHandler<FileBrowserEvents>("notify_transfer_status")
|
||||
private handleTransferStatus(event: FileBrowserEvents["notify_transfer_status"]) {
|
||||
const index = this.fileList.findIndex(e => e.transfer?.id === event.id);
|
||||
if (index === -1)
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let element = this.fileList[index];
|
||||
if (event.status === "errored") {
|
|
@ -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 {useEffect, useRef, useState} from "react";
|
||||
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
|
||||
import {TransferStatus} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {HTMLRenderer} from "tc-shared/ui/react-elements/HTMLRenderer";
|
||||
import {ProgressBar} from "tc-shared/ui/react-elements/ProgressBar";
|
||||
|
@ -11,60 +10,14 @@ import {format_time, network} from "tc-shared/ui/frames/chat";
|
|||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {TransferStatus} from "tc-shared/ui/modal/transfer/FileDefinitions";
|
||||
import {TransferInfoData, TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions";
|
||||
|
||||
const cssStyle = require("./TransferInfo.scss");
|
||||
const cssStyle = require("./FileTransferInfoRenderer.scss");
|
||||
const iconArrow = require("./icon_double_arrow.svg");
|
||||
const iconTransferUpload = require("./icon_transfer_upload.svg");
|
||||
const iconTransferDownload = require("./icon_transfer_download.svg");
|
||||
|
||||
export interface TransferInfoEvents {
|
||||
query_transfers: {},
|
||||
query_transfer_result: {
|
||||
status: "success" | "error" | "timeout";
|
||||
|
||||
error?: string;
|
||||
transfers?: TransferInfoData[],
|
||||
showFinished?: boolean
|
||||
}
|
||||
|
||||
action_toggle_expansion: { visible: boolean },
|
||||
action_toggle_finished_transfers: { visible: boolean },
|
||||
action_remove_finished: {},
|
||||
|
||||
notify_transfer_registered: { transfer: TransferInfoData },
|
||||
notify_transfer_status: {
|
||||
id: number,
|
||||
status: TransferStatus,
|
||||
error?: string
|
||||
},
|
||||
notify_transfer_progress: {
|
||||
id: number;
|
||||
status: TransferStatus,
|
||||
progress: TransferProgress
|
||||
},
|
||||
|
||||
notify_modal_closed: {}
|
||||
}
|
||||
|
||||
export interface TransferInfoData {
|
||||
id: number;
|
||||
|
||||
direction: "upload" | "download";
|
||||
status: TransferStatus;
|
||||
|
||||
name: string;
|
||||
path: string;
|
||||
|
||||
progress: number;
|
||||
error?: string;
|
||||
|
||||
timestampRegistered: number;
|
||||
timestampBegin: number;
|
||||
timestampEnd: number;
|
||||
|
||||
transferredBytes: number;
|
||||
}
|
||||
|
||||
const ExpendState = (props: { extended: boolean, events: Registry<TransferInfoEvents> }) => {
|
||||
const [expended, setExpended] = useState(props.extended);
|
||||
|
||||
|
@ -495,7 +448,7 @@ const ExtendedInfo = (props: { events: Registry<TransferInfoEvents> }) => {
|
|||
</div>;
|
||||
};
|
||||
|
||||
export const TransferInfo = (props: { events: Registry<TransferInfoEvents> }) => (
|
||||
export const FileTransferInfo = (props: { events: Registry<TransferInfoEvents> }) => (
|
||||
<div className={cssStyle.container}>
|
||||
<ExtendedInfo events={props.events}/>
|
||||
<BottomBar events={props.events}/>
|
|
@ -7,14 +7,14 @@ import {
|
|||
TransferProgress,
|
||||
TransferProperties
|
||||
} from "../../../file/Transfer";
|
||||
import {Settings, settings} from "../../../settings";
|
||||
import {
|
||||
avatarsPathPrefix,
|
||||
channelPathPrefix,
|
||||
iconPathPrefix,
|
||||
TransferStatus
|
||||
} from "../../../ui/modal/transfer/ModalFileTransfer";
|
||||
import {Settings, settings} from "../../../settings";
|
||||
import {TransferInfoData, TransferInfoEvents} from "../../../ui/modal/transfer/TransferInfo";
|
||||
} from "tc-shared/ui/modal/transfer/FileDefinitions";
|
||||
import {TransferInfoData, TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions";
|
||||
|
||||
export const initializeTransferInfoController = (connection: ConnectionHandler, events: Registry<TransferInfoEvents>) => {
|
||||
const generateTransferPath = (properties: TransferProperties) => {
|
||||
|
@ -128,10 +128,10 @@ export const initializeTransferInfoController = (connection: ConnectionHandler,
|
|||
events.fire("notify_transfer_registered", {transfer: generateTransferInfo(transfer)});
|
||||
|
||||
const closeListener = () => unregisterEvents();
|
||||
events.on("notify_modal_closed", closeListener);
|
||||
events.on("notify_destroy", closeListener);
|
||||
|
||||
const unregisterEvents = () => {
|
||||
events.off("notify_modal_closed", closeListener);
|
||||
events.off("notify_destroy", closeListener);
|
||||
transfer.events.off("notify_progress", progressListener);
|
||||
};
|
||||
};
|
||||
|
@ -139,7 +139,7 @@ export const initializeTransferInfoController = (connection: ConnectionHandler,
|
|||
|
||||
const registeredListener = event => listenToTransfer(event.transfer);
|
||||
connection.fileManager.events.on("notify_transfer_registered", registeredListener);
|
||||
events.on("notify_modal_closed", () => connection.fileManager.events.off("notify_transfer_registered", registeredListener));
|
||||
events.on("notify_destroy", () => connection.fileManager.events.off("notify_transfer_registered", registeredListener));
|
||||
|
||||
connection.fileManager.registeredTransfers().forEach(transfer => listenToTransfer(transfer));
|
||||
}
|
|
@ -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 * as React from "react";
|
||||
import {FileType} from "tc-shared/file/FileManager";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {FileBrowser, NavigationBar} from "tc-shared/ui/modal/transfer/FileBrowser";
|
||||
import {TransferInfo, TransferInfoEvents} from "tc-shared/ui/modal/transfer/TransferInfo";
|
||||
import {initializeRemoteFileBrowserController} from "tc-shared/ui/modal/transfer/RemoteFileBrowserController";
|
||||
import {ChannelEntry} from "tc-shared/tree/Channel";
|
||||
import {initializeTransferInfoController} from "tc-shared/ui/modal/transfer/TransferInfoController";
|
||||
import {FileBrowserRenderer, NavigationBar} from "./FileBrowserRenderer";
|
||||
import {FileTransferInfo} from "./FileTransferInfo";
|
||||
import {initializeRemoteFileBrowserController} from "./FileBrowserControllerRemote";
|
||||
import {initializeTransferInfoController} from "./FileTransferInfoController";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||
import {server_connections} from "tc-shared/ConnectionManager";
|
||||
import {channelPathPrefix, FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions";
|
||||
import {TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions";
|
||||
|
||||
const cssStyle = require("./ModalFileTransfer.scss");
|
||||
export const channelPathPrefix = tr("Channel") + " ";
|
||||
export const iconPathPrefix = tr("Icons");
|
||||
export const avatarsPathPrefix = tr("Avatars");
|
||||
export const FileTransferUrlMediaType = "application/x-teaspeak-ft-urls";
|
||||
|
||||
export type TransferStatus = "pending" | "transferring" | "finished" | "errored" | "none";
|
||||
export type FileMode = "password" | "empty" | "create" | "creating" | "normal" | "uploading";
|
||||
|
||||
export type ListedFileInfo = {
|
||||
path: string;
|
||||
name: string;
|
||||
type: FileType;
|
||||
|
||||
datetime: number;
|
||||
size: number;
|
||||
|
||||
virtual: boolean;
|
||||
mode: FileMode;
|
||||
|
||||
transfer?: {
|
||||
id: number;
|
||||
direction: "upload" | "download";
|
||||
status: TransferStatus;
|
||||
percent: number;
|
||||
} | undefined
|
||||
};
|
||||
|
||||
export type PathInfo = {
|
||||
channelId: number;
|
||||
channel: ChannelEntry;
|
||||
|
||||
path: string;
|
||||
type: "icon" | "avatar" | "channel" | "root";
|
||||
}
|
||||
|
||||
export interface FileBrowserEvents {
|
||||
query_files: { path: string },
|
||||
query_files_result: {
|
||||
path: string,
|
||||
status: "success" | "timeout" | "error" | "no-permissions" | "invalid-password",
|
||||
|
||||
error?: string,
|
||||
files?: ListedFileInfo[]
|
||||
},
|
||||
|
||||
action_navigate_to: {
|
||||
path: string
|
||||
},
|
||||
action_navigate_to_result: {
|
||||
path: string,
|
||||
status: "success" | "timeout" | "error";
|
||||
error?: string;
|
||||
pathInfo?: PathInfo
|
||||
}
|
||||
|
||||
action_delete_file: {
|
||||
files: {
|
||||
path: string,
|
||||
name: string
|
||||
}[] | "selection";
|
||||
mode: "force" | "ask";
|
||||
},
|
||||
action_delete_file_result: {
|
||||
results: {
|
||||
path: string,
|
||||
name: string,
|
||||
status: "success" | "timeout" | "error";
|
||||
error?: string;
|
||||
}[],
|
||||
},
|
||||
|
||||
action_start_create_directory: {
|
||||
defaultName: string
|
||||
},
|
||||
action_create_directory: {
|
||||
path: string,
|
||||
name: string
|
||||
},
|
||||
action_create_directory_result: {
|
||||
path: string,
|
||||
name: string,
|
||||
status: "success" | "timeout" | "error";
|
||||
|
||||
error?: string;
|
||||
},
|
||||
|
||||
action_rename_file: {
|
||||
oldPath: string,
|
||||
oldName: string,
|
||||
|
||||
newPath: string;
|
||||
newName: string
|
||||
},
|
||||
action_rename_file_result: {
|
||||
oldPath: string,
|
||||
oldName: string,
|
||||
status: "success" | "timeout" | "error" | "no-changes";
|
||||
|
||||
newPath?: string,
|
||||
newName?: string,
|
||||
error?: string;
|
||||
},
|
||||
|
||||
action_start_rename: {
|
||||
path: string;
|
||||
name: string;
|
||||
},
|
||||
|
||||
action_select_files: {
|
||||
files: {
|
||||
name: string,
|
||||
type: FileType
|
||||
}[]
|
||||
mode: "exclusive" | "toggle"
|
||||
},
|
||||
action_selection_context_menu: {
|
||||
pageX: number,
|
||||
pageY: number
|
||||
},
|
||||
|
||||
action_start_download: {
|
||||
files: {
|
||||
path: string,
|
||||
name: string
|
||||
}[]
|
||||
},
|
||||
action_start_upload: {
|
||||
path: string;
|
||||
mode: "files" | "browse";
|
||||
|
||||
files?: File[];
|
||||
},
|
||||
|
||||
notify_transfer_start: {
|
||||
path: string;
|
||||
name: string;
|
||||
|
||||
id: number;
|
||||
mode: "upload" | "download";
|
||||
},
|
||||
|
||||
notify_transfer_status: {
|
||||
id: number;
|
||||
status: TransferStatus;
|
||||
fileSize?: number;
|
||||
},
|
||||
notify_transfer_progress: {
|
||||
id: number;
|
||||
progress: number;
|
||||
fileSize: number;
|
||||
status: TransferStatus
|
||||
}
|
||||
|
||||
|
||||
notify_modal_closed: {},
|
||||
notify_drag_ended: {},
|
||||
|
||||
/* Attention: Only use in sync mode! */
|
||||
notify_drag_started: {
|
||||
event: DragEvent
|
||||
}
|
||||
}
|
||||
|
||||
const cssStyle = require("./FileBrowserRenderer.scss");
|
||||
|
||||
class FileTransferModal extends InternalModal {
|
||||
readonly remoteBrowseEvents = new Registry<FileBrowserEvents>();
|
||||
|
@ -196,12 +33,12 @@ class FileTransferModal extends InternalModal {
|
|||
|
||||
protected onInitialize() {
|
||||
const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/";
|
||||
this.remoteBrowseEvents.fire("action_navigate_to", {path: path});
|
||||
this.remoteBrowseEvents.fire("action_navigate_to", { path: path });
|
||||
}
|
||||
|
||||
protected onDestroy() {
|
||||
this.remoteBrowseEvents.fire("notify_modal_closed");
|
||||
this.transferInfoEvents.fire("notify_modal_closed");
|
||||
this.remoteBrowseEvents.fire("notify_destroy");
|
||||
this.transferInfoEvents.fire("notify_destroy");
|
||||
}
|
||||
|
||||
title() {
|
||||
|
@ -210,11 +47,13 @@ class FileTransferModal extends InternalModal {
|
|||
|
||||
renderBody() {
|
||||
const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/";
|
||||
return <div className={cssStyle.container}>
|
||||
<NavigationBar events={this.remoteBrowseEvents} currentPath={path}/>
|
||||
<FileBrowser events={this.remoteBrowseEvents} currentPath={path}/>
|
||||
<TransferInfo events={this.transferInfoEvents}/>
|
||||
</div>
|
||||
return (
|
||||
<div className={cssStyle.container}>
|
||||
<NavigationBar events={this.remoteBrowseEvents} initialPath={path} />
|
||||
<FileBrowserRenderer events={this.remoteBrowseEvents} initialPath={path} />
|
||||
<FileTransferInfo events={this.transferInfoEvents} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
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";
|
||||
|
||||
onFocus?: () => void;
|
||||
onFocus?: (event: React.FocusEvent | React.MouseEvent) => void;
|
||||
onBlur?: () => void;
|
||||
|
||||
onChange?: (newValue: string) => void;
|
||||
|
|
|
@ -137,8 +137,9 @@ export class Table extends React.Component<TableProperties, TableState> {
|
|||
return rowRenderer(row, columns, "tr-" + row.__rowIndex);
|
||||
});
|
||||
|
||||
if(this.props.bodyOverlay)
|
||||
if(this.props.bodyOverlay) {
|
||||
body.push(this.props.bodyOverlay());
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
Loading…
Reference in New Issue